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
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:
NODE_OPTIONS: --max-old-space-size=8192

View file

@ -35,7 +35,7 @@ jobs:
exit 1
fi
- name: Audit prod NPM dependencies
run: npm audit --omit dev
run: node utils/check_audit.js
lint-snippets:
name: "Lint snippets"
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
[![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)
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | 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: |
| 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
<select multiple>
<option value="red">Red</div>
<option value="green">Green</div>
<option value="blue">Blue</div>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
```
@ -2332,7 +2332,7 @@ This method expects [Locator] to point to an
## async method: Locator.tap
* 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**
@ -2478,6 +2478,18 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.uncheck.trial = %%-input-trial-%%
* 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
* 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
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
* 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
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
* 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"
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');
@ -143,8 +143,8 @@ Create `playwright/fixtures.ts` file that will [override `storageState` fixture]
```js title="playwright/fixtures.ts"
import { test as baseTest, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import fs from 'fs';
import path from 'path';
export * from '@playwright/test';
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"
import { test as baseTest, request } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import fs from 'fs';
import path from 'path';
export * from '@playwright/test';
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"
import { test as base, chromium, type BrowserContext } from '@playwright/test';
import * as path from 'path';
import path from 'path';
export const test = base.extend<{
context: BrowserContext;

View file

@ -389,7 +389,7 @@ Next, add init script to the page.
```js
import { test, expect } from '@playwright/test';
import * as path from 'path';
import path from 'path';
test.beforeEach(async ({ page }) => {
// 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.
```js
await page.locator('button').locator('visible=true').click();
await page.locator('button').visible().click();
```
```java
page.locator("button").locator("visible=true").click();
page.locator("button").visible().click();
```
```python async
await page.locator("button").locator("visible=true").click()
await page.locator("button").visible().click()
```
```python sync
page.locator("button").locator("visible=true").click()
page.locator("button").visible().click()
```
```csharp
await page.Locator("button").Locator("visible=true").ClickAsync();
await page.Locator("button").Visible().ClickAsync();
```
## Lists

View file

@ -708,9 +708,13 @@ Playwright uses simplified glob patterns for URL matching in network interceptio
- A double `**` matches any characters including `/`
1. Question mark `?` matches any single character except `/`
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:
- `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`
- `**/*.{png,jpg,jpeg}` matches all image requests

View file

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

View file

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

View file

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

View file

@ -800,9 +800,9 @@ This version was also tested against the following stable channels:
```html
<select multiple>
<option value="red">Red</div>
<option value="green">Green</div>
<option value="blue">Blue</div>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</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.
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**
@ -291,7 +291,7 @@ Here is an example that uses [`method: TestInfo.outputPath`] to create a tempora
```js
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import fs from 'fs';
test('example test', async ({}, testInfo) => {
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-%%
* 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
* since: v1.10
- type: ?<[PreserveOutput]<"always"|"never"|"failures-only">>
@ -680,7 +660,7 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
url: 'http://localhost:3000',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
@ -709,19 +689,19 @@ export default defineConfig({
webServer: [
{
command: 'npm run start',
url: 'http://127.0.0.1:3000',
url: 'http://localhost:3000',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
{
command: 'npm run backend',
url: 'http://127.0.0.1:3333',
url: 'http://localhost:3333',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
}
],
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
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import fs from 'fs';
test('example test', async ({}, testInfo) => {
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
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import fs from 'fs';
test('example test', async ({}, testInfo) => {
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?
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: {
// 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.
trace: 'on-first-retry',
@ -50,7 +50,7 @@ export default defineConfig({
// Run your local dev server before starting the tests.
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
url: 'http://localhost:3000',
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"
import debug from 'debug';
import * as fs from 'fs';
import fs from 'fs';
import { test as base } from '@playwright/test';
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"
import { defineConfig } from '@playwright/test';
import dotenv from 'dotenv';
import * as path from 'path';
import path from 'path';
// Read from ".env" file.
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:
```js title="test.spec.ts"
import * as fs from 'fs';
import * as path from 'path';
import fs from 'fs';
import path from 'path';
import { test } from '@playwright/test';
import { parse } from 'csv-parse/sync';

View file

@ -17,7 +17,7 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// 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.
storageState: 'state.json',

View file

@ -18,7 +18,7 @@ export default defineConfig({
// Run your local dev server before starting the tests
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
stdout: 'ignore',
stderr: 'pipe',
@ -52,7 +52,7 @@ export default defineConfig({
// Run your local dev server before starting the tests
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
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.
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"
import { defineConfig } from '@playwright/test';
@ -74,11 +74,11 @@ export default defineConfig({
// Run your local dev server before starting the tests
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
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';
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');
});
```
@ -106,19 +106,19 @@ export default defineConfig({
webServer: [
{
command: 'npm run start',
url: 'http://127.0.0.1:3000',
url: 'http://localhost:3000',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
{
command: 'npm run backend',
url: 'http://127.0.0.1:3333',
url: 'http://localhost:3333',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
}
],
use: {
baseURL: 'http://127.0.0.1:3000',
baseURL: 'http://localhost:3000',
},
});
```

View file

@ -1,19 +1,13 @@
---
id: touch-events
title: "Emulating touch events"
title: "Emulating legacy touch events"
---
## 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.
### 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
### 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.
@ -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.

View file

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

View file

@ -177,7 +177,7 @@ const noBooleanCompareRules = {
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 2,
};
const noRestrictedGlobalsRules = {
const noWebGlobalsRules = {
'no-restricted-globals': [
'error',
{ 'name': 'window' },
@ -186,6 +186,13 @@ const noRestrictedGlobalsRules = {
],
};
const noNodeGlobalsRules = {
'no-restricted-globals': [
'error',
{ 'name': 'process' },
],
};
const importOrderRules = {
'import/order': [2, {
'groups': ['builtin', 'external', 'internal', ['parent', 'sibling'], 'index', 'type'],
@ -249,7 +256,19 @@ export default [{
files: ['packages/playwright-core/src/server/injected/**/*.ts'],
languageOptions: languageOptionsWithTsConfig,
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,
...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",
"ssim.js": "^3.5.0",
"typescript": "^5.7.3",
"vite": "^5.4.14",
"vite": "^6.1.0",
"ws": "^8.17.1",
"xml2js": "^0.5.0",
"yaml": "^2.6.0"
"yaml": "2.6.0"
}
}

View file

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

View file

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

View file

@ -267,6 +267,26 @@ article, aside, details, figcaption, figure, footer, header, main, menu, nav, se
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) {
.subnav-item, .form-control {
border-radius: 0 !important;

View file

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

View file

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

View file

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

View file

@ -17,7 +17,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { bundle } from './bundle';
import * as path from 'path';
import path from 'path';
// https://vitejs.dev/config/
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": [
{
"name": "chromium",
"revision": "1159",
"revision": "1160",
"installByDefault": true,
"browserVersion": "134.0.6998.15"
"browserVersion": "134.0.6998.23"
},
{
"name": "chromium-headless-shell",
"revision": "1159",
"revision": "1160",
"installByDefault": true,
"browserVersion": "134.0.6998.15"
"browserVersion": "134.0.6998.23"
},
{
"name": "chromium-tip-of-tree",
"revision": "1303",
"revision": "1304",
"installByDefault": false,
"browserVersion": "135.0.7015.0"
"browserVersion": "135.0.7021.0"
},
{
"name": "chromium-tip-of-tree-headless-shell",
"revision": "1303",
"revision": "1304",
"installByDefault": false,
"browserVersion": "135.0.7015.0"
"browserVersion": "135.0.7021.0"
},
{
"name": "firefox",
@ -33,13 +33,13 @@
},
{
"name": "firefox-beta",
"revision": "1470",
"revision": "1471",
"installByDefault": false,
"browserVersion": "135.0b10"
"browserVersion": "136.0b4"
},
{
"name": "webkit",
"revision": "2137",
"revision": "2140",
"installByDefault": true,
"revisionOverrides": {
"debian11-x64": "2105",

View file

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

View file

@ -16,16 +16,16 @@
/* eslint-disable no-console */
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import fs from 'fs';
import os from 'os';
import path from 'path';
import * as playwright from '../..';
import { launchBrowserServer, printApiJson, runDriver, runServer } from './driver';
import { registry, writeDockerVersion } from '../server';
import { gracefullyProcessExitDoNotHang } from '../utils';
import { gracefullyProcessExitDoNotHang, isLikelyNpxGlobal } from '../utils';
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 { dotenv, program } from '../utilsBundle';

View file

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

View file

@ -246,15 +246,15 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
setDefaultNavigationTimeout(timeout: number | undefined) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
this._wrapApiCall(async () => {
this._channel.setDefaultNavigationTimeoutNoReply({ timeout }).catch(() => {});
}, true);
await this._channel.setDefaultNavigationTimeoutNoReply({ timeout });
}, true).catch(() => {});
}
setDefaultTimeout(timeout: number | undefined) {
this._timeoutSettings.setDefaultTimeout(timeout);
this._wrapApiCall(async () => {
this._channel.setDefaultTimeoutNoReply({ timeout }).catch(() => {});
}, true);
await this._channel.setDefaultTimeoutNoReply({ timeout });
}, true).catch(() => {});
}
browser(): Browser | null {
@ -559,7 +559,7 @@ export async function prepareBrowserContextParams(platform: Platform, options: B
};
}
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;
}

