From 07e0543a0c7e5f305a417da04273721face3d8a6 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Thu, 17 Sep 2026 06:17:26 +0800 Subject: [PATCH] test(dashboard): let the credential panel count settle before asserting it The model-provider category assertion waited for one credential panel to become visible and then took a one-shot count. A category switch is a state transition, not a settled fact, so that count can still observe the pane mid-mount: the required dashboard-acceptance job failed on that line for #4593 and #4604 while the same tree passed on main's own Python Tests run, and a one-shot count cannot say whether the page hosted zero or two panels. The assertion now waits for exactly one panel, and when the count never settles it names every matching node with its owning section and visibility, so the next CI failure is attributable without a machine that reproduces it. Because two matching panels also make a strict-mode locator wait throw, the count settles before the visibility check instead of after it. Assertion strength is unchanged: exactly one visible credential panel is still required inside the model-provider category, and the readback labels are still asserted below. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../typed-actions.mjs | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/examples/personal-workspace-browser/typed-actions.mjs b/examples/personal-workspace-browser/typed-actions.mjs index d4af495410..8b747ba16b 100644 --- a/examples/personal-workspace-browser/typed-actions.mjs +++ b/examples/personal-workspace-browser/typed-actions.mjs @@ -7,6 +7,34 @@ import { } from "./fixture.mjs"; import { openWorkspacePage } from "./scenario-context.mjs"; +// A category switch is a state transition, not a settled fact: the panel can +// still be mid-mount when a one-shot count runs, which is how this assertion +// failed on CI while the same tree passed on main. Wait for the declared count +// and, when it never settles, name what the page actually hosted so the next +// failure is attributable without a local reproduction. +async function waitForSelectorCount(page, selector, expected, timeoutMs = 10_000) { + const deadline = Date.now() + timeoutMs; + let count = await page.locator(selector).count(); + while (count !== expected && Date.now() < deadline) { + await page.waitForTimeout(100); + count = await page.locator(selector).count(); + } + if (count === expected) { + return; + } + const hosted = await page.evaluate((target) => [...document.querySelectorAll(target)].map((node) => { + const rect = node.getBoundingClientRect(); + const section = node.closest("section, main, div"); + return `${node.tagName.toLowerCase()}.${node.className.toString().trim().split(/\s+/).join(".")}` + + ` section=${section ? section.className.toString().trim().split(/\s+/)[0] : ""}` + + ` visible=${rect.width > 0 && rect.height > 0}`; + }), selector); + throw new Error( + `Expected exactly ${expected} ${selector} panel(s) after the category settled, found ${count}` + + `${hosted.length ? `: ${hosted.join(" | ")}` : ""}`, + ); +} + function operationProposal({ id, title, lifecycleState, status, resultDelivery = null }) { const outcomeObserved = lifecycleState === "outcome_observed"; return { @@ -1049,10 +1077,8 @@ export const typedActionsScenario = { }); await page.getByRole("button", { name: /模型 Provider 配置/ }).click(); await page.getByRole("heading", { level: 1, name: "模型 Provider 配置", exact: true }).waitFor({ state: "visible" }); - await page.locator(".personal-operator-credential").waitFor({ state: "visible" }); - if (await page.locator(".personal-operator-credential").count() !== 1) { - throw new Error("The model provider category did not host exactly one credential panel"); - } + await waitForSelectorCount(page, ".personal-operator-credential", 1); + await page.locator(".personal-operator-credential").first().waitFor({ state: "visible" }); await page.locator(".personal-operator-credential-readback").waitFor({ state: "visible" }); for (const label of [/^API key$/u, /^指纹$/u, /^Endpoint base URL$/u]) { await page.getByText(label).first().waitFor({ state: "visible" });