Recent updates sheet (Feature 21) #176
@@ -84,6 +84,12 @@ function homeReads(fetchMock: Mock<typeof fetch>) {
|
||||
.length
|
||||
}
|
||||
|
||||
function updatesReads(fetchMock: Mock<typeof fetch>) {
|
||||
return fetchMock.mock.calls.filter(
|
||||
([input, init]) => String(input) === UPDATES_PATH && (init?.method ?? 'GET') === 'GET',
|
||||
).length
|
||||
}
|
||||
|
||||
// Where shows the route a row went to.
|
||||
function Where() {
|
||||
const location = useLocation()
|
||||
@@ -201,6 +207,56 @@ describe('Recent updates sheet', () => {
|
||||
expect(screen.queryByTestId('unread-dot')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('on an immediate reopen waits for that opening’s read, and marks the newer event seen, not the list held from before', async () => {
|
||||
const stored = [...UPDATES.updates]
|
||||
let reads = 0
|
||||
let answer: (() => void) | undefined
|
||||
const fetchMock = stubServer({
|
||||
[`GET ${UPDATES_PATH}`]: () => {
|
||||
const body = { updates: [...stored] }
|
||||
reads++
|
||||
// Every reading after the first is held until the test answers it.
|
||||
if (reads === 1) return jsonResponse(200, body)
|
||||
return new Promise<Response>((resolve) => (answer = () => resolve(jsonResponse(200, body))))
|
||||
},
|
||||
})
|
||||
await renderHome()
|
||||
await rows(await openSheet())
|
||||
await waitFor(() => expect(posts(fetchMock)).toHaveLength(1))
|
||||
|
||||
// Closed and opened again at once, before any cache-collection timer
|
||||
// could run, with a new event stored in between.
|
||||
fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' })
|
||||
const reminder = {
|
||||
type: 'day_of_reminder',
|
||||
title: 'Tacos tonight',
|
||||
body: 'Dinner at 6:00.',
|
||||
url: '/plan',
|
||||
created_at: new Date().toISOString(),
|
||||
}
|
||||
stored.unshift(reminder)
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Recent updates/ }))
|
||||
const sheet = screen.getByRole('dialog', { name: 'Recent updates' })
|
||||
|
||||
// The list held from the first opening is neither shown nor marked
|
||||
// seen while this opening's read is in flight.
|
||||
await waitFor(() => expect(updatesReads(fetchMock)).toBe(2))
|
||||
expect(within(sheet).getByLabelText('Updates loading')).toBeInTheDocument()
|
||||
expect(within(sheet).queryByRole('list')).not.toBeInTheDocument()
|
||||
expect(posts(fetchMock)).toHaveLength(1)
|
||||
|
||||
answer!()
|
||||
const listed = await rows(sheet)
|
||||
expect(listed).toHaveLength(4)
|
||||
expect(listed[0]).toHaveTextContent('Tacos tonight')
|
||||
await waitFor(() =>
|
||||
expect(posts(fetchMock)).toEqual([
|
||||
JSON.stringify({ through: UPDATES.updates[0]!.created_at }),
|
||||
JSON.stringify({ through: reminder.created_at }),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
[0, '/plan'],
|
||||
[1, '/discover'],
|
||||
|
||||
@@ -44,7 +44,10 @@ function Updates({ onOpen }: { onOpen: (url: string) => void }) {
|
||||
const updates = useUpdates()
|
||||
const markSeen = useMarkUpdatesSeen()
|
||||
const { mutate } = markSeen
|
||||
const newest = updates.data?.[0]?.created_at
|
||||
// fresh says this opening's read has come back; until then a list still
|
||||
// held from an earlier opening is neither shown nor marked seen.
|
||||
const fresh = updates.isSuccess && updates.isFetchedAfterMount
|
||||
const newest = fresh ? updates.data[0]?.created_at : undefined
|
||||
// The instant the relative times count from, fixed while the sheet is
|
||||
// open.
|
||||
const [now] = useState(() => new Date())
|
||||
@@ -58,7 +61,7 @@ function Updates({ onOpen }: { onOpen: (url: string) => void }) {
|
||||
<ErrorBanner onRetry={() => void updates.refetch()}>Couldn't load your updates. {updates.error.message}</ErrorBanner>
|
||||
)
|
||||
}
|
||||
if (updates.isPending) {
|
||||
if (!fresh) {
|
||||
return (
|
||||
<div aria-busy="true" aria-label="Updates loading" className={styles.rows}>
|
||||
{Array.from({ length: 3 }, (_, i) => (
|
||||
|
||||
+4
-4
@@ -144,16 +144,16 @@ export function parseUpdates(where: string, body: unknown, origin: string): Upda
|
||||
}
|
||||
|
||||
// useUpdates is the Recent updates (GET /api/v1/updates): the last ten
|
||||
// notification events, newest first. The list is dropped as soon as the
|
||||
// sheet showing it closes (gcTime 0), so each opening reads it afresh and
|
||||
// never shows, or marks seen, a list read before an event stored since.
|
||||
// notification events, newest first. Each opening of the sheet reads it
|
||||
// afresh (refetchOnMount 'always'), whatever the query client still holds;
|
||||
// the sheet shows, and marks seen, only the list that opening read.
|
||||
export function useUpdates() {
|
||||
const { apiFetch } = useApi()
|
||||
return useQuery({
|
||||
queryKey: UPDATES_KEY,
|
||||
queryFn: async () =>
|
||||
parseUpdates(`GET ${UPDATES_PATH}`, await apiFetch<unknown>(UPDATES_PATH), window.location.origin),
|
||||
gcTime: 0,
|
||||
refetchOnMount: 'always',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user