From c47f96a5d0ef59197fb9f44eb329d3ce25f5b48c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Jun 2026 11:12:40 -0700 Subject: [PATCH 1/7] chore: revert bogus keyboard navigability attempts (#41432) --- packages/html-reporter/src/tabbedPane.tsx | 13 +---- .../trace-viewer/src/ui/networkFilters.tsx | 18 +------ packages/web/src/components/tabbedPane.tsx | 17 ++----- packages/web/src/uiUtils.ts | 21 -------- .../ui-mode-test-network-tab.spec.ts | 48 ------------------- 5 files changed, 7 insertions(+), 110 deletions(-) diff --git a/packages/html-reporter/src/tabbedPane.tsx b/packages/html-reporter/src/tabbedPane.tsx index a2e8fab51cab2..a1e533348b873 100644 --- a/packages/html-reporter/src/tabbedPane.tsx +++ b/packages/html-reporter/src/tabbedPane.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { clsx, handleTabListKeyDown } from '@web/uiUtils'; +import { clsx } from '@web/uiUtils'; import './colors.css'; import './tabbedPane.css'; import * as React from 'react'; @@ -32,25 +32,16 @@ export const TabbedPane: React.FunctionComponent<{ setSelectedTab: (tab: string) => void }> = ({ tabs, selectedTab, setSelectedTab }) => { const idPrefix = React.useId(); - const tabStripRef = React.useRef(null); - - const handleKeyDown = (e: React.KeyboardEvent) => { - const nextIndex = handleTabListKeyDown(e, tabStripRef.current); - if (nextIndex !== -1) - setSelectedTab(tabs[nextIndex].id); - }; - return
-
{ +
{ tabs.map(tab => (
setSelectedTab(tab.id)} id={`${idPrefix}-${tab.id}`} key={tab.id} role='tab' - tabIndex={selectedTab === tab.id ? 0 : -1} aria-selected={selectedTab === tab.id}>
{tab.title}
diff --git a/packages/trace-viewer/src/ui/networkFilters.tsx b/packages/trace-viewer/src/ui/networkFilters.tsx index df6fcbca58aeb..df0f0b9e7307a 100644 --- a/packages/trace-viewer/src/ui/networkFilters.tsx +++ b/packages/trace-viewer/src/ui/networkFilters.tsx @@ -14,8 +14,6 @@ * limitations under the License. */ -import * as React from 'react'; -import { handleTabListKeyDown } from '@web/uiUtils'; import './networkFilters.css'; const resourceTypes = ['Fetch', 'HTML', 'JS', 'CSS', 'Font', 'Image', 'WS'] as const; @@ -32,14 +30,6 @@ export const NetworkFilters = ({ filterState, onFilterStateChange }: { filterState: FilterState, onFilterStateChange: (filterState: FilterState) => void, }) => { - const tabListRef = React.useRef(null); - - const handleKeyDown = (e: React.KeyboardEvent) => { - handleTabListKeyDown(e, tabListRef.current); - }; - - const isAllSelected = filterState.resourceTypes.size === 0; - return (
onFilterStateChange({ ...filterState, searchValue: e.target.value })} /> -
+
onFilterStateChange({ ...filterState, resourceTypes: new Set() })} - className={`network-filters-resource-type ${isAllSelected ? 'selected' : ''}`} - role='tab' - tabIndex={isAllSelected ? 0 : -1} - aria-selected={isAllSelected} + className={`network-filters-resource-type ${filterState.resourceTypes.size === 0 ? 'selected' : ''}`} > All
@@ -77,7 +64,6 @@ export const NetworkFilters = ({ filterState, onFilterStateChange }: { }} className={`network-filters-resource-type ${filterState.resourceTypes.has(resourceType) ? 'selected' : ''}`} role='tab' - tabIndex={filterState.resourceTypes.has(resourceType) ? 0 : -1} aria-selected={filterState.resourceTypes.has(resourceType)} > {resourceType} diff --git a/packages/web/src/components/tabbedPane.tsx b/packages/web/src/components/tabbedPane.tsx index b0084b4abc6d5..a031cdaa077ba 100644 --- a/packages/web/src/components/tabbedPane.tsx +++ b/packages/web/src/components/tabbedPane.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { clsx, handleTabListKeyDown } from '../uiUtils'; +import { clsx } from '../uiUtils'; import './tabbedPane.css'; import { Toolbar } from './toolbar'; import * as React from 'react'; @@ -38,25 +38,17 @@ export const TabbedPane: React.FunctionComponent<{ mode?: 'default' | 'select', }> = ({ tabs, selectedTab, setSelectedTab, leftToolbar, rightToolbar, dataTestId, mode }) => { const id = React.useId(); - const tabListRef = React.useRef(null); if (!selectedTab) selectedTab = tabs[0].id; if (!mode) mode = 'default'; - - const handleKeyDown = (e: React.KeyboardEvent) => { - const nextIndex = handleTabListKeyDown(e, tabListRef.current); - if (nextIndex !== -1) - setSelectedTab?.(tabs[nextIndex].id); - }; - return
{ leftToolbar &&
{...leftToolbar}
} - {mode === 'default' &&
+ {mode === 'default' &&
{[...tabs.map(tab => ( )), ]}
} @@ -110,13 +101,11 @@ export const TabbedPaneTab: React.FunctionComponent<{ selected?: boolean, onSelect?: (id: string) => void, ariaControls?: string, - tabIndex?: number, -}> = ({ id, title, count, errorCount, selected, onSelect, ariaControls, tabIndex }) => { +}> = ({ id, title, count, errorCount, selected, onSelect, ariaControls }) => { return
onSelect?.(id)} role='tab' title={title} - tabIndex={tabIndex ?? (selected ? 0 : -1)} aria-controls={ariaControls} aria-selected={selected}>
{title}
diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts index bd2c73344e8e8..48b68f91e0a8a 100644 --- a/packages/web/src/uiUtils.ts +++ b/packages/web/src/uiUtils.ts @@ -210,27 +210,6 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) { element?.scrollIntoView(); } -export function handleTabListKeyDown(e: React.KeyboardEvent, tabListElement: HTMLElement | null): number { - const tabElements = Array.from(tabListElement?.querySelectorAll('[role="tab"]') ?? []) as HTMLElement[]; - const currentIndex = tabElements.findIndex(el => el === document.activeElement); - if (currentIndex === -1) - return -1; - let nextIndex = currentIndex; - if (e.key === 'ArrowRight') - nextIndex = (currentIndex + 1) % tabElements.length; - else if (e.key === 'ArrowLeft') - nextIndex = (currentIndex - 1 + tabElements.length) % tabElements.length; - else if (e.key === 'Home') - nextIndex = 0; - else if (e.key === 'End') - nextIndex = tabElements.length - 1; - else - return -1; - e.preventDefault(); - tabElements[nextIndex].focus(); - return nextIndex; -} - const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f'; export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug'); diff --git a/tests/playwright-test/ui-mode-test-network-tab.spec.ts b/tests/playwright-test/ui-mode-test-network-tab.spec.ts index 77225d56f41ad..406336e75cae2 100644 --- a/tests/playwright-test/ui-mode-test-network-tab.spec.ts +++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts @@ -478,51 +478,3 @@ test('should preserve selection during test run', async ({ runUITest, server }, await page.waitForTimeout(1000); await expect(headersPanel).toBeVisible(); }); - -test('should support keyboard navigation for resource type filters', async ({ runUITest, server }) => { - server.setRoute('/api/endpoint', (_, res) => res.setHeader('Content-Type', 'application/json').end()); - - const { page } = await runUITest({ - 'network-tab.test.ts': ` - import { test, expect } from '@playwright/test'; - test('network tab test', async ({ page }) => { - await page.goto('${server.PREFIX}/network-tab/network.html'); - await page.evaluate(() => (window as any).donePromise); - }); - `, - }); - - await page.getByRole('treeitem', { name: 'network tab test' }).dblclick(); - await expect(page.getByTestId('workbench-run-status')).toContainText('Passed'); - - await page.getByRole('tab', { name: 'Network' }).click(); - - const filters = page.locator('.network-filters-resource-types'); - - // Focus the "All" tab and navigate with arrow keys. - await filters.getByText('All', { exact: true }).focus(); - await page.keyboard.press('ArrowRight'); - await expect(filters.getByText('Fetch', { exact: true })).toBeFocused(); - - await page.keyboard.press('ArrowRight'); - await expect(filters.getByText('HTML', { exact: true })).toBeFocused(); - - await page.keyboard.press('ArrowRight'); - await expect(filters.getByText('JS', { exact: true })).toBeFocused(); - - // ArrowLeft goes back. - await page.keyboard.press('ArrowLeft'); - await expect(filters.getByText('HTML', { exact: true })).toBeFocused(); - - // Home jumps to first tab. - await page.keyboard.press('Home'); - await expect(filters.getByText('All', { exact: true })).toBeFocused(); - - // End jumps to last tab. - await page.keyboard.press('End'); - await expect(filters.getByText('WS', { exact: true })).toBeFocused(); - - // Wraps around from last to first. - await page.keyboard.press('ArrowRight'); - await expect(filters.getByText('All', { exact: true })).toBeFocused(); -}); From 8304ec9bb6171b5a1f55732b886e39edbb37936d Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Jun 2026 11:14:23 -0700 Subject: [PATCH 2/7] fix(chromium): do not steal OS focus when creating pages (#41430) --- packages/playwright-core/src/server/chromium/crBrowser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index e4d95a68432d2..6a2b2a512ccea 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -370,7 +370,7 @@ export class CRBrowserContext extends BrowserContext { } override async doCreateNewPage(): Promise { - const { targetId } = await this._browser._session.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId }); + const { targetId } = await this._browser._session.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId, background: false, focus: false }); return this._browser._crPages.get(targetId)!._page; } From d50dde82fbe5773cf2aae124d5a9454f003dde9a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Jun 2026 11:14:36 -0700 Subject: [PATCH 3/7] fix(runner): --last-failed should run nothing when previous run had no failures (#41431) --- packages/playwright/src/runner/lastRun.ts | 6 +++--- packages/playwright/src/runner/tasks.ts | 2 +- packages/playwright/src/runner/testRunner.ts | 2 +- tests/playwright-test/runner.spec.ts | 19 +++++++++++++++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/playwright/src/runner/lastRun.ts b/packages/playwright/src/runner/lastRun.ts index a4081da782f22..57a073f541401 100644 --- a/packages/playwright/src/runner/lastRun.ts +++ b/packages/playwright/src/runner/lastRun.ts @@ -43,14 +43,14 @@ export class LastRunReporter implements ReporterV2 { } } - async filterLastFailed(): Promise { + async filterLastFailed(): Promise { if (!this._lastRunFile) - return []; + return undefined; try { const lastRunInfo = JSON.parse(await fs.promises.readFile(this._lastRunFile, 'utf8')) as LastRunInfo; return lastRunInfo.failedTests; } catch { - return []; + return undefined; } } diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 1a829fdb9857b..a2b2d328d818e 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -336,7 +336,7 @@ export function createLoadTask(mode: 'out-of-process' | 'in-process', options: { }); } - if (testRun.options.lastFailedTestIds?.length) { + if (testRun.options.lastFailedTestIds) { const failedTestIds = new Set(testRun.options.lastFailedTestIds); testRun.postShardTestFilters.push(test => failedTestIds.has(test.id)); } diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index 31a09983955d4..9f8b9c37d5fb9 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -455,7 +455,7 @@ export async function runAllTestsWithConfig(config: FullConfigInternal, options: const lastRun = new LastRunReporter(filteredProjects, options.listMode, options.lastFailedFile); if (options.lastFailed) { const lastFailedTestIds = await lastRun.filterLastFailed(); - if (lastFailedTestIds.length) + if (lastFailedTestIds) options = { ...options, lastFailedTestIds }; } diff --git a/tests/playwright-test/runner.spec.ts b/tests/playwright-test/runner.spec.ts index ea0ed5129121d..4973103484383 100644 --- a/tests/playwright-test/runner.spec.ts +++ b/tests/playwright-test/runner.spec.ts @@ -847,6 +847,25 @@ test('should run last failed tests', async ({ runInlineTest }) => { expect(result2.failed).toBe(1); }); +test('should run nothing with --last-failed when previous run had no failures', async ({ runInlineTest }) => { + const workspace = { + 'a.spec.js': ` + import { test, expect } from '@playwright/test'; + test('a', async () => {}); + test('b', async () => {}); + ` + }; + const result1 = await runInlineTest(workspace); + expect(result1.exitCode).toBe(0); + expect(result1.passed).toBe(2); + + const result2 = await runInlineTest(workspace, {}, {}, { additionalArgs: ['--last-failed', '--pass-with-no-tests'] }); + expect(result2.exitCode).toBe(0); + expect(result2.passed).toBe(0); + expect(result2.failed).toBe(0); + expect(result2.didNotRun).toBe(0); +}); + test('should run last failed tests in a shard', async ({ runInlineTest }) => { const workspace = { 'a.spec.js': ` From 72601dfd6198504118722dcfa001f92c07c4a7db Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Jun 2026 12:46:28 -0700 Subject: [PATCH 4/7] feat(web): canonical keyboard traversal and focus rings (#41434) --- packages/web/src/common.css | 5 +++++ packages/web/src/components/codeMirrorWrapper.css | 4 ++++ packages/web/src/components/codeMirrorWrapper.tsx | 4 +++- packages/web/src/components/tabbedPane.css | 9 +++++++++ packages/web/src/components/tabbedPane.tsx | 5 +++-- packages/web/src/components/toolbar.css | 5 +++++ packages/web/src/components/toolbarButton.css | 5 +++++ 7 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/web/src/common.css b/packages/web/src/common.css index ec84251f46612..19f7cdc5749bf 100644 --- a/packages/web/src/common.css +++ b/packages/web/src/common.css @@ -177,6 +177,11 @@ input[type=text], input[type=search] { outline: none; } +input[type=text]:focus-visible, input[type=search]:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + .codicon-loading { animation: spin 1s infinite linear; } diff --git a/packages/web/src/components/codeMirrorWrapper.css b/packages/web/src/components/codeMirrorWrapper.css index 61ba36bbf06ee..afec44ffded3d 100644 --- a/packages/web/src/components/codeMirrorWrapper.css +++ b/packages/web/src/components/codeMirrorWrapper.css @@ -20,6 +20,10 @@ line-height: 18px; } +.cm-wrapper:has(textarea:focus-visible) { + outline: 1px solid var(--vscode-focusBorder); +} + .cm-wrapper, .cm-wrapper > div { width: 100%; height: 100%; diff --git a/packages/web/src/components/codeMirrorWrapper.tsx b/packages/web/src/components/codeMirrorWrapper.tsx index 385a42b60316f..19f48c26aefc8 100644 --- a/packages/web/src/components/codeMirrorWrapper.tsx +++ b/packages/web/src/components/codeMirrorWrapper.tsx @@ -110,7 +110,9 @@ export const CodeMirrorWrapper: React.FC = ({ autoCloseBrackets: true, extraKeys: { 'Ctrl-F': 'findPersistent', - 'Cmd-F': 'findPersistent' + 'Cmd-F': 'findPersistent', + 'Tab': false, + 'Shift-Tab': false, } }); codemirrorRef.current = { cm }; diff --git a/packages/web/src/components/tabbedPane.css b/packages/web/src/components/tabbedPane.css index d3291e02bfb4a..b5ba591988476 100644 --- a/packages/web/src/components/tabbedPane.css +++ b/packages/web/src/components/tabbedPane.css @@ -39,11 +39,20 @@ align-items: center; justify-content: center; user-select: none; + background: none; + border: none; border-bottom: 2px solid transparent; + color: inherit; + font: inherit; outline: none; height: 100%; } +.tabbed-pane-tab:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + .tabbed-pane-tab-label { max-width: 250px; white-space: pre; diff --git a/packages/web/src/components/tabbedPane.tsx b/packages/web/src/components/tabbedPane.tsx index a031cdaa077ba..5b9132ce8bf38 100644 --- a/packages/web/src/components/tabbedPane.tsx +++ b/packages/web/src/components/tabbedPane.tsx @@ -42,6 +42,7 @@ export const TabbedPane: React.FunctionComponent<{ selectedTab = tabs[0].id; if (!mode) mode = 'default'; + return
@@ -102,7 +103,7 @@ export const TabbedPaneTab: React.FunctionComponent<{ onSelect?: (id: string) => void, ariaControls?: string, }> = ({ id, title, count, errorCount, selected, onSelect, ariaControls }) => { - return
onSelect?.(id)} role='tab' title={title} @@ -111,5 +112,5 @@ export const TabbedPaneTab: React.FunctionComponent<{
{title}
{!!count &&
{count}
} {!!errorCount &&
{errorCount}
} -
; + ; }; diff --git a/packages/web/src/components/toolbar.css b/packages/web/src/components/toolbar.css index 74b1f4664ae5d..b3458a42f6bfe 100644 --- a/packages/web/src/components/toolbar.css +++ b/packages/web/src/components/toolbar.css @@ -68,3 +68,8 @@ color: var(--vscode-input-foreground); background-color: var(--vscode-input-background); } + +.toolbar input:focus-visible, .toolbar select:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} diff --git a/packages/web/src/components/toolbarButton.css b/packages/web/src/components/toolbarButton.css index 6a1135636115b..5feeaddfa6726 100644 --- a/packages/web/src/components/toolbarButton.css +++ b/packages/web/src/components/toolbarButton.css @@ -28,6 +28,11 @@ border-radius: 4px; } +.toolbar-button:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + .toolbar-button:disabled { color: var(--vscode-disabledForeground) !important; cursor: default; From e4a50ba06d49f1a9672252d085f31a917aa08ba6 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 23 Jun 2026 20:47:14 +0100 Subject: [PATCH 5/7] devops: publish stable release on tag push instead of release event (#41425) --- .github/workflows/publish_release.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 07eb51e35d67c..bfd4d6d5fa9d2 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -7,8 +7,10 @@ on: push: branches: - release-* - release: - types: [published] + tags: + # TODO: revert this to "published release" once github.ref is set there. + # See https://github.com/actions/runner/issues/2788 as well. + - 'v1.61.1' jobs: publish-npm-and-driver: @@ -47,7 +49,7 @@ jobs: node utils/build/update_canary_version.js --beta --commit-timestamp utils/publish_all_packages.sh --beta - name: "publish release to NPM" - if: github.event_name == 'release' && github.event.action == 'published' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') run: utils/publish_all_packages.sh --release - name: Azure Login @@ -58,7 +60,7 @@ jobs: subscription-id: ${{ secrets.AZURE_PW_CDN_SUBSCRIPTION_ID }} - name: build & publish driver env: - AZ_UPLOAD_FOLDER: ${{ github.event_name == 'release' && 'driver' || 'driver/next' }} + AZ_UPLOAD_FOLDER: ${{ startsWith(github.ref, 'refs/tags/v') && 'driver' || 'driver/next' }} run: | utils/build/build-playwright-driver.sh utils/build/upload-playwright-driver.sh @@ -89,7 +91,7 @@ jobs: env: GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }} - name: Deploy Stable - if: github.event_name == 'release' && github.event.action == 'published' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') run: bash utils/build/deploy-trace-viewer.sh --stable env: GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }} From 1b3da20a34fa93dfb3338e9d7029107d88b5747a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Jun 2026 13:27:46 -0700 Subject: [PATCH 6/7] feat(actions): add scroll option to opt out of autoscroll (#41433) --- docs/src/api/class-elementhandle.md | 21 ++ docs/src/api/class-frame.md | 24 ++ docs/src/api/class-locator.md | 24 ++ docs/src/api/class-page.md | 24 ++ docs/src/api/params.md | 8 + packages/playwright-client/types/types.d.ts | 248 ++++++++++++++++++ .../playwright-core/src/client/channels.d.ts | 26 ++ .../playwright-core/src/protocol/validator.ts | 13 + .../playwright-core/src/server/channels.d.ts | 26 ++ packages/playwright-core/src/server/dom.ts | 6 +- packages/playwright-core/src/server/types.ts | 1 + packages/playwright-core/types/types.d.ts | 248 ++++++++++++++++++ packages/protocol/spec/frame.yml | 35 +++ packages/protocol/spec/handles.yml | 30 +++ tests/page/page-click-scroll.spec.ts | 52 ++++ 15 files changed, 784 insertions(+), 2 deletions(-) diff --git a/docs/src/api/class-elementhandle.md b/docs/src/api/class-elementhandle.md index 47f622a536655..10a862cfcf77f 100644 --- a/docs/src/api/class-elementhandle.md +++ b/docs/src/api/class-elementhandle.md @@ -179,6 +179,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.check.force = %%-input-force-%% * since: v1.8 +### option: ElementHandle.check.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: ElementHandle.check.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 @@ -224,6 +227,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.click.force = %%-input-force-%% * since: v1.8 +### option: ElementHandle.click.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: ElementHandle.click.noWaitAfter = %%-input-no-wait-after-%% * since: v1.8 @@ -280,6 +286,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.dblclick.force = %%-input-force-%% * since: v1.8 +### option: ElementHandle.dblclick.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: ElementHandle.dblclick.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 @@ -593,6 +602,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.hover.force = %%-input-force-%% * since: v1.8 +### option: ElementHandle.hover.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: ElementHandle.hover.timeout = %%-input-timeout-%% * since: v1.8 @@ -935,6 +947,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.setChecked.force = %%-input-force-%% * since: v1.15 +### option: ElementHandle.setChecked.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: ElementHandle.setChecked.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.15 @@ -1000,6 +1015,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.tap.force = %%-input-force-%% * since: v1.8 +### option: ElementHandle.tap.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: ElementHandle.tap.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 @@ -1073,6 +1091,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.uncheck.force = %%-input-force-%% * since: v1.8 +### option: ElementHandle.uncheck.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: ElementHandle.uncheck.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index 6f5ceedea976c..28e7820b4a9be 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -209,6 +209,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.check.force = %%-input-force-%% * since: v1.8 +### option: Frame.check.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Frame.check.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 @@ -262,6 +265,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.click.force = %%-input-force-%% * since: v1.8 +### option: Frame.click.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Frame.click.modifiers = %%-input-modifiers-%% * since: v1.8 @@ -320,6 +326,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.dblclick.force = %%-input-force-%% * since: v1.8 +### option: Frame.dblclick.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Frame.dblclick.delay = %%-input-down-up-delay-%% * since: v1.8 @@ -461,6 +470,9 @@ Optional event-specific initialization properties. ### option: Frame.dragAndDrop.force = %%-input-force-%% * since: v1.13 +### option: Frame.dragAndDrop.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Frame.dragAndDrop.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.13 @@ -1151,6 +1163,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.hover.force = %%-input-force-%% * since: v1.8 +### option: Frame.hover.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Frame.hover.strict = %%-input-strict-%% * since: v1.14 @@ -1601,6 +1616,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.setChecked.force = %%-input-force-%% * since: v1.15 +### option: Frame.setChecked.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Frame.setChecked.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.15 @@ -1692,6 +1710,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.tap.force = %%-input-force-%% * since: v1.8 +### option: Frame.tap.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Frame.tap.modifiers = %%-input-modifiers-%% * since: v1.8 @@ -1800,6 +1821,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.uncheck.force = %%-input-force-%% * since: v1.8 +### option: Frame.uncheck.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Frame.uncheck.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index a7f1fbd9f1bf7..fd0e717843829 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -354,6 +354,9 @@ await page.GetByRole(AriaRole.Checkbox).CheckAsync(); ### option: Locator.check.force = %%-input-force-%% * since: v1.14 +### option: Locator.check.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Locator.check.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.14 @@ -512,6 +515,9 @@ await page.Locator("canvas").ClickAsync(new() { ### option: Locator.click.force = %%-input-force-%% * since: v1.14 +### option: Locator.click.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Locator.click.noWaitAfter = %%-input-no-wait-after-%% * since: v1.14 @@ -598,6 +604,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Locator.dblclick.force = %%-input-force-%% * since: v1.14 +### option: Locator.dblclick.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Locator.dblclick.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.14 @@ -894,6 +903,9 @@ Locator of the element to drag to. ### option: Locator.dragTo.force = %%-input-force-%% * since: v1.18 +### option: Locator.dragTo.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Locator.dragTo.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.18 @@ -1553,6 +1565,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Locator.hover.force = %%-input-force-%% * since: v1.14 +### option: Locator.hover.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Locator.hover.timeout = %%-input-timeout-%% * since: v1.14 @@ -2446,6 +2461,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Locator.setChecked.force = %%-input-force-%% * since: v1.15 +### option: Locator.setChecked.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Locator.setChecked.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.15 @@ -2627,6 +2645,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Locator.tap.force = %%-input-force-%% * since: v1.14 +### option: Locator.tap.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Locator.tap.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.14 @@ -2741,6 +2762,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Locator.uncheck.force = %%-input-force-%% * since: v1.14 +### option: Locator.uncheck.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Locator.uncheck.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.14 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index e16df91a4feeb..30c14dbe92b37 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -745,6 +745,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.check.force = %%-input-force-%% * since: v1.8 +### option: Page.check.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Page.check.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 @@ -794,6 +797,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.click.force = %%-input-force-%% * since: v1.8 +### option: Page.click.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Page.click.modifiers = %%-input-modifiers-%% * since: v1.8 @@ -894,6 +900,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.dblclick.force = %%-input-force-%% * since: v1.8 +### option: Page.dblclick.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Page.dblclick.delay = %%-input-down-up-delay-%% * since: v1.8 @@ -1088,6 +1097,9 @@ await Page.DragAndDropAsync("#source", "#target", new() ### option: Page.dragAndDrop.force = %%-input-force-%% * since: v1.13 +### option: Page.dragAndDrop.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Page.dragAndDrop.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.13 @@ -2481,6 +2493,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.hover.force = %%-input-force-%% * since: v1.8 +### option: Page.hover.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Page.hover.modifiers = %%-input-modifiers-%% * since: v1.8 @@ -4068,6 +4083,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.setChecked.force = %%-input-force-%% * since: v1.15 +### option: Page.setChecked.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Page.setChecked.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.15 @@ -4310,6 +4328,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.tap.force = %%-input-force-%% * since: v1.8 +### option: Page.tap.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Page.tap.modifiers = %%-input-modifiers-%% * since: v1.8 @@ -4422,6 +4443,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.uncheck.force = %%-input-force-%% * since: v1.8 +### option: Page.uncheck.scroll = %%-input-scroll-%% +* since: v1.62 + ### option: Page.uncheck.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 1a59644289661..8000f7e5ba577 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -80,6 +80,14 @@ This option has no effect. Whether to bypass the [actionability](../actionability.md) checks. Defaults to `false`. +## input-scroll +- `scroll` <[ScrollMode]<"auto"|"none">> + +Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, +which scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to +`"none"`, Playwright does not scroll the element and the action fails if the element is not already in the viewport. +This is useful to assert that an element is reachable by the user without additional scrolling. + ## input-selector - `selector` <[string]> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 5f01196f87178..b2150842591d0 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -2138,6 +2138,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -2245,6 +2253,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -2384,6 +2400,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -2503,6 +2527,14 @@ export interface Page { */ noWaitAfter?: boolean; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Clicks on the source element at this point relative to the top-left corner of the element's padding box. If not * specified, some visible point of the element is used. @@ -3330,6 +3362,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -4313,6 +4353,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -4574,6 +4622,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -4722,6 +4778,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -6243,6 +6307,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -6339,6 +6411,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -6435,6 +6515,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -6540,6 +6628,14 @@ export interface Frame { */ noWaitAfter?: boolean; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Clicks on the source element at this point relative to the top-left corner of the element's padding box. If not * specified, some visible point of the element is used. @@ -7168,6 +7264,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -7702,6 +7806,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -7877,6 +7989,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -8025,6 +8145,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -11741,6 +11869,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -11825,6 +11961,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. @@ -11914,6 +12058,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. @@ -12088,6 +12240,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -12530,6 +12690,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -12658,6 +12826,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -12762,6 +12938,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -13244,6 +13428,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -13389,6 +13581,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. @@ -13512,6 +13712,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. @@ -13662,6 +13870,14 @@ export interface Locator { */ noWaitAfter?: boolean; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Clicks on the source element at this point relative to the top-left corner of the element's padding box. If not * specified, some visible point of the element is used. @@ -14354,6 +14570,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -15026,6 +15250,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -15182,6 +15414,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -15304,6 +15544,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the diff --git a/packages/playwright-core/src/client/channels.d.ts b/packages/playwright-core/src/client/channels.d.ts index ea59dfbb5807a..c66c5f2b5f249 100644 --- a/packages/playwright-core/src/client/channels.d.ts +++ b/packages/playwright-core/src/client/channels.d.ts @@ -2462,6 +2462,7 @@ export type FrameCheckParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', position?: Point, timeout: number, trial?: boolean, @@ -2469,6 +2470,7 @@ export type FrameCheckParams = { export type FrameCheckOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', position?: Point, trial?: boolean, }; @@ -2477,6 +2479,7 @@ export type FrameClickParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', noWaitAfter?: boolean, modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, @@ -2490,6 +2493,7 @@ export type FrameClickParams = { export type FrameClickOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', noWaitAfter?: boolean, modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, @@ -2509,6 +2513,7 @@ export type FrameDragAndDropParams = { source: string, target: string, force?: boolean, + scroll?: 'auto' | 'none', timeout: number, trial?: boolean, sourcePosition?: Point, @@ -2518,6 +2523,7 @@ export type FrameDragAndDropParams = { }; export type FrameDragAndDropOptions = { force?: boolean, + scroll?: 'auto' | 'none', trial?: boolean, sourcePosition?: Point, targetPosition?: Point, @@ -2562,6 +2568,7 @@ export type FrameDblclickParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, delay?: number, @@ -2573,6 +2580,7 @@ export type FrameDblclickParams = { export type FrameDblclickOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, delay?: number, @@ -2693,6 +2701,7 @@ export type FrameHoverParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, timeout: number, @@ -2701,6 +2710,7 @@ export type FrameHoverParams = { export type FrameHoverOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, trial?: boolean, @@ -2912,6 +2922,7 @@ export type FrameTapParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, timeout: number, @@ -2920,6 +2931,7 @@ export type FrameTapParams = { export type FrameTapOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, trial?: boolean, @@ -2957,6 +2969,7 @@ export type FrameUncheckParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', position?: Point, timeout: number, trial?: boolean, @@ -2964,6 +2977,7 @@ export type FrameUncheckParams = { export type FrameUncheckOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', position?: Point, trial?: boolean, }; @@ -3191,18 +3205,21 @@ export type ElementHandleBoundingBoxResult = { }; export type ElementHandleCheckParams = { force?: boolean, + scroll?: 'auto' | 'none', position?: Point, timeout: number, trial?: boolean, }; export type ElementHandleCheckOptions = { force?: boolean, + scroll?: 'auto' | 'none', position?: Point, trial?: boolean, }; export type ElementHandleCheckResult = void; export type ElementHandleClickParams = { force?: boolean, + scroll?: 'auto' | 'none', noWaitAfter?: boolean, modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, @@ -3215,6 +3232,7 @@ export type ElementHandleClickParams = { }; export type ElementHandleClickOptions = { force?: boolean, + scroll?: 'auto' | 'none', noWaitAfter?: boolean, modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, @@ -3232,6 +3250,7 @@ export type ElementHandleContentFrameResult = { }; export type ElementHandleDblclickParams = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, delay?: number, @@ -3242,6 +3261,7 @@ export type ElementHandleDblclickParams = { }; export type ElementHandleDblclickOptions = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, delay?: number, @@ -3281,6 +3301,7 @@ export type ElementHandleGetAttributeResult = { }; export type ElementHandleHoverParams = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, timeout: number, @@ -3288,6 +3309,7 @@ export type ElementHandleHoverParams = { }; export type ElementHandleHoverOptions = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, trial?: boolean, @@ -3470,6 +3492,7 @@ export type ElementHandleSetInputFilesOptions = { export type ElementHandleSetInputFilesResult = void; export type ElementHandleTapParams = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, timeout: number, @@ -3477,6 +3500,7 @@ export type ElementHandleTapParams = { }; export type ElementHandleTapOptions = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, trial?: boolean, @@ -3498,12 +3522,14 @@ export type ElementHandleTypeOptions = { export type ElementHandleTypeResult = void; export type ElementHandleUncheckParams = { force?: boolean, + scroll?: 'auto' | 'none', position?: Point, timeout: number, trial?: boolean, }; export type ElementHandleUncheckOptions = { force?: boolean, + scroll?: 'auto' | 'none', position?: Point, trial?: boolean, }; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 5fe1cce5d2052..2e037ab2b58e4 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1296,6 +1296,7 @@ scheme.FrameCheckParams = tObject({ selector: tString, strict: tOptional(tBoolean), force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), position: tOptional(tType('Point')), timeout: tFloat, trial: tOptional(tBoolean), @@ -1305,6 +1306,7 @@ scheme.FrameClickParams = tObject({ selector: tString, strict: tOptional(tBoolean), force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), noWaitAfter: tOptional(tBoolean), modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))), position: tOptional(tType('Point')), @@ -1324,6 +1326,7 @@ scheme.FrameDragAndDropParams = tObject({ source: tString, target: tString, force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), timeout: tFloat, trial: tOptional(tBoolean), sourcePosition: tOptional(tType('Point')), @@ -1354,6 +1357,7 @@ scheme.FrameDblclickParams = tObject({ selector: tString, strict: tOptional(tBoolean), force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))), position: tOptional(tType('Point')), delay: tOptional(tFloat), @@ -1442,6 +1446,7 @@ scheme.FrameHoverParams = tObject({ selector: tString, strict: tOptional(tBoolean), force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))), position: tOptional(tType('Point')), timeout: tFloat, @@ -1587,6 +1592,7 @@ scheme.FrameTapParams = tObject({ selector: tString, strict: tOptional(tBoolean), force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))), position: tOptional(tType('Point')), timeout: tFloat, @@ -1617,6 +1623,7 @@ scheme.FrameUncheckParams = tObject({ selector: tString, strict: tOptional(tBoolean), force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), position: tOptional(tType('Point')), timeout: tFloat, trial: tOptional(tBoolean), @@ -1751,6 +1758,7 @@ scheme.ElementHandleBoundingBoxResult = tObject({ }); scheme.ElementHandleCheckParams = tObject({ force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), position: tOptional(tType('Point')), timeout: tFloat, trial: tOptional(tBoolean), @@ -1758,6 +1766,7 @@ scheme.ElementHandleCheckParams = tObject({ scheme.ElementHandleCheckResult = tOptional(tObject({})); scheme.ElementHandleClickParams = tObject({ force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), noWaitAfter: tOptional(tBoolean), modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))), position: tOptional(tType('Point')), @@ -1775,6 +1784,7 @@ scheme.ElementHandleContentFrameResult = tObject({ }); scheme.ElementHandleDblclickParams = tObject({ force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))), position: tOptional(tType('Point')), delay: tOptional(tFloat), @@ -1805,6 +1815,7 @@ scheme.ElementHandleGetAttributeResult = tObject({ }); scheme.ElementHandleHoverParams = tObject({ force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))), position: tOptional(tType('Point')), timeout: tFloat, @@ -1927,6 +1938,7 @@ scheme.ElementHandleSetInputFilesParams = tObject({ scheme.ElementHandleSetInputFilesResult = tOptional(tObject({})); scheme.ElementHandleTapParams = tObject({ force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))), position: tOptional(tType('Point')), timeout: tFloat, @@ -1945,6 +1957,7 @@ scheme.ElementHandleTypeParams = tObject({ scheme.ElementHandleTypeResult = tOptional(tObject({})); scheme.ElementHandleUncheckParams = tObject({ force: tOptional(tBoolean), + scroll: tOptional(tEnum(['auto', 'none'])), position: tOptional(tType('Point')), timeout: tFloat, trial: tOptional(tBoolean), diff --git a/packages/playwright-core/src/server/channels.d.ts b/packages/playwright-core/src/server/channels.d.ts index e8c7c0ddc8765..153920cac69b2 100644 --- a/packages/playwright-core/src/server/channels.d.ts +++ b/packages/playwright-core/src/server/channels.d.ts @@ -2465,6 +2465,7 @@ export type FrameCheckParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', position?: Point, timeout: number, trial?: boolean, @@ -2472,6 +2473,7 @@ export type FrameCheckParams = { export type FrameCheckOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', position?: Point, trial?: boolean, }; @@ -2480,6 +2482,7 @@ export type FrameClickParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', noWaitAfter?: boolean, modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, @@ -2493,6 +2496,7 @@ export type FrameClickParams = { export type FrameClickOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', noWaitAfter?: boolean, modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, @@ -2512,6 +2516,7 @@ export type FrameDragAndDropParams = { source: string, target: string, force?: boolean, + scroll?: 'auto' | 'none', timeout: number, trial?: boolean, sourcePosition?: Point, @@ -2521,6 +2526,7 @@ export type FrameDragAndDropParams = { }; export type FrameDragAndDropOptions = { force?: boolean, + scroll?: 'auto' | 'none', trial?: boolean, sourcePosition?: Point, targetPosition?: Point, @@ -2565,6 +2571,7 @@ export type FrameDblclickParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, delay?: number, @@ -2576,6 +2583,7 @@ export type FrameDblclickParams = { export type FrameDblclickOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, delay?: number, @@ -2696,6 +2704,7 @@ export type FrameHoverParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, timeout: number, @@ -2704,6 +2713,7 @@ export type FrameHoverParams = { export type FrameHoverOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, trial?: boolean, @@ -2915,6 +2925,7 @@ export type FrameTapParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, timeout: number, @@ -2923,6 +2934,7 @@ export type FrameTapParams = { export type FrameTapOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, trial?: boolean, @@ -2960,6 +2972,7 @@ export type FrameUncheckParams = { selector: string, strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', position?: Point, timeout: number, trial?: boolean, @@ -2967,6 +2980,7 @@ export type FrameUncheckParams = { export type FrameUncheckOptions = { strict?: boolean, force?: boolean, + scroll?: 'auto' | 'none', position?: Point, trial?: boolean, }; @@ -3194,18 +3208,21 @@ export type ElementHandleBoundingBoxResult = { }; export type ElementHandleCheckParams = { force?: boolean, + scroll?: 'auto' | 'none', position?: Point, timeout: number, trial?: boolean, }; export type ElementHandleCheckOptions = { force?: boolean, + scroll?: 'auto' | 'none', position?: Point, trial?: boolean, }; export type ElementHandleCheckResult = void; export type ElementHandleClickParams = { force?: boolean, + scroll?: 'auto' | 'none', noWaitAfter?: boolean, modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, @@ -3218,6 +3235,7 @@ export type ElementHandleClickParams = { }; export type ElementHandleClickOptions = { force?: boolean, + scroll?: 'auto' | 'none', noWaitAfter?: boolean, modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, @@ -3235,6 +3253,7 @@ export type ElementHandleContentFrameResult = { }; export type ElementHandleDblclickParams = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, delay?: number, @@ -3245,6 +3264,7 @@ export type ElementHandleDblclickParams = { }; export type ElementHandleDblclickOptions = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, delay?: number, @@ -3284,6 +3304,7 @@ export type ElementHandleGetAttributeResult = { }; export type ElementHandleHoverParams = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, timeout: number, @@ -3291,6 +3312,7 @@ export type ElementHandleHoverParams = { }; export type ElementHandleHoverOptions = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, trial?: boolean, @@ -3473,6 +3495,7 @@ export type ElementHandleSetInputFilesOptions = { export type ElementHandleSetInputFilesResult = void; export type ElementHandleTapParams = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, timeout: number, @@ -3480,6 +3503,7 @@ export type ElementHandleTapParams = { }; export type ElementHandleTapOptions = { force?: boolean, + scroll?: 'auto' | 'none', modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[], position?: Point, trial?: boolean, @@ -3501,12 +3525,14 @@ export type ElementHandleTypeOptions = { export type ElementHandleTypeResult = void; export type ElementHandleUncheckParams = { force?: boolean, + scroll?: 'auto' | 'none', position?: Point, timeout: number, trial?: boolean, }; export type ElementHandleUncheckOptions = { force?: boolean, + scroll?: 'auto' | 'none', position?: Point, trial?: boolean, }; diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 872e294f1b6c7..c58c9029146ad 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -404,9 +404,11 @@ export class ElementHandle extends js.JSHandle { forceScrollOptions: ScrollIntoViewOptions | undefined, options: { waitAfter: boolean | 'disabled' } & types.PointerActionOptions & types.PointerActionWaitOptions, ): Promise { - const { force = false, position } = options; + const { force = false, position, scroll } = options; - const doScrollIntoView = async (progress: Progress) => { + const doScrollIntoView = async (progress: Progress): Promise<'error:notvisible' | 'error:notconnected' | 'done'> => { + if (scroll === 'none') + return 'done'; if (forceScrollOptions) { return await progress.race(this.evaluateInUtility(([injected, node, options]) => { if (node.nodeType === 1 /* Node.ELEMENT_NODE */) diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index 33efd23934b69..4c3fb2cf22c5e 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -38,6 +38,7 @@ export type NavigateOptions = { export type CommonActionOptions = StrictOptions & { force?: boolean, noAutoWaiting?: boolean, + scroll?: 'auto' | 'none', }; export type PointerActionWaitOptions = CommonActionOptions & { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 5f01196f87178..b2150842591d0 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2138,6 +2138,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -2245,6 +2253,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -2384,6 +2400,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -2503,6 +2527,14 @@ export interface Page { */ noWaitAfter?: boolean; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Clicks on the source element at this point relative to the top-left corner of the element's padding box. If not * specified, some visible point of the element is used. @@ -3330,6 +3362,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -4313,6 +4353,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -4574,6 +4622,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -4722,6 +4778,14 @@ export interface Page { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -6243,6 +6307,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -6339,6 +6411,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -6435,6 +6515,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -6540,6 +6628,14 @@ export interface Frame { */ noWaitAfter?: boolean; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Clicks on the source element at this point relative to the top-left corner of the element's padding box. If not * specified, some visible point of the element is used. @@ -7168,6 +7264,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -7702,6 +7806,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -7877,6 +7989,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -8025,6 +8145,14 @@ export interface Frame { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -11741,6 +11869,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -11825,6 +11961,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. @@ -11914,6 +12058,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. @@ -12088,6 +12240,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -12530,6 +12690,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -12658,6 +12826,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -12762,6 +12938,14 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -13244,6 +13428,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -13389,6 +13581,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. @@ -13512,6 +13712,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. @@ -13662,6 +13870,14 @@ export interface Locator { */ noWaitAfter?: boolean; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Clicks on the source element at this point relative to the top-left corner of the element's padding box. If not * specified, some visible point of the element is used. @@ -14354,6 +14570,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -15026,6 +15250,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -15182,6 +15414,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -15304,6 +15544,14 @@ export interface Locator { y: number; }; + /** + * Controls whether Playwright scrolls the element into view before performing the action. Defaults to `"auto"`, which + * scrolls the element into view when necessary, including scrolling nested scrollable containers. When set to + * `"none"`, Playwright does not scroll the element and the action fails if the element is not already in the + * viewport. This is useful to assert that an element is reachable by the user without additional scrolling. + */ + scroll?: "auto"|"none"; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the diff --git a/packages/protocol/spec/frame.yml b/packages/protocol/spec/frame.yml index 2ff4909e77f07..ec1813aa96e79 100644 --- a/packages/protocol/spec/frame.yml +++ b/packages/protocol/spec/frame.yml @@ -110,6 +110,11 @@ Frame: selector: string strict: boolean? force: boolean? + scroll: + type: enum? + literals: + - auto + - none position: Point? timeout: float trial: boolean? @@ -126,6 +131,11 @@ Frame: selector: string strict: boolean? force: boolean? + scroll: + type: enum? + literals: + - auto + - none noWaitAfter: boolean? modifiers: type: array? @@ -170,6 +180,11 @@ Frame: source: string target: string force: boolean? + scroll: + type: enum? + literals: + - auto + - none timeout: float trial: boolean? sourcePosition: Point? @@ -225,6 +240,11 @@ Frame: selector: string strict: boolean? force: boolean? + scroll: + type: enum? + literals: + - auto + - none modifiers: type: array? items: @@ -374,6 +394,11 @@ Frame: selector: string strict: boolean? force: boolean? + scroll: + type: enum? + literals: + - auto + - none modifiers: type: array? items: @@ -634,6 +659,11 @@ Frame: selector: string strict: boolean? force: boolean? + scroll: + type: enum? + literals: + - auto + - none modifiers: type: array? items: @@ -694,6 +724,11 @@ Frame: selector: string strict: boolean? force: boolean? + scroll: + type: enum? + literals: + - auto + - none position: Point? timeout: float trial: boolean? diff --git a/packages/protocol/spec/handles.yml b/packages/protocol/spec/handles.yml index edab0387c63d6..54440f7018607 100644 --- a/packages/protocol/spec/handles.yml +++ b/packages/protocol/spec/handles.yml @@ -125,6 +125,11 @@ ElementHandle: title: Check parameters: force: boolean? + scroll: + type: enum? + literals: + - auto + - none position: Point? timeout: float trial: boolean? @@ -139,6 +144,11 @@ ElementHandle: title: Click parameters: force: boolean? + scroll: + type: enum? + literals: + - auto + - none noWaitAfter: boolean? modifiers: type: array? @@ -179,6 +189,11 @@ ElementHandle: title: Double click parameters: force: boolean? + scroll: + type: enum? + literals: + - auto + - none modifiers: type: array? items: @@ -252,6 +267,11 @@ ElementHandle: title: Hover parameters: force: boolean? + scroll: + type: enum? + literals: + - auto + - none modifiers: type: array? items: @@ -490,6 +510,11 @@ ElementHandle: title: Tap parameters: force: boolean? + scroll: + type: enum? + literals: + - auto + - none modifiers: type: array? items: @@ -536,6 +561,11 @@ ElementHandle: title: Uncheck parameters: force: boolean? + scroll: + type: enum? + literals: + - auto + - none position: Point? timeout: float trial: boolean? diff --git a/tests/page/page-click-scroll.spec.ts b/tests/page/page-click-scroll.spec.ts index f8c9c1f976a2c..e1c0f80c3bd4c 100644 --- a/tests/page/page-click-scroll.spec.ts +++ b/tests/page/page-click-scroll.spec.ts @@ -104,3 +104,55 @@ it('should scroll into view element in iframe', async ({ page, isAndroid, server `); await page.frameLocator('iframe').getByRole('button').click({ timeout: 5000 }); }); + +it('should not scroll the page when scroll is "none"', async ({ page }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41421' }); + await page.setContent(` +
+ + `); + const error = await page.locator('button').click({ scroll: 'none', timeout: 2000 }).catch(e => e); + expect(error.message).toContain('element is outside of the viewport'); + expect(await page.evaluate('window._clicked')).toBeFalsy(); + expect(await page.evaluate(() => window.scrollY)).toBe(0); +}); + +it('should click in-viewport element when scroll is "none"', async ({ page }) => { + await page.setContent(` + +
+ `); + await page.locator('button').click({ scroll: 'none', timeout: 2000 }); + expect(await page.evaluate('window._clicked')).toBe(true); + expect(await page.evaluate(() => window.scrollY)).toBe(0); +}); + +it('should not scroll nested container when scroll is "none"', async ({ page }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41421' }); + await page.setContent(` +
+
A
+
B
+ +
+ `); + const button = page.locator('button'); + // The button is scrolled out of the nested overflow container. + const error = await button.click({ scroll: 'none', timeout: 2000 }).catch(e => e); + expect(error).toBeTruthy(); + expect(await page.evaluate('window._clicked')).toBeFalsy(); + // Default behavior scrolls the nested container into view and succeeds. + await button.click({ timeout: 2000 }); + expect(await page.evaluate('window._clicked')).toBe(true); +}); + +it('should not scroll on hover when scroll is "none"', async ({ page }) => { + await page.setContent(` +
+
hover me
+ `); + const error = await page.locator('div >> text=hover me').hover({ scroll: 'none', timeout: 2000 }).catch(e => e); + expect(error.message).toContain('element is outside of the viewport'); + expect(await page.evaluate('window._hovered')).toBeFalsy(); + expect(await page.evaluate(() => window.scrollY)).toBe(0); +}); From 51fa5badd8d4c2648bb21751668223190818f1c7 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Jun 2026 16:23:13 -0700 Subject: [PATCH 7/7] fix(chromium): do not forward cookie header on route.continue (#41435) --- .../src/server/chromium/crNetworkManager.ts | 11 +++- tests/page/page-request-continue.spec.ts | 56 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index e353798b037c5..4f57947001e00 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -346,7 +346,7 @@ export class CRNetworkManager { headersOverride = redirectedFrom?._originalRequestRoute?._alreadyContinuedParams?.headers; if (headersOverride) { const originalHeaders = Object.entries(requestPausedEvent.request.headers).map(([name, value]) => ({ name, value })); - headersOverride = network.applyHeadersOverrides(originalHeaders, headersOverride); + headersOverride = removeCookieHeader(network.applyHeadersOverrides(originalHeaders, headersOverride)); } requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers: headersOverride }); } else { @@ -663,7 +663,7 @@ class RouteImpl implements network.RouteDelegate { this._alreadyContinuedParams = { requestId: this._interceptionId!, url: overrides.url, - headers: overrides.headers, + headers: overrides.headers && removeCookieHeader(overrides.headers), method: overrides.method, postData: overrides.postData ? overrides.postData.toString('base64') : undefined }; @@ -714,6 +714,13 @@ async function catchDisallowedErrors(callback: () => Promise) { } +// Never forward the `cookie` header to Fetch.continueRequest: since Chromium 145 it overrides +// the cookie store, which leaks the value captured at interception time. Omitting it lets the +// network stack source the cookie from the store. https://github.com/microsoft/playwright/issues/41428 +function removeCookieHeader(headers: types.HeadersArray): types.HeadersArray { + return headers.filter(header => header.name.toLowerCase() !== 'cookie'); +} + function splitSetCookieHeader(headers: types.HeadersArray): types.HeadersArray { const index = headers.findIndex(({ name }) => name.toLowerCase() === 'set-cookie'); if (index === -1) diff --git a/tests/page/page-request-continue.spec.ts b/tests/page/page-request-continue.spec.ts index 0166f520fecad..4226f7374bfb1 100644 --- a/tests/page/page-request-continue.spec.ts +++ b/tests/page/page-request-continue.spec.ts @@ -477,6 +477,62 @@ it('continue should not override cookie', { expect(serverRequest.headers['custom']).toBe('value'); }); +it('continue with headers should send fresh cookie from the browser cookie store', { + annotation: [ + { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41428' }, + ] +}, async ({ page, server }) => { + server.setRoute('/set-cookie', (request, response) => { + response.writeHead(200, { 'Set-Cookie': 'foo=v1;' }); + response.end(); + }); + await page.goto(server.PREFIX + '/set-cookie'); + expect(await page.evaluate(() => document.cookie)).toBe('foo=v1'); + + await page.route('**/empty.html', async route => { + // Cookie store changes between interception and continuation. + await page.context().addCookies([{ name: 'foo', value: 'v2', url: server.PREFIX }]); + await route.continue({ headers: route.request().headers() }); + }); + + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE) + ]); + // The fresh cookie from the browser cookie store should be sent, not the stale captured value. + expect(serverRequest.headers['cookie']).toBe('foo=v2'); +}); + +it('continue with headers should send fresh cookie after a redirect', { + annotation: [ + { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41428' }, + ] +}, async ({ page, server }) => { + server.setRoute('/set-cookie', (request, response) => { + response.writeHead(200, { 'Set-Cookie': 'foo=v1;' }); + response.end(); + }); + await page.goto(server.PREFIX + '/set-cookie'); + expect(await page.evaluate(() => document.cookie)).toBe('foo=v1'); + + // The redirect updates the cookie before bouncing to the final destination. + server.setRoute('/redirect', (request, response) => { + response.writeHead(302, { 'Set-Cookie': 'foo=v2;', 'location': server.PREFIX + '/empty.html' }); + response.end(); + }); + + await page.route('**/redirect', route => { + void route.continue({ headers: route.request().headers() }); + }); + + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.PREFIX + '/redirect') + ]); + // The redirected request should carry the cookie updated by the redirect, not the stale captured value. + expect(serverRequest.headers['cookie']).toBe('foo=v2'); +}); + it('redirect after continue should be able to delete cookie', { annotation: [ { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/35168' },