import { describe, expect, it } from 'vitest' import { statusMessagesFor } from './status' import type { PageState, PubInfo } from './types' function pub(verified: boolean, url = 'https://blog.example.com'): PubInfo { return { uri: 'at://did:plc:abc123/site.standard.publication/3kpubrkey', did: 'did:plc:abc123', pds: 'https://pds.example.com', record: { $type: 'site.standard.publication', url, name: 'Example Blog' }, verified, } } function state(overrides: Partial): PageState { return { url: 'https://reader.example.com/p/x', fetchedAt: 0, ...overrides } } const base = { httpPage: true, signedIn: false } describe('statusMessagesFor', () => { it('explains that non-http pages are not checked, and says nothing else', () => { const msgs = statusMessagesFor({ state: undefined, httpPage: false, signedIn: false }) expect(msgs).toHaveLength(1) expect(msgs[0]?.tone).toBe('info') expect(msgs[0]?.text).toMatch(/regular web pages/) }) it('reports a failed background request as an error', () => { const msgs = statusMessagesFor({ ...base, state: undefined, requestFailed: true }) expect(msgs[0]?.tone).toBe('error') expect(msgs[0]?.text).toMatch(/Refresh/) }) 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', () => { const offline = statusMessagesFor({ ...base, state: state({ error: 'offline' }) }) expect(offline[0]?.tone).toBe('error') expect(offline[0]?.text).toMatch(/offline/) const failed = statusMessagesFor({ ...base, state: state({ error: 'fetch-failed' }) }) expect(failed[0]?.tone).toBe('error') expect(failed[0]?.text).toMatch(/records/) expect(failed[0]?.text).not.toMatch(/offline/) }) it('states plainly when no publication was found', () => { const msgs = statusMessagesFor({ ...base, state: state({}) }) expect(msgs).toHaveLength(1) expect(msgs[0]?.tone).toBe('info') expect(msgs[0]?.text).toMatch(/No standard\.site publication/) }) it('names the publication and links its real home when the site is not the verified publisher', () => { const msgs = statusMessagesFor({ ...base, signedIn: true, state: state({ pub: pub(false), subscriptionRkey: null }), }) expect(msgs).toHaveLength(1) expect(msgs[0]?.tone).toBe('warn') expect(msgs[0]?.text).toMatch(/isn’t the verified publisher for “Example Blog”/) expect(msgs[0]?.link).toEqual({ text: 'blog.example.com', href: 'https://blog.example.com' }) }) it('omits the home host and link when the record url is unparseable', () => { const msgs = statusMessagesFor({ ...base, signedIn: true, state: state({ pub: pub(false, 'not a url'), subscriptionRkey: null }), }) expect(msgs[0]?.tone).toBe('warn') expect(msgs[0]?.link).toBeUndefined() }) it('names the blocked account as an alert, and links its profile', () => { const msgs = statusMessagesFor({ ...base, signedIn: true, state: state({ pub: { ...pub(true), handle: 'blocked.example.com' }, blocked: true, subscriptionRkey: null, }), }) expect(msgs).toHaveLength(1) expect(msgs[0]?.tone).toBe('alert') expect(msgs[0]?.text).toMatch(/You have blocked @blocked\.example\.com/) expect(msgs[0]?.link).toEqual({ text: '@blocked.example.com', href: 'https://bsky.app/profile/blocked.example.com', }) }) it('falls back to the did when the publication has no handle', () => { const msgs = statusMessagesFor({ ...base, signedIn: true, state: state({ pub: pub(true), blocked: true, subscriptionRkey: null }), }) expect(msgs[0]?.text).toMatch(/You have blocked did:plc:abc123/) expect(msgs[0]?.link?.href).toBe('https://bsky.app/profile/did:plc:abc123') }) it('says nothing about blocks when the block list is unknown or the account is not blocked', () => { for (const blocked of [undefined, false]) { const msgs = statusMessagesFor({ ...base, signedIn: true, state: state({ pub: pub(true), blocked, subscriptionRkey: null }), }) expect(msgs).toEqual([]) } }) it('ranks the block above the unverified-publisher warning', () => { const msgs = statusMessagesFor({ ...base, signedIn: true, state: state({ pub: pub(false), blocked: true, subscriptionRkey: null }), }) // Not just insertion order: the block outranks it by tone as well. expect(msgs.map((m) => m.tone)).toEqual(['alert', 'warn']) expect(msgs[0]?.text).toMatch(/You have blocked/) expect(msgs[1]?.text).toMatch(/verified publisher/) }) it('warns when signed in but the subscription list could not be fetched', () => { const msgs = statusMessagesFor({ ...base, signedIn: true, state: state({ pub: pub(true), subscriptionRkey: undefined }), }) expect(msgs).toHaveLength(1) expect(msgs[0]?.tone).toBe('warn') expect(msgs[0]?.text).toMatch(/subscriptions/) }) it('stacks every applicable message, most severe first', () => { // Expired session (info, pushed first) + failed worker request (error): // the error must still come out on top. const msgs = statusMessagesFor({ ...base, state: undefined, requestFailed: true, sessionExpired: true, }) expect(msgs.map((m) => m.tone)).toEqual(['error', 'info']) // Unverified publisher (warn) + unloadable subscription list (warn) keep // their insertion order within the tone. const warns = statusMessagesFor({ ...base, signedIn: true, state: state({ pub: pub(false), subscriptionRkey: undefined }), }) expect(warns.map((m) => m.tone)).toEqual(['warn', 'warn']) expect(warns[0]?.text).toMatch(/verified publisher/) expect(warns[1]?.text).toMatch(/subscriptions/) // A block (alert) sits between the two: above every warning, below a // failure of the extension itself. const ranked = statusMessagesFor({ ...base, signedIn: true, sessionExpired: true, state: state({ pub: pub(false), blocked: true, subscriptionRkey: null }), }) expect(ranked.map((m) => m.tone)).toEqual(['alert', 'warn', 'info']) }) it('stays quiet for a signed-out visitor until the Subscribe button is clicked', () => { expect(statusMessagesFor({ ...base, state: state({ pub: pub(true) }) })).toEqual([]) }) it('answers a signed-out Subscribe click with the Atmosphere sign-in pill', () => { const msgs = statusMessagesFor({ ...base, signinNudge: true, state: state({ pub: pub(true) }) }) expect(msgs).toHaveLength(1) expect(msgs[0]?.tone).toBe('warn') expect(msgs[0]?.text).toMatch(/Atmosphere account/) expect(msgs[0]?.link?.href).toMatch(/atmosphereaccount\.com/) }) it('ignores the nudge once signed in', () => { const msgs = statusMessagesFor({ ...base, signedIn: true, signinNudge: true, state: state({ pub: pub(true), subscriptionRkey: null }), }) expect(msgs).toEqual([]) }) it('shows the nudge alongside the expired-session notice, ranked above it', () => { const msgs = statusMessagesFor({ ...base, sessionExpired: true, signinNudge: true, state: state({ pub: pub(true) }), }) expect(msgs.map((m) => m.tone)).toEqual(['warn', 'info']) expect(msgs[0]?.text).toMatch(/Atmosphere account/) expect(msgs[1]?.text).toMatch(/session expired/i) }) it('stays quiet for a verified publication with known subscription state', () => { for (const rkey of ['3ksub', null] as const) { expect( statusMessagesFor({ ...base, signedIn: true, state: state({ pub: pub(true), subscriptionRkey: rkey }), }), ).toEqual([]) } }) })