From 593fa5921a0c0043fe718568983895764f289bba Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 17 Sep 2026 21:39:22 -0700 Subject: [PATCH] test(e2e/ui): wait for the filtered budget list before clicking a row action All three budget specs searched by typing into the search box and moved on immediately. The search is debounced 300ms, and while the filtered query is in flight react-query serves the previous page as placeholder data, which the list hook reports as isLoading, which makes the table swap its whole body for skeleton rows. So the row assertion passed against the pre-search rows, and roughly 300ms later the skeleton swap unmounted the row the spec had just opened the action menu on. Playwright logged "element is not stable" twice and then "element was detached from the DOM", and since the menu never reopened the click burned the full 15s action timeout on all three attempts. Losing that race was pure timing: build 386 and build 387 of the UI suite ran the same commit 4b368bf0669c, and 386 passed where 387 failed on this spec plus "Delete a budget" searchForBudget now waits for the GET that carries q=, matching what projectDetachment.spec.ts already does for a key search. That also gives the row assertion something real to assert, since until now it could pass without the search having filtered anything --- tests/e2e/ui/tests/budgets/budgets.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/e2e/ui/tests/budgets/budgets.spec.ts b/tests/e2e/ui/tests/budgets/budgets.spec.ts index 1ad1e488d25..89691c05605 100644 --- a/tests/e2e/ui/tests/budgets/budgets.spec.ts +++ b/tests/e2e/ui/tests/budgets/budgets.spec.ts @@ -4,6 +4,8 @@ import { Page } from "../../fixtures/pages"; import { navigateToPage, dismissFeedbackPopup } from "../../helpers/navigation"; import { masterKey } from "../../helpers/traffic"; +const BUDGET_LIST_PATH = "/management/v1/budgets"; + interface StoredBudget { budget_id: string; max_budget: number | null; @@ -30,7 +32,17 @@ async function createBudgetViaApi(page: PlaywrightPage, budget: Partial { + const searched = page.waitForResponse((response) => { + const url = new URL(response.url()); + return ( + response.request().method() === "GET" && + url.pathname === BUDGET_LIST_PATH && + url.searchParams.get("q") === budgetId + ); + }); await page.getByPlaceholder("Search by budget ID").fill(budgetId); + const response = await searched; + expect(response.ok(), `GET ${BUDGET_LIST_PATH}?q=${budgetId} (${response.status()})`).toBe(true); } test.describe("Budgets", () => {