From ffaee4a77dbf5d1d3979123c4762a3e095b1b890 Mon Sep 17 00:00:00 2001 From: probe Date: Fri, 23 Oct 2026 08:27:50 +0800 Subject: [PATCH] test(webui): read the provider picker's exit animation where it changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The provider picker's exit animation was read by polling a live DOM node across the process boundary, on a poll grid (100ms and coarser) that is coarser than the animation it looked for. The popup's exit window is 120ms and the skeleton unmounts the popup when that window is over, so a sample that arrives after the unmount reads a detached node — whose computed style is empty — and a sample that arrives before the popup has started leaving reads the entrance animation. Both outcomes fail the assertion while the popup was never in the wrong state. The reading now happens in the page, where the state changes: a MutationObserver records the popup's own computed animation-name at the moment its class changes, and the test reads that record. The record outlives the popup, so the sample cannot miss the window. The expectation is unchanged: the popup carries the exit animation rather than the entrance one while it fades out. A failure now shows the recorded state instead of an empty string. --- .../webui/test/browser/provider-form.spec.mjs | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/webui/test/browser/provider-form.spec.mjs b/packages/webui/test/browser/provider-form.spec.mjs index 48051eab5..b0e984f25 100644 --- a/packages/webui/test/browser/provider-form.spec.mjs +++ b/packages/webui/test/browser/provider-form.spec.mjs @@ -102,6 +102,43 @@ async function fillNewProvider(page, dialog) { await dialog.getByLabel("模型名称").fill("draft-model"); } +/** + * Records what the provider picker's popup carried, in the page. + * + * The exit animation is a 120ms window: the popup starts leaving, the + * stylesheet fades it out, and the skeleton unmounts it when the fade is + * over. A reading taken across the wire can arrive after that unmount — and a + * popup that is no longer in the document has no computed style to read, so + * the reading comes back empty and the test fails on a state it never saw. + * It can also arrive before the popup has started leaving, which reads as + * the entrance animation. + * + * So the reading is taken where the state changes: the page watches the + * popup's own class and records the popup's computed animation-name at the + * moment that class changes. The record outlives the popup, so the test can + * read it whenever it gets there, and it cannot miss the window. + */ +async function recordProviderPickerMotion(page) { + await page.evaluate(() => { + const selector = '[role="listbox"][aria-label="提供商列表"]'; + window.__webuiProviderPickerMotion = []; + new MutationObserver((records) => { + for (const record of records) { + const node = record.target; + if (!(node instanceof Element) || !node.matches(selector)) continue; + window.__webuiProviderPickerMotion.push({ + leaving: node.classList.contains("is-leaving"), + animationName: getComputedStyle(node).animationName, + }); + } + }).observe(document.documentElement, { + attributes: true, + subtree: true, + attributeFilter: ["class"], + }); + }); +} + test("the custom tab lists the saved provider and its model row", async ({ page }) => { await configureFixture(page, seedProviderSurface); await openCustomModels(page); @@ -180,12 +217,18 @@ test.describe("with motion allowed", () => { // opens and closes the same way. expect(await listbox.evaluate((node) => getComputedStyle(node).animationName)).toBe("webui-dropdown-in"); + // The recorder is in place before the press, so what it records is this + // press's leaving. + await recordProviderPickerMotion(page); await page.mouse.click(6, 6); + // While it is fading out it is still mounted, and it carries the exit - // animation rather than the entrance one. + // animation rather than the entrance one. The reading is the one the page + // took at the moment the popup started leaving — not a sample of a popup + // that has already been unmounted. await expect - .poll(() => listbox.evaluate((node) => getComputedStyle(node).animationName).catch(() => "gone")) - .toBe("webui-dropdown-out"); + .poll(() => page.evaluate(() => (window.__webuiProviderPickerMotion ?? []).find((entry) => entry.leaving) ?? null)) + .toEqual({ leaving: true, animationName: "webui-dropdown-out" }); await expect(listbox).toHaveCount(0); }); });