Merge branch 'microsoft:main' into cnm_dev

This commit is contained in:
Christopher Tangonan 2025-02-25 23:30:08 -08:00 committed by GitHub
commit 40a7985224
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
253 changed files with 27066 additions and 2547 deletions

View file

@ -33,7 +33,7 @@ jobs:
- name: Merge reports - name: Merge reports
run: | run: |
npx playwright merge-reports --reporter=html,packages/playwright/lib/reporters/markdown.js ./all-blob-reports npx playwright merge-reports --config .github/workflows/merge.config.ts ./all-blob-reports
env: env:
NODE_OPTIONS: --max-old-space-size=8192 NODE_OPTIONS: --max-old-space-size=8192

View file

@ -35,7 +35,7 @@ jobs:
exit 1 exit 1
fi fi
- name: Audit prod NPM dependencies - name: Audit prod NPM dependencies
run: npm audit --omit dev run: node utils/check_audit.js
lint-snippets: lint-snippets:
name: "Lint snippets" name: "Lint snippets"
runs-on: ubuntu-latest runs-on: ubuntu-latest

4
.github/workflows/merge.config.ts vendored Normal file
View file

@ -0,0 +1,4 @@
export default {
testDir: '../../tests',
reporter: [[require.resolve('../../packages/playwright/lib/reporters/markdown')], ['html']]
};

View file

@ -1,6 +1,6 @@
# 🎭 Playwright # 🎭 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-134.0.6998.15-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-135.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-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) [![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-134.0.6998.23-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-135.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-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) ## [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 | | | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: | | :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->134.0.6998.15<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Chromium <!-- GEN:chromium-version -->134.0.6998.23<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->135.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox <!-- GEN:firefox-version -->135.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

View file

