diff --git a/src/lib/status.test.ts b/src/lib/status.test.ts index 87b9b0b..ae590c0 100644 --- a/src/lib/status.test.ts +++ b/src/lib/status.test.ts @@ -32,8 +32,25 @@ describe('statusMessagesFor', () => { expect(msgs[0]?.text).toMatch(/Refresh/) }) - it('shows checking while no state is known yet', () => { - expect(statusMessagesFor({ ...base, state: undefined })[0]?.text).toMatch(/Checking/) + it('shows checking while no state is known yet, and marks it as not an answer', () => { + const checking = statusMessagesFor({ ...base, state: undefined })[0] + expect(checking?.text).toMatch(/Checking/) + // The popup turns this into aria-busy on the status region; a screenshot + // taken while it is up is a screenshot of detection that has not finished + // (scripts/capture-status-docs.mjs). + expect(checking?.pending).toBe(true) + }) + + it('marks nothing else pending: every other line is an answer', () => { + const answers = [ + statusMessagesFor({ ...base, state: undefined, requestFailed: true }), + statusMessagesFor({ ...base, state: undefined, httpPage: false }), + statusMessagesFor({ ...base, state: state({ error: 'offline' }) }), + statusMessagesFor({ ...base, state: state({}), sessionExpired: true }), + ] + for (const msgs of answers) { + expect(msgs.some((msg) => msg.pending)).toBe(false) + } }) it('distinguishes offline from a record fetch failure', () => { diff --git a/src/lib/status.ts b/src/lib/status.ts index 3ceb9ef..f3f0a0e 100644 --- a/src/lib/status.ts +++ b/src/lib/status.ts @@ -22,6 +22,14 @@ export interface StatusMessage { tone: StatusTone /** A substring of `text` to render as a link pointing at `href`. */ link?: { text: string; href: string } + /** + * This line is not an answer, it is the absence of one: the worker has not + * said anything about this page yet. The popup marks the status region + * `aria-busy` for it, the same way the publication card does while its own + * lookups are out — so "still working" is one question anything outside the + * popup can ask, including the capture (scripts/capture-status-docs.mjs). + */ + pending?: boolean } export interface StatusInput { @@ -80,7 +88,7 @@ export function statusMessagesFor(input: StatusInput): StatusMessage[] { tone: 'error', }) } else if (!state) { - messages.push({ text: 'Checking this page…', tone: 'info' }) + messages.push({ text: 'Checking this page…', tone: 'info', pending: true }) } else { if (state.error === 'offline') { messages.push({ diff --git a/src/popup/cards/loading.test.ts b/src/popup/cards/loading.test.ts index d123e3b..e4c8e4a 100644 --- a/src/popup/cards/loading.test.ts +++ b/src/popup/cards/loading.test.ts @@ -120,4 +120,34 @@ describe('LoadTracker', () => { expect(tracker.busy('labels')).toBe(false) expect(tracker.anyPending()).toBe(false) }) + + // What `aria-busy` is worth to anything reading it from outside — assistive + // technology, and the capture (scripts/capture-status-docs.mjs), which + // refuses to shoot a card that has it. The card sets it from anyPending, so + // a placeholder on screen with the attribute already gone would be a + // screenshot of a half-drawn popup that the capture had been told was done. + it('is pending for as long as anything is drawing a placeholder', async () => { + const tracker = new LoadTracker(vi.fn()) + const fast = deferred() + const slow = deferred() + tracker.track('owner', fast.work) + tracker.track('subscribers', slow.work) + + for (const at of [0, SHOW_AFTER_MS - 1, 1, 500]) { + await tick(at) + const showing = ['owner', 'subscribers'].filter((id) => tracker.busy(id)) + if (showing.length > 0) expect(tracker.anyPending()).toBe(true) + } + + fast.settle() + await tick(0) + // The slow one is still drawing, so the card is still busy. + expect(tracker.busy('subscribers')).toBe(true) + expect(tracker.anyPending()).toBe(true) + + slow.settle() + await tick(0) + expect(tracker.busy('subscribers')).toBe(false) + expect(tracker.anyPending()).toBe(false) + }) }) diff --git a/src/popup/popup.ts b/src/popup/popup.ts index 1b82558..23759b9 100644 --- a/src/popup/popup.ts +++ b/src/popup/popup.ts @@ -288,6 +288,11 @@ function renderStatus() { const el = $('status') el.hidden = messages.length === 0 el.replaceChildren(...messages.map(pillFor)) + // "Checking this page…" is the worker not having answered yet, which is the + // same kind of not-finished the publication card reports while its lookups + // are out — said here in the same way, on the region that is showing it. + if (messages.some((msg) => msg.pending)) el.setAttribute('aria-busy', 'true') + else el.removeAttribute('aria-busy') } function pillFor(msg: StatusMessage): HTMLElement {