test(web): scope the aria age normalizer to the region that needs it

Collapsing every relative-time bucket to `{{age}}` reached the session-tree
goldens, where a literal age is the assertion: a fresh row reads `now` and an
older one does not. Six e2e files failed, two of them by aborting mid-scenario
and leaving their replay fixtures half-consumed.

`captureStableAria` now takes the rule as an opt-in, and only the reference
menu — whose rows are dated from the live Host list — asks for it.

Refs #3154
This commit is contained in:
Yichen Jiang
2026-08-27 12:10:52 +08:00
parent c8e8f8249f
commit db14361372
2 changed files with 30 additions and 14 deletions
+5 -1
View File
@@ -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')
+25 -13
View File
@@ -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<string> {
export async function captureStableAria(
page: Page,
selector: string,
workspaceCwd: string,
options: { normalizeAge?: boolean } = {},
): Promise<string> {
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