Merge branch 'main' into sharding-algorithm

This commit is contained in:
Mathias Leppich 2024-05-31 00:02:24 +02:00
commit 8aee7df482
52 changed files with 3379 additions and 223 deletions

View file

@ -32,7 +32,7 @@ jobs:
const title = '[Ports]: Backport client side changes for ' + currentPlaywrightVersion;
for (const repo of ['playwright-python', 'playwright-java', 'playwright-dotnet']) {
const { data: issuesData } = await github.rest.search.issuesAndPullRequests({
q: `is:issue is:open repo:microsoft/${repo} in:title "${title}" author:playwrightmachine"`
q: `is:issue is:open repo:microsoft/${repo} in:title "${title}" author:playwrightmachine`
})
let issueNumber = null;
let issueBody = '';

View file

@ -1,6 +1,6 @@
# 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-126.0.6478.17-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-126.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop -->
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-126.0.6478.26-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-126.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop -->
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->126.0.6478.17<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->126.0.6478.26<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->126.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

View file

@ -189,7 +189,7 @@ All responses returned by [`method: APIRequestContext.get`] and similar methods
* since: v1.45
- `reason` <[string]>
The reason to be reported to the operations interrupted by the context disposure.
The reason to be reported to the operations interrupted by the context disposal.
## async method: APIRequestContext.fetch
* since: v1.16

View file

@ -98,6 +98,12 @@ context.BackgroundPage += (_, backgroundPage) =>
```
## property: BrowserContext.clock
* since: v1.45
- type: <[Clock]>
Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock.
## event: BrowserContext.close
* since: v1.8
- argument: <[BrowserContext]>

View file

@ -0,0 +1,86 @@
# class: Clock
* since: v1.45
Playwright uses [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) for clock emulation. Clock is installed for the entire [BrowserContext], so the time
in all the pages and iframes is controlled by the same clock.
## async method: Clock.install
* since: v1.45
Creates a clock and installs it globally.
### option: Clock.install.now
* since: v1.45
- `now` <[int]|[Date]>
Install fake timers with the specified unix epoch (default: 0).
### option: Clock.install.toFake
* since: v1.45
- `toFake` <[Array]<[FakeMethod]<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">>>
An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake: ['setTimeout'] })` will fake only `setTimeout()`.
By default, `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` and `Date` are faked.
### option: Clock.install.loopLimit
* since: v1.45
- `loopLimit` <[int]>
The maximum number of timers that will be run when calling [`method: Clock.runAll`]. Defaults to `1000`.
### option: Clock.install.shouldAdvanceTime
* since: v1.45
- `shouldAdvanceTime` <[boolean]>
Tells `@sinonjs/fake-timers` to increment mocked time automatically based on the real system time shift (e.g., the mocked time will be incremented by
20ms for every 20ms change in the real system time). Defaults to `false`.
### option: Clock.install.advanceTimeDelta
* since: v1.45
- `advanceTimeDelta` <[int]>
Relevant only when using with [`option: shouldAdvanceTime`]. Increment mocked time by advanceTimeDelta ms every advanceTimeDelta ms change
in the real system time (default: 20).
## async method: Clock.jump
* since: v1.45
Advance the clock by jumping forward in time, firing callbacks at most once. Returns fake milliseconds since the unix epoch.
This can be used to simulate the JS engine (such as a browser) being put to sleep and resumed later, skipping intermediary timers.
### param: Clock.jump.time
* since: v1.45
- `time` <[int]|[string]>
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
## async method: Clock.runAll
* since: v1.45
- returns: <[int]> Fake milliseconds since the unix epoch.
Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well.
This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, or the delays in those timers.
It runs a maximum of [`option: loopLimit`] times after which it assumes there is an infinite loop of timers and throws an error.
## async method: Clock.runToLast
* since: v1.45
- returns: <[int]> Fake milliseconds since the unix epoch.
This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as necessary.
If new timers are added while it is executing they will be run only if they would occur before this time.
This is useful when you want to run a test to completion, but the test recursively sets timers that would cause runAll to trigger an infinite loop warning.
## async method: Clock.tick
* since: v1.45
- returns: <[int]> Fake milliseconds since the unix epoch.
Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch.
### param: Clock.tick.time
* since: v1.45
- `time` <[int]|[string]>
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.

View file

@ -151,6 +151,12 @@ page.Load += PageLoadHandler;
page.Load -= PageLoadHandler;
```
## property: Page.clock
* since: v1.45
- type: <[Clock]>
Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock.
## event: Page.close
* since: v1.8
- argument: <[Page]>

View file

@ -571,7 +571,7 @@ Whether to emulate network being offline. Defaults to `false`. Learn more about
- `username` <[string]>
- `password` <[string]>
- `origin` ?<[string]> Restrain sending http credentials on specific origin (scheme://host:port).
- `sendImmediately` ?<[boolean]> Whether to send `Authorization` header with the first API request. By default, the credentials are sent only when 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent from the browser.
- `send` ?<[HttpCredentialsSend]<"unauthorized"|"always">> This option only applies to the requests sent from corresponding [APIRequestContext] and does not affect requests sent from the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with `WWW-Authenticate` header is received. Defaults to `'unauthorized'`.
Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
If no origin is specified, the username and password are sent to any servers upon unauthorized responses.

View file

@ -45,8 +45,14 @@ The testing sidebar can be opened by clicking on the testing icon in the activit
You can run a single test by clicking the green triangle next to your test block to run your test. Playwright will run through each line of the test and when it finishes you will see a green tick next to your test block as well as the time it took to run the test.
<LiteYouTube
id="mQmcIBMsc38"
title="Getting Started with Playwright in VS Code"
/>
![run a single test](https://github.com/microsoft/playwright/assets/13063165/69dbccfc-4e9f-40e7-bcdf-7d5c5a11f988)
### Run tests and show browsers
You can also run your tests and show the browsers by selecting the option **Show Browsers** in the testing sidebar. Then when you click the green triangle to run your test the browser will open and you will visually see it run through your test. Leave this selected if you want browsers open for all your tests or uncheck it if you prefer your tests to run in headless mode with no browser open.

View file

@ -443,7 +443,7 @@ Test files that took more than `threshold` milliseconds are considered slow, and
* since: v1.45
- type: ?<[boolean]>
Whether to skip entries from `.gitignore` when searching for test files. By default, if neither [`property: TestConfig.testDir`] nor [`property: TestProject.testDir`] are explicitely specified, Playwright will ignore any test files matching `.gitignore` entries. This option allows to override that behavior.
Whether to skip entries from `.gitignore` when searching for test files. By default, if neither [`property: TestConfig.testDir`] nor [`property: TestProject.testDir`] are explicitly specified, Playwright will ignore any test files matching `.gitignore` entries.
## property: TestConfig.retries
* since: v1.10

View file

@ -3,31 +3,31 @@
"browsers": [
{
"name": "chromium",
"revision": "1120",
"revision": "1121",
"installByDefault": true,
"browserVersion": "126.0.6478.17"
"browserVersion": "126.0.6478.26"
},
{
"name": "chromium-tip-of-tree",
"revision": "1226",
"revision": "1227",
"installByDefault": false,
"browserVersion": "127.0.6505.0"
"browserVersion": "127.0.6510.0"
},
{
"name": "firefox",
"revision": "1451",
"revision": "1452",
"installByDefault": true,
"browserVersion": "126.0"
},
{
"name": "firefox-beta",
"revision": "1451",
"revision": "1452",
"installByDefault": false,
"browserVersion": "127.0b3"
},
{
"name": "webkit",
"revision": "2013",
"revision": "2014",
"installByDefault": true,
"revisionOverrides": {
"mac10.14": "1446",

View file

@ -20,6 +20,7 @@ export { Browser } from './browser';
export { BrowserContext } from './browserContext';
export type { BrowserServer } from './browserType';
export { BrowserType } from './browserType';
export { Clock } from './clock';
export { ConsoleMessage } from './consoleMessage';
export { Coverage } from './coverage';
export { Dialog } from './dialog';

View file

@ -44,6 +44,7 @@ import { ConsoleMessage } from './consoleMessage';
import { Dialog } from './dialog';
import { WebError } from './webError';
import { TargetClosedError, parseError } from './errors';
import { Clock } from './clock';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
_pages = new Set<Page>();
@ -58,6 +59,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
readonly request: APIRequestContext;
readonly tracing: Tracing;
readonly clock: Clock;
readonly _backgroundPages = new Set<Page>();
readonly _serviceWorkers = new Set<Worker>();
readonly _isChromium: boolean;
@ -82,6 +85,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this._isChromium = this._browser?._name === 'chromium';
this.tracing = Tracing.from(initializer.tracing);
this.request = APIRequestContext.from(initializer.requestContext);
this.clock = new Clock(this);
this._channel.on('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding)));
this._channel.on('close', () => this._onClose());

View file

@ -0,0 +1,57 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type * as api from '../../types/types';
import type * as channels from '@protocol/channels';
import type { BrowserContext } from './browserContext';
export class Clock implements api.Clock {
private _browserContext: BrowserContext;
constructor(browserContext: BrowserContext) {
this._browserContext = browserContext;
}
async install(options?: Omit<channels.BrowserContextClockInstallOptions, 'now'> & { now?: number | Date }) {
const now = options && options.now ? (options.now instanceof Date ? options.now.getTime() : options.now) : undefined;
await this._browserContext._channel.clockInstall({ ...options, now });
}
async jump(time: number | string) {
await this._browserContext._channel.clockJump({
timeNumber: typeof time === 'number' ? time : undefined,
timeString: typeof time === 'string' ? time : undefined
});
}
async runAll(): Promise<number> {
const result = await this._browserContext._channel.clockRunAll();
return result.fakeTime;
}
async runToLast(): Promise<number> {
const result = await this._browserContext._channel.clockRunToLast();
return result.fakeTime;
}
async tick(time: number | string): Promise<number> {
const result = await this._browserContext._channel.clockTick({
timeNumber: typeof time === 'number' ? time : undefined,
timeString: typeof time === 'string' ? time : undefined
});
return result.fakeTime;
}
}

View file

@ -49,6 +49,7 @@ import { Video } from './video';
import { Waiter } from './waiter';
import { Worker } from './worker';
import { HarRouter } from './harRouter';
import type { Clock } from './clock';
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
width?: string | number,
@ -87,6 +88,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
readonly mouse: Mouse;
readonly request: APIRequestContext;
readonly touchscreen: Touchscreen;
readonly clock: Clock;
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
readonly _timeoutSettings: TimeoutSettings;
@ -116,6 +119,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this.mouse = new Mouse(this);
this.request = this._browserContext.request;
this.touchscreen = new Touchscreen(this);
this.clock = this._browserContext.clock;
this._mainFrame = Frame.from(initializer.mainFrame);
this._mainFrame._page = this;

View file

@ -34,8 +34,8 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
}
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) {
this._includeSources = !!options.sources;
const traceName = await this._wrapApiCall(async () => {
await this._wrapApiCall(async () => {
this._includeSources = !!options.sources;
await this._channel.tracingStart({
name: options.name,
snapshots: options.snapshots,
@ -43,14 +43,15 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
live: options._live,
});
const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
return response.traceName;
await this._startCollectingStacks(response.traceName);
}, true);
await this._startCollectingStacks(traceName);
}
async startChunk(options: { name?: string, title?: string } = {}) {
const { traceName } = await this._channel.tracingStartChunk(options);
await this._startCollectingStacks(traceName);
await this._wrapApiCall(async () => {
const { traceName } = await this._channel.tracingStartChunk(options);
await this._startCollectingStacks(traceName);
}, true);
}
private async _startCollectingStacks(traceName: string) {

View file

@ -334,7 +334,7 @@ scheme.PlaywrightNewRequestParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
send: tOptional(tEnum(['always', 'unauthorized'])),
})),
proxy: tOptional(tObject({
server: tString,
@ -548,7 +548,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
send: tOptional(tEnum(['always', 'unauthorized'])),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),
@ -627,7 +627,7 @@ scheme.BrowserNewContextParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
send: tOptional(tEnum(['always', 'unauthorized'])),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),
@ -689,7 +689,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
send: tOptional(tEnum(['always', 'unauthorized'])),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),
@ -963,6 +963,34 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({
enabled: tBoolean,
});
scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
scheme.BrowserContextClockInstallParams = tObject({
now: tOptional(tNumber),
toFake: tOptional(tArray(tString)),
loopLimit: tOptional(tNumber),
shouldAdvanceTime: tOptional(tBoolean),
advanceTimeDelta: tOptional(tNumber),
});
scheme.BrowserContextClockInstallResult = tOptional(tObject({}));
scheme.BrowserContextClockJumpParams = tObject({
timeNumber: tOptional(tNumber),
timeString: tOptional(tString),
});
scheme.BrowserContextClockJumpResult = tOptional(tObject({}));
scheme.BrowserContextClockRunAllParams = tOptional(tObject({}));
scheme.BrowserContextClockRunAllResult = tObject({
fakeTime: tNumber,
});
scheme.BrowserContextClockRunToLastParams = tOptional(tObject({}));
scheme.BrowserContextClockRunToLastResult = tObject({
fakeTime: tNumber,
});
scheme.BrowserContextClockTickParams = tObject({
timeNumber: tOptional(tNumber),
timeString: tOptional(tString),
});
scheme.BrowserContextClockTickResult = tObject({
fakeTime: tNumber,
});
scheme.PageInitializer = tObject({
mainFrame: tChannel(['Frame']),
viewportSize: tOptional(tObject({
@ -2478,7 +2506,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
send: tOptional(tEnum(['always', 'unauthorized'])),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),

View file

@ -41,6 +41,7 @@ import { Recorder } from './recorder';
import * as consoleApiSource from '../generated/consoleApiSource';
import { BrowserContextAPIRequestContext } from './fetch';
import type { Artifact } from './artifact';
import { Clock } from './clock';
export abstract class BrowserContext extends SdkObject {
static Events = {
@ -87,6 +88,7 @@ export abstract class BrowserContext extends SdkObject {
private _routesInFlight = new Set<network.Route>();
private _debugger!: Debugger;
_closeReason: string | undefined;
readonly clock: Clock;
constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) {
super(browser, 'browser-context');
@ -103,6 +105,7 @@ export abstract class BrowserContext extends SdkObject {
this._harRecorders.set('', new HarRecorder(this, null, this._options.recordHar));
this.tracing = new Tracing(this, browser.options.tracesDir);
this.clock = new Clock(this);
}
isPersistentContext(): boolean {

View file

@ -920,6 +920,9 @@ would be `example.test`.
*/
export interface CookieDeprecationMetadataIssueDetails {
allowedSites: string[];
optOutPercentage: number;
isOptOutTopLevel: boolean;
operation: CookieOperation;
}
export type ClientHintIssueReason = "MetaTagAllowListInvalidOrigin"|"MetaTagModifiedHTML";
export interface FederatedAuthRequestIssueDetails {

View file

@ -0,0 +1,79 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type * as channels from '@protocol/channels';
import type { BrowserContext } from './browserContext';
import * as fakeTimersSource from '../generated/fakeTimersSource';
export class Clock {
private _browserContext: BrowserContext;
private _installed = false;
constructor(browserContext: BrowserContext) {
this._browserContext = browserContext;
}
async install(params: channels.BrowserContextClockInstallOptions) {
if (this._installed)
throw new Error('Cannot install more than one clock per context');
this._installed = true;
const script = `(() => {
const module = {};
${fakeTimersSource.source}
globalThis.__pwFakeTimers = (module.exports.install())(${JSON.stringify(params)});
})();`;
await this._addAndEvaluate(script);
}
async jump(time: number | string) {
this._assertInstalled();
await this._addAndEvaluate(`globalThis.__pwFakeTimers.jump(${JSON.stringify(time)}); 0`);
}
async runAll(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.runAll()`);
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.runAllAsync()`);
}
async runToLast(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.runToLast()`);
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.runToLastAsync()`);
}
async tick(time: number | string): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.tick(${JSON.stringify(time)})`);
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.tickAsync(${JSON.stringify(time)})`);
}
private async _addAndEvaluate(script: string) {
await this._browserContext.addInitScript(script);
return await this._evaluateInFrames(script);
}
private async _evaluateInFrames(script: string) {
const frames = this._browserContext.pages().map(page => page.frames()).flat();
const results = await Promise.all(frames.map(frame => frame.evaluateExpression(script)));
return results[0];
}
private _assertInstalled() {
if (!this._installed)
throw new Error('Clock is not installed');
}
}

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit"
},
"Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 740,
"height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 320,
"height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 658,
"height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36",
"viewport": {
"width": 712,
"height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36",
"viewport": {
"width": 1138,
"height": 712
@ -978,7 +978,7 @@
"defaultBrowserType": "webkit"
},
"LG Optimus L70": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@ -989,7 +989,7 @@
"defaultBrowserType": "chromium"
},
"LG Optimus L70 landscape": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@ -1000,7 +1000,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@ -1011,7 +1011,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@ -1022,7 +1022,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@ -1033,7 +1033,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@ -1044,7 +1044,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36",
"viewport": {
"width": 800,
"height": 1280
@ -1055,7 +1055,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36",
"viewport": {
"width": 1280,
"height": 800
@ -1066,7 +1066,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@ -1077,7 +1077,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@ -1088,7 +1088,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -1099,7 +1099,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -1110,7 +1110,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1121,7 +1121,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1132,7 +1132,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1143,7 +1143,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1154,7 +1154,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1165,7 +1165,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1176,7 +1176,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36",
"viewport": {
"width": 600,
"height": 960
@ -1187,7 +1187,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36",
"viewport": {
"width": 960,
"height": 600
@ -1242,7 +1242,7 @@
"defaultBrowserType": "webkit"
},
"Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 731
@ -1253,7 +1253,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 731,
"height": 411
@ -1264,7 +1264,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 823
@ -1275,7 +1275,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 823,
"height": 411
@ -1286,7 +1286,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 393,
"height": 786
@ -1297,7 +1297,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 786,
"height": 393
@ -1308,7 +1308,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 353,
"height": 745
@ -1319,7 +1319,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 745,
"height": 353
@ -1330,7 +1330,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 892
@ -1345,7 +1345,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"screen": {
"height": 892,
"width": 412
@ -1360,7 +1360,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"screen": {
"width": 393,
"height": 851
@ -1375,7 +1375,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"screen": {
"width": 851,
"height": 393
@ -1390,7 +1390,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 915
@ -1405,7 +1405,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"screen": {
"width": 915,
"height": 412
@ -1420,7 +1420,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -1431,7 +1431,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -1442,7 +1442,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36",
"screen": {
"width": 1792,
"height": 1120
@ -1457,7 +1457,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Safari/537.36 Edg/126.0.6478.17",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36 Edg/126.0.6478.26",
"screen": {
"width": 1792,
"height": 1120
@ -1502,7 +1502,7 @@
"defaultBrowserType": "webkit"
},
"Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36",
"screen": {
"width": 1920,
"height": 1080
@ -1517,7 +1517,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.17 Safari/537.36 Edg/126.0.6478.17",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36 Edg/126.0.6478.26",
"screen": {
"width": 1920,
"height": 1080

View file

@ -312,6 +312,26 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
return { artifact: ArtifactDispatcher.from(this, artifact) };
}
async clockInstall(params: channels.BrowserContextClockInstallParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallResult> {
await this._context.clock.install(params);
}
async clockJump(params: channels.BrowserContextClockJumpParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockJumpResult> {
await this._context.clock.jump(params.timeString || params.timeNumber || 0);
}
async clockRunAll(params: channels.BrowserContextClockRunAllParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunAllResult> {
return { fakeTime: await this._context.clock.runAll() };
}
async clockRunToLast(params: channels.BrowserContextClockRunToLastParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunToLastResult> {
return { fakeTime: await this._context.clock.runToLast() };
}
async clockTick(params: channels.BrowserContextClockTickParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockTickResult> {
return { fakeTime: await this._context.clock.tick(params.timeString || params.timeNumber || 0) };
}
async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise<void> {
if (params.enabled)
this._subscriptions.add(params.event);

View file

@ -159,7 +159,7 @@ export abstract class APIRequestContext extends SdkObject {
}
const credentials = this._getHttpCredentials(requestUrl);
if (credentials?.sendImmediately)
if (credentials?.send === 'always')
setBasicAuthorizationHeader(headers, credentials);
const method = params.method?.toUpperCase() || 'GET';

View file

@ -1,4 +1,7 @@
# Files in this folder are used in browser environment, they can only depend on isomorphic files.
[*]
../isomorphic/
../../utils/isomorphic
../../utils/isomorphic
[fakeTimers.ts]
../../third_party/fake-timers-src

View file

@ -0,0 +1,23 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-ignore
import SinonFakeTimers from '../../third_party/fake-timers-src';
import type * as channels from '@protocol/channels';
export function install(params: channels.BrowserContextClockInstallOptions) {
return SinonFakeTimers.install(params);
}

View file

@ -267,7 +267,9 @@ export class WKBrowserContext extends BrowserContext {
const cc = network.rewriteCookies(cookies).map(c => ({
...c,
session: c.expires === -1 || c.expires === undefined,
expires: c.expires && c.expires !== -1 ? c.expires * 1000 : c.expires
expires: c.expires && c.expires !== -1 ? c.expires * 1000 : c.expires,
// TODO: make WebKit on linux work without eplicit sameSite.
sameSite: c.sameSite ?? (process.platform === 'linux' ? 'Lax' : undefined)
})) as Protocol.Playwright.SetCookieParam[];
await this._browser._browserSession.send('Playwright.setCookies', { cookies: cc, browserContextId: this._browserContextId });
}

File diff suppressed because it is too large Load diff

View file

@ -920,6 +920,9 @@ would be `example.test`.
*/
export interface CookieDeprecationMetadataIssueDetails {
allowedSites: string[];
optOutPercentage: number;
isOptOutTopLevel: boolean;
operation: CookieOperation;
}
export type ClientHintIssueReason = "MetaTagAllowListInvalidOrigin"|"MetaTagModifiedHTML";
export interface FederatedAuthRequestIssueDetails {

View file

@ -4863,6 +4863,11 @@ export interface Page {
*/
accessibility: Accessibility;
/**
* Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock.
*/
clock: Clock;
/**
* **NOTE** Only available for Chromium atm.
*
@ -8980,6 +8985,11 @@ export interface BrowserContext {
waitForEvent(event: 'weberror', optionsOrPredicate?: { predicate?: (webError: WebError) => boolean | Promise<boolean>, timeout?: number } | ((webError: WebError) => boolean | Promise<boolean>)): Promise<WebError>;
/**
* Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock.
*/
clock: Clock;
/**
* API testing helper associated with this context. Requests made with this API will use context cookies.
*/
@ -13383,11 +13393,12 @@ export interface BrowserType<Unused = {}> {
origin?: string;
/**
* Whether to send `Authorization` header with the first API request. By default, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
* This option only applies to the requests sent from corresponding {@link APIRequestContext} and does not affect
* requests sent from the browser. `'always'` - `Authorization` header with basic authentication credentials will be
* sent with the each API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response
* with `WWW-Authenticate` header is received. Defaults to `'unauthorized'`.
*/
sendImmediately?: boolean;
send?: "unauthorized"|"always";
};
/**
@ -14920,11 +14931,12 @@ export interface AndroidDevice {
origin?: string;
/**
* Whether to send `Authorization` header with the first API request. By default, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
* This option only applies to the requests sent from corresponding {@link APIRequestContext} and does not affect
* requests sent from the browser. `'always'` - `Authorization` header with basic authentication credentials will be
* sent with the each API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response
* with `WWW-Authenticate` header is received. Defaults to `'unauthorized'`.
*/
sendImmediately?: boolean;
send?: "unauthorized"|"always";
};
/**
@ -15651,11 +15663,12 @@ export interface APIRequest {
origin?: string;
/**
* Whether to send `Authorization` header with the first API request. By default, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
* This option only applies to the requests sent from corresponding {@link APIRequestContext} and does not affect
* requests sent from the browser. `'always'` - `Authorization` header with basic authentication credentials will be
* sent with the each API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response
* with `WWW-Authenticate` header is received. Defaults to `'unauthorized'`.
*/
sendImmediately?: boolean;
send?: "unauthorized"|"always";
};
/**
@ -15864,7 +15877,7 @@ export interface APIRequestContext {
*/
dispose(options?: {
/**
* The reason to be reported to the operations interrupted by the context disposure.
* The reason to be reported to the operations interrupted by the context disposal.
*/
reason?: string;
}): Promise<void>;
@ -16808,11 +16821,12 @@ export interface Browser extends EventEmitter {
origin?: string;
/**
* Whether to send `Authorization` header with the first API request. By default, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
* This option only applies to the requests sent from corresponding {@link APIRequestContext} and does not affect
* requests sent from the browser. `'always'` - `Authorization` header with basic authentication credentials will be
* sent with the each API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response
* with `WWW-Authenticate` header is received. Defaults to `'unauthorized'`.
*/
sendImmediately?: boolean;
send?: "unauthorized"|"always";
};
/**
@ -17224,6 +17238,81 @@ export interface BrowserServer {
[Symbol.asyncDispose](): Promise<void>;
}
/**
* Playwright uses [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) for clock emulation. Clock is
* installed for the entire {@link BrowserContext}, so the time in all the pages and iframes is controlled by the same
* clock.
*/
export interface Clock {
/**
* Creates a clock and installs it globally.
* @param options
*/
install(options?: {
/**
* Relevant only when using with `shouldAdvanceTime`. Increment mocked time by advanceTimeDelta ms every
* advanceTimeDelta ms change in the real system time (default: 20).
*/
advanceTimeDelta?: number;
/**
* The maximum number of timers that will be run when calling
* [clock.runAll()](https://playwright.dev/docs/api/class-clock#clock-run-all). Defaults to `1000`.
*/
loopLimit?: number;
/**
* Install fake timers with the specified unix epoch (default: 0).
*/
now?: number|Date;
/**
* Tells `@sinonjs/fake-timers` to increment mocked time automatically based on the real system time shift (e.g., the
* mocked time will be incremented by 20ms for every 20ms change in the real system time). Defaults to `false`.
*/
shouldAdvanceTime?: boolean;
/**
* An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake:
* ['setTimeout'] })` will fake only `setTimeout()`. By default, `setTimeout`, `clearTimeout`, `setInterval`,
* `clearInterval` and `Date` are faked.
*/
toFake?: Array<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">;
}): Promise<void>;
/**
* Advance the clock by jumping forward in time, firing callbacks at most once. Returns fake milliseconds since the
* unix epoch. This can be used to simulate the JS engine (such as a browser) being put to sleep and resumed later,
* skipping intermediary timers.
* @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
*/
jump(time: number|string): Promise<void>;
/**
* Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be
* run as well. This makes it easier to run asynchronous tests to completion without worrying about the number of
* timers they use, or the delays in those timers. It runs a maximum of `loopLimit` times after which it assumes there
* is an infinite loop of timers and throws an error.
*/
runAll(): Promise<number>;
/**
* This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as
* necessary. If new timers are added while it is executing they will be run only if they would occur before this
* time. This is useful when you want to run a test to completion, but the test recursively sets timers that would
* cause runAll to trigger an infinite loop warning.
*/
runToLast(): Promise<number>;
/**
* Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch.
* @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
*/
tick(time: number|string): Promise<number>;
}
/**
* {@link ConsoleMessage} objects are dispatched by page via the
* [page.on('console')](https://playwright.dev/docs/api/class-page#page-event-console) event. For each console message
@ -17705,11 +17794,12 @@ export interface Electron {
origin?: string;
/**
* Whether to send `Authorization` header with the first API request. By default, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
* This option only applies to the requests sent from corresponding {@link APIRequestContext} and does not affect
* requests sent from the browser. `'always'` - `Authorization` header with basic authentication credentials will be
* sent with the each API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response
* with `WWW-Authenticate` header is received. Defaults to `'unauthorized'`.
*/
sendImmediately?: boolean;
send?: "unauthorized"|"always";
};
/**
@ -20373,11 +20463,12 @@ export interface HTTPCredentials {
origin?: string;
/**
* Whether to send `Authorization` header with the first API request. By default, the credentials are sent only when
* 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent
* from the browser.
* This option only applies to the requests sent from corresponding {@link APIRequestContext} and does not affect
* requests sent from the browser. `'always'` - `Authorization` header with basic authentication credentials will be
* sent with the each API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response
* with `WWW-Authenticate` header is received. Defaults to `'unauthorized'`.
*/
sendImmediately?: boolean;
send?: "unauthorized"|"always";
}
export interface Geolocation {

View file

@ -42,3 +42,19 @@ export function setIsWorkerProcess() {
export function isWorkerProcess() {
return _isWorkerProcess;
}
export interface TestLifecycleInstrumentation {
onTestBegin?(): Promise<void>;
onTestFunctionEnd?(): Promise<void>;
onTestEnd?(): Promise<void>;
}
let _testLifecycleInstrumentation: TestLifecycleInstrumentation | undefined;
export function setTestLifecycleInstrumentation(instrumentation: TestLifecycleInstrumentation | undefined) {
_testLifecycleInstrumentation = instrumentation;
}
export function testLifecycleInstrumentation() {
return _testLifecycleInstrumentation;
}

View file

@ -16,15 +16,14 @@
import * as fs from 'fs';
import * as path from 'path';
import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
import * as playwrightLibrary from 'playwright-core';
import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video, PageScreenshotOptions } from 'playwright-core';
import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { TestInfoImpl } from './worker/testInfo';
import { rootTestType } from './common/testType';
import type { ContextReuseMode } from './common/config';
import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
import { currentTestInfo } from './common/globals';
import { currentTestInfo, setTestLifecycleInstrumentation, type TestLifecycleInstrumentation } from './common/globals';
export { expect } from './matchers/expect';
export const _baseTest: TestType<{}, {}> = rootTestType.test;
@ -45,11 +44,12 @@ if ((process as any)['__pw_initiator__']) {
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
_combinedContextOptions: BrowserContextOptions,
_setupContextOptions: void;
_setupArtifacts: void;
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
};
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
// Same as "playwright", but exposed so that our internal tests can override it.
_playwrightImpl: PlaywrightWorkerArgs['playwright'];
_browserOptions: LaunchOptions;
_optionContextReuseMode: ContextReuseMode,
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
@ -59,9 +59,14 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
defaultBrowserType: ['chromium', { scope: 'worker', option: true }],
browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker', option: true }],
playwright: [async ({}, use) => {
await use(require('playwright-core'));
_playwrightImpl: [({}, use) => use(require('playwright-core')), { scope: 'worker' }],
playwright: [async ({ _playwrightImpl, screenshot }, use) => {
await connector.setPlaywright(_playwrightImpl, screenshot);
await use(_playwrightImpl);
await connector.setPlaywright(undefined, screenshot);
}, { scope: 'worker', _hideStep: true } as any],
headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }],
channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }],
launchOptions: [{}, { scope: 'worker', option: true }],
@ -222,7 +227,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
_setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
if (testIdAttribute)
playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute);
playwright.selectors.setTestIdAttribute(testIdAttribute);
testInfo.snapshotSuffix = process.platform;
if (debugMode())
testInfo.setTimeout(0);
@ -243,58 +248,6 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
}
}, { auto: 'all-hooks-included', _title: 'context configuration' } as any],
_setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => {
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
const csiListener: ClientInstrumentationListener = {
onApiCallBegin: (apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) => {
const testInfo = currentTestInfo();
if (!testInfo || apiName.includes('setTestIdAttribute'))
return { userObject: null };
const step = testInfo._addStep({
location: frames[0] as any,
category: 'pw:api',
title: renderApiCall(apiName, params),
apiName,
params,
});
userData.userObject = step;
out.stepId = step.stepId;
},
onApiCallEnd: (userData: any, error?: Error) => {
const step = userData.userObject;
step?.complete({ error });
},
onWillPause: () => {
currentTestInfo()?.setTimeout(0);
},
runAfterCreateBrowserContext: async (context: BrowserContext) => {
await artifactsRecorder?.didCreateBrowserContext(context);
const testInfo = currentTestInfo();
if (testInfo)
attachConnectedHeaderIfNeeded(testInfo, context.browser());
},
runAfterCreateRequestContext: async (context: APIRequestContext) => {
await artifactsRecorder?.didCreateRequestContext(context);
},
runBeforeCloseBrowserContext: async (context: BrowserContext) => {
await artifactsRecorder?.willCloseBrowserContext(context);
},
runBeforeCloseRequestContext: async (context: APIRequestContext) => {
await artifactsRecorder?.willCloseRequestContext(context);
},
};
const clientInstrumentation = (playwright as any)._instrumentation as ClientInstrumentation;
clientInstrumentation.addListener(csiListener);
await use();
clientInstrumentation.removeListener(csiListener);
await artifactsRecorder.didFinishTest();
}, { auto: 'all-hooks-included', _title: 'trace recording' } as any],
_contextFactory: [async ({ browser, video, _reuseContext }, use, testInfo) => {
const testInfoImpl = testInfo as TestInfoImpl;
const videoMode = normalizeVideoMode(video);
@ -471,7 +424,7 @@ class ArtifactsRecorder {
private _playwright: Playwright;
private _artifactsDir: string;
private _screenshotMode: ScreenshotMode;
private _screenshotOptions: { mode: ScreenshotMode } & Pick<playwrightLibrary.PageScreenshotOptions, 'fullPage' | 'omitBackground'> | undefined;
private _screenshotOptions: { mode: ScreenshotMode } & Pick<PageScreenshotOptions, 'fullPage' | 'omitBackground'> | undefined;
private _temporaryScreenshots: string[] = [];
private _temporaryArtifacts: string[] = [];
private _reusedContexts = new Set<BrowserContext>();
@ -496,7 +449,6 @@ class ArtifactsRecorder {
async willStartTest(testInfo: TestInfoImpl) {
this._testInfo = testInfo;
testInfo._onDidFinishTestFunction = () => this.didFinishTestFunction();
// Since beforeAll(s), test and afterAll(s) reuse the same TestInfo, make sure we do not
// overwrite previous screenshots.
@ -678,6 +630,101 @@ function tracing() {
return (test.info() as TestInfoImpl)._tracing;
}
class InstrumentationConnector implements TestLifecycleInstrumentation, ClientInstrumentationListener {
private _playwright: PlaywrightWorkerArgs['playwright'] | undefined;
private _screenshot: ScreenshotOption = 'off';
private _artifactsRecorder: ArtifactsRecorder | undefined;
private _testIsRunning = false;
constructor() {
setTestLifecycleInstrumentation(this);
}
async setPlaywright(playwright: PlaywrightWorkerArgs['playwright'] | undefined, screenshot: ScreenshotOption) {
if (this._playwright) {
if (this._testIsRunning) {
// When "playwright" is destroyed during a test, collect artifacts immediately.
await this.onTestEnd();
}
const clientInstrumentation = (this._playwright as any)._instrumentation as ClientInstrumentation;
clientInstrumentation.removeListener(this);
}
this._playwright = playwright;
this._screenshot = screenshot;
if (this._playwright) {
const clientInstrumentation = (this._playwright as any)._instrumentation as ClientInstrumentation;
clientInstrumentation.addListener(this);
if (this._testIsRunning) {
// When "playwright" is created during a test, wire it up immediately.
await this.onTestBegin();
}
}
}
async onTestBegin() {
this._testIsRunning = true;
if (this._playwright) {
this._artifactsRecorder = new ArtifactsRecorder(this._playwright, tracing().artifactsDir(), this._screenshot);
await this._artifactsRecorder.willStartTest(currentTestInfo() as TestInfoImpl);
}
}
async onTestFunctionEnd() {
await this._artifactsRecorder?.didFinishTestFunction();
}
async onTestEnd() {
await this._artifactsRecorder?.didFinishTest();
this._artifactsRecorder = undefined;
this._testIsRunning = false;
}
onApiCallBegin(apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) {
const testInfo = currentTestInfo();
if (!testInfo || apiName.includes('setTestIdAttribute'))
return { userObject: null };
const step = testInfo._addStep({
location: frames[0] as any,
category: 'pw:api',
title: renderApiCall(apiName, params),
apiName,
params,
});
userData.userObject = step;
out.stepId = step.stepId;
}
onApiCallEnd(userData: any, error?: Error) {
const step = userData.userObject;
step?.complete({ error });
}
onWillPause() {
currentTestInfo()?.setTimeout(0);
}
async runAfterCreateBrowserContext(context: BrowserContext) {
await this._artifactsRecorder?.didCreateBrowserContext(context);
const testInfo = currentTestInfo();
if (testInfo)
attachConnectedHeaderIfNeeded(testInfo, context.browser());
}
async runAfterCreateRequestContext(context: APIRequestContext) {
await this._artifactsRecorder?.didCreateRequestContext(context);
}
async runBeforeCloseBrowserContext(context: BrowserContext) {
await this._artifactsRecorder?.willCloseBrowserContext(context);
}
async runBeforeCloseRequestContext(context: APIRequestContext) {
await this._artifactsRecorder?.willCloseRequestContext(context);
}
}
const connector = new InstrumentationConnector();
export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
export { defineConfig } from './common/configLoader';

View file

@ -68,7 +68,6 @@ export class TestInfoImpl implements TestInfo {
readonly _projectInternal: FullProjectInternal;
readonly _configInternal: FullConfigInternal;
private readonly _steps: TestStepInternal[] = [];
_onDidFinishTestFunction: (() => Promise<void>) | undefined;
private readonly _stages: TestStage[] = [];
_hasNonRetriableError = false;
_hasUnhandledError = false;

View file

@ -17,7 +17,7 @@
import { colors } from 'playwright-core/lib/utilsBundle';
import { debugTest, relativeFilePath, serializeError } from '../util';
import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc';
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
import { setCurrentTestInfo, setIsWorkerProcess, testLifecycleInstrumentation } from '../common/globals';
import { deserializeConfig } from '../common/configLoader';
import type { Suite, TestCase } from '../common/test';
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
@ -304,10 +304,11 @@ export class WorkerMain extends ProcessRunner {
if (this._lastRunningTests.length > 10)
this._lastRunningTests.shift();
let shouldRunAfterEachHooks = false;
const tracingSlot = { timeout: this._project.project.timeout, elapsed: 0 };
testInfo._allowSkips = true;
await testInfo._runAsStage({ title: 'setup and test' }, async () => {
await testInfo._runAsStage({ title: 'start tracing', runnable: { type: 'test' } }, async () => {
await testInfo._runAsStage({ title: 'start tracing', runnable: { type: 'test', slot: tracingSlot } }, async () => {
// Ideally, "trace" would be an config-level option belonging to the
// test runner instead of a fixture belonging to Playwright.
// However, for backwards compatibility, we have to read it from a fixture today.
@ -318,6 +319,7 @@ export class WorkerMain extends ProcessRunner {
if (typeof traceFixtureRegistration.fn === 'function')
throw new Error(`"trace" option cannot be a function`);
await testInfo._tracing.startIfNeeded(traceFixtureRegistration.fn);
await testLifecycleInstrumentation()?.onTestBegin?.();
});
if (this._isStopped || isSkipped) {
@ -372,10 +374,10 @@ export class WorkerMain extends ProcessRunner {
try {
// Run "immediately upon test function finish" callback.
await testInfo._runAsStage({ title: 'on-test-function-finish', runnable: { type: 'test', slot: afterHooksSlot } }, async () => testInfo._onDidFinishTestFunction?.());
await testInfo._runAsStage({ title: 'on-test-function-finish', runnable: { type: 'test', slot: tracingSlot } }, async () => {
await testLifecycleInstrumentation()?.onTestFunctionEnd?.();
});
} catch (error) {
if (error instanceof TimeoutManagerError)
didTimeoutInAfterHooks = true;
firstAfterHooksError = firstAfterHooksError ?? error;
}
@ -458,8 +460,8 @@ export class WorkerMain extends ProcessRunner {
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
}
const tracingSlot = { timeout: this._project.project.timeout, elapsed: 0 };
await testInfo._runAsStage({ title: 'stop tracing', runnable: { type: 'test', slot: tracingSlot } }, async () => {
await testLifecycleInstrumentation()?.onTestEnd?.();
await testInfo._tracing.stopIfNeeded();
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.

View file

@ -1374,9 +1374,8 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
/**
* Whether to skip entries from `.gitignore` when searching for test files. By default, if neither
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir) nor
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir) are explicitely
* specified, Playwright will ignore any test files matching `.gitignore` entries. This option allows to override that
* behavior.
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir) are explicitly
* specified, Playwright will ignore any test files matching `.gitignore` entries.
*/
respectGitIgnore?: boolean;

View file

@ -578,7 +578,7 @@ export type PlaywrightNewRequestParams = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
send?: 'always' | 'unauthorized',
},
proxy?: {
server: string,
@ -602,7 +602,7 @@ export type PlaywrightNewRequestOptions = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
send?: 'always' | 'unauthorized',
},
proxy?: {
server: string,
@ -959,7 +959,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
send?: 'always' | 'unauthorized',
},
deviceScaleFactor?: number,
isMobile?: boolean,
@ -1032,7 +1032,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
send?: 'always' | 'unauthorized',
},
deviceScaleFactor?: number,
isMobile?: boolean,
@ -1140,7 +1140,7 @@ export type BrowserNewContextParams = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
send?: 'always' | 'unauthorized',
},
deviceScaleFactor?: number,
isMobile?: boolean,
@ -1199,7 +1199,7 @@ export type BrowserNewContextOptions = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
send?: 'always' | 'unauthorized',
},
deviceScaleFactor?: number,
isMobile?: boolean,
@ -1261,7 +1261,7 @@ export type BrowserNewContextForReuseParams = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
send?: 'always' | 'unauthorized',
},
deviceScaleFactor?: number,
isMobile?: boolean,
@ -1320,7 +1320,7 @@ export type BrowserNewContextForReuseOptions = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
send?: 'always' | 'unauthorized',
},
deviceScaleFactor?: number,
isMobile?: boolean,
@ -1460,6 +1460,11 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise<BrowserContextCreateTempFileResult>;
updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>;
clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallResult>;
clockJump(params: BrowserContextClockJumpParams, metadata?: CallMetadata): Promise<BrowserContextClockJumpResult>;
clockRunAll(params?: BrowserContextClockRunAllParams, metadata?: CallMetadata): Promise<BrowserContextClockRunAllResult>;
clockRunToLast(params?: BrowserContextClockRunToLastParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToLastResult>;
clockTick(params: BrowserContextClockTickParams, metadata?: CallMetadata): Promise<BrowserContextClockTickResult>;
}
export type BrowserContextBindingCallEvent = {
binding: BindingCallChannel,
@ -1748,6 +1753,51 @@ export type BrowserContextUpdateSubscriptionOptions = {
};
export type BrowserContextUpdateSubscriptionResult = void;
export type BrowserContextClockInstallParams = {
now?: number,
toFake?: string[],
loopLimit?: number,
shouldAdvanceTime?: boolean,
advanceTimeDelta?: number,
};
export type BrowserContextClockInstallOptions = {
now?: number,
toFake?: string[],
loopLimit?: number,
shouldAdvanceTime?: boolean,
advanceTimeDelta?: number,
};
export type BrowserContextClockInstallResult = void;
export type BrowserContextClockJumpParams = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockJumpOptions = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockJumpResult = void;
export type BrowserContextClockRunAllParams = {};
export type BrowserContextClockRunAllOptions = {};
export type BrowserContextClockRunAllResult = {
fakeTime: number,
};
export type BrowserContextClockRunToLastParams = {};
export type BrowserContextClockRunToLastOptions = {};
export type BrowserContextClockRunToLastResult = {
fakeTime: number,
};
export type BrowserContextClockTickParams = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockTickOptions = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockTickResult = {
fakeTime: number,
};
export interface BrowserContextEvents {
'bindingCall': BrowserContextBindingCallEvent;
@ -4479,7 +4529,7 @@ export type AndroidDeviceLaunchBrowserParams = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
send?: 'always' | 'unauthorized',
},
deviceScaleFactor?: number,
isMobile?: boolean,
@ -4536,7 +4586,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
username: string,
password: string,
origin?: string,
sendImmediately?: boolean,
send?: 'always' | 'unauthorized',
},
deviceScaleFactor?: number,
isMobile?: boolean,

View file

@ -456,7 +456,11 @@ ContextOptions:
username: string
password: string
origin: string?
sendImmediately: boolean?
send:
type: enum?
literals:
- always
- unauthorized
deviceScaleFactor: number?
isMobile: boolean?
hasTouch: boolean?
@ -674,7 +678,11 @@ Playwright:
username: string
password: string
origin: string?
sendImmediately: boolean?
send:
type: enum?
literals:
- always
- unauthorized
proxy:
type: object?
properties:
@ -1196,6 +1204,36 @@ BrowserContext:
- requestFailed
enabled: boolean
clockInstall:
parameters:
now: number?
toFake:
type: array?
items: string
loopLimit: number?
shouldAdvanceTime: boolean?
advanceTimeDelta: number?
clockJump:
parameters:
timeNumber: number?
timeString: string?
clockRunAll:
returns:
fakeTime: number
clockRunToLast:
returns:
fakeTime: number
clockTick:
parameters:
timeNumber: number?
timeString: string?
returns:
fakeTime: number
events:
bindingCall:

View file

@ -76,6 +76,10 @@ export const NetworkTab: React.FunctionComponent<{
return { renderedEntries };
}, [networkModel.resources, networkModel.contextIdMap, sorting, boundaries]);
const [columnWidths, setColumnWidths] = React.useState<Map<ColumnName, number>>(() => {
return new Map(allColumns().map(column => [column, columnWidth(column)]));
});
if (!networkModel.resources.length)
return <PlaceholderPanel text='No network calls' />;
@ -87,7 +91,8 @@ export const NetworkTab: React.FunctionComponent<{
onHighlighted={item => onEntryHovered(item?.resource)}
columns={visibleColumns(!!selectedEntry, renderedEntries)}
columnTitle={columnTitle}
columnWidth={columnWidth}
columnWidths={columnWidths}
setColumnWidths={setColumnWidths}
isError={item => item.status.code >= 400}
isInfo={item => !!item.route}
render={(item, column) => renderCell(item, column)}
@ -96,7 +101,7 @@ export const NetworkTab: React.FunctionComponent<{
/>;
return <>
{!selectedEntry && grid}
{selectedEntry && <SplitView sidebarSize={200} sidebarIsFirst={true} orientation='horizontal'>
{selectedEntry && <SplitView sidebarSize={columnWidths.get('name')!} sidebarIsFirst={true} orientation='horizontal' settingName='networkResourceDetails'>
<NetworkResourceDetails resource={selectedEntry.resource} onClose={() => setSelectedEntry(undefined)} />
{grid}
</SplitView>}
@ -140,15 +145,22 @@ const columnWidth = (column: ColumnName) => {
};
function visibleColumns(entrySelected: boolean, renderedEntries: RenderedEntry[]): (keyof RenderedEntry)[] {
if (entrySelected)
return ['name'];
const columns: (keyof RenderedEntry)[] = [];
if (hasMultipleContexts(renderedEntries))
columns.push('contextId');
columns.push('name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route');
if (entrySelected) {
const columns: (keyof RenderedEntry)[] = ['name'];
if (hasMultipleContexts(renderedEntries))
columns.unshift('contextId');
return columns;
}
let columns: (keyof RenderedEntry)[] = allColumns();
if (!hasMultipleContexts(renderedEntries))
columns = columns.filter(name => name !== 'contextId');
return columns;
}
function allColumns(): (keyof RenderedEntry)[] {
return ['contextId', 'name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route'];
}
const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell => {
if (column === 'contextId') {
return {

View file

@ -30,19 +30,34 @@ export type RenderedGridCell = {
export type GridViewProps<T> = Omit<ListViewProps<T>, 'render'> & {
columns: (keyof T)[],
columnTitle: (column: keyof T) => string,
columnWidth: (column: keyof T) => number,
columnWidths: Map<keyof T, number>,
setColumnWidths: (widths: Map<keyof T, number>) => void,
render: (item: T, column: keyof T, index: number) => RenderedGridCell,
sorting?: Sorting<T>,
setSorting?: (sorting: Sorting<T> | undefined) => void,
};
export function GridView<T>(model: GridViewProps<T>) {
const initialOffsets: number[] = [];
for (let i = 0; i < model.columns.length - 1; ++i) {
const column = model.columns[i];
initialOffsets[i] = (initialOffsets[i - 1] || 0) + model.columnWidth(column);
const [offsets, setOffsets] = React.useState<number[]>([]);
React.useEffect(() => {
const offsets: number[] = [];
for (let i = 0; i < model.columns.length - 1; ++i) {
const column = model.columns[i];
offsets[i] = (offsets[i - 1] || 0) + model.columnWidths.get(column)!;
}
setOffsets(offsets);
}, [model.columns, model.columnWidths]);
function updateColumnWidths(offsets: number[]) {
const widths = new Map(model.columnWidths.entries());
for (let i = 0; i < offsets.length; ++i) {
const width = offsets[i] - (offsets[i - 1] || 0);
const column = model.columns[i];
widths.set(column, width);
}
model.setColumnWidths(widths);
}
const [offsets, setOffsets] = React.useState<number[]>(initialOffsets);
const toggleSorting = React.useCallback((f: keyof T) => {
model.setSorting?.({ by: f, negate: model.sorting?.by === f ? !model.sorting.negate : false });
@ -52,7 +67,7 @@ export function GridView<T>(model: GridViewProps<T>) {
<ResizeView
orientation={'horizontal'}
offsets={offsets}
setOffsets={setOffsets}
setOffsets={updateColumnWidths}
resizerColor='var(--vscode-panel-border)'
resizerWidth={1}
minColumnWidth={25}>
@ -63,7 +78,7 @@ export function GridView<T>(model: GridViewProps<T>) {
return <div
className={'grid-view-header-cell ' + sortingHeader(column, model.sorting)}
style={{
width: offsets[i] - (offsets[i - 1] || 0),
width: i < model.columns.length - 1 ? model.columnWidths.get(column) : undefined,
}}
onClick={() => model.setSorting && toggleSorting(column)}
>
@ -84,7 +99,9 @@ export function GridView<T>(model: GridViewProps<T>) {
return <div
className={`grid-view-cell grid-view-column-${String(column)}`}
title={title}
style={{ width: offsets[i] - (offsets[i - 1] || 0) }}>
style={{
width: i < model.columns.length - 1 ? model.columnWidths.get(column) : undefined,
}}>
{body}
</div>;
})}

View file

@ -58,7 +58,7 @@ export const ResizeView: React.FC<{
right: 0,
bottom: 0,
left: -(7 - resizerWidth) / 2,
zIndex: 1000,
zIndex: 100, // Above the content, but below the film strip hover.
pointerEvents: 'none',
}}
ref={ref}>

View file

@ -67,10 +67,12 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
await run(false);
}, { scope: 'worker' }],
defaultSameSiteCookieValue: [async ({ browserName, browserMajorVersion, channel }, run) => {
defaultSameSiteCookieValue: [async ({ browserName, browserMajorVersion, channel, isLinux }, run) => {
if (browserName === 'chromium')
await run('Lax');
else if (browserName === 'webkit')
else if (browserName === 'webkit' && isLinux)
await run('Lax');
else if (browserName === 'webkit' && !isLinux)
await run('None');
else if (browserName === 'firefox' && channel === 'firefox-beta')
await run(browserMajorVersion >= 103 && browserMajorVersion < 110 ? 'Lax' : 'None');

View file

@ -30,11 +30,12 @@ export type TestModeTestFixtures = {
export type TestModeWorkerFixtures = {
toImplInWorkerScope: (rpcObject?: any) => any;
playwright: typeof import('@playwright/test');
_playwrightImpl: typeof import('@playwright/test');
};
export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOptions & TestModeWorkerFixtures>({
mode: ['default', { scope: 'worker', option: true }],
playwright: [async ({ mode }, run) => {
_playwrightImpl: [async ({ mode }, run) => {
const testMode = {
'default': new DefaultTestMode(),
'service': new DefaultTestMode(),

View file

@ -24,7 +24,7 @@ it('should work @smoke', async ({ context, page, server }) => {
await context.addCookies([{
url: server.EMPTY_PAGE,
name: 'password',
value: '123456'
value: '123456',
}]);
expect(await page.evaluate(() => document.cookie)).toEqual('password=123456');
});
@ -224,7 +224,7 @@ it('should have |expires| set to |-1| for session cookies', async ({ context, se
expect(cookies[0].expires).toBe(-1);
});
it('should set cookie with reasonable defaults', async ({ context, server, browserName }) => {
it('should set cookie with reasonable defaults', async ({ context, server, defaultSameSiteCookieValue }) => {
await context.addCookies([{
url: server.EMPTY_PAGE,
name: 'defaults',
@ -239,7 +239,7 @@ it('should set cookie with reasonable defaults', async ({ context, server, brows
expires: -1,
httpOnly: false,
secure: false,
sameSite: browserName === 'chromium' ? 'Lax' : 'None',
sameSite: defaultSameSiteCookieValue,
}]);
});

View file

@ -384,7 +384,7 @@ it('should support requestStorageAccess', async ({ page, server, channel, browse
server.waitForRequest('/title.html'),
frame.evaluate(() => fetch('/title.html'))
]);
if (!isMac && browserName === 'webkit')
if (isWindows && browserName === 'webkit')
expect(serverRequest.headers.cookie).toBe('name=value');
else
expect(serverRequest.headers.cookie).toBeFalsy();
@ -396,7 +396,10 @@ it('should support requestStorageAccess', async ({ page, server, channel, browse
server.waitForRequest('/title.html'),
frame.evaluate(() => fetch('/title.html'))
]);
expect(serverRequest.headers.cookie).toBe('name=value');
if (isLinux && browserName === 'webkit')
expect(serverRequest.headers.cookie).toBe(undefined);
else
expect(serverRequest.headers.cookie).toBe('name=value');
}
}
});

View file

@ -435,10 +435,10 @@ it('should return error with wrong credentials', async ({ context, server }) =>
expect(response2.status()).toBe(401);
});
it('should support HTTPCredentials.sendImmediately', async ({ contextFactory, server }) => {
it('should support HTTPCredentials.sendImmediately for newContext', async ({ contextFactory, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const context = await contextFactory({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), sendImmediately: true }
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }
});
{
const [serverRequest, response] = await Promise.all([
@ -459,6 +459,31 @@ it('should support HTTPCredentials.sendImmediately', async ({ contextFactory, se
}
});
it('should support HTTPCredentials.sendImmediately for browser.newPage', async ({ contextFactory, server, browser }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const page = await browser.newPage({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }
});
{
const [serverRequest, response] = await Promise.all([
server.waitForRequest('/empty.html'),
page.request.get(server.EMPTY_PAGE)
]);
expect(serverRequest.headers.authorization).toBe('Basic ' + Buffer.from('user:pass').toString('base64'));
expect(response.status()).toBe(200);
}
{
const [serverRequest, response] = await Promise.all([
server.waitForRequest('/empty.html'),
page.request.get(server.CROSS_PROCESS_PREFIX + '/empty.html')
]);
// Not sent to another origin.
expect(serverRequest.headers.authorization).toBe(undefined);
expect(response.status()).toBe(200);
}
await page.close();
});
it('delete should support post data', async ({ context, server }) => {
const [request, response] = await Promise.all([
server.waitForRequest('/simple.json'),
@ -1202,7 +1227,7 @@ it('fetch should not throw on long set-cookie value', async ({ context, server }
expect(cookies.map(c => c.name)).toContain('bar');
});
it('should support set-cookie with SameSite and without Secure attribute over HTTP', async ({ page, server, browserName, isWindows }) => {
it('should support set-cookie with SameSite and without Secure attribute over HTTP', async ({ page, server, browserName, isWindows, isLinux }) => {
for (const value of ['None', 'Lax', 'Strict']) {
await it.step(`SameSite=${value}`, async () => {
server.setRoute('/empty.html', (req, res) => {
@ -1213,6 +1238,8 @@ it('should support set-cookie with SameSite and without Secure attribute over HT
const [cookie] = await page.context().cookies();
if (browserName === 'chromium' && value === 'None')
expect(cookie).toBeFalsy();
else if (browserName === 'webkit' && isLinux && value === 'None')
expect(cookie).toBeFalsy();
else if (browserName === 'webkit' && isWindows)
expect(cookie.sameSite).toBe('None');
else

View file

@ -109,7 +109,7 @@ it('should fall back to context.route', async ({ browser, server }) => {
await context.close();
});
it('should support Set-Cookie header', async ({ contextFactory, server, browserName, defaultSameSiteCookieValue }) => {
it('should support Set-Cookie header', async ({ contextFactory, defaultSameSiteCookieValue }) => {
const context = await contextFactory();
const page = await context.newPage();
await page.route('https://example.com/', (route, request) => {
@ -152,7 +152,7 @@ it('should ignore secure Set-Cookie header for insecure requests', async ({ cont
expect(await context.cookies()).toEqual([]);
});
it('should use Set-Cookie header in future requests', async ({ contextFactory, server, browserName, defaultSameSiteCookieValue }) => {
it('should use Set-Cookie header in future requests', async ({ contextFactory, server, defaultSameSiteCookieValue }) => {
const context = await contextFactory();
const page = await context.newPage();

View file

@ -157,7 +157,7 @@ it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
it('should support HTTPCredentials.sendImmediately', async ({ playwright, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const request = await playwright.request.newContext({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), sendImmediately: true }
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }
});
{
const [serverRequest, response] = await Promise.all([

View file

@ -255,8 +255,7 @@ it('frame.press should work', async ({ page, server }) => {
expect(await frame.evaluate(() => document.querySelector('textarea').value)).toBe('a');
});
it('has navigator.webdriver set to true', async ({ page, browserName }) => {
it.skip(browserName === 'firefox');
it('has navigator.webdriver set to true', async ({ page }) => {
expect(await page.evaluate(() => navigator.webdriver)).toBe(true);
});

View file

@ -0,0 +1,614 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './pageTest';
declare global {
interface Window {
stub: (param?: any) => void
}
}
const it = test.extend<{ calls: { params: any[] }[] }>({
calls: async ({ page }, use) => {
const calls = [];
await page.exposeFunction('stub', async (...params: any[]) => {
calls.push({ params });
});
await use(calls);
}
});
it.describe('tick', () => {
it('triggers immediately without specified delay', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub);
});
await page.clock.tick(0);
expect(calls).toEqual([{ params: [] }]);
});
it('does not trigger without sufficient delay', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 100);
});
await page.clock.tick(10);
expect(calls).toEqual([]);
});
it('triggers after sufficient delay', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 100);
});
await page.clock.tick(100);
expect(calls).toEqual([{ params: [] }]);
});
it('triggers simultaneous timers', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 100);
setTimeout(window.stub, 100);
});
await page.clock.tick(100);
expect(calls).toEqual([{ params: [] }, { params: [] }]);
});
it('triggers multiple simultaneous timers', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 100);
setTimeout(window.stub, 100);
setTimeout(window.stub, 99);
setTimeout(window.stub, 100);
});
await page.clock.tick(100);
expect(calls.length).toBe(4);
});
it('waits after setTimeout was called', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 150);
});
await page.clock.tick(50);
expect(calls).toEqual([]);
await page.clock.tick(100);
expect(calls).toEqual([{ params: [] }]);
});
it('triggers event when some throw', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => { throw new Error(); }, 100);
setTimeout(window.stub, 120);
});
await expect(page.clock.tick(120)).rejects.toThrow();
expect(calls).toEqual([{ params: [] }]);
});
it('creates updated Date while ticking', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setInterval(() => {
window.stub(new Date().getTime());
}, 10);
});
await page.clock.tick(100);
expect(calls).toEqual([
{ params: [10] },
{ params: [20] },
{ params: [30] },
{ params: [40] },
{ params: [50] },
{ params: [60] },
{ params: [70] },
{ params: [80] },
{ params: [90] },
{ params: [100] },
]);
});
it('passes 8 seconds', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setInterval(window.stub, 4000);
});
await page.clock.tick('08');
expect(calls.length).toBe(2);
});
it('passes 1 minute', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setInterval(window.stub, 6000);
});
await page.clock.tick('01:00');
expect(calls.length).toBe(10);
});
it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setInterval(window.stub, 10000);
});
await page.clock.tick('02:34:10');
expect(calls.length).toBe(925);
});
it('throws for invalid format', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setInterval(window.stub, 10000);
});
await expect(page.clock.tick('12:02:34:10')).rejects.toThrow();
expect(calls).toEqual([]);
});
it('returns the current now value', async ({ page }) => {
await page.clock.install();
const value = 200;
await page.clock.tick(value);
expect(await page.evaluate(() => Date.now())).toBe(value);
});
});
it.describe('jump', () => {
it(`ignores timers which wouldn't be run`, async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
window.stub('should not be logged');
}, 1000);
});
await page.clock.jump(500);
expect(calls).toEqual([]);
});
it('pushes back execution time for skipped timers', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
window.stub(Date.now());
}, 1000);
});
await page.clock.jump(2000);
expect(calls).toEqual([{ params: [2000] }]);
});
it('supports string time arguments', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
window.stub(Date.now());
}, 100000); // 100000 = 1:40
});
await page.clock.jump('01:50');
expect(calls).toEqual([{ params: [110000] }]);
});
});
it.describe('runAllAsyn', () => {
it('if there are no timers just return', async ({ page }) => {
await page.clock.install();
await page.clock.runAll();
});
it('runs all timers', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 10);
setTimeout(window.stub, 50);
});
await page.clock.runAll();
expect(calls.length).toBe(2);
});
it('new timers added while running are also run', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
setTimeout(window.stub, 50);
}, 10);
});
await page.clock.runAll();
expect(calls.length).toBe(1);
});
it('new timers added in promises while running are also run', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => {
setTimeout(window.stub, 50);
});
}, 10);
});
await page.clock.runAll();
expect(calls.length).toBe(1);
});
it('throws before allowing infinite recursion', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
const recursiveCallback = () => {
window.stub();
setTimeout(recursiveCallback, 10);
};
setTimeout(recursiveCallback, 10);
});
await expect(page.clock.runAll()).rejects.toThrow();
expect(calls).toHaveLength(1000);
});
it('throws before allowing infinite recursion from promises', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
const recursiveCallback = () => {
window.stub();
void Promise.resolve().then(() => {
setTimeout(recursiveCallback, 10);
});
};
setTimeout(recursiveCallback, 10);
});
await expect(page.clock.runAll()).rejects.toThrow();
expect(calls).toHaveLength(1000);
});
it('the loop limit can be set when creating a clock', async ({ page, calls }) => {
await page.clock.install({ loopLimit: 1 });
await page.evaluate(async () => {
setTimeout(window.stub, 10);
setTimeout(window.stub, 50);
});
await expect(page.clock.runAll()).rejects.toThrow();
expect(calls).toHaveLength(1);
});
it('should settle user-created promises', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => window.stub());
}, 55);
});
await page.clock.runAll();
expect(calls).toHaveLength(1);
});
it('should settle nested user-created promises', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => {
void Promise.resolve().then(() => {
void Promise.resolve().then(() => window.stub());
});
});
}, 55);
});
await page.clock.runAll();
expect(calls).toHaveLength(1);
});
it('should settle local promises before firing timers', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
void Promise.resolve().then(() => window.stub(1));
setTimeout(() => window.stub(2), 55);
});
await page.clock.runAll();
expect(calls).toEqual([
{ params: [1] },
{ params: [2] },
]);
});
});
it.describe('runToLast', () => {
it('returns current time when there are no timers', async ({ page }) => {
await page.clock.install();
const time = await page.clock.runToLast();
expect(time).toBe(0);
});
it('runs all existing timers', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 10);
setTimeout(window.stub, 50);
});
await page.clock.runToLast();
expect(calls.length).toBe(2);
});
it('returns time of the last timer', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 10);
setTimeout(window.stub, 50);
});
const time = await page.clock.runToLast();
expect(time).toBe(50);
});
it('runs all existing timers when two timers are matched for being last', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 10);
setTimeout(window.stub, 10);
});
await page.clock.runToLast();
expect(calls.length).toBe(2);
});
it('new timers added with a call time later than the last existing timer are NOT run', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
window.stub();
setTimeout(window.stub, 50);
}, 10);
});
await page.clock.runToLast();
expect(calls.length).toBe(1);
});
it('new timers added with a call time earlier than the last existing timer are run', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 100);
setTimeout(() => {
setTimeout(window.stub, 50);
}, 10);
});
await page.clock.runToLast();
expect(calls.length).toBe(2);
});
it('new timers cannot cause an infinite loop', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
const recursiveCallback = () => {
window.stub();
setTimeout(recursiveCallback, 0);
};
setTimeout(recursiveCallback, 0);
setTimeout(window.stub, 100);
});
await page.clock.runToLast();
expect(calls.length).toBe(102);
});
it('should support clocks with start time', async ({ page, calls }) => {
await page.clock.install({ now: 200 });
await page.evaluate(async () => {
setTimeout(function cb() {
window.stub();
setTimeout(cb, 50);
}, 50);
});
await page.clock.runToLast();
expect(calls.length).toBe(1);
});
it('new timers created from promises cannot cause an infinite loop', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
const recursiveCallback = () => {
void Promise.resolve().then(() => {
setTimeout(recursiveCallback, 0);
});
};
setTimeout(recursiveCallback, 0);
setTimeout(window.stub, 100);
});
await page.clock.runToLast();
expect(calls.length).toBe(1);
});
it('should settle user-created promises', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => window.stub());
}, 55);
});
await page.clock.runToLast();
expect(calls.length).toBe(1);
});
it('should settle nested user-created promises', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => {
void Promise.resolve().then(() => {
void Promise.resolve().then(() => window.stub());
});
});
}, 55);
});
await page.clock.runToLast();
expect(calls.length).toBe(1);
});
it('should settle local promises before firing timers', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
void Promise.resolve().then(() => window.stub(1));
setTimeout(() => window.stub(2), 55);
});
await page.clock.runToLast();
expect(calls).toEqual([
{ params: [1] },
{ params: [2] },
]);
});
it('should settle user-created promises before firing more timers', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => window.stub(1));
}, 55);
setTimeout(() => window.stub(2), 75);
});
await page.clock.runToLast();
expect(calls).toEqual([
{ params: [1] },
{ params: [2] },
]);
});
});
it.describe('stubTimers', () => {
it('sets initial timestamp', async ({ page, calls }) => {
await page.clock.install({ now: 1400 });
expect(await page.evaluate(() => Date.now())).toBe(1400);
});
it('replaces global setTimeout', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setTimeout(window.stub, 1000);
});
await page.clock.tick(1000);
expect(calls.length).toBe(1);
});
it('global fake setTimeout should return id', async ({ page, calls }) => {
await page.clock.install();
const to = await page.evaluate(() => setTimeout(window.stub, 1000));
expect(typeof to).toBe('number');
});
it('replaces global clearTimeout', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
const to = setTimeout(window.stub, 1000);
clearTimeout(to);
});
await page.clock.tick(1000);
expect(calls).toEqual([]);
});
it('replaces global setInterval', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
setInterval(window.stub, 500);
});
await page.clock.tick(1000);
expect(calls.length).toBe(2);
});
it('replaces global clearInterval', async ({ page, calls }) => {
await page.clock.install();
await page.evaluate(async () => {
const to = setInterval(window.stub, 500);
clearInterval(to);
});
await page.clock.tick(1000);
expect(calls).toEqual([]);
});
it('replaces global performance.now', async ({ page }) => {
await page.clock.install();
const promise = page.evaluate(async () => {
const prev = performance.now();
await new Promise(f => setTimeout(f, 1000));
const next = performance.now();
return { prev, next };
});
await page.clock.tick(1000);
expect(await promise).toEqual({ prev: 0, next: 1000 });
});
it('fakes Date constructor', async ({ page }) => {
await page.clock.install({ now: 0 });
const now = await page.evaluate(() => new Date().getTime());
expect(now).toBe(0);
});
it('does not fake methods not provided', async ({ page }) => {
await page.clock.install({
now: 0,
toFake: ['Date'],
});
// Should not stall.
await page.evaluate(() => {
return new Promise(f => setTimeout(f, 1));
});
});
});
it.describe('shouldAdvanceTime', () => {
it('should create an auto advancing timer', async ({ page, calls }) => {
const testDelay = 29;
const now = new Date('2015-09-25');
await page.clock.install({ now, shouldAdvanceTime: true });
const pageNow = await page.evaluate(() => Date.now());
expect(pageNow).toBe(1443139200000);
await page.evaluate(async testDelay => {
return new Promise<void>(f => {
const timeoutStarted = Date.now();
setTimeout(() => {
window.stub(Date.now() - timeoutStarted);
f();
}, testDelay);
});
}, testDelay);
expect(calls).toEqual([
{ params: [testDelay] }
]);
});
it('should test setInterval', async ({ page, calls }) => {
const now = new Date('2015-09-25');
await page.clock.install({ now, shouldAdvanceTime: true });
const timeDifference = await page.evaluate(async () => {
return new Promise(f => {
const interval = 20;
const cyclesToTrigger = 3;
const timeoutStarted = Date.now();
let intervalsTriggered = 0;
const intervalId = setInterval(() => {
if (++intervalsTriggered === cyclesToTrigger) {
clearInterval(intervalId);
const timeDifference = Date.now() - timeoutStarted;
f(timeDifference - interval * cyclesToTrigger);
}
}, interval);
});
});
expect(timeDifference).toBe(0);
});
});

