Recent updates sheet (Feature 21) #176

3 changed files with 65 additions and 6 deletions
Showing only changes of commit 6956f6f049 - Show all commits
+56
View File
@@ -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'],
+5 -2
View File
@@ -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
View File
@@ -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',
})
}