From ca731e24277daadc0fb5ec85785b8987df88f24b Mon Sep 17 00:00:00 2001 From: DavertMik Date: Thu, 10 Sep 2026 14:29:14 +0300 Subject: [PATCH] fix(Playwright): skip visibleLocator for scrollTo and grab* methods scrollTo must reach elements that scrolling reveals: scroll-reveal elements hidden with visibility:hidden, empty anchors and zero-height infinite-scroll sentinels. Playwright's visible() drops them, so scrollTo failed with "element not found". Off-screen and opacity:0 elements were never affected. grab* methods read hidden elements on purpose: csrf-token meta tags, hidden inputs, collapsed content. They skip the filter too, which also makes grabTextFrom consistent with the rest of the family. stepOpts({ visibleLocator: true }) still re-enables the filter for a single step. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Gaie9qVda6jpWHogaHfnHZ --- docs/helpers/Playwright.md | 2 +- lib/helper/Playwright.js | 7 ++++--- test/data/app/view/form/scroll.php | 2 ++ test/helper/Playwright_test.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md index b9855e61f..9c350b65e 100644 --- a/docs/helpers/Playwright.md +++ b/docs/helpers/Playwright.md @@ -78,7 +78,7 @@ Type: [object][6] * `ignoreHTTPSErrors` **[boolean][27]?** Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false` * `bypassCSP` **[boolean][27]?** bypass Content Security Policy or CSP * `highlightElement` **[boolean][27]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose). -* `visibleLocator` **[boolean][27]?** append [`visible()`][49] to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`, which check the DOM regardless of visibility. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found. +* `visibleLocator` **[boolean][27]?** append [`visible()`][49] to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to steps that must reach hidden elements: `grab*` methods, `scrollTo`, `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found. * `recordHar` **[object][6]?** record HAR and will be saved to `output/har`. See more of [HAR options][3]. * `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][50]. * `storageState` **([string][9] | [object][6])?** Playwright storage state (path to JSON file or object) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 33399fdef..99b89bec4 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -50,7 +50,7 @@ let defaultSelectorEnginesInitialized = false const popupStore = new Popup() const consoleLogStore = new Console() const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron'] -const domPresenceSteps = ['seeElementInDOM', 'dontSeeElementInDOM', 'seeNumberOfElements'] +const visibilityAgnosticSteps = ['seeElementInDOM', 'dontSeeElementInDOM', 'seeNumberOfElements', 'scrollTo'] const checkableRoles = ['checkbox', 'radio', 'switch'] import { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } from './extras/PlaywrightRestartOpts.js' @@ -103,7 +103,7 @@ const pathSeparator = path.sep * @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false` * @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose). - * @prop {boolean} [visibleLocator=false] - append [`visible()`](https://playwright.dev/docs/api/class-locator#locator-visible) to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`, which check the DOM regardless of visibility. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found. + * @prop {boolean} [visibleLocator=false] - append [`visible()`](https://playwright.dev/docs/api/class-locator#locator-visible) to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to steps that must reach hidden elements: `grab*` methods, `scrollTo`, `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found. * @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har). * @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id). * @prop {string|object} [storageState] - Playwright storage state (path to JSON file or object) @@ -559,7 +559,8 @@ class Playwright extends Helper { } _beforeStep(step) { - store.visibleLocator = step.opts?.visibleLocator ?? (this.options.visibleLocator && !domPresenceSteps.includes(step.helperMethod)) + const reachesHidden = step.helperMethod?.startsWith('grab') || visibilityAgnosticSteps.includes(step.helperMethod) + store.visibleLocator = step.opts?.visibleLocator ?? (this.options.visibleLocator && !reachesHidden) } async _before(test) { diff --git a/test/data/app/view/form/scroll.php b/test/data/app/view/form/scroll.php index b70cbcf34..9d5ca423e 100644 --- a/test/data/app/view/form/scroll.php +++ b/test/data/app/view/form/scroll.php @@ -42,6 +42,8 @@ + + diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index 21bd02deb..b312b8591 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -219,6 +219,36 @@ describe('Playwright', function () { await I.dontSeeElementInDOM({ css: 'button[data-missing]' }) }) + it('should scroll to elements revealed by scrolling', async () => { + await I.amOnPage('/form/scroll') + await I.resizeWindow(500, 700) + I.options.visibleLocator = true + step('scrollTo') + + await I.scrollTo('#reveal_on_scroll') + const { y } = await I.grabPageScrollPosition() + assert.notEqual(y, 0) + + await I.scrollPageToTop() + await I.scrollTo('#scroll_anchor') + const { y: anchorY } = await I.grabPageScrollPosition() + assert.notEqual(anchorY, 0) + }) + + it('should grab from hidden elements', async () => { + I.options.visibleLocator = true + + await I.amOnPage('/form/hidden') + step('grabValueFrom') + expect(await I.grabValueFrom('#action')).to.equal('kill_people') + step('grabAttributeFrom') + expect(await I.grabAttributeFrom('#action', 'name')).to.equal('action') + + await I.amOnPage('/invisible_elements') + step('grabHTMLFrom') + expect(await I.grabHTMLFrom('button[style]')).to.equal('Hello World') + }) + it('should apply to playwright locators', async () => { await I.amOnPage('/invisible_elements') I.options.visibleLocator = true