@ -2035,9 +2035,9 @@ Triggers a `change` and `input` event once all the provided options have been se
```html ```html
<select multiple> <select multiple>
<option value="red">Red</div> <option value="red">Red</option>
<option value="green">Green</div> <option value="green">Green</option>
<option value="blue">Blue</div> <option value="blue">Blue</option>
</select> </select>
``` ```
@ -2332,7 +2332,7 @@ This method expects [Locator] to point to an
## async method: Locator.tap ## async method: Locator.tap
* since: v1.14 * since: v1.14
Perform a tap gesture on the element matching the locator. Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually dispatching touch events, see the [emulating legacy touch events](../touch-events.md) page.
**Details** **Details**
@ -2478,6 +2478,18 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.uncheck.trial = %%-input-trial-%% ### option: Locator.uncheck.trial = %%-input-trial-%%
* since: v1.14 * since: v1.14
## method: Locator.visible
* since: v1.51
- returns: <[Locator]>
Returns a locator that only matches [visible](../actionability.md#visible) elements.
### option: Locator.visible.visible
* since: v1.51
- `visible` <[boolean]>
Whether to match visible or invisible elements.
## async method: Locator.waitFor ## async method: Locator.waitFor
* since: v1.16 * since: v1.16

View file

@ -4,6 +4,8 @@
The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
touchscreen can only be used in browser contexts that have been initialized with `hasTouch` set to true. touchscreen can only be used in browser contexts that have been initialized with `hasTouch` set to true.
This class is limited to emulating tap gestures. For examples of other gestures simulated by manually dispatching touch events, see the [emulating legacy touch events](../touch-events.md) page.
## async method: Touchscreen.tap ## async method: Touchscreen.tap
* since: v1.8 * since: v1.8

View file

@ -1229,6 +1229,7 @@ Specify screenshot type, defaults to `png`.
Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with
a pink box `#FF00FF` (customized by [`option: maskColor`]) that completely covers its bounding box. a pink box `#FF00FF` (customized by [`option: maskColor`]) that completely covers its bounding box.
The mask is also applied to invisible elements, see [Matching only visible elements](../locators.md#matching-only-visible-elements) to disable that.
## screenshot-option-mask-color ## screenshot-option-mask-color
* since: v1.35 * since: v1.35

View file

@ -47,7 +47,7 @@ Create `tests/auth.setup.ts` that will prepare authenticated browser state for a
```js title="tests/auth.setup.ts" ```js title="tests/auth.setup.ts"
import { test as setup, expect } from '@playwright/test'; import { test as setup, expect } from '@playwright/test';
import * as path from 'path'; import path from 'path';
const authFile = path.join(__dirname, '../playwright/.auth/user.json'); const authFile = path.join(__dirname, '../playwright/.auth/user.json');
@ -143,8 +143,8 @@ Create `playwright/fixtures.ts` file that will [override `storageState` fixture]
```js title="playwright/fixtures.ts" ```js title="playwright/fixtures.ts"
import { test as baseTest, expect } from '@playwright/test'; import { test as baseTest, expect } from '@playwright/test';
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
export * from '@playwright/test'; export * from '@playwright/test';
export const test = baseTest.extend<{}, { workerStorageState: string }>({ export const test = baseTest.extend<{}, { workerStorageState: string }>({
@ -348,8 +348,8 @@ Alternatively, in a [worker fixture](#moderate-one-account-per-parallel-worker):
```js title="playwright/fixtures.ts" ```js title="playwright/fixtures.ts"
import { test as baseTest, request } from '@playwright/test'; import { test as baseTest, request } from '@playwright/test';
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
export * from '@playwright/test'; export * from '@playwright/test';
export const test = baseTest.extend<{}, { workerStorageState: string }>({ export const test = baseTest.extend<{}, { workerStorageState: string }>({

View file

@ -109,7 +109,7 @@ First, add fixtures that will load the extension:
```js title="fixtures.ts" ```js title="fixtures.ts"
import { test as base, chromium, type BrowserContext } from '@playwright/test'; import { test as base, chromium, type BrowserContext } from '@playwright/test';
import * as path from 'path'; import path from 'path';
export const test = base.extend<{ export const test = base.extend<{
context: BrowserContext; context: BrowserContext;

View file

@ -389,7 +389,7 @@ Next, add init script to the page.
```js ```js
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import * as path from 'path'; import path from 'path';
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Add script for every test in the beforeEach hook. // Add script for every test in the beforeEach hook.

View file

@ -1310,19 +1310,19 @@ Consider a page with two buttons, the first invisible and the second [visible](.
* This will only find a second button, because it is visible, and then click it. * This will only find a second button, because it is visible, and then click it.
```js ```js
await page.locator('button').locator('visible=true').click(); await page.locator('button').visible().click();
``` ```
```java ```java
page.locator("button").locator("visible=true").click(); page.locator("button").visible().click();
``` ```
```python async ```python async
await page.locator("button").locator("visible=true").click() await page.locator("button").visible().click()
``` ```
```python sync ```python sync
page.locator("button").locator("visible=true").click() page.locator("button").visible().click()
``` ```
```csharp ```csharp
await page.Locator("button").Locator("visible=true").ClickAsync(); await page.Locator("button").Visible().ClickAsync();
``` ```
## Lists ## Lists

View file

@ -708,9 +708,13 @@ Playwright uses simplified glob patterns for URL matching in network interceptio
- A double `**` matches any characters including `/` - A double `**` matches any characters including `/`
1. Question mark `?` matches any single character except `/` 1. Question mark `?` matches any single character except `/`
1. Curly braces `{}` can be used to match a list of options separated by commas `,` 1. Curly braces `{}` can be used to match a list of options separated by commas `,`
1. Square brackets `[]` can be used to match a set of characters
1. Backslash `\` can be used to escape any of special characters (note to escape backslash itself as `\\`)
Examples: Examples:
- `https://example.com/*.js` matches `https://example.com/file.js` but not `https://example.com/path/file.js` - `https://example.com/*.js` matches `https://example.com/file.js` but not `https://example.com/path/file.js`
- `https://example.com/\\?page=1` matches `https://example.com/?page=1` but not `https://example.com`
- `**/v[0-9]*` matches `https://example.com/v1/` but not `https://example.com/vote/`
- `**/*.js` matches both `https://example.com/file.js` and `https://example.com/path/file.js` - `**/*.js` matches both `https://example.com/file.js` and `https://example.com/path/file.js`
- `**/*.{png,jpg,jpeg}` matches all image requests - `**/*.{png,jpg,jpeg}` matches all image requests

View file

@ -824,9 +824,9 @@ This version was also tested against the following stable channels:
```html ```html
<select multiple> <select multiple>
<option value="red">Red</div> <option value="red">Red</option>
<option value="green">Green</div> <option value="green">Green</option>
<option value="blue">Blue</div> <option value="blue">Blue</option>
</select> </select>
``` ```

View file

@ -888,9 +888,9 @@ This version was also tested against the following stable channels:
```html ```html
<select multiple> <select multiple>
<option value="red">Red</div> <option value="red">Red</option>
<option value="green">Green</div> <option value="green">Green</option>
<option value="blue">Blue</div> <option value="blue">Blue</option>
</select> </select>
``` ```

View file

@ -1498,9 +1498,9 @@ This version was also tested against the following stable channels:
```html ```html
<select multiple> <select multiple>
<option value="red">Red</div> <option value="red">Red</option>
<option value="green">Green</div> <option value="green">Green</option>
<option value="blue">Blue</div> <option value="blue">Blue</option>
</select> </select>
``` ```

View file

@ -800,9 +800,9 @@ This version was also tested against the following stable channels:
```html ```html
<select multiple> <select multiple>
<option value="red">Red</div> <option value="red">Red</option>
<option value="green">Green</div> <option value="green">Green</option>
<option value="blue">Blue</div> <option value="blue">Blue</option>
</select> </select>
``` ```

View file

@ -239,7 +239,7 @@ export default defineConfig({
Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as key-value pairs, and JSON report will include metadata serialized as json. Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as key-value pairs, and JSON report will include metadata serialized as json.
See also [`property: TestConfig.populateGitInfo`] that populates metadata. Providing `'git.commit.info': {}` property will populate it with the git commit details. This is useful for CI/CD environments.
**Usage** **Usage**
@ -291,7 +291,7 @@ Here is an example that uses [`method: TestInfo.outputPath`] to create a tempora
```js ```js
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import * as fs from 'fs'; import fs from 'fs';
test('example test', async ({}, testInfo) => { test('example test', async ({}, testInfo) => {
const file = testInfo.outputPath('temporary-file.txt'); const file = testInfo.outputPath('temporary-file.txt');
@ -326,26 +326,6 @@ This path will serve as the base directory for each test file snapshot directory
## property: TestConfig.snapshotPathTemplate = %%-test-config-snapshot-path-template-%% ## property: TestConfig.snapshotPathTemplate = %%-test-config-snapshot-path-template-%%
* since: v1.28 * since: v1.28
## property: TestConfig.populateGitInfo
* since: v1.51
- type: ?<[boolean]>
Whether to populate `'git.commit.info'` field of the [`property: TestConfig.metadata`] with Git commit info and CI/CD information.
This information will appear in the HTML and JSON reports and is available in the Reporter API.
On Github Actions, this feature is enabled by default.
**Usage**
```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';
export default defineConfig({
populateGitInfo: !!process.env.CI,
});
```
## property: TestConfig.preserveOutput ## property: TestConfig.preserveOutput
* since: v1.10 * since: v1.10
- type: ?<[PreserveOutput]<"always"|"never"|"failures-only">> - type: ?<[PreserveOutput]<"always"|"never"|"failures-only">>
@ -680,7 +660,7 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({ export default defineConfig({
webServer: { webServer: {
command: 'npm run start', command: 'npm run start',
url: 'http://127.0.0.1:3000', url: 'http://localhost:3000',
timeout: 120 * 1000, timeout: 120 * 1000,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
@ -709,19 +689,19 @@ export default defineConfig({
webServer: [ webServer: [
{ {
command: 'npm run start', command: 'npm run start',
url: 'http://127.0.0.1:3000', url: 'http://localhost:3000',
timeout: 120 * 1000, timeout: 120 * 1000,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
{ {
command: 'npm run backend', command: 'npm run backend',
url: 'http://127.0.0.1:3333', url: 'http://localhost:3333',
timeout: 120 * 1000, timeout: 120 * 1000,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
} }
], ],
use: { use: {
baseURL: 'http://127.0.0.1:3000', baseURL: 'http://localhost:3000',
}, },
}); });
``` ```

View file

@ -254,7 +254,7 @@ Returns a path inside the [`property: TestInfo.outputDir`] where the test can sa
```js ```js
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import * as fs from 'fs'; import fs from 'fs';
test('example test', async ({}, testInfo) => { test('example test', async ({}, testInfo) => {
const file = testInfo.outputPath('dir', 'temporary-file.txt'); const file = testInfo.outputPath('dir', 'temporary-file.txt');

View file

@ -212,7 +212,7 @@ Here is an example that uses [`method: TestInfo.outputPath`] to create a tempora
```js ```js
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import * as fs from 'fs'; import fs from 'fs';
test('example test', async ({}, testInfo) => { test('example test', async ({}, testInfo) => {
const file = testInfo.outputPath('temporary-file.txt'); const file = testInfo.outputPath('temporary-file.txt');

View file

@ -853,6 +853,14 @@ export default defineConfig({
}); });
``` ```
### How do I use CSS imports?
If you have a component that imports CSS, Vite will handle it automatically. You can also use CSS pre-processors such as Sass, Less, or Stylus, and Vite will handle them as well without any additional configuration. However, corresponding CSS pre-processor needs to be installed.
Vite has a hard requirement that all CSS Modules are named `*.module.[css extension]`. If you have a custom build config for your project normally and have imports of the form `import styles from 'styles.css'` you must rename your files to properly indicate they are to be treated as modules. You could also write a Vite plugin to handle this for you.
Check [Vite documentation](https://vite.dev/guide/features#css) for more details.
### How can I test components that uses Pinia? ### How can I test components that uses Pinia?
Pinia needs to be initialized in `playwright/index.{js,ts,jsx,tsx}`. If you do this inside a `beforeMount` hook, the `initialState` can be overwritten on a per-test basis: Pinia needs to be initialized in `playwright/index.{js,ts,jsx,tsx}`. If you do this inside a `beforeMount` hook, the `initialState` can be overwritten on a per-test basis:

View file

@ -35,7 +35,7 @@ export default defineConfig({
use: { use: {
// Base URL to use in actions like `await page.goto('/')`. // Base URL to use in actions like `await page.goto('/')`.
baseURL: 'http://127.0.0.1:3000', baseURL: 'http://localhost:3000',
// Collect trace when retrying the failed test. // Collect trace when retrying the failed test.
trace: 'on-first-retry', trace: 'on-first-retry',
@ -50,7 +50,7 @@ export default defineConfig({
// Run your local dev server before starting the tests. // Run your local dev server before starting the tests.
webServer: { webServer: {
command: 'npm run start', command: 'npm run start',
url: 'http://127.0.0.1:3000', url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
}); });

View file

@ -408,7 +408,7 @@ Here is an example fixture that automatically attaches debug logs when the test
```js title="my-test.ts" ```js title="my-test.ts"
import debug from 'debug'; import debug from 'debug';
import * as fs from 'fs'; import fs from 'fs';
import { test as base } from '@playwright/test'; import { test as base } from '@playwright/test';
export const test = base.extend<{ saveLogs: void }>({ export const test = base.extend<{ saveLogs: void }>({

View file

@ -262,7 +262,7 @@ To make environment variables easier to manage, consider something like `.env` f
```js title="playwright.config.ts" ```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test'; import { defineConfig } from '@playwright/test';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import * as path from 'path'; import path from 'path';
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, '.env') }); dotenv.config({ path: path.resolve(__dirname, '.env') });
@ -309,8 +309,8 @@ See for example this CSV file, in our example `input.csv`:
Based on this we'll generate some tests by using the [csv-parse](https://www.npmjs.com/package/csv-parse) library from NPM: Based on this we'll generate some tests by using the [csv-parse](https://www.npmjs.com/package/csv-parse) library from NPM:
```js title="test.spec.ts" ```js title="test.spec.ts"
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { parse } from 'csv-parse/sync'; import { parse } from 'csv-parse/sync';

View file

@ -17,7 +17,7 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({ export default defineConfig({
use: { use: {
// Base URL to use in actions like `await page.goto('/')`. // Base URL to use in actions like `await page.goto('/')`.
baseURL: 'http://127.0.0.1:3000', baseURL: 'http://localhost:3000',
// Populates context with given storage state. // Populates context with given storage state.
storageState: 'state.json', storageState: 'state.json',

View file

@ -18,7 +18,7 @@ export default defineConfig({
// Run your local dev server before starting the tests // Run your local dev server before starting the tests
webServer: { webServer: {
command: 'npm run start', command: 'npm run start',
url: 'http://127.0.0.1:3000', url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
stdout: 'ignore', stdout: 'ignore',
stderr: 'pipe', stderr: 'pipe',
@ -52,7 +52,7 @@ export default defineConfig({
// Run your local dev server before starting the tests // Run your local dev server before starting the tests
webServer: { webServer: {
command: 'npm run start', command: 'npm run start',
url: 'http://127.0.0.1:3000', url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
timeout: 120 * 1000, timeout: 120 * 1000,
}, },
@ -63,7 +63,7 @@ export default defineConfig({
It is also recommended to specify the `baseURL` in the `use: {}` section of your config, so that tests can use relative urls and you don't have to specify the full URL over and over again. It is also recommended to specify the `baseURL` in the `use: {}` section of your config, so that tests can use relative urls and you don't have to specify the full URL over and over again.
When using [`method: Page.goto`], [`method: Page.route`], [`method: Page.waitForURL`], [`method: Page.waitForRequest`], or [`method: Page.waitForResponse`] it takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. For Example, by setting the baseURL to `http://127.0.0.1:3000` and navigating to `/login` in your tests, Playwright will run the test using `http://127.0.0.1:3000/login`. When using [`method: Page.goto`], [`method: Page.route`], [`method: Page.waitForURL`], [`method: Page.waitForRequest`], or [`method: Page.waitForResponse`] it takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. For Example, by setting the baseURL to `http://localhost:3000` and navigating to `/login` in your tests, Playwright will run the test using `http://localhost:3000/login`.
```js title="playwright.config.ts" ```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test'; import { defineConfig } from '@playwright/test';
@ -74,11 +74,11 @@ export default defineConfig({
// Run your local dev server before starting the tests // Run your local dev server before starting the tests
webServer: { webServer: {
command: 'npm run start', command: 'npm run start',
url: 'http://127.0.0.1:3000', url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
use: { use: {
baseURL: 'http://127.0.0.1:3000', baseURL: 'http://localhost:3000',
}, },
}); });
``` ```
@ -89,7 +89,7 @@ Now you can use a relative path when navigating the page:
import { test } from '@playwright/test'; import { test } from '@playwright/test';
test('test', async ({ page }) => { test('test', async ({ page }) => {
// This will navigate to http://127.0.0.1:3000/login // This will navigate to http://localhost:3000/login
await page.goto('./login'); await page.goto('./login');
}); });
``` ```
@ -106,19 +106,19 @@ export default defineConfig({
webServer: [ webServer: [
{ {
command: 'npm run start', command: 'npm run start',
url: 'http://127.0.0.1:3000', url: 'http://localhost:3000',
timeout: 120 * 1000, timeout: 120 * 1000,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
{ {
command: 'npm run backend', command: 'npm run backend',
url: 'http://127.0.0.1:3333', url: 'http://localhost:3333',
timeout: 120 * 1000, timeout: 120 * 1000,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
} }
], ],
use: { use: {
baseURL: 'http://127.0.0.1:3000', baseURL: 'http://localhost:3000',
}, },
}); });
``` ```

View file

@ -1,19 +1,13 @@
--- ---
id: touch-events id: touch-events
title: "Emulating touch events" title: "Emulating legacy touch events"
--- ---
## Introduction ## Introduction
Mobile web sites may listen to [touch events](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events) and react to user touch gestures such as swipe, pinch, tap etc. To test this functionality you can manually generate [TouchEvent]s in the page context using [`method: Locator.evaluate`]. Web applications that handle [touch events](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events) to respond to gestures like swipe, pinch, and tap can be tested by manually dispatching [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent)s to the page. The examples below demonstrate how to use [`method: Locator.dispatchEvent`] and pass [Touch](https://developer.mozilla.org/en-US/docs/Web/API/Touch) points as arguments.
If your web application relies on [pointer events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) instead of touch events, you can use [`method: Locator.click`] and raw [`Mouse`] events to simulate a single-finger touch, and this will trigger all the same pointer events. ### Emulating pan gesture
### Dispatching TouchEvent
You can dispatch touch events to the page using [`method: Locator.dispatchEvent`]. [Touch](https://developer.mozilla.org/en-US/docs/Web/API/Touch) points can be passed as arguments, see examples below.
#### Emulating pan gesture
In the example below, we emulate pan gesture that is expected to move the map. The app under test only uses `clientX/clientY` coordinates of the touch point, so we initialize just that. In a more complex scenario you may need to also set `pageX/pageY/screenX/screenY`, if your app needs them. In the example below, we emulate pan gesture that is expected to move the map. The app under test only uses `clientX/clientY` coordinates of the touch point, so we initialize just that. In a more complex scenario you may need to also set `pageX/pageY/screenX/screenY`, if your app needs them.
@ -69,7 +63,7 @@ test(`pan gesture to move the map`, async ({ page }) => {
}); });
``` ```
#### Emulating pinch gesture ### Emulating pinch gesture
In the example below, we emulate pinch gesture, i.e. two touch points moving closer to each other. It is expected to zoom out the map. The app under test only uses `clientX/clientY` coordinates of touch points, so we initialize just that. In a more complex scenario you may need to also set `pageX/pageY/screenX/screenY`, if your app needs them. In the example below, we emulate pinch gesture, i.e. two touch points moving closer to each other. It is expected to zoom out the map. The app under test only uses `clientX/clientY` coordinates of touch points, so we initialize just that. In a more complex scenario you may need to also set `pageX/pageY/screenX/screenY`, if your app needs them.

View file

@ -72,9 +72,9 @@ Using the following, Playwright will run your WebView2 application as a sub-proc
```js title="webView2Test.ts" ```js title="webView2Test.ts"
import { test as base } from '@playwright/test'; import { test as base } from '@playwright/test';
import * as fs from 'fs'; import fs from 'fs';
import * as os from 'os'; import os from 'os';
import * as path from 'path'; import path from 'path';
import childProcess from 'child_process'; import childProcess from 'child_process';
const EXECUTABLE_PATH = path.join( const EXECUTABLE_PATH = path.join(

View file

@ -177,7 +177,7 @@ const noBooleanCompareRules = {
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 2, '@typescript-eslint/no-unnecessary-boolean-literal-compare': 2,
}; };
const noRestrictedGlobalsRules = { const noWebGlobalsRules = {
'no-restricted-globals': [ 'no-restricted-globals': [
'error', 'error',
{ 'name': 'window' }, { 'name': 'window' },
@ -186,6 +186,13 @@ const noRestrictedGlobalsRules = {
], ],
}; };
const noNodeGlobalsRules = {
'no-restricted-globals': [
'error',
{ 'name': 'process' },
],
};
const importOrderRules = { const importOrderRules = {
'import/order': [2, { 'import/order': [2, {
'groups': ['builtin', 'external', 'internal', ['parent', 'sibling'], 'index', 'type'], 'groups': ['builtin', 'external', 'internal', ['parent', 'sibling'], 'index', 'type'],
@ -249,7 +256,19 @@ export default [{
files: ['packages/playwright-core/src/server/injected/**/*.ts'], files: ['packages/playwright-core/src/server/injected/**/*.ts'],
languageOptions: languageOptionsWithTsConfig, languageOptions: languageOptionsWithTsConfig,
rules: { rules: {
...noRestrictedGlobalsRules, ...noWebGlobalsRules,
...noFloatingPromisesRules,
...noBooleanCompareRules,
}
}, {
files: [
'packages/playwright-core/src/client/**/*.ts',
'packages/playwright-core/src/protocol/**/*.ts',
'packages/playwright-core/src/utils/**/*.ts',
],
languageOptions: languageOptionsWithTsConfig,
rules: {
...noNodeGlobalsRules,
...noFloatingPromisesRules, ...noFloatingPromisesRules,
...noBooleanCompareRules, ...noBooleanCompareRules,
} }

3796
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -105,9 +105,9 @@
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"ssim.js": "^3.5.0", "ssim.js": "^3.5.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"vite": "^5.4.14", "vite": "^6.1.0",
"ws": "^8.17.1", "ws": "^8.17.1",
"xml2js": "^0.5.0", "xml2js": "^0.5.0",
"yaml": "^2.6.0" "yaml": "2.6.0"
} }
} }

View file

@ -14,8 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
import type { Plugin, UserConfig } from 'vite'; import type { Plugin, UserConfig } from 'vite';
export function bundle(): Plugin { export function bundle(): Plugin {

View file

@ -15,8 +15,8 @@
*/ */
import { devices, defineConfig } from '@playwright/experimental-ct-react'; import { devices, defineConfig } from '@playwright/experimental-ct-react';
import * as path from 'path'; import path from 'path';
import * as url from 'url'; import url from 'url';
export default defineConfig({ export default defineConfig({
testDir: 'src', testDir: 'src',

View file

@ -267,6 +267,26 @@ article, aside, details, figcaption, figure, footer, header, main, menu, nav, se
flex: none; flex: none;
} }
.button {
flex: none;
height: 24px;
border: 1px solid var(--color-btn-border);
outline: none;
color: var(--color-btn-text);
background: var(--color-btn-bg);
padding: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.button:not(:disabled):hover {
border-color: var(--color-btn-hover-border);
background-color: var(--color-btn-hover-bg);
}
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
.subnav-item, .form-control { .subnav-item, .form-control {
border-radius: 0 !important; border-radius: 0 !important;

View file

@ -37,10 +37,6 @@
line-height: 24px; line-height: 24px;
} }
.metadata-section {
align-items: center;
}
.metadata-properties { .metadata-properties {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -57,9 +53,8 @@
border-bottom: 1px solid var(--color-border-default); border-bottom: 1px solid var(--color-border-default);
} }
.git-commit-info a { .metadata-view a {
color: var(--color-fg-default); color: var(--color-fg-default);
font-weight: 600;
} }
.copyable-property { .copyable-property {

View file

@ -87,12 +87,12 @@ const InnerMetadataView = () => {
<GitCommitInfoView info={gitCommitInfo}/> <GitCommitInfoView info={gitCommitInfo}/>
{entries.length > 0 && <div className='metadata-separator' />} {entries.length > 0 && <div className='metadata-separator' />}
</>} </>}
<div className='metadata-section metadata-properties'> <div className='metadata-section metadata-properties' role='list'>
{entries.map(([propertyName, value]) => { {entries.map(([propertyName, value]) => {
const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value); const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString; const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
return ( return (
<div key={propertyName} className='copyable-property'> <div key={propertyName} className='copyable-property' role='listitem'>
<CopyToClipboardContainer value={valueString}> <CopyToClipboardContainer value={valueString}>
<span style={{ fontWeight: 'bold' }} title={propertyName}>{propertyName}</span> <span style={{ fontWeight: 'bold' }} title={propertyName}>{propertyName}</span>
: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span> : <span title={trimmedValue}>{linkifyText(trimmedValue)}</span>
@ -105,47 +105,38 @@ const InnerMetadataView = () => {
}; };
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => { const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
const email = info['revision.email'] ? ` <${info['revision.email']}>` : ''; const email = info.revision?.email ? ` <${info.revision?.email}>` : '';
const author = `${info['revision.author'] || ''}${email}`; const author = `${info.revision?.author || ''}${email}`;
let subject = info['revision.subject'] || ''; let subject = info.revision?.subject || '';
let link = info['revision.link']; let link = info.revision?.link;
let shortSubject = info['revision.id']?.slice(0, 7) || 'unknown';
if (info['pull.link'] && info['pull.title']) { if (info.pull_request?.link && info.pull_request?.title) {
subject = info['pull.title']; subject = info.pull_request?.title;
link = info['pull.link']; link = info.pull_request?.link;
shortSubject = link ? 'Pull Request' : '';
} }
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info['revision.timestamp']); const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info.revision?.timestamp);
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info['revision.timestamp']); const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info.revision?.timestamp);
return <div className='hbox git-commit-info metadata-section'> return <div className='metadata-section' role='list'>
<div className='vbox metadata-properties'> <div role='listitem'>
<div> {link ? (
{link ? ( <a href={link} target='_blank' rel='noopener noreferrer' title={subject}>
<a href={link} target='_blank' rel='noopener noreferrer' title={subject}>
{subject}
</a>
) : <span title={subject}>
{subject} {subject}
</span>} </a>
</div> ) : <span title={subject}>
<div className='hbox'> {subject}
<span className='mr-1'>{author}</span> </span>}
<span title={longTimestamp}> on {shortTimestamp}</span> </div>
{info['ci.link'] && ( <div role='listitem' className='hbox'>
<> <span className='mr-1'>{author}</span>
<span className='mx-2'>·</span> <span title={longTimestamp}> on {shortTimestamp}</span>
<a href={info['ci.link']} target='_blank' rel='noopener noreferrer' title='CI/CD logs'>Logs</a> {info.ci?.link && (
</> <>
)} <span className='mx-2'>·</span>
</div> <a href={info.ci?.link} target='_blank' rel='noopener noreferrer' title='CI/CD logs'>Logs</a>
</>
)}
</div> </div>
{link ? (
<a href={link} target='_blank' rel='noopener noreferrer' title='View commit details'>
{shortSubject}
</a>
) : !!shortSubject && <span>{shortSubject}</span>}
</div>; </div>;
}; };

View file

@ -34,29 +34,3 @@
.test-error-text { .test-error-text {
font-family: monospace; font-family: monospace;
} }
.prompt-button {
flex: none;
height: 24px;
width: 80px;
border: 1px solid var(--color-btn-border);
outline: none;
color: var(--color-btn-text);
background: var(--color-btn-bg);
padding: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.prompt-button svg {
color: var(--color-fg-subtle);
}
.prompt-button:not(:disabled):hover {
border-color: var(--color-btn-hover-border);
background-color: var(--color-btn-hover-bg);
}

View file

@ -17,7 +17,6 @@
import { ansi2html } from '@web/ansi2html'; import { ansi2html } from '@web/ansi2html';
import * as React from 'react'; import * as React from 'react';
import './testErrorView.css'; import './testErrorView.css';
import * as icons from './icons';
import type { ImageDiff } from '@web/shared/imageDiffView'; import type { ImageDiff } from '@web/shared/imageDiffView';
import { ImageDiffView } from '@web/shared/imageDiffView'; import { ImageDiffView } from '@web/shared/imageDiffView';
import type { TestResult } from './types'; import type { TestResult } from './types';
@ -27,7 +26,7 @@ import { useGitCommitInfo } from './metadataView';
export const TestErrorView: React.FC<{ error: string; testId?: string; result?: TestResult }> = ({ error, testId, result }) => { export const TestErrorView: React.FC<{ error: string; testId?: string; result?: TestResult }> = ({ error, testId, result }) => {
return ( return (
<CodeSnippet code={error} testId={testId}> <CodeSnippet code={error} testId={testId}>
<div style={{ float: 'right', padding: '5px' }}> <div style={{ float: 'right', margin: 10 }}>
<PromptButton error={error} result={result} /> <PromptButton error={error} result={result} />
</div> </div>
</CodeSnippet> </CodeSnippet>
@ -51,14 +50,15 @@ const PromptButton: React.FC<{
const gitCommitInfo = useGitCommitInfo(); const gitCommitInfo = useGitCommitInfo();
const prompt = React.useMemo(() => fixTestPrompt( const prompt = React.useMemo(() => fixTestPrompt(
error, error,
gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'], gitCommitInfo?.pull_request?.diff ?? gitCommitInfo?.revision?.diff,
result?.attachments.find(a => a.name === 'pageSnapshot')?.body result?.attachments.find(a => a.name === 'pageSnapshot')?.body
), [gitCommitInfo, result, error]); ), [gitCommitInfo, result, error]);
const [copied, setCopied] = React.useState(false); const [copied, setCopied] = React.useState(false);
return <button return <button
className='prompt-button' className='button'
style={{ minWidth: 100 }}
onClick={async () => { onClick={async () => {
await navigator.clipboard.writeText(prompt); await navigator.clipboard.writeText(prompt);
setCopied(true); setCopied(true);
@ -66,7 +66,7 @@ const PromptButton: React.FC<{
setCopied(false); setCopied(false);
}, 3000); }, 3000);
}}> }}>
{copied ? <span className='prompt-button-copied'>Copied <icons.copy/></span> : 'Fix with AI'} {copied ? 'Copied' : 'Copy as Prompt'}
</button>; </button>;
}; };

View file

@ -17,7 +17,7 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { bundle } from './bundle'; import { bundle } from './bundle';
import * as path from 'path'; import path from 'path';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({

25
packages/playwright-client/index.d.ts vendored Normal file
View file

@ -0,0 +1,25 @@
/**
* 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 { Browser } from './types/types';
export * from './types/types';
export type Options = {
headless?: boolean;
};
export const connect: (wsEndpoint: string, browserName: string, options: Options) => Promise<Browser>;

View file

@ -0,0 +1,17 @@
/**
* 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.
*/
module.exports = require('./lib/index');

View file

@ -0,0 +1,17 @@
/**
* 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.
*/
export { connect } from './index.js';

View file

@ -0,0 +1,34 @@
{
"name": "@playwright/client",
"private": true,
"version": "1.51.0-next",
"description": "A thin client for Playwright",
"repository": {
"type": "git",
"url": "git+https://github.com/microsoft/playwright.git"
},
"homepage": "https://playwright.dev",
"engines": {
"node": ">=18"
},
"author": {
"name": "Microsoft Corporation"
},
"license": "Apache-2.0",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.mjs",
"require": "./index.js",
"default": "./index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "esbuild ./src/index.ts --outdir=lib --format=cjs --bundle --platform=node --target=ES2019",
"watch": "esbuild ./src/index.ts --outdir=lib --format=cjs --bundle --platform=node --target=ES2019 --watch"
},
"dependencies": {
"playwright-core": "1.51.0-next"
}
}

View file

@ -0,0 +1,40 @@
/**
* 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 { Connection } from '../../playwright-core/src/client/connection';
import { webPlatform } from './webPlatform';
import type { Browser } from '../../playwright-core/src/client/browser';
export type Options = {
headless?: boolean;
};
export async function connect(wsEndpoint: string, browserName: string, options: Options): Promise<Browser> {
const ws = new WebSocket(`${wsEndpoint}?browser=${browserName}&launch-options=${JSON.stringify(options)}`);
await new Promise((f, r) => {
ws.addEventListener('open', f);
ws.addEventListener('error', r);
});
const connection = new Connection(webPlatform);
connection.onmessage = message => ws.send(JSON.stringify(message));
ws.addEventListener('message', message => connection.dispatch(JSON.parse(message.data)));
ws.addEventListener('close', () => connection.close());
const playwright = await connection.initializePlaywright();
return playwright._preLaunchedBrowser();
}

View file

@ -0,0 +1,48 @@
/* eslint-disable no-console */
/**
* 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 { emptyPlatform } from '../../playwright-core/src/client/platform';
import type { Platform } from '../../playwright-core/src/client/platform';
export const webPlatform: Platform = {
...emptyPlatform,
name: 'web',
boxedStackPrefixes: () => [],
calculateSha1: async (text: string) => {
const bytes = new TextEncoder().encode(text);
const hashBuffer = await window.crypto.subtle.digest('SHA-1', bytes);
return Array.from(new Uint8Array(hashBuffer), b => b.toString(16).padStart(2, '0')).join('');
},
createGuid: () => {
return Array.from(window.crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('');
},
isLogEnabled(name: 'api' | 'channel') {
return true;
},
log(name: 'api' | 'channel', message: string | Error | object) {
console.debug(name, message);
},
showInternalStackFrames: () => true,
};

22992
packages/playwright-client/types/types.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,27 +3,27 @@
"browsers": [ "browsers": [
{ {
"name": "chromium", "name": "chromium",
"revision": "1159", "revision": "1160",
"installByDefault": true, "installByDefault": true,
"browserVersion": "134.0.6998.15" "browserVersion": "134.0.6998.23"
}, },
{ {
"name": "chromium-headless-shell", "name": "chromium-headless-shell",
"revision": "1159", "revision": "1160",
"installByDefault": true, "installByDefault": true,
"browserVersion": "134.0.6998.15" "browserVersion": "134.0.6998.23"
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",
"revision": "1303", "revision": "1304",
"installByDefault": false, "installByDefault": false,
"browserVersion": "135.0.7015.0" "browserVersion": "135.0.7021.0"
}, },
{ {
"name": "chromium-tip-of-tree-headless-shell", "name": "chromium-tip-of-tree-headless-shell",
"revision": "1303", "revision": "1304",
"installByDefault": false, "installByDefault": false,
"browserVersion": "135.0.7015.0" "browserVersion": "135.0.7021.0"
}, },
{ {
"name": "firefox", "name": "firefox",
@ -33,13 +33,13 @@
}, },
{ {
"name": "firefox-beta", "name": "firefox-beta",
"revision": "1470", "revision": "1471",
"installByDefault": false, "installByDefault": false,
"browserVersion": "135.0b10" "browserVersion": "136.0b4"
}, },
{ {
"name": "webkit", "name": "webkit",
"revision": "2137", "revision": "2140",
"installByDefault": true, "installByDefault": true,
"revisionOverrides": { "revisionOverrides": {
"debian11-x64": "2105", "debian11-x64": "2105",

View file

@ -16,7 +16,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import * as fs from 'fs'; import fs from 'fs';
import * as playwright from '../..'; import * as playwright from '../..';
import { PipeTransport } from '../server/utils/pipeTransport'; import { PipeTransport } from '../server/utils/pipeTransport';

View file

@ -16,16 +16,16 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import * as fs from 'fs'; import fs from 'fs';
import * as os from 'os'; import os from 'os';
import * as path from 'path'; import path from 'path';
import * as playwright from '../..'; import * as playwright from '../..';
import { launchBrowserServer, printApiJson, runDriver, runServer } from './driver'; import { launchBrowserServer, printApiJson, runDriver, runServer } from './driver';
import { registry, writeDockerVersion } from '../server'; import { registry, writeDockerVersion } from '../server';
import { gracefullyProcessExitDoNotHang } from '../utils'; import { gracefullyProcessExitDoNotHang, isLikelyNpxGlobal } from '../utils';
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer'; import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
import { assert, getPackageManagerExecCommand, isLikelyNpxGlobal } from '../utils'; import { assert, getPackageManagerExecCommand } from '../utils';
import { wrapInASCIIBox } from '../server/utils/ascii'; import { wrapInASCIIBox } from '../server/utils/ascii';
import { dotenv, program } from '../utilsBundle'; import { dotenv, program } from '../utilsBundle';

View file

@ -51,7 +51,9 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
setDefaultTimeout(timeout: number) { setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout); this._timeoutSettings.setDefaultTimeout(timeout);
this._channel.setDefaultTimeoutNoReply({ timeout }); this._wrapApiCall(async () => {
await this._channel.setDefaultTimeoutNoReply({ timeout });
}, true).catch(() => {});
} }
async devices(options: { port?: number } = {}): Promise<AndroidDevice[]> { async devices(options: { port?: number } = {}): Promise<AndroidDevice[]> {
@ -133,7 +135,9 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
setDefaultTimeout(timeout: number) { setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout); this._timeoutSettings.setDefaultTimeout(timeout);
this._channel.setDefaultTimeoutNoReply({ timeout }); this._wrapApiCall(async () => {
await this._channel.setDefaultTimeoutNoReply({ timeout });
}, true).catch(() => {});
} }
serial(): string { serial(): string {
@ -393,7 +397,7 @@ export class AndroidWebView extends EventEmitter implements api.AndroidWebView {
private _pagePromise: Promise<Page> | undefined; private _pagePromise: Promise<Page> | undefined;
constructor(device: AndroidDevice, data: channels.AndroidWebView) { constructor(device: AndroidDevice, data: channels.AndroidWebView) {
super(); super(device._platform);
this._device = device; this._device = device;
this._data = data; this._data = data;
} }

View file

@ -246,15 +246,15 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
setDefaultNavigationTimeout(timeout: number | undefined) { setDefaultNavigationTimeout(timeout: number | undefined) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout); this._timeoutSettings.setDefaultNavigationTimeout(timeout);
this._wrapApiCall(async () => { this._wrapApiCall(async () => {
this._channel.setDefaultNavigationTimeoutNoReply({ timeout }).catch(() => {}); await this._channel.setDefaultNavigationTimeoutNoReply({ timeout });
}, true); }, true).catch(() => {});
} }
setDefaultTimeout(timeout: number | undefined) { setDefaultTimeout(timeout: number | undefined) {
this._timeoutSettings.setDefaultTimeout(timeout); this._timeoutSettings.setDefaultTimeout(timeout);
this._wrapApiCall(async () => { this._wrapApiCall(async () => {
this._channel.setDefaultTimeoutNoReply({ timeout }).catch(() => {}); await this._channel.setDefaultTimeoutNoReply({ timeout });
}, true); }, true).catch(() => {});
} }
browser(): Browser | null { browser(): Browser | null {
@ -559,7 +559,7 @@ export async function prepareBrowserContextParams(platform: Platform, options: B
}; };
} }
if (contextParams.recordVideo && contextParams.recordVideo.dir) if (contextParams.recordVideo && contextParams.recordVideo.dir)
contextParams.recordVideo.dir = platform.path().resolve(process.cwd(), contextParams.recordVideo.dir); contextParams.recordVideo.dir = platform.path().resolve(contextParams.recordVideo.dir);
return contextParams; return contextParams;
} }

View file

@ -38,21 +38,20 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
readonly _channel: T; readonly _channel: T;
readonly _initializer: channels.InitializerTraits<T>; readonly _initializer: channels.InitializerTraits<T>;
_logger: Logger | undefined; _logger: Logger | undefined;
readonly _platform: Platform;
readonly _instrumentation: ClientInstrumentation; readonly _instrumentation: ClientInstrumentation;
private _eventToSubscriptionMapping: Map<string, string> = new Map(); private _eventToSubscriptionMapping: Map<string, string> = new Map();
private _isInternalType = false; private _isInternalType = false;
_wasCollected: boolean = false; _wasCollected: boolean = false;
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>) { constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>) {
super(); const connection = parent instanceof ChannelOwner ? parent._connection : parent;
super(connection._platform);
this.setMaxListeners(0); this.setMaxListeners(0);
this._connection = parent instanceof ChannelOwner ? parent._connection : parent; this._connection = connection;
this._type = type; this._type = type;
this._guid = guid; this._guid = guid;
this._parent = parent instanceof ChannelOwner ? parent : undefined; this._parent = parent instanceof ChannelOwner ? parent : undefined;
this._instrumentation = this._connection._instrumentation; this._instrumentation = this._connection._instrumentation;
this._platform = this._connection.platform;
this._connection._objects.set(guid, this); this._connection._objects.set(guid, this);
if (this._parent) { if (this._parent) {
@ -60,7 +59,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
this._logger = this._parent._logger; this._logger = this._parent._logger;
} }
this._channel = this._createChannel(new EventEmitter()); this._channel = this._createChannel(new EventEmitter(connection._platform));
this._initializer = initializer; this._initializer = initializer;
} }
@ -142,6 +141,14 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
}; };
} }
private _validatorToWireContext(): ValidatorContext {
return {
tChannelImpl: tChannelImplToWire,
binary: this._connection.rawBuffers() ? 'buffer' : 'toBase64',
isUnderTest: () => this._platform.isUnderTest(),
};
}
private _createChannel(base: Object): T { private _createChannel(base: Object): T {
const channel = new Proxy(base, { const channel = new Proxy(base, {
get: (obj: any, prop: string | symbol) => { get: (obj: any, prop: string | symbol) => {
@ -150,7 +157,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
if (validator) { if (validator) {
return async (params: any) => { return async (params: any) => {
return await this._wrapApiCall(async apiZone => { return await this._wrapApiCall(async apiZone => {
const validatedParams = validator(params, '', { tChannelImpl: tChannelImplToWire, binary: this._connection.rawBuffers() ? 'buffer' : 'toBase64' }); const validatedParams = validator(params, '', this._validatorToWireContext());
if (!apiZone.isInternal && !apiZone.reported) { if (!apiZone.isInternal && !apiZone.reported) {
// Reporting/tracing/logging this api call for the first time. // Reporting/tracing/logging this api call for the first time.
apiZone.params = params; apiZone.params = params;
@ -192,7 +199,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
} }
return result; return result;
} catch (e) { } catch (e) {
const innerError = ((process.env.PWDEBUGIMPL || this._platform.isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : ''; const innerError = ((this._platform.showInternalStackFrames() || this._platform.isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : '';
if (apiZone.apiName && !apiZone.apiName.includes('<anonymous>')) if (apiZone.apiName && !apiZone.apiName.includes('<anonymous>'))
e.message = apiZone.apiName + ': ' + e.message; e.message = apiZone.apiName + ': ' + e.message;
const stackFrames = '\n' + stringifyStackFrames(stackTrace.frames).join('\n') + innerError; const stackFrames = '\n' + stringifyStackFrames(stackTrace.frames).join('\n') + innerError;

View file

@ -1,29 +0,0 @@
/**
* 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 { Connection } from './connection';
import { setPlatformForSelectors } from './selectors';
import { setPlatformForEventEmitter } from './eventEmitter';
import { setIsUnderTestForValidator } from '../protocol/validatorPrimitives';
import type { Platform } from './platform';
export function createConnectionFactory(platform: Platform): () => Connection {
setPlatformForSelectors(platform);
setPlatformForEventEmitter(platform);
setIsUnderTestForValidator(() => platform.isUnderTest());
return () => new Connection(platform);
}

View file

@ -28,7 +28,7 @@ export function captureLibraryStackTrace(platform: Platform): { frames: StackFra
isPlaywrightLibrary: boolean; isPlaywrightLibrary: boolean;
}; };
let parsedFrames = stack.map(line => { let parsedFrames = stack.map(line => {
const frame = parseStackFrame(line, platform.pathSeparator); const frame = parseStackFrame(line, platform.pathSeparator, platform.showInternalStackFrames());
if (!frame || !frame.file) if (!frame || !frame.file)
return null; return null;
const isPlaywrightLibrary = !!platform.coreDir && frame.file.startsWith(platform.coreDir); const isPlaywrightLibrary = !!platform.coreDir && frame.file.startsWith(platform.coreDir);
@ -62,10 +62,8 @@ export function captureLibraryStackTrace(platform: Platform): { frames: StackFra
} }
// This is for the inspector so that it did not include the test runner stack frames. // This is for the inspector so that it did not include the test runner stack frames.
const filterPrefixes = platform.coreDir ? [platform.coreDir, ...platform.boxedStackPrefixes()] : platform.boxedStackPrefixes(); const filterPrefixes = platform.boxedStackPrefixes();
parsedFrames = parsedFrames.filter(f => { parsedFrames = parsedFrames.filter(f => {
if (process.env.PWDEBUGIMPL)
return true;
if (filterPrefixes.some(prefix => f.frame.file.startsWith(prefix))) if (filterPrefixes.some(prefix => f.frame.file.startsWith(prefix)))
return false; return false;
return true; return true;

View file

@ -78,15 +78,13 @@ export class Connection extends EventEmitter {
toImpl: ((client: ChannelOwner) => any) | undefined; toImpl: ((client: ChannelOwner) => any) | undefined;
private _tracingCount = 0; private _tracingCount = 0;
readonly _instrumentation: ClientInstrumentation; readonly _instrumentation: ClientInstrumentation;
readonly platform: Platform;
// Used from @playwright/test fixtures -> TODO remove? // Used from @playwright/test fixtures -> TODO remove?
readonly headers: HeadersArray; readonly headers: HeadersArray;
constructor(platform: Platform, localUtils?: LocalUtils, instrumentation?: ClientInstrumentation, headers: HeadersArray = []) { constructor(platform: Platform, localUtils?: LocalUtils, instrumentation?: ClientInstrumentation, headers: HeadersArray = []) {
super(); super(platform);
this._instrumentation = instrumentation || createInstrumentation(); this._instrumentation = instrumentation || createInstrumentation();
this._localUtils = localUtils; this._localUtils = localUtils;
this.platform = platform;
this._rootObject = new Root(this); this._rootObject = new Root(this);
this.headers = headers; this.headers = headers;
} }
@ -136,9 +134,9 @@ export class Connection extends EventEmitter {
const type = object._type; const type = object._type;
const id = ++this._lastId; const id = ++this._lastId;
const message = { id, guid, method, params }; const message = { id, guid, method, params };
if (this.platform.isLogEnabled('channel')) { if (this._platform.isLogEnabled('channel')) {
// Do not include metadata in debug logs to avoid noise. // Do not include metadata in debug logs to avoid noise.
this.platform.log('channel', 'SEND> ' + JSON.stringify(message)); this._platform.log('channel', 'SEND> ' + JSON.stringify(message));
} }
const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined; const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined;
const metadata: channels.Metadata = { apiName, location, internal: !apiName, stepId }; const metadata: channels.Metadata = { apiName, location, internal: !apiName, stepId };
@ -146,35 +144,43 @@ export class Connection extends EventEmitter {
this._localUtils?.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {}); this._localUtils?.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
// We need to exit zones before calling into the server, otherwise // We need to exit zones before calling into the server, otherwise
// when we receive events from the server, we would be in an API zone. // when we receive events from the server, we would be in an API zone.
this.platform.zones.empty.run(() => this.onmessage({ ...message, metadata })); this._platform.zones.empty.run(() => this.onmessage({ ...message, metadata }));
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method })); return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method }));
} }
private _validatorFromWireContext(): ValidatorContext {
return {
tChannelImpl: this._tChannelImplFromWire.bind(this),
binary: this._rawBuffers ? 'buffer' : 'fromBase64',
isUnderTest: () => this._platform.isUnderTest(),
};
}
dispatch(message: object) { dispatch(message: object) {
if (this._closedError) if (this._closedError)
return; return;
const { id, guid, method, params, result, error, log } = message as any; const { id, guid, method, params, result, error, log } = message as any;
if (id) { if (id) {
if (this.platform.isLogEnabled('channel')) if (this._platform.isLogEnabled('channel'))
this.platform.log('channel', '<RECV ' + JSON.stringify(message)); this._platform.log('channel', '<RECV ' + JSON.stringify(message));
const callback = this._callbacks.get(id); const callback = this._callbacks.get(id);
if (!callback) if (!callback)
throw new Error(`Cannot find command to respond: ${id}`); throw new Error(`Cannot find command to respond: ${id}`);
this._callbacks.delete(id); this._callbacks.delete(id);
if (error && !result) { if (error && !result) {
const parsedError = parseError(error); const parsedError = parseError(error);
rewriteErrorMessage(parsedError, parsedError.message + formatCallLog(this.platform, log)); rewriteErrorMessage(parsedError, parsedError.message + formatCallLog(this._platform, log));
callback.reject(parsedError); callback.reject(parsedError);
} else { } else {
const validator = findValidator(callback.type, callback.method, 'Result'); const validator = findValidator(callback.type, callback.method, 'Result');
callback.resolve(validator(result, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._rawBuffers ? 'buffer' : 'fromBase64' })); callback.resolve(validator(result, '', this._validatorFromWireContext()));
} }
return; return;
} }
if (this.platform.isLogEnabled('channel')) if (this._platform.isLogEnabled('channel'))
this.platform.log('channel', '<EVENT ' + JSON.stringify(message)); this._platform.log('channel', '<EVENT ' + JSON.stringify(message));
if (method === '__create__') { if (method === '__create__') {
this._createRemoteObject(guid, params.type, params.guid, params.initializer); this._createRemoteObject(guid, params.type, params.guid, params.initializer);
return; return;
@ -198,7 +204,7 @@ export class Connection extends EventEmitter {
} }
const validator = findValidator(object._type, method, 'Event'); const validator = findValidator(object._type, method, 'Event');
(object._channel as any).emit(method, validator(params, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._rawBuffers ? 'buffer' : 'fromBase64' })); (object._channel as any).emit(method, validator(params, '', this._validatorFromWireContext()));
} }
close(cause?: string) { close(cause?: string) {
@ -229,7 +235,7 @@ export class Connection extends EventEmitter {
throw new Error(`Cannot find parent object ${parentGuid} to create ${guid}`); throw new Error(`Cannot find parent object ${parentGuid} to create ${guid}`);
let result: ChannelOwner<any>; let result: ChannelOwner<any>;
const validator = findValidator(type, '', 'Initializer'); const validator = findValidator(type, '', 'Initializer');
initializer = validator(initializer, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._rawBuffers ? 'buffer' : 'fromBase64' }); initializer = validator(initializer, '', this._validatorFromWireContext());
switch (type) { switch (type) {
case 'Android': case 'Android':
result = new Android(parent, type, guid, initializer); result = new Android(parent, type, guid, initializer);

View file

@ -54,7 +54,7 @@ export class Electron extends ChannelOwner<channels.ElectronChannel> implements
async launch(options: ElectronOptions = {}): Promise<ElectronApplication> { async launch(options: ElectronOptions = {}): Promise<ElectronApplication> {
const params: channels.ElectronLaunchParams = { const params: channels.ElectronLaunchParams = {
...await prepareBrowserContextParams(this._platform, options), ...await prepareBrowserContextParams(this._platform, options),
env: envObjectToArray(options.env ? options.env : process.env), env: envObjectToArray(options.env ? options.env : this._platform.env),
tracesDir: options.tracesDir, tracesDir: options.tracesDir,
}; };
const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication); const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication);

View file

@ -22,8 +22,6 @@
* USE OR OTHER DEALINGS IN THE SOFTWARE. * USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { emptyPlatform } from './platform';
import type { EventEmitter as EventEmitterType } from 'events'; import type { EventEmitter as EventEmitterType } from 'events';
import type { Platform } from './platform'; import type { Platform } from './platform';
@ -31,12 +29,6 @@ type EventType = string | symbol;
type Listener = (...args: any[]) => any; type Listener = (...args: any[]) => any;
type EventMap = Record<EventType, Listener | Listener[]>; type EventMap = Record<EventType, Listener | Listener[]>;
let platform = emptyPlatform;
export function setPlatformForEventEmitter(p: Platform) {
platform = p;
}
export class EventEmitter implements EventEmitterType { export class EventEmitter implements EventEmitterType {
private _events: EventMap | undefined = undefined; private _events: EventMap | undefined = undefined;
@ -44,8 +36,10 @@ export class EventEmitter implements EventEmitterType {
private _maxListeners: number | undefined = undefined; private _maxListeners: number | undefined = undefined;
readonly _pendingHandlers = new Map<EventType, Set<Promise<void>>>(); readonly _pendingHandlers = new Map<EventType, Set<Promise<void>>>();
private _rejectionHandler: ((error: Error) => void) | undefined; private _rejectionHandler: ((error: Error) => void) | undefined;
readonly _platform: Platform;
constructor() { constructor(platform: Platform) {
this._platform = platform;
if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) { if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) {
this._events = Object.create(null); this._events = Object.create(null);
this._eventsCount = 0; this._eventsCount = 0;
@ -63,7 +57,7 @@ export class EventEmitter implements EventEmitterType {
} }
getMaxListeners(): number { getMaxListeners(): number {
return this._maxListeners === undefined ? platform.defaultMaxListeners() : this._maxListeners; return this._maxListeners === undefined ? this._platform.defaultMaxListeners() : this._maxListeners;
} }
emit(type: EventType, ...args: any[]): boolean { emit(type: EventType, ...args: any[]): boolean {
@ -161,7 +155,7 @@ export class EventEmitter implements EventEmitterType {
w.emitter = this; w.emitter = this;
w.type = type; w.type = type;
w.count = existing.length; w.count = existing.length;
if (!platform.isUnderTest()) { if (!this._platform.isUnderTest()) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn(w); console.warn(w);
} }

View file

@ -338,16 +338,18 @@ export class APIResponse implements api.APIResponse {
} }
async body(): Promise<Buffer> { async body(): Promise<Buffer> {
try { return await this._request._wrapApiCall(async () => {
const result = await this._request._channel.fetchResponseBody({ fetchUid: this._fetchUid() }); try {
if (result.binary === undefined) const result = await this._request._channel.fetchResponseBody({ fetchUid: this._fetchUid() });
throw new Error('Response has been disposed'); if (result.binary === undefined)
return result.binary; throw new Error('Response has been disposed');
} catch (e) { return result.binary;
if (isTargetClosedError(e)) } catch (e) {
throw new Error('Response has been disposed'); if (isTargetClosedError(e))
throw e; throw new Error('Response has been disposed');
} throw e;
}
}, true);
} }
async text(): Promise<string> { async text(): Promise<string> {

View file

@ -64,7 +64,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.FrameInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.FrameInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this._eventEmitter = new EventEmitter(); this._eventEmitter = new EventEmitter(parent._platform);
this._eventEmitter.setMaxListeners(0); this._eventEmitter.setMaxListeners(0);
this._parentFrame = Frame.fromNullable(initializer.parentFrame); this._parentFrame = Frame.fromNullable(initializer.parentFrame);
if (this._parentFrame) if (this._parentFrame)

View file

@ -218,6 +218,11 @@ export class Locator implements api.Locator {
return new Locator(this._frame, this._selector + ` >> nth=${index}`); return new Locator(this._frame, this._selector + ` >> nth=${index}`);
} }
visible(options: { visible?: boolean } = {}): Locator {
const { visible = true } = options;
return new Locator(this._frame, this._selector + ` >> visible=${visible ? 'true' : 'false'}`);
}
and(locator: Locator): Locator { and(locator: Locator): Locator {
if (locator._frame !== this._frame) if (locator._frame !== this._frame)
throw new Error(`Locators must belong to the same frame.`); throw new Error(`Locators must belong to the same frame.`);

View file

@ -277,15 +277,15 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
setDefaultNavigationTimeout(timeout: number) { setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout); this._timeoutSettings.setDefaultNavigationTimeout(timeout);
this._wrapApiCall(async () => { this._wrapApiCall(async () => {
this._channel.setDefaultNavigationTimeoutNoReply({ timeout }).catch(() => {}); await this._channel.setDefaultNavigationTimeoutNoReply({ timeout });
}, true); }, true).catch(() => {});
} }
setDefaultTimeout(timeout: number) { setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout); this._timeoutSettings.setDefaultTimeout(timeout);
this._wrapApiCall(async () => { this._wrapApiCall(async () => {
this._channel.setDefaultTimeoutNoReply({ timeout }).catch(() => {}); await this._channel.setDefaultTimeoutNoReply({ timeout });
}, true); }, true).catch(() => {});
} }
private _forceVideo(): Video { private _forceVideo(): Video {

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { webColors, noColors } from '../utils/isomorphic/colors'; import { webColors } from '../utils/isomorphic/colors';
import type * as fs from 'fs'; import type * as fs from 'fs';
import type * as path from 'path'; import type * as path from 'path';
@ -45,6 +45,7 @@ export type Platform = {
coreDir?: string; coreDir?: string;
createGuid: () => string; createGuid: () => string;
defaultMaxListeners: () => number; defaultMaxListeners: () => number;
env: Record<string, string | undefined>;
fs: () => typeof fs; fs: () => typeof fs;
inspectCustom: symbol | undefined; inspectCustom: symbol | undefined;
isDebugMode: () => boolean; isDebugMode: () => boolean;
@ -54,6 +55,7 @@ export type Platform = {
log: (name: 'api' | 'channel', message: string | Error | object) => void; log: (name: 'api' | 'channel', message: string | Error | object) => void;
path: () => typeof path; path: () => typeof path;
pathSeparator: string; pathSeparator: string;
showInternalStackFrames: () => boolean,
streamFile: (path: string, writable: Writable) => Promise<void>, streamFile: (path: string, writable: Writable) => Promise<void>,
streamReadable: (channel: channels.StreamChannel) => Readable, streamReadable: (channel: channels.StreamChannel) => Readable,
streamWritable: (channel: channels.WritableStreamChannel) => Writable, streamWritable: (channel: channels.WritableStreamChannel) => Writable,
@ -69,7 +71,7 @@ export const emptyPlatform: Platform = {
throw new Error('Not implemented'); throw new Error('Not implemented');
}, },
colors: noColors, colors: webColors,
createGuid: () => { createGuid: () => {
throw new Error('Not implemented'); throw new Error('Not implemented');
@ -77,6 +79,8 @@ export const emptyPlatform: Platform = {
defaultMaxListeners: () => 10, defaultMaxListeners: () => 10,
env: {},
fs: () => { fs: () => {
throw new Error('Not implemented'); throw new Error('Not implemented');
}, },
@ -101,6 +105,8 @@ export const emptyPlatform: Platform = {
pathSeparator: '/', pathSeparator: '/',
showInternalStackFrames: () => false,
streamFile(path: string, writable: Writable): Promise<void> { streamFile(path: string, writable: Writable): Promise<void> {
throw new Error('Streams are not available'); throw new Error('Streams are not available');
}, },
@ -115,23 +121,3 @@ export const emptyPlatform: Platform = {
zones: { empty: noopZone, current: () => noopZone }, zones: { empty: noopZone, current: () => noopZone },
}; };
export const webPlatform: Platform = {
...emptyPlatform,
name: 'web',
boxedStackPrefixes: () => [],
calculateSha1: async (text: string) => {
const bytes = new TextEncoder().encode(text);
const hashBuffer = await window.crypto.subtle.digest('SHA-1', bytes);
return Array.from(new Uint8Array(hashBuffer), b => b.toString(16).padStart(2, '0')).join('');
},
colors: webColors,
createGuid: () => {
return Array.from(window.crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('');
},
};

View file

@ -15,6 +15,7 @@
*/ */
import { Android } from './android'; import { Android } from './android';
import { Browser } from './browser';
import { BrowserType } from './browserType'; import { BrowserType } from './browserType';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import { Electron } from './electron'; import { Electron } from './electron';
@ -86,6 +87,12 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
return [this.chromium, this.firefox, this.webkit, this._bidiChromium, this._bidiFirefox]; return [this.chromium, this.firefox, this.webkit, this._bidiChromium, this._bidiFirefox];
} }
_preLaunchedBrowser(): Browser {
const browser = Browser.from(this._initializer.preLaunchedBrowser!);
browser._browserType = this[browser._name as 'chromium' | 'firefox' | 'webkit'];
return browser;
}
_allContexts() { _allContexts() {
return this._browserTypes().flatMap(type => [...type._contexts]); return this._browserTypes().flatMap(type => [...type._contexts]);
} }

View file

@ -96,8 +96,8 @@ export class Waiter {
log(s: string) { log(s: string) {
this._logs.push(s); this._logs.push(s);
this._channelOwner._wrapApiCall(async () => { this._channelOwner._wrapApiCall(async () => {
await this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'log', message: s } }).catch(() => {}); await this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'log', message: s } });
}, true); }, true).catch(() => {});
} }
private _rejectOn(promise: Promise<any>, dispose?: () => void) { private _rejectOn(promise: Promise<any>, dispose?: () => void) {

View file

@ -24,7 +24,7 @@ export async function connectOverWebSocket(parentConnection: Connection, params:
const localUtils = parentConnection.localUtils(); const localUtils = parentConnection.localUtils();
const transport = localUtils ? new JsonPipeTransport(localUtils) : new WebSocketTransport(); const transport = localUtils ? new JsonPipeTransport(localUtils) : new WebSocketTransport();
const connectHeaders = await transport.connect(params); const connectHeaders = await transport.connect(params);
const connection = new Connection(parentConnection.platform, localUtils, parentConnection._instrumentation, connectHeaders); const connection = new Connection(parentConnection._platform, localUtils, parentConnection._instrumentation, connectHeaders);
connection.markAsRemote(); connection.markAsRemote();
connection.on('close', () => transport.close()); connection.on('close', () => transport.close());
@ -39,7 +39,7 @@ export async function connectOverWebSocket(parentConnection: Connection, params:
connection!.dispatch(message); connection!.dispatch(message);
} catch (e) { } catch (e) {
closeError = String(e); closeError = String(e);
transport.close(); transport.close().catch(() => {});
} }
}); });
return connection; return connection;
@ -70,7 +70,7 @@ class JsonPipeTransport implements Transport {
} }
async send(message: object) { async send(message: object) {
this._owner._wrapApiCall(async () => { await this._owner._wrapApiCall(async () => {
await this._pipe!.send({ message }); await this._pipe!.send({ message });
}, /* isInternal */ true); }, /* isInternal */ true);
} }

View file

@ -16,19 +16,18 @@
import { AndroidServerLauncherImpl } from './androidServerImpl'; import { AndroidServerLauncherImpl } from './androidServerImpl';
import { BrowserServerLauncherImpl } from './browserServerImpl'; import { BrowserServerLauncherImpl } from './browserServerImpl';
import { createConnectionFactory } from './client/clientBundle';
import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlaywright } from './server'; import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlaywright } from './server';
import { nodePlatform } from './server/utils/nodePlatform'; import { nodePlatform } from './server/utils/nodePlatform';
import { Connection } from './client/connection';
import { setPlatformForSelectors } from './client/selectors';
import type { Playwright as PlaywrightAPI } from './client/playwright'; import type { Playwright as PlaywrightAPI } from './client/playwright';
import type { Language } from './utils'; import type { Language } from './utils';
const connectionFactory = createConnectionFactory(nodePlatform);
export function createInProcessPlaywright(): PlaywrightAPI { export function createInProcessPlaywright(): PlaywrightAPI {
const playwright = createPlaywright({ sdkLanguage: (process.env.PW_LANG_NAME as Language | undefined) || 'javascript' }); const playwright = createPlaywright({ sdkLanguage: (process.env.PW_LANG_NAME as Language | undefined) || 'javascript' });
setPlatformForSelectors(nodePlatform);
const clientConnection = connectionFactory(); const clientConnection = new Connection(nodePlatform);
clientConnection.useRawBuffers(); clientConnection.useRawBuffers();
const dispatcherConnection = new DispatcherConnection(true /* local */); const dispatcherConnection = new DispatcherConnection(true /* local */);

View file

@ -15,18 +15,19 @@
*/ */
import * as childProcess from 'child_process'; import * as childProcess from 'child_process';
import * as path from 'path'; import path from 'path';
import { createConnectionFactory } from './client/clientBundle'; import { Connection } from './client/connection';
import { PipeTransport } from './server/utils/pipeTransport'; import { PipeTransport } from './server/utils/pipeTransport';
import { ManualPromise } from './utils/isomorphic/manualPromise'; import { ManualPromise } from './utils/isomorphic/manualPromise';
import { nodePlatform } from './server/utils/nodePlatform'; import { nodePlatform } from './server/utils/nodePlatform';
import { setPlatformForSelectors } from './client/selectors';
import type { Playwright } from './client/playwright'; import type { Playwright } from './client/playwright';
const connectionFactory = createConnectionFactory(nodePlatform);
export async function start(env: any = {}): Promise<{ playwright: Playwright, stop: () => Promise<void> }> { export async function start(env: any = {}): Promise<{ playwright: Playwright, stop: () => Promise<void> }> {
setPlatformForSelectors(nodePlatform);
const client = new PlaywrightClient(env); const client = new PlaywrightClient(env);
const playwright = await client._playwright; const playwright = await client._playwright;
(playwright as any).driverProcess = client._driverProcess; (playwright as any).driverProcess = client._driverProcess;
@ -50,7 +51,7 @@ class PlaywrightClient {
this._driverProcess.unref(); this._driverProcess.unref();
this._driverProcess.stderr!.on('data', data => process.stderr.write(data)); this._driverProcess.stderr!.on('data', data => process.stderr.write(data));
const connection = connectionFactory(); const connection = new Connection(nodePlatform);
const transport = new PipeTransport(this._driverProcess.stdin!, this._driverProcess.stdout!); const transport = new PipeTransport(this._driverProcess.stdin!, this._driverProcess.stdout!);
connection.onmessage = message => transport.send(JSON.stringify(message)); connection.onmessage = message => transport.send(JSON.stringify(message));
transport.onmessage = message => connection.dispatch(JSON.parse(message)); transport.onmessage = message => connection.dispatch(JSON.parse(message));

View file

@ -14,17 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
let isUnderTest = () => false;
export function setIsUnderTestForValidator(getter: () => boolean) {
isUnderTest = getter;
}
export class ValidationError extends Error {} export class ValidationError extends Error {}
export type Validator = (arg: any, path: string, context: ValidatorContext) => any; export type Validator = (arg: any, path: string, context: ValidatorContext) => any;
export type ValidatorContext = { export type ValidatorContext = {
tChannelImpl: (names: '*' | string[], arg: any, path: string, context: ValidatorContext) => any, tChannelImpl: (names: '*' | string[], arg: any, path: string, context: ValidatorContext) => any;
binary: 'toBase64' | 'fromBase64' | 'buffer', binary: 'toBase64' | 'fromBase64' | 'buffer';
isUnderTest: () => boolean;
}; };
export const scheme: { [key: string]: Validator } = {}; export const scheme: { [key: string]: Validator } = {};
@ -117,7 +112,7 @@ export const tObject = (s: { [key: string]: Validator }): Validator => {
if (!Object.is(value, undefined)) if (!Object.is(value, undefined))
result[key] = value; result[key] = value;
} }
if (isUnderTest()) { if (context.isUnderTest()) {
for (const [key, value] of Object.entries(arg)) { for (const [key, value] of Object.entries(arg)) {
if (key.startsWith('__testHook')) if (key.startsWith('__testHook'))
result[key] = value; result[key] = value;

View file

@ -15,9 +15,9 @@
*/ */
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import * as fs from 'fs'; import fs from 'fs';
import * as os from 'os'; import os from 'os';
import * as path from 'path'; import path from 'path';
import { TimeoutSettings } from '../timeoutSettings'; import { TimeoutSettings } from '../timeoutSettings';
import { PipeTransport } from '../utils/pipeTransport'; import { PipeTransport } from '../utils/pipeTransport';

View file

@ -15,7 +15,7 @@
*/ */
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import * as net from 'net'; import net from 'net';
import { assert } from '../../utils/isomorphic/assert'; import { assert } from '../../utils/isomorphic/assert';
import { createGuid } from '../utils/crypto'; import { createGuid } from '../utils/crypto';

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import { assert } from '../utils'; import { assert } from '../utils';
import { TargetClosedError } from './errors'; import { TargetClosedError } from './errors';

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as os from 'os'; import os from 'os';
import { assert } from '../../utils'; import { assert } from '../../utils';
import { wrapInASCIIBox } from '../utils/ascii'; import { wrapInASCIIBox } from '../utils/ascii';

View file

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert } from '../../utils';
import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers'; import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers';
import * as js from '../javascript'; import * as js from '../javascript';
import * as dom from '../dom'; import * as dom from '../dom';
@ -60,7 +61,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
} }
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> { async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
const response = await this._session.send('script.evaluate', { const response = await this._session.send('script.evaluate', {
expression, expression,
target: this._target, target: this._target,
@ -71,7 +72,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
}); });
if (response.type === 'success') { if (response.type === 'success') {
if ('handle' in response.result) if ('handle' in response.result)
return response.result.handle!; return createHandle(context, response.result);
throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result)); throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result));
} }
if (response.type === 'exception') if (response.type === 'exception')
@ -79,14 +80,14 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
} }
async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle, values: any[], handles: js.JSHandle[]): Promise<any> {
const response = await this._session.send('script.callFunction', { const response = await this._session.send('script.callFunction', {
functionDeclaration, functionDeclaration,
target: this._target, target: this._target,
arguments: [ arguments: [
{ handle: utilityScript._objectId! }, { handle: utilityScript._objectId! },
...values.map(BidiSerializer.serialize), ...values.map(BidiSerializer.serialize),
...objectIds.map(handle => ({ handle })), ...handles.map(handle => ({ handle: handle._objectId! })),
], ],
resultOwnership: returnByValue ? undefined : bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned. resultOwnership: returnByValue ? undefined : bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
serializationOptions: returnByValue ? {} : { maxObjectDepth: 0, maxDomDepth: 0 }, serializationOptions: returnByValue ? {} : { maxObjectDepth: 0, maxDomDepth: 0 },
@ -98,43 +99,34 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
if (response.type === 'success') { if (response.type === 'success') {
if (returnByValue) if (returnByValue)
return parseEvaluationResultValue(BidiDeserializer.deserialize(response.result)); return parseEvaluationResultValue(BidiDeserializer.deserialize(response.result));
const objectId = 'handle' in response.result ? response.result.handle : undefined ; return createHandle(utilityScript._context, response.result);
return utilityScript._context.createHandle({ objectId, ...response.result });
} }
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
} }
async getProperties(context: js.ExecutionContext, objectId: js.ObjectId): Promise<Map<string, js.JSHandle>> { async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
const handle = this.createHandle(context, { objectId }); const names = await handle.evaluate(object => {
try { const names = [];
const names = await handle.evaluate(object => { const descriptors = Object.getOwnPropertyDescriptors(object);
const names = []; for (const name in descriptors) {
const descriptors = Object.getOwnPropertyDescriptors(object); if (descriptors[name]?.enumerable)
for (const name in descriptors) { names.push(name);
if (descriptors[name]?.enumerable) }
names.push(name); return names;
} });
return names; const values = await Promise.all(names.map(name => handle.evaluateHandle((object, name) => object[name], name)));
}); const map = new Map<string, js.JSHandle>();
const values = await Promise.all(names.map(name => handle.evaluateHandle((object, name) => object[name], name))); for (let i = 0; i < names.length; i++)
const map = new Map<string, js.JSHandle>(); map.set(names[i], values[i]);
for (let i = 0; i < names.length; i++) return map;
map.set(names[i], values[i]);
return map;
} finally {
handle.dispose();
}
} }
createHandle(context: js.ExecutionContext, jsRemoteObject: js.RemoteObject): js.JSHandle { async releaseHandle(handle: js.JSHandle): Promise<void> {
const remoteObject: bidi.Script.RemoteValue = jsRemoteObject as bidi.Script.RemoteValue; if (!handle._objectId)
return new js.JSHandle(context, remoteObject.type, renderPreview(remoteObject), jsRemoteObject.objectId, remoteObjectValue(remoteObject)); return;
}
async releaseHandle(objectId: js.ObjectId): Promise<void> {
await this._session.send('script.disown', { await this._session.send('script.disown', {
target: this._target, target: this._target,
handles: [objectId], handles: [handle._objectId],
}); });
} }
@ -149,11 +141,11 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
}; };
} }
async remoteObjectForNodeId(nodeId: bidi.Script.SharedReference): Promise<js.RemoteObject> { async remoteObjectForNodeId(context: dom.FrameExecutionContext, nodeId: bidi.Script.SharedReference): Promise<js.JSHandle> {
const result = await this._remoteValueForReference(nodeId); const result = await this._remoteValueForReference(nodeId, true);
if ('handle' in result) if (!('handle' in result))
return { objectId: result.handle!, ...result }; throw new Error('Can\'t get remote object for nodeId');
throw new Error('Can\'t get remote object for nodeId'); return createHandle(context, result);
} }
async contentFrameIdForFrame(handle: dom.ElementHandle) { async contentFrameIdForFrame(handle: dom.ElementHandle) {
@ -172,16 +164,17 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
return null; return null;
} }
private async _remoteValueForReference(reference: bidi.Script.RemoteReference) { private async _remoteValueForReference(reference: bidi.Script.RemoteReference, createHandle?: boolean) {
return await this._rawCallFunction('e => e', reference); return await this._rawCallFunction('e => e', reference, createHandle);
} }
private async _rawCallFunction(functionDeclaration: string, arg: bidi.Script.LocalValue): Promise<bidi.Script.RemoteValue> { private async _rawCallFunction(functionDeclaration: string, arg: bidi.Script.LocalValue, createHandle?: boolean): Promise<bidi.Script.RemoteValue> {
const response = await this._session.send('script.callFunction', { const response = await this._session.send('script.callFunction', {
functionDeclaration, functionDeclaration,
target: this._target, target: this._target,
arguments: [arg], arguments: [arg],
resultOwnership: bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned. // "Root" is necessary for the handle to be returned.
resultOwnership: createHandle ? bidi.Script.ResultOwnership.Root : bidi.Script.ResultOwnership.None,
serializationOptions: { maxObjectDepth: 0, maxDomDepth: 0 }, serializationOptions: { maxObjectDepth: 0, maxDomDepth: 0 },
awaitPromise: true, awaitPromise: true,
userActivation: true, userActivation: true,
@ -215,3 +208,12 @@ function remoteObjectValue(remoteObject: bidi.Script.RemoteValue): any {
return remoteObject.value; return remoteObject.value;
return undefined; return undefined;
} }
export function createHandle(context: js.ExecutionContext, remoteObject: bidi.Script.RemoteValue): js.JSHandle {
if (remoteObject.type === 'node') {
assert(context instanceof dom.FrameExecutionContext);
return new dom.ElementHandle(context, remoteObject.handle!);
}
const objectId = 'handle' in remoteObject ? remoteObject.handle : undefined;
return new js.JSHandle(context, remoteObject.type, renderPreview(remoteObject), objectId, remoteObjectValue(remoteObject));
}

View file

@ -14,8 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as os from 'os'; import os from 'os';
import * as path from 'path'; import path from 'path';
import { assert } from '../../utils'; import { assert } from '../../utils';
import { wrapInASCIIBox } from '../utils/ascii'; import { wrapInASCIIBox } from '../utils/ascii';

View file

@ -20,7 +20,7 @@ import { BrowserContext } from '../browserContext';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import * as dom from '../dom'; import * as dom from '../dom';
import { Page } from '../page'; import { Page } from '../page';
import { BidiExecutionContext } from './bidiExecutionContext'; import { BidiExecutionContext, createHandle } from './bidiExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput'; import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput';
import { BidiNetworkManager } from './bidiNetworkManager'; import { BidiNetworkManager } from './bidiNetworkManager';
import { BidiPDF } from './bidiPdf'; import { BidiPDF } from './bidiPdf';
@ -130,7 +130,6 @@ export class BidiPage implements PageDelegate {
const frame = this._page._frameManager.frame(realmInfo.context); const frame = this._page._frameManager.frame(realmInfo.context);
if (!frame) if (!frame)
return; return;
const delegate = new BidiExecutionContext(this._session, realmInfo);
let worldName: types.World; let worldName: types.World;
if (!realmInfo.sandbox) { if (!realmInfo.sandbox) {
worldName = 'main'; worldName = 'main';
@ -141,8 +140,8 @@ export class BidiPage implements PageDelegate {
} else { } else {
return; return;
} }
const delegate = new BidiExecutionContext(this._session, realmInfo);
const context = new dom.FrameExecutionContext(delegate, frame, worldName); const context = new dom.FrameExecutionContext(delegate, frame, worldName);
(context as any)[contextDelegateSymbol] = delegate;
frame._contextCreated(worldName, context); frame._contextCreated(worldName, context);
this._realmToContext.set(realmInfo.realm, context); this._realmToContext.set(realmInfo.realm, context);
} }
@ -242,7 +241,7 @@ export class BidiPage implements PageDelegate {
return; return;
const callFrame = params.stackTrace?.callFrames[0]; const callFrame = params.stackTrace?.callFrames[0];
const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 }; const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 };
this._page._addConsoleMessage(entry.method, entry.args.map(arg => context.createHandle({ objectId: (arg as any).handle, ...arg })), location, params.text || undefined); this._page._addConsoleMessage(entry.method, entry.args.map(arg => createHandle(context, arg)), location, params.text || undefined);
} }
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> { async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
@ -431,10 +430,6 @@ export class BidiPage implements PageDelegate {
return executionContext.frameIdForWindowHandle(windowHandle); return executionContext.frameIdForWindowHandle(windowHandle);
} }
isElementHandle(remoteObject: bidi.Script.RemoteValue): boolean {
return remoteObject.type === 'node';
}
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> { async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const box = await handle.evaluate(element => { const box = await handle.evaluate(element => {
if (!(element instanceof Element)) if (!(element instanceof Element))
@ -532,10 +527,7 @@ export class BidiPage implements PageDelegate {
const fromContext = toBidiExecutionContext(handle._context); const fromContext = toBidiExecutionContext(handle._context);
const nodeId = await fromContext.nodeIdForElementHandle(handle); const nodeId = await fromContext.nodeIdForElementHandle(handle);
const executionContext = toBidiExecutionContext(to); const executionContext = toBidiExecutionContext(to);
const objectId = await executionContext.remoteObjectForNodeId(nodeId); return await executionContext.remoteObjectForNodeId(to, nodeId) as dom.ElementHandle<T>;
if (objectId)
return to.createHandle(objectId) as dom.ElementHandle<T>;
throw new Error('Failed to adopt element handle.');
} }
async getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> { async getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
@ -586,7 +578,5 @@ function addMainBinding(callback: (arg: any) => void) {
} }
function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext { function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext {
return (executionContext as any)[contextDelegateSymbol] as BidiExecutionContext; return executionContext.delegate as BidiExecutionContext;
} }
const contextDelegateSymbol = Symbol('delegate');

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
/* eslint-disable curly, indent */ /* eslint-disable curly, indent */

View file

@ -15,8 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
import { TimeoutSettings } from './timeoutSettings'; import { TimeoutSettings } from './timeoutSettings';
import { createGuid } from './utils/crypto'; import { createGuid } from './utils/crypto';

View file

@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as os from 'os'; import os from 'os';
import * as path from 'path'; import path from 'path';
import { normalizeProxySettings, validateBrowserContextOptions } from './browserContext'; import { normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
import { DEFAULT_TIMEOUT, TimeoutSettings } from './timeoutSettings'; import { DEFAULT_TIMEOUT, TimeoutSettings } from './timeoutSettings';

View file

@ -15,9 +15,9 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as os from 'os'; import os from 'os';
import * as path from 'path'; import path from 'path';
import { chromiumSwitches } from './chromiumSwitches'; import { chromiumSwitches } from './chromiumSwitches';
import { CRBrowser } from './crBrowser'; import { CRBrowser } from './crBrowser';

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as path from 'path'; import path from 'path';
import { assert } from '../../utils/isomorphic/assert'; import { assert } from '../../utils/isomorphic/assert';
import { createGuid } from '../utils/crypto'; import { createGuid } from '../utils/crypto';

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import type { CRSession } from './crConnection'; import type { CRSession } from './crConnection';

View file

@ -15,10 +15,12 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert } from '../../utils/isomorphic/assert';
import { getExceptionMessage, releaseObject } from './crProtocolHelper'; import { getExceptionMessage, releaseObject } from './crProtocolHelper';
import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace'; import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace';
import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers'; import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers';
import * as js from '../javascript'; import * as js from '../javascript';
import * as dom from '../dom';
import { isSessionClosedError } from '../protocolError'; import { isSessionClosedError } from '../protocolError';
import type { CRSession } from './crConnection'; import type { CRSession } from './crConnection';
@ -44,24 +46,24 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
return remoteObject.value; return remoteObject.value;
} }
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> { async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
expression, expression,
contextId: this._contextId, contextId: this._contextId,
}).catch(rewriteError); }).catch(rewriteError);
if (exceptionDetails) if (exceptionDetails)
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails)); throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
return remoteObject.objectId!; return createHandle(context, remoteObject);
} }
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle, values: any[], handles: js.JSHandle[]): Promise<any> {
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', { const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: expression, functionDeclaration: expression,
objectId: utilityScript._objectId, objectId: utilityScript._objectId,
arguments: [ arguments: [
{ objectId: utilityScript._objectId }, { objectId: utilityScript._objectId },
...values.map(value => ({ value })), ...values.map(value => ({ value })),
...objectIds.map(objectId => ({ objectId })), ...handles.map(handle => ({ objectId: handle._objectId! })),
], ],
returnByValue, returnByValue,
awaitPromise: true, awaitPromise: true,
@ -69,29 +71,27 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
}).catch(rewriteError); }).catch(rewriteError);
if (exceptionDetails) if (exceptionDetails)
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails)); throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
return returnByValue ? parseEvaluationResultValue(remoteObject.value) : utilityScript._context.createHandle(remoteObject); return returnByValue ? parseEvaluationResultValue(remoteObject.value) : createHandle(utilityScript._context, remoteObject);
} }
async getProperties(context: js.ExecutionContext, objectId: js.ObjectId): Promise<Map<string, js.JSHandle>> { async getProperties(object: js.JSHandle): Promise<Map<string, js.JSHandle>> {
const response = await this._client.send('Runtime.getProperties', { const response = await this._client.send('Runtime.getProperties', {
objectId, objectId: object._objectId!,
ownProperties: true ownProperties: true
}); });
const result = new Map(); const result = new Map();
for (const property of response.result) { for (const property of response.result) {
if (!property.enumerable || !property.value) if (!property.enumerable || !property.value)
continue; continue;
result.set(property.name, context.createHandle(property.value)); result.set(property.name, createHandle(object._context, property.value));
} }
return result; return result;
} }
createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle { async releaseHandle(handle: js.JSHandle): Promise<void> {
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type, renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject)); if (!handle._objectId)
} return;
await releaseObject(this._client, handle._objectId);
async releaseHandle(objectId: js.ObjectId): Promise<void> {
await releaseObject(this._client, objectId);
} }
} }
@ -132,3 +132,11 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine
return js.sparseArrayToString(object.preview.properties); return js.sparseArrayToString(object.preview.properties);
return object.description; return object.description;
} }
export function createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
if (remoteObject.subtype === 'node') {
assert(context instanceof dom.FrameExecutionContext);
return new dom.ElementHandle(context, remoteObject.objectId!);
}
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type, renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
}

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as path from 'path'; import path from 'path';
import { assert } from '../../utils/isomorphic/assert'; import { assert } from '../../utils/isomorphic/assert';
import { createGuid } from '../utils/crypto'; import { createGuid } from '../utils/crypto';
@ -33,7 +33,7 @@ import { getAccessibilityTree } from './crAccessibility';
import { CRBrowserContext } from './crBrowser'; import { CRBrowserContext } from './crBrowser';
import { CRCoverage } from './crCoverage'; import { CRCoverage } from './crCoverage';
import { DragManager } from './crDragDrop'; import { DragManager } from './crDragDrop';
import { CRExecutionContext } from './crExecutionContext'; import { createHandle, CRExecutionContext } from './crExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './crInput'; import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './crInput';
import { CRNetworkManager } from './crNetworkManager'; import { CRNetworkManager } from './crNetworkManager';
import { CRPDF } from './crPdf'; import { CRPDF } from './crPdf';
@ -281,10 +281,6 @@ export class CRPage implements PageDelegate {
return this._sessionForHandle(handle)._getOwnerFrame(handle); return this._sessionForHandle(handle)._getOwnerFrame(handle);
} }
isElementHandle(remoteObject: any): boolean {
return (remoteObject as Protocol.Runtime.RemoteObject).subtype === 'node';
}
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> { async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
return this._sessionForHandle(handle)._getBoundingBox(handle); return this._sessionForHandle(handle)._getBoundingBox(handle);
} }
@ -679,7 +675,6 @@ class FrameSession {
else if (contextPayload.name === UTILITY_WORLD_NAME) else if (contextPayload.name === UTILITY_WORLD_NAME)
worldName = 'utility'; worldName = 'utility';
const context = new dom.FrameExecutionContext(delegate, frame, worldName); const context = new dom.FrameExecutionContext(delegate, frame, worldName);
(context as any)[contextDelegateSymbol] = delegate;
if (worldName) if (worldName)
frame._contextCreated(worldName, context); frame._contextCreated(worldName, context);
this._contextIdToContext.set(contextPayload.id, context); this._contextIdToContext.set(contextPayload.id, context);
@ -739,7 +734,7 @@ class FrameSession {
session.on('Target.attachedToTarget', event => this._onAttachedToTarget(event)); session.on('Target.attachedToTarget', event => this._onAttachedToTarget(event));
session.on('Target.detachedFromTarget', event => this._onDetachedFromTarget(event)); session.on('Target.detachedFromTarget', event => this._onDetachedFromTarget(event));
session.on('Runtime.consoleAPICalled', event => { session.on('Runtime.consoleAPICalled', event => {
const args = event.args.map(o => worker._existingExecutionContext!.createHandle(o)); const args = event.args.map(o => createHandle(worker._existingExecutionContext!, o));
this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace)); this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace));
}); });
session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page)); session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page));
@ -802,7 +797,7 @@ class FrameSession {
const context = this._contextIdToContext.get(event.executionContextId); const context = this._contextIdToContext.get(event.executionContextId);
if (!context) if (!context)
return; return;
const values = event.args.map(arg => context.createHandle(arg)); const values = event.args.map(arg => createHandle(context, arg));
this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace)); this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace));
} }
@ -1167,11 +1162,11 @@ class FrameSession {
async _adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise<dom.ElementHandle> { async _adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise<dom.ElementHandle> {
const result = await this._client._sendMayFail('DOM.resolveNode', { const result = await this._client._sendMayFail('DOM.resolveNode', {
backendNodeId, backendNodeId,
executionContextId: ((to as any)[contextDelegateSymbol] as CRExecutionContext)._contextId, executionContextId: (to.delegate as CRExecutionContext)._contextId,
}); });
if (!result || result.object.subtype === 'null') if (!result || result.object.subtype === 'null')
throw new Error(dom.kUnableToAdoptErrorMessage); throw new Error(dom.kUnableToAdoptErrorMessage);
return to.createHandle(result.object).asElement()!; return createHandle(to, result.object).asElement()!;
} }
} }
@ -1200,8 +1195,6 @@ async function emulateTimezone(session: CRSession, timezoneId: string) {
} }
} }
const contextDelegateSymbol = Symbol('delegate');
// Chromium reference: https://source.chromium.org/chromium/chromium/src/+/main:components/embedder_support/user_agent_utils.cc;l=434;drc=70a6711e08e9f9e0d8e4c48e9ba5cab62eb010c2 // Chromium reference: https://source.chromium.org/chromium/chromium/src/+/main:components/embedder_support/user_agent_utils.cc;l=434;drc=70a6711e08e9f9e0d8e4c48e9ba5cab62eb010c2
function calculateUserAgentMetadata(options: types.BrowserContextOptions) { function calculateUserAgentMetadata(options: types.BrowserContextOptions) {
const ua = options.userAgent; const ua = options.userAgent;

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import { splitErrorMessage } from '../../utils/isomorphic/stackTrace'; import { splitErrorMessage } from '../../utils/isomorphic/stackTrace';
import { mkdirIfNeeded } from '../utils/fileUtils'; import { mkdirIfNeeded } from '../utils/fileUtils';

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Galaxy S5": { "Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S5 landscape": { "Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S8": { "Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 740 "height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S8 landscape": { "Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 740, "width": 740,
"height": 360 "height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S9+": { "Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 320, "width": 320,
"height": 658 "height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S9+ landscape": { "Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 658, "width": 658,
"height": 320 "height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy Tab S4": { "Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
"viewport": { "viewport": {
"width": 712, "width": 712,
"height": 1138 "height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy Tab S4 landscape": { "Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
"viewport": { "viewport": {
"width": 1138, "width": 1138,
"height": 712 "height": 712
@ -1098,7 +1098,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"LG Optimus L70": { "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/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 384, "width": 384,
"height": 640 "height": 640
@ -1109,7 +1109,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"LG Optimus L70 landscape": { "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/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 384 "height": 384
@ -1120,7 +1120,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 550": { "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/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1131,7 +1131,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 550 landscape": { "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/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1142,7 +1142,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 950": { "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/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1153,7 +1153,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 950 landscape": { "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/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1164,7 +1164,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 10": { "Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
"viewport": { "viewport": {
"width": 800, "width": 800,
"height": 1280 "height": 1280
@ -1175,7 +1175,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 10 landscape": { "Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
"viewport": { "viewport": {
"width": 1280, "width": 1280,
"height": 800 "height": 800
@ -1186,7 +1186,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 4": { "Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 384, "width": 384,
"height": 640 "height": 640
@ -1197,7 +1197,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 4 landscape": { "Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 384 "height": 384
@ -1208,7 +1208,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5": { "Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1219,7 +1219,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5 landscape": { "Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1230,7 +1230,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5X": { "Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1241,7 +1241,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5X landscape": { "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/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1252,7 +1252,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6": { "Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1263,7 +1263,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6 landscape": { "Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1274,7 +1274,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6P": { "Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1285,7 +1285,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6P landscape": { "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/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1296,7 +1296,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 7": { "Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
"viewport": { "viewport": {
"width": 600, "width": 600,
"height": 960 "height": 960
@ -1307,7 +1307,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 7 landscape": { "Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
"viewport": { "viewport": {
"width": 960, "width": 960,
"height": 600 "height": 600
@ -1362,7 +1362,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Pixel 2": { "Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 411, "width": 411,
"height": 731 "height": 731
@ -1373,7 +1373,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 landscape": { "Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 731, "width": 731,
"height": 411 "height": 411
@ -1384,7 +1384,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 XL": { "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/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 411, "width": 411,
"height": 823 "height": 823
@ -1395,7 +1395,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 XL landscape": { "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/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 823, "width": 823,
"height": 411 "height": 411
@ -1406,7 +1406,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 3": { "Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 393, "width": 393,
"height": 786 "height": 786
@ -1417,7 +1417,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 3 landscape": { "Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 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/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 786, "width": 786,
"height": 393 "height": 393
@ -1428,7 +1428,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4": { "Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 353, "width": 353,
"height": 745 "height": 745
@ -1439,7 +1439,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4 landscape": { "Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 745, "width": 745,
"height": 353 "height": 353
@ -1450,7 +1450,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4a (5G)": { "Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"screen": { "screen": {
"width": 412, "width": 412,
"height": 892 "height": 892
@ -1465,7 +1465,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4a (5G) landscape": { "Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"screen": { "screen": {
"height": 892, "height": 892,
"width": 412 "width": 412
@ -1480,7 +1480,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 5": { "Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"screen": { "screen": {
"width": 393, "width": 393,
"height": 851 "height": 851
@ -1495,7 +1495,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 5 landscape": { "Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"screen": { "screen": {
"width": 851, "width": 851,
"height": 393 "height": 393
@ -1510,7 +1510,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 7": { "Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"screen": { "screen": {
"width": 412, "width": 412,
"height": 915 "height": 915
@ -1525,7 +1525,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 7 landscape": { "Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"screen": { "screen": {
"width": 915, "width": 915,
"height": 412 "height": 412
@ -1540,7 +1540,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Moto G4": { "Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1551,7 +1551,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Moto G4 landscape": { "Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1562,7 +1562,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Chrome HiDPI": { "Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1577,7 +1577,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Edge HiDPI": { "Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36 Edg/134.0.6998.15", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36 Edg/134.0.6998.23",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1622,7 +1622,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Desktop Chrome": { "Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080
@ -1637,7 +1637,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Edge": { "Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36 Edg/134.0.6998.15", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.23 Safari/537.36 Edg/134.0.6998.23",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import { Dispatcher, existingDispatcher } from './dispatcher'; import { Dispatcher, existingDispatcher } from './dispatcher';
import { StreamDispatcher } from './streamDispatcher'; import { StreamDispatcher } from './streamDispatcher';

View file

@ -14,8 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { ArtifactDispatcher } from './artifactDispatcher'; import { ArtifactDispatcher } from './artifactDispatcher';

View file

@ -200,13 +200,13 @@ export class DispatcherConnection {
sendEvent(dispatcher: DispatcherScope, event: string, params: any) { sendEvent(dispatcher: DispatcherScope, event: string, params: any) {
const validator = findValidator(dispatcher._type, event, 'Event'); const validator = findValidator(dispatcher._type, event, 'Event');
params = validator(params, '', { tChannelImpl: this._tChannelImplToWire.bind(this), binary: this._isLocal ? 'buffer' : 'toBase64' }); params = validator(params, '', this._validatorToWireContext());
this.onmessage({ guid: dispatcher._guid, method: event, params }); this.onmessage({ guid: dispatcher._guid, method: event, params });
} }
sendCreate(parent: DispatcherScope, type: string, guid: string, initializer: any) { sendCreate(parent: DispatcherScope, type: string, guid: string, initializer: any) {
const validator = findValidator(type, '', 'Initializer'); const validator = findValidator(type, '', 'Initializer');
initializer = validator(initializer, '', { tChannelImpl: this._tChannelImplToWire.bind(this), binary: this._isLocal ? 'buffer' : 'toBase64' }); initializer = validator(initializer, '', this._validatorToWireContext());
this.onmessage({ guid: parent._guid, method: '__create__', params: { type, initializer, guid } }); this.onmessage({ guid: parent._guid, method: '__create__', params: { type, initializer, guid } });
} }
@ -218,6 +218,22 @@ export class DispatcherConnection {
this.onmessage({ guid: dispatcher._guid, method: '__dispose__', params: { reason } }); this.onmessage({ guid: dispatcher._guid, method: '__dispose__', params: { reason } });
} }
private _validatorToWireContext(): ValidatorContext {
return {
tChannelImpl: this._tChannelImplToWire.bind(this),
binary: this._isLocal ? 'buffer' : 'toBase64',
isUnderTest,
};
}
private _validatorFromWireContext(): ValidatorContext {
return {
tChannelImpl: this._tChannelImplFromWire.bind(this),
binary: this._isLocal ? 'buffer' : 'fromBase64',
isUnderTest,
};
}
private _tChannelImplFromWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext): any { private _tChannelImplFromWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext): any {
if (arg && typeof arg === 'object' && typeof arg.guid === 'string') { if (arg && typeof arg === 'object' && typeof arg.guid === 'string') {
const guid = arg.guid; const guid = arg.guid;
@ -279,8 +295,9 @@ export class DispatcherConnection {
let validMetadata: channels.Metadata; let validMetadata: channels.Metadata;
try { try {
const validator = findValidator(dispatcher._type, method, 'Params'); const validator = findValidator(dispatcher._type, method, 'Params');
validParams = validator(params, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._isLocal ? 'buffer' : 'fromBase64' }); const validatorContext = this._validatorFromWireContext();
validMetadata = metadataValidator(metadata, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._isLocal ? 'buffer' : 'fromBase64' }); validParams = validator(params, '', validatorContext);
validMetadata = metadataValidator(metadata, '', validatorContext);
if (typeof (dispatcher as any)[method] !== 'function') if (typeof (dispatcher as any)[method] !== 'function')
throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`); throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`);
} catch (e) { } catch (e) {
@ -338,7 +355,7 @@ export class DispatcherConnection {
try { try {
const result = await dispatcher._handleCommand(callMetadata, method, validParams); const result = await dispatcher._handleCommand(callMetadata, method, validParams);
const validator = findValidator(dispatcher._type, method, 'Result'); const validator = findValidator(dispatcher._type, method, 'Result');
response.result = validator(result, '', { tChannelImpl: this._tChannelImplToWire.bind(this), binary: this._isLocal ? 'buffer' : 'toBase64' }); response.result = validator(result, '', this._validatorToWireContext());
callMetadata.result = result; callMetadata.result = result;
} catch (e) { } catch (e) {
if (isTargetClosedError(e) && sdkObject) { if (isTargetClosedError(e) && sdkObject) {

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import { Dispatcher } from './dispatcher'; import { Dispatcher } from './dispatcher';
import { createGuid } from '../utils/crypto'; import { createGuid } from '../utils/crypto';

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as js from './javascript'; import * as js from './javascript';
import { ProgressController } from './progress'; import { ProgressController } from './progress';
@ -83,12 +83,6 @@ export class FrameExecutionContext extends js.ExecutionContext {
return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg); return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg);
} }
override createHandle(remoteObject: js.RemoteObject): js.JSHandle {
if (this.frame._page._delegate.isElementHandle(remoteObject))
return new ElementHandle(this, remoteObject.objectId!);
return super.createHandle(remoteObject);
}
injectedScript(): Promise<js.JSHandle<InjectedScript>> { injectedScript(): Promise<js.JSHandle<InjectedScript>> {
if (!this._injectedScriptPromise) { if (!this._injectedScriptPromise) {
const custom: string[] = []; const custom: string[] = [];
@ -111,7 +105,11 @@ export class FrameExecutionContext extends js.ExecutionContext {
); );
})(); })();
`; `;
this._injectedScriptPromise = this.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', 'InjectedScript', objectId)); this._injectedScriptPromise = this.rawEvaluateHandle(source)
.then(handle => {
handle._setPreview('InjectedScript');
return handle;
});
} }
return this._injectedScriptPromise; return this._injectedScriptPromise;
} }

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as path from 'path'; import path from 'path';
import { Page } from './page'; import { Page } from './page';
import { assert } from '../utils'; import { assert } from '../utils';

View file

@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as os from 'os'; import os from 'os';
import * as path from 'path'; import path from 'path';
import * as readline from 'readline'; import * as readline from 'readline';
import { TimeoutSettings } from '../timeoutSettings'; import { TimeoutSettings } from '../timeoutSettings';
@ -27,7 +27,7 @@ import { eventsHelper } from '../utils/eventsHelper';
import { validateBrowserContextOptions } from '../browserContext'; import { validateBrowserContextOptions } from '../browserContext';
import { CRBrowser } from '../chromium/crBrowser'; import { CRBrowser } from '../chromium/crBrowser';
import { CRConnection } from '../chromium/crConnection'; import { CRConnection } from '../chromium/crConnection';
import { CRExecutionContext } from '../chromium/crExecutionContext'; import { createHandle, CRExecutionContext } from '../chromium/crExecutionContext';
import { toConsoleMessageLocation } from '../chromium/crProtocolHelper'; import { toConsoleMessageLocation } from '../chromium/crProtocolHelper';
import { ConsoleMessage } from '../console'; import { ConsoleMessage } from '../console';
import { helper } from '../helper'; import { helper } from '../helper';
@ -116,7 +116,7 @@ export class ElectronApplication extends SdkObject {
} }
if (!this._nodeExecutionContext) if (!this._nodeExecutionContext)
return; return;
const args = event.args.map(arg => this._nodeExecutionContext!.createHandle(arg)); const args = event.args.map(arg => createHandle(this._nodeExecutionContext!, arg));
const message = new ConsoleMessage(null, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace)); const message = new ConsoleMessage(null, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace));
this.emit(ElectronApplication.Events.Console, message); this.emit(ElectronApplication.Events.Console, message);
} }

View file

@ -14,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import * as http from 'http'; import http from 'http';
import * as https from 'https'; import https from 'https';
import { Transform, pipeline } from 'stream'; import { Transform, pipeline } from 'stream';
import { TLSSocket } from 'tls'; import { TLSSocket } from 'tls';
import * as url from 'url'; import url from 'url';
import * as zlib from 'zlib'; import * as zlib from 'zlib';
import { TimeoutSettings } from './timeoutSettings'; import { TimeoutSettings } from './timeoutSettings';

View file

@ -14,8 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
import { assert } from '../utils/isomorphic/assert'; import { assert } from '../utils/isomorphic/assert';
import { mime } from '../utilsBundle'; import { mime } from '../utilsBundle';

View file

@ -15,9 +15,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert } from '../../utils/isomorphic/assert';
import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace'; import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace';
import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers'; import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers';
import * as js from '../javascript'; import * as js from '../javascript';
import * as dom from '../dom';
import { isSessionClosedError } from '../protocolError'; import { isSessionClosedError } from '../protocolError';
import type { FFSession } from './ffConnection'; import type { FFSession } from './ffConnection';
@ -42,23 +44,23 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
return payload.result!.value; return payload.result!.value;
} }
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> { async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
const payload = await this._session.send('Runtime.evaluate', { const payload = await this._session.send('Runtime.evaluate', {
expression, expression,
returnByValue: false, returnByValue: false,
executionContextId: this._executionContextId, executionContextId: this._executionContextId,
}).catch(rewriteError); }).catch(rewriteError);
checkException(payload.exceptionDetails); checkException(payload.exceptionDetails);
return payload.result!.objectId!; return createHandle(context, payload.result!);
} }
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle, values: any[], handles: js.JSHandle[]): Promise<any> {
const payload = await this._session.send('Runtime.callFunction', { const payload = await this._session.send('Runtime.callFunction', {
functionDeclaration: expression, functionDeclaration: expression,
args: [ args: [
{ objectId: utilityScript._objectId, value: undefined }, { objectId: utilityScript._objectId, value: undefined },
...values.map(value => ({ value })), ...values.map(value => ({ value })),
...objectIds.map(objectId => ({ objectId, value: undefined })), ...handles.map(handle => ({ objectId: handle._objectId!, value: undefined })),
], ],
returnByValue, returnByValue,
executionContextId: this._executionContextId executionContextId: this._executionContextId
@ -66,28 +68,26 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
checkException(payload.exceptionDetails); checkException(payload.exceptionDetails);
if (returnByValue) if (returnByValue)
return parseEvaluationResultValue(payload.result!.value); return parseEvaluationResultValue(payload.result!.value);
return utilityScript._context.createHandle(payload.result!); return createHandle(utilityScript._context, payload.result!);
} }
async getProperties(context: js.ExecutionContext, objectId: js.ObjectId): Promise<Map<string, js.JSHandle>> { async getProperties(object: js.JSHandle): Promise<Map<string, js.JSHandle>> {
const response = await this._session.send('Runtime.getObjectProperties', { const response = await this._session.send('Runtime.getObjectProperties', {
executionContextId: this._executionContextId, executionContextId: this._executionContextId,
objectId, objectId: object._objectId!,
}); });
const result = new Map(); const result = new Map();
for (const property of response.properties) for (const property of response.properties)
result.set(property.name, context.createHandle(property.value)); result.set(property.name, createHandle(object._context, property.value));
return result; return result;
} }
createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle { async releaseHandle(handle: js.JSHandle): Promise<void> {
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type || '', renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject)); if (!handle._objectId)
} return;
async releaseHandle(objectId: js.ObjectId): Promise<void> {
await this._session.send('Runtime.disposeObject', { await this._session.send('Runtime.disposeObject', {
executionContextId: this._executionContextId, executionContextId: this._executionContextId,
objectId objectId: handle._objectId,
}); });
} }
} }
@ -135,3 +135,11 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine
if ('value' in object) if ('value' in object)
return String(object.value); return String(object.value);
} }
export function createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
if (remoteObject.subtype === 'node') {
assert(context instanceof dom.FrameExecutionContext);
return new dom.ElementHandle(context, remoteObject.objectId!);
}
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type || '', renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
}

View file

@ -22,7 +22,7 @@ import { InitScript } from '../page';
import { Page, Worker } from '../page'; import { Page, Worker } from '../page';
import { getAccessibilityTree } from './ffAccessibility'; import { getAccessibilityTree } from './ffAccessibility';
import { FFSession } from './ffConnection'; import { FFSession } from './ffConnection';
import { FFExecutionContext } from './ffExecutionContext'; import { createHandle, FFExecutionContext } from './ffExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './ffInput'; import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './ffInput';
import { FFNetworkManager } from './ffNetworkManager'; import { FFNetworkManager } from './ffNetworkManager';
import { debugLogger } from '../utils/debugLogger'; import { debugLogger } from '../utils/debugLogger';
@ -150,7 +150,6 @@ export class FFPage implements PageDelegate {
else if (!auxData.name) else if (!auxData.name)
worldName = 'main'; worldName = 'main';
const context = new dom.FrameExecutionContext(delegate, frame, worldName); const context = new dom.FrameExecutionContext(delegate, frame, worldName);
(context as any)[contextDelegateSymbol] = delegate;
if (worldName) if (worldName)
frame._contextCreated(worldName, context); frame._contextCreated(worldName, context);
this._contextIdToContext.set(executionContextId, context); this._contextIdToContext.set(executionContextId, context);
@ -234,7 +233,7 @@ export class FFPage implements PageDelegate {
if (!context) if (!context)
return; return;
// Juggler reports 'warn' for some internal messages generated by the browser. // Juggler reports 'warn' for some internal messages generated by the browser.
this._page._addConsoleMessage(type === 'warn' ? 'warning' : type, args.map(arg => context.createHandle(arg)), location); this._page._addConsoleMessage(type === 'warn' ? 'warning' : type, args.map(arg => createHandle(context, arg)), location);
} }
_onDialogOpened(params: Protocol.Page.dialogOpenedPayload) { _onDialogOpened(params: Protocol.Page.dialogOpenedPayload) {
@ -262,7 +261,7 @@ export class FFPage implements PageDelegate {
const context = this._contextIdToContext.get(executionContextId); const context = this._contextIdToContext.get(executionContextId);
if (!context) if (!context)
return; return;
const handle = context.createHandle(element).asElement()!; const handle = createHandle(context, element).asElement()!;
await this._page._onFileChooserOpened(handle); await this._page._onFileChooserOpened(handle);
} }
@ -286,7 +285,7 @@ export class FFPage implements PageDelegate {
workerSession.on('Runtime.console', event => { workerSession.on('Runtime.console', event => {
const { type, args, location } = event; const { type, args, location } = event;
const context = worker._existingExecutionContext!; const context = worker._existingExecutionContext!;
this._page._addConsoleMessage(type, args.map(arg => context.createHandle(arg)), location); this._page._addConsoleMessage(type, args.map(arg => createHandle(context, arg)), location);
}); });
// Note: we receive worker exceptions directly from the page. // Note: we receive worker exceptions directly from the page.
} }
@ -443,10 +442,6 @@ export class FFPage implements PageDelegate {
return ownerFrameId || null; return ownerFrameId || null;
} }
isElementHandle(remoteObject: any): boolean {
return remoteObject.subtype === 'node';
}
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> { async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const quads = await this.getContentQuads(handle); const quads = await this.getContentQuads(handle);
if (!quads || !quads.length) if (!quads || !quads.length)
@ -531,11 +526,11 @@ export class FFPage implements PageDelegate {
const result = await this._session.send('Page.adoptNode', { const result = await this._session.send('Page.adoptNode', {
frameId: handle._context.frame._id, frameId: handle._context.frame._id,
objectId: handle._objectId, objectId: handle._objectId,
executionContextId: ((to as any)[contextDelegateSymbol] as FFExecutionContext)._executionContextId executionContextId: (to.delegate as FFExecutionContext)._executionContextId
}); });
if (!result.remoteObject) if (!result.remoteObject)
throw new Error(dom.kUnableToAdoptErrorMessage); throw new Error(dom.kUnableToAdoptErrorMessage);
return to.createHandle(result.remoteObject) as dom.ElementHandle<T>; return createHandle(to, result.remoteObject) as dom.ElementHandle<T>;
} }
async getAccessibilityTree(needle?: dom.ElementHandle) { async getAccessibilityTree(needle?: dom.ElementHandle) {
@ -560,11 +555,11 @@ export class FFPage implements PageDelegate {
const context = await parent._mainContext(); const context = await parent._mainContext();
const result = await this._session.send('Page.adoptNode', { const result = await this._session.send('Page.adoptNode', {
frameId: frame._id, frameId: frame._id,
executionContextId: ((context as any)[contextDelegateSymbol] as FFExecutionContext)._executionContextId executionContextId: (context.delegate as FFExecutionContext)._executionContextId
}); });
if (!result.remoteObject) if (!result.remoteObject)
throw new Error('Frame has been detached.'); throw new Error('Frame has been detached.');
return context.createHandle(result.remoteObject) as dom.ElementHandle; return createHandle(context, result.remoteObject) as dom.ElementHandle;
} }
shouldToggleStyleSheetToSyncAnimations(): boolean { shouldToggleStyleSheetToSyncAnimations(): boolean {
@ -575,5 +570,3 @@ export class FFPage implements PageDelegate {
function webSocketId(frameId: string, wsid: string): string { function webSocketId(frameId: string, wsid: string): string {
return `${frameId}---${wsid}`; return `${frameId}---${wsid}`;
} }
const contextDelegateSymbol = Symbol('delegate');

View file

@ -15,8 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as os from 'os'; import os from 'os';
import * as path from 'path'; import path from 'path';
import { FFBrowser } from './ffBrowser'; import { FFBrowser } from './ffBrowser';
import { kBrowserCloseMessageId } from './ffConnection'; import { kBrowserCloseMessageId } from './ffConnection';

View file

@ -14,8 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
import { Artifact } from '../artifact'; import { Artifact } from '../artifact';
import { HarTracer } from './harTracer'; import { HarTracer } from './harTracer';

View file

@ -14,8 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
import { createGuid } from './utils/crypto'; import { createGuid } from './utils/crypto';
import { ZipFile } from './utils/zipFile'; import { ZipFile } from './utils/zipFile';

View file

@ -337,7 +337,7 @@ function trimFlatString(s: string): string {
function asFlatString(s: string): string { function asFlatString(s: string): string {
// "Flat string" at https://w3c.github.io/accname/#terminology // "Flat string" at https://w3c.github.io/accname/#terminology
// Note that non-breaking spaces are preserved. // Note that non-breaking spaces are preserved.
return s.split('\u00A0').map(chunk => chunk.replace(/\r\n/g, '\n').replace(/\s\s*/g, ' ')).join('\u00A0').trim(); return s.split('\u00A0').map(chunk => chunk.replace(/\r\n/g, '\n').replace(/[\u200b\u00ad]/g, '').replace(/\s\s*/g, ' ')).join('\u00A0').trim();
} }
function queryInAriaOwned(element: Element, selector: string): Element[] { function queryInAriaOwned(element: Element, selector: string): Element[] {

View file

@ -23,12 +23,6 @@ import { LongStandingScope } from '../utils/isomorphic/manualPromise';
import type * as dom from './dom'; import type * as dom from './dom';
import type { UtilityScript } from './injected/utilityScript'; import type { UtilityScript } from './injected/utilityScript';
export type ObjectId = string;
export type RemoteObject = {
objectId?: ObjectId,
value?: any
};
interface TaggedAsJSHandle<T> { interface TaggedAsJSHandle<T> {
__jshandle: T; __jshandle: T;
} }
@ -53,15 +47,14 @@ export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>
export interface ExecutionContextDelegate { export interface ExecutionContextDelegate {
rawEvaluateJSON(expression: string): Promise<any>; rawEvaluateJSON(expression: string): Promise<any>;
rawEvaluateHandle(expression: string): Promise<ObjectId>; rawEvaluateHandle(context: ExecutionContext, expression: string): Promise<JSHandle>;
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>; evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle, values: any[], handles: JSHandle[]): Promise<any>;
getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>>; getProperties(object: JSHandle): Promise<Map<string, JSHandle>>;
createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle; releaseHandle(handle: JSHandle): Promise<void>;
releaseHandle(objectId: ObjectId): Promise<void>;
} }
export class ExecutionContext extends SdkObject { export class ExecutionContext extends SdkObject {
private _delegate: ExecutionContextDelegate; readonly delegate: ExecutionContextDelegate;
private _utilityScriptPromise: Promise<JSHandle> | undefined; private _utilityScriptPromise: Promise<JSHandle> | undefined;
private _contextDestroyedScope = new LongStandingScope(); private _contextDestroyedScope = new LongStandingScope();
readonly worldNameForTest: string; readonly worldNameForTest: string;
@ -69,7 +62,7 @@ export class ExecutionContext extends SdkObject {
constructor(parent: SdkObject, delegate: ExecutionContextDelegate, worldNameForTest: string) { constructor(parent: SdkObject, delegate: ExecutionContextDelegate, worldNameForTest: string) {
super(parent, 'execution-context'); super(parent, 'execution-context');
this.worldNameForTest = worldNameForTest; this.worldNameForTest = worldNameForTest;
this._delegate = delegate; this.delegate = delegate;
} }
contextDestroyed(reason: string) { contextDestroyed(reason: string) {
@ -81,34 +74,31 @@ export class ExecutionContext extends SdkObject {
} }
rawEvaluateJSON(expression: string): Promise<any> { rawEvaluateJSON(expression: string): Promise<any> {
return this._raceAgainstContextDestroyed(this._delegate.rawEvaluateJSON(expression)); return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateJSON(expression));
} }
rawEvaluateHandle(expression: string): Promise<ObjectId> { rawEvaluateHandle(expression: string): Promise<JSHandle> {
return this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(expression)); return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, expression));
} }
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any> { async evaluateWithArguments(expression: string, returnByValue: boolean, values: any[], handles: JSHandle[]): Promise<any> {
return this._raceAgainstContextDestroyed(this._delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, objectIds)); const utilityScript = await this._utilityScript();
return this._raceAgainstContextDestroyed(this.delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, handles));
} }
getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>> { getProperties(object: JSHandle): Promise<Map<string, JSHandle>> {
return this._raceAgainstContextDestroyed(this._delegate.getProperties(context, objectId)); return this._raceAgainstContextDestroyed(this.delegate.getProperties(object));
} }
createHandle(remoteObject: RemoteObject): JSHandle { releaseHandle(handle: JSHandle): Promise<void> {
return this._delegate.createHandle(this, remoteObject); return this.delegate.releaseHandle(handle);
}
releaseHandle(objectId: ObjectId): Promise<void> {
return this._delegate.releaseHandle(objectId);
} }
adoptIfNeeded(handle: JSHandle): Promise<JSHandle> | null { adoptIfNeeded(handle: JSHandle): Promise<JSHandle> | null {
return null; return null;
} }
utilityScript(): Promise<JSHandle<UtilityScript>> { private _utilityScript(): Promise<JSHandle<UtilityScript>> {
if (!this._utilityScriptPromise) { if (!this._utilityScriptPromise) {
const source = ` const source = `
(() => { (() => {
@ -116,7 +106,11 @@ export class ExecutionContext extends SdkObject {
${utilityScriptSource.source} ${utilityScriptSource.source}
return new (module.exports.UtilityScript())(${isUnderTest()}); return new (module.exports.UtilityScript())(${isUnderTest()});
})();`; })();`;
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', 'UtilityScript', objectId))); this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, source))
.then(handle => {
handle._setPreview('UtilityScript');
return handle;
});
} }
return this._utilityScriptPromise; return this._utilityScriptPromise;
} }
@ -130,13 +124,13 @@ export class JSHandle<T = any> extends SdkObject {
__jshandle: T = true as any; __jshandle: T = true as any;
readonly _context: ExecutionContext; readonly _context: ExecutionContext;
_disposed = false; _disposed = false;
readonly _objectId: ObjectId | undefined; readonly _objectId: string | undefined;
readonly _value: any; readonly _value: any;
private _objectType: string; private _objectType: string;
protected _preview: string; protected _preview: string;
private _previewCallback: ((preview: string) => void) | undefined; private _previewCallback: ((preview: string) => void) | undefined;
constructor(context: ExecutionContext, type: string, preview: string | undefined, objectId?: ObjectId, value?: any) { constructor(context: ExecutionContext, type: string, preview: string | undefined, objectId?: string, value?: any) {
super(context, 'handle'); super(context, 'handle');
this._context = context; this._context = context;
this._objectId = objectId; this._objectId = objectId;
@ -182,7 +176,7 @@ export class JSHandle<T = any> extends SdkObject {
async getProperties(): Promise<Map<string, JSHandle>> { async getProperties(): Promise<Map<string, JSHandle>> {
if (!this._objectId) if (!this._objectId)
return new Map(); return new Map();
return this._context.getProperties(this._context, this._objectId); return this._context.getProperties(this);
} }
rawValue() { rawValue() {
@ -192,9 +186,8 @@ export class JSHandle<T = any> extends SdkObject {
async jsonValue(): Promise<T> { async jsonValue(): Promise<T> {
if (!this._objectId) if (!this._objectId)
return this._value; return this._value;
const utilityScript = await this._context.utilityScript();
const script = `(utilityScript, ...args) => utilityScript.jsonValue(...args)`; const script = `(utilityScript, ...args) => utilityScript.jsonValue(...args)`;
return this._context.evaluateWithArguments(script, true, utilityScript, [true], [this._objectId]); return this._context.evaluateWithArguments(script, true, [true], [this]);
} }
asElement(): dom.ElementHandle | null { asElement(): dom.ElementHandle | null {
@ -206,7 +199,7 @@ export class JSHandle<T = any> extends SdkObject {
return; return;
this._disposed = true; this._disposed = true;
if (this._objectId) { if (this._objectId) {
this._context.releaseHandle(this._objectId).catch(e => {}); this._context.releaseHandle(this).catch(e => {});
if ((globalThis as any).leakedJSHandles) if ((globalThis as any).leakedJSHandles)
(globalThis as any).leakedJSHandles.delete(this); (globalThis as any).leakedJSHandles.delete(this);
} }
@ -240,7 +233,6 @@ export async function evaluate(context: ExecutionContext, returnByValue: boolean
} }
export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean }, ...args: any[]): Promise<any> { export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean }, ...args: any[]): Promise<any> {
const utilityScript = await context.utilityScript();
expression = normalizeEvaluationExpression(expression, options.isFunction); expression = normalizeEvaluationExpression(expression, options.isFunction);
const handles: (Promise<JSHandle>)[] = []; const handles: (Promise<JSHandle>)[] = [];
const toDispose: Promise<JSHandle>[] = []; const toDispose: Promise<JSHandle>[] = [];
@ -264,11 +256,11 @@ export async function evaluateExpression(context: ExecutionContext, expression:
return { fallThrough: handle }; return { fallThrough: handle };
})); }));
const utilityScriptObjectIds: ObjectId[] = []; const utilityScriptObjects: JSHandle[] = [];
for (const handle of await Promise.all(handles)) { for (const handle of await Promise.all(handles)) {
if (handle._context !== context) if (handle._context !== context)
throw new JavaScriptErrorInEvaluate('JSHandles can be evaluated only in the context they were created!'); throw new JavaScriptErrorInEvaluate('JSHandles can be evaluated only in the context they were created!');
utilityScriptObjectIds.push(handle._objectId!); utilityScriptObjects.push(handle);
} }
// See UtilityScript for arguments. // See UtilityScript for arguments.
@ -276,7 +268,7 @@ export async function evaluateExpression(context: ExecutionContext, expression:
const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`; const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`;
try { try {
return await context.evaluateWithArguments(script, options.returnByValue || false, utilityScript, utilityScriptValues, utilityScriptObjectIds); return await context.evaluateWithArguments(script, options.returnByValue || false, utilityScriptValues, utilityScriptObjects);
} finally { } finally {
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose())); toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
} }

Some files were not shown because too many files have changed in this diff Show more