Skip to content

Commit 9185685

Browse files
committed
test(showcase-smoke): poll the chart box against a bounded deadline instead of a fixed 1500 ms wait (#7569)
The harness half of #7569, on the PM ruling that extended this claim's file surface to the spec file after the premise check found the fixed wait was never under `docs/qa/platform-checklist/`. `page.waitForTimeout(1500)` followed by a single `boundingBox()` read measured the recharts `ResponsiveContainer` initial-layout window, not the product: across 3 fresh loads each drawing 5 real charts, polling every 400 ms returned null ONLY at t = 1.5 s — exactly where the suite measured — and non-null on all 19 later samples; the same surfaces in isolation were 4/4 green. Both go, per the #7472 / #7409 shape: loop the predicate against a deadline derived from the case's own budget, never sleep-then-read-once. A bigger fixed number is not the fix. Three properties kept deliberately: - the last non-null box is retained, so a chart that IS laid out but collapsed still fails on its width (#2616 D) instead of being reported as "no chart SVG" — those are different defects and now read differently; - `pollUntil` returns its last sample rather than throwing, so a timeout fails as the assertion it belongs to, naming the surface; - the `pageErrors` / placeholder / main-content assertions moved after the waits, so the error-collection window is as long as the render actually took rather than a fixed slice of it. `nav-surfaces-render` is reconciled in the same commit: its knownGaps entry recording this harness gap is removed now that the gap is closed, and its revision-3 history entry covers both halves.
1 parent 7c6ccaa commit 9185685

2 files changed

Lines changed: 83 additions & 17 deletions

File tree

‎docs/qa/platform-checklist/areas/platform-core.json‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -220,25 +220,24 @@
220220
"fixtures": {
221221
"app": "showcase",
222222
"knownGaps": [
223-
"no stock showcase fixture deliberately throws inside a route, so the ErrorBoundary fallback ('Something went wrong' + Try Again / Go Home — objectui packages/app-shell/src/chrome/ErrorBoundary.tsx) is verified opportunistically on any failure encountered, plus via the bad-route probe below",
224-
"the pinned suite still MEASURES charts with a fixed `page.waitForTimeout(1500)` followed by a SINGLE `boundingBox()` read (examples/app-showcase/e2e/showcase-smoke.spec.ts) — a coin-flip red whenever that fixed instant lands inside the recharts ResponsiveContainer initial-layout window. Until that read is replaced by a bounded poll-until-non-null (the #7472 / #7409 de-flake shape: loop the predicate against a DEADLINE derived from the case's own budget, never sleep-then-read-once — a bigger fixed number is not the fix), a null-box chart red must be RE-MEASURED per the steps before it is recorded. Tracked by #7569, whose harness half lands in the spec file, not in this checklist"
223+
"no stock showcase fixture deliberately throws inside a route, so the ErrorBoundary fallback ('Something went wrong' + Try Again / Go Home — objectui packages/app-shell/src/chrome/ErrorBoundary.tsx) is verified opportunistically on any failure encountered, plus via the bad-route probe below"
225224
]
226225
},
227226
"steps": [
228227
"run the pinned suite: pnpm -C examples/app-showcase test:smoke (the SURFACES array in e2e/showcase-smoke.spec.ts — 31 surfaces: pages, object lists, dashboards, reports, the view-gallery pages)",
229228
"GET /api/v1/meta/app?id=com.example.showcase and extract every nav destination from the response",
230229
"diff the served nav destinations against the SURFACES array; hand-walk any destination the suite does not cover (screenshot first, then DOM)",
231230
"on each hand-walked surface: wait for render, screenshot, then check for pageerror / empty <main> / placeholder leaks ('no actions configured')",
232-
"chart surfaces (the chart:true entries in SURFACES — Delivery Operations, Chart Gallery, Command Center): if the suite reports a NULL chart boundingBox, do NOT record it as a product red — RE-MEASURE it by polling boundingBox until non-null within a bounded budget. #7569 measured the difference on this exact assertion: 3 fresh loads each drew 5 real charts, and polling every 400 ms returned null ONLY at t = 1.5 s — the suite's fixed measurement instant, and exactly the recharts ResponsiveContainer initial-layout window — and non-null on all 19 later samples; the same item re-run in isolation is 4/4 green. A box that stays null through the whole bounded poll IS a product finding; one that resolves on a later sample is the harness's measurement window, not the product",
231+
"chart surfaces (the chart:true entries in SURFACES — Delivery Operations, Chart Gallery, Command Center): the suite now POLLS boundingBox against a bounded deadline (SURFACE_BUDGET_MS in the spec) instead of reading it once after a fixed wait, so a chart red here is a real finding and not the measurement window — #7569 measured the difference: 3 fresh loads each drew 5 real charts, and polling every 400 ms returned null ONLY at t = 1.5 s, the old fixed measurement instant and exactly the recharts ResponsiveContainer initial-layout window, then non-null on all 19 later samples. Read WHICH assertion failed: 'no chart SVG laid out' means the box stayed null for the whole budget; a width/height failure means the chart laid out but collapsed (#2616 D). If you hand-walk a chart surface outside the suite, poll the same way — never sleep-then-read-once",
233232
"probe the failure path: navigate to a nonexistent route under the app (/_console/apps/com.example.showcase/object_that_does_not_exist); screenshot what renders",
234233
"if ANY surface fails during the sweep, verify the failure presents as the shell's error boundary or a named empty/error state — capture it"
235234
],
236235
"acceptance": [
237236
{
238237
"clause": "no surface throws a pageerror, renders an empty <main>, or leaks a 'no actions configured' placeholder; chart surfaces draw a real SVG whose box is measured by POLLING until non-null within a bounded budget — never by a fixed wall-clock wait followed by a single boundingBox read, which measures the layout window rather than the product",
239238
"oracle": "test",
240-
"verify": "pnpm -C examples/app-showcase test:smoke (SURFACES array in e2e/showcase-smoke.spec.ts) — green; a chart clause that failed on a NULL box counts only after the bounded re-measure in the steps confirms it stays null (the pinned suite still reads the box once after a fixed 1500 ms wait — see knownGaps, #7569)",
241-
"evidence": "test run output +, for any null-box chart failure, the bounded re-measure's sample series"
239+
"verify": "pnpm -C examples/app-showcase test:smoke (SURFACES array in e2e/showcase-smoke.spec.ts) — green; the chart assertion polls boundingBox against a bounded deadline and keeps the last non-null sample, so a null-box failure and a collapsed-panel failure report as different assertions (#7569)",
240+
"evidence": "test run output + the failing assertion's message for any chart red"
242241
},
243242
{
244243
"clause": "the smoke's coverage is CURRENT: every nav destination served in meta/app is either in the SURFACES array or hand-walked this run — no surface silently outside the net",
@@ -262,7 +261,7 @@
262261
"negative": [
263262
"a surface that renders a blank <main> is a FAIL even with no console error — transitional emptiness must be ruled out by the screenshot-first protocol, then the persistent blank is the finding",
264263
"a bad route producing a white page (shell gone) is a FAIL of the error-boundary clause",
265-
"a chart red obtained from a SINGLE boundingBox read at a fixed instant is NOT a product finding and must not be filed as one — it measures the ResponsiveContainer layout window, and it costs a triage cycle every run (#7569). Re-measure with a bounded poll first. The converse still holds both ways: a box that stays null through the whole poll, or a resolved width ≤ 200 (the #2616 D collapsed-panel case), IS a FAIL"
264+
"a chart red obtained from a SINGLE boundingBox read at a fixed instant is NOT a product finding and must not be filed as one — it measures the ResponsiveContainer layout window, and it cost a triage cycle every run until #7569 replaced that read with a bounded poll. Re-introducing any fixed sleep-then-read-once in this suite re-opens it. The converse holds both ways: a box that stays null through the WHOLE bounded poll, or a laid-out width ≤ 200 (the #2616 D collapsed-panel case), IS a FAIL"
266265
],
267266
"automated": { "kind": "e2e", "ref": "examples/app-showcase/e2e/showcase-smoke.spec.ts" },
268267
"traps": ["hydration-race", "single-datapoint", "stale-console-bundle"],
@@ -275,7 +274,7 @@
275274
"history": [
276275
{ "revision": 1, "date": "2026-08-07", "change": "initial — wraps the existing automated smoke as a checklist row so runs report it alongside manual items", "ref": "#3358" },
277276
{ "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" },
278-
{ "revision": 3, "date": "2026-08-11", "change": "de-flaked the chart clause's MEASUREMENT: a null boundingBox read once at a fixed instant is the recharts ResponsiveContainer layout window, not a product defect (measured: null only at t=1.5s, non-null on all 19 later samples, 5 real charts on each of 3 fresh loads). The clause now requires a bounded poll-until-non-null, the steps tell the runner to re-measure before recording a red, and knownGaps records that the pinned suite's fixed 1500 ms wait + single read is still there", "ref": "#7569" }
277+
{ "revision": 3, "date": "2026-08-11", "change": "de-flaked the chart clause's MEASUREMENT, both halves in one change: a null boundingBox read once at a fixed instant is the recharts ResponsiveContainer layout window, not a product defect (measured: null only at t=1.5s, non-null on all 19 later samples, 5 real charts on each of 3 fresh loads). The clause now requires a bounded poll-until-non-null, and the pinned suite was changed to match — showcase-smoke.spec.ts's fixed page.waitForTimeout(1500) plus single boundingBox() read are gone, replaced by deadline-bounded polling (the #7472/#7409 shape), keeping the last non-null sample so a collapsed panel (#2616 D) still fails on its width rather than as 'no chart SVG'", "ref": "#7569" }
279278
]
280279
},
281280
{

‎examples/app-showcase/e2e/showcase-smoke.spec.ts‎

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,32 +43,99 @@ const SURFACES: { name: string; path: string; chart?: boolean }[] = [
4343
{ name: 'Work Map', path: base('page/showcase_task_map') },
4444
];
4545

46+
/**
47+
* The budget every wait in this file spends, measured from the start of the
48+
* surface that calls it — and the SOURCE of each wait's deadline, rather than
49+
* one more number chosen in advance. It sits inside the config's 45s per-test
50+
* `timeout`, which stays the only wall-clock budget left to expire after it.
51+
*
52+
* ⛔ Never put a fixed wall-clock wait back in front of an assertion here. The
53+
* shape this replaced was `await page.waitForTimeout(1500)` followed by a
54+
* SINGLE `boundingBox()` read, and #7569 measured what that costs: across 3
55+
* fresh loads each drawing 5 real charts, polling every 400 ms returned null
56+
* ONLY at t = 1.5 s — precisely the instant this suite measured at, and
57+
* precisely the recharts `ResponsiveContainer` initial-layout window — and
58+
* non-null on all 19 later samples; the same surfaces re-run in isolation were
59+
* 4/4 green. So the assertion reported a failing surface whenever the fixed
60+
* wait landed inside the layout window: a coin-flip red on a correct product,
61+
* costing a triage cycle every run. A bigger fixed number is not the fix — the
62+
* budget is a constant while the thing it times is not. Wait on a predicate
63+
* the page can actually satisfy, bounded by a deadline.
64+
*/
65+
const SURFACE_BUDGET_MS = 25_000;
66+
const POLL_MS = 100;
67+
68+
const surfaceDeadline = (): number => Date.now() + SURFACE_BUDGET_MS;
69+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
70+
71+
/**
72+
* Poll `probe` until it returns a truthy value, or until `deadline`. Returns
73+
* whatever the last sample was either way, so the CALLER still asserts on it —
74+
* a timeout then fails as the assertion it belongs to, naming the surface,
75+
* instead of as a bare timeout that says nothing about what was missing.
76+
*/
77+
async function pollUntil<T>(probe: () => Promise<T>, deadline: number): Promise<T> {
78+
let sample = await probe();
79+
while (!sample && Date.now() < deadline) {
80+
await sleep(POLL_MS);
81+
sample = await probe();
82+
}
83+
return sample;
84+
}
85+
4686
for (const surface of SURFACES) {
4787
test(`surface renders cleanly: ${surface.name}`, async ({ page }) => {
4888
const pageErrors: string[] = [];
4989
page.on('pageerror', (e) => pageErrors.push(e.message));
5090

91+
const deadline = surfaceDeadline();
5192
await page.goto(surface.path, { waitUntil: 'domcontentloaded' });
52-
await page.locator('main').first().waitFor({ state: 'visible', timeout: 25_000 });
53-
await page.waitForTimeout(1500);
93+
const main = page.locator('main').first();
94+
await main.waitFor({ state: 'visible', timeout: SURFACE_BUDGET_MS });
95+
96+
// Replaces the fixed 1500 ms settle with a wait on the thing the assertion
97+
// below actually needs — rendered main content — on the same deadline.
98+
const mainText = await pollUntil(
99+
async () => ((await main.innerText().catch(() => '')) || '').trim(),
100+
deadline,
101+
);
54102

103+
let chartBox: { width: number; height: number } | null = null;
104+
if (surface.chart) {
105+
const svg = page.locator('.recharts-wrapper svg, .recharts-surface').first();
106+
await svg.waitFor({ state: 'visible', timeout: SURFACE_BUDGET_MS });
107+
// `visible` commits before ResponsiveContainer has sized its child, which
108+
// is why reading the box once lands inside the layout window. Poll it
109+
// instead, and keep the last non-null sample: a chart that IS laid out but
110+
// collapsed must still fail on its width (#2616 D below) rather than be
111+
// reported as "no chart SVG", which is a different defect.
112+
let lastSeen: { width: number; height: number } | null = null;
113+
const laidOut = await pollUntil(async () => {
114+
const box = await svg.boundingBox();
115+
if (box) lastSeen = box;
116+
return box && box.width > 200 && box.height > 0 ? box : null;
117+
}, deadline);
118+
chartBox = laidOut ?? lastSeen;
119+
}
120+
121+
// Asserted after the waits above, so the error-collection window is as long
122+
// as the render actually took rather than a fixed slice of it.
55123
expect(pageErrors, `uncaught errors on ${surface.name}`).toEqual([]);
56124
await expect(
57125
page.getByText(/no actions configured/i),
58126
`leaked placeholder on ${surface.name}`,
59127
).toHaveCount(0);
60-
const mainText = (await page.locator('main').first().innerText().catch(() => '')) || '';
61-
expect(mainText.trim().length, `${surface.name} rendered no main content`).toBeGreaterThan(0);
128+
expect(mainText.length, `${surface.name} rendered no main content`).toBeGreaterThan(0);
62129

63130
if (surface.chart) {
64-
const svg = page.locator('.recharts-wrapper svg, .recharts-surface').first();
65-
await svg.waitFor({ state: 'visible', timeout: 25_000 });
66-
const box = await svg.boundingBox();
67-
expect(box, `${surface.name}: no chart SVG`).not.toBeNull();
131+
expect(
132+
chartBox,
133+
`${surface.name}: no chart SVG laid out within ${SURFACE_BUDGET_MS}ms`,
134+
).not.toBeNull();
68135
// >0 alone previously passed even on a collapsed ~130px-wide chart panel
69136
// (#2616 D) — require a width a real chart panel would actually have.
70-
expect(box!.width, `${surface.name}: chart width`).toBeGreaterThan(200);
71-
expect(box!.height, `${surface.name}: chart height`).toBeGreaterThan(0);
137+
expect(chartBox!.width, `${surface.name}: chart width`).toBeGreaterThan(200);
138+
expect(chartBox!.height, `${surface.name}: chart height`).toBeGreaterThan(0);
72139
}
73140
});
74141
}

0 commit comments

Comments
 (0)