archy/neode-ui/src/composables/__tests__/useMessageToast.test.ts

179 lines
5.6 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
const mockPush = vi.fn()
vi.mock('vue-router', () => ({
useRouter: () => ({ push: mockPush }),
}))
vi.mock('@/api/rpc-client', () => ({
rpcClient: {
getReceivedMessages: vi.fn(),
},
}))
import { useMessageToast } from '../useMessageToast'
import { rpcClient } from '@/api/rpc-client'
const mockedRpc = vi.mocked(rpcClient)
describe('useMessageToast', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
// Reset shared singleton state
const toast = useMessageToast()
toast.stopPolling()
toast.receivedMessages.value = []
toast.lastMessageCount.value = 0
toast.loadingMessages.value = false
toast.toastMessage.value = { show: false, text: '' }
})
afterEach(() => {
const toast = useMessageToast()
toast.stopPolling()
vi.useRealTimers()
})
it('starts with empty state', () => {
const toast = useMessageToast()
expect(toast.receivedMessages.value).toEqual([])
expect(toast.lastMessageCount.value).toBe(0)
expect(toast.loadingMessages.value).toBe(false)
expect(toast.toastMessage.value.show).toBe(false)
expect(toast.unreadCount.value).toBe(0)
})
it('loadReceivedMessages fetches and stores messages', async () => {
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [
{ from_pubkey: 'abc', message: 'Hello', timestamp: '2026-01-01' },
],
})
const toast = useMessageToast()
await toast.loadReceivedMessages()
expect(toast.receivedMessages.value.length).toBe(1)
expect(toast.lastMessageCount.value).toBe(1)
expect(toast.loadingMessages.value).toBe(false)
})
it('does not show toast on initial load', async () => {
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [{ from_pubkey: 'a', message: 'Hi', timestamp: '2026-01-01' }],
})
const toast = useMessageToast()
await toast.loadReceivedMessages()
expect(toast.toastMessage.value.show).toBe(false)
})
it('shows toast when new messages arrive after initial load', async () => {
const toast = useMessageToast()
// Initial load
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [{ from_pubkey: 'a', message: 'First', timestamp: '2026-01-01' }],
})
await toast.loadReceivedMessages()
// New message arrives
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [
{ from_pubkey: 'a', message: 'First', timestamp: '2026-01-01' },
{ from_pubkey: 'b', message: 'Second', timestamp: '2026-01-02' },
],
})
await toast.loadReceivedMessages()
expect(toast.toastMessage.value.show).toBe(true)
expect(toast.toastMessage.value.text).toBe('Second')
})
it('shows count for multiple new messages', async () => {
const toast = useMessageToast()
// Initial load
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [{ from_pubkey: 'a', message: 'One', timestamp: '2026-01-01' }],
})
await toast.loadReceivedMessages()
// Multiple new messages
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [
{ from_pubkey: 'a', message: 'One', timestamp: '2026-01-01' },
{ from_pubkey: 'b', message: 'Two', timestamp: '2026-01-02' },
{ from_pubkey: 'c', message: 'Three', timestamp: '2026-01-03' },
],
})
await toast.loadReceivedMessages()
expect(toast.toastMessage.value.show).toBe(true)
expect(toast.toastMessage.value.text).toBe('2 new messages')
})
it('unreadCount reflects difference', async () => {
const toast = useMessageToast()
toast.receivedMessages.value = [
{ from_pubkey: 'a', message: 'Hi', timestamp: '2026-01-01' },
{ from_pubkey: 'b', message: 'Hey', timestamp: '2026-01-02' },
]
toast.lastMessageCount.value = 1
expect(toast.unreadCount.value).toBe(1)
})
it('unreadCount is never negative', () => {
const toast = useMessageToast()
toast.receivedMessages.value = []
toast.lastMessageCount.value = 5
expect(toast.unreadCount.value).toBe(0)
})
it('markAsRead syncs lastMessageCount', () => {
const toast = useMessageToast()
toast.receivedMessages.value = [
{ from_pubkey: 'a', message: 'Hi', timestamp: '2026-01-01' },
{ from_pubkey: 'b', message: 'Hey', timestamp: '2026-01-02' },
]
toast.lastMessageCount.value = 0
toast.markAsRead()
expect(toast.lastMessageCount.value).toBe(2)
expect(toast.unreadCount.value).toBe(0)
})
it('dismissToastAndOpenMessages clears toast and navigates', () => {
const toast = useMessageToast()
toast.toastMessage.value = { show: true, text: 'New message' }
toast.dismissToastAndOpenMessages()
expect(toast.toastMessage.value.show).toBe(false)
fix: production onboarding, CI tests, container security, keyboard nav Install & Onboarding: - Remove DEV_MODE=true from production ISO service file (auto-created users, skipped password setup) - Auto-install no longer overwrites rootfs service file with bad template - Login.vue always checks auth.isSetup — shows password creation form on fresh install without requiring dev build flag - Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes - First-boot-containers sources image-versions.sh, runs podman as archipelago user (rootless), enables linger + podman.socket - Correct volume ownership (100000:100000 for rootless UID mapping) Container Security: - FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access - FileBrowser: add --read-only, /data volume for database, proper cmd args - First-boot script matches backend config (security hardening + health check) CI Pipeline: - Add vue-tsc type check + vitest run to build-iso.yml (runs every push) - Add post-install-tests.yml workflow (workflow_dispatch, SSH to target) - Build report: set +eo pipefail, fix rootfs path, add || true guards - Bundle run-post-install-tests.sh into ISO E2E Test Suite (scripts/run-post-install-tests.sh): - Phase 1: Install verification (files, services, podman, linger, DEV_MODE check) - Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete) - Phase 3: Container lifecycle (install 3 apps via package.install RPC, verify running, stop, verify stopped, restart, verify running, health) - Phase 4: Log verification (first-boot log, diagnostics, journal errors) - Correct package.install params: {"id", "dockerImage"} Frontend: - Fix backdrop-filter tab-switch bug (keep animations paused during rebuild) - Dashboard glitch animations paused during tab-hidden - Gamepad nav: auto-focus first container on route change - Tab roving: Left/Right on role="tab" cycles and activates sibling tabs - ContainerApps: data-controller-launch on running app cards - 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
expect(mockPush).toHaveBeenCalledWith('/dashboard/mesh')
})
it('stops polling on 401 error', async () => {
const toast = useMessageToast()
mockedRpc.getReceivedMessages.mockRejectedValue(new Error('401 Unauthorized'))
toast.startPolling()
// Wait for initial load triggered by startPolling
await vi.advanceTimersByTimeAsync(0)
// Polling should have stopped, so advancing time should NOT call again
vi.clearAllMocks()
await vi.advanceTimersByTimeAsync(60000)
expect(mockedRpc.getReceivedMessages).not.toHaveBeenCalled()
})
it('startPolling does not create duplicate timers', () => {
const toast = useMessageToast()
mockedRpc.getReceivedMessages.mockResolvedValue({ messages: [] })
toast.startPolling()
toast.startPolling()
toast.startPolling()
// Should only have one timer — verify by stopping and checking no more calls
toast.stopPolling()
})
})