From 15835b29385be58dff8962d2d8727cd48a3fe0bc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 16:49:08 +0000 Subject: [PATCH] FE-2013: Wait for controls inside an opening `Collapse` to be fully shown before clicking A control inside a `Collapse` that is still opening is visible and stable to Playwright while the `Collapse` clips it. The click scrolls the clipping wrapper to reach the control, the wrapper scrolls back as it grows, and the pointer event lands beside the control. `expectFullyShown` waits until `toBeInViewport({ ratio: 1 })` passes, which happens only once no ancestor clips the element. The PDF and DOCX search tests, the parent-card remove button, and the sidebar entity type item now wait for it before clicking. The PDF search test also asserts that the result counter moves from "1 of 2" to "2 of 2". --- .../tests/features/entities-page.spec.ts | 3 ++- .../tests/features/entity-type-editing.spec.ts | 2 ++ .../tests/features/file-preview/docx.spec.ts | 11 +++++++---- .../tests/features/file-preview/pdf.spec.ts | 13 +++++++++---- .../tests/shared/expect-fully-shown.ts | 12 ++++++++++++ 5 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 tests/hash-playwright/tests/shared/expect-fully-shown.ts diff --git a/tests/hash-playwright/tests/features/entities-page.spec.ts b/tests/hash-playwright/tests/features/entities-page.spec.ts index ec46dfef933..3bb3de28a99 100644 --- a/tests/hash-playwright/tests/features/entities-page.spec.ts +++ b/tests/hash-playwright/tests/features/entities-page.spec.ts @@ -2,6 +2,7 @@ import { changeSidebarListDisplay, expandSidebarSection, } from "../shared/change-sidebar-list-display"; +import { expectFullyShown } from "../shared/expect-fully-shown"; import { expect, test } from "../shared/runtime"; test("user can visit a page listing entities of a type", async ({ page }) => { @@ -18,7 +19,7 @@ test("user can visit a page listing entities of a type", async ({ page }) => { const sidebar = page.getByTestId("page-sidebar"); const documentItem = sidebar.getByText("Document", { exact: true }); - await expect(documentItem).toBeVisible(); + await expectFullyShown(documentItem); await documentItem.click(); await page.waitForURL((url) => { diff --git a/tests/hash-playwright/tests/features/entity-type-editing.spec.ts b/tests/hash-playwright/tests/features/entity-type-editing.spec.ts index 44f2a6a71ef..74f733f5e90 100644 --- a/tests/hash-playwright/tests/features/entity-type-editing.spec.ts +++ b/tests/hash-playwright/tests/features/entity-type-editing.spec.ts @@ -13,6 +13,7 @@ import { rowMenuButton, saveChanges, } from "../shared/entity-type-editor"; +import { expectFullyShown } from "../shared/expect-fully-shown"; import { expect, test } from "../shared/runtime"; /** @@ -39,6 +40,7 @@ test("removing a type's only parent survives a reload", async ({ page }) => { await expect(parentCard(page, parentName)).toBeVisible(); await parentCard(page, parentName).hover(); + await expectFullyShown(removeParentButton(page, parentName)); await removeParentButton(page, parentName).click(); await expect(parentCard(page, parentName)).toHaveCount(0); diff --git a/tests/hash-playwright/tests/features/file-preview/docx.spec.ts b/tests/hash-playwright/tests/features/file-preview/docx.spec.ts index 978901b544b..eadba5906e0 100644 --- a/tests/hash-playwright/tests/features/file-preview/docx.spec.ts +++ b/tests/hash-playwright/tests/features/file-preview/docx.spec.ts @@ -1,3 +1,4 @@ +import { expectFullyShown } from "../../shared/expect-fully-shown"; import { generateHostileTestDocx, generateDocx, @@ -99,13 +100,15 @@ test.describe("DOCX preview", () => { page, }) => { await page.getByRole("button", { name: "Show search" }).click(); - await page - .getByPlaceholder("Search the document...") - .fill(testDocumentSearchWord); + const searchInput = page.getByPlaceholder("Search the document..."); + await expectFullyShown(searchInput); + await searchInput.fill(testDocumentSearchWord); await expect(page.getByText("1 of 2", { exact: true })).toBeVisible(); await expect(page.locator("[data-folio-selection-rect]")).toBeVisible(); - await page.getByRole("button", { name: "Next result" }).click(); + const nextResult = page.getByRole("button", { name: "Next result" }); + await expectFullyShown(nextResult); + await nextResult.click(); await expect(page.getByText("2 of 2", { exact: true })).toBeVisible(); await expect(getDocxPage(page, thirdPage.heading)).toBeInViewport(); await expect(page.locator("[data-folio-selection-rect]")).toBeVisible(); diff --git a/tests/hash-playwright/tests/features/file-preview/pdf.spec.ts b/tests/hash-playwright/tests/features/file-preview/pdf.spec.ts index 1972a8175a9..bcd6f89d0f1 100644 --- a/tests/hash-playwright/tests/features/file-preview/pdf.spec.ts +++ b/tests/hash-playwright/tests/features/file-preview/pdf.spec.ts @@ -1,3 +1,4 @@ +import { expectFullyShown } from "../../shared/expect-fully-shown"; import { generatePdf } from "../../shared/file-preview/generate-test-pdf"; import { testDocumentPages, @@ -56,17 +57,21 @@ test.describe("PDF preview", () => { const pdfPage = getPdfPage(page); await page.getByRole("button", { name: "Show search" }).click(); - await page - .getByPlaceholder("Search the document...") - .fill(testDocumentSearchWord); + const searchInput = page.getByPlaceholder("Search the document..."); + await expectFullyShown(searchInput); + await searchInput.fill(testDocumentSearchWord); const searchHighlight = pdfPage.locator('span[style*="border-bottom"]', { hasText: testDocumentSearchWord, }); await expect(searchHighlight).toBeVisible(); + await expect(page.getByText("1 of 2", { exact: true })).toBeVisible(); - await page.getByRole("button", { name: "Next result" }).click(); + const nextResult = page.getByRole("button", { name: "Next result" }); + await expectFullyShown(nextResult); + await nextResult.click(); + await expect(page.getByText("2 of 2", { exact: true })).toBeVisible(); await expect( pdfPage.getByText(thirdPage.heading, { exact: true }), ).toBeVisible(); diff --git a/tests/hash-playwright/tests/shared/expect-fully-shown.ts b/tests/hash-playwright/tests/shared/expect-fully-shown.ts new file mode 100644 index 00000000000..5266954b4bf --- /dev/null +++ b/tests/hash-playwright/tests/shared/expect-fully-shown.ts @@ -0,0 +1,12 @@ +import { expect } from "./runtime"; + +import type { Locator } from "./runtime"; + +/** + * Wait until no ancestor clips any part of the element, such as a `Collapse` + * that is still opening. Playwright clicks a clipped element by scrolling its + * clipping ancestor, which scrolls back as the ancestor grows and moves the + * element out from under the pointer before the click lands. + */ +export const expectFullyShown = (locator: Locator) => + expect(locator).toBeInViewport({ ratio: 1 });