fix(site/src): repair failing storybook interaction tests and pixel captures (#28417)

The Storybook suites fail on main in two independent ways: three
interaction tests in the locally-run Vitest suite (`pnpm vitest run
--project=storybook`, which does not run in CI), and three story
captures in CI's pixel snapshot job, whose failures do not fail the
GitHub check (the job PATCHes `status=failed` to pixel.coder.com and
still exits 0).

Vitest interaction failures:

* `ModelForm > Cost Estimate Shows Loading While Debounce Pending` has
been broken since it landed in #28298. In add mode the model identifier
field is the Known Model autocomplete, which only commits typed text to
`form.values.model` on option selection or dropdown close, so typing
free text left the form's model empty and the pricing section never
entered its loading state. The story now selects the catalog option.
This also fixes the same story's capture failure in the pixel job.
* The `SecretsPageView` import error stories raced the dialog enter
animation: `findByText` resolves on DOM presence, and under
parallel-suite CPU load `toBeVisible()` can run while the Radix dialog
content is still at `opacity: 0` from its `fade-in-0` animation. These
now poll with `waitFor(...toBeVisible())`, matching the file's other
import stories.

Pixel capture failures:

* `NavbarView > ForMCPCreateOnlyAdmin` / `ForMCPDeleteOnlyAdmin` inherit
the meta's tablet+desktop matrix but click the "Admin settings" button
that only exists in the desktop navbar, so their tablet captures always
fail. They now pin `pixelWithDesktop` like every other admin story in
the file.
* `AgentsPageLayout > OrganizationScopedMCPServers` fails intermittently
because `CompactOrgSelector` option names are timing-dependent: when an
org icon fails to load (as in the pixel static server), the avatar
fallback initials render and join the option's accessible name (`"MY My
Organization 2"`), breaking exact-name queries and polluting
screen-reader output. The decorative avatar is now `aria-hidden`, which
was verified red-green by forcing unloadable icons locally.

> [!NOTE]
> Shux (AI agent) authored this PR on behalf of @ibetitsmike.

<!-- shux-attribution: model=claude-opus-4-6 thinking=high -->
This commit is contained in:
Michael Suchacz
2026-08-21 08:19:17 +02:00
committed by GitHub
parent 05fcd06979
commit 58c1248cf3
4 changed files with 32 additions and 15 deletions
@@ -158,6 +158,7 @@ export const ForMCPUpdateOnlyAdmin: Story = {
export const ForMCPDeleteOnlyAdmin: Story = {
decorators: [withAuthProvider],
parameters: {
pixel: { matrix: pixelWithDesktop },
queries: [{ key: ["tasks", memberTasksFilter], data: [] }],
user: MockUserMember,
permissions: {
@@ -205,6 +206,7 @@ export const ForMCPDeleteOnlyAdmin: Story = {
export const ForMCPCreateOnlyAdmin: Story = {
decorators: [withAuthProvider],
parameters: {
pixel: { matrix: pixelWithDesktop },
queries: [{ key: ["tasks", memberTasksFilter], data: [] }],
user: MockUserMember,
permissions: {
@@ -636,9 +636,11 @@ export const CostEstimateDebouncesLivePriceLookup: Story = {
},
};
// On an entitled form the live lookup may override the catalog, so typing
// into an empty identifier shows the loading placeholder immediately rather
// than briefly flashing catalog prices before the lookup fires.
// On an entitled form the live lookup may override the catalog, so
// committing a catalog model shows the loading placeholder immediately
// rather than briefly flashing catalog prices before the lookup fires. In
// add mode the identifier autocomplete only commits the model when an
// option is selected, so select one instead of typing free text.
export const CostEstimateShowsLoadingWhileDebouncePending: Story = {
args: {
selectedProviderState: MockAnthropicProviderState,
@@ -653,7 +655,10 @@ export const CostEstimateShowsLoadingWhileDebouncePending: Story = {
);
await userEvent.type(
canvas.getByLabelText(/model identifier/i),
"claude-haiku-4-5",
"claude-haiku",
);
await userEvent.click(
await screen.findByRole("option", { name: /claude haiku 4\.5/i }),
);
await waitForPriceLoading(canvas);
// The catalog price must not render while the lookup is pending.
@@ -104,6 +104,10 @@ export const CompactOrgSelector: FC<CompactOrgSelectorProps> = ({
>
{" "}
<Avatar
// Decorative: without this the fallback initials join
// the option's accessible name once they render, making
// name queries and screen-reader output timing-dependent.
aria-hidden
size="sm"
src={org.icon}
fallback={org.display_name || org.name}
@@ -762,9 +762,11 @@ export const ImportSecretsParseError: Story = {
new File(["GOOD=1\nbad line"], "secrets.env"),
);
await expect(
await dialog.findByText("Failed to parse secrets file."),
).toBeVisible();
// findByText retries only DOM presence, so a one-shot toBeVisible can
// catch the dialog mid fade-in at opacity 0. Retry visibility itself.
await waitFor(() =>
expect(dialog.getByText("Failed to parse secrets file.")).toBeVisible(),
);
expect(dialog.getByText("Line 2 must contain KEY=VALUE.")).toBeVisible();
expect(dialog.queryByText("Response data")).not.toBeInTheDocument();
expect(dialog.queryByText("Stack Trace")).not.toBeInTheDocument();
@@ -785,9 +787,11 @@ export const ImportSecretsFileReadError: Story = {
new File(["A=1"], "secrets.env"),
);
await expect(
await dialog.findByText("Failed to read the selected file."),
).toBeVisible();
await waitFor(() =>
expect(
dialog.getByText("Failed to read the selected file."),
).toBeVisible(),
);
},
};
@@ -838,11 +842,13 @@ export const ImportSecretsRemoveAndRetry: Story = {
new File(["not a secret"], "bad.txt"),
);
await expect(
await dialog.findByText(
"Unsupported file type. Import a .env, .json, .yaml, or .yml file.",
),
).toBeVisible();
await waitFor(() =>
expect(
dialog.getByText(
"Unsupported file type. Import a .env, .json, .yaml, or .yml file.",
),
).toBeVisible(),
);
await user.click(dialog.getByRole("button", { name: "Remove file" }));
expect(
dialog.queryByText(