// A render of every screen, to the extent one process without a browser can. // // The build already proves the module graph resolves. What this adds is that // each screen renders what it is given: a component reading a field the wire // shape does not have fails here rather than on the page. // // renderToString runs no effects, so a query never fetches. Each screen's // answers are put into the cache under the key that screen reads, and a key // that moves shows up as a screen rendering its skeleton. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderToString } from 'react-dom/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { App } from '#/app.jsx'; import { SessionProvider } from '#/lib/session.jsx'; const ALICE = 'did:plc:alice'; const BOB = 'did:plc:bob'; const CAROL = 'did:plc:carol'; const SPACE = `at://${ALICE}/space/example.bsky.forum/general`; const RKEY = '3mtjzqmmgxsrb'; const COLLECTION = 'com.example.post'; /** @type {any} */ const session = { did: ALICE, handle: 'alice.example.com', pds: 'https://alice.example', scope: 'atproto space:*?authority=*&skey=*&collection=*&action=create', }; const VALUE = { $type: COLLECTION, text: 'a post in a space', tags: ['one', 'two'], createdAt: '2026-08-20T00:00:00.000Z', image: { $type: 'blob', ref: { $link: 'bafkreiblob' }, mimeType: 'image/png', size: 70, }, }; /** @param {QueryClient} client */ function prime(client) { client.setQueryData(['spaces', ALICE], { spaces: [ { uri: SPACE, isOwner: true }, { uri: `at://${BOB}/space/example.bsky.forum/theirs`, isOwner: false }, ], }); client.setQueryData(['space-config', SPACE], { value: { uri: SPACE, policy: { $type: 'com.atproto.memberspace.policy' }, }, source: { method: 'com.atproto.space.getSpace', host: 'https://alice.example', auth: 'session', }, }); client.setQueryData(['commit', SPACE, null], { rev: RKEY, data: { $bytes: 'AAECqv8' }, }); client.setQueryData(['index', SPACE, null], { records: [{ collection: COLLECTION, rkey: RKEY, cid: 'bafyrecord' }], complete: true, }); client.setQueryData(['repos', SPACE], { repos: [ { did: ALICE, rev: RKEY, data: { $bytes: 'AAEC' } }, { did: BOB, rev: RKEY, data: { $bytes: 'AAED' } }, ], }); client.setQueryData(['members', SPACE], { members: [{ did: ALICE }] }); client.setQueryData(['records', SPACE, null, COLLECTION], { records: [ { collection: COLLECTION, rkey: RKEY, cid: 'bafyrecord', value: VALUE }, ], }); client.setQueryData(['record', SPACE, null, COLLECTION, RKEY], { uri: `${SPACE}/${ALICE}/${COLLECTION}/${RKEY}`, cid: 'bafyrecord', value: VALUE, }); client.setQueryData(['ops', SPACE, null], { ops: [ { rev: RKEY, collection: COLLECTION, rkey: RKEY, cid: 'bafy', prev: null, }, { rev: RKEY, collection: COLLECTION, rkey: 'gone', cid: null, prev: 'x' }, ], }); client.setQueryData(['blobs', SPACE, null], { cids: ['bafkreiblob'] }); client.setQueryData(['identity', ALICE], { did: ALICE, handle: 'alice.example.com', verified: true, }); client.setQueryData(['identity', BOB], { did: BOB, handle: 'bob.example.com', verified: false, }); client.setQueryData(['profile', BOB], { avatar: 'https://bob.example/xrpc/com.atproto.sync.getBlob?did=did:plc:bob&cid=bafkrei', displayName: 'Bob', }); client.setQueryData(['app-profile', 'example.bsky.forum'], { avatar: 'https://app.example/xrpc/com.atproto.sync.getBlob?did=did:plc:app&cid=bafkapp', displayName: 'Forum', }); client.setQueryData(['identity', CAROL], { did: CAROL, handle: 'carol.example.com', verified: true, }); } /** @param {string} href */ function render(href) { const url = new URL(href, 'https://spaces.example.com'); vi.stubGlobal('window', { location: { origin: url.origin, hostname: url.hostname, pathname: url.pathname, search: url.search, }, addEventListener() {}, removeEventListener() {}, history: { pushState() {} }, scrollTo() {}, }); const client = new QueryClient(); prime(client); return renderToString( , ); } beforeEach(() => { vi.stubGlobal('localStorage', { getItem: () => JSON.stringify(session), setItem() {}, removeItem() {}, }); }); describe('the screens', () => { it('lists the spaces the account holds', () => { const html = render('/'); expect(html).toContain('example.bsky.forum'); expect(html).toContain('general'); expect(html).toContain('you host this'); expect(html).toContain('alice.example.com'); }); it('shows a space with its policy, its commit and its collections', () => { const html = render(`/space?uri=${encodeURIComponent(SPACE)}`); expect(html).toContain('Members on a list'); expect(html).toContain('Any client'); // The state hash reads as hex, which is how it compares by eye against // another implementation's output. expect(html).toContain('000102aaff'); expect(html).toContain(COLLECTION); // The collection card links to that collection's records, carrying the // space in the query rather than in the path. expect(html).toContain(`/records?uri=${encodeURIComponent(SPACE)}`); // Members is offered because this account is the space's own authority. expect(html).toContain('>members<'); }); it('renders a record value, blob reference and all', () => { const html = render( `/records?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}`, ); expect(html).toContain('a post in a space'); expect(html).toContain('image/png'); expect(html).toContain('70 B'); expect(html).toContain(RKEY); }); it('renders one record with its URI and CID', () => { const html = render( `/record?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}&rkey=${RKEY}`, ); expect(html).toContain('a post in a space'); expect(html).toContain('bafyrecord'); // A record URI names the authority and the author, so both DIDs read as // handles while the copy stays canonical. expect(html).toContain('alice.example.com'); expect(html).toContain(`title="${SPACE}/${ALICE}/${COLLECTION}/${RKEY}"`); // The scope granted a write, so the editor is offered. expect(html).toContain('Edit'); expect(html).toContain('Delete'); }); it('names each op by what it did', () => { const html = render(`/ops?uri=${encodeURIComponent(SPACE)}`); expect(html).toContain('Write log'); expect(html).toContain('create'); expect(html).toContain('delete'); }); // A page of DIDs says nothing about who anybody is. // Clicking into another member's repo lands on their records with the list // that was clicked no longer on screen, so every screen under a space says // whose repo it is and offers the way back. it("names another account's repo and offers a way back to one's own", () => { const html = render( `/records?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}&repo=${BOB}`, ); expect(html).toContain('bob.example.com'); expect(html).toContain('Your repo'); // The way back drops the repo, and carries no collection: a collection one // member holds is not one another does. expect(html).toContain(`/space?uri=${encodeURIComponent(SPACE)}"`); }); // A copy control keeps its width whether or not it is showing, so in running // text it opens a hole between the name and the word after it. it('sets a handle in a sentence with nothing between it and the next word', () => { const html = render( `/records?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}&repo=${CAROL}`, ); const name = html.indexOf('carol.example.com'); const after = html.indexOf(''s repo', name); expect(after).toBeGreaterThan(name); expect(html.slice(name, after)).not.toContain(' { const html = render( `/records?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}`, ); expect(html).toContain('Your repo in this space'); }); it('scopes every screen under a space, not just the records list', () => { for (const path of ['/space', '/ops', '/blobs']) { const html = render( `${path}?uri=${encodeURIComponent(SPACE)}&repo=${BOB}`, ); expect(html).toContain('bob.example.com'); expect(html).toContain('Your repo'); } }); it('names the authority of a space it does not host', () => { const html = render('/'); expect(html).toContain('bob.example.com'); }); // An NSID reverses to a domain, and these domains are atproto handles, so // the app's icon is an account's picture served by that account's own PDS. it("shows the space app's picture, read from a PDS rather than a web server", () => { const html = render('/'); expect(html).toContain('app.example/xrpc/com.atproto.sync.getBlob'); expect(html).not.toContain('unavatar'); expect(html).not.toContain('favicon'); }); // The picture comes from the account's own PDS, a host this app is already // reading from, rather than from an appview and its CDN. it("shows the authority's picture, served by their own PDS", () => { const html = render('/'); expect(html).toContain('bob.example/xrpc/com.atproto.sync.getBlob'); expect(html).not.toContain('cdn.bsky.app'); }); // Anybody may claim any handle in their own DID document, so one that does // not resolve back is marked rather than shown as an identity. it('marks a handle that is claimed but not confirmed', () => { const html = render('/'); expect(html).toContain('unconfirmed handle'); }); it('falls back to the DID while the lookup is outstanding', () => { const html = render( `/records?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}&repo=did:plc:stranger`, ); expect(html).toContain('did:plc:stranger'); }); // A space is served by two roles on two hosts, and which one answered is the // thing a reader most often wants and can least often tell. // A URI reads as a fact, so an unconfirmed handle in one would put somebody // else's name on an address. it('leaves an unconfirmed handle as its DID inside a URI', () => { const theirs = `at://${BOB}/space/example.bsky.forum/theirs`; const html = render('/'); const shown = html.slice(html.indexOf(theirs.slice(0, 20))); expect(html).toContain(`title="${theirs}"`); // bob.example.com is claimed but does not resolve back, so the URI keeps // the DID even though the prose beside it names him. expect(shown.slice(0, 400)).toContain(BOB); }); it('offers the provenance of the configuration it shows', () => { const html = render(`/space?uri=${encodeURIComponent(SPACE)}`); expect(html).toContain('Where these values came from'); }); it('renders the blob list', () => { const html = render(`/blobs?uri=${encodeURIComponent(SPACE)}`); expect(html).toContain('Blobs'); expect(html).toContain('bafkreiblob'); }); it('offers no editor to a read-only session', () => { vi.stubGlobal('localStorage', { getItem: () => JSON.stringify({ ...session, scope: 'atproto space:*?action=read' }), setItem() {}, removeItem() {}, }); const html = render( `/record?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}&rkey=${RKEY}`, ); expect(html).toContain('read only'); expect(html).not.toContain('>Delete<'); }); it('offers a way into a space the list does not name', () => { const html = render('/'); // listSpaces reports only spaces this account has written to, so a URI box // is the only route to one it has been admitted to and not written in. expect(html).toContain('Open a space by URI'); }); it('offers the policy editor to the space authority', () => { const html = render(`/space?uri=${encodeURIComponent(SPACE)}`); expect(html).toContain('>Edit<'); expect(html).toContain('Delete this space'); }); it('offers neither editor for a space this account does not host', () => { const theirs = `at://did:plc:someone/space/example.bsky.forum/general`; const html = render(`/space?uri=${encodeURIComponent(theirs)}`); expect(html).not.toContain('Delete this space'); expect(html).not.toContain('>members<'); }); it('renders the credential inspector before anything is exchanged', () => { const html = render(`/credential?uri=${encodeURIComponent(SPACE)}`); // React escapes the apostrophe in "The credential's key". expect(html).toContain('credential's key'); expect(html).toContain('Run the exchange'); }); it('offers sign-in when no session is held', () => { vi.stubGlobal('localStorage', { getItem: () => null, setItem() {}, removeItem() {}, }); const html = render('/'); expect(html).toContain('Handle or DID'); expect(html).toContain('Read only'); expect(html).toContain('Read and write'); // A deployment serves whoever visits it, so the field names nobody. The // host's own handle in there would be wrong for everybody else. expect(html).toContain('value=""'); }); });