Skip to content

Commit ca731e2

Browse files
DavertMikclaude
andcommitted
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gaie9qVda6jpWHogaHfnHZ
1 parent b9a7366 commit ca731e2

4 files changed

Lines changed: 37 additions & 4 deletions

File tree

docs/helpers/Playwright.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Type: [object][6]
7878
* `ignoreHTTPSErrors` **[boolean][27]?** Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
7979
* `bypassCSP` **[boolean][27]?** bypass Content Security Policy or CSP
8080
* `highlightElement` **[boolean][27]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
81-
* `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.
81+
* `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.
8282
* `recordHar` **[object][6]?** record HAR and will be saved to `output/har`. See more of [HAR options][3].
8383
* `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][50].
8484
* `storageState` **([string][9] | [object][6])?** Playwright storage state (path to JSON file or object)

lib/helper/Playwright.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ let defaultSelectorEnginesInitialized = false
5050
const popupStore = new Popup()
5151
const consoleLogStore = new Console()
5252
const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
53-
const domPresenceSteps = ['seeElementInDOM', 'dontSeeElementInDOM', 'seeNumberOfElements']
53+
const visibilityAgnosticSteps = ['seeElementInDOM', 'dontSeeElementInDOM', 'seeNumberOfElements', 'scrollTo']
5454
const checkableRoles = ['checkbox', 'radio', 'switch']
5555

5656
import { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } from './extras/PlaywrightRestartOpts.js'
@@ -103,7 +103,7 @@ const pathSeparator = path.sep
103103
* @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
104104
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
105105
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
106-
* @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.
106+
* @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.
107107
* @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).
108108
* @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).
109109
* @prop {string|object} [storageState] - Playwright storage state (path to JSON file or object)
@@ -559,7 +559,8 @@ class Playwright extends Helper {
559559
}
560560

561561
_beforeStep(step) {
562-
store.visibleLocator = step.opts?.visibleLocator ?? (this.options.visibleLocator && !domPresenceSteps.includes(step.helperMethod))
562+
const reachesHidden = step.helperMethod?.startsWith('grab') || visibilityAgnosticSteps.includes(step.helperMethod)
563+
store.visibleLocator = step.opts?.visibleLocator ?? (this.options.visibleLocator && !reachesHidden)
563564
}
564565

565566
async _before(test) {

test/data/app/view/form/scroll.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
<button type="submit" name="button4" value="fourth">A Submit Button</button>
4343
</form>
4444
</div>
45+
<div id="reveal_on_scroll" style="visibility: hidden">Revealed on scroll</div>
46+
<a id="scroll_anchor"></a>
4547
<script>
4648
4749
</script>

test/helper/Playwright_test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,36 @@ describe('Playwright', function () {
219219
await I.dontSeeElementInDOM({ css: 'button[data-missing]' })
220220
})
221221

222+
it('should scroll to elements revealed by scrolling', async () => {
223+
await I.amOnPage('/form/scroll')
224+
await I.resizeWindow(500, 700)
225+
I.options.visibleLocator = true
226+
step('scrollTo')
227+
228+
await I.scrollTo('#reveal_on_scroll')
229+
const { y } = await I.grabPageScrollPosition()
230+
assert.notEqual(y, 0)
231+
232+
await I.scrollPageToTop()
233+
await I.scrollTo('#scroll_anchor')
234+
const { y: anchorY } = await I.grabPageScrollPosition()
235+
assert.notEqual(anchorY, 0)
236+
})
237+
238+
it('should grab from hidden elements', async () => {
239+
I.options.visibleLocator = true
240+
241+
await I.amOnPage('/form/hidden')
242+
step('grabValueFrom')
243+
expect(await I.grabValueFrom('#action')).to.equal('kill_people')
244+
step('grabAttributeFrom')
245+
expect(await I.grabAttributeFrom('#action', 'name')).to.equal('action')
246+
247+
await I.amOnPage('/invisible_elements')
248+
step('grabHTMLFrom')
249+
expect(await I.grabHTMLFrom('button[style]')).to.equal('Hello World')
250+
})
251+
222252
it('should apply to playwright locators', async () => {
223253
await I.amOnPage('/invisible_elements')
224254
I.options.visibleLocator = true

0 commit comments

Comments
 (0)