Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/hub-ui/src/client/state/messages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { DevframeMessageEntry, DevframeMessagesListDelta } from '@devframes/hub'
import type { DocksContext } from '@devframes/hub/client'
import { afterEach, expect, it, vi } from 'vitest'

afterEach(async () => {
const { dismissToast, useToasts } = await import('./toasts')
for (const toast of [...useToasts()]) dismissToast(toast.id)
vi.restoreAllMocks()
vi.resetModules()
vi.useRealTimers()
})

async function fixture() {
const { useMessages } = await import('./messages')
const { useToasts } = await import('./toasts')
let delta: DevframeMessagesListDelta = { entries: [], removedIds: [], version: 0, full: true }
let refresh = () => {}
const call = vi.fn(async () => delta)
// eslint-disable-next-line slop/no-chained-type-assertions -- Only the RPC feed participates in this state test.
const state = useMessages({ rpc: {
call,
isTrusted: true,
ensureTrusted: async () => {},
client: { register: (command: { handler: () => void }) => { refresh = command.handler } },
} } as unknown as DocksContext)
await vi.waitUntil(() => call.mock.calls.length === 1)
await Promise.resolve()
async function publish(next: DevframeMessagesListDelta) {
delta = next
refresh()
await Promise.resolve()
}
const message: DevframeMessageEntry = { id: 'pending', message: 'Pending approval', level: 'info', from: 'browser', timestamp: 0, notify: true, autoDismiss: 60_000 }
await publish({ entries: [message], removedIds: [], version: 1, full: false })
return { state, toasts: useToasts(), publish, message }
}

it.each([false, true])('removes visible toasts and timers when the message is removed (full=%s)', async (full) => {
expect.assertions(5)
const { state, toasts, publish } = await fixture()
const clearTimer = vi.spyOn(globalThis, 'clearTimeout')
expect(state.entries).toHaveLength(1)
expect(toasts).toHaveLength(1)
await publish({ entries: [], removedIds: full ? [] : ['pending'], version: 2, full })
expect(state.entries).toHaveLength(0)
expect(toasts).toHaveLength(0)
expect(clearTimer).toHaveBeenCalledOnce()
})

it('keeps surviving toasts and unread counts when receiving a full snapshot', async () => {
expect.assertions(3)
const { state, toasts, publish, message } = await fixture()
await publish({ entries: [message], removedIds: [], version: 2, full: true })
expect(state.entries).toHaveLength(1)
expect(toasts).toHaveLength(1)
expect(state.unreadCount).toBe(1)
})
17 changes: 10 additions & 7 deletions packages/hub-ui/src/client/state/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { DevframeMessageEntry, DevframeMessagesListDelta, DevframeRpcClient
import type { DocksContext } from '@devframes/hub/client'
import type { Reactive } from 'vue'
import { reactive } from 'vue'
import { addToast } from './toasts'
import { addToast, dismissToast } from './toasts'

export interface MessagesState {
entries: DevframeMessageEntry[]
Expand Down Expand Up @@ -44,13 +44,16 @@ export function useMessages(context: DocksContext): Reactive<MessagesState> {
) as DevframeMessagesListDelta
let newCount = 0

// A full snapshot resets any locally cached list before applying it.
if (result.full)
entryMap.clear()

// Apply removals
for (const id of result.removedIds)
/** Preserve surviving entries so a full snapshot does not notify them again. */
let removedIds = result.removedIds
if (result.full) {
const retainedIds = new Set(result.entries.map(entry => entry.id))
removedIds = [...entryMap.keys()].filter(id => !retainedIds.has(id))
}
for (const id of removedIds) {
entryMap.delete(id)
dismissToast(id)
}

// Apply new/updated entries. On initial fetch (page refresh) only entries
// still loading toast; afterwards any notifying new or changed entry does.
Expand Down
Loading