From 4286f10b880208488d47cc920a167c0e81df4c38 Mon Sep 17 00:00:00 2001 From: nihal467 Date: Thu, 16 Jul 2026 12:21:40 +0530 Subject: [PATCH] playwright: document overlapping Radix overlay close-animation race Radix Dialog/Sheet/Popover keep content mounted during their close animation, so opening a second overlay right after closing the first makes a shared locator match both elements (strict-mode 'resolved to 2 elements'). Add a flaky-triage entry (SKILL.md), a Common Pitfall, and a Pattern Gallery snippet showing the toBeHidden() wait and single-dialog getByRole scoping. Learned while fixing the flaky device service history edit test (care_fe QA-214). --- playwright/PLAYWRIGHT_GUIDE.md | 30 ++++++++++++++++++++++++++++-- playwright/SKILL.md | 6 +++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/playwright/PLAYWRIGHT_GUIDE.md b/playwright/PLAYWRIGHT_GUIDE.md index a0eb716..2fef42a 100644 --- a/playwright/PLAYWRIGHT_GUIDE.md +++ b/playwright/PLAYWRIGHT_GUIDE.md @@ -592,6 +592,27 @@ import { closeAnyOpenPopovers } from "tests/helper/ui"; await closeAnyOpenPopovers(page); ``` +### Wait for a Closing Overlay Before Opening the Next + +Radix `Dialog`/`Sheet`/`Popover` stay mounted during their close animation. If you open a +second overlay that shares controls with the first (e.g. an Add sheet and an Edit sheet that +both render a "Service Date" picker), a shared locator matches **both** → strict-mode +"resolved to 2 elements". Wait for the first to fully unmount before touching the next: + +```typescript +// After saving in the "Add" sheet, wait for it to close before opening "Edit". +await page.getByRole("button", { name: "Save" }).click(); +await expect(page.getByRole("button", { name: "Save" })).toBeHidden(); + +await page.getByRole("button", { name: "Edit" }).click(); + +// Now the edit sheet is the only dialog mounted, so this scopes unambiguously +// (and avoids a hard-coded, locale-dependent dialog title). +const editSheet = page.getByRole("dialog"); +await expect(editSheet).toBeVisible(); +await editSheet.getByRole("textbox", { name: "Notes" }).fill(notes); +``` + ### File Upload & Camera ```typescript @@ -924,8 +945,13 @@ component tests with no single route may live in a `facility/components/` dir. polling/websockets and never resolves on pages with background activity. Use it only as a documented last resort. (This is Critical Rule #10 in `SKILL.md`.) 8. **Non-camelCase directory names** — see naming conventions above. - -# Available Constants +9. **Overlapping Radix overlays during close animations** — Radix `Dialog`/`Sheet`/`Popover` + keep their content mounted while they animate closed. Opening a second overlay (or + clicking a control) right after closing the first makes a shared locator match **both** + → strict-mode "resolved to 2 elements". Assert a control unique to the closing overlay is + `toBeHidden()` before interacting with the next one. `getByRole("dialog")` is a good way + to drop a hard-coded (locale-dependent) title, but it is only unambiguous once exactly + one dialog is mounted — so wait for the previous one to unmount first. ```typescript import { BODY_SITES, KNOWN_USERNAMES } from "tests/helper/commonConstants"; diff --git a/playwright/SKILL.md b/playwright/SKILL.md index 2ea41b7..03b7495 100644 --- a/playwright/SKILL.md +++ b/playwright/SKILL.md @@ -184,4 +184,8 @@ Fail → check report → `--headed` → `--debug` → if data looks stale, `npm **Flaky triage:** race (element before data) → wait for response/text, not `networkidle` · animation → `waitFor({ state: "visible" })` first · parallel collision → `Date.now()` suffix · -stale DB → `db-restore` · polling/WebSocket → wait for a specific DOM change. +stale DB → `db-restore` · polling/WebSocket → wait for a specific DOM change · +overlapping Radix overlays (a dialog/sheet/popover lingers in the DOM during its close +animation → strict-mode "resolved to 2 elements") → assert a control unique to the closing +overlay is `toBeHidden()` **before** opening the next; `getByRole("dialog")` is only +unambiguous once a single dialog is mounted.