View file

@ -151,10 +151,8 @@ test('should work with screenshot: on', async ({ runInlineTest }, testInfo) => {
' test-finished-1.png',
'artifacts-shared-shared-failing',
' test-failed-1.png',
' test-failed-2.png',
'artifacts-shared-shared-passing',
' test-finished-1.png',
' test-finished-2.png',
'artifacts-two-contexts',
' test-finished-1.png',
' test-finished-2.png',
@ -185,7 +183,6 @@ test('should work with screenshot: only-on-failure', async ({ runInlineTest }, t
' test-failed-1.png',
'artifacts-shared-shared-failing',
' test-failed-1.png',
' test-failed-2.png',
'artifacts-two-contexts-failing',
' test-failed-1.png',
' test-failed-2.png',

View file

@ -569,14 +569,19 @@ test('should opt out of attachments', async ({ runInlineTest, server }, testInfo
expect([...trace.resources.keys()].filter(f => f.startsWith('resources/'))).toHaveLength(0);
});
test('should record with custom page fixture', async ({ runInlineTest }, testInfo) => {
test('should record with custom page fixture that closes the context', async ({ runInlineTest }, testInfo) => {
// Note that original issue did not close the context, but we do not support such usecase.
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23220' });
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
myPage: async ({ browser }, use) => {
await use(await browser.newPage());
const page = await browser.newPage();
await use(page);
await page.close();
},
});
@ -1112,3 +1117,121 @@ test('trace:retain-on-first-failure should create trace if request context is di
expect(trace.apiNames).toContain('apiRequestContext.get');
expect(result.failed).toBe(1);
});
test('should record trace in workerStorageState', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30287' });
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
storageState: ({ workerStorageState }, use) => use(workerStorageState),
workerStorageState: [async ({ browser }, use) => {
const page = await browser.newPage({ storageState: undefined });
await page.setContent('<div>hello</div>');
await page.close();
await use(undefined);
}, { scope: 'worker' }],
})
test('pass', async ({ page }) => {
await page.goto('data:text/html,<div>hi</div>');
});
`,
}, { trace: 'on' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
const tracePath = test.info().outputPath('test-results', 'a-pass', 'trace.zip');
const trace = await parseTrace(tracePath);
expect(trace.actionTree).toEqual([
'Before Hooks',
' fixture: browser',
' browserType.launch',
' fixture: workerStorageState',
' browser.newPage',
' page.setContent',
' page.close',
' fixture: context',
' browser.newContext',
' fixture: page',
' browserContext.newPage',
'page.goto',
'After Hooks',
' fixture: page',
' fixture: context',
]);
});
test('should record trace after fixture teardown timeout', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30718' });
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
fixture: async ({}, use) => {
await use('foo');
await new Promise(() => {});
},
})
test('fails', async ({ fixture, page }) => {
await page.evaluate(() => console.log('from the page'));
});
`,
}, { trace: 'on', timeout: '4000' });
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
const tracePath = test.info().outputPath('test-results', 'a-fails', 'trace.zip');
const trace = await parseTrace(tracePath);
expect(trace.actionTree).toEqual([
'Before Hooks',
' fixture: fixture',
' fixture: browser',
' browserType.launch',
' fixture: context',
' browser.newContext',
' fixture: page',
' browserContext.newPage',
'page.evaluate',
'After Hooks',
' fixture: page',
' fixture: context',
' fixture: fixture',
'Worker Cleanup',
' fixture: browser',
]);
// Check console events to make sure that library trace is recorded.
expect(trace.events).toContainEqual(expect.objectContaining({ type: 'console', text: 'from the page' }));
});
test('should take a screenshot-on-failure in workerStorageState', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30959' });
const result = await runInlineTest({
'playwright.config.ts': `
export default {
use: {
screenshot: 'only-on-failure',
},
};
`,
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
storageState: ({ workerStorageState }, use) => use(workerStorageState),
workerStorageState: [async ({ browser }, use) => {
const page = await browser.newPage({ storageState: undefined });
await page.setContent('hello world!');
throw new Error('Failed!');
await use(undefined);
}, { scope: 'worker' }],
})
test('fail', async ({ page }) => {
});
`,
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
expect(fs.existsSync(test.info().outputPath('test-results', 'a-fail', 'test-failed-1.png'))).toBeTruthy();
});

View file

@ -4,7 +4,7 @@ set -x
trap "cd $(pwd -P)" EXIT
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
NODE_VERSION="20.13.1" # autogenerated via ./update-playwright-driver-version.mjs
NODE_VERSION="20.14.0" # autogenerated via ./update-playwright-driver-version.mjs
cd "$(dirname "$0")"
PACKAGE_VERSION=$(node -p "require('../../package.json').version")

View file

@ -865,6 +865,8 @@ function csharpOptionOverloadSuffix(option, type) {
case 'function': return 'Func';
case 'Buffer': return 'Byte';
case 'Serializable': return 'Object';
case 'int': return 'Int';
case 'Date': return 'Date';
}
throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`);
}

View file

@ -50,6 +50,12 @@ const injectedScripts = [
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
true,
],
[
path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'fakeTimers.ts'),
path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'),
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
true,
],
[
path.join(ROOT, 'packages', 'playwright-ct-core', 'src', 'injected', 'index.ts'),
path.join(ROOT, 'packages', 'playwright-ct-core', 'lib', 'injected', 'packed'),