jevstrudel.git / website / src / jev / DataTab.test.mjs

The data tab's pages rendered from the Worker's real answers (worker/src/data.ts against testD1): they show what the answers hold, and everything a listener wrote as text, never as markup.

4import { createElement } from 'react';
5import { renderToString } from 'react-dom/server';
6import { beforeAll, describe, expect, it } from 'vitest';
7import { memoryBudgets } from '../../../worker/test/budget';
8import { testD1 } from '../../../worker/test/d1';
9import { memoryKV } from '../../../worker/test/kv';
10import { memoryR2 } from '../../../worker/test/r2';
11import { memoryTabHubs } from '../../../worker/test/tab-hubs';
12import { d1Accounts } from '../../../worker/src/accounts-store';
13import { d1Content } from '../../../worker/src/content-store';
14import { data } from '../../../worker/src/data';
15import { d1Listening } from '../../../worker/src/listening-store';
16import { d1Radio } from '../../../worker/src/radio';
17import { newSessionToken, SESSION_TTL_MS } from '../../../worker/src/session';
18import { AccountPage, PerformancePage, SongPage } from './DataTab.jsx';
20const NOON = Date.UTC(2026, 8, 27, 12);
21const HOSTILE_CODE = 's("bd") // <script>alert(1)</script>';
22const HOSTILE_COMMENT = '<img src=x onerror=alert(1)>';
23let db;
24let userId;
25
26const env = () => ({
27  DB: db,
28  CACHE: memoryKV(),
29  OAUTH_KV: memoryKV(),
30  COVERS: memoryR2(),
31  BUDGET: memoryBudgets(1000, () => NOON),
32  TAB_HUB: memoryTabHubs(),
33  JEV_DAILY_PER_USER: 1000,
34  DATA_LIMIT: { limit: async () => ({ success: true }) },
35  LOBBY: { idFromName: () => ({}), get: () => ({ summary: async () => ({ open: 1, listed: 0, playing: 0 }) }) },
36  PARTY_DIRECTORY: { idFromName: () => ({}), get: () => ({ live: async () => [] }) },
37});
38const read = async (path) =>
39  (await data(new Request(`https://jevstrudel.example/jev/data${path}`), env(), () => NOON)).json();

React marks where one text node ends and the next begins; the text is what counts

41const html = (Page, body) => renderToString(createElement(Page, { body, open: () => {} })).replaceAll('<!-- -->', '');
43beforeAll(async () => {
44  db = testD1();
45  userId = newSessionToken().slice(0, 22);
46  await d1Accounts(db, () => NOON).createUser(
47    { id: userId, displayName: 'Navi' },
48    { id: 'credential-id-CCCCCCCCCCCC', publicKey: new Uint8Array([1, 2, 3]), signCount: 0, transports: ['hybrid'] },
49    newSessionToken(),
50    SESSION_TTL_MS,
51  );
52  await d1Radio(db, () => NOON).add(userId, 'jev/hey-listen');
53  const content = d1Content(db, () => NOON);
54  await content.createSong(userId, 'S'.repeat(22), 'R'.repeat(22), {
55    title: 'Hey <b>listen</b>',
56    description: 'a fairy',
57    spec: '# spec',
58    code: HOSTILE_CODE,
59  });
60  await content.recordScreen('revision', 'R'.repeat(22), 'abuse', 0.91);
61  await content.addComment('C'.repeat(22), userId, { site: 'jev/hey-listen' }, HOSTILE_COMMENT);
62  await d1Listening(db).record('P'.repeat(22), '2026-09-27', {
63    song: 'jev/hey-listen',
64    codeHash: 'e'.repeat(64),
65    model: 'jev-1.13.0',
66    every: 4,
67    ended: 'stopped',
68    form: { jev: 0, question: 'section' },
69    jevs: [
70      {
71        segments: [
72          { status: 'opening', values: { section: { choice: 'intro' }, energy: { score: 1.4 } } },
73          { status: 'fallback', ms: 5000, values: { section: { choice: 'drop' }, energy: { score: 2 } } },
74        ],
75      },
76    ],
77  });
78});
79
80describe('the data tab', () => {
81  it('shows an account: its name, passkeys by date, radio, held song and comment', async () => {
82    const out = html(AccountPage, await read(`/accounts/${userId}`));
83    expect(out).toContain('Navi');
84    expect(out).toContain('hybrid');
85    expect(out).toContain('jev/hey-listen');
86    expect(out).toContain('abuse');
87    expect(out).toContain('0 of 1000 calls');
88    expect(out).not.toContain('credential-id-CCCCCCCCCCCC');
89    // a listener's writing is text
90    expect(out).toContain('&lt;img src=x onerror=alert(1)&gt;');
91    expect(out).not.toContain(HOSTILE_COMMENT);
92  });
93
94  it('shows a held song whole, as text', async () => {
95    const out = html(SongPage, await read(`/songs/${'S'.repeat(22)}`));
96    expect(out).toContain('Hey &lt;b&gt;listen&lt;/b&gt;');
97    expect(out).toContain('&lt;script&gt;alert(1)&lt;/script&gt;');
98    expect(out).not.toContain('<script>');
99    expect(out).toContain('abuse 91%');
100  });
101
102  it('shows a performance: each decision, its section and time', async () => {
103    const out = html(PerformancePage, await read(`/performances/${'P'.repeat(22)}`));
104    expect(out).toContain('jev/hey-listen');
105    expect(out).toContain('fallback');
106    expect(out).toContain('5000 ms');
107    expect(out).toContain('section drop');
108    expect(out).toContain('energy 2.0');
109    expect(out).toContain(`?performance=${'P'.repeat(22)}`);
110  });
111});