// 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('