/** * Demo capture — the product working, not just the deployment answering. * * The walkthrough's other nine captures prove the cluster is real: it converges, * it refuses bad credentials, it commits a quorum write, it can be observed and * restored. None of them shows what tidalDB is FOR. This one does, and the claim * is deliberately narrow: a signal write changes the order of the next query, * with nothing in between. * * It is one image holding both states. Two separate stills of a list would force * a viewer to diff two frames from memory; stacked before/after makes the * movement self-evident. The lists are composed at their native resolution rather * than scaled to fill the frame — a downscaled list is the BUG-010 mistake. * * This runs against a LOCAL standalone node with fixture data, and the frame says * so. `demo/audience-brief.md` forbids implying a dataset that is not what it * appears to be, and `skip` being `permanent: true` is exactly why the fixture * never touches production. */ import { expect, test } from '@playwright/test'; import { CAPTURE_HEIGHT, CAPTURE_WIDTH, recordScreenshot, writeManifestFragment, type CaptureRecord, } from '../support/proof-panel.ts'; import { startApp } from '../../app/harness.ts'; const records: CaptureRecord[] = []; /** * Rows shown in each half. * * Five, not six: two six-row lists plus their labels came to ~922px inside a * 900px frame, which pushed the environment footer off the bottom and clipped the * last row. The footer is what stops a fixture screenshot reading as production, * so it is not optional — and the fix is fewer rows, never a downscaled list * (that was the BUG-010 mistake). */ const ROWS = 5; /** Viewport that makes the page's centred column fill the frame exactly. */ const APP_VIEWPORT = { width: 1240, height: 900 }; type Row = { position: string; title: string }; function composition(before: string, after: string, movedTitle: string, rows: Row[]): string { const label = (text: string) => `
${text}
`; return ` ${label(`before — ${rows.length} of 60 items, no signals written yet. Everything ties, so the order is the tie-break.`)} ${label(`after — one like on “${movedTitle}”, then the same query again. It is now first.`)} `; } test.describe('feed-app product surface', () => { test('CAP-016 a signal write reorders the feed immediately', async ({ page }, testInfo) => { const app = await startApp(); try { await page.setViewportSize(APP_VIEWPORT); await page.goto(`${app.url}/?limit=${ROWS}`, { waitUntil: 'networkidle' }); await page.waitForSelector('#feed li'); await page.evaluate(() => document.fonts.ready); const list = page.locator('#feed'); const readRows = (): Promise => page.$$eval('#feed li', (nodes) => nodes.map((li) => ({ position: li.querySelector('.pos')!.textContent!.trim().replace(/\s+/g, ' '), title: li.querySelector('.title')!.textContent!.trim().replace(/\s+/g, ' '), })), ); const before = await readRows(); expect(before.length, `the page must render ${ROWS} rows to compose from`).toBe(ROWS); const beforeShot = await list.screenshot(); // The last visible row, so the movement spans the whole frame. const targetTitle = before[before.length - 1]!.title; await page.locator('#feed li').last().locator('button[data-signal="like"]').click(); // Wait for the re-read to land by watching the thing under test change, // rather than sleeping a guessed interval. `startsWith`, not equality: once // a row moves, its title element also carries the delta badge ("\u25b25"). await page.waitForFunction( (title) => document.querySelector('#feed li .title')?.textContent?.trim().startsWith(title) === true, targetTitle, { timeout: 15_000 }, ); const after = await readRows(); const afterIndex = after.findIndex((row) => row.title.startsWith(targetTitle)); // Assert BEFORE photographing. A capture is a photograph of an // already-proven state; if the order did not change there is nothing // honest to show. expect( afterIndex, `the liked item must have moved to the top; "${targetTitle}" is at index ${afterIndex}`, ).toBe(0); // Move the pointer off the list and drop focus before photographing. // The cursor physically stays where it clicked, so after the re-render a // DIFFERENT row's button sits under it and picks up `:hover` — a highlighted // control on an unrelated row, which reads as meaningful when it is not. await page.mouse.move(0, 0); await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); const afterShot = await list.screenshot(); const dataUri = (buffer: Buffer): string => `data:image/png;base64,${buffer.toString('base64')}`; await page.setViewportSize({ width: CAPTURE_WIDTH, height: CAPTURE_HEIGHT }); await page.setContent( composition(dataUri(beforeShot), dataUri(afterShot), targetTitle, before), { waitUntil: 'load' }, ); await page.evaluate(() => document.fonts.ready); records.push( await recordScreenshot(await page.screenshot(), testInfo, { captureId: 'CAP-016-feed-reorder', capabilityId: 'CAP-016', expected: 'The same query, before and after one like: the liked item moves from last to first, with its like_boost visible', businessPurpose: 'A signal write changes the order with no ETL in between — the product thesis, not the deployment', personas: ['cluster operator', 'application developer'], width: CAPTURE_WIDTH, height: CAPTURE_HEIGHT, }), ); } finally { await app.close(); } }); test.afterAll(async () => { await writeManifestFragment('feed-app', records); }); });