Merge branch 'main' into fix-32951

This commit is contained in:
Simon Knott 2024-10-07 18:07:18 +02:00
commit 131eece567
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
64 changed files with 649 additions and 693 deletions

View file

@ -193,6 +193,8 @@ Resumes timers. Once this method is called, time resumes flowing, timers are fir
Makes `Date.now` and `new Date()` return fixed fake time at all times, Makes `Date.now` and `new Date()` return fixed fake time at all times,
keeps all the timers running. keeps all the timers running.
Use this method for simple scenarios where you only need to test with a predefined time. For more advanced scenarios, use [`method: Clock.install`] instead. Read docs on [clock emulation](../clock.md) to learn more.
**Usage** **Usage**
```js ```js
@ -249,7 +251,7 @@ Time to be set.
## async method: Clock.setSystemTime ## async method: Clock.setSystemTime
* since: v1.45 * since: v1.45
Sets current system time but does not trigger any timers. Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example switching from summer to winter time, or changing time zones.
**Usage** **Usage**

View file

@ -1217,8 +1217,6 @@ await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches);
// → true // → true
await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches); await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches);
// → false // → false
await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches);
// → false
``` ```
```java ```java
@ -1227,8 +1225,6 @@ page.evaluate("() => matchMedia('(prefers-color-scheme: dark)').matches");
// → true // → true
page.evaluate("() => matchMedia('(prefers-color-scheme: light)').matches"); page.evaluate("() => matchMedia('(prefers-color-scheme: light)').matches");
// → false // → false
page.evaluate("() => matchMedia('(prefers-color-scheme: no-preference)').matches");
// → false
``` ```
```python async ```python async
@ -1237,8 +1233,6 @@ await page.evaluate("matchMedia('(prefers-color-scheme: dark)').matches")
# → True # → True
await page.evaluate("matchMedia('(prefers-color-scheme: light)').matches") await page.evaluate("matchMedia('(prefers-color-scheme: light)').matches")
# → False # → False
await page.evaluate("matchMedia('(prefers-color-scheme: no-preference)').matches")
# → False
``` ```
```python sync ```python sync
@ -1247,7 +1241,6 @@ page.evaluate("matchMedia('(prefers-color-scheme: dark)').matches")
# → True # → True
page.evaluate("matchMedia('(prefers-color-scheme: light)').matches") page.evaluate("matchMedia('(prefers-color-scheme: light)').matches")
# → False # → False
page.evaluate("matchMedia('(prefers-color-scheme: no-preference)').matches")
``` ```
```csharp ```csharp
@ -1256,8 +1249,6 @@ await page.EvaluateAsync("matchMedia('(prefers-color-scheme: dark)').matches");
// → true // → true
await page.EvaluateAsync("matchMedia('(prefers-color-scheme: light)').matches"); await page.EvaluateAsync("matchMedia('(prefers-color-scheme: light)').matches");
// → false // → false
await page.EvaluateAsync("matchMedia('(prefers-color-scheme: no-preference)').matches");
// → false
``` ```
### option: Page.emulateMedia.media ### option: Page.emulateMedia.media
@ -1281,16 +1272,16 @@ Passing `'Null'` disables CSS media emulation.
* langs: js, java * langs: js, java
- `colorScheme` <null|[ColorScheme]<"light"|"dark"|"no-preference">> - `colorScheme` <null|[ColorScheme]<"light"|"dark"|"no-preference">>
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. Passing Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media feature, supported values are `'light'` and `'dark'`. Passing
`null` disables color scheme emulation. `null` disables color scheme emulation. `'no-preference'` is deprecated.
### option: Page.emulateMedia.colorScheme ### option: Page.emulateMedia.colorScheme
* since: v1.9 * since: v1.9
* langs: csharp, python * langs: csharp, python
- `colorScheme` <[ColorScheme]<"light"|"dark"|"no-preference"|"null">> - `colorScheme` <[ColorScheme]<"light"|"dark"|"no-preference"|"null">>
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. Passing Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media feature, supported values are `'light'` and `'dark'`. Passing
`'Null'` disables color scheme emulation. `'Null'` disables color scheme emulation. `'no-preference'` is deprecated.
### option: Page.emulateMedia.reducedMotion ### option: Page.emulateMedia.reducedMotion
* since: v1.12 * since: v1.12

View file

@ -639,14 +639,14 @@ If no origin is specified, the username and password are sent to any servers upo
* langs: js, java * langs: js, java
- `colorScheme` <null|[ColorScheme]<"light"|"dark"|"no-preference">> - `colorScheme` <null|[ColorScheme]<"light"|"dark"|"no-preference">>
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media feature, supported values are `'light'` and `'dark'`. See
[`method: Page.emulateMedia`] for more details. Passing `null` resets emulation to system defaults. Defaults to `'light'`. [`method: Page.emulateMedia`] for more details. Passing `null` resets emulation to system defaults. Defaults to `'light'`.
## context-option-colorscheme-csharp-python ## context-option-colorscheme-csharp-python
* langs: csharp, python * langs: csharp, python
- `colorScheme` <[ColorScheme]<"light"|"dark"|"no-preference"|"null">> - `colorScheme` <[ColorScheme]<"light"|"dark"|"no-preference"|"null">>
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media feature, supported values are `'light'` and `'dark'`. See
[`method: Page.emulateMedia`] for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'light'`. [`method: Page.emulateMedia`] for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'light'`.
## context-option-reducedMotion ## context-option-reducedMotion

View file