View file

@ -38,21 +38,20 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
readonly _channel: T;
readonly _initializer: channels.InitializerTraits<T>;
_logger: Logger | undefined;
readonly _platform: Platform;
readonly _instrumentation: ClientInstrumentation;
private _eventToSubscriptionMapping: Map<string, string> = new Map();
private _isInternalType = false;
_wasCollected: boolean = false;
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._connection = parent instanceof ChannelOwner ? parent._connection : parent;
this._connection = connection;
this._type = type;
this._guid = guid;
this._parent = parent instanceof ChannelOwner ? parent : undefined;
this._instrumentation = this._connection._instrumentation;
this._platform = this._connection.platform;
this._connection._objects.set(guid, this);
if (this._parent) {
@ -60,7 +59,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
this._logger = this._parent._logger;
}
this._channel = this._createChannel(new EventEmitter());
this._channel = this._createChannel(new EventEmitter(connection._platform));
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 {
const channel = new Proxy(base, {
get: (obj: any, prop: string | symbol) => {
@ -150,7 +157,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
if (validator) {
return async (params: any) => {
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) {
// Reporting/tracing/logging this api call for the first time.
apiZone.params = params;
@ -192,7 +199,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
}
return result;
} 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>'))
e.message = apiZone.apiName + ': ' + e.message;
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;
};
let parsedFrames = stack.map(line => {
const frame = parseStackFrame(line, platform.pathSeparator);
const frame = parseStackFrame(line, platform.pathSeparator, platform.showInternalStackFrames());
if (!frame || !frame.file)
return null;
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.
const filterPrefixes = platform.coreDir ? [platform.coreDir, ...platform.boxedStackPrefixes()] : platform.boxedStackPrefixes();
const filterPrefixes = platform.boxedStackPrefixes();
parsedFrames = parsedFrames.filter(f => {
if (process.env.PWDEBUGIMPL)
return true;
if (filterPrefixes.some(prefix => f.frame.file.startsWith(prefix)))
return false;
return true;

View file

@ -78,15 +78,13 @@ export class Connection extends EventEmitter {
toImpl: ((client: ChannelOwner) => any) | undefined;
private _tracingCount = 0;
readonly _instrumentation: ClientInstrumentation;
readonly platform: Platform;
// Used from @playwright/test fixtures -> TODO remove?
readonly headers: HeadersArray;
constructor(platform: Platform, localUtils?: LocalUtils, instrumentation?: ClientInstrumentation, headers: HeadersArray = []) {
super();
super(platform);
this._instrumentation = instrumentation || createInstrumentation();
this._localUtils = localUtils;
this.platform = platform;
this._rootObject = new Root(this);
this.headers = headers;
}
@ -136,9 +134,9 @@ export class Connection extends EventEmitter {
const type = object._type;
const id = ++this._lastId;
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.
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 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(() => {});
// 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.
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 }));
}
private _validatorFromWireContext(): ValidatorContext {
return {
tChannelImpl: this._tChannelImplFromWire.bind(this),
binary: this._rawBuffers ? 'buffer' : 'fromBase64',
isUnderTest: () => this._platform.isUnderTest(),
};
}
dispatch(message: object) {
if (this._closedError)
return;
const { id, guid, method, params, result, error, log } = message as any;
if (id) {
if (this.platform.isLogEnabled('channel'))
this.platform.log('channel', '<RECV ' + JSON.stringify(message));
if (this._platform.isLogEnabled('channel'))
this._platform.log('channel', '<RECV ' + JSON.stringify(message));
const callback = this._callbacks.get(id);
if (!callback)
throw new Error(`Cannot find command to respond: ${id}`);
this._callbacks.delete(id);
if (error && !result) {
const parsedError = parseError(error);
rewriteErrorMessage(parsedError, parsedError.message + formatCallLog(this.platform, log));
rewriteErrorMessage(parsedError, parsedError.message + formatCallLog(this._platform, log));
callback.reject(parsedError);
} else {
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;
}
if (this.platform.isLogEnabled('channel'))
this.platform.log('channel', '<EVENT ' + JSON.stringify(message));
if (this._platform.isLogEnabled('channel'))
this._platform.log('channel', '<EVENT ' + JSON.stringify(message));
if (method === '__create__') {
this._createRemoteObject(guid, params.type, params.guid, params.initializer);
return;
@ -198,7 +204,7 @@ export class Connection extends EventEmitter {
}
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) {
@ -229,7 +235,7 @@ export class Connection extends EventEmitter {
throw new Error(`Cannot find parent object ${parentGuid} to create ${guid}`);
let result: ChannelOwner<any>;
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) {
case 'Android':
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> {
const params: channels.ElectronLaunchParams = {
...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,
};
const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication);

View file

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

View file

@ -338,16 +338,18 @@ export class APIResponse implements api.APIResponse {
}
async body(): Promise<Buffer> {
try {
const result = await this._request._channel.fetchResponseBody({ fetchUid: this._fetchUid() });
if (result.binary === undefined)
throw new Error('Response has been disposed');
return result.binary;
} catch (e) {
if (isTargetClosedError(e))
throw new Error('Response has been disposed');
throw e;
}
return await this._request._wrapApiCall(async () => {
try {
const result = await this._request._channel.fetchResponseBody({ fetchUid: this._fetchUid() });
if (result.binary === undefined)
throw new Error('Response has been disposed');
return result.binary;
} catch (e) {
if (isTargetClosedError(e))
throw new Error('Response has been disposed');
throw e;
}
}, true);
}
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) {
super(parent, type, guid, initializer);
this._eventEmitter = new EventEmitter();
this._eventEmitter = new EventEmitter(parent._platform);
this._eventEmitter.setMaxListeners(0);
this._parentFrame = Frame.fromNullable(initializer.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}`);
}
visible(options: { visible?: boolean } = {}): Locator {
const { visible = true } = options;
return new Locator(this._frame, this._selector + ` >> visible=${visible ? 'true' : 'false'}`);
}
and(locator: Locator): Locator {
if (locator._frame !== this._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) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
this._wrapApiCall(async () => {
this._channel.setDefaultNavigationTimeoutNoReply({ timeout }).catch(() => {});
}, true);
await this._channel.setDefaultNavigationTimeoutNoReply({ timeout });
}, true).catch(() => {});
}
setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
this._wrapApiCall(async () => {
this._channel.setDefaultTimeoutNoReply({ timeout }).catch(() => {});
}, true);
await this._channel.setDefaultTimeoutNoReply({ timeout });
}, true).catch(() => {});
}
private _forceVideo(): Video {

View file

@ -14,7 +14,7 @@
* 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 path from 'path';
@ -45,6 +45,7 @@ export type Platform = {
coreDir?: string;
createGuid: () => string;
defaultMaxListeners: () => number;
env: Record<string, string | undefined>;
fs: () => typeof fs;
inspectCustom: symbol | undefined;
isDebugMode: () => boolean;
@ -54,6 +55,7 @@ export type Platform = {
log: (name: 'api' | 'channel', message: string | Error | object) => void;
path: () => typeof path;
pathSeparator: string;
showInternalStackFrames: () => boolean,
streamFile: (path: string, writable: Writable) => Promise<void>,
streamReadable: (channel: channels.StreamChannel) => Readable,
streamWritable: (channel: channels.WritableStreamChannel) => Writable,
@ -69,7 +71,7 @@ export const emptyPlatform: Platform = {
throw new Error('Not implemented');
},
colors: noColors,
colors: webColors,
createGuid: () => {
throw new Error('Not implemented');
@ -77,6 +79,8 @@ export const emptyPlatform: Platform = {
defaultMaxListeners: () => 10,
env: {},
fs: () => {
throw new Error('Not implemented');
},
@ -101,6 +105,8 @@ export const emptyPlatform: Platform = {
pathSeparator: '/',
showInternalStackFrames: () => false,
streamFile(path: string, writable: Writable): Promise<void> {
throw new Error('Streams are not available');
},
@ -115,23 +121,3 @@ export const emptyPlatform: Platform = {
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 { Browser } from './browser';
import { BrowserType } from './browserType';
import { ChannelOwner } from './channelOwner';
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];
}
_preLaunchedBrowser(): Browser {
const browser = Browser.from(this._initializer.preLaunchedBrowser!);
browser._browserType = this[browser._name as 'chromium' | 'firefox' | 'webkit'];
return browser;
}
_allContexts() {
return this._browserTypes().flatMap(type => [...type._contexts]);
}

View file

@ -96,8 +96,8 @@ export class Waiter {
log(s: string) {
this._logs.push(s);
this._channelOwner._wrapApiCall(async () => {
await this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'log', message: s } }).catch(() => {});
}, true);
await this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'log', message: s } });
}, true).catch(() => {});
}
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 transport = localUtils ? new JsonPipeTransport(localUtils) : new WebSocketTransport();
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.on('close', () => transport.close());
@ -39,7 +39,7 @@ export async function connectOverWebSocket(parentConnection: Connection, params:
connection!.dispatch(message);
} catch (e) {
closeError = String(e);
transport.close();
transport.close().catch(() => {});
}
});
return connection;
@ -70,7 +70,7 @@ class JsonPipeTransport implements Transport {
}
async send(message: object) {
this._owner._wrapApiCall(async () => {
await this._owner._wrapApiCall(async () => {
await this._pipe!.send({ message });
}, /* isInternal */ true);
}

View file

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

View file

@ -15,18 +15,19 @@
*/
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 { ManualPromise } from './utils/isomorphic/manualPromise';
import { nodePlatform } from './server/utils/nodePlatform';
import { setPlatformForSelectors } from './client/selectors';
import type { Playwright } from './client/playwright';
const connectionFactory = createConnectionFactory(nodePlatform);
export async function start(env: any = {}): Promise<{ playwright: Playwright, stop: () => Promise<void> }> {
setPlatformForSelectors(nodePlatform);
const client = new PlaywrightClient(env);
const playwright = await client._playwright;
(playwright as any).driverProcess = client._driverProcess;
@ -50,7 +51,7 @@ class PlaywrightClient {
this._driverProcess.unref();
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!);
connection.onmessage = message => transport.send(JSON.stringify(message));
transport.onmessage = message => connection.dispatch(JSON.parse(message));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ import { BrowserContext } from '../browserContext';
import * as dialog from '../dialog';
import * as dom from '../dom';
import { Page } from '../page';
import { BidiExecutionContext } from './bidiExecutionContext';
import { BidiExecutionContext, createHandle } from './bidiExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput';
import { BidiNetworkManager } from './bidiNetworkManager';
import { BidiPDF } from './bidiPdf';
@ -130,7 +130,6 @@ export class BidiPage implements PageDelegate {
const frame = this._page._frameManager.frame(realmInfo.context);
if (!frame)
return;
const delegate = new BidiExecutionContext(this._session, realmInfo);
let worldName: types.World;
if (!realmInfo.sandbox) {
worldName = 'main';
@ -141,8 +140,8 @@ export class BidiPage implements PageDelegate {
} else {
return;
}
const delegate = new BidiExecutionContext(this._session, realmInfo);
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
(context as any)[contextDelegateSymbol] = delegate;
frame._contextCreated(worldName, context);
this._realmToContext.set(realmInfo.realm, context);
}
@ -242,7 +241,7 @@ export class BidiPage implements PageDelegate {
return;
const callFrame = params.stackTrace?.callFrames[0];
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> {
@ -431,10 +430,6 @@ export class BidiPage implements PageDelegate {
return executionContext.frameIdForWindowHandle(windowHandle);
}
isElementHandle(remoteObject: bidi.Script.RemoteValue): boolean {
return remoteObject.type === 'node';
}
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const box = await handle.evaluate(element => {
if (!(element instanceof Element))
@ -532,10 +527,7 @@ export class BidiPage implements PageDelegate {
const fromContext = toBidiExecutionContext(handle._context);
const nodeId = await fromContext.nodeIdForElementHandle(handle);
const executionContext = toBidiExecutionContext(to);
const objectId = await executionContext.remoteObjectForNodeId(nodeId);
if (objectId)
return to.createHandle(objectId) as dom.ElementHandle<T>;
throw new Error('Failed to adopt element handle.');
return await executionContext.remoteObjectForNodeId(to, nodeId) as dom.ElementHandle<T>;
}
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 {
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
*/
import * as fs from 'fs';
import * as path from 'path';
import fs from 'fs';
import path from 'path';
/* eslint-disable curly, indent */

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,10 +15,12 @@
* limitations under the License.
*/
import { assert } from '../../utils/isomorphic/assert';
import { getExceptionMessage, releaseObject } from './crProtocolHelper';
import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace';
import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers';
import * as js from '../javascript';
import * as dom from '../dom';
import { isSessionClosedError } from '../protocolError';
import type { CRSession } from './crConnection';
@ -44,24 +46,24 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
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', {
expression,
contextId: this._contextId,
}).catch(rewriteError);
if (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', {
functionDeclaration: expression,
objectId: utilityScript._objectId,
arguments: [
{ objectId: utilityScript._objectId },
...values.map(value => ({ value })),
...objectIds.map(objectId => ({ objectId })),
...handles.map(handle => ({ objectId: handle._objectId! })),
],
returnByValue,
awaitPromise: true,
@ -69,29 +71,27 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
}).catch(rewriteError);
if (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', {
objectId,
objectId: object._objectId!,
ownProperties: true
});
const result = new Map();
for (const property of response.result) {
if (!property.enumerable || !property.value)
continue;
result.set(property.name, context.createHandle(property.value));
result.set(property.name, createHandle(object._context, property.value));
}
return result;
}
createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type, renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
}
async releaseHandle(objectId: js.ObjectId): Promise<void> {
await releaseObject(this._client, objectId);
async releaseHandle(handle: js.JSHandle): Promise<void> {
if (!handle._objectId)
return;
await releaseObject(this._client, handle._objectId);
}
}
@ -132,3 +132,11 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine
return js.sparseArrayToString(object.preview.properties);
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.
*/
import * as path from 'path';
import path from 'path';
import { assert } from '../../utils/isomorphic/assert';
import { createGuid } from '../utils/crypto';
@ -33,7 +33,7 @@ import { getAccessibilityTree } from './crAccessibility';
import { CRBrowserContext } from './crBrowser';
import { CRCoverage } from './crCoverage';
import { DragManager } from './crDragDrop';
import { CRExecutionContext } from './crExecutionContext';
import { createHandle, CRExecutionContext } from './crExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './crInput';
import { CRNetworkManager } from './crNetworkManager';
import { CRPDF } from './crPdf';
@ -281,10 +281,6 @@ export class CRPage implements PageDelegate {
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> {
return this._sessionForHandle(handle)._getBoundingBox(handle);
}
@ -679,7 +675,6 @@ class FrameSession {
else if (contextPayload.name === UTILITY_WORLD_NAME)
worldName = 'utility';
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
(context as any)[contextDelegateSymbol] = delegate;
if (worldName)
frame._contextCreated(worldName, context);
this._contextIdToContext.set(contextPayload.id, context);
@ -739,7 +734,7 @@ class FrameSession {
session.on('Target.attachedToTarget', event => this._onAttachedToTarget(event));
session.on('Target.detachedFromTarget', event => this._onDetachedFromTarget(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));
});
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);
if (!context)
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));
}
@ -1167,11 +1162,11 @@ class FrameSession {
async _adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise<dom.ElementHandle> {
const result = await this._client._sendMayFail('DOM.resolveNode', {
backendNodeId,
executionContextId: ((to as any)[contextDelegateSymbol] as CRExecutionContext)._contextId,
executionContextId: (to.delegate as CRExecutionContext)._contextId,
});
if (!result || result.object.subtype === 'null')
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
function calculateUserAgentMetadata(options: types.BrowserContextOptions) {
const ua = options.userAgent;

View file

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

View file

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

View file

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

View file

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

View file

@ -200,13 +200,13 @@ export class DispatcherConnection {
sendEvent(dispatcher: DispatcherScope, event: string, params: any) {
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 });
}
sendCreate(parent: DispatcherScope, type: string, guid: string, initializer: any) {
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 } });
}
@ -218,6 +218,22 @@ export class DispatcherConnection {
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 {
if (arg && typeof arg === 'object' && typeof arg.guid === 'string') {
const guid = arg.guid;
@ -279,8 +295,9 @@ export class DispatcherConnection {
let validMetadata: channels.Metadata;
try {
const validator = findValidator(dispatcher._type, method, 'Params');
validParams = validator(params, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._isLocal ? 'buffer' : 'fromBase64' });
validMetadata = metadataValidator(metadata, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._isLocal ? 'buffer' : 'fromBase64' });
const validatorContext = this._validatorFromWireContext();
validParams = validator(params, '', validatorContext);
validMetadata = metadataValidator(metadata, '', validatorContext);
if (typeof (dispatcher as any)[method] !== 'function')
throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`);
} catch (e) {
@ -338,7 +355,7 @@ export class DispatcherConnection {
try {
const result = await dispatcher._handleCommand(callMetadata, method, validParams);
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;
} catch (e) {
if (isTargetClosedError(e) && sdkObject) {

View file

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

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
import * as fs from 'fs';
import fs from 'fs';
import * as js from './javascript';
import { ProgressController } from './progress';
@ -83,12 +83,6 @@ export class FrameExecutionContext extends js.ExecutionContext {
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>> {
if (!this._injectedScriptPromise) {
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;
}

View file

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

View file

@ -14,9 +14,9 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import fs from 'fs';
import os from 'os';
import path from 'path';
import * as readline from 'readline';
import { TimeoutSettings } from '../timeoutSettings';
@ -27,7 +27,7 @@ import { eventsHelper } from '../utils/eventsHelper';
import { validateBrowserContextOptions } from '../browserContext';
import { CRBrowser } from '../chromium/crBrowser';
import { CRConnection } from '../chromium/crConnection';
import { CRExecutionContext } from '../chromium/crExecutionContext';
import { createHandle, CRExecutionContext } from '../chromium/crExecutionContext';
import { toConsoleMessageLocation } from '../chromium/crProtocolHelper';
import { ConsoleMessage } from '../console';
import { helper } from '../helper';
@ -116,7 +116,7 @@ export class ElectronApplication extends SdkObject {
}
if (!this._nodeExecutionContext)
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));
this.emit(ElectronApplication.Events.Console, message);
}

View file

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

View file

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

View file

@ -15,9 +15,11 @@
* limitations under the License.
*/
import { assert } from '../../utils/isomorphic/assert';
import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace';
import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers';
import * as js from '../javascript';
import * as dom from '../dom';
import { isSessionClosedError } from '../protocolError';
import type { FFSession } from './ffConnection';
@ -42,23 +44,23 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
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', {
expression,
returnByValue: false,
executionContextId: this._executionContextId,
}).catch(rewriteError);
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', {
functionDeclaration: expression,
args: [
{ objectId: utilityScript._objectId, value: undefined },
...values.map(value => ({ value })),
...objectIds.map(objectId => ({ objectId, value: undefined })),
...handles.map(handle => ({ objectId: handle._objectId!, value: undefined })),
],
returnByValue,
executionContextId: this._executionContextId
@ -66,28 +68,26 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
checkException(payload.exceptionDetails);
if (returnByValue)
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', {
executionContextId: this._executionContextId,
objectId,
objectId: object._objectId!,
});
const result = new Map();
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;
}
createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type || '', renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
}
async releaseHandle(objectId: js.ObjectId): Promise<void> {
async releaseHandle(handle: js.JSHandle): Promise<void> {
if (!handle._objectId)
return;
await this._session.send('Runtime.disposeObject', {
executionContextId: this._executionContextId,
objectId
objectId: handle._objectId,
});
}
}
@ -135,3 +135,11 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine
if ('value' in object)
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 { getAccessibilityTree } from './ffAccessibility';
import { FFSession } from './ffConnection';
import { FFExecutionContext } from './ffExecutionContext';
import { createHandle, FFExecutionContext } from './ffExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './ffInput';
import { FFNetworkManager } from './ffNetworkManager';
import { debugLogger } from '../utils/debugLogger';
@ -150,7 +150,6 @@ export class FFPage implements PageDelegate {
else if (!auxData.name)
worldName = 'main';
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
(context as any)[contextDelegateSymbol] = delegate;
if (worldName)
frame._contextCreated(worldName, context);
this._contextIdToContext.set(executionContextId, context);
@ -234,7 +233,7 @@ export class FFPage implements PageDelegate {
if (!context)
return;
// 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) {
@ -262,7 +261,7 @@ export class FFPage implements PageDelegate {
const context = this._contextIdToContext.get(executionContextId);
if (!context)
return;
const handle = context.createHandle(element).asElement()!;
const handle = createHandle(context, element).asElement()!;
await this._page._onFileChooserOpened(handle);
}
@ -286,7 +285,7 @@ export class FFPage implements PageDelegate {
workerSession.on('Runtime.console', event => {
const { type, args, location } = event;
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.
}
@ -443,10 +442,6 @@ export class FFPage implements PageDelegate {
return ownerFrameId || null;
}
isElementHandle(remoteObject: any): boolean {
return remoteObject.subtype === 'node';
}
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const quads = await this.getContentQuads(handle);
if (!quads || !quads.length)
@ -531,11 +526,11 @@ export class FFPage implements PageDelegate {
const result = await this._session.send('Page.adoptNode', {
frameId: handle._context.frame._id,
objectId: handle._objectId,
executionContextId: ((to as any)[contextDelegateSymbol] as FFExecutionContext)._executionContextId
executionContextId: (to.delegate as FFExecutionContext)._executionContextId
});
if (!result.remoteObject)
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) {
@ -560,11 +555,11 @@ export class FFPage implements PageDelegate {
const context = await parent._mainContext();
const result = await this._session.send('Page.adoptNode', {
frameId: frame._id,
executionContextId: ((context as any)[contextDelegateSymbol] as FFExecutionContext)._executionContextId
executionContextId: (context.delegate as FFExecutionContext)._executionContextId
});
if (!result.remoteObject)
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 {
@ -575,5 +570,3 @@ export class FFPage implements PageDelegate {
function webSocketId(frameId: string, wsid: string): string {
return `${frameId}---${wsid}`;
}
const contextDelegateSymbol = Symbol('delegate');

View file

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

View file

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

View file

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

View file

@ -337,7 +337,7 @@ function trimFlatString(s: string): string {
function asFlatString(s: string): string {
// "Flat string" at https://w3c.github.io/accname/#terminology
// 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[] {

View file

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

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