diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index f2ee2433b4..38ca329769 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -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, 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** ```js @@ -249,7 +251,7 @@ Time to be set. ## async method: Clock.setSystemTime * 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** diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 7a3be809c6..373649154d 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -1217,8 +1217,6 @@ await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches); // → true await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches); // → false -await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches); -// → false ``` ```java @@ -1227,8 +1225,6 @@ page.evaluate("() => matchMedia('(prefers-color-scheme: dark)').matches"); // → true page.evaluate("() => matchMedia('(prefers-color-scheme: light)').matches"); // → false -page.evaluate("() => matchMedia('(prefers-color-scheme: no-preference)').matches"); -// → false ``` ```python async @@ -1237,8 +1233,6 @@ await page.evaluate("matchMedia('(prefers-color-scheme: dark)').matches") # → True await page.evaluate("matchMedia('(prefers-color-scheme: light)').matches") # → False -await page.evaluate("matchMedia('(prefers-color-scheme: no-preference)').matches") -# → False ``` ```python sync @@ -1247,7 +1241,6 @@ page.evaluate("matchMedia('(prefers-color-scheme: dark)').matches") # → True page.evaluate("matchMedia('(prefers-color-scheme: light)').matches") # → False -page.evaluate("matchMedia('(prefers-color-scheme: no-preference)').matches") ``` ```csharp @@ -1256,8 +1249,6 @@ await page.EvaluateAsync("matchMedia('(prefers-color-scheme: dark)').matches"); // → true await page.EvaluateAsync("matchMedia('(prefers-color-scheme: light)').matches"); // → false -await page.EvaluateAsync("matchMedia('(prefers-color-scheme: no-preference)').matches"); -// → false ``` ### option: Page.emulateMedia.media @@ -1281,16 +1272,16 @@ Passing `'Null'` disables CSS media emulation. * langs: js, java - `colorScheme` > -Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. Passing -`null` disables color scheme emulation. +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. `'no-preference'` is deprecated. ### option: Page.emulateMedia.colorScheme * since: v1.9 * langs: csharp, python - `colorScheme` <[ColorScheme]<"light"|"dark"|"no-preference"|"null">> -Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. Passing -`'Null'` disables color scheme emulation. +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. `'no-preference'` is deprecated. ### option: Page.emulateMedia.reducedMotion * since: v1.12 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 462b225e6e..63693030bb 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -639,14 +639,14 @@ If no origin is specified, the username and password are sent to any servers upo * langs: js, java - `colorScheme` > -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'`. ## context-option-colorscheme-csharp-python * langs: csharp, python - `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'`. ## context-option-reducedMotion diff --git a/docs/src/best-practices-js.md b/docs/src/best-practices-js.md index 3b70817923..0c4e71d3a5 100644 --- a/docs/src/best-practices-js.md +++ b/docs/src/best-practices-js.md @@ -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. + + + ```bash npx playwright codegen playwright.dev ``` + + + + +```bash +yarn playwright codegen playwright.dev +``` + + + + + +```bash +pnpm exec playwright codegen playwright.dev +``` + + + + + 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. @@ -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. + + + ```bash npx playwright test --debug ``` + + + + +```bash +yarn playwright test --debug +``` + + + + + +```bash +pnpm exec playwright test --debug +``` + + + + + 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. debugging with the playwright inspector @@ -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. + + + ```bash npx playwright test example.spec.ts:9 --debug ``` + + + + + +```bash +yarn playwright test example.spec.ts:9 --debug +``` + + + + + +```bash +pnpm exec playwright test example.spec.ts:9 --debug +``` + + + + #### 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. @@ -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. + + + ```bash npx playwright test --trace on ``` + + + + + +```bash +yarn playwright test --trace on +``` + + + + + +```bash +pnpm exec playwright test --trace on +``` + + + + + Once you run this command your traces will be recorded for each test and can be viewed directly from the HTML report. + + + ```bash npx playwright show-report -```` +``` + + + + + +```bash +yarn playwright show-report +``` + + + + + +```bash +pnpm exec playwright show-report +``` + + + + Playwrights HTML report @@ -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. + + + ```bash npm install -D @playwright/test@latest ``` + + + + + +```bash +yarn add --dev @playwright/test@latest +``` + + + + + +```bash +pnpm install --save-dev @playwright/test@latest +``` + + + + + 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. + + + ```bash npx playwright --version ``` + + + + +```bash +yarn playwright --version +``` + + + + + +```bash +pnpm exec playwright --version +``` + + + + + ### 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. @@ -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. + + + ```bash npx playwright test --shard=1/3 ``` + + + + +```bash +yarn playwright test --shard=1/3 +``` + + + + + +```bash +pnpm exec playwright test --shard=1/3 +``` + + + + + ## Productivity tips ### Use Soft assertions diff --git a/docs/src/ci.md b/docs/src/ci.md index 6f6fc9a80e..99033a3e2e 100644 --- a/docs/src/ci.md +++ b/docs/src/ci.md @@ -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. 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 on: push: diff --git a/docs/src/emulation.md b/docs/src/emulation.md index 12b44a49c8..fd0009f71c 100644 --- a/docs/src/emulation.md +++ b/docs/src/emulation.md @@ -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. ## 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" import { defineConfig } from '@playwright/test'; diff --git a/docs/src/test-reporters-js.md b/docs/src/test-reporters-js.md index 952d74b923..ce5698005c 100644 --- a/docs/src/test-reporters-js.md +++ b/docs/src/test-reporters-js.md @@ -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" ``` -## 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) -* [Argos Visual Testing](https://argos-ci.com/docs/playwright) -* [Currents](https://www.npmjs.com/package/@currents/playwright) -* [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) +* [Allure Reporter](https://github.com/allure-framework/allure-js/tree/main/packages/allure-playwright) +* [Github Actions Reporter](https://github.com/estruyf/playwright-github-actions-reporter) +* [Mail Reporter](https://github.com/estruyf/playwright-mail-reporter) * [ReportPortal](https://github.com/reportportal/agent-js-playwright) -* [Serenity/JS](https://serenity-js.org/handbook/test-runners/playwright-test) -* [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) +* [Monocart](https://github.com/cenfun/monocart-reporter) diff --git a/docs/src/test-use-options-js.md b/docs/src/test-use-options-js.md index c8c93c7cee..6e1da0a228 100644 --- a/docs/src/test-use-options-js.md +++ b/docs/src/test-use-options-js.md @@ -64,7 +64,7 @@ export default defineConfig({ | 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.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. | diff --git a/packages/html-reporter/src/testCaseView.css b/packages/html-reporter/src/testCaseView.css index 90e2f4057b..56be1d9620 100644 --- a/packages/html-reporter/src/testCaseView.css +++ b/packages/html-reporter/src/testCaseView.css @@ -61,6 +61,7 @@ align-items: center; padding: 0 8px; line-height: 24px; + white-space: pre-wrap; } @media only screen and (max-width: 600px) { diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index 0a3ca6a5f4..a5d4ca7d0b 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -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/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) - brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion) - 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) - get-stream@5.2.0 (https://github.com/sindresorhus/get-stream) - 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) - is-docker@2.2.1 (https://github.com/sindresorhus/is-docker) - 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) - signal-exit@3.0.7 (https://github.com/tapjs/signal-exit) - 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) - sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js) - 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 -%% 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) -Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net> +Copyright (c) 2013 Nathan Rajlich Permission is hereby granted, free of charge, to any person obtaining 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, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 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 ========================================= @@ -629,124 +506,11 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= 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) -Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net> +Copyright (c) 2013 Nathan Rajlich Permission is hereby granted, free of charge, to any person obtaining 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, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 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 ========================================= @@ -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 -%% 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) -Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net> +Copyright (c) 2013 Nathan Rajlich Permission is hereby granted, free of charge, to any person obtaining 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 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 ========================================= diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index eef68ef8ee..0e5e761433 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -13,7 +13,7 @@ "debug": "^4.3.4", "dotenv": "^16.4.5", "graceful-fs": "4.2.10", - "https-proxy-agent": "5.0.0", + "https-proxy-agent": "7.0.5", "jpeg-js": "0.4.4", "mime": "^3.0.0", "minimatch": "^3.1.2", @@ -23,7 +23,7 @@ "proxy-from-env": "1.1.0", "retry": "0.12.0", "signal-exit": "3.0.7", - "socks-proxy-agent": "6.1.1", + "socks-proxy-agent": "8.0.4", "stack-utils": "2.0.5", "ws": "8.17.1" }, @@ -130,14 +130,15 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "license": "MIT", "dependencies": { - "debug": "4" + "debug": "^4.3.4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/balanced-match": { @@ -224,15 +225,16 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/ip-address": { @@ -382,16 +384,17 @@ } }, "node_modules/socks-proxy-agent": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", - "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "license": "MIT", "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.1", - "socks": "^2.6.1" + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, "node_modules/sprintf-js": { @@ -523,11 +526,11 @@ } }, "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "requires": { - "debug": "4" + "debug": "^4.3.4" } }, "balanced-match": { @@ -588,11 +591,11 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "requires": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" } }, @@ -696,13 +699,13 @@ } }, "socks-proxy-agent": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", - "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.1", - "socks": "^2.6.1" + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" } }, "sprintf-js": { diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json index a7c66192e0..06637adabe 100644 --- a/packages/playwright-core/bundles/utils/package.json +++ b/packages/playwright-core/bundles/utils/package.json @@ -14,7 +14,7 @@ "debug": "^4.3.4", "dotenv": "^16.4.5", "graceful-fs": "4.2.10", - "https-proxy-agent": "5.0.0", + "https-proxy-agent": "7.0.5", "jpeg-js": "0.4.4", "mime": "^3.0.0", "minimatch": "^3.1.2", @@ -24,7 +24,7 @@ "proxy-from-env": "1.1.0", "retry": "0.12.0", "signal-exit": "3.0.7", - "socks-proxy-agent": "6.1.1", + "socks-proxy-agent": "8.0.4", "stack-utils": "2.0.5", "ws": "8.17.1" }, diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index c2ed979fbe..76708245d4 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -292,7 +292,7 @@ export class ElementHandle extends js.JSHandle { }; } - async _retryAction(progress: Progress, actionName: string, action: (retry: number) => Promise, options: { trial?: boolean, force?: boolean, skipLocatorHandlersCheckpoint?: boolean }): Promise<'error:notconnected' | 'done'> { + async _retryAction(progress: Progress, actionName: string, action: (retry: number) => Promise, options: { trial?: boolean, force?: boolean, skipActionPreChecks?: boolean }): Promise<'error:notconnected' | 'done'> { let retry = 0; // We progressively wait longer between retries, up to 500ms. const waitTime = [0, 20, 100, 100, 500]; @@ -310,8 +310,8 @@ export class ElementHandle extends js.JSHandle { } else { progress.log(`attempting ${actionName} action${options.trial ? ' (trial run)' : ''}`); } - if (!options.skipLocatorHandlersCheckpoint && !options.force) - await this._frame._page.performLocatorHandlersCheckpoint(progress); + if (!options.skipActionPreChecks && !options.force) + await this._frame._page.performActionPreChecks(progress); const result = await action(retry); ++retry; if (result === 'error:notvisible') { @@ -346,7 +346,7 @@ export class ElementHandle extends js.JSHandle { async _retryPointerAction(progress: Progress, actionName: ActionName, waitForEnabled: boolean, action: (point: types.Point) => Promise, 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. - const skipLocatorHandlersCheckpoint = actionName === 'move and up'; + const skipActionPreChecks = actionName === 'move and up'; return await this._retryAction(progress, actionName, async retry => { // By default, we scroll with protocol method to reveal the action point. // However, that might not work to scroll from under position:sticky elements @@ -360,7 +360,7 @@ export class ElementHandle extends js.JSHandle { ]; const forceScrollOptions = scrollOptions[retry % scrollOptions.length]; return await this._performPointerAction(progress, actionName, waitForEnabled, action, forceScrollOptions, options); - }, { ...options, skipLocatorHandlersCheckpoint }); + }, { ...options, skipActionPreChecks }); } async _performPointerAction( diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 12f1d21d93..243e89cf1c 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -20,7 +20,6 @@ import http from 'http'; import https from 'https'; import type { Readable, TransformCallback } from 'stream'; import { pipeline, Transform } from 'stream'; -import url from 'url'; import zlib from 'zlib'; import type { HTTPCredentials } from '../../types/types'; 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 const happyEyeBallsTimings = timingForSocket(socket); dnsLookupAt = happyEyeBallsTimings.dnsLookupAt; - tcpConnectionAt = happyEyeBallsTimings.tcpConnectionAt; + tcpConnectionAt ??= happyEyeBallsTimings.tcpConnectionAt; // non-happy-eyeballs sockets listeners.push( eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }), - eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt = monotonicTime(); }), + eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt ??= monotonicTime(); }), eventsHelper.addEventListener(socket, 'secureConnect', () => { 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; serverPort = socket.remotePort; }); request.on('finish', () => { requestFinishAt = monotonicTime(); }); + // http proxy + request.on('proxyConnect', () => { + tcpConnectionAt ??= monotonicTime(); + }); + + progress.log(`→ ${options.method} ${url.toString()}`); if (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) { - const proxyOpts = url.parse(proxy.server); - if (proxyOpts.protocol?.startsWith('socks')) { - return new SocksProxyAgent({ - host: proxyOpts.hostname, - port: proxyOpts.port || undefined, - }); - } + const proxyURL = new URL(proxy.server); + if (proxyURL.protocol?.startsWith('socks')) + return new SocksProxyAgent(proxyURL); + if (proxy.username) - proxyOpts.auth = `${proxy.username}:${proxy.password || ''}`; - // TODO: We should use HttpProxyAgent conditional on proxyOpts.protocol instead of always using CONNECT method. - return new HttpsProxyAgent(proxyOpts); + proxyURL.username = proxy.username; + if (proxy.password) + 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 { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 7dc992813b..6dc412c235 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -248,6 +248,11 @@ export class FrameManager { const frame = this._frames.get(frameId); if (!frame) 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; const navigationEvent: NavigationEvent = { url, name: frame._name, isPublic: true }; this._fireInternalFrameNavigation(frame, navigationEvent); @@ -786,11 +791,11 @@ export class Frame extends SdkObject { }, this._page._timeoutSettings.timeout(options)); } - async waitForSelectorInternal(progress: Progress, selector: string, performLocatorHandlersCheckpoint: boolean, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise | null> { + async waitForSelectorInternal(progress: Progress, selector: string, performActionPreChecks: boolean, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise | null> { const { state = 'visible' } = options; const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { - if (performLocatorHandlersCheckpoint) - await this._page.performLocatorHandlersCheckpoint(progress); + if (performActionPreChecks) + await this._page.performActionPreChecks(progress); const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope); progress.throwIfAborted(); @@ -800,6 +805,8 @@ export class Frame extends SdkObject { return continuePolling; } 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 element: Element | undefined = elements[0]; const visible = element ? injected.utils.isElementVisible(element) : false; @@ -1113,12 +1120,12 @@ export class Frame extends SdkObject { progress: Progress, selector: string, strict: boolean | undefined, - performLocatorHandlersCheckpoint: boolean, + performActionPreChecks: boolean, action: (handle: dom.ElementHandle) => Promise): Promise { progress.log(`waiting for ${this._asLocator(selector)}`); return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { - if (performLocatorHandlersCheckpoint) - await this._page.performLocatorHandlersCheckpoint(progress); + if (performActionPreChecks) + await this._page.performActionPreChecks(progress); const resolved = await this.selectors.resolveInjectedForSelector(selector, { strict }); progress.throwIfAborted(); @@ -1162,7 +1169,7 @@ export class Frame extends SdkObject { } async rafrafTimeoutScreenshotElementWithProgress(progress: Progress, selector: string, timeout: number, options: ScreenshotOptions): Promise { - 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); 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) { const controller = new ProgressController(metadata, this); 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)); } async dblclick(metadata: CallMetadata, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions = {}) { const controller = new ProgressController(metadata, this); 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)); } async dragAndDrop(metadata: CallMetadata, source: string, target: string, options: types.DragActionOptions & types.PointerActionWaitOptions = {}) { const controller = new ProgressController(metadata, this); 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 => { await this._page.mouse.move(point.x, point.y); 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. - 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 => { await this._page.mouse.move(point.x, point.y); 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.'); const controller = new ProgressController(metadata, this); 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)); } async fill(metadata: CallMetadata, selector: string, value: string, options: types.TimeoutOptions & types.StrictOptions & { force?: boolean }) { const controller = new ProgressController(metadata, this); 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)); } async focus(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}) { const controller = new ProgressController(metadata, this); 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)); } async blur(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}) { const controller = new ProgressController(metadata, this); 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)); } @@ -1344,14 +1351,14 @@ export class Frame extends SdkObject { async hover(metadata: CallMetadata, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) { const controller = new ProgressController(metadata, this); 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)); } async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions = {}): Promise { const controller = new ProgressController(metadata, this); 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)); } @@ -1359,35 +1366,35 @@ export class Frame extends SdkObject { const inputFileItems = await prepareFilesForUpload(this, params); const controller = new ProgressController(metadata, this); 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)); } async type(metadata: CallMetadata, selector: string, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions = {}) { const controller = new ProgressController(metadata, this); 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)); } async press(metadata: CallMetadata, selector: string, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions = {}) { const controller = new ProgressController(metadata, this); 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)); } async check(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions = {}) { const controller = new ProgressController(metadata, this); 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)); } async uncheck(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions = {}) { const controller = new ProgressController(metadata, this); 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)); } @@ -1416,7 +1423,7 @@ export class Frame extends SdkObject { await (new ProgressController(metadata, this)).run(async progress => { progress.log(`${metadata.apiName}${timeout ? ` with timeout ${timeout}ms` : ''}`); progress.log(`waiting for ${this._asLocator(selector)}`); - await this._page.performLocatorHandlersCheckpoint(progress); + await this._page.performActionPreChecks(progress); }, 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. return await (new ProgressController(metadata, this)).run(async progress => { 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); if (matches === options.isNot) { // Keep waiting in these cases: diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 3fb72bb0c3..f7f1ef67e8 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -31,7 +31,7 @@ import * as accessibility from './accessibility'; import { FileChooser } from './fileChooser'; import type { Progress } 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 { debugLogger } from '../utils/debugLogger'; import type { ImageComparatorOptions } from '../utils/comparators'; @@ -45,6 +45,7 @@ import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSe import type { SerializedValue } from './isomorphic/utilityScriptSerializers'; import { TargetClosedError } from './errors'; import { asLocator } from '../utils'; +import { helper } from './helper'; export interface PageDelegate { readonly rawMouse: input.RawMouse; @@ -455,7 +456,34 @@ export class Page extends SdkObject { 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. if (this._locatorHandlerRunningCounter) return; @@ -559,7 +587,7 @@ export class Page extends SdkObject { const rafrafScreenshot = locator ? async (progress: Progress, timeout: number) => { return await locator.frame.rafrafTimeoutScreenshotElementWithProgress(progress, locator.selector, timeout, options || {}); } : async (progress: Progress, timeout: number) => { - await this.performLocatorHandlersCheckpoint(progress); + await this.performActionPreChecks(progress); await this.mainFrame().rafrafTimeout(timeout); return await this._screenshotter.screenshotPage(progress, options || {}); }; diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index b041ffb829..4e850f4a84 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -98,7 +98,7 @@ class SocksProxyConnection { async connect() { 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 this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port); diff --git a/packages/playwright-core/src/utils/network.ts b/packages/playwright-core/src/utils/network.ts index f04b828d67..632c74fe3a 100644 --- a/packages/playwright-core/src/utils/network.ts +++ b/packages/playwright-core/src/utils/network.ts @@ -50,7 +50,7 @@ export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.Inco const proxyURL = getProxyForUrl(params.url); if (proxyURL) { - const parsedProxyURL = url.parse(proxyURL); + const parsedProxyURL = new URL(proxyURL); if (params.url.startsWith('http:')) { options = { path: parsedUrl.href, diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 1f165c3d93..fe182dac4e 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2553,16 +2553,15 @@ export interface Page { * // → true * await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches); * // → false - * await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches); - * // → false * ``` * * @param options */ emulateMedia(options?: { /** - * Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. - * Passing `null` disables color scheme emulation. + * 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. + * `'no-preference'` is deprecated. */ 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. * Passing `null` resets emulation to system defaults. Defaults to `'light'`. */ @@ -14726,7 +14726,8 @@ export interface BrowserType { }>; /** - * 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. * Passing `null` resets emulation to system defaults. Defaults to `'light'`. */ @@ -16522,7 +16523,8 @@ export interface AndroidDevice { 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. * 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. * + * 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** * * ```js @@ -18555,7 +18561,8 @@ export interface Clock { setFixedTime(time: number|string|Date): Promise; /** - * 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** * @@ -18998,7 +19005,8 @@ export interface Electron { 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. * 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. * Passing `null` resets emulation to system defaults. Defaults to `'light'`. */ diff --git a/packages/playwright-ct-vue/registerSource.mjs b/packages/playwright-ct-vue/registerSource.mjs index 07ce5298f4..33bc74e731 100644 --- a/packages/playwright-ct-vue/registerSource.mjs +++ b/packages/playwright-ct-vue/registerSource.mjs @@ -188,7 +188,7 @@ function __pwWrapFunctions(slots) { for (const [key, value] of Object.entries(slots || {})) slotsWithRenderFunctions[key] = () => [value]; } else if (slots?.length) { - slots['default'] = () => slots; + slotsWithRenderFunctions['default'] = () => slots; } return slotsWithRenderFunctions; } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 400d7cbcf3..41c88940c9 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -5225,7 +5225,8 @@ export interface PlaywrightTestOptions { */ 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. * Passing `null` resets emulation to system defaults. Defaults to `'light'`. * diff --git a/tests/components/ct-vue-vite/src/components/SlotDefaultValue.vue b/tests/components/ct-vue-vite/src/components/SlotDefaultValue.vue new file mode 100644 index 0000000000..c5944a3f8a --- /dev/null +++ b/tests/components/ct-vue-vite/src/components/SlotDefaultValue.vue @@ -0,0 +1,3 @@ + diff --git a/tests/components/ct-vue-vite/tests/slots/slots.spec.js b/tests/components/ct-vue-vite/tests/slots/slots.spec.js index a33c9dac92..7d36d9733b 100644 --- a/tests/components/ct-vue-vite/tests/slots/slots.spec.js +++ b/tests/components/ct-vue-vite/tests/slots/slots.spec.js @@ -2,6 +2,7 @@ import { test, expect } from '@playwright/experimental-ct-vue'; import DefaultSlot from '@/components/DefaultSlot.vue'; import NamedSlots from '@/components/NamedSlots.vue'; import Button from '@/components/Button.vue'; +import SlotDefaultValue from "@/components/SlotDefaultValue.vue"; test('render a default slot', async ({ mount }) => { 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('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'); +}); diff --git a/tests/components/ct-vue-vite/tests/slots/slots.spec.ts b/tests/components/ct-vue-vite/tests/slots/slots.spec.ts index a33c9dac92..7d36d9733b 100644 --- a/tests/components/ct-vue-vite/tests/slots/slots.spec.ts +++ b/tests/components/ct-vue-vite/tests/slots/slots.spec.ts @@ -2,6 +2,7 @@ import { test, expect } from '@playwright/experimental-ct-vue'; import DefaultSlot from '@/components/DefaultSlot.vue'; import NamedSlots from '@/components/NamedSlots.vue'; import Button from '@/components/Button.vue'; +import SlotDefaultValue from "@/components/SlotDefaultValue.vue"; test('render a default slot', async ({ mount }) => { 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('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'); +}); diff --git a/tests/config/utils.ts b/tests/config/utils.ts index 74158aeeae..3743e97d80 100644 --- a/tests/config/utils.ts +++ b/tests/config/utils.ts @@ -61,7 +61,7 @@ export function expectedSSLError(browserName: string, platform: string): RegExp else if (platform === 'win32') return /SSL peer certificate or SSH remote key was not OK/; else - return /Unacceptable TLS certificate/; + return /Unacceptable TLS certificate|Operation was cancelled/; } return /SSL_ERROR_UNKNOWN/; } diff --git a/tests/library/browsercontext-basic.spec.ts b/tests/library/browsercontext-basic.spec.ts index f075184cf9..df9db587d6 100644 --- a/tests/library/browsercontext-basic.spec.ts +++ b/tests/library/browsercontext-basic.spec.ts @@ -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.fixme(browserName === 'webkit' || browserName === 'firefox'); + it.fixme(browserName === 'firefox'); const context = await contextFactory({ javaScriptEnabled: false }); const page = await context.newPage(); expect(await page.evaluate(() => 1)).toBe(1); diff --git a/tests/library/browsercontext-cookies.spec.ts b/tests/library/browsercontext-cookies.spec.ts index 9bb600c786..f4dbefad58 100644 --- a/tests/library/browsercontext-cookies.spec.ts +++ b/tests/library/browsercontext-cookies.spec.ts @@ -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.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) => { res.setHeader('set-cookie', [ diff --git a/tests/library/browsercontext-page-event.spec.ts b/tests/library/browsercontext-page-event.spec.ts index b851dd0ce9..15e2b66163 100644 --- a/tests/library/browsercontext-page-event.spec.ts +++ b/tests/library/browsercontext-page-event.spec.ts @@ -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.fixme(browserName === 'firefox', 'Reports an opener in this case.'); - const context = await browser.newContext(); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); @@ -181,22 +179,6 @@ it('should work with Ctrl-clicking', async ({ browser, server, browserName }) => context.waitForEvent('page'), page.click('a', { modifiers: ['ControlOrMeta'] }), ]); - expect(await popup.opener()).toBe(null); + expect(await popup.opener()).toBe(browserName === 'firefox' ? page : null); 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('yo'); - 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(); -}); diff --git a/tests/library/browsercontext-proxy.spec.ts b/tests/library/browsercontext-proxy.spec.ts index 460c242998..58bda44632 100644 --- a/tests/library/browsercontext-proxy.spec.ts +++ b/tests/library/browsercontext-proxy.spec.ts @@ -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); proxyServer.forwardTo(server.PORT, { allowConnectRequests: true }); diff --git a/tests/library/browsercontext-viewport.spec.ts b/tests/library/browsercontext-viewport.spec.ts index 4fdd181f8f..356e330079 100644 --- a/tests/library/browsercontext-viewport.spec.ts +++ b/tests/library/browsercontext-viewport.spec.ts @@ -139,7 +139,9 @@ browserTest('should report null viewportSize when given null viewport', async ({ 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 }); await page.goto(server.PREFIX + '/drag-n-drop.html'); await page.hover('#source'); diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index d494f63b19..a732985b5b 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -761,7 +761,6 @@ for (const kind of ['launchServer', 'run-server'] as const) { }); test.describe('socks proxy', () => { - test.fixme(({ platform, browserName }) => browserName === 'webkit' && platform === 'win32'); test.skip(({ mode }) => mode !== 'default'); test.skip(kind === 'launchServer', 'not supported yet'); diff --git a/tests/library/capabilities.spec.ts b/tests/library/capabilities.spec.ts index 3f71c7db9e..864022a1f4 100644 --- a/tests/library/capabilities.spec.ts +++ b/tests/library/capabilities.spec.ts @@ -65,13 +65,9 @@ it('should respect CSP @smoke', async ({ page, server }) => { expect(await page.evaluate(() => window['testStatus'])).toBe('SUCCESS'); }); -it('should play video @smoke', async ({ page, asset, browserName, platform, macVersion, mode }) => { - // TODO: the test passes on Windows locally but fails on GitHub Action bot, - // apparently due to a Media Pack issue in the Windows Server. - // 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('should play video @smoke', async ({ page, asset, browserName, isWindows, isLinux, mode }) => { + it.skip(browserName === 'webkit' && isWindows, 'passes locally but fails on GitHub Action bot, apparently due to a Media Pack issue in the Windows Server'); + it.fixme(browserName === 'firefox' && isLinux, 'https://github.com/microsoft/playwright/issues/5721'); it.skip(mode.startsWith('service')); // 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.fixme(browserName === 'webkit' && platform === 'darwin' && macVersion === 11, 'Does not work on BigSur'); - it.fixme(browserName === 'webkit' && platform === 'win32'); + it.skip(browserName === 'webkit' && platform === 'win32', 'not supported'); it.skip(mode.startsWith('service')); 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.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'); await page.goto(server.EMPTY_PAGE); await page.setContent(``); @@ -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.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(``); 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.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.evaluate(() => { const result = new Promise(resolve => document.addEventListener('fullscreenchange', resolve)); diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index cadf49deb9..10c8a52235 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -728,8 +728,7 @@ test.describe('browser', () => { }); 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.fixme(browserName === 'webkit' && isLinux, 'WebKit on Linux does not support http2 https://bugs.webkit.org/show_bug.cgi?id=276990'); + test.skip(browserName === 'webkit' && isMac, 'WebKit on macOS does not proxy localhost'); test.skip(+process.versions.node.split('.')[0] < 20, 'http2.performServerHandshake is not supported in older Node.js versions'); const serverURL = await startCCServer({ http2: true }); diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts index daad405e70..3db8e968a2 100644 --- a/tests/library/clock.spec.ts +++ b/tests/library/clock.spec.ts @@ -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 const proto = { Date, setTimeout: () => {}, @@ -1167,8 +1167,10 @@ it.describe('stubTimers', () => { const { clock } = rawInstall(myGlobal, { now: 0, toFake: ['setTimeout'] }); expect(myGlobal.hasOwnProperty('setTimeout')).toBeTruthy(); + expect(myGlobal.setTimeout).not.toBe(proto.setTimeout); clock.uninstall(); - expect(myGlobal.hasOwnProperty('setTimeout')).toBeFalsy(); + expect(myGlobal.hasOwnProperty('setTimeout')).toBeTruthy(); + expect(myGlobal.setTimeout).toBe(proto.setTimeout); }); it('fakes Date constructor', ({ installEx }) => { diff --git a/tests/library/download.spec.ts b/tests/library/download.spec.ts index 9e6fb2b8e5..33e33f21b4 100644 --- a/tests/library/download.spec.ts +++ b/tests/library/download.spec.ts @@ -305,10 +305,8 @@ it.describe('download event', () => { }); 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) => { res.setHeader('Content-Type', 'application/octet-stream'); res.end(`Hello world`); diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index bbd4dcc49b..3a10347825 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -24,9 +24,9 @@ import type { Log } from '../../packages/trace/src/har'; import { parseHar } from '../config/utils'; const { createHttp2Server } = require('../../packages/playwright-core/lib/utils'); -async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise, testInfo: any, options: { outputPath?: string } & Partial> = {}) { +async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise, testInfo: any, options: { outputPath?: string, proxy?: BrowserContextOptions['proxy'] } & Partial> = {}) { 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(); return { page, @@ -858,6 +858,25 @@ it('should respect minimal mode for API Requests', async ({ contextFactory, serv 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) => { server.setRedirect('/redirect-me', '/simple.json'); const { page, getLog } = await pageWithHar(contextFactory, testInfo); diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index cfeb9711c9..5832ef44da 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -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.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 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.fixme(browserName === 'chromium', 'Click is offset by the infobar height'); - server.setRoute('/empty.html', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(``); @@ -250,8 +248,8 @@ it('should click in OOPIF', async ({ browserName, launchPersistent, server }) => expect(consoleLog).toContain('ok'); }); -it('should click bottom row w/ infobar in OOPIF', async ({ browserName, launchPersistent, server }) => { - it.fixme(browserName === 'chromium', 'Click is offset by the infobar height'); +it('should click bottom row w/ infobar in OOPIF', async ({ browserName, launchPersistent, server, isWindows }) => { + it.fixme(browserName === 'chromium' && isWindows, 'Click is offset by the infobar height'); server.setRoute('/empty.html', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index efde7d52f5..e4aa0a0786 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -106,7 +106,6 @@ await page.CloseAsync();`); }); 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(); await recorder.setContentAndWait(`
@@ -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.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551'); const { page, recorder } = await openRecorder(); await recorder.setContentAndWait(` @@ -168,7 +166,6 @@ await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-uplo }); 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(); await recorder.setContentAndWait(` diff --git a/tests/library/page-clock.spec.ts b/tests/library/page-clock.spec.ts index d541d5d135..a660e5e8a4 100644 --- a/tests/library/page-clock.spec.ts +++ b/tests/library/page-clock.spec.ts @@ -354,6 +354,7 @@ it.describe('popup', () => { page.waitForEvent('popup'), page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'), ]); + await popup.waitForLoadState(); const popupTime = await popup.evaluate('time'); expect(popupTime).toBe(1000); }); diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts index 0797a7e18b..f9f7fdd7ce 100644 --- a/tests/library/permissions.spec.ts +++ b/tests/library/permissions.spec.ts @@ -176,7 +176,8 @@ it('should support clipboard read', async ({ page, context, server, browserName, it('storage access', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31227' } }, async ({ page, context, server, browserName }) => { - it.fixme(browserName !== 'chromium'); + it.skip(browserName !== 'chromium', 'chromium-only api'); + await context.grantPermissions(['storage-access']); expect(await getPermission(page, 'storage-access')).toBe('granted'); server.setRoute('/set-cookie.html', (req, res) => { diff --git a/tests/library/popup.spec.ts b/tests/library/popup.spec.ts index 56865bdb53..1dcede604b 100644 --- a/tests/library/popup.spec.ts +++ b/tests/library/popup.spec.ts @@ -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.fixme(browserName === 'firefox'); + it.fixme(browserName === 'firefox', 'locator.click: Target page, context or browser has been closed'); + await page.goto(server.EMPTY_PAGE); const [popup] = await Promise.all([ page.waitForEvent('popup'), - page.evaluate(() => { + page.evaluate(async browserName => { const w = window.open('about:blank'); + if (browserName === 'firefox') + await new Promise(x => w.onload = x); w.document.body.innerHTML = ``; - }), + }, browserName), ]); await popup.getByRole('button').click(); }); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index f369051a69..0eda4b092a 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -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.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) => { - res.writeHead(200); + res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(``); }); server.setRoute('/level-1.html', (req, res) => { - res.writeHead(200); + res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(``); }); server.setRoute('/level-2.html', (req, res) => { - res.writeHead(200); + res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(``); }); diff --git a/tests/page/elementhandle-bounding-box.spec.ts b/tests/page/elementhandle-bounding-box.spec.ts index 215dce0222..c86d516e19 100644 --- a/tests/page/elementhandle-bounding-box.spec.ts +++ b/tests/page/elementhandle-bounding-box.spec.ts @@ -20,8 +20,6 @@ import { test as it, expect } from './pageTest'; it.skip(({ isAndroid }) => isAndroid); it('should work', async ({ page, server, browserName, headless, isLinux }) => { - it.fixme(browserName === 'firefox' && !headless && !isLinux); - await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const elementHandle = await page.$('.box:nth-of-type(13)'); diff --git a/tests/page/elementhandle-wait-for-element-state.spec.ts b/tests/page/elementhandle-wait-for-element-state.spec.ts index bbccf620c7..ae818339e4 100644 --- a/tests/page/elementhandle-wait-for-element-state.spec.ts +++ b/tests/page/elementhandle-wait-for-element-state.spec.ts @@ -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.fixme(browserName === 'firefox' && platform === 'linux'); - await page.goto(server.PREFIX + '/input/button.html'); const button = await page.$('button'); await page.$eval('button', button => { diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index 36aa641510..2242a55378 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -349,7 +349,8 @@ test.describe('toBeInViewport', () => { }); 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(`
diff --git a/tests/page/frame-evaluate.spec.ts b/tests/page/frame-evaluate.spec.ts index b7feb2c0d6..16bcf6eac7 100644 --- a/tests/page/frame-evaluate.spec.ts +++ b/tests/page/frame-evaluate.spec.ts @@ -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.fail(browserName === 'chromium'); - it.fixme(browserName === 'firefox'); + it.fixme(browserName !== 'webkit'); // - Firefox does not report domcontentloaded for the iframe. // - Chromium and Firefox report empty url. diff --git a/tests/page/frame-goto.spec.ts b/tests/page/frame-goto.spec.ts index 56146572f2..6603282bb3 100644 --- a/tests/page/frame-goto.spec.ts +++ b/tests/page/frame-goto.spec.ts @@ -43,8 +43,9 @@ it('should reject when frame detaches', async ({ page, server, browserName }) => 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(browserName === 'firefox', 'script.js is requested before navigationCommitted arrives'); server.setRoute('/frames/script.js', () => {}); const url = server.PREFIX + '/frames/child-redirect.html'; diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index e50b5fdb08..9447a80fcd 100644 --- a/tests/page/interception.spec.ts +++ b/tests/page/interception.spec.ts @@ -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', { 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.fixme(browserName === 'chromium'); + it.skip(browserName === 'chromium' && browserMajorVersion < 130, 'fixed in Chromium 130'); await page.goto(server.EMPTY_PAGE); server.setRoute('/data_for_worker', (req, res) => res.end('failed to intercept')); diff --git a/tests/page/locator-misc-1.spec.ts b/tests/page/locator-misc-1.spec.ts index 0174536898..616beb3e7f 100644 --- a/tests/page/locator-misc-1.spec.ts +++ b/tests/page/locator-misc-1.spec.ts @@ -25,7 +25,9 @@ it('should hover @smoke', async ({ page, server }) => { 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.evaluate(() => delete window['Node']); const button = page.locator('#button-6'); diff --git a/tests/page/locator-misc-2.spec.ts b/tests/page/locator-misc-2.spec.ts index eb5765dbf1..4eaf5972a0 100644 --- a/tests/page/locator-misc-2.spec.ts +++ b/tests/page/locator-misc-2.spec.ts @@ -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.fixme(isAndroid || isElectron || isWebView2); +it('should scroll zero-sized element into view', async ({ page, isAndroid, isElectron, browserName, isMac, macVersion }) => { + it.fixme(isAndroid || isElectron); it.skip(browserName === 'webkit' && isMac && macVersion < 11, 'WebKit for macOS 10.15 is frozen.'); 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.fixme(browserName === 'firefox' && !headless && !isLinux); it.skip(isAndroid); await page.setViewportSize({ width: 500, height: 500 }); diff --git a/tests/page/page-accessibility.spec.ts b/tests/page/page-accessibility.spec.ts index 63a267ec94..c6619b7f9c 100644 --- a/tests/page/page-accessibility.spec.ts +++ b/tests/page/page-accessibility.spec.ts @@ -143,9 +143,8 @@ it('should not report text nodes inside controls', async function({ page, browse 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(isWebView2, 'WebView2 is missing a Chromium fix'); await page.setContent(`
@@ -177,9 +176,8 @@ it('rich text editable fields should have children', async function({ page, brow 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(isWebView2, 'WebView2 is missing a Chromium fix'); await page.setContent(`
diff --git a/tests/page/page-autowaiting-no-hang.spec.ts b/tests/page/page-autowaiting-no-hang.spec.ts index f580b8629a..6fc7a088af 100644 --- a/tests/page/page-autowaiting-no-hang.spec.ts +++ b/tests/page/page-autowaiting-no-hang.spec.ts @@ -15,7 +15,7 @@ * 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 }) => { 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), ]); }); + +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; +}); diff --git a/tests/page/page-click-react.spec.ts b/tests/page/page-click-react.spec.ts index 35626910c1..f1ee9af3da 100644 --- a/tests/page/page-click-react.spec.ts +++ b/tests/page/page-click-react.spec.ts @@ -21,47 +21,6 @@ declare const renderComponent; declare const e; 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 }) => { const dialogPromise = page.waitForEvent('dialog'); await page.setContent(`
Click me
`); @@ -71,40 +30,6 @@ it('should timeout when click opens alert', async ({ page, server }) => { 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 }) => { await page.goto(server.PREFIX + '/react.html'); await page.evaluate(() => { diff --git a/tests/page/page-click-scroll.spec.ts b/tests/page/page-click-scroll.spec.ts index 758a8d0eb9..d3e55cdffb 100644 --- a/tests/page/page-click-scroll.spec.ts +++ b/tests/page/page-click-scroll.spec.ts @@ -78,9 +78,8 @@ it('should scroll into view display:contents with position', async ({ page, brow 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.fixme(isWebView2); await page.setContent(``); const error = await page.locator('input').click({ force: true, timeout: 2000 }).catch(e => e); diff --git a/tests/page/page-click.spec.ts b/tests/page/page-click.spec.ts index aa02d210eb..fccef2ed0e 100644 --- a/tests/page/page-click.spec.ts +++ b/tests/page/page-click.spec.ts @@ -94,7 +94,9 @@ it('should not throw UnhandledPromiseRejection when page closes', async ({ page, ]).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(`
`); await page.click('div'); 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.fixme(browserName === 'chromium' || browserName === 'webkit'); + it.fixme(browserName === 'chromium'); // @see https://github.com/GoogleChrome/puppeteer/issues/4110 // @see https://bugs.chromium.org/p/chromium/issues/detail?id=986390 diff --git a/tests/page/page-mouse.spec.ts b/tests/page/page-mouse.spec.ts index ae8ce84c7b..f93ca4e2e2 100644 --- a/tests/page/page-mouse.spec.ts +++ b/tests/page/page-mouse.spec.ts @@ -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.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/20823' }); - it.fixme(browserName === 'firefox'); - it.skip(browserName === 'chromium' && isWindows, 'context menu support is best-effort for Linux and MacOS'); + it.fixme(browserName === 'chromium' && isWindows, 'context menu support is best-effort for Linux and MacOS'); await page.evaluate(() => { window['contextMenuPromise'] = new Promise(x => { window.addEventListener('contextmenu', x, false); diff --git a/tests/page/page-request-fulfill.spec.ts b/tests/page/page-request-fulfill.spec.ts index 8170cb73d5..3edcd0ba0a 100644 --- a/tests/page/page-request-fulfill.spec.ts +++ b/tests/page/page-request-fulfill.spec.ts @@ -309,8 +309,10 @@ it('should fetch original request and fulfill', async ({ page, server, isElectro 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(isAndroid, 'Android does not have an isolated context per test, so we get cookies from other tests'); + const cookies = ['a=b', 'c=d']; await page.route('**/multiple-set-cookie.html', async route => { void route.fulfill({ diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index c441641b4d..07e53e4a71 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -291,10 +291,9 @@ it.describe('page screenshot', () => { 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.skip(isElectron); - it.skip(isWebView2); await page.goto('data:text/html,'); await page.evaluate(() => { const canvas = document.querySelector('canvas'); diff --git a/tests/page/page-wait-for-selector-2.spec.ts b/tests/page/page-wait-for-selector-2.spec.ts index 5a23e581c7..4f473f86f2 100644 --- a/tests/page/page-wait-for-selector-2.spec.ts +++ b/tests/page/page-wait-for-selector-2.spec.ts @@ -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); 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(``); + 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(``); + 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(``); + const button = await page.$('button'); + const promise = button.waitForSelector('something', { state: 'detached' }); + await page.waitForTimeout(100); + await page.evaluate(() => document.body.innerText = ''); + await promise; +}); diff --git a/tests/page/selectors-css.spec.ts b/tests/page/selectors-css.spec.ts index 172493dbea..3c34bd5c55 100644 --- a/tests/page/selectors-css.spec.ts +++ b/tests/page/selectors-css.spec.ts @@ -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.fixme(browserName === 'firefox', 'Should enable once Firefox supports this syntax'); it.skip(browserName === 'chromium' && browserMajorVersion < 111, 'https://caniuse.com/css-nth-child-of'); await page.setContent(` diff --git a/tests/page/selectors-frame.spec.ts b/tests/page/selectors-frame.spec.ts index c32a00e8e9..fda55e9958 100644 --- a/tests/page/selectors-frame.spec.ts +++ b/tests/page/selectors-frame.spec.ts @@ -308,17 +308,6 @@ it('click should survive navigation', async ({ page, server }) => { 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 }) => { await routeIframe(page); await page.setContent('
'); diff --git a/tests/webview2/webview2-app/webview2.csproj b/tests/webview2/webview2-app/webview2.csproj index 84849567c4..4636d47030 100644 --- a/tests/webview2/webview2-app/webview2.csproj +++ b/tests/webview2/webview2-app/webview2.csproj @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/utils/flakiness-dashboard/host.json b/utils/flakiness-dashboard/host.json index 6432c26df3..851a54849c 100644 --- a/utils/flakiness-dashboard/host.json +++ b/utils/flakiness-dashboard/host.json @@ -12,6 +12,9 @@ "queues": { "batchSize": 1, "newBatchThreshold": 0 + }, + "blobs": { + "maxDegreeOfParallelism": 1 } }, "extensionBundle": { diff --git a/utils/markdown.js b/utils/markdown.js index 3e206ade3b..faf62f1951 100644 --- a/utils/markdown.js +++ b/utils/markdown.js @@ -46,6 +46,7 @@ * lines: string[], * codeLang: string, * title?: string, + * highlight?: string, * }} MarkdownCodeNode */ /** @typedef {MarkdownBaseNode & { @@ -165,13 +166,14 @@ function buildTree(lines) { // Remaining items respect indent-based nesting. const [, indent, content] = /** @type {string[]} */ (line.match('^([ ]*)(.*)')); if (content.startsWith('```')) { - const [codeLang, title] = parseCodeBlockMetadata(content); + const [codeLang, title, highlight] = parseCodeBlockMetadata(content); /** @type {MarkdownNode} */ const node = { type: 'code', lines: [], codeLang, title, + highlight, }; line = lines[++i]; while (!line.trim().startsWith('```')) { @@ -255,14 +257,18 @@ function buildTree(lines) { /** * @param {String} firstLine - * @returns {[string, string|undefined]} + * @returns {[string, string|undefined, string|undefined]} */ function parseCodeBlockMetadata(firstLine) { const withoutBackticks = firstLine.substring(3); - const match = withoutBackticks.match(/ title="(.+)"$/); - if (match) - return [withoutBackticks.substring(0, match.index), match[1]]; - return [withoutBackticks, undefined]; + const titleMatch = withoutBackticks.match(/ title="(.+)"/); + const highlightMatch = withoutBackticks.match(/\{.*\}/); + + 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') { 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) result.push(`${indent}// ${node.title}`); for (const line of node.lines)