@ -112,10 +112,40 @@ Playwright has a [test generator](./codegen.md) that can generate tests and pick
To pick a locator run the `codegen` command followed by the URL that you would like to pick a locator from. To pick a locator run the `codegen` command followed by the URL that you would like to pick a locator from.
<Tabs
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash ```bash
npx playwright codegen playwright.dev npx playwright codegen playwright.dev
``` ```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright codegen playwright.dev
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright codegen playwright.dev
```
</TabItem>
</Tabs>
This will open a new browser window as well as the Playwright inspector. To pick a locator first click on the 'Record' button to stop the recording. By default when you run the `codegen` command it will start a new recording. Once you stop the recording the 'Pick Locator' button will be available to click. This will open a new browser window as well as the Playwright inspector. To pick a locator first click on the 'Record' button to stop the recording. By default when you run the `codegen` command it will start a new recording. Once you stop the recording the 'Pick Locator' button will be available to click.
You can then hover over any element on your page in the browser window and see the locator highlighted below your cursor. Clicking on an element will add the locator into the Playwright inspector. You can either copy the locator and paste into your test file or continue to explore the locator by editing it in the Playwright Inspector, for example by modifying the text, and seeing the results in the browser window. You can then hover over any element on your page in the browser window and see the locator highlighted below your cursor. Clicking on an element will add the locator into the Playwright inspector. You can either copy the locator and paste into your test file or continue to explore the locator by editing it in the Playwright Inspector, for example by modifying the text, and seeing the results in the browser window.
@ -170,10 +200,40 @@ You can live debug your test by clicking or editing the locators in your test in
You can also debug your tests with the Playwright inspector by running your tests with the `--debug` flag. You can also debug your tests with the Playwright inspector by running your tests with the `--debug` flag.
<Tabs
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash ```bash
npx playwright test --debug npx playwright test --debug
``` ```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright test --debug
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright test --debug
```
</TabItem>
</Tabs>
You can then step through your test, view actionability logs and edit the locator live and see it highlighted in the browser window. This will show you which locators match, how many of them there are. You can then step through your test, view actionability logs and edit the locator live and see it highlighted in the browser window. This will show you which locators match, how many of them there are.
<img width="1350" alt="debugging with the playwright inspector" loading="lazy" src="https://user-images.githubusercontent.com/13063165/212276296-4f5b18e7-2bd7-4766-9aa5-783517bd4aa2.png" /> <img width="1350" alt="debugging with the playwright inspector" loading="lazy" src="https://user-images.githubusercontent.com/13063165/212276296-4f5b18e7-2bd7-4766-9aa5-783517bd4aa2.png" />
@ -182,9 +242,39 @@ You can then step through your test, view actionability logs and edit the locato
To debug a specific test add the name of the test file and the line number of the test followed by the `--debug` flag. To debug a specific test add the name of the test file and the line number of the test followed by the `--debug` flag.
<Tabs
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash ```bash
npx playwright test example.spec.ts:9 --debug npx playwright test example.spec.ts:9 --debug
``` ```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright test example.spec.ts:9 --debug
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright test example.spec.ts:9 --debug
```
</TabItem>
</Tabs>
#### Debugging on CI #### Debugging on CI
For CI failures, use the Playwright [trace viewer](./trace-viewer.md) instead of videos and screenshots. The trace viewer gives you a full trace of your tests as a local Progressive Web App (PWA) that can easily be shared. With the trace viewer you can view the timeline, inspect DOM snapshots for each action using dev tools, view network requests and more. For CI failures, use the Playwright [trace viewer](./trace-viewer.md) instead of videos and screenshots. The trace viewer gives you a full trace of your tests as a local Progressive Web App (PWA) that can easily be shared. With the trace viewer you can view the timeline, inspect DOM snapshots for each action using dev tools, view network requests and more.
@ -193,14 +283,75 @@ For CI failures, use the Playwright [trace viewer](./trace-viewer.md) instead of
Traces are configured in the Playwright config file and are set to run on CI on the first retry of a failed test. We don't recommend setting this to `on` so that traces are run on every test as it's very performance heavy. However you can run a trace locally when developing with the `--trace` flag. Traces are configured in the Playwright config file and are set to run on CI on the first retry of a failed test. We don't recommend setting this to `on` so that traces are run on every test as it's very performance heavy. However you can run a trace locally when developing with the `--trace` flag.
<Tabs
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash ```bash
npx playwright test --trace on npx playwright test --trace on
``` ```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright test --trace on
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright test --trace on
```
</TabItem>
</Tabs>
Once you run this command your traces will be recorded for each test and can be viewed directly from the HTML report. Once you run this command your traces will be recorded for each test and can be viewed directly from the HTML report.
<Tabs
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash ```bash
npx playwright show-report npx playwright show-report
```` ```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright show-report
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright show-report
```
</TabItem>
</Tabs>
<img width="1516" alt="Playwrights HTML report" loading="lazy" src="https://user-images.githubusercontent.com/13063165/212279022-d929d4c0-2271-486a-a75f-166ac231d25f.png" /> <img width="1516" alt="Playwrights HTML report" loading="lazy" src="https://user-images.githubusercontent.com/13063165/212279022-d929d4c0-2271-486a-a75f-166ac231d25f.png" />
@ -246,17 +397,78 @@ export default defineConfig({
By keeping your Playwright version up to date you will be able to test your app on the latest browser versions and catch failures before the latest browser version is released to the public. By keeping your Playwright version up to date you will be able to test your app on the latest browser versions and catch failures before the latest browser version is released to the public.
<Tabs
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash ```bash
npm install -D @playwright/test@latest npm install -D @playwright/test@latest
``` ```
</TabItem>
<TabItem value="yarn">
```bash
yarn add --dev @playwright/test@latest
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm install --save-dev @playwright/test@latest
```
</TabItem>
</Tabs>
Check the [release notes](./release-notes.md) to see what the latest version is and what changes have been released. Check the [release notes](./release-notes.md) to see what the latest version is and what changes have been released.
You can see what version of Playwright you have by running the following command. You can see what version of Playwright you have by running the following command.
<Tabs
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash ```bash
npx playwright --version npx playwright --version
``` ```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright --version
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright --version
```
</TabItem>
</Tabs>
### Run tests on CI ### Run tests on CI
Setup CI/CD and run your tests frequently. The more often you run your tests the better. Ideally you should run your tests on each commit and pull request. Playwright comes with a [GitHub actions workflow](/ci-intro.md) so that tests will run on CI for you with no setup required. Playwright can also be setup on the [CI environment](/ci.md) of your choice. Setup CI/CD and run your tests frequently. The more often you run your tests the better. Ideally you should run your tests on each commit and pull request. Playwright comes with a [GitHub actions workflow](/ci-intro.md) so that tests will run on CI for you with no setup required. Playwright can also be setup on the [CI environment](/ci.md) of your choice.
@ -282,10 +494,40 @@ test('runs in parallel 2', async ({ page }) => { /* ... */ });
Playwright can [shard](./test-parallel.md#shard-tests-between-multiple-machines) a test suite, so that it can be executed on multiple machines. Playwright can [shard](./test-parallel.md#shard-tests-between-multiple-machines) a test suite, so that it can be executed on multiple machines.
<Tabs
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash ```bash
npx playwright test --shard=1/3 npx playwright test --shard=1/3
``` ```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright test --shard=1/3
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright test --shard=1/3
```
</TabItem>
</Tabs>
## Productivity tips ## Productivity tips
### Use Soft assertions ### Use Soft assertions

View file

@ -415,7 +415,7 @@ Large test suites can take very long to execute. By executing a preliminary test
This will give you a faster feedback loop and slightly lower CI consumption while working on Pull Requests. This will give you a faster feedback loop and slightly lower CI consumption while working on Pull Requests.
To detect test files affected by your changeset, `--only-changed` analyses your suites' dependency graph. This is a heuristic and might miss tests, so it's important that you always run the full test suite after the preliminary test run. To detect test files affected by your changeset, `--only-changed` analyses your suites' dependency graph. This is a heuristic and might miss tests, so it's important that you always run the full test suite after the preliminary test run.
```yml js title=".github/workflows/playwright.yml" ```yml js title=".github/workflows/playwright.yml" {24-26}
name: Playwright Tests name: Playwright Tests
on: on:
push: push:

View file

@ -558,7 +558,7 @@ await context.SetGeolocationAsync(new Geolocation() { Longitude = 48.858455, Lat
**Note** you can only change geolocation for all pages in the context. **Note** you can only change geolocation for all pages in the context.
## Color Scheme and Media ## Color Scheme and Media
Emulate the users `"colorScheme"`. Supported values are 'light', 'dark', 'no-preference'. You can also emulate the media type with [`method: Page.emulateMedia`]. Emulate the users `"colorScheme"`. Supported values are 'light' and 'dark'. You can also emulate the media type with [`method: Page.emulateMedia`].
```js title="playwright.config.ts" ```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test'; import { defineConfig } from '@playwright/test';

View file

@ -422,18 +422,10 @@ Or just pass the reporter file path as `--reporter` command line option:
npx playwright test --reporter="./myreporter/my-awesome-reporter.ts" npx playwright test --reporter="./myreporter/my-awesome-reporter.ts"
``` ```
## Third party reporter showcase Here's a short list of open source reporter implementations that you can take a look at when writing your own reporter:
* [Allure](https://www.npmjs.com/package/allure-playwright) * [Allure Reporter](https://github.com/allure-framework/allure-js/tree/main/packages/allure-playwright)
* [Argos Visual Testing](https://argos-ci.com/docs/playwright) * [Github Actions Reporter](https://github.com/estruyf/playwright-github-actions-reporter)
* [Currents](https://www.npmjs.com/package/@currents/playwright) * [Mail Reporter](https://github.com/estruyf/playwright-mail-reporter)
* [GitHub Actions Reporter](https://www.npmjs.com/package/@estruyf/github-actions-reporter)
* [GitHub Pull Request Comment](https://github.com/marketplace/actions/playwright-report-comment)
* [Mail Reporter](https://www.npmjs.com/package/playwright-mail-reporter)
* [Microsoft Teams Reporter](https://www.npmjs.com/package/playwright-msteams-reporter)
* [Monocart](https://github.com/cenfun/monocart-reporter)
* [ReportPortal](https://github.com/reportportal/agent-js-playwright) * [ReportPortal](https://github.com/reportportal/agent-js-playwright)
* [Serenity/JS](https://serenity-js.org/handbook/test-runners/playwright-test) * [Monocart](https://github.com/cenfun/monocart-reporter)
* [Testmo](https://github.com/jonasclaes/playwright-testmo-reporter)
* [Testomat.io](https://github.com/testomatio/reporter/blob/master/docs/frameworks.md#playwright)
* [Tesults](https://www.tesults.com/docs/playwright)

View file

@ -64,7 +64,7 @@ export default defineConfig({
| Option | Description | | Option | Description |
| :- | :- | | :- | :- |
| [`property: TestOptions.colorScheme`] | [Emulates](./emulation.md#color-scheme-and-media) `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'` | | [`property: TestOptions.colorScheme`] | [Emulates](./emulation.md#color-scheme-and-media) `'prefers-colors-scheme'` media feature, supported values are `'light'` and `'dark'` |
| [`property: TestOptions.geolocation`] | Context [geolocation](./emulation.md#geolocation). | | [`property: TestOptions.geolocation`] | Context [geolocation](./emulation.md#geolocation). |
| [`property: TestOptions.locale`] | [Emulates](./emulation.md#locale--timezone) the user locale, for example `en-GB`, `de-DE`, etc. | | [`property: TestOptions.locale`] | [Emulates](./emulation.md#locale--timezone) the user locale, for example `en-GB`, `de-DE`, etc. |
| [`property: TestOptions.permissions`] | A list of [permissions](./emulation.md#permissions) to grant to all pages in the context. | | [`property: TestOptions.permissions`] | A list of [permissions](./emulation.md#permissions) to grant to all pages in the context. |

View file

@ -61,6 +61,7 @@
align-items: center; align-items: center;
padding: 0 8px; padding: 0 8px;
line-height: 24px; line-height: 24px;
white-space: pre-wrap;
} }
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {

View file

@ -6,7 +6,7 @@ This project incorporates components from the projects listed below. The origina
- @types/node@17.0.24 (https://github.com/DefinitelyTyped/DefinitelyTyped) - @types/node@17.0.24 (https://github.com/DefinitelyTyped/DefinitelyTyped)
- @types/yauzl@2.10.0 (https://github.com/DefinitelyTyped/DefinitelyTyped) - @types/yauzl@2.10.0 (https://github.com/DefinitelyTyped/DefinitelyTyped)
- agent-base@6.0.2 (https://github.com/TooTallNate/node-agent-base) - agent-base@7.1.1 (https://github.com/TooTallNate/proxy-agents)
- balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match) - balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match)
- brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion) - brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion)
- buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32) - buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32)
@ -23,7 +23,7 @@ This project incorporates components from the projects listed below. The origina
- fd-slicer@1.1.0 (https://github.com/andrewrk/node-fd-slicer) - fd-slicer@1.1.0 (https://github.com/andrewrk/node-fd-slicer)
- get-stream@5.2.0 (https://github.com/sindresorhus/get-stream) - get-stream@5.2.0 (https://github.com/sindresorhus/get-stream)
- graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs) - graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs)
- https-proxy-agent@5.0.0 (https://github.com/TooTallNate/node-https-proxy-agent) - https-proxy-agent@7.0.5 (https://github.com/TooTallNate/proxy-agents)
- ip-address@9.0.5 (https://github.com/beaugunderson/ip-address) - ip-address@9.0.5 (https://github.com/beaugunderson/ip-address)
- is-docker@2.2.1 (https://github.com/sindresorhus/is-docker) - is-docker@2.2.1 (https://github.com/sindresorhus/is-docker)
- is-wsl@2.2.0 (https://github.com/sindresorhus/is-wsl) - is-wsl@2.2.0 (https://github.com/sindresorhus/is-wsl)
@ -42,7 +42,7 @@ This project incorporates components from the projects listed below. The origina
- retry@0.12.0 (https://github.com/tim-kos/node-retry) - retry@0.12.0 (https://github.com/tim-kos/node-retry)
- signal-exit@3.0.7 (https://github.com/tapjs/signal-exit) - signal-exit@3.0.7 (https://github.com/tapjs/signal-exit)
- smart-buffer@4.2.0 (https://github.com/JoshGlazebrook/smart-buffer) - smart-buffer@4.2.0 (https://github.com/JoshGlazebrook/smart-buffer)
- socks-proxy-agent@6.1.1 (https://github.com/TooTallNate/node-socks-proxy-agent) - socks-proxy-agent@8.0.4 (https://github.com/TooTallNate/proxy-agents)
- socks@2.8.3 (https://github.com/JoshGlazebrook/socks) - socks@2.8.3 (https://github.com/JoshGlazebrook/socks)
- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js) - sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js)
- stack-utils@2.0.5 (https://github.com/tapjs/stack-utils) - stack-utils@2.0.5 (https://github.com/tapjs/stack-utils)
@ -103,128 +103,11 @@ MIT License
========================================= =========================================
END OF @types/yauzl@2.10.0 AND INFORMATION END OF @types/yauzl@2.10.0 AND INFORMATION
%% agent-base@6.0.2 NOTICES AND INFORMATION BEGIN HERE %% agent-base@7.1.1 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
agent-base
==========
### Turn a function into an [`http.Agent`][http.Agent] instance
[![Build Status](https://github.com/TooTallNate/node-agent-base/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-agent-base/actions?workflow=Node+CI)
This module provides an `http.Agent` generator. That is, you pass it an async
callback function, and it returns a new `http.Agent` instance that will invoke the
given callback function when sending outbound HTTP requests.
#### Some subclasses:
Here's some more interesting uses of `agent-base`.
Send a pull request to list yours!
* [`http-proxy-agent`][http-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTP endpoints
* [`https-proxy-agent`][https-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTPS endpoints
* [`pac-proxy-agent`][pac-proxy-agent]: A PAC file proxy `http.Agent` implementation for HTTP and HTTPS
* [`socks-proxy-agent`][socks-proxy-agent]: A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS
Installation
------------
Install with `npm`:
``` bash
$ npm install agent-base
```
Example
-------
Here's a minimal example that creates a new `net.Socket` connection to the server
for every HTTP request (i.e. the equivalent of `agent: false` option):
```js
var net = require('net');
var tls = require('tls');
var url = require('url');
var http = require('http');
var agent = require('agent-base');
var endpoint = 'http://nodejs.org/api/';
var parsed = url.parse(endpoint);
// This is the important part!
parsed.agent = agent(function (req, opts) {
var socket;
// `secureEndpoint` is true when using the https module
if (opts.secureEndpoint) {
socket = tls.connect(opts);
} else {
socket = net.connect(opts);
}
return socket;
});
// Everything else works just like normal...
http.get(parsed, function (res) {
console.log('"response" event!', res.headers);
res.pipe(process.stdout);
});
```
Returning a Promise or using an `async` function is also supported:
```js
agent(async function (req, opts) {
await sleep(1000);
// etc…
});
```
Return another `http.Agent` instance to "pass through" the responsibility
for that HTTP request to that agent:
```js
agent(function (req, opts) {
return opts.secureEndpoint ? https.globalAgent : http.globalAgent;
});
```
API
---
## Agent(Function callback[, Object options]) → [http.Agent][]
Creates a base `http.Agent` that will execute the callback function `callback`
for every HTTP request that it is used as the `agent` for. The callback function
is responsible for creating a `stream.Duplex` instance of some kind that will be
used as the underlying socket in the HTTP request.
The `options` object accepts the following properties:
* `timeout` - Number - Timeout for the `callback()` function in milliseconds. Defaults to Infinity (optional).
The callback function should have the following signature:
### callback(http.ClientRequest req, Object options, Function cb) → undefined
The ClientRequest `req` can be accessed to read request headers and
and the path, etc. The `options` object contains the options passed
to the `http.request()`/`https.request()` function call, and is formatted
to be directly passed to `net.connect()`/`tls.connect()`, or however
else you want a Socket to be created. Pass the created socket to
the callback function `cb` once created, and the HTTP request will
continue to proceed.
If the `https` module is used to invoke the HTTP request, then the
`secureEndpoint` property on `options` _will be set to `true`_.
License
-------
(The MIT License) (The MIT License)
Copyright (c) 2013 Nathan Rajlich &lt;nathan@tootallnate.net&gt; Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the
@ -244,14 +127,8 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[http-proxy-agent]: https://github.com/TooTallNate/node-http-proxy-agent
[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent
[pac-proxy-agent]: https://github.com/TooTallNate/node-pac-proxy-agent
[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent
[http.Agent]: https://nodejs.org/api/http.html#http_class_http_agent
========================================= =========================================
END OF agent-base@6.0.2 AND INFORMATION END OF agent-base@7.1.1 AND INFORMATION
%% balanced-match@1.0.2 NOTICES AND INFORMATION BEGIN HERE %% balanced-match@1.0.2 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
@ -629,124 +506,11 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
========================================= =========================================
END OF graceful-fs@4.2.10 AND INFORMATION END OF graceful-fs@4.2.10 AND INFORMATION
%% https-proxy-agent@5.0.0 NOTICES AND INFORMATION BEGIN HERE %% https-proxy-agent@7.0.5 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
https-proxy-agent
================
### An HTTP(s) proxy `http.Agent` implementation for HTTPS
[![Build Status](https://github.com/TooTallNate/node-https-proxy-agent/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-https-proxy-agent/actions?workflow=Node+CI)
This module provides an `http.Agent` implementation that connects to a specified
HTTP or HTTPS proxy server, and can be used with the built-in `https` module.
Specifically, this `Agent` implementation connects to an intermediary "proxy"
server and issues the [CONNECT HTTP method][CONNECT], which tells the proxy to
open a direct TCP connection to the destination server.
Since this agent implements the CONNECT HTTP method, it also works with other
protocols that use this method when connecting over proxies (i.e. WebSockets).
See the "Examples" section below for more.
Installation
------------
Install with `npm`:
``` bash
$ npm install https-proxy-agent
```
Examples
--------
#### `https` module example
``` js
var url = require('url');
var https = require('https');
var HttpsProxyAgent = require('https-proxy-agent');
// HTTP/HTTPS proxy to connect to
var proxy = process.env.http_proxy || 'http://168.63.76.32:3128';
console.log('using proxy server %j', proxy);
// HTTPS endpoint for the proxy to connect to
var endpoint = process.argv[2] || 'https://graph.facebook.com/tootallnate';
console.log('attempting to GET %j', endpoint);
var options = url.parse(endpoint);
// create an instance of the `HttpsProxyAgent` class with the proxy server information
var agent = new HttpsProxyAgent(proxy);
options.agent = agent;
https.get(options, function (res) {
console.log('"response" event!', res.headers);
res.pipe(process.stdout);
});
```
#### `ws` WebSocket connection example
``` js
var url = require('url');
var WebSocket = require('ws');
var HttpsProxyAgent = require('https-proxy-agent');
// HTTP/HTTPS proxy to connect to
var proxy = process.env.http_proxy || 'http://168.63.76.32:3128';
console.log('using proxy server %j', proxy);
// WebSocket endpoint for the proxy to connect to
var endpoint = process.argv[2] || 'ws://echo.websocket.org';
var parsed = url.parse(endpoint);
console.log('attempting to connect to WebSocket %j', endpoint);
// create an instance of the `HttpsProxyAgent` class with the proxy server information
var options = url.parse(proxy);
var agent = new HttpsProxyAgent(options);
// finally, initiate the WebSocket connection
var socket = new WebSocket(endpoint, { agent: agent });
socket.on('open', function () {
console.log('"open" event!');
socket.send('hello world');
});
socket.on('message', function (data, flags) {
console.log('"message" event! %j %j', data, flags);
socket.close();
});
```
API
---
### new HttpsProxyAgent(Object options)
The `HttpsProxyAgent` class implements an `http.Agent` subclass that connects
to the specified "HTTP(s) proxy server" in order to proxy HTTPS and/or WebSocket
requests. This is achieved by using the [HTTP `CONNECT` method][CONNECT].
The `options` argument may either be a string URI of the proxy server to use, or an
"options" object with more specific properties:
* `host` - String - Proxy host to connect to (may use `hostname` as well). Required.
* `port` - Number - Proxy port to connect to. Required.
* `protocol` - String - If `https:`, then use TLS to connect to the proxy.
* `headers` - Object - Additional HTTP headers to be sent on the HTTP CONNECT method.
* Any other options given are passed to the `net.connect()`/`tls.connect()` functions.
License
-------
(The MIT License) (The MIT License)
Copyright (c) 2013 Nathan Rajlich &lt;nathan@tootallnate.net&gt; Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the
@ -766,10 +530,8 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[CONNECT]: http://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_Tunneling
========================================= =========================================
END OF https-proxy-agent@5.0.0 AND INFORMATION END OF https-proxy-agent@7.0.5 AND INFORMATION
%% ip-address@9.0.5 NOTICES AND INFORMATION BEGIN HERE %% ip-address@9.0.5 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
@ -1207,141 +969,11 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
========================================= =========================================
END OF smart-buffer@4.2.0 AND INFORMATION END OF smart-buffer@4.2.0 AND INFORMATION
%% socks-proxy-agent@6.1.1 NOTICES AND INFORMATION BEGIN HERE %% socks-proxy-agent@8.0.4 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
socks-proxy-agent
================
### A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS
[![Build Status](https://github.com/TooTallNate/node-socks-proxy-agent/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-socks-proxy-agent/actions?workflow=Node+CI)
This module provides an `http.Agent` implementation that connects to a
specified SOCKS proxy server, and can be used with the built-in `http`
and `https` modules.
It can also be used in conjunction with the `ws` module to establish a WebSocket
connection over a SOCKS proxy. See the "Examples" section below.
Installation
------------
Install with `npm`:
``` bash
$ npm install socks-proxy-agent
```
Examples
--------
#### TypeScript example
```ts
import https from 'https';
import { SocksProxyAgent } from 'socks-proxy-agent';
const info = {
host: 'br41.nordvpn.com',
userId: 'your-name@gmail.com',
password: 'abcdef12345124'
};
const agent = new SocksProxyAgent(info);
https.get('https://jsonip.org', { agent }, (res) => {
console.log(res.headers);
res.pipe(process.stdout);
});
```
#### `http` module example
```js
var url = require('url');
var http = require('http');
var SocksProxyAgent = require('socks-proxy-agent');
// SOCKS proxy to connect to
var proxy = process.env.socks_proxy || 'socks://127.0.0.1:1080';
console.log('using proxy server %j', proxy);
// HTTP endpoint for the proxy to connect to
var endpoint = process.argv[2] || 'http://nodejs.org/api/';
console.log('attempting to GET %j', endpoint);
var opts = url.parse(endpoint);
// create an instance of the `SocksProxyAgent` class with the proxy server information
var agent = new SocksProxyAgent(proxy);
opts.agent = agent;
http.get(opts, function (res) {
console.log('"response" event!', res.headers);
res.pipe(process.stdout);
});
```
#### `https` module example
```js
var url = require('url');
var https = require('https');
var SocksProxyAgent = require('socks-proxy-agent');
// SOCKS proxy to connect to
var proxy = process.env.socks_proxy || 'socks://127.0.0.1:1080';
console.log('using proxy server %j', proxy);
// HTTP endpoint for the proxy to connect to
var endpoint = process.argv[2] || 'https://encrypted.google.com/';
console.log('attempting to GET %j', endpoint);
var opts = url.parse(endpoint);
// create an instance of the `SocksProxyAgent` class with the proxy server information
var agent = new SocksProxyAgent(proxy);
opts.agent = agent;
https.get(opts, function (res) {
console.log('"response" event!', res.headers);
res.pipe(process.stdout);
});
```
#### `ws` WebSocket connection example
``` js
var WebSocket = require('ws');
var SocksProxyAgent = require('socks-proxy-agent');
// SOCKS proxy to connect to
var proxy = process.env.socks_proxy || 'socks://127.0.0.1:1080';
console.log('using proxy server %j', proxy);
// WebSocket endpoint for the proxy to connect to
var endpoint = process.argv[2] || 'ws://echo.websocket.org';
console.log('attempting to connect to WebSocket %j', endpoint);
// create an instance of the `SocksProxyAgent` class with the proxy server information
var agent = new SocksProxyAgent(proxy);
// initiate the WebSocket connection
var socket = new WebSocket(endpoint, { agent: agent });
socket.on('open', function () {
console.log('"open" event!');
socket.send('hello world');
});
socket.on('message', function (data, flags) {
console.log('"message" event! %j %j', data, flags);
socket.close();
});
```
License
-------
(The MIT License) (The MIT License)
Copyright (c) 2013 Nathan Rajlich &lt;nathan@tootallnate.net&gt; Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the
@ -1362,7 +994,7 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
========================================= =========================================
END OF socks-proxy-agent@6.1.1 AND INFORMATION END OF socks-proxy-agent@8.0.4 AND INFORMATION
%% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE %% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================

View file

@ -13,7 +13,7 @@
"debug": "^4.3.4", "debug": "^4.3.4",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"graceful-fs": "4.2.10", "graceful-fs": "4.2.10",
"https-proxy-agent": "5.0.0", "https-proxy-agent": "7.0.5",
"jpeg-js": "0.4.4", "jpeg-js": "0.4.4",
"mime": "^3.0.0", "mime": "^3.0.0",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
@ -23,7 +23,7 @@
"proxy-from-env": "1.1.0", "proxy-from-env": "1.1.0",
"retry": "0.12.0", "retry": "0.12.0",
"signal-exit": "3.0.7", "signal-exit": "3.0.7",
"socks-proxy-agent": "6.1.1", "socks-proxy-agent": "8.0.4",
"stack-utils": "2.0.5", "stack-utils": "2.0.5",
"ws": "8.17.1" "ws": "8.17.1"
}, },
@ -130,14 +130,15 @@
} }
}, },
"node_modules/agent-base": { "node_modules/agent-base": {
"version": "6.0.2", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"license": "MIT",
"dependencies": { "dependencies": {
"debug": "4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
"node": ">= 6.0.0" "node": ">= 14"
} }
}, },
"node_modules/balanced-match": { "node_modules/balanced-match": {
@ -224,15 +225,16 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
}, },
"node_modules/https-proxy-agent": { "node_modules/https-proxy-agent": {
"version": "5.0.0", "version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"license": "MIT",
"dependencies": { "dependencies": {
"agent-base": "6", "agent-base": "^7.0.2",
"debug": "4" "debug": "4"
}, },
"engines": { "engines": {
"node": ">= 6" "node": ">= 14"
} }
}, },
"node_modules/ip-address": { "node_modules/ip-address": {
@ -382,16 +384,17 @@
} }
}, },
"node_modules/socks-proxy-agent": { "node_modules/socks-proxy-agent": {
"version": "6.1.1", "version": "8.0.4",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
"integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
"license": "MIT",
"dependencies": { "dependencies": {
"agent-base": "^6.0.2", "agent-base": "^7.1.1",
"debug": "^4.3.1", "debug": "^4.3.4",
"socks": "^2.6.1" "socks": "^2.8.3"
}, },
"engines": { "engines": {
"node": ">= 10" "node": ">= 14"
} }
}, },
"node_modules/sprintf-js": { "node_modules/sprintf-js": {
@ -523,11 +526,11 @@
} }
}, },
"agent-base": { "agent-base": {
"version": "6.0.2", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"requires": { "requires": {
"debug": "4" "debug": "^4.3.4"
} }
}, },
"balanced-match": { "balanced-match": {
@ -588,11 +591,11 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
}, },
"https-proxy-agent": { "https-proxy-agent": {
"version": "5.0.0", "version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"requires": { "requires": {
"agent-base": "6", "agent-base": "^7.0.2",
"debug": "4" "debug": "4"
} }
}, },
@ -696,13 +699,13 @@
} }
}, },
"socks-proxy-agent": { "socks-proxy-agent": {
"version": "6.1.1", "version": "8.0.4",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
"integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
"requires": { "requires": {
"agent-base": "^6.0.2", "agent-base": "^7.1.1",
"debug": "^4.3.1", "debug": "^4.3.4",
"socks": "^2.6.1" "socks": "^2.8.3"
} }
}, },
"sprintf-js": { "sprintf-js": {

View file

@ -14,7 +14,7 @@
"debug": "^4.3.4", "debug": "^4.3.4",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"graceful-fs": "4.2.10", "graceful-fs": "4.2.10",
"https-proxy-agent": "5.0.0", "https-proxy-agent": "7.0.5",
"jpeg-js": "0.4.4", "jpeg-js": "0.4.4",
"mime": "^3.0.0", "mime": "^3.0.0",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
@ -24,7 +24,7 @@
"proxy-from-env": "1.1.0", "proxy-from-env": "1.1.0",
"retry": "0.12.0", "retry": "0.12.0",
"signal-exit": "3.0.7", "signal-exit": "3.0.7",
"socks-proxy-agent": "6.1.1", "socks-proxy-agent": "8.0.4",
"stack-utils": "2.0.5", "stack-utils": "2.0.5",
"ws": "8.17.1" "ws": "8.17.1"
}, },

View file

@ -292,7 +292,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}; };
} }
async _retryAction(progress: Progress, actionName: string, action: (retry: number) => Promise<PerformActionResult>, options: { trial?: boolean, force?: boolean, skipLocatorHandlersCheckpoint?: boolean }): Promise<'error:notconnected' | 'done'> { async _retryAction(progress: Progress, actionName: string, action: (retry: number) => Promise<PerformActionResult>, options: { trial?: boolean, force?: boolean, skipActionPreChecks?: boolean }): Promise<'error:notconnected' | 'done'> {
let retry = 0; let retry = 0;
// We progressively wait longer between retries, up to 500ms. // We progressively wait longer between retries, up to 500ms.
const waitTime = [0, 20, 100, 100, 500]; const waitTime = [0, 20, 100, 100, 500];
@ -310,8 +310,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} else { } else {
progress.log(`attempting ${actionName} action${options.trial ? ' (trial run)' : ''}`); progress.log(`attempting ${actionName} action${options.trial ? ' (trial run)' : ''}`);
} }
if (!options.skipLocatorHandlersCheckpoint && !options.force) if (!options.skipActionPreChecks && !options.force)
await this._frame._page.performLocatorHandlersCheckpoint(progress); await this._frame._page.performActionPreChecks(progress);
const result = await action(retry); const result = await action(retry);
++retry; ++retry;
if (result === 'error:notvisible') { if (result === 'error:notvisible') {
@ -346,7 +346,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async _retryPointerAction(progress: Progress, actionName: ActionName, waitForEnabled: boolean, action: (point: types.Point) => Promise<void>, async _retryPointerAction(progress: Progress, actionName: ActionName, waitForEnabled: boolean, action: (point: types.Point) => Promise<void>,
options: { waitAfter: boolean | 'disabled' } & types.PointerActionOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> { options: { waitAfter: boolean | 'disabled' } & types.PointerActionOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> {
// Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation. // Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation.
const skipLocatorHandlersCheckpoint = actionName === 'move and up'; const skipActionPreChecks = actionName === 'move and up';
return await this._retryAction(progress, actionName, async retry => { return await this._retryAction(progress, actionName, async retry => {
// By default, we scroll with protocol method to reveal the action point. // By default, we scroll with protocol method to reveal the action point.
// However, that might not work to scroll from under position:sticky elements // However, that might not work to scroll from under position:sticky elements
@ -360,7 +360,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
]; ];
const forceScrollOptions = scrollOptions[retry % scrollOptions.length]; const forceScrollOptions = scrollOptions[retry % scrollOptions.length];
return await this._performPointerAction(progress, actionName, waitForEnabled, action, forceScrollOptions, options); return await this._performPointerAction(progress, actionName, waitForEnabled, action, forceScrollOptions, options);
}, { ...options, skipLocatorHandlersCheckpoint }); }, { ...options, skipActionPreChecks });
} }
async _performPointerAction( async _performPointerAction(

View file

@ -20,7 +20,6 @@ import http from 'http';
import https from 'https'; import https from 'https';
import type { Readable, TransformCallback } from 'stream'; import type { Readable, TransformCallback } from 'stream';
import { pipeline, Transform } from 'stream'; import { pipeline, Transform } from 'stream';
import url from 'url';
import zlib from 'zlib'; import zlib from 'zlib';
import type { HTTPCredentials } from '../../types/types'; import type { HTTPCredentials } from '../../types/types';
import { TimeoutSettings } from '../common/timeoutSettings'; import { TimeoutSettings } from '../common/timeoutSettings';
@ -493,12 +492,12 @@ export abstract class APIRequestContext extends SdkObject {
// happy eyeballs don't emit lookup and connect events, so we use our custom ones // happy eyeballs don't emit lookup and connect events, so we use our custom ones
const happyEyeBallsTimings = timingForSocket(socket); const happyEyeBallsTimings = timingForSocket(socket);
dnsLookupAt = happyEyeBallsTimings.dnsLookupAt; dnsLookupAt = happyEyeBallsTimings.dnsLookupAt;
tcpConnectionAt = happyEyeBallsTimings.tcpConnectionAt; tcpConnectionAt ??= happyEyeBallsTimings.tcpConnectionAt;
// non-happy-eyeballs sockets // non-happy-eyeballs sockets
listeners.push( listeners.push(
eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }), eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }),
eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt = monotonicTime(); }), eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt ??= monotonicTime(); }),
eventsHelper.addEventListener(socket, 'secureConnect', () => { eventsHelper.addEventListener(socket, 'secureConnect', () => {
tlsHandshakeAt = monotonicTime(); tlsHandshakeAt = monotonicTime();
@ -515,11 +514,21 @@ export abstract class APIRequestContext extends SdkObject {
}), }),
); );
// when using socks proxy, having the socket means the connection got established
if (agent instanceof SocksProxyAgent)
tcpConnectionAt ??= monotonicTime();
serverIPAddress = socket.remoteAddress; serverIPAddress = socket.remoteAddress;
serverPort = socket.remotePort; serverPort = socket.remotePort;
}); });
request.on('finish', () => { requestFinishAt = monotonicTime(); }); request.on('finish', () => { requestFinishAt = monotonicTime(); });
// http proxy
request.on('proxyConnect', () => {
tcpConnectionAt ??= monotonicTime();
});
progress.log(`${options.method} ${url.toString()}`); progress.log(`${options.method} ${url.toString()}`);
if (options.headers) { if (options.headers) {
for (const [name, value] of Object.entries(options.headers)) for (const [name, value] of Object.entries(options.headers))
@ -686,17 +695,16 @@ export class GlobalAPIRequestContext extends APIRequestContext {
} }
export function createProxyAgent(proxy: types.ProxySettings) { export function createProxyAgent(proxy: types.ProxySettings) {
const proxyOpts = url.parse(proxy.server); const proxyURL = new URL(proxy.server);
if (proxyOpts.protocol?.startsWith('socks')) { if (proxyURL.protocol?.startsWith('socks'))
return new SocksProxyAgent({ return new SocksProxyAgent(proxyURL);
host: proxyOpts.hostname,
port: proxyOpts.port || undefined,
});
}
if (proxy.username) if (proxy.username)
proxyOpts.auth = `${proxy.username}:${proxy.password || ''}`; proxyURL.username = proxy.username;
// TODO: We should use HttpProxyAgent conditional on proxyOpts.protocol instead of always using CONNECT method. if (proxy.password)
return new HttpsProxyAgent(proxyOpts); proxyURL.password = proxy.password;
// TODO: We should use HttpProxyAgent conditional on proxyURL.protocol instead of always using CONNECT method.
return new HttpsProxyAgent(proxyURL);
} }
function toHeadersArray(rawHeaders: string[]): types.HeadersArray { function toHeadersArray(rawHeaders: string[]): types.HeadersArray {

View file

@ -248,6 +248,11 @@ export class FrameManager {
const frame = this._frames.get(frameId); const frame = this._frames.get(frameId);
if (!frame) if (!frame)
return; return;
const pending = frame.pendingDocument();
if (pending && pending.documentId === undefined && pending.request === undefined) {
// WebKit has notified about the same-document navigation being requested, so clear it.
frame.setPendingDocument(undefined);
}
frame._url = url; frame._url = url;
const navigationEvent: NavigationEvent = { url, name: frame._name, isPublic: true }; const navigationEvent: NavigationEvent = { url, name: frame._name, isPublic: true };
this._fireInternalFrameNavigation(frame, navigationEvent); this._fireInternalFrameNavigation(frame, navigationEvent);
@ -786,11 +791,11 @@ export class Frame extends SdkObject {
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async waitForSelectorInternal(progress: Progress, selector: string, performLocatorHandlersCheckpoint: boolean, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> { async waitForSelectorInternal(progress: Progress, selector: string, performActionPreChecks: boolean, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
const { state = 'visible' } = options; const { state = 'visible' } = options;
const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
if (performLocatorHandlersCheckpoint) if (performActionPreChecks)
await this._page.performLocatorHandlersCheckpoint(progress); await this._page.performActionPreChecks(progress);
const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope); const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope);
progress.throwIfAborted(); progress.throwIfAborted();
@ -800,6 +805,8 @@ export class Frame extends SdkObject {
return continuePolling; return continuePolling;
} }
const result = await resolved.injected.evaluateHandle((injected, { info, root }) => { const result = await resolved.injected.evaluateHandle((injected, { info, root }) => {
if (root && !root.isConnected)
throw injected.createStacklessError('Element is not attached to the DOM');
const elements = injected.querySelectorAll(info.parsed, root || document); const elements = injected.querySelectorAll(info.parsed, root || document);
const element: Element | undefined = elements[0]; const element: Element | undefined = elements[0];
const visible = element ? injected.utils.isElementVisible(element) : false; const visible = element ? injected.utils.isElementVisible(element) : false;
@ -1113,12 +1120,12 @@ export class Frame extends SdkObject {
progress: Progress, progress: Progress,
selector: string, selector: string,
strict: boolean | undefined, strict: boolean | undefined,
performLocatorHandlersCheckpoint: boolean, performActionPreChecks: boolean,
action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> { action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
progress.log(`waiting for ${this._asLocator(selector)}`); progress.log(`waiting for ${this._asLocator(selector)}`);
return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
if (performLocatorHandlersCheckpoint) if (performActionPreChecks)
await this._page.performLocatorHandlersCheckpoint(progress); await this._page.performActionPreChecks(progress);
const resolved = await this.selectors.resolveInjectedForSelector(selector, { strict }); const resolved = await this.selectors.resolveInjectedForSelector(selector, { strict });
progress.throwIfAborted(); progress.throwIfAborted();
@ -1162,7 +1169,7 @@ export class Frame extends SdkObject {
} }
async rafrafTimeoutScreenshotElementWithProgress(progress: Progress, selector: string, timeout: number, options: ScreenshotOptions): Promise<Buffer> { async rafrafTimeoutScreenshotElementWithProgress(progress: Progress, selector: string, timeout: number, options: ScreenshotOptions): Promise<Buffer> {
return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, true /* performLocatorHandlersCheckpoint */, async handle => { return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, true /* performActionPreChecks */, async handle => {
await handle._frame.rafrafTimeout(timeout); await handle._frame.rafrafTimeout(timeout);
return await this._page._screenshotter.screenshotElement(progress, handle, options); return await this._page._screenshotter.screenshotElement(progress, handle, options);
}); });
@ -1171,21 +1178,21 @@ export class Frame extends SdkObject {
async click(metadata: CallMetadata, selector: string, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions) { async click(metadata: CallMetadata, selector: string, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._click(progress, { ...options, waitAfter: !options.noWaitAfter }))); return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performActionPreChecks */, handle => handle._click(progress, { ...options, waitAfter: !options.noWaitAfter })));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async dblclick(metadata: CallMetadata, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions = {}) { async dblclick(metadata: CallMetadata, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._dblclick(progress, options))); return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performActionPreChecks */, handle => handle._dblclick(progress, options)));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async dragAndDrop(metadata: CallMetadata, source: string, target: string, options: types.DragActionOptions & types.PointerActionWaitOptions = {}) { async dragAndDrop(metadata: CallMetadata, source: string, target: string, options: types.DragActionOptions & types.PointerActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
await controller.run(async progress => { await controller.run(async progress => {
dom.assertDone(await this._retryWithProgressIfNotConnected(progress, source, options.strict, !options.force /* performLocatorHandlersCheckpoint */, async handle => { dom.assertDone(await this._retryWithProgressIfNotConnected(progress, source, options.strict, !options.force /* performActionPreChecks */, async handle => {
return handle._retryPointerAction(progress, 'move and down', false, async point => { return handle._retryPointerAction(progress, 'move and down', false, async point => {
await this._page.mouse.move(point.x, point.y); await this._page.mouse.move(point.x, point.y);
await this._page.mouse.down(); await this._page.mouse.down();
@ -1197,7 +1204,7 @@ export class Frame extends SdkObject {
}); });
})); }));
// Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation. // Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation.
dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, options.strict, false /* performLocatorHandlersCheckpoint */, async handle => { dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, options.strict, false /* performActionPreChecks */, async handle => {
return handle._retryPointerAction(progress, 'move and up', false, async point => { return handle._retryPointerAction(progress, 'move and up', false, async point => {
await this._page.mouse.move(point.x, point.y); await this._page.mouse.move(point.x, point.y);
await this._page.mouse.up(); await this._page.mouse.up();
@ -1216,28 +1223,28 @@ export class Frame extends SdkObject {
throw new Error('The page does not support tap. Use hasTouch context option to enable touch support.'); throw new Error('The page does not support tap. Use hasTouch context option to enable touch support.');
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._tap(progress, options))); return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performActionPreChecks */, handle => handle._tap(progress, options)));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async fill(metadata: CallMetadata, selector: string, value: string, options: types.TimeoutOptions & types.StrictOptions & { force?: boolean }) { async fill(metadata: CallMetadata, selector: string, value: string, options: types.TimeoutOptions & types.StrictOptions & { force?: boolean }) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._fill(progress, value, options))); return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performActionPreChecks */, handle => handle._fill(progress, value, options)));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async focus(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}) { async focus(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
await controller.run(async progress => { await controller.run(async progress => {
dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, true /* performLocatorHandlersCheckpoint */, handle => handle._focus(progress))); dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, true /* performActionPreChecks */, handle => handle._focus(progress)));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async blur(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}) { async blur(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
await controller.run(async progress => { await controller.run(async progress => {
dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, true /* performLocatorHandlersCheckpoint */, handle => handle._blur(progress))); dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, true /* performActionPreChecks */, handle => handle._blur(progress)));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
@ -1344,14 +1351,14 @@ export class Frame extends SdkObject {
async hover(metadata: CallMetadata, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) { async hover(metadata: CallMetadata, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._hover(progress, options))); return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performActionPreChecks */, handle => handle._hover(progress, options)));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions = {}): Promise<string[]> { async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions = {}): Promise<string[]> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._selectOption(progress, elements, values, options)); return await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performActionPreChecks */, handle => handle._selectOption(progress, elements, values, options));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
@ -1359,35 +1366,35 @@ export class Frame extends SdkObject {
const inputFileItems = await prepareFilesForUpload(this, params); const inputFileItems = await prepareFilesForUpload(this, params);
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, params.strict, true /* performLocatorHandlersCheckpoint */, handle => handle._setInputFiles(progress, inputFileItems))); return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, params.strict, true /* performActionPreChecks */, handle => handle._setInputFiles(progress, inputFileItems)));
}, this._page._timeoutSettings.timeout(params)); }, this._page._timeoutSettings.timeout(params));
} }
async type(metadata: CallMetadata, selector: string, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions = {}) { async type(metadata: CallMetadata, selector: string, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions = {}) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, true /* performLocatorHandlersCheckpoint */, handle => handle._type(progress, text, options))); return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, true /* performActionPreChecks */, handle => handle._type(progress, text, options)));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async press(metadata: CallMetadata, selector: string, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions = {}) { async press(metadata: CallMetadata, selector: string, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions = {}) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, true /* performLocatorHandlersCheckpoint */, handle => handle._press(progress, key, options))); return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, true /* performActionPreChecks */, handle => handle._press(progress, key, options)));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async check(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions = {}) { async check(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._setChecked(progress, true, options))); return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performActionPreChecks */, handle => handle._setChecked(progress, true, options)));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async uncheck(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions = {}) { async uncheck(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._setChecked(progress, false, options))); return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performActionPreChecks */, handle => handle._setChecked(progress, false, options)));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
@ -1416,7 +1423,7 @@ export class Frame extends SdkObject {
await (new ProgressController(metadata, this)).run(async progress => { await (new ProgressController(metadata, this)).run(async progress => {
progress.log(`${metadata.apiName}${timeout ? ` with timeout ${timeout}ms` : ''}`); progress.log(`${metadata.apiName}${timeout ? ` with timeout ${timeout}ms` : ''}`);
progress.log(`waiting for ${this._asLocator(selector)}`); progress.log(`waiting for ${this._asLocator(selector)}`);
await this._page.performLocatorHandlersCheckpoint(progress); await this._page.performActionPreChecks(progress);
}, timeout); }, timeout);
// Step 2: perform one-shot expect check without a timeout. // Step 2: perform one-shot expect check without a timeout.
@ -1443,7 +1450,7 @@ export class Frame extends SdkObject {
// Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time. // Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time.
return await (new ProgressController(metadata, this)).run(async progress => { return await (new ProgressController(metadata, this)).run(async progress => {
return await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => { return await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => {
await this._page.performLocatorHandlersCheckpoint(progress); await this._page.performActionPreChecks(progress);
const { matches, received } = await this._expectInternal(progress, selector, options, lastIntermediateResult); const { matches, received } = await this._expectInternal(progress, selector, options, lastIntermediateResult);
if (matches === options.isNot) { if (matches === options.isNot) {
// Keep waiting in these cases: // Keep waiting in these cases:

View file

@ -31,7 +31,7 @@ import * as accessibility from './accessibility';
import { FileChooser } from './fileChooser'; import { FileChooser } from './fileChooser';
import type { Progress } from './progress'; import type { Progress } from './progress';
import { ProgressController } from './progress'; import { ProgressController } from './progress';
import { LongStandingScope, assert, createGuid } from '../utils'; import { LongStandingScope, assert, createGuid, trimStringWithEllipsis } from '../utils';
import { ManualPromise } from '../utils/manualPromise'; import { ManualPromise } from '../utils/manualPromise';
import { debugLogger } from '../utils/debugLogger'; import { debugLogger } from '../utils/debugLogger';
import type { ImageComparatorOptions } from '../utils/comparators'; import type { ImageComparatorOptions } from '../utils/comparators';
@ -45,6 +45,7 @@ import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSe
import type { SerializedValue } from './isomorphic/utilityScriptSerializers'; import type { SerializedValue } from './isomorphic/utilityScriptSerializers';
import { TargetClosedError } from './errors'; import { TargetClosedError } from './errors';
import { asLocator } from '../utils'; import { asLocator } from '../utils';
import { helper } from './helper';
export interface PageDelegate { export interface PageDelegate {
readonly rawMouse: input.RawMouse; readonly rawMouse: input.RawMouse;
@ -455,7 +456,34 @@ export class Page extends SdkObject {
this._locatorHandlers.delete(uid); this._locatorHandlers.delete(uid);
} }
async performLocatorHandlersCheckpoint(progress: Progress) { async performActionPreChecks(progress: Progress) {
await this._performWaitForNavigationCheck(progress);
progress.throwIfAborted();
await this._performLocatorHandlersCheckpoint(progress);
progress.throwIfAborted();
// Wait once again, just in case a locator handler caused a navigation.
await this._performWaitForNavigationCheck(progress);
}
private async _performWaitForNavigationCheck(progress: Progress) {
if (process.env.PLAYWRIGHT_SKIP_NAVIGATION_CHECK)
return;
const mainFrame = this._frameManager.mainFrame();
if (!mainFrame || !mainFrame.pendingDocument())
return;
const url = mainFrame.pendingDocument()?.request?.url();
const toUrl = url ? `" ${trimStringWithEllipsis(url, 200)}"` : '';
progress.log(` waiting for${toUrl} navigation to finish...`);
await helper.waitForEvent(progress, mainFrame, frames.Frame.Events.InternalNavigation, (e: frames.NavigationEvent) => {
if (!e.isPublic)
return false;
if (!e.error)
progress.log(` navigated to "${trimStringWithEllipsis(mainFrame.url(), 200)}"`);
return true;
}).promise;
}
private async _performLocatorHandlersCheckpoint(progress: Progress) {
// Do not run locator handlers from inside locator handler callbacks to avoid deadlocks. // Do not run locator handlers from inside locator handler callbacks to avoid deadlocks.
if (this._locatorHandlerRunningCounter) if (this._locatorHandlerRunningCounter)
return; return;
@ -559,7 +587,7 @@ export class Page extends SdkObject {
const rafrafScreenshot = locator ? async (progress: Progress, timeout: number) => { const rafrafScreenshot = locator ? async (progress: Progress, timeout: number) => {
return await locator.frame.rafrafTimeoutScreenshotElementWithProgress(progress, locator.selector, timeout, options || {}); return await locator.frame.rafrafTimeoutScreenshotElementWithProgress(progress, locator.selector, timeout, options || {});
} : async (progress: Progress, timeout: number) => { } : async (progress: Progress, timeout: number) => {
await this.performLocatorHandlersCheckpoint(progress); await this.performActionPreChecks(progress);
await this.mainFrame().rafrafTimeout(timeout); await this.mainFrame().rafrafTimeout(timeout);
return await this._screenshotter.screenshotPage(progress, options || {}); return await this._screenshotter.screenshotPage(progress, options || {});
}; };

View file

@ -98,7 +98,7 @@ class SocksProxyConnection {
async connect() { async connect() {
if (this.socksProxy.proxyAgentFromOptions) if (this.socksProxy.proxyAgentFromOptions)
this.target = await this.socksProxy.proxyAgentFromOptions.callback(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false }); this.target = await this.socksProxy.proxyAgentFromOptions.connect(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false });
else else
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port); this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);

View file

@ -50,7 +50,7 @@ export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.Inco
const proxyURL = getProxyForUrl(params.url); const proxyURL = getProxyForUrl(params.url);
if (proxyURL) { if (proxyURL) {
const parsedProxyURL = url.parse(proxyURL); const parsedProxyURL = new URL(proxyURL);
if (params.url.startsWith('http:')) { if (params.url.startsWith('http:')) {
options = { options = {
path: parsedUrl.href, path: parsedUrl.href,

View file

@ -2553,16 +2553,15 @@ export interface Page {
* // → true * // → true
* await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches); * await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches);
* // → false * // → false
* await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches);
* // → false
* ``` * ```
* *
* @param options * @param options
*/ */
emulateMedia(options?: { emulateMedia(options?: {
/** /**
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. * Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
* Passing `null` disables color scheme emulation. * media feature, supported values are `'light'` and `'dark'`. Passing `null` disables color scheme emulation.
* `'no-preference'` is deprecated.
*/ */
colorScheme?: null|"light"|"dark"|"no-preference"; colorScheme?: null|"light"|"dark"|"no-preference";
@ -9761,7 +9760,8 @@ export interface Browser {
}>; }>;
/** /**
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See * Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
* media feature, supported values are `'light'` and `'dark'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
* Passing `null` resets emulation to system defaults. Defaults to `'light'`. * Passing `null` resets emulation to system defaults. Defaults to `'light'`.
*/ */
@ -14726,7 +14726,8 @@ export interface BrowserType<Unused = {}> {
}>; }>;
/** /**
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See * Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
* media feature, supported values are `'light'` and `'dark'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
* Passing `null` resets emulation to system defaults. Defaults to `'light'`. * Passing `null` resets emulation to system defaults. Defaults to `'light'`.
*/ */
@ -16522,7 +16523,8 @@ export interface AndroidDevice {
bypassCSP?: boolean; bypassCSP?: boolean;
/** /**
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See * Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
* media feature, supported values are `'light'` and `'dark'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
* Passing `null` resets emulation to system defaults. Defaults to `'light'`. * Passing `null` resets emulation to system defaults. Defaults to `'light'`.
*/ */
@ -18542,6 +18544,10 @@ export interface Clock {
/** /**
* Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running. * Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running.
* *
* Use this method for simple scenarios where you only need to test with a predefined time. For more advanced
* scenarios, use [clock.install([options])](https://playwright.dev/docs/api/class-clock#clock-install) instead. Read
* docs on [clock emulation](https://playwright.dev/docs/clock) to learn more.
*
* **Usage** * **Usage**
* *
* ```js * ```js
@ -18555,7 +18561,8 @@ export interface Clock {
setFixedTime(time: number|string|Date): Promise<void>; setFixedTime(time: number|string|Date): Promise<void>;
/** /**
* Sets current system time but does not trigger any timers. * Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for
* example switching from summer to winter time, or changing time zones.
* *
* **Usage** * **Usage**
* *
@ -18998,7 +19005,8 @@ export interface Electron {
bypassCSP?: boolean; bypassCSP?: boolean;
/** /**
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See * Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
* media feature, supported values are `'light'` and `'dark'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
* Passing `null` resets emulation to system defaults. Defaults to `'light'`. * Passing `null` resets emulation to system defaults. Defaults to `'light'`.
*/ */
@ -21820,7 +21828,8 @@ export interface BrowserContextOptions {
}>; }>;
/** /**
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See * Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
* media feature, supported values are `'light'` and `'dark'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
* Passing `null` resets emulation to system defaults. Defaults to `'light'`. * Passing `null` resets emulation to system defaults. Defaults to `'light'`.
*/ */

View file

@ -188,7 +188,7 @@ function __pwWrapFunctions(slots) {
for (const [key, value] of Object.entries(slots || {})) for (const [key, value] of Object.entries(slots || {}))
slotsWithRenderFunctions[key] = () => [value]; slotsWithRenderFunctions[key] = () => [value];
} else if (slots?.length) { } else if (slots?.length) {
slots['default'] = () => slots; slotsWithRenderFunctions['default'] = () => slots;
} }
return slotsWithRenderFunctions; return slotsWithRenderFunctions;
} }

View file

@ -5225,7 +5225,8 @@ export interface PlaywrightTestOptions {
*/ */
bypassCSP: boolean; bypassCSP: boolean;
/** /**
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See * Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
* media feature, supported values are `'light'` and `'dark'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
* Passing `null` resets emulation to system defaults. Defaults to `'light'`. * Passing `null` resets emulation to system defaults. Defaults to `'light'`.
* *

View file

@ -0,0 +1,3 @@
<template>
<slot>default value</slot>
</template>

View file

@ -2,6 +2,7 @@ import { test, expect } from '@playwright/experimental-ct-vue';
import DefaultSlot from '@/components/DefaultSlot.vue'; import DefaultSlot from '@/components/DefaultSlot.vue';
import NamedSlots from '@/components/NamedSlots.vue'; import NamedSlots from '@/components/NamedSlots.vue';
import Button from '@/components/Button.vue'; import Button from '@/components/Button.vue';
import SlotDefaultValue from "@/components/SlotDefaultValue.vue";
test('render a default slot', async ({ mount }) => { test('render a default slot', async ({ mount }) => {
const component = await mount(DefaultSlot, { const component = await mount(DefaultSlot, {
@ -49,3 +50,13 @@ test('render a component with a named slot', async ({ mount }) => {
await expect(component).toContainText('Main Content'); await expect(component).toContainText('Main Content');
await expect(component).toContainText('Footer'); await expect(component).toContainText('Footer');
}); });
test('updating default slot should work', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32809' } }, async ({ mount }) => {
const slots = { default: 'foo' };
const component = await mount(SlotDefaultValue, { slots });
await expect(component).toHaveText('foo');
await component.update({ slots });
await expect(component).toHaveText('foo');
});

View file

@ -2,6 +2,7 @@ import { test, expect } from '@playwright/experimental-ct-vue';
import DefaultSlot from '@/components/DefaultSlot.vue'; import DefaultSlot from '@/components/DefaultSlot.vue';
import NamedSlots from '@/components/NamedSlots.vue'; import NamedSlots from '@/components/NamedSlots.vue';
import Button from '@/components/Button.vue'; import Button from '@/components/Button.vue';
import SlotDefaultValue from "@/components/SlotDefaultValue.vue";
test('render a default slot', async ({ mount }) => { test('render a default slot', async ({ mount }) => {
const component = await mount(DefaultSlot, { const component = await mount(DefaultSlot, {
@ -49,3 +50,13 @@ test('render a component with a named slot', async ({ mount }) => {
await expect(component).toContainText('Main Content'); await expect(component).toContainText('Main Content');
await expect(component).toContainText('Footer'); await expect(component).toContainText('Footer');
}); });
test('updating default slot should work', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32809' } }, async ({ mount }) => {
const slots = { default: 'foo' };
const component = await mount(SlotDefaultValue, { slots });
await expect(component).toHaveText('foo');
await component.update({ slots });
await expect(component).toHaveText('foo');
});

View file

@ -61,7 +61,7 @@ export function expectedSSLError(browserName: string, platform: string): RegExp
else if (platform === 'win32') else if (platform === 'win32')
return /SSL peer certificate or SSH remote key was not OK/; return /SSL peer certificate or SSH remote key was not OK/;
else else
return /Unacceptable TLS certificate/; return /Unacceptable TLS certificate|Operation was cancelled/;
} }
return /SSL_ERROR_UNKNOWN/; return /SSL_ERROR_UNKNOWN/;
} }

View file

@ -254,7 +254,7 @@ it('should be able to navigate after disabling javascript', async ({ browser, se
}); });
it('should not hang on promises after disabling javascript', async ({ browserName, contextFactory }) => { it('should not hang on promises after disabling javascript', async ({ browserName, contextFactory }) => {
it.fixme(browserName === 'webkit' || browserName === 'firefox'); it.fixme(browserName === 'firefox');
const context = await contextFactory({ javaScriptEnabled: false }); const context = await contextFactory({ javaScriptEnabled: false });
const page = await context.newPage(); const page = await context.newPage();
expect(await page.evaluate(() => 1)).toBe(1); expect(await page.evaluate(() => 1)).toBe(1);

View file

@ -406,7 +406,6 @@ it('should support requestStorageAccess', async ({ page, server, channel, browse
it('should parse cookie with large Max-Age correctly', async ({ server, page, defaultSameSiteCookieValue, browserName, platform }) => { it('should parse cookie with large Max-Age correctly', async ({ server, page, defaultSameSiteCookieValue, browserName, platform }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30305' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30305' });
it.fixme(browserName === 'webkit' && platform === 'linux', 'https://github.com/microsoft/playwright/issues/30305');
server.setRoute('/foobar', (req, res) => { server.setRoute('/foobar', (req, res) => {
res.setHeader('set-cookie', [ res.setHeader('set-cookie', [

View file

@ -171,8 +171,6 @@ it('should work with Shift-clicking', async ({ browser, server, browserName }) =
}); });
it('should work with Ctrl-clicking', async ({ browser, server, browserName }) => { it('should work with Ctrl-clicking', async ({ browser, server, browserName }) => {
it.fixme(browserName === 'firefox', 'Reports an opener in this case.');
const context = await browser.newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
@ -181,22 +179,6 @@ it('should work with Ctrl-clicking', async ({ browser, server, browserName }) =>
context.waitForEvent('page'), context.waitForEvent('page'),
page.click('a', { modifiers: ['ControlOrMeta'] }), page.click('a', { modifiers: ['ControlOrMeta'] }),
]); ]);
expect(await popup.opener()).toBe(null); expect(await popup.opener()).toBe(browserName === 'firefox' ? page : null);
await context.close(); await context.close();
}); });
it('should not hang on ctrl-click during provisional load', async ({ context, page, server, isWindows, browserName, isLinux }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/11595' });
it.skip(browserName === 'chromium', 'Chromium does not dispatch renderer messages while navigation is provisional.');
it.fixme(browserName === 'webkit' && isWindows, 'Timesout while trying to click');
it.fixme(browserName === 'webkit' && isLinux, 'Timesout while trying to click');
await page.goto(server.EMPTY_PAGE);
await page.setContent('<a href="/one-style.html">yo</a>');
server.setRoute('/slow.html', () => {});
const [popup] = await Promise.all([
context.waitForEvent('page'),
server.waitForRequest('/slow.html').then(() => page.click('a', { modifiers: ['ControlOrMeta'] })),
page.evaluate(url => setTimeout(() => location.href = url, 0), server.CROSS_PROCESS_PREFIX + '/slow.html'),
]);
expect(popup).toBeTruthy();
});

View file

@ -65,7 +65,9 @@ it('should use proxy', async ({ contextFactory, server, proxyServer }) => {
}); });
it('should set cookie for top-level domain', async ({ contextFactory, server, proxyServer, browserName, isLinux }) => { it('should set cookie for top-level domain', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/18362' }
}, async ({ contextFactory, server, proxyServer, browserName, isLinux }) => {
it.fixme(browserName === 'webkit' && isLinux); it.fixme(browserName === 'webkit' && isLinux);
proxyServer.forwardTo(server.PORT, { allowConnectRequests: true }); proxyServer.forwardTo(server.PORT, { allowConnectRequests: true });

View file

@ -139,7 +139,9 @@ browserTest('should report null viewportSize when given null viewport', async ({
await context.close(); await context.close();
}); });
browserTest('should drag with high dpi', async ({ browser, server }) => { browserTest('should drag with high dpi', async ({ browser, server, headless }) => {
browserTest.fixme(!headless, 'Flaky on all browser in headed');
const page = await browser.newPage({ deviceScaleFactor: 2 }); const page = await browser.newPage({ deviceScaleFactor: 2 });
await page.goto(server.PREFIX + '/drag-n-drop.html'); await page.goto(server.PREFIX + '/drag-n-drop.html');
await page.hover('#source'); await page.hover('#source');

View file

@ -761,7 +761,6 @@ for (const kind of ['launchServer', 'run-server'] as const) {
}); });
test.describe('socks proxy', () => { test.describe('socks proxy', () => {
test.fixme(({ platform, browserName }) => browserName === 'webkit' && platform === 'win32');
test.skip(({ mode }) => mode !== 'default'); test.skip(({ mode }) => mode !== 'default');
test.skip(kind === 'launchServer', 'not supported yet'); test.skip(kind === 'launchServer', 'not supported yet');

View file

@ -65,13 +65,9 @@ it('should respect CSP @smoke', async ({ page, server }) => {
expect(await page.evaluate(() => window['testStatus'])).toBe('SUCCESS'); expect(await page.evaluate(() => window['testStatus'])).toBe('SUCCESS');
}); });
it('should play video @smoke', async ({ page, asset, browserName, platform, macVersion, mode }) => { it('should play video @smoke', async ({ page, asset, browserName, isWindows, isLinux, mode }) => {
// TODO: the test passes on Windows locally but fails on GitHub Action bot, it.skip(browserName === 'webkit' && isWindows, 'passes locally but fails on GitHub Action bot, apparently due to a Media Pack issue in the Windows Server');
// apparently due to a Media Pack issue in the Windows Server. it.fixme(browserName === 'firefox' && isLinux, 'https://github.com/microsoft/playwright/issues/5721');
// Also the test is very flaky on Linux WebKit.
it.fixme(browserName === 'webkit' && platform !== 'darwin');
it.fixme(browserName === 'firefox', 'https://github.com/microsoft/playwright/issues/5721');
it.fixme(browserName === 'webkit' && platform === 'darwin' && macVersion === 11, 'Does not work on BigSur');
it.skip(mode.startsWith('service')); it.skip(mode.startsWith('service'));
// Safari only plays mp4 so we test WebKit with an .mp4 clip. // Safari only plays mp4 so we test WebKit with an .mp4 clip.
@ -85,8 +81,7 @@ it('should play video @smoke', async ({ page, asset, browserName, platform, macV
}); });
it('should play webm video @smoke', async ({ page, asset, browserName, platform, macVersion, mode }) => { it('should play webm video @smoke', async ({ page, asset, browserName, platform, macVersion, mode }) => {
it.fixme(browserName === 'webkit' && platform === 'darwin' && macVersion === 11, 'Does not work on BigSur'); it.skip(browserName === 'webkit' && platform === 'win32', 'not supported');
it.fixme(browserName === 'webkit' && platform === 'win32');
it.skip(mode.startsWith('service')); it.skip(mode.startsWith('service'));
const absolutePath = asset('video_webm.html'); const absolutePath = asset('video_webm.html');
@ -98,8 +93,6 @@ it('should play webm video @smoke', async ({ page, asset, browserName, platform,
}); });
it('should play audio @smoke', async ({ page, server, browserName, platform }) => { it('should play audio @smoke', async ({ page, server, browserName, platform }) => {
it.fixme(browserName === 'firefox' && platform === 'win32', 'https://github.com/microsoft/playwright/issues/10887');
it.fixme(browserName === 'firefox' && platform === 'linux', 'https://github.com/microsoft/playwright/issues/10887');
it.fixme(browserName === 'webkit' && platform === 'win32', 'https://github.com/microsoft/playwright/issues/10892'); it.fixme(browserName === 'webkit' && platform === 'win32', 'https://github.com/microsoft/playwright/issues/10892');
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.setContent(`<audio src="${server.PREFIX}/example.mp3"></audio>`); await page.setContent(`<audio src="${server.PREFIX}/example.mp3"></audio>`);
@ -133,7 +126,6 @@ it('should support webgl 2 @smoke', async ({ page, browserName, headless, isWind
it('should not crash on page with mp4 @smoke', async ({ page, server, platform, browserName }) => { it('should not crash on page with mp4 @smoke', async ({ page, server, platform, browserName }) => {
it.fixme(browserName === 'webkit' && platform === 'win32', 'https://github.com/microsoft/playwright/issues/11009, times out in setContent'); it.fixme(browserName === 'webkit' && platform === 'win32', 'https://github.com/microsoft/playwright/issues/11009, times out in setContent');
it.fixme(browserName === 'firefox', 'https://bugzilla.mozilla.org/show_bug.cgi?id=1697004');
await page.setContent(`<video><source src="${server.PREFIX}/movie.mp4"/></video>`); await page.setContent(`<video><source src="${server.PREFIX}/movie.mp4"/></video>`);
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
}); });
@ -261,7 +253,6 @@ it('window.GestureEvent in WebKit', async ({ page, server, browserName }) => {
it('requestFullscreen', async ({ page, server, browserName, headless, isLinux }) => { it('requestFullscreen', async ({ page, server, browserName, headless, isLinux }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22832' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22832' });
it.fixme(browserName === 'chromium' && headless, 'fullscreenchange is not fired in headless Chromium');
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => { await page.evaluate(() => {
const result = new Promise(resolve => document.addEventListener('fullscreenchange', resolve)); const result = new Promise(resolve => document.addEventListener('fullscreenchange', resolve));

View file

@ -728,8 +728,7 @@ test.describe('browser', () => {
}); });
test('should return target connection errors when using http2', async ({ browser, startCCServer, asset, browserName, isMac, isLinux }) => { test('should return target connection errors when using http2', async ({ browser, startCCServer, asset, browserName, isMac, isLinux }) => {
test.skip(browserName === 'webkit' && isMac, 'WebKit on macOS doesn\n proxy localhost'); test.skip(browserName === 'webkit' && isMac, 'WebKit on macOS does not proxy localhost');
test.fixme(browserName === 'webkit' && isLinux, 'WebKit on Linux does not support http2 https://bugs.webkit.org/show_bug.cgi?id=276990');
test.skip(+process.versions.node.split('.')[0] < 20, 'http2.performServerHandshake is not supported in older Node.js versions'); test.skip(+process.versions.node.split('.')[0] < 20, 'http2.performServerHandshake is not supported in older Node.js versions');
const serverURL = await startCCServer({ http2: true }); const serverURL = await startCCServer({ http2: true });

View file

@ -1155,7 +1155,7 @@ it.describe('stubTimers', () => {
}); });
}); });
it.fixme('deletes global property on uninstall if it was inherited onto the global object', ({}) => { it('restores global property on uninstall if it was inherited onto the global object', ({}) => {
// Give the global object an inherited 'setTimeout' method // Give the global object an inherited 'setTimeout' method
const proto = { Date, const proto = { Date,
setTimeout: () => {}, setTimeout: () => {},
@ -1167,8 +1167,10 @@ it.describe('stubTimers', () => {
const { clock } = rawInstall(myGlobal, { now: 0, toFake: ['setTimeout'] }); const { clock } = rawInstall(myGlobal, { now: 0, toFake: ['setTimeout'] });
expect(myGlobal.hasOwnProperty('setTimeout')).toBeTruthy(); expect(myGlobal.hasOwnProperty('setTimeout')).toBeTruthy();
expect(myGlobal.setTimeout).not.toBe(proto.setTimeout);
clock.uninstall(); clock.uninstall();
expect(myGlobal.hasOwnProperty('setTimeout')).toBeFalsy(); expect(myGlobal.hasOwnProperty('setTimeout')).toBeTruthy();
expect(myGlobal.setTimeout).toBe(proto.setTimeout);
}); });
it('fakes Date constructor', ({ installEx }) => { it('fakes Date constructor', ({ installEx }) => {

View file

@ -305,10 +305,8 @@ it.describe('download event', () => {
}); });
it('should report alt-click downloads', async ({ browser, server, browserName }) => { it('should report alt-click downloads', async ({ browser, server, browserName }) => {
it.fixme(browserName === 'firefox'); it.skip(browserName === 'firefox', 'Firefox does not download on alt-click.');
// Firefox does not download on alt-click by default.
// Our WebKit embedder does not download on alt-click, although Safari does.
server.setRoute('/download', (req, res) => { server.setRoute('/download', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Type', 'application/octet-stream');
res.end(`Hello world`); res.end(`Hello world`);

View file

@ -24,9 +24,9 @@ import type { Log } from '../../packages/trace/src/har';
import { parseHar } from '../config/utils'; import { parseHar } from '../config/utils';
const { createHttp2Server } = require('../../packages/playwright-core/lib/utils'); const { createHttp2Server } = require('../../packages/playwright-core/lib/utils');
async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, testInfo: any, options: { outputPath?: string } & Partial<Pick<BrowserContextOptions['recordHar'], 'content' | 'omitContent' | 'mode'>> = {}) { async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, testInfo: any, options: { outputPath?: string, proxy?: BrowserContextOptions['proxy'] } & Partial<Pick<BrowserContextOptions['recordHar'], 'content' | 'omitContent' | 'mode'>> = {}) {
const harPath = testInfo.outputPath(options.outputPath || 'test.har'); const harPath = testInfo.outputPath(options.outputPath || 'test.har');
const context = await contextFactory({ recordHar: { path: harPath, ...options }, ignoreHTTPSErrors: true }); const context = await contextFactory({ recordHar: { path: harPath, ...options }, ignoreHTTPSErrors: true, proxy: options.proxy });
const page = await context.newPage(); const page = await context.newPage();
return { return {
page, page,
@ -858,6 +858,25 @@ it('should respect minimal mode for API Requests', async ({ contextFactory, serv
expect(entry.response.bodySize).toBe(-1); expect(entry.response.bodySize).toBe(-1);
}); });
it('should include timings when using http proxy', async ({ contextFactory, server, proxyServer }, testInfo) => {
proxyServer.forwardTo(server.PORT, { allowConnectRequests: true });
const { page, getLog } = await pageWithHar(contextFactory, testInfo, { proxy: { server: `localhost:${proxyServer.PORT}` } });
const response = await page.request.get(server.EMPTY_PAGE);
await response.body();
await expect(response).toBeOK();
const log = await getLog();
expect(log.entries[0].timings.connect).toBeGreaterThan(0);
});
it('should include timings when using socks proxy', async ({ contextFactory, server, socksPort }, testInfo) => {
const { page, getLog } = await pageWithHar(contextFactory, testInfo, { proxy: { server: `socks5://localhost:${socksPort}` } });
const response = await page.request.get(server.EMPTY_PAGE);
await response.body();
await expect(response).toBeOK();
const log = await getLog();
expect(log.entries[0].timings.connect).toBeGreaterThan(0);
});
it('should include redirects from API request', async ({ contextFactory, server }, testInfo) => { it('should include redirects from API request', async ({ contextFactory, server }, testInfo) => {
server.setRedirect('/redirect-me', '/simple.json'); server.setRedirect('/redirect-me', '/simple.json');
const { page, getLog } = await pageWithHar(contextFactory, testInfo); const { page, getLog } = await pageWithHar(contextFactory, testInfo);

View file

@ -192,7 +192,7 @@ it('should not block third party SameSite=None cookies', async ({ httpsServer, b
}); });
it('should not override viewport size when passed null', async function({ browserName, server, browser }) { it('should not override viewport size when passed null', async function({ browserName, server, browser }) {
it.fixme(browserName === 'webkit', 'Our WebKit embedder does not respect window features'); it.skip(browserName === 'webkit', 'Our WebKit embedder does not respect window features');
const context = await browser.newContext({ viewport: null }); const context = await browser.newContext({ viewport: null });
const page = await context.newPage(); const page = await context.newPage();
@ -230,8 +230,6 @@ it('Page.bringToFront should work', async ({ browser }) => {
}); });
it('should click in OOPIF', async ({ browserName, launchPersistent, server }) => { it('should click in OOPIF', async ({ browserName, launchPersistent, server }) => {
it.fixme(browserName === 'chromium', 'Click is offset by the infobar height');
server.setRoute('/empty.html', (req, res) => { server.setRoute('/empty.html', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' }); res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`<iframe src="${server.CROSS_PROCESS_PREFIX}/iframe.html"></iframe>`); res.end(`<iframe src="${server.CROSS_PROCESS_PREFIX}/iframe.html"></iframe>`);
@ -250,8 +248,8 @@ it('should click in OOPIF', async ({ browserName, launchPersistent, server }) =>
expect(consoleLog).toContain('ok'); expect(consoleLog).toContain('ok');
}); });
it('should click bottom row w/ infobar in OOPIF', async ({ browserName, launchPersistent, server }) => { it('should click bottom row w/ infobar in OOPIF', async ({ browserName, launchPersistent, server, isWindows }) => {
it.fixme(browserName === 'chromium', 'Click is offset by the infobar height'); it.fixme(browserName === 'chromium' && isWindows, 'Click is offset by the infobar height');
server.setRoute('/empty.html', (req, res) => { server.setRoute('/empty.html', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' }); res.writeHead(200, { 'Content-Type': 'text/html' });

View file

@ -106,7 +106,6 @@ await page.CloseAsync();`);
}); });
test('should upload a single file', async ({ openRecorder, browserName, asset, isLinux }) => { test('should upload a single file', async ({ openRecorder, browserName, asset, isLinux }) => {
test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551');
const { page, recorder } = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<form> <form>
@ -137,7 +136,6 @@ await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-uplo
}); });
test('should upload multiple files', async ({ openRecorder, browserName, asset, isLinux }) => { test('should upload multiple files', async ({ openRecorder, browserName, asset, isLinux }) => {
test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551');
const { page, recorder } = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<form> <form>
@ -168,7 +166,6 @@ await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-uplo
}); });
test('should clear files', async ({ openRecorder, browserName, asset, isLinux }) => { test('should clear files', async ({ openRecorder, browserName, asset, isLinux }) => {
test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551');
const { page, recorder } = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<form> <form>

View file

@ -354,6 +354,7 @@ it.describe('popup', () => {
page.waitForEvent('popup'), page.waitForEvent('popup'),
page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'), page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'),
]); ]);
await popup.waitForLoadState();
const popupTime = await popup.evaluate('time'); const popupTime = await popup.evaluate('time');
expect(popupTime).toBe(1000); expect(popupTime).toBe(1000);
}); });

View file

@ -176,7 +176,8 @@ it('should support clipboard read', async ({ page, context, server, browserName,
it('storage access', { it('storage access', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31227' } annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31227' }
}, async ({ page, context, server, browserName }) => { }, async ({ page, context, server, browserName }) => {
it.fixme(browserName !== 'chromium'); it.skip(browserName !== 'chromium', 'chromium-only api');
await context.grantPermissions(['storage-access']); await context.grantPermissions(['storage-access']);
expect(await getPermission(page, 'storage-access')).toBe('granted'); expect(await getPermission(page, 'storage-access')).toBe('granted');
server.setRoute('/set-cookie.html', (req, res) => { server.setRoute('/set-cookie.html', (req, res) => {

View file

@ -262,14 +262,17 @@ it('should not throttle rAF in the opener page', async ({ page, server }) => {
}); });
it('should not throw when click closes popup', async ({ browserName, page, server }) => { it('should not throw when click closes popup', async ({ browserName, page, server }) => {
it.fixme(browserName === 'firefox'); it.fixme(browserName === 'firefox', 'locator.click: Target page, context or browser has been closed');
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const [popup] = await Promise.all([ const [popup] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup'),
page.evaluate(() => { page.evaluate(async browserName => {
const w = window.open('about:blank'); const w = window.open('about:blank');
if (browserName === 'firefox')
await new Promise(x => w.onload = x);
w.document.body.innerHTML = `<button onclick="window.close()">close</button>`; w.document.body.innerHTML = `<button onclick="window.close()">close</button>`;
}), }, browserName),
]); ]);
await popup.getByRole('button').click(); await popup.getByRole('button').click();
}); });

View file

@ -1452,18 +1452,18 @@ test.skip('should handle case where neither snapshots nor screenshots exist', as
}); });
test('should show only one pointer with multilevel iframes', async ({ page, runAndTrace, server, browserName }) => { test('should show only one pointer with multilevel iframes', async ({ page, runAndTrace, server, browserName }) => {
test.fixme(browserName !== 'chromium', 'Elements in iframe are not marked'); test.fixme(browserName === 'firefox', 'Elements in iframe are not marked');
server.setRoute('/level-0.html', (req, res) => { server.setRoute('/level-0.html', (req, res) => {
res.writeHead(200); res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`<iframe src="/level-1.html" style="position: absolute; left: 100px"></iframe>`); res.end(`<iframe src="/level-1.html" style="position: absolute; left: 100px"></iframe>`);
}); });
server.setRoute('/level-1.html', (req, res) => { server.setRoute('/level-1.html', (req, res) => {
res.writeHead(200); res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`<iframe src="/level-2.html"></iframe>`); res.end(`<iframe src="/level-2.html"></iframe>`);
}); });
server.setRoute('/level-2.html', (req, res) => { server.setRoute('/level-2.html', (req, res) => {
res.writeHead(200); res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`<button>Click me</button>`); res.end(`<button>Click me</button>`);
}); });

View file

@ -20,8 +20,6 @@ import { test as it, expect } from './pageTest';
it.skip(({ isAndroid }) => isAndroid); it.skip(({ isAndroid }) => isAndroid);
it('should work', async ({ page, server, browserName, headless, isLinux }) => { it('should work', async ({ page, server, browserName, headless, isLinux }) => {
it.fixme(browserName === 'firefox' && !headless && !isLinux);
await page.setViewportSize({ width: 500, height: 500 }); await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html'); await page.goto(server.PREFIX + '/grid.html');
const elementHandle = await page.$('.box:nth-of-type(13)'); const elementHandle = await page.$('.box:nth-of-type(13)');

View file

@ -113,8 +113,6 @@ it('should wait for button with an aria-disabled parent', async ({ page }) => {
}); });
it('should wait for stable position', async ({ page, server, browserName, platform }) => { it('should wait for stable position', async ({ page, server, browserName, platform }) => {
it.fixme(browserName === 'firefox' && platform === 'linux');
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button'); const button = await page.$('button');
await page.$eval('button', button => { await page.$eval('button', button => {

View file

@ -349,7 +349,8 @@ test.describe('toBeInViewport', () => {
}); });
test('should respect ratio option', async ({ page, isAndroid }) => { test('should respect ratio option', async ({ page, isAndroid }) => {
test.fixme(isAndroid, 'fails due an upstream bug in Chrome, updating Chrome will fix it.'); test.fixme(isAndroid, 'ratio 0.24 is not in viewport for unknown reason');
await page.setContent(` await page.setContent(`
<style>body, div, html { padding: 0; margin: 0; }</style> <style>body, div, html { padding: 0; margin: 0; }</style>
<div id=big style="height: 400vh;"></div> <div id=big style="height: 400vh;"></div>

View file

@ -133,8 +133,7 @@ it('should be isolated between frames', async ({ page, server }) => {
}); });
it('should work in iframes that failed initial navigation', async ({ page, browserName }) => { it('should work in iframes that failed initial navigation', async ({ page, browserName }) => {
it.fail(browserName === 'chromium'); it.fixme(browserName !== 'webkit');
it.fixme(browserName === 'firefox');
// - Firefox does not report domcontentloaded for the iframe. // - Firefox does not report domcontentloaded for the iframe.
// - Chromium and Firefox report empty url. // - Chromium and Firefox report empty url.

View file

@ -43,8 +43,9 @@ it('should reject when frame detaches', async ({ page, server, browserName }) =>
expect(error.message.toLowerCase()).toContain('frame was detached'); expect(error.message.toLowerCase()).toContain('frame was detached');
}); });
it('should continue after client redirect', async ({ page, server, isAndroid, mode }) => { it('should continue after client redirect', async ({ page, server, isAndroid, browserName }) => {
it.fixme(isAndroid); it.fixme(isAndroid);
it.fixme(browserName === 'firefox', 'script.js is requested before navigationCommitted arrives');
server.setRoute('/frames/script.js', () => {}); server.setRoute('/frames/script.js', () => {});
const url = server.PREFIX + '/frames/child-redirect.html'; const url = server.PREFIX + '/frames/child-redirect.html';

View file

@ -123,9 +123,9 @@ it('should intercept network activity from worker', async function({ page, serve
it('should intercept worker requests when enabled after worker creation', { it('should intercept worker requests when enabled after worker creation', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32355' } annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32355' }
}, async ({ page, server, isAndroid, browserName }) => { }, async ({ page, server, isAndroid, browserName, browserMajorVersion }) => {
it.skip(isAndroid); it.skip(isAndroid);
it.fixme(browserName === 'chromium'); it.skip(browserName === 'chromium' && browserMajorVersion < 130, 'fixed in Chromium 130');
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
server.setRoute('/data_for_worker', (req, res) => res.end('failed to intercept')); server.setRoute('/data_for_worker', (req, res) => res.end('failed to intercept'));

View file

@ -25,7 +25,9 @@ it('should hover @smoke', async ({ page, server }) => {
expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6'); expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6');
}); });
it('should hover when Node is removed', async ({ page, server }) => { it('should hover when Node is removed', async ({ page, server, headless }) => {
it.skip(!headless, 'headed messes up with hover');
await page.goto(server.PREFIX + '/input/scrollable.html'); await page.goto(server.PREFIX + '/input/scrollable.html');
await page.evaluate(() => delete window['Node']); await page.evaluate(() => delete window['Node']);
const button = page.locator('#button-6'); const button = page.locator('#button-6');

View file

@ -42,8 +42,8 @@ it('should scroll into view', async ({ page, server, isAndroid }) => {
} }
}); });
it('should scroll zero-sized element into view', async ({ page, isAndroid, isElectron, isWebView2, browserName, isMac, macVersion }) => { it('should scroll zero-sized element into view', async ({ page, isAndroid, isElectron, browserName, isMac, macVersion }) => {
it.fixme(isAndroid || isElectron || isWebView2); it.fixme(isAndroid || isElectron);
it.skip(browserName === 'webkit' && isMac && macVersion < 11, 'WebKit for macOS 10.15 is frozen.'); it.skip(browserName === 'webkit' && isMac && macVersion < 11, 'WebKit for macOS 10.15 is frozen.');
await page.setContent(` await page.setContent(`
@ -111,7 +111,6 @@ it('should take screenshot', async ({ page, server, browserName, headless, isAnd
}); });
it('should return bounding box', async ({ page, server, browserName, headless, isAndroid, isLinux }) => { it('should return bounding box', async ({ page, server, browserName, headless, isAndroid, isLinux }) => {
it.fixme(browserName === 'firefox' && !headless && !isLinux);
it.skip(isAndroid); it.skip(isAndroid);
await page.setViewportSize({ width: 500, height: 500 }); await page.setViewportSize({ width: 500, height: 500 });

View file

@ -143,9 +143,8 @@ it('should not report text nodes inside controls', async function({ page, browse
expect(await page.accessibility.snapshot()).toEqual(golden); expect(await page.accessibility.snapshot()).toEqual(golden);
}); });
it('rich text editable fields should have children', async function({ page, browserName, browserVersion, isWebView2 }) { it('rich text editable fields should have children', async function({ page, browserName, browserVersion }) {
it.skip(browserName === 'webkit', 'WebKit rich text accessibility is iffy'); it.skip(browserName === 'webkit', 'WebKit rich text accessibility is iffy');
it.skip(isWebView2, 'WebView2 is missing a Chromium fix');
await page.setContent(` await page.setContent(`
<div contenteditable="true"> <div contenteditable="true">
@ -177,9 +176,8 @@ it('rich text editable fields should have children', async function({ page, brow
expect(snapshot.children[0]).toEqual(golden); expect(snapshot.children[0]).toEqual(golden);
}); });
it('rich text editable fields with role should have children', async function({ page, browserName, browserVersion, isWebView2 }) { it('rich text editable fields with role should have children', async function({ page, browserName, browserVersion }) {
it.skip(browserName === 'webkit', 'WebKit rich text accessibility is iffy'); it.skip(browserName === 'webkit', 'WebKit rich text accessibility is iffy');
it.skip(isWebView2, 'WebView2 is missing a Chromium fix');
await page.setContent(` await page.setContent(`
<div contenteditable="true" role='textbox'> <div contenteditable="true" role='textbox'>

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { test as it } from './pageTest'; import { test as it, expect } from './pageTest';
it('clicking on links which do not commit navigation', async ({ page, server, httpsServer }) => { it('clicking on links which do not commit navigation', async ({ page, server, httpsServer }) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
@ -65,3 +65,78 @@ it('opening a popup', async function({ page, server }) {
page.evaluate(() => window.open(window.location.href) && 1), page.evaluate(() => window.open(window.location.href) && 1),
]); ]);
}); });
it('clicking in the middle of navigation that aborts', async ({ page, server }) => {
let abortCallback;
const abortPromise = new Promise(f => abortCallback = f);
let stallCallback;
const stallPromise = new Promise(f => stallCallback = f);
server.setRoute('/stall.html', async (req, res) => {
stallCallback();
await abortPromise;
req.socket.destroy();
});
await page.goto(server.PREFIX + '/one-style.html');
page.goto(server.PREFIX + '/stall.html').catch(() => {});
await stallPromise;
const clickPromise = page.click('body');
await page.waitForTimeout(1000);
abortCallback();
await clickPromise;
});
it('clicking in the middle of navigation that commits', async ({ page, server }) => {
let commitCallback;
const abortPromise = new Promise(f => commitCallback = f);
let stallCallback;
const stallPromise = new Promise(f => stallCallback = f);
server.setRoute('/stall.html', async (req, res) => {
stallCallback();
await abortPromise;
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('hello world');
});
await page.goto(server.PREFIX + '/one-style.html');
page.goto(server.PREFIX + '/stall.html').catch(() => {});
await stallPromise;
const clickPromise = page.click('body');
await page.waitForTimeout(1000);
commitCallback();
await clickPromise;
await expect(page.locator('body')).toContainText('hello world');
});
it('goBack in the middle of navigation that commits', async ({ page, server }) => {
let commitCallback;
const abortPromise = new Promise(f => commitCallback = f);
let stallCallback;
const stallPromise = new Promise(f => stallCallback = f);
server.setRoute('/stall.html', async (req, res) => {
stallCallback();
await abortPromise;
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('hello world');
});
await page.goto(server.PREFIX + '/one-style.html');
page.goto(server.PREFIX + '/stall.html').catch(() => {});
await stallPromise;
const goBackPromise = page.goBack().catch(() => {});
await page.waitForTimeout(1000);
commitCallback();
await goBackPromise;
});

View file

@ -21,47 +21,6 @@ declare const renderComponent;
declare const e; declare const e;
declare const MyButton; declare const MyButton;
it('should report that selector does not match anymore', async ({ page, server }) => {
it.fixme();
await page.goto(server.PREFIX + '/react.html');
await page.evaluate(() => {
renderComponent(e('div', {}, [e(MyButton, { name: 'button1' }), e(MyButton, { name: 'button2' })]));
});
const __testHookAfterStable = () => page.evaluate(() => {
window['counter'] = (window['counter'] || 0) + 1;
if (window['counter'] === 1)
renderComponent(e('div', {}, [e(MyButton, { name: 'button2' }), e(MyButton, { name: 'button1' })]));
else
renderComponent(e('div', {}, []));
});
const error = await page.dblclick('text=button1', { __testHookAfterStable, timeout: 3000 } as any).catch(e => e);
expect(await page.evaluate('window.button1')).toBe(undefined);
expect(await page.evaluate('window.button2')).toBe(undefined);
expect(error.message).toContain('page.dblclick: Timeout 3000ms exceeded.');
expect(error.message).toContain('element does not match the selector anymore');
});
it('should not retarget the handle when element is recycled', async ({ page, server }) => {
it.fixme();
await page.goto(server.PREFIX + '/react.html');
await page.evaluate(() => {
renderComponent(e('div', {}, [e(MyButton, { name: 'button1' }), e(MyButton, { name: 'button2', disabled: true })]));
});
const __testHookBeforeStable = () => page.evaluate(() => {
window['counter'] = (window['counter'] || 0) + 1;
if (window['counter'] === 1)
renderComponent(e('div', {}, [e(MyButton, { name: 'button2', disabled: true }), e(MyButton, { name: 'button1' })]));
});
const handle = await page.$('text=button1');
const error = await handle.click({ __testHookBeforeStable, timeout: 3000 } as any).catch(e => e);
expect(await page.evaluate('window.button1')).toBe(undefined);
expect(await page.evaluate('window.button2')).toBe(undefined);
expect(error.message).toContain('elementHandle.click: Timeout 3000ms exceeded.');
expect(error.message).toContain('element is disabled - waiting');
});
it('should timeout when click opens alert', async ({ page, server }) => { it('should timeout when click opens alert', async ({ page, server }) => {
const dialogPromise = page.waitForEvent('dialog'); const dialogPromise = page.waitForEvent('dialog');
await page.setContent(`<div onclick='window.alert(123)'>Click me</div>`); await page.setContent(`<div onclick='window.alert(123)'>Click me</div>`);
@ -71,40 +30,6 @@ it('should timeout when click opens alert', async ({ page, server }) => {
await dialog.dismiss(); await dialog.dismiss();
}); });
it('should retarget when element is recycled during hit testing', async ({ page, server }) => {
it.fixme();
await page.goto(server.PREFIX + '/react.html');
await page.evaluate(() => {
renderComponent(e('div', {}, [e(MyButton, { name: 'button1' }), e(MyButton, { name: 'button2' })]));
});
const __testHookAfterStable = () => page.evaluate(() => {
window['counter'] = (window['counter'] || 0) + 1;
if (window['counter'] === 1)
renderComponent(e('div', {}, [e(MyButton, { name: 'button2' }), e(MyButton, { name: 'button1' })]));
});
await page.click('text=button1', { __testHookAfterStable } as any);
expect(await page.evaluate('window.button1')).toBe(true);
expect(await page.evaluate('window.button2')).toBe(undefined);
});
it('should retarget when element is recycled before enabled check', async ({ page, server }) => {
it.fixme();
await page.goto(server.PREFIX + '/react.html');
await page.evaluate(() => {
renderComponent(e('div', {}, [e(MyButton, { name: 'button1' }), e(MyButton, { name: 'button2', disabled: true })]));
});
const __testHookBeforeStable = () => page.evaluate(() => {
window['counter'] = (window['counter'] || 0) + 1;
if (window['counter'] === 1)
renderComponent(e('div', {}, [e(MyButton, { name: 'button2', disabled: true }), e(MyButton, { name: 'button1' })]));
});
await page.click('text=button1', { __testHookBeforeStable } as any);
expect(await page.evaluate('window.button1')).toBe(true);
expect(await page.evaluate('window.button2')).toBe(undefined);
});
it('should not retarget when element changes on hover', async ({ page, server }) => { it('should not retarget when element changes on hover', async ({ page, server }) => {
await page.goto(server.PREFIX + '/react.html'); await page.goto(server.PREFIX + '/react.html');
await page.evaluate(() => { await page.evaluate(() => {

View file

@ -78,9 +78,8 @@ it('should scroll into view display:contents with position', async ({ page, brow
expect(await page.evaluate('window._clicked')).toBe(true); expect(await page.evaluate('window._clicked')).toBe(true);
}); });
it('should not crash when force-clicking hidden input', async ({ page, isWebView2 }) => { it('should not crash when force-clicking hidden input', async ({ page }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/18183' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/18183' });
it.fixme(isWebView2);
await page.setContent(`<input type=hidden>`); await page.setContent(`<input type=hidden>`);
const error = await page.locator('input').click({ force: true, timeout: 2000 }).catch(e => e); const error = await page.locator('input').click({ force: true, timeout: 2000 }).catch(e => e);

View file

@ -94,7 +94,9 @@ it('should not throw UnhandledPromiseRejection when page closes', async ({ page,
]).catch(e => {}); ]).catch(e => {});
}); });
it('should click the 1x1 div', async ({ page }) => { it('should click the 1x1 div', async ({ page, browserName, isWindows }) => {
it.fixme(browserName === 'firefox' && isWindows, 'always times out');
await page.setContent(`<div style="width: 1px; height: 1px;" onclick="window.__clicked = true"></div>`); await page.setContent(`<div style="width: 1px; height: 1px;" onclick="window.__clicked = true"></div>`);
await page.click('div'); await page.click('div');
expect(await page.evaluate('window.__clicked')).toBe(true); expect(await page.evaluate('window.__clicked')).toBe(true);
@ -336,7 +338,7 @@ it('should click the button inside an iframe', async ({ page, server }) => {
}); });
it('should click the button with fixed position inside an iframe', async ({ page, server, browserName }) => { it('should click the button with fixed position inside an iframe', async ({ page, server, browserName }) => {
it.fixme(browserName === 'chromium' || browserName === 'webkit'); it.fixme(browserName === 'chromium');
// @see https://github.com/GoogleChrome/puppeteer/issues/4110 // @see https://github.com/GoogleChrome/puppeteer/issues/4110
// @see https://bugs.chromium.org/p/chromium/issues/detail?id=986390 // @see https://bugs.chromium.org/p/chromium/issues/detail?id=986390

View file

@ -289,8 +289,7 @@ it('should not crash on mouse drag with any button', async ({ page }) => {
it('should dispatch mouse move after context menu was opened', async ({ page, browserName, isWindows }) => { it('should dispatch mouse move after context menu was opened', async ({ page, browserName, isWindows }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/20823' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/20823' });
it.fixme(browserName === 'firefox'); it.fixme(browserName === 'chromium' && isWindows, 'context menu support is best-effort for Linux and MacOS');
it.skip(browserName === 'chromium' && isWindows, 'context menu support is best-effort for Linux and MacOS');
await page.evaluate(() => { await page.evaluate(() => {
window['contextMenuPromise'] = new Promise(x => { window['contextMenuPromise'] = new Promise(x => {
window.addEventListener('contextmenu', x, false); window.addEventListener('contextmenu', x, false);

View file

@ -309,8 +309,10 @@ it('should fetch original request and fulfill', async ({ page, server, isElectro
expect(await page.title()).toEqual('Woof-Woof'); expect(await page.title()).toEqual('Woof-Woof');
}); });
it('should fulfill with multiple set-cookie', async ({ page, server, isElectron, electronMajorVersion }) => { it('should fulfill with multiple set-cookie', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
it.skip(isElectron && electronMajorVersion < 14, 'Electron 14+ is required'); it.skip(isElectron && electronMajorVersion < 14, 'Electron 14+ is required');
it.skip(isAndroid, 'Android does not have an isolated context per test, so we get cookies from other tests');
const cookies = ['a=b', 'c=d']; const cookies = ['a=b', 'c=d'];
await page.route('**/multiple-set-cookie.html', async route => { await page.route('**/multiple-set-cookie.html', async route => {
void route.fulfill({ void route.fulfill({

View file

@ -291,10 +291,9 @@ it.describe('page screenshot', () => {
expect(screenshot).toMatchSnapshot('screenshot-canvas.png'); expect(screenshot).toMatchSnapshot('screenshot-canvas.png');
}); });
it('should capture canvas changes', async ({ page, isElectron, browserName, isMac, isWebView2 }) => { it('should capture canvas changes', async ({ page, isElectron, browserName, isMac }) => {
it.fixme(browserName === 'webkit' && isMac, 'https://github.com/microsoft/playwright/issues/8796,https://github.com/microsoft/playwright/issues/16180'); it.fixme(browserName === 'webkit' && isMac, 'https://github.com/microsoft/playwright/issues/8796,https://github.com/microsoft/playwright/issues/16180');
it.skip(isElectron); it.skip(isElectron);
it.skip(isWebView2);
await page.goto('data:text/html,<canvas></canvas>'); await page.goto('data:text/html,<canvas></canvas>');
await page.evaluate(() => { await page.evaluate(() => {
const canvas = document.querySelector('canvas'); const canvas = document.querySelector('canvas');

View file

@ -328,3 +328,31 @@ it('should fail when navigating while on handle', async ({ page, mode, server })
const error = await body.waitForSelector('div', { __testHookBeforeAdoptNode } as any).catch(e => e); const error = await body.waitForSelector('div', { __testHookBeforeAdoptNode } as any).catch(e => e);
expect(error.message).toContain(`waiting for locator('div') to be visible`); expect(error.message).toContain(`waiting for locator('div') to be visible`);
}); });
it('should fail if element handle was detached while waiting', async ({ page, server }) => {
await page.setContent(`<button>hello</button>`);
const button = await page.$('button');
const promise = button.waitForSelector('something').catch(e => e);
await page.waitForTimeout(100);
await page.evaluate(() => document.body.innerText = '');
const error = await promise;
expect(error.message).toContain('Element is not attached to the DOM');
});
it('should succeed if element handle was detached while waiting for hidden', async ({ page, server }) => {
await page.setContent(`<button>hello</button>`);
const button = await page.$('button');
const promise = button.waitForSelector('something', { state: 'hidden' });
await page.waitForTimeout(100);
await page.evaluate(() => document.body.innerText = '');
await promise;
});
it('should succeed if element handle was detached while waiting for detached', async ({ page, server }) => {
await page.setContent(`<button>hello</button>`);
const button = await page.$('button');
const promise = button.waitForSelector('something', { state: 'detached' });
await page.waitForTimeout(100);
await page.evaluate(() => document.body.innerText = '');
await promise;
});

View file

@ -275,7 +275,6 @@ it('should work with :nth-child', async ({ page, server }) => {
}); });
it('should work with :nth-child(of) notation with nested functions', async ({ page, browserName, browserMajorVersion }) => { it('should work with :nth-child(of) notation with nested functions', async ({ page, browserName, browserMajorVersion }) => {
it.fixme(browserName === 'firefox', 'Should enable once Firefox supports this syntax');
it.skip(browserName === 'chromium' && browserMajorVersion < 111, 'https://caniuse.com/css-nth-child-of'); it.skip(browserName === 'chromium' && browserMajorVersion < 111, 'https://caniuse.com/css-nth-child-of');
await page.setContent(` await page.setContent(`

View file

@ -308,17 +308,6 @@ it('click should survive navigation', async ({ page, server }) => {
await promise; await promise;
}); });
it('should fail if element removed while waiting on element handle', async ({ page, server }) => {
it.fixme();
await routeIframe(page);
await page.goto(server.PREFIX + '/iframe.html');
const button = await page.$('button');
const promise = button.waitForSelector('something');
await page.waitForTimeout(100);
await page.evaluate(() => document.body.innerText = '');
await promise;
});
it('should non work for non-frame', async ({ page, server }) => { it('should non work for non-frame', async ({ page, server }) => {
await routeIframe(page); await routeIframe(page);
await page.setContent('<div></div>'); await page.setContent('<div></div>');

View file

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2210.55" /> <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2792.45" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -12,6 +12,9 @@
"queues": { "queues": {
"batchSize": 1, "batchSize": 1,
"newBatchThreshold": 0 "newBatchThreshold": 0
},
"blobs": {
"maxDegreeOfParallelism": 1
} }
}, },
"extensionBundle": { "extensionBundle": {

View file

@ -46,6 +46,7 @@
* lines: string[], * lines: string[],
* codeLang: string, * codeLang: string,
* title?: string, * title?: string,
* highlight?: string,
* }} MarkdownCodeNode */ * }} MarkdownCodeNode */
/** @typedef {MarkdownBaseNode & { /** @typedef {MarkdownBaseNode & {
@ -165,13 +166,14 @@ function buildTree(lines) {
// Remaining items respect indent-based nesting. // Remaining items respect indent-based nesting.
const [, indent, content] = /** @type {string[]} */ (line.match('^([ ]*)(.*)')); const [, indent, content] = /** @type {string[]} */ (line.match('^([ ]*)(.*)'));
if (content.startsWith('```')) { if (content.startsWith('```')) {
const [codeLang, title] = parseCodeBlockMetadata(content); const [codeLang, title, highlight] = parseCodeBlockMetadata(content);
/** @type {MarkdownNode} */ /** @type {MarkdownNode} */
const node = { const node = {
type: 'code', type: 'code',
lines: [], lines: [],
codeLang, codeLang,
title, title,
highlight,
}; };
line = lines[++i]; line = lines[++i];
while (!line.trim().startsWith('```')) { while (!line.trim().startsWith('```')) {
@ -255,14 +257,18 @@ function buildTree(lines) {
/** /**
* @param {String} firstLine * @param {String} firstLine
* @returns {[string, string|undefined]} * @returns {[string, string|undefined, string|undefined]}
*/ */
function parseCodeBlockMetadata(firstLine) { function parseCodeBlockMetadata(firstLine) {
const withoutBackticks = firstLine.substring(3); const withoutBackticks = firstLine.substring(3);
const match = withoutBackticks.match(/ title="(.+)"$/); const titleMatch = withoutBackticks.match(/ title="(.+)"/);
if (match) const highlightMatch = withoutBackticks.match(/\{.*\}/);
return [withoutBackticks.substring(0, match.index), match[1]];
return [withoutBackticks, undefined]; let codeLang = withoutBackticks;
if (titleMatch || highlightMatch)
codeLang = withoutBackticks.substring(0, titleMatch?.index ?? highlightMatch?.index);
return [codeLang, titleMatch?.[1], highlightMatch?.[0]];
} }
/** /**
@ -328,7 +334,7 @@ function innerRenderMdNode(indent, node, lastNode, result, options) {
if (node.type === 'code') { if (node.type === 'code') {
newLine(); newLine();
result.push(`${indent}\`\`\`${node.codeLang}${(options?.renderCodeBlockTitlesInHeader && node.title) ? ' title="' + node.title + '"' : ''}`); result.push(`${indent}\`\`\`${node.codeLang}${(options?.renderCodeBlockTitlesInHeader && node.title) ? ' title="' + node.title + '"' : ''}${node.highlight ? ' ' + node.highlight : ''}`);
if (!options?.renderCodeBlockTitlesInHeader && node.title) if (!options?.renderCodeBlockTitlesInHeader && node.title)
result.push(`${indent}// ${node.title}`); result.push(`${indent}// ${node.title}`);
for (const line of node.lines) for (const line of node.lines)