diff --git a/apps/web/tests/reference-composer.e2e.ts b/apps/web/tests/reference-composer.e2e.ts index 2ea7eed5e4..ed4e0ed4aa 100644 --- a/apps/web/tests/reference-composer.e2e.ts +++ b/apps/web/tests/reference-composer.e2e.ts @@ -147,7 +147,11 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through await input.fill('@') await expect.poll(() => menu.getByRole('option').count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(2) - const snapshot = await captureStableAria(page, '[role="listbox"]', scaffold.workspaceCwd) + // Session rows are dated from the live Host list, so their age bucket + // advances while the suite runs. + const snapshot = await captureStableAria( + page, '[role="listbox"]', scaffold.workspaceCwd, { normalizeAge: true }, + ) await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE) expect(snapshot).toContain('Files & folders') expect(snapshot).toContain('Sessions') diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index b43d13ae28..7b725ae64c 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -1094,22 +1094,26 @@ async function persistSeedSession( * on one machine (measured 69 → 70 tok/s) and swings wildly on a fast replay * (26333 tok/s for a 3 ms stream). */ -function normalizeAria(snapshot: string, workspaceCwd: string): string { +/** + * Relative-time buckets rendered by a dated row, in both dictionaries. + * + * Opt-in per capture: a session-tree golden asserts its own literal age (a + * fresh row reads `now`, an older one does not), so collapsing the vocabulary + * everywhere would delete that assertion. A region whose rows are dated from + * live wall-clock state asks for it instead. Anchored on an aria label's + * closing quote, where the bucket is always last. + */ +const ARIA_AGE = + /(?:now|\d+min|\d+h|\d+d|\d+mo|\d+y|刚刚|\d+分钟|\d+小时|\d+天|\d+个月|\d+年)(?=")/g + +function normalizeAria(snapshot: string, workspaceCwd: string, age: boolean): string { // The session heading renders the workspace's basename, not the full // path, so both spellings must collapse to the token. const base = workspaceCwd.split('/').pop()! - return snapshot + return (age ? snapshot.replace(ARIA_AGE, '{{age}}') : snapshot) .split(workspaceCwd).join('{{cwd}}') .split(base).join('{{workspace}}') .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '{{uuid}}') - // Rows dating a session render the shared relative-time bucket, which - // advances while the suite runs. Anchored on an aria label's closing - // quote, where the bucket is always last, and applied before the duration - // rules so the whole vocabulary collapses to one token. - .replace( - /(?:now|\d+min|\d+h|\d+d|\d+mo|\d+y|刚刚|\d+分钟|\d+小时|\d+天|\d+个月|\d+年)(?=")/g, - '{{age}}', - ) // The optional space in `\d+m ?\d+s` covers both minute spellings: the // stats line's compact `2m42s` and the message-chrome template's `2m 42s`. .replace( @@ -1141,13 +1145,21 @@ function normalizeAria(snapshot: string, workspaceCwd: string): string { * @param page - the page under test. * @param selector - the region locator selector. * @param workspaceCwd - normalization input. + * @param options - `normalizeAge` collapses relative-time buckets to `{{age}}` + * for a region whose rows are dated from live wall-clock state. * @returns the stable normalized snapshot. */ -export async function captureStableAria(page: Page, selector: string, workspaceCwd: string): Promise { +export async function captureStableAria( + page: Page, + selector: string, + workspaceCwd: string, + options: { normalizeAge?: boolean } = {}, +): Promise { const region = page.locator(selector).first() - let previous = normalizeAria(await region.ariaSnapshot(), workspaceCwd) + const age = options.normalizeAge === true + let previous = normalizeAria(await region.ariaSnapshot(), workspaceCwd, age) await expect.poll(async () => { - const current = normalizeAria(await region.ariaSnapshot(), workspaceCwd) + const current = normalizeAria(await region.ariaSnapshot(), workspaceCwd, age) const stable = current === previous previous = current return stable