Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
5.4 kB · 137 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138// The point of the two-stage load: the count is on screen before the follow// set arrives, because on a cold cache that set is a walk of the viewer's whole// follow list and no row may be held open for it (see src/background.ts).//// `load` never touches the DOM — it redraws by calling back into the host — so// this exercises the real card without a document.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'import type { PageState, SessionInfo } from '../../lib/types'import type { CardHost } from './card'
const scanSubscribers = vi.fn()const followedSubscribers = vi.fn()const viewerFollows = vi.fn()const peek = vi.fn()const put = vi.fn()
vi.mock('../../lib/subscribers', () => ({ SCAN_CAP: 500, FOLLOWS_CAP: 10_000, scanSubscribers: (...a: unknown[]) => scanSubscribers(...a), followedSubscribers: (...a: unknown[]) => followedSubscribers(...a),}))vi.mock('./follows', () => ({ viewerFollows: (...a: unknown[]) => viewerFollows(...a), resetFollows: () => {},}))vi.mock('../../lib/cache', () => ({ peek: (...a: unknown[]) => peek(...a), put: (...a: unknown[]) => put(...a),}))vi.mock('../dom', () => ({ $: () => ({}) }))vi.mock('../../lib/atproto', () => ({ bskyProfileUrl: (h: string) => `https://bsky.app/${h}` }))
const { facesCaveat, subscribersCard } = await import('./subscribers')
const PUB = 'at://did:plc:pub00000000000000000000/site.standard.publication/self'const ME = 'did:plc:viewer000000000000000000'const FRIEND = 'did:plc:friend000000000000000000'
/** What the host was told to draw, in the order it was told. */let draws: number[] = []
function hostFor(did?: string): CardHost { return { state: () => ({ url: 'https://pub.example', fetchedAt: 0, pub: { uri: PUB } }) as PageState, session: () => (did ? ({ did } as SessionInfo) : undefined), rerender: () => draws.push(draws.length), loading: () => false, }}
beforeEach(() => { draws = [] subscribersCard.reset?.() for (const m of [scanSubscribers, followedSubscribers, viewerFollows, peek, put]) m.mockReset() peek.mockResolvedValue(undefined) put.mockResolvedValue(undefined) scanSubscribers.mockResolvedValue({ total: 12, dids: [FRIEND], truncated: false }) followedSubscribers.mockResolvedValue({ followed: [{ did: FRIEND }], followedTotal: 1 })})
afterEach(() => { vi.unstubAllGlobals()})
describe('the subscribers card', () => { it('draws the count before the follow set lands, and the faces after', async () => { let release!: (v: unknown) => void viewerFollows.mockReturnValue(new Promise((r) => (release = r)))
const load = subscribersCard.load?.(hostFor(ME), false) // Let the scan settle without settling the follow set. await vi.waitFor(() => expect(draws).toHaveLength(1)) expect(followedSubscribers).not.toHaveBeenCalled()
release({ did: ME, following: new Set([FRIEND]) }) await load expect(draws).toHaveLength(2) expect(followedSubscribers).toHaveBeenCalledOnce() })
it('does not ask the index for DIDs it has nobody to match them against', async () => { await subscribersCard.load?.(hostFor(undefined), false) expect(scanSubscribers).toHaveBeenCalledWith(PUB, undefined, { faces: false }) expect(viewerFollows).not.toHaveBeenCalled() // The count is the whole answer for a signed-out popup, so it is kept. expect(put).toHaveBeenCalledOnce() })
it('keeps the count, uncached, when the follow set never arrives', async () => { viewerFollows.mockResolvedValue(undefined) await subscribersCard.load?.(hostFor(ME), false) expect(draws).toHaveLength(1) // A half-answer is not one to remember for a day. expect(put).not.toHaveBeenCalled() })
it('does not file one account’s faces under another’s key', async () => { // Switched account while the walk was out: the answer is not this key's. viewerFollows.mockResolvedValue({ did: 'did:plc:someoneelse0000000000000', following: new Set() }) await subscribersCard.load?.(hostFor(ME), false) expect(put).not.toHaveBeenCalled() expect(followedSubscribers).not.toHaveBeenCalled() })
it('carries the follow set’s own cap into the summary', async () => { viewerFollows.mockResolvedValue({ did: ME, following: new Set([FRIEND]), truncated: true }) await subscribersCard.load?.(hostFor(ME), false) expect(put.mock.calls[0]?.[1]).toMatchObject({ followsTruncated: true }) })
it('serves a stored summary without touching the index', async () => { peek.mockResolvedValue({ total: 9, followed: [], followedTotal: 0, truncated: false }) await subscribersCard.load?.(hostFor(ME), false) expect(scanSubscribers).not.toHaveBeenCalled() expect(draws).toHaveLength(1) })})
describe('facesCaveat', () => { const summary = { total: 9, followed: [], followedTotal: 1, truncated: false }
it('says nothing when neither cap bit', () => { expect(facesCaveat(summary)).toBe('') })
it('names both windows a face can be missing from', () => { const both = facesCaveat({ ...summary, truncated: true, followsTruncated: true }) expect(both).toContain('500 subscribers') expect(both).toContain(`${(10_000).toLocaleString()} accounts you follow`) })
it('reads an entry cached before the follow cap existed as untruncated', () => { expect(facesCaveat({ ...summary, truncated: true })).not.toContain('you follow') })})