From 62a5eac18682aeb382cb1148fe60726840120c0b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 20 Oct 2026 08:37:57 +0200 Subject: [PATCH] test(e2e): keep developer toasts out of concurrent app assertions Concurrent layout measurements could invoke the same Playwright locator handler before its first dismissal finished, leaving checks waiting for a removed button. Hide only the Expo toast host through test styling while retaining console error capture. --- .../e2e/browser/sidebar-nav-settings.spec.ts | 23 +++++++++++++++++ packages/app/e2e/support/developer-ui.mjs | 25 +++++++++++++------ packages/app/e2e/support/fixtures.ts | 4 +-- packages/desktop/e2e/browser-tabs.e2e.mjs | 4 +-- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/packages/app/e2e/browser/sidebar-nav-settings.spec.ts b/packages/app/e2e/browser/sidebar-nav-settings.spec.ts index a17480b740..177ddd563e 100644 --- a/packages/app/e2e/browser/sidebar-nav-settings.spec.ts +++ b/packages/app/e2e/browser/sidebar-nav-settings.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from "../support/fixtures"; +import type { Page } from "@playwright/test"; import { openAddProjectFlow, addProjectFlow } from "../support/helpers/add-project-flow"; import { installUsageReportsFixture } from "../support/helpers/usage-reports"; import { @@ -31,6 +32,28 @@ import { setSidebarNavItemVisible, } from "../support/helpers/sidebar-nav-settings"; +async function showDeveloperToast(page: Page): Promise { + await page.evaluate(() => { + document.getElementById("error-toast")?.remove(); + const toast = document.createElement("div"); + toast.id = "error-toast"; + toast.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:white"; + // Expo renders its unnamed dismiss button in an open shadow root. + const button = document.createElement("button"); + button.style.cssText = "width:24px;height:24px"; + button.addEventListener("click", () => toast.remove()); + toast.attachShadow({ mode: "open" }).appendChild(button); + document.body.appendChild(toast); + }); +} + +test("developer toasts leave concurrent sidebar layout checks uninterrupted", async ({ page }) => { + await gotoAppShell(page); + await showDeveloperToast(page); + await expectSidebarOrder(page, ["new-workspace", "history", "search", "schedules"]); + await expect(page.locator("#error-toast")).toBeHidden(); +}); + test("fixed footer line keeps its five icons, Help and Settings at the end", async ({ page }) => { test.setTimeout(120_000); await installUsageReportsFixture(page, { lists: [() => claudeAndCodexReports()] }); diff --git a/packages/app/e2e/support/developer-ui.mjs b/packages/app/e2e/support/developer-ui.mjs index 36d5752688..3f71e8e314 100644 --- a/packages/app/e2e/support/developer-ui.mjs +++ b/packages/app/e2e/support/developer-ui.mjs @@ -1,10 +1,21 @@ /** @param {import('@playwright/test').Page} page */ -export async function installDeveloperToastDismissal(page) { - // Developer chrome must not cover app actions or move the test's pointer/focus. +export async function hideDeveloperToasts(page) { + // Keep developer chrome out of app interactions without Playwright locator + // handlers: parallel app assertions can race while dismissing the same toast. // Console errors remain in the runner's logs. - const dismiss = page.locator("#error-toast").getByRole("button", { name: "", exact: true }); - await page.addLocatorHandler(dismiss, () => dismiss.dispatchEvent("click"), { - // A new logged error can replace the toast before the old one disappears. - noWaitAfter: true, - }); + function hideToasts() { + const installStyle = () => { + const style = document.createElement("style"); + style.textContent = "#error-toast { display: none !important; }"; + document.head.appendChild(style); + }; + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", installStyle, { once: true }); + } else { + installStyle(); + } + } + await page.addInitScript(hideToasts); + // Electron connects to an already loaded page; apply there as well. + await page.evaluate(hideToasts); } diff --git a/packages/app/e2e/support/fixtures.ts b/packages/app/e2e/support/fixtures.ts index aa83df8e91..c621f43fce 100644 --- a/packages/app/e2e/support/fixtures.ts +++ b/packages/app/e2e/support/fixtures.ts @@ -1,5 +1,5 @@ import { test as base, expect, type Page } from "@playwright/test"; -import { installDeveloperToastDismissal } from "./developer-ui.mjs"; +import { hideDeveloperToasts } from "./developer-ui.mjs"; import { startOutdatedDaemon, type OutdatedDaemon } from "./helpers/daemon-update"; import { startE2EWorker } from "./helpers/e2e-worker"; import { getE2EDaemonPort } from "./helpers/daemon-port"; @@ -131,7 +131,7 @@ const test = daemonTest.extend<{ await ws.close({ code: 1008, reason: "Blocked connection to localhost:6767 during e2e." }); }); - await installDeveloperToastDismissal(page); + await hideDeveloperToasts(page); const entries: string[] = []; diff --git a/packages/desktop/e2e/browser-tabs.e2e.mjs b/packages/desktop/e2e/browser-tabs.e2e.mjs index af142e6385..d403bfdfc3 100644 --- a/packages/desktop/e2e/browser-tabs.e2e.mjs +++ b/packages/desktop/e2e/browser-tabs.e2e.mjs @@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url"; import { experimental_createMCPClient } from "ai"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { chromium } from "playwright"; -import { installDeveloperToastDismissal } from "../../app/e2e/support/developer-ui.mjs"; +import { hideDeveloperToasts } from "../../app/e2e/support/developer-ui.mjs"; import { runAppearanceFontSizeRegression } from "./appearance-font-size.electron.mjs"; import { runSettingsMemoryRegression } from "./settings-memory.electron.mjs"; @@ -1189,7 +1189,7 @@ async function main() { browser = await chromium.connectOverCDP(`http://127.0.0.1:${cdpPort}`); const page = await waitForAppPage(browser, expoPort); - await installDeveloperToastDismissal(page); + await hideDeveloperToasts(page); const status = await waitForDesktopStatus(page); const checkPluginLinks = () =>