test(ui): wait for the select popup before clicking its option

key_edit_view opened a select and then clicked the option it found by
title text or by raw text. Both queries match the moment the option
enters the DOM, which is one render before the popup finishes entering.
Until then the positioner still carries an inline pointer-events: none,
and user-event refuses to click through it.

That is a race, and a fast machine loses it. Five of the file's 84 tests
failed on every local run while CI stayed green, which is the worst shape
for a test to have: it is only ever red on the machine of whoever is
trying to change the code.

tests/test-utils.tsx already ships chooseSelectOption for exactly this.
It finds the option by role and waits for the positioner to release
pointer events before clicking. The five call sites now use it, and the
helper takes the direct user-event API as well as a setup() instance so
callers do not have to restructure to use it.

Five consecutive full-file runs pass where every previous run failed.

Also finishes this file's screen queries, which brings
prefer-screen-queries to its target of 18.
This commit is contained in:
Yuneng Jiang 2026-09-01 09:46:43 -07:00
parent c5ba2b5fcf
commit a5f9410681
No known key found for this signature in database
3 changed files with 14 additions and 21 deletions

View file

@ -7,5 +7,5 @@
"local/no-long-condition-chain": { "max": 265, "target": 120 },
"testing-library/no-container": { "max": 133, "target": 50 },
"testing-library/no-node-access": { "max": 716, "target": 500 },
"testing-library/prefer-screen-queries": { "max": 21, "target": 18 }
"testing-library/prefer-screen-queries": { "max": 18, "target": 18 }
}

View file

@ -1,7 +1,7 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../tests/test-utils";
import { chooseSelectOption, renderWithProviders } from "../../../tests/test-utils";
import { KeyResponse } from "../key_team_helpers/key_list";
import { MODEL_MAX_BUDGET_PREMIUM_HINT } from "../key_team_helpers/ModelMaxBudgetEditor";
import {
@ -300,7 +300,7 @@ describe("KeyEditView", () => {
});
it("should render", async () => {
const { getByText } = renderWithProviders(
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => {}}
@ -313,12 +313,12 @@ describe("KeyEditView", () => {
);
await waitFor(() => {
expect(getByText("Save Changes")).toBeInTheDocument();
expect(screen.getByText("Save Changes")).toBeInTheDocument();
});
});
it("should render tags", async () => {
const { getByText } = renderWithProviders(
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => {}}
@ -331,12 +331,12 @@ describe("KeyEditView", () => {
);
await waitFor(() => {
expect(getByText("test-tag")).toBeInTheDocument();
expect(screen.getByText("test-tag")).toBeInTheDocument();
});
});
it("should not render tags in metadata textarea", async () => {
const { getByLabelText } = renderWithProviders(
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => {}}
@ -348,7 +348,7 @@ describe("KeyEditView", () => {
/>,
);
const metadataTextarea = getByLabelText("Metadata") as HTMLTextAreaElement;
const metadataTextarea = screen.getByLabelText("Metadata") as HTMLTextAreaElement;
await waitFor(() => {
expect(metadataTextarea).toHaveValue("{}");
});
@ -963,10 +963,7 @@ describe("KeyEditView", () => {
/>,
);
await userEvent.click(await screen.findByLabelText("Reset Budget"));
const weeklyOption = await screen.findByText("weekly");
await userEvent.click(weeklyOption);
await chooseSelectOption(userEvent, await screen.findByLabelText("Reset Budget"), "weekly");
const submitButton = screen.getByRole("button", { name: /save changes/i });
await userEvent.click(submitButton);
@ -1042,8 +1039,7 @@ describe("KeyEditView", () => {
);
const resetBudget = await screen.findByLabelText("Reset Budget");
await userEvent.click(resetBudget);
await userEvent.click(await screen.findByText("Never resets"));
await chooseSelectOption(userEvent, resetBudget, "Never resets");
await waitFor(() => {
expect(resetBudget).toHaveTextContent("Never resets");
@ -1074,8 +1070,7 @@ describe("KeyEditView", () => {
/>,
);
await userEvent.click(await screen.findByLabelText("Reset Budget"));
await userEvent.click(await screen.findByText("Never resets"));
await chooseSelectOption(userEvent, await screen.findByLabelText("Reset Budget"), "Never resets");
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
@ -1946,8 +1941,7 @@ describe("KeyEditView", () => {
await userEvent.clear(duration);
await userEvent.type(duration, "45d");
await userEvent.click(screen.getByLabelText(/TPM Rate Limit Type/));
await userEvent.click(await screen.findByTitle("Guaranteed throughput"));
await chooseSelectOption(userEvent, screen.getByLabelText(/TPM Rate Limit Type/), /^Guaranteed throughput/);
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
@ -2103,8 +2097,7 @@ describe("KeyEditView", () => {
renderForPayload(onSubmitMock);
await screen.findByRole("button", { name: /save changes/i });
await userEvent.click(screen.getByLabelText(/RPM Rate Limit Type/));
await userEvent.click(await screen.findByTitle("Guaranteed throughput"));
await chooseSelectOption(userEvent, screen.getByLabelText(/RPM Rate Limit Type/), /^Guaranteed throughput/);
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));

View file

@ -52,7 +52,7 @@ const pointerBlocked = (element: HTMLElement): boolean => {
* the option text alone is a race that React 19's flush timing loses.
*/
export const chooseSelectOption = async (
user: ReturnType<typeof userEvent.setup>,
user: Pick<ReturnType<typeof userEvent.setup>, "click">,
trigger: HTMLElement,
optionName: string | RegExp,
) => {