Recent updates sheet (Feature 21) #176
@@ -84,6 +84,12 @@ function homeReads(fetchMock: Mock<typeof fetch>) {
|
|||||||
.length
|
.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.
|
// Where shows the route a row went to.
|
||||||
function Where() {
|
function Where() {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -201,6 +207,56 @@ describe('Recent updates sheet', () => {
|
|||||||
expect(screen.queryByTestId('unread-dot')).not.toBeInTheDocument()
|
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([
|
it.each([
|
||||||
[0, '/plan'],
|
[0, '/plan'],
|
||||||
[1, '/discover'],
|
[1, '/discover'],
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ function Updates({ onOpen }: { onOpen: (url: string) => void }) {
|
|||||||
const updates = useUpdates()
|
const updates = useUpdates()
|
||||||
const markSeen = useMarkUpdatesSeen()
|
const markSeen = useMarkUpdatesSeen()
|
||||||
const { mutate } = markSeen
|
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
|
// The instant the relative times count from, fixed while the sheet is
|
||||||
// open.
|
// open.
|
||||||
const [now] = useState(() => new Date())
|
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>
|
<ErrorBanner onRetry={() => void updates.refetch()}>Couldn't load your updates. {updates.error.message}</ErrorBanner>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (updates.isPending) {
|
if (!fresh) {
|
||||||
return (
|
return (
|
||||||
<div aria-busy="true" aria-label="Updates loading" className={styles.rows}>
|
<div aria-busy="true" aria-label="Updates loading" className={styles.rows}>
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
{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
|
// useUpdates is the Recent updates (GET /api/v1/updates): the last ten
|
||||||
// notification events, newest first. The list is dropped as soon as the
|
// notification events, newest first. Each opening of the sheet reads it
|
||||||
// sheet showing it closes (gcTime 0), so each opening reads it afresh and
|
// afresh (refetchOnMount 'always'), whatever the query client still holds;
|
||||||
// never shows, or marks seen, a list read before an event stored since.
|
// the sheet shows, and marks seen, only the list that opening read.
|
||||||
export function useUpdates() {
|
export function useUpdates() {
|
||||||
const { apiFetch } = useApi()
|
const { apiFetch } = useApi()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: UPDATES_KEY,
|
queryKey: UPDATES_KEY,
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
parseUpdates(`GET ${UPDATES_PATH}`, await apiFetch<unknown>(UPDATES_PATH), window.location.origin),
|
parseUpdates(`GET ${UPDATES_PATH}`, await apiFetch<unknown>(UPDATES_PATH), window.location.origin),
|
||||||
gcTime: 0,
|
refetchOnMount: 'always',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user