From 82c995a4c99bf10b5dd6fc40d88c1b971d8a433a Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Mon, 10 Aug 2026 17:22:06 +0200 Subject: [PATCH] test(Playwright): improve `clickDropdownItemByText` --- playwright/fixtures/ui5-fixtures.ts | 50 +++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/playwright/fixtures/ui5-fixtures.ts b/playwright/fixtures/ui5-fixtures.ts index fbe21fa734d..8b41f11700e 100644 --- a/playwright/fixtures/ui5-fixtures.ts +++ b/playwright/fixtures/ui5-fixtures.ts @@ -5,6 +5,17 @@ export interface UI5WCFixtures { ui5wc: UI5WCHelpers; } +/** + * Escapes a value for use inside a double-quoted CSS attribute selector + * (`[attr=""]`). + */ +function escapeAttributeValue(value: string): string { + // `CSS.escape` is a browser-only global; these selector strings are built in + // the Node test process, where `CSS` is undefined. Only `\` and `"` can break + // out of a double-quoted attribute value, so escaping them is sufficient. + return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + export class UI5WCHelpers { constructor(protected page: Page) {} @@ -78,13 +89,38 @@ export class UI5WCHelpers { await expect(dropdown.locator('[ui5-responsive-popover]:not([tokenizer-popover])')).toHaveAttribute('open'); const isSelect = await dropdown.evaluate((el) => el.hasAttribute('ui5-select')); - if (isSelect) { - const item = dropdown.getByText(text, { exact: true }); - await item.click(); - } else { - const item = dropdown.locator(`[text="${text}"]`); - await item.click(); - } + // Select renders its options as plain text nodes; ComboBox/MultiComboBox + // items carry the value in a `text` attribute. escapeAttributeValue guards + // the attribute value so quotes/backslashes in the text cannot break the selector. + const item = isSelect + ? dropdown.getByText(text, { exact: true }).first() + : dropdown.locator(`[text="${escapeAttributeValue(text)}"]`).first(); + await this.clickAtBoundingBoxCenter(item); + } + + /** + * Clicks a locator using raw viewport coordinates at its bounding-box center. + * + * Waits until the located element has a bounding box with a height > 0, then + * clicks its center. Unlike a normal `locator.click()`, this does not run + * Playwright's "receives events" actionability check — which UI5 Web Components + * can fail when their real target sits in the shadow DOM behind an event- + * transparent host. + * + * @param locator Locator to click. + */ + protected async clickAtBoundingBoxCenter(locator: Locator): Promise { + let boundingBox: NonNullable>> | null = null; + await expect + .poll(async () => { + const current = await locator.boundingBox(); + boundingBox = current && current.height > 0 ? current : null; + return boundingBox?.height ?? 0; + }) + .toBeGreaterThan(0); + const centerX = boundingBox!.x + boundingBox!.width / 2; + const centerY = boundingBox!.y + boundingBox!.height / 2; + await this.page.mouse.click(centerX, centerY); } /**