/**
* 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);
});
});