diff --git a/clients/web/src/components/screens/SkillsScreen/SkillsScreen.stories.tsx b/clients/web/src/components/screens/SkillsScreen/SkillsScreen.stories.tsx index dfdc4581..cfb08b60 100644 --- a/clients/web/src/components/screens/SkillsScreen/SkillsScreen.stories.tsx +++ b/clients/web/src/components/screens/SkillsScreen/SkillsScreen.stories.tsx @@ -440,6 +440,45 @@ export const LongSkillDocument: Story = { const geometry = () => sections().map((s) => Math.round(s.getBoundingClientRect().height)); + // Hold the first geometry sample until the Frontmatter section has settled + // on its final content. That section renders its YAML through + // `CodeHighlight`, which paints a plain `Code` block until the lazily + // imported Prism runtime and grammar land and then swaps the highlighter + // in — for this fixture a rendering 23px taller (the theme's `
`
+    // padding against Mantine's). Whether the swap has happened by the time
+    // the story gets here depends on how long those dynamic imports take: on
+    // a quiet machine an earlier story in this file has already loaded them,
+    // and on a loaded one they land between the two samples below, so
+    // `before` measured the placeholder and the reopen measured the
+    // highlighter — `[…, 155, 285]` against `[…, 178, 262]`, one total
+    // redistributed (#2350). Neither sample was mid-layout; the section's
+    // content changed under the story. Waiting for the highlighted output
+    // makes both samples measure the same content, which is what the
+    // collapse-then-reopen comparison at the end assumes.
+    const frontmatterPanel = (
+      await canvas.findByRole("button", { name: /Frontmatter/ })
+    )
+      .closest(".mantine-Accordion-item")
+      ?.querySelector(".mantine-Accordion-panel");
+    if (!(frontmatterPanel instanceof HTMLElement)) {
+      throw new Error("Frontmatter panel not found");
+    }
+    await waitFor(
+      async () => {
+        // `
` is the highlighter's output; the placeholder is a
+        // Mantine `Code` block, a bare `
`.
+        await expect(frontmatterPanel.querySelector("pre code")).not.toBeNull();
+      },
+      // What this waits on is three dynamic imports served by the Vite dev
+      // server every story iframe in the run shares, not a React commit:
+      // 100-150ms cold on an idle server, and past Testing Library's 1000ms
+      // default under the gate's load, which is how the two samples came to
+      // straddle it. The same shape and budget as the Ace-worker wait in
+      // `JsonObjectInput.stories.tsx` (#2292), and for the same reason under
+      // the storybook project's 15s `testTimeout`.
+      { timeout: 10000 },
+    );
+
     // Every section keeps a usable height: none is crushed to nothing by the
     // viewer's content, which is exactly what a content-sized basis did.
     const before = geometry();
@@ -483,12 +522,16 @@ export const LongSkillDocument: Story = {
     // still not settled when the click resolves: `userEvent.click` returns once
     // the event is dispatched, ahead of React committing the `openSections`
     // update, remounting the panel's content, and the browser running the
-    // layout pass that redistributes height across the flex sections. Sampling
-    // once lands mid-redistribution under load and fails intermittently
-    // (#2278), so read the geometry under `waitFor` and let it retry until the
-    // panes stop moving. The reopened layout only ever converges ON `before`
-    // rather than passing through it, so retrying cannot mask a real
-    // regression.
+    // layout pass that redistributes height across the flex sections. So read
+    // the geometry under `waitFor` and let it retry until the panes stop
+    // moving. The reopened layout only ever converges ON `before` rather than
+    // passing through it, so retrying cannot mask a real regression.
+    //
+    // ⚠️ This retry is not what fixed the intermittent `[…, 178, 262]` failure
+    // (#2278, then #2350): that was the Frontmatter highlighter landing between
+    // the two samples — a settled second layout for changed content, which no
+    // amount of retrying converges — and it is removed by the wait above, not
+    // by this one.
     await userEvent.click(viewerControl);
     await userEvent.click(viewerControl);
     await waitFor(async () => {