Skip to content

Commit 7ebfd82

Browse files
committed
fix(hub-ui): dismiss toasts when messages are removed
1 parent 5f6d5be commit 7ebfd82

2 files changed

Lines changed: 66 additions & 7 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { DevframeMessageEntry, DevframeMessagesListDelta } from '@devframes/hub'
2+
import type { DocksContext } from '@devframes/hub/client'
3+
import { afterEach, expect, it, vi } from 'vitest'
4+
5+
afterEach(async () => {
6+
const { dismissToast, useToasts } = await import('./toasts')
7+
for (const toast of [...useToasts()]) dismissToast(toast.id)
8+
vi.restoreAllMocks()
9+
vi.resetModules()
10+
vi.useRealTimers()
11+
})
12+
13+
async function fixture() {
14+
const { useMessages } = await import('./messages')
15+
const { useToasts } = await import('./toasts')
16+
let delta: DevframeMessagesListDelta = { entries: [], removedIds: [], version: 0, full: true }
17+
let refresh = () => {}
18+
const call = vi.fn(async () => delta)
19+
// eslint-disable-next-line slop/no-chained-type-assertions -- Only the RPC feed participates in this state test.
20+
const state = useMessages({ rpc: {
21+
call,
22+
isTrusted: true,
23+
ensureTrusted: async () => {},
24+
client: { register: (command: { handler: () => void }) => { refresh = command.handler } },
25+
} } as unknown as DocksContext)
26+
await vi.waitUntil(() => call.mock.calls.length === 1)
27+
await Promise.resolve()
28+
async function publish(next: DevframeMessagesListDelta) {
29+
delta = next
30+
refresh()
31+
await Promise.resolve()
32+
}
33+
const message: DevframeMessageEntry = { id: 'pending', message: 'Pending approval', level: 'info', from: 'browser', timestamp: 0, notify: true, autoDismiss: 60_000 }
34+
await publish({ entries: [message], removedIds: [], version: 1, full: false })
35+
return { state, toasts: useToasts(), publish, message }
36+
}
37+
38+
it.each([false, true])('removes visible toasts and timers when the message is removed (full=%s)', async (full) => {
39+
expect.assertions(5)
40+
const { state, toasts, publish } = await fixture()
41+
const clearTimer = vi.spyOn(globalThis, 'clearTimeout')
42+
expect(state.entries).toHaveLength(1)
43+
expect(toasts).toHaveLength(1)
44+
await publish({ entries: [], removedIds: full ? [] : ['pending'], version: 2, full })
45+
expect(state.entries).toHaveLength(0)
46+
expect(toasts).toHaveLength(0)
47+
expect(clearTimer).toHaveBeenCalledOnce()
48+
})
49+
50+
it('keeps surviving toasts and unread counts when receiving a full snapshot', async () => {
51+
expect.assertions(3)
52+
const { state, toasts, publish, message } = await fixture()
53+
await publish({ entries: [message], removedIds: [], version: 2, full: true })
54+
expect(state.entries).toHaveLength(1)
55+
expect(toasts).toHaveLength(1)
56+
expect(state.unreadCount).toBe(1)
57+
})

packages/hub-ui/src/client/state/messages.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { DevframeMessageEntry, DevframeMessagesListDelta, DevframeRpcClient
22
import type { DocksContext } from '@devframes/hub/client'
33
import type { Reactive } from 'vue'
44
import { reactive } from 'vue'
5-
import { addToast } from './toasts'
5+
import { addToast, dismissToast } from './toasts'
66

77
export interface MessagesState {
88
entries: DevframeMessageEntry[]
@@ -44,13 +44,15 @@ export function useMessages(context: DocksContext): Reactive<MessagesState> {
4444
) as DevframeMessagesListDelta
4545
let newCount = 0
4646

47-
// A full snapshot resets any locally cached list before applying it.
48-
if (result.full)
49-
entryMap.clear()
50-
51-
// Apply removals
52-
for (const id of result.removedIds)
47+
/** Preserve surviving entries so a full snapshot does not notify them again. */
48+
const retainedIds = new Set(result.entries.map(entry => entry.id))
49+
const removedIds = result.full
50+
? [...entryMap.keys()].filter(id => !retainedIds.has(id))
51+
: result.removedIds
52+
for (const id of removedIds) {
5353
entryMap.delete(id)
54+
dismissToast(id)
55+
}
5456

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

0 commit comments

Comments
 (0)