diff --git a/packages/app/e2e/browser/developer-ui.spec.ts b/packages/app/e2e/browser/developer-ui.spec.ts
new file mode 100644
index 0000000000..7ea2fc2c95
--- /dev/null
+++ b/packages/app/e2e/browser/developer-ui.spec.ts
@@ -0,0 +1,58 @@
+import { metroTest as test, expect } from "../support/fixtures";
+import { installDeveloperToastDismissal } from "../support/developer-ui.mjs";
+import type { Page } from "@playwright/test";
+
+async function showTransientDeveloperToast(page: Page, shadow: boolean) {
+ await page.evaluate((useShadow) => {
+ const toast = document.createElement("div");
+ toast.id = "error-toast";
+ document.body.append(toast);
+ const root = useShadow ? toast.attachShadow({ mode: "open" }) : toast;
+ const button = document.createElement("button");
+ button.style.cssText = "width:32px;height:32px";
+ button.addEventListener("click", () => button.remove());
+ root.append(button);
+ }, shadow);
+}
+
+async function openTransientDeveloperToast(page: Page) {
+ await page.setContent("");
+ await showTransientDeveloperToast(page, false);
+ await installDeveloperToastDismissal(page);
+}
+
+async function navigateWithDeveloperToastDismissal(page: Page) {
+ await installDeveloperToastDismissal(page);
+ await page.goto("data:text/html,");
+ await showTransientDeveloperToast(page, true);
+}
+
+async function inspectLayoutWhileToastDisappears(page: Page) {
+ const action = page.getByRole("button", { name: "Continue", exact: true });
+ const boxes = await Promise.all(Array.from({ length: 4 }, () => action.boundingBox()));
+ for (const box of boxes) expect(box).not.toBeNull();
+ await expect(page.locator("#error-toast").getByRole("button")).toHaveCount(0);
+ await action.click();
+}
+
+test("a disappearing developer toast does not stall concurrent layout checks", async ({ page }) => {
+ test.setTimeout(10000);
+ await openTransientDeveloperToast(page);
+ await inspectLayoutWhileToastDisappears(page);
+});
+
+test("developer toast dismissal survives navigation and observes Expo shadow-root updates", async ({
+ page,
+}) => {
+ test.setTimeout(10000);
+ await navigateWithDeveloperToastDismissal(page);
+ await inspectLayoutWhileToastDisappears(page);
+ await page.evaluate(() => {
+ const root = document.getElementById("error-toast")!.shadowRoot!;
+ const replacement = document.createElement("button");
+ replacement.style.cssText = "width:32px;height:32px";
+ replacement.addEventListener("click", () => replacement.remove());
+ root.append(replacement);
+ });
+ await inspectLayoutWhileToastDisappears(page);
+});
diff --git a/packages/app/e2e/support/developer-ui.mjs b/packages/app/e2e/support/developer-ui.mjs
index 36d5752688..456f0d7088 100644
--- a/packages/app/e2e/support/developer-ui.mjs
+++ b/packages/app/e2e/support/developer-ui.mjs
@@ -1,10 +1,27 @@
/** @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.
- // 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,
- });
+ // Keep developer chrome out of app actions without pointer/focus changes.
+ // Locator handlers can deadlock when concurrent checks dismiss the same toast.
+ const install = () => {
+ let observedRoot;
+ const options = { childList: true, subtree: true };
+ const dismiss = () => {
+ const toast = document.getElementById("error-toast");
+ const root = toast?.shadowRoot ?? toast;
+ if (root && root !== observedRoot) {
+ observer.disconnect();
+ observer.observe(document, options);
+ observer.observe(root, options);
+ observedRoot = root;
+ }
+ // Expo's toast root contains dismiss buttons; its overlay has a different ID.
+ for (const button of root?.querySelectorAll("button") ?? [])
+ button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ };
+ const observer = new MutationObserver(dismiss);
+ observer.observe(document, options);
+ dismiss();
+ };
+ await page.addInitScript(install);
+ await page.evaluate(install);
}