// The rule that decides whether a placeholder is ever seen: a load that lands // from the extension's own cache must not draw one, and a load that goes to // the network must. Everything here is timing, so it is tested on a fake clock // rather than through a popup. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { LoadTracker, SHOW_AFTER_MS, showsPlaceholder } from './loading' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) /** A promise this test settles by hand, standing in for a card's `load`. */ function deferred() { let settle!: (err?: Error) => void const work = new Promise((resolve, reject) => { settle = (err) => (err ? reject(err) : resolve()) }) return { work, settle } } /** Let the timers run to `ms`, and the promise callbacks behind them too. */ const tick = (ms: number) => vi.advanceTimersByTimeAsync(ms) describe('showsPlaceholder', () => { it('shows nothing for the first SHOW_AFTER_MS', () => { expect(showsPlaceholder({ startedAt: 0 }, SHOW_AFTER_MS - 1)).toBe(false) }) it('shows once a load has run past the delay', () => { expect(showsPlaceholder({ startedAt: 0 }, SHOW_AFTER_MS)).toBe(true) }) it('shows nothing for a load that has ended, however long it took', () => { expect(showsPlaceholder({ startedAt: 0, endedAt: 9_000 }, 9_001)).toBe(false) }) }) describe('LoadTracker', () => { it('never draws for a load that lands from cache', async () => { const redraw = vi.fn() const tracker = new LoadTracker(redraw) const { work, settle } = deferred() tracker.track('owner', work) await tick(20) settle() await tick(0) expect(tracker.busy('owner')).toBe(false) expect(tracker.anyPending()).toBe(false) // The redraw scheduled for the delay has to have been called off, or the // popup redraws for a load that finished long before. await tick(SHOW_AFTER_MS * 2) expect(redraw).not.toHaveBeenCalled() }) it('draws once a load crosses the delay, and again when it lands', async () => { const redraw = vi.fn() const tracker = new LoadTracker(redraw) const { work, settle } = deferred() tracker.track('owner', work) await tick(SHOW_AFTER_MS - 1) expect(tracker.busy('owner')).toBe(false) expect(redraw).not.toHaveBeenCalled() await tick(1) expect(tracker.busy('owner')).toBe(true) expect(redraw).toHaveBeenCalledTimes(1) settle() await tick(0) expect(tracker.busy('owner')).toBe(false) // The card redraws for itself when it has an answer, but a load can end // with nothing to draw; the placeholder still has to come down. expect(redraw).toHaveBeenCalledTimes(2) }) it('takes the placeholder down when a load fails', async () => { const redraw = vi.fn() const tracker = new LoadTracker(redraw) const { work, settle } = deferred() tracker.track('subscribers', work) await tick(SHOW_AFTER_MS) expect(tracker.busy('subscribers')).toBe(true) settle(new Error('constellation said no')) await tick(0) expect(tracker.busy('subscribers')).toBe(false) }) it('keeps one timer for loads that started at different times', async () => { const redraw = vi.fn() const tracker = new LoadTracker(redraw) const first = deferred() const second = deferred() tracker.track('owner', first.work) await tick(100) // A second load starting must not push back the first one's placeholder. tracker.track('subscribers', second.work) await tick(50) expect(tracker.busy('owner')).toBe(true) expect(tracker.busy('subscribers')).toBe(false) await tick(100) expect(tracker.busy('subscribers')).toBe(true) expect(redraw).toHaveBeenCalledTimes(2) }) it('is never busy for a card that does not load', () => { const tracker = new LoadTracker(vi.fn()) tracker.track('labels', undefined) 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) }) })