From e540211a395103b3ab392fa7e082de7375be0260 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Tue, 9 Jun 2026 08:42:10 -0400 Subject: [PATCH 1/6] fix(firefox): support video on Debian (#41204) --- packages/playwright-core/src/server/registry/nativeDeps.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/playwright-core/src/server/registry/nativeDeps.ts b/packages/playwright-core/src/server/registry/nativeDeps.ts index bd2c10d34ad62..7cb4583c2a912 100644 --- a/packages/playwright-core/src/server/registry/nativeDeps.ts +++ b/packages/playwright-core/src/server/registry/nativeDeps.ts @@ -925,6 +925,7 @@ export const deps: any = { firefox: [ 'libasound2', 'libatk1.0-0', + 'libavcodec58', 'libcairo-gobject2', 'libcairo2', 'libdbus-1-3', @@ -1133,6 +1134,7 @@ export const deps: any = { firefox: [ 'libasound2', 'libatk1.0-0', + 'libavcodec59', 'libcairo-gobject2', 'libcairo2', 'libdbus-1-3', @@ -1288,6 +1290,7 @@ export const deps: any = { firefox: [ 'libasound2', 'libatk1.0-0t64', + 'libavcodec61', 'libcairo-gobject2', 'libcairo2', 'libdbus-1-3', From 123c3348e507d112795e067c1045a2b773e09fcb Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 9 Jun 2026 14:54:19 +0100 Subject: [PATCH 2/6] test: disable some tests (#41213) --- tests/library/heap.spec.ts | 3 ++- tests/library/screencast.spec.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/library/heap.spec.ts b/tests/library/heap.spec.ts index 7c2b9d8472e35..4c5a5e9a2d5d5 100644 --- a/tests/library/heap.spec.ts +++ b/tests/library/heap.spec.ts @@ -200,8 +200,9 @@ test.describe(() => { }); }); -test('cycle handles', async ({ page, server }) => { +test('cycle handles', async ({ page, server, trace }) => { test.slow(); + test.skip(trace === 'on', 'too slow with 2000 snapshots'); await page.goto(server.EMPTY_PAGE); await page.setContent(`
hi
`.repeat(2000)); diff --git a/tests/library/screencast.spec.ts b/tests/library/screencast.spec.ts index 468880b61e650..a67b0aaaf250a 100644 --- a/tests/library/screencast.spec.ts +++ b/tests/library/screencast.spec.ts @@ -25,8 +25,10 @@ test.skip(({ mode }) => mode !== 'default', 'screencast is not available in remo test.skip(({ video }) => video === 'on', 'conflicts with built-in video recording'); test.slow(); -test('screencast.start delivers frames via onFrame callback', async ({ browser, server, trace }) => { +test('screencast.start delivers frames via onFrame callback', async ({ browser, server, trace, browserName, isMac, headless }) => { test.skip(trace === 'on', 'trace=on has different screencast image configuration'); + test.fixme(browserName === 'firefox' && isMac && !headless, 'wrong frame size in headed Firefox on Mac'); + const context = await browser.newContext({ viewport: { width: 1000, height: 400 } }); const page = await context.newPage(); @@ -52,8 +54,10 @@ test('screencast.start delivers frames via onFrame callback', async ({ browser, await context.close(); }); -test('onFrame receives viewport size', async ({ browser, server, trace }) => { +test('onFrame receives viewport size', async ({ browser, server, trace, browserName, isMac, headless }) => { test.skip(trace === 'on', 'trace=on has different screencast image configuration'); + test.fixme(browserName === 'firefox' && isMac && !headless, 'wrong frame size in headed Firefox on Mac'); + const context = await browser.newContext({ viewport: { width: 1000, height: 400 } }); const page = await context.newPage(); From f95684a74b873834b21f3d2bbb21a7cf65ba1611 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 9 Jun 2026 14:54:32 +0100 Subject: [PATCH 3/6] Revert "feat(keyboard): add array overload to pressSequentially (#40748)" (#41212) --- docs/src/api/class-keyboard.md | 24 ---------- docs/src/api/class-locator.md | 28 +---------- packages/playwright-client/types/types.d.ts | 37 +------------- .../playwright-core/src/protocol/validator.ts | 2 - packages/playwright-core/src/server/dom.ts | 4 +- packages/playwright-core/src/server/frames.ts | 2 +- packages/playwright-core/src/server/input.ts | 48 ++++--------------- packages/playwright-core/types/types.d.ts | 37 +------------- packages/protocol/spec/frame.yml | 1 - packages/protocol/spec/page.yml | 1 - packages/protocol/src/channels.d.ts | 4 -- tests/page/page-keyboard.spec.ts | 16 ------- 12 files changed, 14 insertions(+), 190 deletions(-) diff --git a/docs/src/api/class-keyboard.md b/docs/src/api/class-keyboard.md index 77c561ce3c8c2..0bcaf6a59f96a 100644 --- a/docs/src/api/class-keyboard.md +++ b/docs/src/api/class-keyboard.md @@ -308,8 +308,6 @@ In most cases, you should use [`method: Locator.fill`] instead. You only need to Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. -When [`option: namedKeys`] is `true`, anything inside `{}` is treated as a key name (same format as [`method: Keyboard.press`]). - To press a special key, like `Control` or `ArrowDown`, use [`method: Keyboard.press`]. **Usage** @@ -317,9 +315,6 @@ To press a special key, like `Control` or `ArrowDown`, use [`method: Keyboard.pr ```js await page.keyboard.type('Hello'); // Types instantly await page.keyboard.type('World', { delay: 100 }); // Types slower, like a user - -// Mix text and special keys -await page.keyboard.type('Hello{Enter}World', { namedKeys: true }); ``` ```java @@ -327,33 +322,21 @@ await page.keyboard.type('Hello{Enter}World', { namedKeys: true }); page.keyboard().type("Hello"); // Types slower, like a user page.keyboard().type("World", new Keyboard.TypeOptions().setDelay(100)); - -// Mix text and special keys -page.keyboard().type("Hello{Enter}World", new Keyboard.TypeOptions().setNamedKeys(true)); ``` ```python async await page.keyboard.type("Hello") # types instantly await page.keyboard.type("World", delay=100) # types slower, like a user - -# Mix text and special keys -await page.keyboard.type("Hello{Enter}World", named_keys=True) ``` ```python sync page.keyboard.type("Hello") # types instantly page.keyboard.type("World", delay=100) # types slower, like a user - -# Mix text and special keys -page.keyboard.type("Hello{Enter}World", named_keys=True) ``` ```csharp await page.Keyboard.TypeAsync("Hello"); // types instantly await page.Keyboard.TypeAsync("World", new() { Delay = 100 }); // types slower, like a user - -// Mix text and special keys -await page.Keyboard.TypeAsync("Hello{Enter}World", new() { NamedKeys = true }); ``` :::note @@ -376,13 +359,6 @@ A text to type into a focused element. Time to wait between key presses in milliseconds. Defaults to 0. -### option: Keyboard.type.namedKeys -* since: v1.61 -- `namedKeys` <[boolean]> - -When [`option: namedKeys`] is `true`, anything inside `{}` is treated as a key name (same format as [`method: Keyboard.press`]). -Use `{{` to type a literal brace character. Defaults to `false`. - ## async method: Keyboard.up * since: v1.8 diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index f2004c3318988..59a98671aec5e 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -2103,8 +2103,6 @@ In most cases, you should use [`method: Locator.fill`] instead. You only need to Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. -When [`option: namedKeys`] is `true`, anything inside `{}` is treated as a key name (same format as [`method: Locator.press`]). - To press a special key, like `Control` or `ArrowDown`, use [`method: Locator.press`]. **Usage** @@ -2112,43 +2110,26 @@ To press a special key, like `Control` or `ArrowDown`, use [`method: Locator.pre ```js await locator.pressSequentially('Hello'); // Types instantly await locator.pressSequentially('World', { delay: 100 }); // Types slower, like a user - -// Mix characters and named keys -await locator.pressSequentially('Hello{Enter}World', { namedKeys: true }); -// Use modifier combos -await locator.pressSequentially('{Control+A}{Delete}Hello', { namedKeys: true }); ``` ```java locator.pressSequentially("Hello"); // Types instantly locator.pressSequentially("World", new Locator.pressSequentiallyOptions().setDelay(100)); // Types slower, like a user - -// Mix characters and named keys -locator.pressSequentially("Hello{Enter}World", new Locator.pressSequentiallyOptions().setNamedKeys(true)); ``` ```python async await locator.press_sequentially("hello") # types instantly await locator.press_sequentially("world", delay=100) # types slower, like a user - -# Mix characters and named keys -await locator.press_sequentially("Hello{Enter}World", named_keys=True) ``` ```python sync locator.press_sequentially("hello") # types instantly locator.press_sequentially("world", delay=100) # types slower, like a user - -# Mix characters and named keys -locator.press_sequentially("Hello{Enter}World", named_keys=True) ``` ```csharp await locator.PressSequentiallyAsync("Hello"); // Types instantly await locator.PressSequentiallyAsync("World", new() { Delay = 100 }); // Types slower, like a user - -// Mix characters and named keys -await locator.PressSequentiallyAsync("Hello{Enter}World", new() { NamedKeys = true }); ``` An example of typing into a text field and then submitting the form: @@ -2187,7 +2168,7 @@ await locator.PressAsync("Enter"); * since: v1.38 - `text` <[string]> -String of characters to sequentially press into a focused element. When [`option: namedKeys`] is `true`, anything inside `{}` is treated as a key name (same format as [`method: Locator.press`]). +String of characters to sequentially press into a focused element. ### option: Locator.pressSequentially.delay * since: v1.38 @@ -2195,13 +2176,6 @@ String of characters to sequentially press into a focused element. When [`option Time to wait between key presses in milliseconds. Defaults to 0. -### option: Locator.pressSequentially.namedKeys -* since: v1.61 -- `namedKeys` <[boolean]> - -When [`option: namedKeys`] is `true`, anything inside `{}` is treated as a key name (same format as [`method: Locator.press`]). -Use `{{` to type a literal brace character. Defaults to `false`. - ### option: Locator.pressSequentially.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.38 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index f03a833351206..8d65a2bb6086f 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -14731,10 +14731,6 @@ export interface Locator { * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the * text. * - * When [`namedKeys`](https://playwright.dev/docs/api/class-locator#locator-press-sequentially-option-named-keys) is - * `true`, anything inside `{}` is treated as a key name (same format as - * [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)). - * * To press a special key, like `Control` or `ArrowDown`, use * [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press). * @@ -14743,11 +14739,6 @@ export interface Locator { * ```js * await locator.pressSequentially('Hello'); // Types instantly * await locator.pressSequentially('World', { delay: 100 }); // Types slower, like a user - * - * // Mix characters and named keys - * await locator.pressSequentially('Hello{Enter}World', { namedKeys: true }); - * // Use modifier combos - * await locator.pressSequentially('{Control+A}{Delete}Hello', { namedKeys: true }); * ``` * * An example of typing into a text field and then submitting the form: @@ -14758,10 +14749,7 @@ export interface Locator { * await locator.press('Enter'); * ``` * - * @param text String of characters to sequentially press into a focused element. When - * [`namedKeys`](https://playwright.dev/docs/api/class-locator#locator-press-sequentially-option-named-keys) is - * `true`, anything inside `{}` is treated as a key name (same format as - * [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)). + * @param text String of characters to sequentially press into a focused element. * @param options */ pressSequentially(text: string, options?: { @@ -14770,14 +14758,6 @@ export interface Locator { */ delay?: number; - /** - * When [`namedKeys`](https://playwright.dev/docs/api/class-locator#locator-press-sequentially-option-named-keys) is - * `true`, anything inside `{}` is treated as a key name (same format as - * [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)). Use `{{` to type a - * literal brace character. Defaults to `false`. - */ - namedKeys?: boolean; - /** * This option has no effect. * @deprecated This option has no effect. @@ -19963,10 +19943,6 @@ export interface Keyboard { * * Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. * - * When [`namedKeys`](https://playwright.dev/docs/api/class-keyboard#keyboard-type-option-named-keys) is `true`, - * anything inside `{}` is treated as a key name (same format as - * [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press)). - * * To press a special key, like `Control` or `ArrowDown`, use * [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press). * @@ -19975,9 +19951,6 @@ export interface Keyboard { * ```js * await page.keyboard.type('Hello'); // Types instantly * await page.keyboard.type('World', { delay: 100 }); // Types slower, like a user - * - * // Mix text and special keys - * await page.keyboard.type('Hello{Enter}World', { namedKeys: true }); * ``` * * **NOTE** Modifier keys DO NOT effect `keyboard.type`. Holding down `Shift` will not type the text in upper case. @@ -19992,14 +19965,6 @@ export interface Keyboard { * Time to wait between key presses in milliseconds. Defaults to 0. */ delay?: number; - - /** - * When [`namedKeys`](https://playwright.dev/docs/api/class-keyboard#keyboard-type-option-named-keys) is `true`, - * anything inside `{}` is treated as a key name (same format as - * [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press)). Use `{{` to type - * a literal brace character. Defaults to `false`. - */ - namedKeys?: boolean; }): Promise; /** diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 523e59a669eb2..4a9b428e56fc3 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1605,7 +1605,6 @@ scheme.FrameTypeParams = tObject({ strict: tOptional(tBoolean), text: tString, delay: tOptional(tFloat), - namedKeys: tOptional(tBoolean), timeout: tFloat, }); scheme.FrameTypeResult = tOptional(tObject({})); @@ -2491,7 +2490,6 @@ scheme.PageKeyboardInsertTextResult = tOptional(tObject({})); scheme.PageKeyboardTypeParams = tObject({ text: tString, delay: tOptional(tFloat), - namedKeys: tOptional(tBoolean), }); scheme.PageKeyboardTypeResult = tOptional(tObject({})); scheme.PageKeyboardPressParams = tObject({ diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 4ff97881ebb74..3624a05691a3c 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -774,13 +774,13 @@ export class ElementHandle extends js.JSHandle { return await progress.race(this.evaluateInUtility(([injected, node]) => injected.blurNode(node), {})); } - async type(progress: Progress, text: string, options: { delay?: number, namedKeys?: boolean } & types.StrictOptions): Promise { + async type(progress: Progress, text: string, options: { delay?: number } & types.StrictOptions): Promise { await this._markAsTargetElement(progress); const result = await this._type(progress, text, options); return assertDone(throwRetargetableDOMError(result)); } - async _type(progress: Progress, text: string, options: { delay?: number, namedKeys?: boolean } & types.StrictOptions): Promise<'error:notconnected' | 'done'> { + async _type(progress: Progress, text: string, options: { delay?: number } & types.StrictOptions): Promise<'error:notconnected' | 'done'> { progress.log(`elementHandle.type("${text}")`); await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata)); const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 48a352bcc1f17..31fdaca340b9e 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1470,7 +1470,7 @@ export class Frame extends SdkObject { dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._drop(progress, inputFileItems, data, options))); } - async type(progress: Progress, selector: string, text: string, options: { delay?: number, namedKeys?: boolean, noAutoWaiting?: boolean } & types.StrictOptions) { + async type(progress: Progress, selector: string, text: string, options: { delay?: number, noAutoWaiting?: boolean } & types.StrictOptions) { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._type(progress, text, options))); } diff --git a/packages/playwright-core/src/server/input.ts b/packages/playwright-core/src/server/input.ts index cd69223d0d260..74bde697bf812 100644 --- a/packages/playwright-core/src/server/input.ts +++ b/packages/playwright-core/src/server/input.ts @@ -103,24 +103,20 @@ export class Keyboard { await this._raw.sendText(progress, text); } - async apiType(progress: Progress, text: string, options?: { delay?: number, namedKeys?: boolean }) { + async apiType(progress: Progress, text: string, options?: { delay?: number }) { await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.type(progress, text, options); } - async type(progress: Progress, text: string, options?: { delay?: number, namedKeys?: boolean }) { + async type(progress: Progress, text: string, options?: { delay?: number }) { const delay = (options && options.delay) || undefined; - for (const token of parseNamedKeys(text, !!options?.namedKeys)) { - if (token.type === 'key') { - await this.press(progress, token.value, { delay }); + for (const char of text) { + if (usKeyboardLayout.has(char)) { + await this.press(progress, char, { delay }); } else { - if (usKeyboardLayout.has(token.value)) { - await this.press(progress, token.value, { delay }); - } else { - if (delay) - await progress.wait(delay); - await this.insertText(progress, token.value); - } + if (delay) + await progress.wait(delay); + await this.insertText(progress, char); } } } @@ -358,34 +354,6 @@ function buildLayoutClosure(layout: keyboardLayout.KeyboardLayout): Map { - if (!namedKeys) - return [...text].map(value => ({ type: 'char' as const, value })); - const result: Array<{ type: 'key' | 'char', value: string }> = []; - let i = 0; - while (i < text.length) { - if (text[i] === '{') { - if (i + 1 < text.length && text[i + 1] === '{') { - result.push({ type: 'char', value: '{' }); - i += 2; - } else { - const end = text.indexOf('}', i + 1); - if (end === -1) { - result.push({ type: 'char', value: '{' }); - i += 1; - } else { - result.push({ type: 'key', value: text.substring(i + 1, end) }); - i = end + 1; - } - } - } else { - result.push({ type: 'char', value: text[i] }); - i += 1; - } - } - return result; -} - export interface RawTouchscreen { tap(progress: Progress, x: number, y: number, modifiers: Set): Promise; } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index f03a833351206..8d65a2bb6086f 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -14731,10 +14731,6 @@ export interface Locator { * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the * text. * - * When [`namedKeys`](https://playwright.dev/docs/api/class-locator#locator-press-sequentially-option-named-keys) is - * `true`, anything inside `{}` is treated as a key name (same format as - * [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)). - * * To press a special key, like `Control` or `ArrowDown`, use * [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press). * @@ -14743,11 +14739,6 @@ export interface Locator { * ```js * await locator.pressSequentially('Hello'); // Types instantly * await locator.pressSequentially('World', { delay: 100 }); // Types slower, like a user - * - * // Mix characters and named keys - * await locator.pressSequentially('Hello{Enter}World', { namedKeys: true }); - * // Use modifier combos - * await locator.pressSequentially('{Control+A}{Delete}Hello', { namedKeys: true }); * ``` * * An example of typing into a text field and then submitting the form: @@ -14758,10 +14749,7 @@ export interface Locator { * await locator.press('Enter'); * ``` * - * @param text String of characters to sequentially press into a focused element. When - * [`namedKeys`](https://playwright.dev/docs/api/class-locator#locator-press-sequentially-option-named-keys) is - * `true`, anything inside `{}` is treated as a key name (same format as - * [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)). + * @param text String of characters to sequentially press into a focused element. * @param options */ pressSequentially(text: string, options?: { @@ -14770,14 +14758,6 @@ export interface Locator { */ delay?: number; - /** - * When [`namedKeys`](https://playwright.dev/docs/api/class-locator#locator-press-sequentially-option-named-keys) is - * `true`, anything inside `{}` is treated as a key name (same format as - * [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)). Use `{{` to type a - * literal brace character. Defaults to `false`. - */ - namedKeys?: boolean; - /** * This option has no effect. * @deprecated This option has no effect. @@ -19963,10 +19943,6 @@ export interface Keyboard { * * Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. * - * When [`namedKeys`](https://playwright.dev/docs/api/class-keyboard#keyboard-type-option-named-keys) is `true`, - * anything inside `{}` is treated as a key name (same format as - * [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press)). - * * To press a special key, like `Control` or `ArrowDown`, use * [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press). * @@ -19975,9 +19951,6 @@ export interface Keyboard { * ```js * await page.keyboard.type('Hello'); // Types instantly * await page.keyboard.type('World', { delay: 100 }); // Types slower, like a user - * - * // Mix text and special keys - * await page.keyboard.type('Hello{Enter}World', { namedKeys: true }); * ``` * * **NOTE** Modifier keys DO NOT effect `keyboard.type`. Holding down `Shift` will not type the text in upper case. @@ -19992,14 +19965,6 @@ export interface Keyboard { * Time to wait between key presses in milliseconds. Defaults to 0. */ delay?: number; - - /** - * When [`namedKeys`](https://playwright.dev/docs/api/class-keyboard#keyboard-type-option-named-keys) is `true`, - * anything inside `{}` is treated as a key name (same format as - * [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press)). Use `{{` to type - * a literal brace character. Defaults to `false`. - */ - namedKeys?: boolean; }): Promise; /** diff --git a/packages/protocol/spec/frame.yml b/packages/protocol/spec/frame.yml index a2ca0bfd7103b..706f1d810a560 100644 --- a/packages/protocol/spec/frame.yml +++ b/packages/protocol/spec/frame.yml @@ -681,7 +681,6 @@ Frame: strict: boolean? text: string delay: float? - namedKeys: boolean? timeout: float flags: slowMo: true diff --git a/packages/protocol/spec/page.yml b/packages/protocol/spec/page.yml index 900dc11b5763e..045373af56c4a 100644 --- a/packages/protocol/spec/page.yml +++ b/packages/protocol/spec/page.yml @@ -305,7 +305,6 @@ Page: parameters: text: string delay: float? - namedKeys: boolean? flags: slowMo: true snapshot: true diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 9a19765c00ecb..1f2dc53eb1efe 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2941,13 +2941,11 @@ export type FrameTypeParams = { strict?: boolean, text: string, delay?: number, - namedKeys?: boolean, timeout: number, }; export type FrameTypeOptions = { strict?: boolean, delay?: number, - namedKeys?: boolean, }; export type FrameTypeResult = void; export type FrameUncheckParams = { @@ -4428,11 +4426,9 @@ export type PageKeyboardInsertTextResult = void; export type PageKeyboardTypeParams = { text: string, delay?: number, - namedKeys?: boolean, }; export type PageKeyboardTypeOptions = { delay?: number, - namedKeys?: boolean, }; export type PageKeyboardTypeResult = void; export type PageKeyboardPressParams = { diff --git a/tests/page/page-keyboard.spec.ts b/tests/page/page-keyboard.spec.ts index 8d29c64c96c57..391e2c042e93b 100644 --- a/tests/page/page-keyboard.spec.ts +++ b/tests/page/page-keyboard.spec.ts @@ -787,19 +787,3 @@ it('should close dialog on Escape key press in contenteditable', { await expect(dialog).toHaveJSProperty('open', false); await expect(widget).not.toBeVisible(); }); - -it('should type with namedKeys', async ({ page }) => { - await page.evaluate(() => { - const textarea = document.createElement('textarea'); - document.body.appendChild(textarea); - textarea.focus(); - }); - await page.keyboard.type('He{{ll}o{Enter}Wor{ld', { namedKeys: true }); - expect(await page.evaluate(() => document.querySelector('textarea')!.value)).toBe('He{ll}o\nWor{ld'); -}); - -it('locator should pressSequentially with namedKeys', async ({ page }) => { - await page.setContent(``); - await page.locator('textarea').pressSequentially('Hello{Enter}World', { namedKeys: true }); - expect(await page.$eval('textarea', el => el.value)).toBe('Hello\nWorld'); -}); From 9e45b45ee610083116d85f8d9ac1bd852a400626 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 9 Jun 2026 15:48:12 +0100 Subject: [PATCH 4/6] test: move PW_CLOCK handling into test harness (#41214) --- packages/playwright/src/index.ts | 11 ------ tests/config/browserTest.ts | 25 ++++++++++-- tests/library/browsercontext-proxy.spec.ts | 2 - tests/library/browsercontext-webauthn.spec.ts | 2 - tests/library/browsertype-basic.spec.ts | 1 - tests/library/browsertype-launch.spec.ts | 5 --- tests/library/capabilities.spec.ts | 2 - tests/library/debug-controller.spec.ts | 2 +- tests/library/download.spec.ts | 4 -- tests/library/firefox/launcher.spec.ts | 4 -- tests/library/har.spec.ts | 38 +++++++------------ tests/library/inspector/inspectorTest.ts | 1 - tests/library/launcher.spec.ts | 1 - tests/library/proxy.spec.ts | 2 - tests/library/trace-viewer-scrub.spec.ts | 1 - tests/library/trace-viewer.spec.ts | 1 - tests/library/tracing.spec.ts | 5 --- tests/page/page-event-request.spec.ts | 1 - tests/page/page-filechooser.spec.ts | 1 - tests/page/page-goto.spec.ts | 2 - tests/page/page-history.spec.ts | 1 - tests/page/page-network-response.spec.ts | 1 - tests/page/page-set-input-files.spec.ts | 2 - 23 files changed, 37 insertions(+), 78 deletions(-) diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index e27f1b91c226f..7aa9ccc3d3243 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -418,17 +418,6 @@ const playwrightFixtures: Fixtures { - await context.clock.install({ time: 0 }); - await context.clock.pauseAt(1000); - }, { internal: true }); - } else if (process.env.PW_CLOCK === 'realtime') { - await context._wrapApiCall(async () => { - await context.clock.install({ time: 0 }); - }, { internal: true }); - } - let closed = false; const close = async () => { if (closed) diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index dce99d775f913..2a5c4ba8b5874 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -59,7 +59,9 @@ type BrowserTestTestFixtures = PageTestFixtures & { autoSkipBidiTest: void; }; -const test = baseTest.extend({ +type ContextFactory = (options?: BrowserContextOptions) => Promise<{ context: BrowserContext, close: () => Promise }>; + +const test = baseTest.extend({ browserVersion: [async ({ browser }, run) => { await run(browser.version()); }, { scope: 'worker' }], @@ -115,7 +117,25 @@ const test = baseTest.extend await use(browserName === 'webkit' && (hostPlatform.startsWith('debian11') || hostPlatform.startsWith('ubuntu20.04') || (isMac && macVersion < 15))); }, { scope: 'worker' }], - contextFactory: async ({ _contextFactory }: any, run) => { + _contextFactory: async ({ _contextFactory }, use) => { + await use(async options => { + const result = await _contextFactory(options); + const { context } = result; + if (process.env.PW_CLOCK === 'frozen') { + await (context as any)._wrapApiCall(async () => { + await context.clock.install({ time: 0 }); + await context.clock.pauseAt(1000); + }, { internal: true }); + } else if (process.env.PW_CLOCK === 'realtime') { + await (context as any)._wrapApiCall(async () => { + await context.clock.install({ time: 0 }); + }, { internal: true }); + } + return result; + }); + }, + + contextFactory: async ({ _contextFactory }, run) => { await run(async options => { const { context } = await _contextFactory(options); return context; @@ -123,7 +143,6 @@ const test = baseTest.extend }, createUserDataDir: async ({ mode }, run) => { - test.skip(mode.startsWith('service')); const dirs: string[] = []; // We do not put user data dir in testOutputPath, // because we do not want to upload them as test result artifacts. diff --git a/tests/library/browsercontext-proxy.spec.ts b/tests/library/browsercontext-proxy.spec.ts index 609f32b6c2a04..c5f25e3f5a156 100644 --- a/tests/library/browsercontext-proxy.spec.ts +++ b/tests/library/browsercontext-proxy.spec.ts @@ -16,8 +16,6 @@ import { browserTest as it, expect } from '../config/browserTest'; -it.skip(({ mode }) => mode.startsWith('service')); - it.beforeEach(({ server }) => { server.setRoute('/target.html', async (req, res) => { res.end('Served by the proxy'); diff --git a/tests/library/browsercontext-webauthn.spec.ts b/tests/library/browsercontext-webauthn.spec.ts index 61409c5a27b77..d180e9e93e25a 100644 --- a/tests/library/browsercontext-webauthn.spec.ts +++ b/tests/library/browsercontext-webauthn.spec.ts @@ -16,8 +16,6 @@ import { browserTest as it, expect } from '../config/browserTest'; -it.skip(({ mode }) => mode.startsWith('service')); - it('should not intercept navigator.credentials without install()', async ({ contextFactory, server }) => { const context = await contextFactory(); // Seed a credential, but do not install the interceptor. diff --git a/tests/library/browsertype-basic.spec.ts b/tests/library/browsertype-basic.spec.ts index 39bac27969efc..dacd9969d8f8c 100644 --- a/tests/library/browsertype-basic.spec.ts +++ b/tests/library/browsertype-basic.spec.ts @@ -20,7 +20,6 @@ import { playwrightTest as test, expect } from '../config/browserTest'; test('browserType.executablePath should work', async ({ browserType, channel, mode }) => { test.skip(!!channel, 'We skip browser download when testing a channel'); - test.skip(mode.startsWith('service')); test.skip(!!(browserType as any)._playwright._defaultLaunchOptions.executablePath, 'Skip with custom executable path'); const executablePath = browserType.executablePath(); diff --git a/tests/library/browsertype-launch.spec.ts b/tests/library/browsertype-launch.spec.ts index 11f123c8637af..8c138782ce77e 100644 --- a/tests/library/browsertype-launch.spec.ts +++ b/tests/library/browsertype-launch.spec.ts @@ -62,8 +62,6 @@ it('should throw if page argument is passed', async ({ browserType, browserName, }); it('should reject if launched browser fails immediately', async ({ mode, browserType, asset, channel }) => { - it.skip(mode.startsWith('service')); - const error = await browserType.launch({ executablePath: asset('dummy_bad_browser_executable.js') }).catch(e => e); if (channel === 'webkit-wsl') expect(error.message).toContain('Cannot specify executablePath when using the \"webkit-wsl\" channel.'); @@ -72,7 +70,6 @@ it('should reject if launched browser fails immediately', async ({ mode, browser }); it('should reject if executable path is invalid', async ({ browserType, mode, channel }) => { - it.skip(mode.startsWith('service'), 'on service mode we dont allow passing custom executable path'); let waitError: Error | undefined; await browserType.launch({ executablePath: 'random-invalid-path' }).catch(e => waitError = e); if (channel === 'webkit-wsl') @@ -102,8 +99,6 @@ it('should handle exception and report launch log', async ({ browserType, mode } }); it('should accept objects as options', async ({ mode, browserType }) => { - it.skip(mode.startsWith('service')); - // @ts-expect-error process is not a real option. const browser = await browserType.launch({ process }); await browser.close(); diff --git a/tests/library/capabilities.spec.ts b/tests/library/capabilities.spec.ts index b7b2407bbcf39..30c08692703fa 100644 --- a/tests/library/capabilities.spec.ts +++ b/tests/library/capabilities.spec.ts @@ -68,7 +68,6 @@ it('should respect CSP @smoke', async ({ page, server }) => { it('should play video @smoke', async ({ page, asset, browserName, isWindows, isLinux, mode }) => { it.skip(browserName === 'webkit' && isWindows, 'passes locally but fails on GitHub Action bot, apparently due to a Media Pack issue in the Windows Server'); - it.skip(mode.startsWith('service')); // Safari only plays mp4 so we test WebKit with an .mp4 clip. const fileName = browserName === 'webkit' ? 'video_mp4.html' : 'video.html'; @@ -82,7 +81,6 @@ it('should play video @smoke', async ({ page, asset, browserName, isWindows, isL it('should play webm video @smoke', async ({ page, asset, browserName, platform, macVersion, mode }) => { it.skip(browserName === 'webkit' && platform === 'win32', 'not supported'); - it.skip(mode.startsWith('service')); const absolutePath = asset('video_webm.html'); // Our test server doesn't support range requests required to play on Mac, diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts index 232d818fcbc15..90f26ef4ddf39 100644 --- a/tests/library/debug-controller.spec.ts +++ b/tests/library/debug-controller.spec.ts @@ -76,7 +76,7 @@ const test = baseTest.extend({ }); test.slow(true, 'All controller tests are slow'); -test.skip(({ mode }) => mode.startsWith('service') || mode === 'driver'); +test.skip(({ mode }) => mode === 'driver'); // Force a separate worker to avoid registered selector engines from other tests. // See https://github.com/microsoft/playwright/pull/37103. diff --git a/tests/library/download.spec.ts b/tests/library/download.spec.ts index d64bcd05aa602..cc867bf7122cf 100644 --- a/tests/library/download.spec.ts +++ b/tests/library/download.spec.ts @@ -672,10 +672,6 @@ it('should save to user-specified path', async ({ browser, server, mode }, testI page.waitForEvent('download'), page.click('a') ]); - if (mode.startsWith('service')) { - const error = await download.path().catch(e => e); - expect(error.message).toContain('Path is not available when connecting remotely. Use saveAs() to save a local copy.'); - } const userPath = testInfo.outputPath('download.txt'); await download.saveAs(userPath); expect(fs.existsSync(userPath)).toBeTruthy(); diff --git a/tests/library/firefox/launcher.spec.ts b/tests/library/firefox/launcher.spec.ts index 60ac191fc3021..740c9ccd97316 100644 --- a/tests/library/firefox/launcher.spec.ts +++ b/tests/library/firefox/launcher.spec.ts @@ -20,7 +20,6 @@ import { TestServer } from '../../config/testserver'; import { inheritAndCleanEnv } from '../../config/utils'; it('should pass firefox user preferences', async ({ browserType, mode }) => { - it.skip(mode.startsWith('service')); const browser = await browserType.launch({ firefoxUserPrefs: { 'network.proxy.type': 1, @@ -35,7 +34,6 @@ it('should pass firefox user preferences', async ({ browserType, mode }) => { }); it('should pass firefox user preferences in persistent', async ({ mode, launchPersistent }) => { - it.skip(mode.startsWith('service')); const { page } = await launchPersistent({ firefoxUserPrefs: { 'network.proxy.type': 1, @@ -48,8 +46,6 @@ it('should pass firefox user preferences in persistent', async ({ mode, launchPe }); it('should support custom firefox policies', async ({ browserType, mode, asset, loopback }, testInfo) => { - it.skip(mode.startsWith('service')); - const policies = { 'policies': { 'Certificates': { diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index a1cb818b933a0..1360b6362bae6 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -619,10 +619,8 @@ it('should have connection details', async ({ contextFactory, server, browserNam await page.goto(server.EMPTY_PAGE); const log = await getLog(); const { serverIPAddress, _serverPort: port, _securityDetails: securityDetails } = log.entries[0]; - if (!mode.startsWith('service')) { - expect(serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); - expect(port).toBe(server.PORT); - } + expect(serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); + expect(port).toBe(server.PORT); expect(securityDetails).toEqual({}); }); @@ -634,10 +632,8 @@ it('should have security details', async ({ contextFactory, httpsServer, browser await page.request.get(httpsServer.EMPTY_PAGE); const log = await getLog(); const { serverIPAddress, _serverPort: port, _securityDetails: securityDetails } = log.entries[0]; - if (!mode.startsWith('service')) { - expect(serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); - expect(port).toBe(httpsServer.PORT); - } + expect(serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); + expect(port).toBe(httpsServer.PORT); if (browserName === 'webkit') expect(securityDetails).toEqual({ protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 }); else @@ -658,16 +654,14 @@ it('should have connection details for redirects', async ({ contextFactory, serv if (browserName === 'webkit') { expect(detailsFoo.serverIPAddress).toBeUndefined(); expect(detailsFoo._serverPort).toBeUndefined(); - } else if (!mode.startsWith('service')) { + } else { expect(detailsFoo.serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); expect(detailsFoo._serverPort).toBe(server.PORT); } - if (!mode.startsWith('service')) { - const detailsEmpty = log.entries[1]; - expect(detailsEmpty.serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); - expect(detailsEmpty._serverPort).toBe(server.PORT); - } + const detailsEmpty = log.entries[1]; + expect(detailsEmpty.serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); + expect(detailsEmpty._serverPort).toBe(server.PORT); }); it('should have connection details for failed requests', async ({ contextFactory, server, browserName, platform, mode }, testInfo) => { @@ -678,20 +672,16 @@ it('should have connection details for failed requests', async ({ contextFactory const { page, getLog } = await pageWithHar(contextFactory, testInfo); await page.goto(server.PREFIX + '/one-style.html'); const log = await getLog(); - if (!mode.startsWith('service')) { - const { serverIPAddress, _serverPort: port } = log.entries[0]; - expect(serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); - expect(port).toBe(server.PORT); - } + const { serverIPAddress, _serverPort: port } = log.entries[0]; + expect(serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); + expect(port).toBe(server.PORT); }); it('should return server address directly from response', async ({ page, server, mode }) => { const response = await page.goto(server.EMPTY_PAGE); - if (!mode.startsWith('service')) { - const { ipAddress, port } = (await response!.serverAddr())!; - expect(ipAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); - expect(port).toBe(server.PORT); - } + const { ipAddress, port } = (await response!.serverAddr())!; + expect(ipAddress).toMatch(/^127\.0\.0\.1|\[::1\]/); + expect(port).toBe(server.PORT); }); it('should return security details directly from response', async ({ contextFactory, httpsServer, browserName, platform, channel }) => { diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index 2f261c9c998a8..9fa145a651b01 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -74,7 +74,6 @@ export const test = contextTest.extend({ runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions }, run, testInfo) => { testInfo.slow(); - testInfo.skip(mode.startsWith('service')); let cli: CLIMock | undefined; await run(cliArgs => { diff --git a/tests/library/launcher.spec.ts b/tests/library/launcher.spec.ts index 8c2c37a9ef9f8..ba29451d5c4f0 100644 --- a/tests/library/launcher.spec.ts +++ b/tests/library/launcher.spec.ts @@ -44,7 +44,6 @@ it('should kill browser process on timeout after close', async ({ browserType, m it('should throw a friendly error if its headed and there is no xserver on linux running', async ({ mode, browserType, platform, channel }) => { it.skip(platform !== 'linux'); - it.skip(mode.startsWith('service')); it.skip(channel === 'chromium-headless-shell', 'shell is never headed'); it.skip(channel === 'chromium-tip-of-tree-headless-shell', 'shell is never headed'); diff --git a/tests/library/proxy.spec.ts b/tests/library/proxy.spec.ts index 3a01d101de750..ea3087ca3e67d 100644 --- a/tests/library/proxy.spec.ts +++ b/tests/library/proxy.spec.ts @@ -18,8 +18,6 @@ import { setupSocksForwardingServer } from '../config/proxy'; import { playwrightTest as it, expect } from '../config/browserTest'; import net from 'net'; -it.skip(({ mode }) => mode.startsWith('service')); - it('should throw for bad server value', async ({ browserType }) => { const error = await browserType.launch({ // @ts-expect-error server must be a string diff --git a/tests/library/trace-viewer-scrub.spec.ts b/tests/library/trace-viewer-scrub.spec.ts index c1c609c0553e7..0b5856d208782 100644 --- a/tests/library/trace-viewer-scrub.spec.ts +++ b/tests/library/trace-viewer-scrub.spec.ts @@ -21,7 +21,6 @@ import { expect, playwrightTest } from '../config/browserTest'; const test = playwrightTest.extend(traceViewerFixtures); test.skip(({ trace }) => trace === 'on'); -test.skip(({ mode }) => mode.startsWith('service')); test.skip(process.env.PW_CLOCK === 'frozen'); test.slow(); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 1ea2337efc018..9e69039b7a695 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -32,7 +32,6 @@ const test = playwrightTest.extend(traceViewerFixtures); // NOTE: set PWTEST_DEBUG_TRACE_VIEWER=1 to record/attach traces for these tests. test.skip(({ trace }) => trace === 'on'); -test.skip(({ mode }) => mode.startsWith('service')); test.skip(process.env.PW_CLOCK === 'frozen'); test.slow(); diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index bd077ed4d5019..b565683b6d2d7 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -208,8 +208,6 @@ test('should collect two traces', async ({ context, page, server }, testInfo) => }); test('should respect tracesDir and name', async ({ browserType, server, mode }, testInfo) => { - test.skip(mode.startsWith('service'), 'Service ignores tracesDir'); - const tracesDir = testInfo.outputPath('traces'); const browser = await browserType.launch({ tracesDir }); const context = await browser.newContext(); @@ -717,7 +715,6 @@ test('should store postData for global request', async ({ request, server }, tes }); test('should not flush console events', async ({ context, page, mode }, testInfo) => { - test.skip(mode.startsWith('service'), 'Uses artifactsFolderName'); const testId = test.info().testId; await context.tracing.start({ name: testId }); const promise = new Promise(f => { @@ -782,8 +779,6 @@ test('should flush console events on tracing stop', async ({ context, page }, te }); test('should not emit after w/o before', async ({ browserType, mode }, testInfo) => { - test.skip(mode.startsWith('service'), 'Service ignores tracesDir'); - const tracesDir = testInfo.outputPath('traces'); const browser = await browserType.launch({ tracesDir }); const context = await browser.newContext(); diff --git a/tests/page/page-event-request.spec.ts b/tests/page/page-event-request.spec.ts index 263f1f3c5001a..284c3ebfa4ce1 100644 --- a/tests/page/page-event-request.spec.ts +++ b/tests/page/page-event-request.spec.ts @@ -85,7 +85,6 @@ it('should report requests and responses handled by service worker', async ({ pa it('should report requests and responses handled by service worker with routing', async ({ page, server, isAndroid, isElectron, mode, browserName, platform }) => { it.fixme(isAndroid); it.fixme(isElectron); - it.fixme(mode.startsWith('service') && platform === 'linux', 'Times out for no clear reason'); const interceptedUrls = []; await page.route('**/*', route => { diff --git a/tests/page/page-filechooser.spec.ts b/tests/page/page-filechooser.spec.ts index 6c4262e41d74e..aa89ec3964161 100644 --- a/tests/page/page-filechooser.spec.ts +++ b/tests/page/page-filechooser.spec.ts @@ -23,7 +23,6 @@ import formidable from 'formidable'; test('should upload multiple large files', async ({ page, server, isAndroid, mode }, testInfo) => { test.skip(isAndroid); - test.skip(mode.startsWith('service')); test.slow(); const filesCount = 10; diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index d6bbad9856e37..90c0ad8930d08 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -26,7 +26,6 @@ it('should work @smoke', async ({ page, server }) => { it('should work with file URL', async ({ page, asset, isAndroid, mode, channel }) => { it.skip(isAndroid, 'No files on Android'); - it.skip(mode.startsWith('service')); it.skip(channel === 'webkit-wsl', 'separate filesystem on wsl'); const fileurl = url.pathToFileURL(asset('empty.html')).href; @@ -37,7 +36,6 @@ it('should work with file URL', async ({ page, asset, isAndroid, mode, channel } it('should work with file URL with subframes', async ({ page, asset, isAndroid, mode, channel }) => { it.skip(isAndroid, 'No files on Android'); - it.skip(mode.startsWith('service')); it.skip(channel === 'webkit-wsl', 'separate filesystem on wsl'); const fileurl = url.pathToFileURL(asset('frames/two-frames.html')).href; diff --git a/tests/page/page-history.spec.ts b/tests/page/page-history.spec.ts index 76e41764313af..8d76bb89a6302 100644 --- a/tests/page/page-history.spec.ts +++ b/tests/page/page-history.spec.ts @@ -54,7 +54,6 @@ it('page.goBack should work with HistoryAPI', async ({ page, server }) => { it('page.goBack should work for file urls', async ({ page, server, asset, channel, isAndroid, mode }) => { it.skip(isAndroid, 'No files on Android'); - it.skip(mode.startsWith('service')); it.skip(channel === 'webkit-wsl'); const url1 = url.pathToFileURL(asset('consolelog.html')).href; diff --git a/tests/page/page-network-response.spec.ts b/tests/page/page-network-response.spec.ts index 7eb7e23024486..76a56152387e6 100644 --- a/tests/page/page-network-response.spec.ts +++ b/tests/page/page-network-response.spec.ts @@ -253,7 +253,6 @@ it('should behave the same way for headers and allHeaders', async ({ page, serve it('should provide a Response with a file URL', async ({ page, asset, isAndroid, isElectron, isWindows, browserName, mode, channel }) => { it.skip(isAndroid, 'No files on Android'); it.skip(browserName === 'firefox', 'Firefox does return null for file:// URLs'); - it.skip(mode.startsWith('service')); it.skip(channel === 'webkit-wsl'); const fileurl = url.pathToFileURL(asset('frames/two-frames.html')).href; diff --git a/tests/page/page-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts index c443700b81a20..b5085c712e148 100644 --- a/tests/page/page-set-input-files.spec.ts +++ b/tests/page/page-set-input-files.spec.ts @@ -145,7 +145,6 @@ test('should upload a file after popup', async ({ page, server, asset }) => { test('should upload large file', async ({ page, server, isAndroid, mode }, testInfo) => { test.skip(isAndroid); - test.skip(mode.startsWith('service')); test.slow(); await page.goto(server.PREFIX + '/input/fileupload.html'); @@ -202,7 +201,6 @@ test('should throw an error if the file does not exist', async ({ page, server, test('should upload large file with relative path', async ({ page, server, isAndroid, mode }, testInfo) => { test.skip(isAndroid); - test.skip(mode.startsWith('service')); test.slow(); await page.goto(server.PREFIX + '/input/fileupload.html'); From 66afdb0248250e325d694636b201f329a5f55751 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Tue, 9 Jun 2026 12:06:29 -0400 Subject: [PATCH 5/6] feat(firefox): support mobile mode (#41179) --- packages/playwright-core/browsers.json | 2 +- .../playwright-core/src/cli/browserActions.ts | 3 --- .../src/server/firefox/ffBrowser.ts | 4 ++-- .../playwright-core/src/server/firefox/ffPage.ts | 8 ++++++-- .../src/server/firefox/protocol.d.ts | 15 +++++++++++++++ tests/library/browsercontext-viewport.spec.ts | 4 +--- tests/library/defaultbrowsercontext-2.spec.ts | 2 -- tests/library/video.spec.ts | 2 -- 8 files changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 759927d554455..d12a5e8669e55 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -31,7 +31,7 @@ }, { "name": "firefox", - "revision": "1528", + "revision": "1530", "installByDefault": true, "browserVersion": "151.0", "title": "Firefox" diff --git a/packages/playwright-core/src/cli/browserActions.ts b/packages/playwright-core/src/cli/browserActions.ts index 930e04019316a..d6558c8bdaf36 100644 --- a/packages/playwright-core/src/cli/browserActions.ts +++ b/packages/playwright-core/src/cli/browserActions.ts @@ -85,9 +85,6 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro delete contextOptions.isMobile; } - if (contextOptions.isMobile && browserType.name() === 'firefox') - contextOptions.isMobile = undefined; - if (options.blockServiceWorkers) contextOptions.serviceWorkers = 'block'; diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index c3dc91c9e8ce6..59e8c0c716c22 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -90,8 +90,6 @@ export class FFBrowser extends Browser { } async doCreateNewContext(options: types.BrowserContextOptions): Promise { - if (options.isMobile) - throw new Error('options.isMobile is not supported in Firefox'); const { browserContextId } = await this.session.send('Browser.createBrowserContext', { removeOnDetach: true }); const context = new FFBrowserContext(this, browserContextId, options); await context.initialize(); @@ -359,7 +357,9 @@ export class FFBrowserContext extends BrowserContext { return; const viewport = { viewportSize: { width: this._options.viewport.width, height: this._options.viewport.height }, + screenSize: this._options.screen, deviceScaleFactor: this._options.deviceScaleFactor || 1, + isMobile: !!this._options.isMobile, }; await this._browser.session.send('Browser.setDefaultViewport', { browserContextId: this._browserContextId, viewport }); } diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index c269a97763069..12f386f3560b9 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -380,8 +380,12 @@ export class FFPage implements PageDelegate { } async updateEmulatedViewportSize(): Promise { - const viewportSize = this._page.emulatedSize()?.viewport ?? null; - await this._session.send('Page.setViewportSize', { viewportSize }); + const emulatedSize = this._page.emulatedSize(); + await this._session.send('Page.setViewportSize', { + viewportSize: emulatedSize?.viewport ?? null, + screenSize: emulatedSize?.screen, + isMobile: !!this._browserContext._options.isMobile, + }); } async bringToFront(): Promise { diff --git a/packages/playwright-core/src/server/firefox/protocol.d.ts b/packages/playwright-core/src/server/firefox/protocol.d.ts index 1fbad16e2d7f2..e55a72ce404b7 100644 --- a/packages/playwright-core/src/server/firefox/protocol.d.ts +++ b/packages/playwright-core/src/server/firefox/protocol.d.ts @@ -214,6 +214,11 @@ export namespace Protocol { height: number; }; deviceScaleFactor?: number; + screenSize?: { + width: number; + height: number; + }; + isMobile?: boolean; }|null; }; export type setDefaultViewportReturnValue = void; @@ -343,6 +348,11 @@ export namespace Protocol { height: number; }; deviceScaleFactor?: number; + screenSize?: { + width: number; + height: number; + }; + isMobile?: boolean; }; export type DOMQuad = { p1: { @@ -523,6 +533,11 @@ export namespace Protocol { height: number; }|null; deviceScaleFactor?: number; + screenSize?: { + width: number; + height: number; + }; + isMobile?: boolean; }; export type setViewportSizeReturnValue = void; export type setZoomParameters = { diff --git a/tests/library/browsercontext-viewport.spec.ts b/tests/library/browsercontext-viewport.spec.ts index a3e4fefb42311..787b19bb9b0b1 100644 --- a/tests/library/browsercontext-viewport.spec.ts +++ b/tests/library/browsercontext-viewport.spec.ts @@ -130,7 +130,6 @@ browserTest('should support touch with null viewport', async ({ browser, server }); it('should set both screen and viewport options', async ({ contextFactory, browserName, isBidi }) => { - it.fail(browserName === 'firefox' && !isBidi, 'Screen size is reset to viewport'); const context = await contextFactory({ screen: { 'width': 1280, 'height': 720 }, viewport: { 'width': 1000, 'height': 600 }, @@ -187,9 +186,8 @@ browserTest('should be able to get correct orientation angle on non-mobile devic await context.close(); }); -it('should set window.screen.orientation.type for mobile devices', async ({ contextFactory, browserName, server, isBidi }) => { +it('should set window.screen.orientation.type for mobile devices', async ({ contextFactory, server }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31151' }); - it.skip(browserName === 'firefox' && !isBidi, 'Firefox does not support mobile emulation'); const context = await contextFactory(devices['iPhone 14']); const page = await context.newPage(); await page.goto(server.PREFIX + '/index.html'); diff --git a/tests/library/defaultbrowsercontext-2.spec.ts b/tests/library/defaultbrowsercontext-2.spec.ts index 9b95c4f91d445..516f114a8b141 100644 --- a/tests/library/defaultbrowsercontext-2.spec.ts +++ b/tests/library/defaultbrowsercontext-2.spec.ts @@ -28,8 +28,6 @@ it('should support hasTouch option', async ({ server, launchPersistent }) => { }); it('should work in persistent context', async ({ server, launchPersistent, browserName }) => { - it.skip(browserName === 'firefox', 'Firefox does not support mobile'); - const { page } = await launchPersistent({ viewport: { width: 320, height: 480 }, isMobile: true }); await page.goto(server.PREFIX + '/empty.html'); expect(await page.evaluate(() => window.innerWidth)).toBe(980); diff --git a/tests/library/video.spec.ts b/tests/library/video.spec.ts index 0bdd999cbf274..848e633297683 100644 --- a/tests/library/video.spec.ts +++ b/tests/library/video.spec.ts @@ -503,8 +503,6 @@ it.describe('screencast', () => { }); it('should emulate an iphone', async ({ contextFactory, playwright, browserName }, testInfo) => { - it.skip(browserName === 'firefox', 'isMobile is not supported in Firefox'); - const device = playwright.devices['iPhone 6']; const context = await contextFactory({ ...device, From e8e8d69569de6ad8885b50664bdfd0dc3e8315ed Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 9 Jun 2026 09:43:20 -0700 Subject: [PATCH 6/6] Revert "fix(download): clear error when saving downloads from a non-collocated browser" (#41217) --- .../playwright-core/src/server/artifact.ts | 9 ----- .../server/dispatchers/artifactDispatcher.ts | 10 ------ .../playwright-core/src/server/download.ts | 5 --- .../library/chromium/connect-over-cdp.spec.ts | 35 ------------------- 4 files changed, 59 deletions(-) diff --git a/packages/playwright-core/src/server/artifact.ts b/packages/playwright-core/src/server/artifact.ts index 562d2d9b9ae37..1a08e7e133737 100644 --- a/packages/playwright-core/src/server/artifact.ts +++ b/packages/playwright-core/src/server/artifact.ts @@ -29,7 +29,6 @@ type CancelCallback = () => Promise; export class Artifact extends SdkObject { private _localPath: string; private _unaccessibleErrorMessage: string | undefined; - private _missingFileErrorMessage: string | undefined; private _cancelCallback: CancelCallback | undefined; private _finishedPromise = new ManualPromise(); private _saveCallbacks: SaveCallback[] = []; @@ -44,14 +43,6 @@ export class Artifact extends SdkObject { this._cancelCallback = cancelCallback; } - markMissingFileErrorMessage(errorMessage: string) { - this._missingFileErrorMessage = errorMessage; - } - - missingFileErrorMessage(): string | undefined { - return this._missingFileErrorMessage; - } - async localPathAfterFinished(progress: Progress): Promise { return await progress.race(this._localPathAfterFinished()); } diff --git a/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts b/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts index 2ff6dff8c23b8..ff4e0f868bd60 100644 --- a/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts @@ -46,13 +46,11 @@ export class ArtifactDispatcher extends Dispatcher { - this._assertFileAccessible(); const path = await this._object.localPathAfterFinished(progress); return { value: path }; } async saveAs(params: channels.ArtifactSaveAsParams, progress: Progress): Promise { - this._assertFileAccessible(); return await progress.race(new Promise((resolve, reject) => { this._object.saveAs(progress, async (localPath, error) => { if (error) { @@ -70,14 +68,7 @@ export class ArtifactDispatcher extends Dispatcher { - this._assertFileAccessible(); return await progress.race(new Promise((resolve, reject) => { this._object.saveAs(progress, async (localPath, error) => { if (error) { @@ -103,7 +94,6 @@ export class ArtifactDispatcher extends Dispatcher { - this._assertFileAccessible(); const fileName = await this._object.localPathAfterFinished(progress); const readable = fs.createReadStream(fileName, { highWaterMark: 1024 * 1024 }); return { stream: new StreamDispatcher(this, readable) }; diff --git a/packages/playwright-core/src/server/download.ts b/packages/playwright-core/src/server/download.ts index 91958c3617ffa..0c8725c35b2f6 100644 --- a/packages/playwright-core/src/server/download.ts +++ b/packages/playwright-core/src/server/download.ts @@ -32,11 +32,6 @@ export class Download { if (!downloadPath) throw new Error(`Download filename '${downloadFilename}' escapes download directory`); this.artifact = new Artifact(page, downloadPath, unaccessibleErrorMessage, () => this.cancel()); - if (!page.browserContext._browser._isBrowserCollocatedWithServer) { - this.artifact.markMissingFileErrorMessage( - `Downloaded file is not accessible from the Playwright server because the browser is running on a different host ` + - `(e.g. connected over CDP to a remote browser). Saving downloads requires the browser and the Playwright server to share a filesystem.`); - } this._page = page; this.url = url; this._uuid = uuid; diff --git a/tests/library/chromium/connect-over-cdp.spec.ts b/tests/library/chromium/connect-over-cdp.spec.ts index 5ebca4c902078..13ec3ee49cea2 100644 --- a/tests/library/chromium/connect-over-cdp.spec.ts +++ b/tests/library/chromium/connect-over-cdp.spec.ts @@ -113,7 +113,6 @@ test('should connectOverCDP and manage downloads in default context', async ({ b try { const browser = await browserType.connectOverCDP({ endpointURL: `http://127.0.0.1:${port}/`, - isLocal: true, }); const page = await browser.contexts()[0].newPage(); await page.setContent(`download`); @@ -135,40 +134,6 @@ test('should connectOverCDP and manage downloads in default context', async ({ b } }); -test('should give a clear error for downloads when browser is not co-located with the server', async ({ browserType, server }, testInfo) => { - test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41060' }); - server.setRoute('/downloadWithFilename', (req, res) => { - res.setHeader('Content-Type', 'application/octet-stream'); - res.setHeader('Content-Disposition', 'attachment; filename=file.txt'); - res.end(`Hello world`); - }); - - const port = 9339 + testInfo.workerIndex; - const browserServer = await browserType.launch({ - args: ['--remote-debugging-port=' + port] - }); - - try { - const browser = await browserType.connectOverCDP({ - endpointURL: `http://127.0.0.1:${port}/`, - }); - const page = await browser.contexts()[0].newPage(); - await page.setContent(`download`); - - const [download] = await Promise.all([ - page.waitForEvent('download'), - page.click('a') - ]); - - const saveError = await download.saveAs(testInfo.outputPath('download.txt')).catch(e => e); - expect(saveError.message).toContain('the browser is running on a different host'); - const pathError = await download.path().catch(e => e); - expect(pathError.message).toContain('the browser is running on a different host'); - } finally { - await browserServer.close(); - } -}); - test('should connect to an existing cdp session twice', async ({ browserType, mode, server }, testInfo) => { const port = 9339 + testInfo.workerIndex; const browserServer = await browserType.launch({