- {href ?
{content} : content}
+const InnerMetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ metadataEntries }) => {
+ const gitCommitInfo = metadataEntries.find(([key]) => key === 'git.commit.info')?.[1] as GitCommitInfo | undefined;
+ const entries = metadataEntries.filter(([key]) => key !== 'git.commit.info');
+ if (!gitCommitInfo && !entries.length)
+ return null;
+ return
+ {gitCommitInfo && <>
+
+ {entries.length > 0 &&
}
+ >}
+ {entries.map(([key, value]) => {
+ const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
+ const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
+ return
+ {key}
+ {valueString && : {linkifyText(trimmedValue)} }
+
;
+ })}
+ ;
+};
+
+const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
+ const email = info['revision.email'] ? ` <${info['revision.email']}>` : '';
+ const author = `${info['revision.author'] || ''}${email}`;
+ const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info['revision.timestamp']);
+ const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info['revision.timestamp']);
+ return
;
};
diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx
index cf0f5e5e56..e48064201c 100644
--- a/packages/html-reporter/src/reportView.tsx
+++ b/packages/html-reporter/src/reportView.tsx
@@ -23,8 +23,6 @@ import { HeaderView } from './headerView';
import { Route, SearchParamsContext } from './links';
import type { LoadedReport } from './loadedReport';
import './reportView.css';
-import type { Metainfo } from './metadataView';
-import { MetadataView } from './metadataView';
import { TestCaseView } from './testCaseView';
import { TestFilesHeader, TestFilesView } from './testFilesView';
import './theme.css';
@@ -50,6 +48,7 @@ export const ReportView: React.FC<{
const searchParams = React.useContext(SearchParamsContext);
const [expandedFiles, setExpandedFiles] = React.useState
>(new Map());
const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
+ const [metadataVisible, setMetadataVisible] = React.useState(false);
const testIdToFileIdMap = React.useMemo(() => {
const map = new Map();
@@ -76,9 +75,8 @@ export const ReportView: React.FC<{
return
{report?.json() && }
- {report?.json().metadata && }
-
+ setMetadataVisible(visible => !visible)}/>
= ({ report, filteredStats }) => {
+ metadataVisible: boolean,
+ toggleMetadataVisible: () => void,
+}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
if (!report)
return;
+ const metadataEntries = filterMetadata(report.metadata || {});
return <>
-
+
+ {metadataEntries.length > 0 &&
+ {metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
+
}
{report.projectNames.length === 1 && !!report.projectNames[0] &&
Project: {report.projectNames[0]}
}
{filteredStats &&
Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}
}
{report ? new Date(report.startTime).toLocaleString() : ''}
Total time: {msToString(report.duration ?? 0)}
+ {metadataVisible &&
}
{!!report.errors.length &&
{report.errors.map((error, index) => )}
}
diff --git a/packages/html-reporter/tsconfig.json b/packages/html-reporter/tsconfig.json
index 4fe82eab4e..e642923b96 100644
--- a/packages/html-reporter/tsconfig.json
+++ b/packages/html-reporter/tsconfig.json
@@ -20,6 +20,7 @@
"@protocol/*": ["../protocol/src/*"],
"@web/*": ["../web/src/*"],
"@playwright/*": ["../playwright/src/*"],
+ "@testIsomorphic/*": ["../playwright/src/isomorphic/*"],
"playwright-core/lib/*": ["../playwright-core/src/*"],
"playwright/lib/*": ["../playwright/src/*"],
}
diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt
index 2dc33e8c8f..cc6ee5c884 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@7.1.3 (https://github.com/TooTallNate/proxy-agents)
+- agent-base@6.0.2 (https://github.com/TooTallNate/node-agent-base)
- 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)
@@ -24,7 +24,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@7.0.6 (https://github.com/TooTallNate/proxy-agents)
+- https-proxy-agent@5.0.1 (https://github.com/TooTallNate/node-https-proxy-agent)
- 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)
@@ -43,7 +43,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@8.0.5 (https://github.com/TooTallNate/proxy-agents)
+- socks-proxy-agent@6.1.1 (https://github.com/TooTallNate/node-socks-proxy-agent)
- 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)
@@ -105,11 +105,128 @@ MIT License
=========================================
END OF @types/yauzl@2.10.0 AND INFORMATION
-%% agent-base@7.1.3 NOTICES AND INFORMATION BEGIN HERE
+%% agent-base@6.0.2 NOTICES AND INFORMATION BEGIN HERE
=========================================
+agent-base
+==========
+### Turn a function into an [`http.Agent`][http.Agent] instance
+[](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
+Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -129,8 +246,14 @@ 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@7.1.3 AND INFORMATION
+END OF agent-base@6.0.2 AND INFORMATION
%% balanced-match@1.0.2 NOTICES AND INFORMATION BEGIN HERE
=========================================
@@ -542,11 +665,124 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
=========================================
END OF graceful-fs@4.2.10 AND INFORMATION
-%% https-proxy-agent@7.0.6 NOTICES AND INFORMATION BEGIN HERE
+%% https-proxy-agent@5.0.1 NOTICES AND INFORMATION BEGIN HERE
=========================================
+https-proxy-agent
+================
+### An HTTP(s) proxy `http.Agent` implementation for HTTPS
+[](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
+Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -566,8 +802,10 @@ 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@7.0.6 AND INFORMATION
+END OF https-proxy-agent@5.0.1 AND INFORMATION
%% ip-address@9.0.5 NOTICES AND INFORMATION BEGIN HERE
=========================================
@@ -1005,11 +1243,141 @@ 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@8.0.5 NOTICES AND INFORMATION BEGIN HERE
+%% socks-proxy-agent@6.1.1 NOTICES AND INFORMATION BEGIN HERE
=========================================
+socks-proxy-agent
+================
+### A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS
+[](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
+Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -1030,7 +1398,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@8.0.5 AND INFORMATION
+END OF socks-proxy-agent@6.1.1 AND INFORMATION
%% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE
=========================================
diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json
index 1826120327..94e6fd41f3 100644
--- a/packages/playwright-core/browsers.json
+++ b/packages/playwright-core/browsers.json
@@ -9,9 +9,9 @@
},
{
"name": "chromium-tip-of-tree",
- "revision": "1297",
+ "revision": "1298",
"installByDefault": false,
- "browserVersion": "134.0.6974.0"
+ "browserVersion": "134.0.6984.0"
},
{
"name": "firefox",
diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json
index 44aabaca9d..aae091493b 100644
--- a/packages/playwright-core/bundles/utils/package-lock.json
+++ b/packages/playwright-core/bundles/utils/package-lock.json
@@ -1,7 +1,7 @@
{
"name": "utils-bundle",
"version": "0.0.1",
- "lockfileVersion": 2,
+ "lockfileVersion": 3,
"requires": true,
"packages": {
"": {
@@ -14,7 +14,7 @@
"diff": "^7.0.0",
"dotenv": "^16.4.5",
"graceful-fs": "4.2.10",
- "https-proxy-agent": "7.0.6",
+ "https-proxy-agent": "5.0.1",
"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": "8.0.5",
+ "socks-proxy-agent": "6.1.1",
"stack-utils": "2.0.5",
"ws": "8.17.1",
"yaml": "^2.6.0"
@@ -140,12 +140,14 @@
}
},
"node_modules/agent-base": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
- "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
- "license": "MIT",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dependencies": {
+ "debug": "4"
+ },
"engines": {
- "node": ">= 14"
+ "node": ">= 6.0.0"
}
},
"node_modules/balanced-match": {
@@ -241,16 +243,15 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"node_modules/https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
- "license": "MIT",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dependencies": {
- "agent-base": "^7.1.2",
+ "agent-base": "6",
"debug": "4"
},
"engines": {
- "node": ">= 14"
+ "node": ">= 6"
}
},
"node_modules/ip-address": {
@@ -400,17 +401,16 @@
}
},
"node_modules/socks-proxy-agent": {
- "version": "8.0.5",
- "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
- "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
- "license": "MIT",
+ "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==",
"dependencies": {
- "agent-base": "^7.1.2",
- "debug": "^4.3.4",
- "socks": "^2.8.3"
+ "agent-base": "^6.0.2",
+ "debug": "^4.3.1",
+ "socks": "^2.6.1"
},
"engines": {
- "node": ">= 14"
+ "node": ">= 10"
}
},
"node_modules/sprintf-js": {
@@ -460,312 +460,5 @@
"node": ">= 14"
}
}
- },
- "dependencies": {
- "@types/debug": {
- "version": "4.1.7",
- "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
- "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
- "dev": true,
- "requires": {
- "@types/ms": "*"
- }
- },
- "@types/diff": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/@types/diff/-/diff-6.0.0.tgz",
- "integrity": "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==",
- "dev": true
- },
- "@types/mime": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
- "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
- "dev": true
- },
- "@types/minimatch": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
- "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
- "dev": true
- },
- "@types/ms": {
- "version": "0.7.31",
- "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
- "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
- "dev": true
- },
- "@types/node": {
- "version": "17.0.25",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz",
- "integrity": "sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==",
- "dev": true
- },
- "@types/pngjs": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.1.tgz",
- "integrity": "sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==",
- "dev": true,
- "requires": {
- "@types/node": "*"
- }
- },
- "@types/progress": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.5.tgz",
- "integrity": "sha512-ZYYVc/kSMkhH9W/4dNK/sLNra3cnkfT2nJyOAIDY+C2u6w72wa0s1aXAezVtbTsnN8HID1uhXCrLwDE2ZXpplg==",
- "dev": true,
- "requires": {
- "@types/node": "*"
- }
- },
- "@types/proper-lockfile": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
- "integrity": "sha512-kd4LMvcnpYkspDcp7rmXKedn8iJSCoa331zRRamUp5oanKt/CefbEGPQP7G89enz7sKD4bvsr8mHSsC8j5WOvA==",
- "dev": true,
- "requires": {
- "@types/retry": "*"
- }
- },
- "@types/proxy-from-env": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@types/proxy-from-env/-/proxy-from-env-1.0.1.tgz",
- "integrity": "sha512-luG++TFHyS61eKcfkR1CVV6a1GMNXDjtqEQIIfaSHax75xp0HU3SlezjOi1yqubJwrG8e9DeW59n6wTblIDwFg==",
- "dev": true,
- "requires": {
- "@types/node": "*"
- }
- },
- "@types/retry": {
- "version": "0.12.2",
- "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
- "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
- "dev": true
- },
- "@types/stack-utils": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
- "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
- "dev": true
- },
- "@types/ws": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz",
- "integrity": "sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg==",
- "dev": true,
- "requires": {
- "@types/node": "*"
- }
- },
- "agent-base": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
- "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="
- },
- "balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
- },
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "colors": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
- "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
- },
- "commander": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
- "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
- },
- "concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
- },
- "debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "requires": {
- "ms": "2.1.2"
- }
- },
- "define-lazy-prop": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
- "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="
- },
- "diff": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
- "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="
- },
- "dotenv": {
- "version": "16.4.5",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
- "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
- },
- "escape-string-regexp": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
- "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="
- },
- "graceful-fs": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
- "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
- },
- "https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
- "requires": {
- "agent-base": "^7.1.2",
- "debug": "4"
- }
- },
- "ip-address": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
- "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
- "requires": {
- "jsbn": "1.1.0",
- "sprintf-js": "^1.1.3"
- }
- },
- "is-docker": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
- "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="
- },
- "is-wsl": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
- "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
- "requires": {
- "is-docker": "^2.0.0"
- }
- },
- "jpeg-js": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
- "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="
- },
- "jsbn": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
- "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
- },
- "mime": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
- "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="
- },
- "minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "requires": {
- "brace-expansion": "^1.1.7"
- }
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
- },
- "open": {
- "version": "8.4.0",
- "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz",
- "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==",
- "requires": {
- "define-lazy-prop": "^2.0.0",
- "is-docker": "^2.1.1",
- "is-wsl": "^2.2.0"
- }
- },
- "pngjs": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
- "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="
- },
- "progress": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
- "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
- },
- "proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
- },
- "retry": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
- "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="
- },
- "signal-exit": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
- },
- "smart-buffer": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
- "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
- },
- "socks": {
- "version": "2.8.3",
- "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
- "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
- "requires": {
- "ip-address": "^9.0.5",
- "smart-buffer": "^4.2.0"
- }
- },
- "socks-proxy-agent": {
- "version": "8.0.5",
- "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
- "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
- "requires": {
- "agent-base": "^7.1.2",
- "debug": "^4.3.4",
- "socks": "^2.8.3"
- }
- },
- "sprintf-js": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
- "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
- },
- "stack-utils": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz",
- "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==",
- "requires": {
- "escape-string-regexp": "^2.0.0"
- }
- },
- "ws": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
- "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
- "requires": {}
- },
- "yaml": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
- "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="
- }
}
}
diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json
index a409cca037..3c2c382e0f 100644
--- a/packages/playwright-core/bundles/utils/package.json
+++ b/packages/playwright-core/bundles/utils/package.json
@@ -15,7 +15,7 @@
"diff": "^7.0.0",
"dotenv": "^16.4.5",
"graceful-fs": "4.2.10",
- "https-proxy-agent": "7.0.6",
+ "https-proxy-agent": "5.0.1",
"jpeg-js": "0.4.4",
"mime": "^3.0.0",
"minimatch": "^3.1.2",
@@ -25,7 +25,7 @@
"proxy-from-env": "1.1.0",
"retry": "0.12.0",
"signal-exit": "3.0.7",
- "socks-proxy-agent": "8.0.5",
+ "socks-proxy-agent": "6.1.1",
"stack-utils": "2.0.5",
"ws": "8.17.1",
"yaml": "^2.6.0"
diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts
index f231c907c0..336203f33b 100644
--- a/packages/playwright-core/src/server/fetch.ts
+++ b/packages/playwright-core/src/server/fetch.ts
@@ -20,6 +20,7 @@ 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';
@@ -499,12 +500,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();
@@ -521,21 +522,11 @@ 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))
@@ -702,16 +693,17 @@ export class GlobalAPIRequestContext extends APIRequestContext {
}
export function createProxyAgent(proxy: types.ProxySettings) {
- const proxyURL = new URL(proxy.server);
- if (proxyURL.protocol?.startsWith('socks'))
- return new SocksProxyAgent(proxyURL);
-
+ const proxyOpts = url.parse(proxy.server);
+ if (proxyOpts.protocol?.startsWith('socks')) {
+ return new SocksProxyAgent({
+ host: proxyOpts.hostname,
+ port: proxyOpts.port || undefined,
+ });
+ }
if (proxy.username)
- 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);
+ 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);
}
function toHeadersArray(rawHeaders: string[]): types.HeadersArray {
diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts
index 1d1c60e6ce..df314ed4de 100644
--- a/packages/playwright-core/src/server/firefox/ffBrowser.ts
+++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts
@@ -435,4 +435,6 @@ function toJugglerProxyOptions(proxy: types.ProxySettings) {
// Prefs for quick fixes that didn't make it to the build.
// Should all be moved to `playwright.cfg`.
-const kBandaidFirefoxUserPrefs = {};
+const kBandaidFirefoxUserPrefs = {
+ 'dom.fetchKeepalive.enabled': false,
+};
diff --git a/packages/playwright-core/src/server/injected/recorder/pollingRecorder.ts b/packages/playwright-core/src/server/injected/recorder/pollingRecorder.ts
index bc96ff2185..86b2babdd9 100644
--- a/packages/playwright-core/src/server/injected/recorder/pollingRecorder.ts
+++ b/packages/playwright-core/src/server/injected/recorder/pollingRecorder.ts
@@ -34,6 +34,7 @@ export class PollingRecorder implements RecorderDelegate {
private _recorder: Recorder;
private _embedder: Embedder;
private _pollRecorderModeTimer: number | undefined;
+ private _lastStateJSON: string | undefined;
constructor(injectedScript: InjectedScript) {
this._recorder = new Recorder(injectedScript);
@@ -42,6 +43,7 @@ export class PollingRecorder implements RecorderDelegate {
injectedScript.onGlobalListenersRemoved.add(() => this._recorder.installListeners());
const refreshOverlay = () => {
+ this._lastStateJSON = undefined;
this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console
};
this._embedder.__pw_refreshOverlay = refreshOverlay;
@@ -57,13 +59,19 @@ export class PollingRecorder implements RecorderDelegate {
this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod);
return;
}
- const win = this._recorder.document.defaultView!;
- if (win.top !== win) {
- // Only show action point in the main frame, since it is relative to the page's viewport.
- // Otherwise we'll see multiple action points at different locations.
- state.actionPoint = undefined;
+
+ const stringifiedState = JSON.stringify(state);
+ if (this._lastStateJSON !== stringifiedState) {
+ this._lastStateJSON = stringifiedState;
+ const win = this._recorder.document.defaultView!;
+ if (win.top !== win) {
+ // Only show action point in the main frame, since it is relative to the page's viewport.
+ // Otherwise we'll see multiple action points at different locations.
+ state.actionPoint = undefined;
+ }
+ this._recorder.setUIState(state, this);
}
- this._recorder.setUIState(state, this);
+
this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod);
}
diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts
index 5ed6fd7b85..a7b3cd1d4b 100644
--- a/packages/playwright-core/src/server/injected/roleUtils.ts
+++ b/packages/playwright-core/src/server/injected/roleUtils.ts
@@ -354,27 +354,34 @@ export function getPseudoContent(element: Element, pseudo: '::before' | '::after
if (cache?.has(element))
return cache?.get(element) || '';
const pseudoStyle = getElementComputedStyle(element, pseudo);
- const content = getPseudoContentImpl(pseudoStyle);
+ const content = getPseudoContentImpl(element, pseudoStyle);
if (cache)
cache.set(element, content);
return content;
}
-function getPseudoContentImpl(pseudoStyle: CSSStyleDeclaration | undefined) {
+function getPseudoContentImpl(element: Element, pseudoStyle: CSSStyleDeclaration | undefined) {
// Note: all browsers ignore display:none and visibility:hidden pseudos.
if (!pseudoStyle || pseudoStyle.display === 'none' || pseudoStyle.visibility === 'hidden')
return '';
const content = pseudoStyle.content;
+ let resolvedContent: string | undefined;
if ((content[0] === '\'' && content[content.length - 1] === '\'') ||
(content[0] === '"' && content[content.length - 1] === '"')) {
- const unquoted = content.substring(1, content.length - 1);
+ resolvedContent = content.substring(1, content.length - 1);
+ } else if (content.startsWith('attr(') && content.endsWith(')')) {
+ // Firefox does not resolve attribute accessors in content.
+ const attrName = content.substring('attr('.length, content.length - 1).trim();
+ resolvedContent = element.getAttribute(attrName) || '';
+ }
+ if (resolvedContent !== undefined) {
// SPEC DIFFERENCE.
// Spec says "CSS textual content, without a space", but we account for display
// to pass "name_file-label-inline-block-styles-manual.html"
const display = pseudoStyle.display || 'inline';
if (display !== 'inline')
- return ' ' + unquoted + ' ';
- return unquoted;
+ return ' ' + resolvedContent + ' ';
+ return resolvedContent;
}
return '';
}
diff --git a/packages/playwright-core/src/server/recorder/recorderCollection.ts b/packages/playwright-core/src/server/recorder/recorderCollection.ts
index 162d5d6813..9f28efd930 100644
--- a/packages/playwright-core/src/server/recorder/recorderCollection.ts
+++ b/packages/playwright-core/src/server/recorder/recorderCollection.ts
@@ -85,7 +85,7 @@ export class RecorderCollection extends EventEmitter {
let generateGoto = false;
if (!lastAction)
generateGoto = true;
- else if (lastAction.action.name !== 'click' && lastAction.action.name !== 'press')
+ else if (lastAction.action.name !== 'click' && lastAction.action.name !== 'press' && lastAction.action.name !== 'fill')
generateGoto = true;
else if (timestamp - lastAction.startTime > signalThreshold)
generateGoto = true;
diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts
index 2517ea24ce..6413cd1ddf 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.connect(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false });
+ this.target = await this.socksProxy.proxyAgentFromOptions.callback(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 25d3a11156..b800a8773d 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 = new URL(proxyURL);
+ const parsedProxyURL = url.parse(proxyURL);
if (params.url.startsWith('http:')) {
options = {
path: parsedUrl.href,
diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts
index 443cb58319..b55c3d6527 100644
--- a/packages/playwright/src/common/config.ts
+++ b/packages/playwright/src/common/config.ts
@@ -46,6 +46,7 @@ export class FullConfigInternal {
readonly plugins: TestRunnerPluginRegistration[];
readonly projects: FullProjectInternal[] = [];
readonly singleTSConfigPath?: string;
+ readonly populateGitInfo: boolean;
cliArgs: string[] = [];
cliGrep: string | undefined;
cliGrepInvert: string | undefined;
@@ -75,10 +76,15 @@ export class FullConfigInternal {
const privateConfiguration = (userConfig as any)['@playwright/test'];
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig);
+ this.populateGitInfo = takeFirst(userConfig.populateGitInfo, false);
this.globalSetups = (Array.isArray(userConfig.globalSetup) ? userConfig.globalSetup : [userConfig.globalSetup]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined);
this.globalTeardowns = (Array.isArray(userConfig.globalTeardown) ? userConfig.globalTeardown : [userConfig.globalTeardown]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined);
+ // Make sure we reuse same metadata instance between FullConfigInternal instances,
+ // so that plugins such as gitCommitInfoPlugin can populate metadata once.
+ userConfig.metadata = userConfig.metadata || {};
+
this.config = {
configFile: resolvedConfigFile,
rootDir: pathResolve(configDir, userConfig.testDir) || configDir,
@@ -90,7 +96,7 @@ export class FullConfigInternal {
grep: takeFirst(userConfig.grep, defaultGrep),
grepInvert: takeFirst(userConfig.grepInvert, null),
maxFailures: takeFirst(configCLIOverrides.debug ? 1 : undefined, configCLIOverrides.maxFailures, userConfig.maxFailures, 0),
- metadata: takeFirst(userConfig.metadata, {}),
+ metadata: userConfig.metadata,
preserveOutput: takeFirst(userConfig.preserveOutput, 'always'),
reporter: takeFirst(configCLIOverrides.reporter, resolveReporters(userConfig.reporter, configDir), [[defaultReporter]]),
reportSlowTests: takeFirst(userConfig.reportSlowTests, { max: 5, threshold: 15000 }),
@@ -164,7 +170,7 @@ export class FullProjectInternal {
readonly fullyParallel: boolean;
readonly expect: Project['expect'];
readonly respectGitIgnore: boolean;
- readonly snapshotPathTemplate: string;
+ readonly snapshotPathTemplate: string | undefined;
readonly ignoreSnapshots: boolean;
id = '';
deps: FullProjectInternal[] = [];
@@ -173,8 +179,7 @@ export class FullProjectInternal {
constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, packageJsonDir: string) {
this.fullConfig = fullConfig;
const testDir = takeFirst(pathResolve(configDir, projectConfig.testDir), pathResolve(configDir, config.testDir), fullConfig.configDir);
- const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
- this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
+ this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate);
this.project = {
grep: takeFirst(projectConfig.grep, config.grep, defaultGrep),
diff --git a/packages/playwright/src/isomorphic/types.d.ts b/packages/playwright/src/isomorphic/types.d.ts
new file mode 100644
index 0000000000..72c6db3533
--- /dev/null
+++ b/packages/playwright/src/isomorphic/types.d.ts
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface GitCommitInfo {
+ 'revision.id'?: string;
+ 'revision.author'?: string;
+ 'revision.email'?: string;
+ 'revision.subject'?: string;
+ 'revision.timestamp'?: number | Date;
+ 'revision.link'?: string;
+ 'ci.link'?: string;
+}
diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts
index e4944fbfc1..d5dbb5a666 100644
--- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts
+++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts
@@ -49,6 +49,8 @@ export async function toMatchAriaSnapshot(
return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected: '' };
const updateSnapshots = testInfo.config.updateSnapshots;
+ const pathTemplate = testInfo._projectInternal.expect?.toMatchAriaSnapshot?.pathTemplate;
+ const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}';
const matcherOptions = {
isNot: this.isNot,
@@ -63,7 +65,7 @@ export async function toMatchAriaSnapshot(
timeout = options.timeout ?? this.timeout;
} else {
if (expectedParam?.name) {
- expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
+ expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeFilePathBeforeExtension(expectedParam.name)]);
} else {
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
if (!snapshotNames) {
@@ -71,7 +73,7 @@ export async function toMatchAriaSnapshot(
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
}
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
- expectedPath = testInfo.snapshotPath(sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml');
+ expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml']);
}
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
timeout = expectedParam?.timeout ?? this.timeout;
diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts
index 8d16f11fc2..193b57058f 100644
--- a/packages/playwright/src/matchers/toMatchSnapshot.ts
+++ b/packages/playwright/src/matchers/toMatchSnapshot.ts
@@ -148,7 +148,8 @@ class SnapshotHelper {
outputBasePath = testInfo._getOutputPath(sanitizedName);
this.attachmentBaseName = sanitizedName;
}
- this.expectedPath = testInfo.snapshotPath(...expectedPathSegments);
+ const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
+ this.expectedPath = testInfo._resolveSnapshotPath(configOptions.pathTemplate, defaultTemplate, expectedPathSegments);
this.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected');
this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');
diff --git a/packages/playwright/src/plugins/gitCommitInfoPlugin.ts b/packages/playwright/src/plugins/gitCommitInfoPlugin.ts
index 4c55a3eab3..c69d953b62 100644
--- a/packages/playwright/src/plugins/gitCommitInfoPlugin.ts
+++ b/packages/playwright/src/plugins/gitCommitInfoPlugin.ts
@@ -17,46 +17,38 @@
import { createGuid, spawnAsync } from 'playwright-core/lib/utils';
import type { TestRunnerPlugin } from './';
import type { FullConfig } from '../../types/testReporter';
+import type { FullConfigInternal } from '../common/config';
+import type { GitCommitInfo } from '../isomorphic/types';
const GIT_OPERATIONS_TIMEOUT_MS = 1500;
+export const addGitCommitInfoPlugin = (fullConfig: FullConfigInternal) => {
+ if (fullConfig.populateGitInfo)
+ fullConfig.plugins.push({ factory: gitCommitInfo });
+};
+
export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => {
return {
name: 'playwright:git-commit-info',
setup: async (config: FullConfig, configDir: string) => {
- const info = {
- ...linksFromEnv(),
- ...options?.info ? options.info : await gitStatusFromCLI(options?.directory || configDir),
- timestamp: Date.now(),
- };
- // Normalize dates
- const timestamp = info['revision.timestamp'];
- if (timestamp instanceof Date)
- info['revision.timestamp'] = timestamp.getTime();
+ const fromEnv = linksFromEnv();
+ const fromCLI = await gitStatusFromCLI(options?.directory || configDir);
+ const info = { ...fromEnv, ...fromCLI };
+ if (info['revision.timestamp'] instanceof Date)
+ info['revision.timestamp'] = info['revision.timestamp'].getTime();
config.metadata = config.metadata || {};
- Object.assign(config.metadata, info);
+ config.metadata['git.commit.info'] = info;
},
};
};
-export interface GitCommitInfoPluginOptions {
- directory?: string;
- info?: Info;
+interface GitCommitInfoPluginOptions {
+ directory?: string;
}
-export interface Info {
- 'revision.id'?: string;
- 'revision.author'?: string;
- 'revision.email'?: string;
- 'revision.subject'?: string;
- 'revision.timestamp'?: number | Date;
- 'revision.link'?: string;
- 'ci.link'?: string;
-}
-
-const linksFromEnv = (): Pick => {
+function linksFromEnv(): Pick {
const out: { 'revision.link'?: string; 'ci.link'?: string; } = {};
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
if (process.env.BUILD_URL)
@@ -72,9 +64,9 @@ const linksFromEnv = (): Pick => {
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
out['ci.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
return out;
-};
+}
-export const gitStatusFromCLI = async (gitDir: string): Promise => {
+async function gitStatusFromCLI(gitDir: string): Promise {
const separator = `:${createGuid().slice(0, 4)}:`;
const { code, stdout } = await spawnAsync(
'git',
@@ -95,4 +87,4 @@ export const gitStatusFromCLI = async (gitDir: string): Promise config.plugins.push({ factory: p }));
diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts
index 84cffac573..2d0b57f13a 100644
--- a/packages/playwright/src/runner/tasks.ts
+++ b/packages/playwright/src/runner/tasks.ts
@@ -34,7 +34,7 @@ import { detectChangedTestFiles } from './vcs';
import type { InternalReporter } from '../reporters/internalReporter';
import { cacheDir } from '../transform/compilationCache';
import type { FullResult } from '../../types/testReporter';
-import { applySuggestedRebaselines } from './rebase';
+import { applySuggestedRebaselines, clearSuggestedRebaselines } from './rebase';
const readDirAsync = promisify(fs.readdir);
@@ -284,6 +284,9 @@ export function createLoadTask(mode: 'out-of-process' | 'in-process', options: {
export function createApplyRebaselinesTask(): Task {
return {
title: 'apply rebaselines',
+ setup: async () => {
+ clearSuggestedRebaselines();
+ },
teardown: async ({ config, reporter }) => {
await applySuggestedRebaselines(config, reporter);
},
diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts
index e3a61329e2..e4ff3a7a2f 100644
--- a/packages/playwright/src/runner/testServer.ts
+++ b/packages/playwright/src/runner/testServer.ts
@@ -39,6 +39,7 @@ import { baseFullConfig } from '../isomorphic/teleReceiver';
import { InternalReporter } from '../reporters/internalReporter';
import type { ReporterV2 } from '../reporters/reporterV2';
import { internalScreen } from '../reporters/base';
+import { addGitCommitInfoPlugin } from '../plugins/gitCommitInfoPlugin';
const originalStdoutWrite = process.stdout.write;
const originalStderrWrite = process.stderr.write;
@@ -406,6 +407,7 @@ export class TestServerDispatcher implements TestServerInterface {
// Preserve plugin instances between setup and build.
if (!this._plugins) {
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
+ addGitCommitInfoPlugin(config);
this._plugins = config.plugins || [];
} else {
config.plugins.splice(0, config.plugins.length, ...this._plugins);
diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts
index 6e6ab4660c..3efd3b3750 100644
--- a/packages/playwright/src/worker/testInfo.ts
+++ b/packages/playwright/src/worker/testInfo.ts
@@ -454,14 +454,15 @@ export class TestInfoImpl implements TestInfo {
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
}
- snapshotPath(...pathSegments: string[]) {
+ _resolveSnapshotPath(template: string | undefined, defaultTemplate: string, pathSegments: string[]) {
const subPath = path.join(...pathSegments);
const parsedSubPath = path.parse(subPath);
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
- const snapshotPath = (this._projectInternal.snapshotPathTemplate || '')
+ const actualTemplate = (template || this._projectInternal.snapshotPathTemplate || defaultTemplate);
+ const snapshotPath = actualTemplate
.replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir)
.replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir)
.replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '')
@@ -477,6 +478,11 @@ export class TestInfoImpl implements TestInfo {
return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath));
}
+ snapshotPath(...pathSegments: string[]) {
+ const legacyTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
+ return this._resolveSnapshotPath(undefined, legacyTemplate, pathSegments);
+ }
+
skip(...args: [arg?: any, description?: string]) {
this._modifier('skip', args);
}
diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts
index 50192d30ae..5516c15839 100644
--- a/packages/playwright/types/test.d.ts
+++ b/packages/playwright/types/test.d.ts
@@ -214,6 +214,27 @@ interface TestProject {
* [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot).
*/
stylePath?: string|Array;
+
+ /**
+ * A template controlling location of the screenshots. See
+ * [testProject.snapshotPathTemplate](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-path-template)
+ * for details.
+ */
+ pathTemplate?: string;
+ };
+
+ /**
+ * Configuration for the
+ * [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
+ * method.
+ */
+ toMatchAriaSnapshot?: {
+ /**
+ * A template controlling location of the aria snapshots. See
+ * [testProject.snapshotPathTemplate](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-path-template)
+ * for details.
+ */
+ pathTemplate?: string;
};
/**
@@ -404,10 +425,14 @@ interface TestProject {
/**
* This option configures a template controlling location of snapshots generated by
- * [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1)
+ * [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1),
+ * [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* and
* [expect(value).toMatchSnapshot(name[, options])](https://playwright.dev/docs/api/class-snapshotassertions#snapshot-assertions-to-match-snapshot-1).
*
+ * You can configure templates for each assertion separately in
+ * [testConfig.expect](https://playwright.dev/docs/api/class-testconfig#test-config-expect).
+ *
* **Usage**
*
* ```js
@@ -416,7 +441,19 @@ interface TestProject {
*
* export default defineConfig({
* testDir: './tests',
+ *
+ * // Single template for all assertions
* snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
+ *
+ * // Assertion-specific templates
+ * expect: {
+ * toHaveScreenshot: {
+ * pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
+ * },
+ * toMatchAriaSnapshot: {
+ * pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
+ * },
+ * },
* });
* ```
*
@@ -447,27 +484,27 @@ interface TestProject {
* ```
*
* The list of supported tokens:
- * - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
- * `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
- * snapshot name.
+ * - `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to
+ * `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be
+ * an auto-generated snapshot name.
* - Value: `foo/bar/baz`
- * - `{ext}` - snapshot extension (with dots)
+ * - `{ext}` - Snapshot extension (with the leading dot).
* - Value: `.png`
* - `{platform}` - The value of `process.platform`.
* - `{projectName}` - Project's file-system-sanitized name, if any.
* - Value: `''` (empty string).
* - `{snapshotDir}` - Project's
- * [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
+ * [testProject.snapshotDir](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-dir).
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* - `{testDir}` - Project's
- * [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
- * - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
+ * [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
+ * - Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with
* config)
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* - Value: `page`
* - `{testFileName}` - Test file name with extension.
* - Value: `page-click.spec.ts`
- * - `{testFilePath}` - Relative path from `testDir` to **test file**
+ * - `{testFilePath}` - Relative path from `testDir` to **test file**.
* - Value: `page/page-click.spec.ts`
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* - Value: `suite-test-should-work`
@@ -991,6 +1028,27 @@ interface TestConfig {
* [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
*/
threshold?: number;
+
+ /**
+ * A template controlling location of the screenshots. See
+ * [testConfig.snapshotPathTemplate](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template)
+ * for details.
+ */
+ pathTemplate?: string;
+ };
+
+ /**
+ * Configuration for the
+ * [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
+ * method.
+ */
+ toMatchAriaSnapshot?: {
+ /**
+ * A template controlling location of the aria snapshots. See
+ * [testConfig.snapshotPathTemplate](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template)
+ * for details.
+ */
+ pathTemplate?: string;
};
/**
@@ -1220,7 +1278,12 @@ interface TestConfig {
maxFailures?: number;
/**
- * Metadata that will be put directly to the test report serialized as JSON.
+ * Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as
+ * key-value pairs, and JSON report will include metadata serialized as json.
+ *
+ * See also
+ * [testConfig.populateGitInfo](https://playwright.dev/docs/api/class-testconfig#test-config-populate-git-info) that
+ * populates metadata.
*
* **Usage**
*
@@ -1229,7 +1292,7 @@ interface TestConfig {
* import { defineConfig } from '@playwright/test';
*
* export default defineConfig({
- * metadata: 'acceptance tests',
+ * metadata: { title: 'acceptance tests' },
* });
* ```
*
@@ -1293,6 +1356,27 @@ interface TestConfig {
*/
outputDir?: string;
+ /**
+ * Whether to populate `'git.commit.info'` field of the
+ * [testConfig.metadata](https://playwright.dev/docs/api/class-testconfig#test-config-metadata) with Git commit info
+ * and CI/CD information.
+ *
+ * This information will appear in the HTML and JSON reports and is available in the Reporter API.
+ *
+ * **Usage**
+ *
+ * ```js
+ * // playwright.config.ts
+ * import { defineConfig } from '@playwright/test';
+ *
+ * export default defineConfig({
+ * populateGitInfo: !!process.env.CI,
+ * });
+ * ```
+ *
+ */
+ populateGitInfo?: boolean;
+
/**
* Whether to preserve test output in the
* [testConfig.outputDir](https://playwright.dev/docs/api/class-testconfig#test-config-output-dir). Defaults to
@@ -1468,10 +1552,14 @@ interface TestConfig {
/**
* This option configures a template controlling location of snapshots generated by
- * [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1)
+ * [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1),
+ * [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* and
* [expect(value).toMatchSnapshot(name[, options])](https://playwright.dev/docs/api/class-snapshotassertions#snapshot-assertions-to-match-snapshot-1).
*
+ * You can configure templates for each assertion separately in
+ * [testConfig.expect](https://playwright.dev/docs/api/class-testconfig#test-config-expect).
+ *
* **Usage**
*
* ```js
@@ -1480,7 +1568,19 @@ interface TestConfig {
*
* export default defineConfig({
* testDir: './tests',
+ *
+ * // Single template for all assertions
* snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
+ *
+ * // Assertion-specific templates
+ * expect: {
+ * toHaveScreenshot: {
+ * pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
+ * },
+ * toMatchAriaSnapshot: {
+ * pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
+ * },
+ * },
* });
* ```
*
@@ -1511,27 +1611,27 @@ interface TestConfig {
* ```
*
* The list of supported tokens:
- * - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
- * `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
- * snapshot name.
+ * - `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to
+ * `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be
+ * an auto-generated snapshot name.
* - Value: `foo/bar/baz`
- * - `{ext}` - snapshot extension (with dots)
+ * - `{ext}` - Snapshot extension (with the leading dot).
* - Value: `.png`
* - `{platform}` - The value of `process.platform`.
* - `{projectName}` - Project's file-system-sanitized name, if any.
* - Value: `''` (empty string).
* - `{snapshotDir}` - Project's
- * [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
+ * [testProject.snapshotDir](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-dir).
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* - `{testDir}` - Project's
- * [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
- * - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
+ * [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
+ * - Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with
* config)
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* - Value: `page`
* - `{testFileName}` - Test file name with extension.
* - Value: `page-click.spec.ts`
- * - `{testFilePath}` - Relative path from `testDir` to **test file**
+ * - `{testFilePath}` - Relative path from `testDir` to **test file**.
* - Value: `page/page-click.spec.ts`
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* - Value: `suite-test-should-work`
@@ -8685,20 +8785,23 @@ interface LocatorAssertions {
/**
* Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots).
*
+ * Snapshot is stored in a separate `.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate`
+ * and/or `snapshotPathTemplate` properties in the configuration file.
+ *
* **Usage**
*
* ```js
* await expect(page.locator('body')).toMatchAriaSnapshot();
- * await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot.yml' });
*
+ * await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
* ```
*
* @param options
*/
toMatchAriaSnapshot(options?: {
/**
- * Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test. Generates sequential
- * names if not specified.
+ * Name of the snapshot to store in the snapshot folder corresponding to this test. Generates sequential names if not
+ * specified.
*/
name?: string;
diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx
index ceaafdcec5..65faffd01b 100644
--- a/packages/trace-viewer/src/ui/networkTab.tsx
+++ b/packages/trace-viewer/src/ui/networkTab.tsx
@@ -268,6 +268,8 @@ const renderEntry = (resource: Entry, boundaries: Boundaries, contextIdGenerator
resourceName = url.pathname.substring(url.pathname.lastIndexOf('/') + 1);
if (!resourceName)
resourceName = url.host;
+ if (url.search)
+ resourceName += url.search;
} catch {
resourceName = resource.request.url;
}
diff --git a/tests/config/proxy.ts b/tests/config/proxy.ts
index 7efe389b12..94f2b57ed7 100644
--- a/tests/config/proxy.ts
+++ b/tests/config/proxy.ts
@@ -138,7 +138,7 @@ export async function setupSocksForwardingServer({
const socksProxy = new SocksProxy();
socksProxy.setPattern('*');
socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
- if (!['127.0.0.1', '0:0:0:0:0:0:0:1', 'fake-localhost-127-0-0-1.nip.io', 'localhost'].includes(payload.host) || payload.port !== allowedTargetPort) {
+ if (!['127.0.0.1', 'fake-localhost-127-0-0-1.nip.io', 'localhost'].includes(payload.host) || payload.port !== allowedTargetPort) {
socksProxy.sendSocketError({ uid: payload.uid, error: 'ECONNREFUSED' });
return;
}
diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts
index 367c0eabfc..9c81581de5 100644
--- a/tests/library/browsertype-connect.spec.ts
+++ b/tests/library/browsertype-connect.spec.ts
@@ -53,7 +53,8 @@ const test = playwrightTest.extend({
const server = createHttpServer((req: http.IncomingMessage, res: http.ServerResponse) => {
res.end('from-dummy-server');
});
- await new Promise(resolve => server.listen(0, resolve));
+ // Only listen on IPv4 to check that we don't try to connect to it via IPv6.
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
await use((server.address() as net.AddressInfo).port);
await new Promise(resolve => server.close(resolve));
},
@@ -792,9 +793,23 @@ for (const kind of ['launchServer', 'run-server'] as const) {
const remoteServer = await startRemoteServer(kind);
const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, dummyServerPort);
const page = await browser.newPage();
- await page.goto(`http://127.0.0.1:${examplePort}/foo.html`);
- expect(await page.content()).toContain('from-dummy-server');
- expect(reachedOriginalTarget).toBe(false);
+ {
+ await page.setContent('empty');
+ await page.goto(`http://127.0.0.1:${examplePort}/foo.html`);
+ expect(await page.content()).toContain('from-dummy-server');
+ expect(reachedOriginalTarget).toBe(false);
+ }
+ {
+ await page.setContent('empty');
+ await page.goto(`http://localhost:${examplePort}/foo.html`);
+ expect(await page.content()).toContain('from-dummy-server');
+ expect(reachedOriginalTarget).toBe(false);
+ }
+ {
+ const error = await page.goto(`http://[::1]:${examplePort}/foo.html`).catch(() => 'failed');
+ expect(error).toBe('failed');
+ expect(reachedOriginalTarget).toBe(false);
+ }
});
test('should proxy ipv6 localhost requests @smoke', async ({ startRemoteServer, server, browserName, connect, platform, ipV6ServerPort }, testInfo) => {
@@ -809,15 +824,26 @@ for (const kind of ['launchServer', 'run-server'] as const) {
const remoteServer = await startRemoteServer(kind);
const browser = await connect(remoteServer.wsEndpoint(), { exposeNetwork: '*' }, ipV6ServerPort);
const page = await browser.newPage();
- await page.goto(`http://[::1]:${examplePort}/foo.html`);
- expect(await page.content()).toContain('from-ipv6-server');
- const page2 = await browser.newPage();
- await page2.goto(`http://localhost:${examplePort}/foo.html`);
- expect(await page2.content()).toContain('from-ipv6-server');
- expect(reachedOriginalTarget).toBe(false);
+ {
+ await page.setContent('empty');
+ await page.goto(`http://[::1]:${examplePort}/foo.html`);
+ expect(await page.content()).toContain('from-ipv6-server');
+ expect(reachedOriginalTarget).toBe(false);
+ }
+ {
+ await page.setContent('empty');
+ await page.goto(`http://localhost:${examplePort}/foo.html`);
+ expect(await page.content()).toContain('from-ipv6-server');
+ expect(reachedOriginalTarget).toBe(false);
+ }
+ {
+ const error = await page.goto(`http://127.0.0.1:${examplePort}/foo.html`).catch(() => 'failed');
+ expect(error).toBe('failed');
+ expect(reachedOriginalTarget).toBe(false);
+ }
});
- test('should proxy localhost requests from fetch api', async ({ startRemoteServer, server, browserName, connect, channel, platform, dummyServerPort }, workerInfo) => {
+ test('should proxy requests from fetch api', async ({ startRemoteServer, server, browserName, connect, channel, platform, dummyServerPort }, workerInfo) => {
test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying');
let reachedOriginalTarget = false;
@@ -829,10 +855,54 @@ for (const kind of ['launchServer', 'run-server'] as const) {
const remoteServer = await startRemoteServer(kind);
const browser = await connect(remoteServer.wsEndpoint(), { exposeNetwork: '*' }, dummyServerPort);
const page = await browser.newPage();
- const response = await page.request.get(`http://127.0.0.1:${examplePort}/foo.html`);
- expect(response.status()).toBe(200);
- expect(await response.text()).toContain('from-dummy-server');
- expect(reachedOriginalTarget).toBe(false);
+ {
+ const response = await page.request.get(`http://localhost:${examplePort}/foo.html`);
+ expect(response.status()).toBe(200);
+ expect(await response.text()).toContain('from-dummy-server');
+ expect(reachedOriginalTarget).toBe(false);
+ }
+ {
+ const response = await page.request.get(`http://127.0.0.1:${examplePort}/foo.html`);
+ expect(response.status()).toBe(200);
+ expect(await response.text()).toContain('from-dummy-server');
+ expect(reachedOriginalTarget).toBe(false);
+ }
+ {
+ const error = await page.request.get(`http://[::1]:${examplePort}/foo.html`).catch(e => 'failed');
+ expect(error).toBe('failed');
+ expect(reachedOriginalTarget).toBe(false);
+ }
+ });
+
+ test('should proxy requests from fetch api over ipv6', async ({ startRemoteServer, server, browserName, connect, channel, platform, ipV6ServerPort }, workerInfo) => {
+ test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying');
+
+ let reachedOriginalTarget = false;
+ server.setRoute('/foo.html', async (req, res) => {
+ reachedOriginalTarget = true;
+ res.end('');
+ });
+ const examplePort = 20_000 + workerInfo.workerIndex * 3;
+ const remoteServer = await startRemoteServer(kind);
+ const browser = await connect(remoteServer.wsEndpoint(), { exposeNetwork: '*' }, ipV6ServerPort);
+ const page = await browser.newPage();
+ {
+ const response = await page.request.get(`http://localhost:${examplePort}/foo.html`);
+ expect(response.status()).toBe(200);
+ expect(await response.text()).toContain('from-ipv6-server');
+ expect(reachedOriginalTarget).toBe(false);
+ }
+ {
+ const response = await page.request.get(`http://[::1]:${examplePort}/foo.html`);
+ expect(response.status()).toBe(200);
+ expect(await response.text()).toContain('from-ipv6-server');
+ expect(reachedOriginalTarget).toBe(false);
+ }
+ {
+ const error = await page.request.get(`http://127.0.0.1:${examplePort}/foo.html`).catch(e => 'failed');
+ expect(error).toBe('failed');
+ expect(reachedOriginalTarget).toBe(false);
+ }
});
test('should proxy local.playwright requests', async ({ connect, server, dummyServerPort, startRemoteServer }, workerInfo) => {
diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts
index 648fef0d75..108dde2edc 100644
--- a/tests/library/client-certificates.spec.ts
+++ b/tests/library/client-certificates.spec.ts
@@ -390,7 +390,7 @@ test.describe('browser', () => {
});
expect(connectHosts).toEqual([]);
await page.goto(serverURL);
- const host = browserName === 'webkit' && isMac ? '0:0:0:0:0:0:0:1' : '127.0.0.1';
+ const host = browserName === 'webkit' && isMac ? 'localhost' : '127.0.0.1';
expect(connectHosts).toEqual([`${host}:${serverPort}`]);
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
await page.close();
diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts
index 88419e742c..6d920d133a 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, proxy?: BrowserContextOptions['proxy'] } & Partial> = {}) {
+async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise, testInfo: any, options: { outputPath?: string } & Partial> = {}) {
const harPath = testInfo.outputPath(options.outputPath || 'test.har');
- const context = await contextFactory({ recordHar: { path: harPath, ...options }, ignoreHTTPSErrors: true, proxy: options.proxy });
+ const context = await contextFactory({ recordHar: { path: harPath, ...options }, ignoreHTTPSErrors: true });
const page = await context.newPage();
return {
page,
@@ -861,38 +861,6 @@ 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);
- expect(proxyServer.connectHosts).toEqual([`localhost:${server.PORT}`]);
- 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);
- expect(await response.text()).toContain('Served by the SOCKS proxy');
- await expect(response).toBeOK();
- const log = await getLog();
- expect(log.entries[0].timings.connect).toBeGreaterThan(0);
-});
-
-it('should not have connect and dns timings when socket is reused', async ({ contextFactory, server }, testInfo) => {
- const { page, getLog } = await pageWithHar(contextFactory, testInfo);
- await page.request.get(server.EMPTY_PAGE);
- await page.request.get(server.EMPTY_PAGE);
-
- const log = await getLog();
- expect(log.entries).toHaveLength(2);
- const request2 = log.entries[1];
- expect.soft(request2.timings.connect).toBe(-1);
- expect.soft(request2.timings.dns).toBe(-1);
- expect.soft(request2.timings.blocked).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/inspector/cli-codegen-1.spec.ts b/tests/library/inspector/cli-codegen-1.spec.ts
index 6936aeee41..aa52daffa9 100644
--- a/tests/library/inspector/cli-codegen-1.spec.ts
+++ b/tests/library/inspector/cli-codegen-1.spec.ts
@@ -778,6 +778,70 @@ await page.GetByText("link").ClickAsync();`);
expect(page.url()).toContain('about:blank#foo');
});
+ test('should attribute navigation to press/fill', async ({ openRecorder }) => {
+ const { page, recorder } = await openRecorder();
+
+ await recorder.setContentAndWait(` `);
+
+ const locator = await recorder.hoverOverElement('input');
+ expect(locator).toBe(`getByRole('textbox')`);
+ await recorder.trustedClick();
+ await expect.poll(() => page.locator('input').evaluate(e => e === document.activeElement)).toBeTruthy();
+ const [, sources] = await Promise.all([
+ page.waitForNavigation(),
+ recorder.waitForOutput('JavaScript', '.fill'),
+ recorder.trustedPress('h'),
+ ]);
+
+ expect.soft(sources.get('JavaScript')!.text).toContain(`
+ await page.goto('about:blank');
+ await page.getByRole('textbox').click();
+ await page.getByRole('textbox').fill('h');
+
+ // ---------------------
+ await context.close();`);
+
+ expect.soft(sources.get('Playwright Test')!.text).toContain(`
+ await page.goto('about:blank');
+ await page.getByRole('textbox').click();
+ await page.getByRole('textbox').fill('h');
+});`);
+
+ expect.soft(sources.get('Java')!.text).toContain(`
+ page.navigate(\"about:blank\");
+ page.getByRole(AriaRole.TEXTBOX).click();
+ page.getByRole(AriaRole.TEXTBOX).fill(\"h\");
+ }`);
+
+ expect.soft(sources.get('Python')!.text).toContain(`
+ page.goto("about:blank")
+ page.get_by_role("textbox").click()
+ page.get_by_role("textbox").fill("h")
+
+ # ---------------------
+ context.close()`);
+
+ expect.soft(sources.get('Python Async')!.text).toContain(`
+ await page.goto("about:blank")
+ await page.get_by_role("textbox").click()
+ await page.get_by_role("textbox").fill("h")
+
+ # ---------------------
+ await context.close()`);
+
+ expect.soft(sources.get('Pytest')!.text).toContain(`
+ page.goto("about:blank")
+ page.get_by_role("textbox").click()
+ page.get_by_role("textbox").fill("h")`);
+
+ expect.soft(sources.get('C#')!.text).toContain(`
+await page.GotoAsync("about:blank");
+await page.GetByRole(AriaRole.Textbox).ClickAsync();
+await page.GetByRole(AriaRole.Textbox).FillAsync("h");`);
+
+ expect(page.url()).toContain('about:blank#foo');
+ });
+
test('should ignore AltGraph', async ({ openRecorder, browserName }) => {
test.skip(browserName === 'firefox', 'The TextInputProcessor in Firefox does not work with AltGraph.');
const { recorder } = await openRecorder();
diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts
index b94bfc09a0..f5e3631b3a 100644
--- a/tests/library/inspector/inspectorTest.ts
+++ b/tests/library/inspector/inspectorTest.ts
@@ -218,6 +218,10 @@ export class Recorder {
await this.page.mouse.up(options);
}
+ async trustedPress(text: string) {
+ await this.page.keyboard.press(text);
+ }
+
async trustedDblclick() {
await this.page.mouse.down();
await this.page.mouse.up();
diff --git a/tests/library/role-utils.spec.ts b/tests/library/role-utils.spec.ts
index 2b5792d0f1..1b625a106a 100644
--- a/tests/library/role-utils.spec.ts
+++ b/tests/library/role-utils.spec.ts
@@ -495,6 +495,21 @@ test('should not include hidden pseudo into accessible name', async ({ page }) =
expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello hello' });
});
+test('should resolve pseudo content from attr', async ({ page }) => {
+ await page.setContent(`
+
+
+ world
+
+ `);
+ expect(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello world' });
+});
+
test('should ignore invalid aria-labelledby', async ({ page }) => {
await page.setContent(`
diff --git a/tests/page/page-event-request.spec.ts b/tests/page/page-event-request.spec.ts
index b2e13a4c2f..84717026a8 100644
--- a/tests/page/page-event-request.spec.ts
+++ b/tests/page/page-event-request.spec.ts
@@ -47,7 +47,6 @@ it('should fire for fetches with keepalive: true', {
description: 'https://github.com/microsoft/playwright/issues/34497'
}
}, async ({ page, server, browserName }) => {
- it.fixme(browserName === 'firefox');
const requests = [];
page.on('request', request => requests.push(request));
await page.goto(server.EMPTY_PAGE);
diff --git a/tests/playwright-test/aria-snapshot-file.spec.ts b/tests/playwright-test/aria-snapshot-file.spec.ts
index f9c2563f37..c121d623d1 100644
--- a/tests/playwright-test/aria-snapshot-file.spec.ts
+++ b/tests/playwright-test/aria-snapshot-file.spec.ts
@@ -22,12 +22,7 @@ test.describe.configure({ mode: 'parallel' });
test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
- 'playwright.config.ts': `
- export default {
- snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
- };
- `,
- '__snapshots__/a.spec.ts/test.yml': `
+ 'a.spec.ts-snapshots/test.yml': `
- heading "hello world"
`,
'a.spec.ts': `
@@ -44,11 +39,6 @@ test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
test('should generate multiple missing', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
- 'playwright.config.ts': `
- export default {
- snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
- };
- `,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
@@ -61,25 +51,20 @@ test('should generate multiple missing', async ({ runInlineTest }, testInfo) =>
});
expect(result.exitCode).toBe(1);
- expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-1.yml, writing actual`);
- expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-2.yml, writing actual`);
- const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml'), 'utf8');
+ expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-1.yml, writing actual`);
+ expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-2.yml, writing actual`);
+ const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.yml'), 'utf8');
expect(snapshot1).toBe('- heading "hello world" [level=1]');
- const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-2.yml'), 'utf8');
+ const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-2.yml'), 'utf8');
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
});
test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
- 'playwright.config.ts': `
- export default {
- snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
- };
- `,
- '__snapshots__/a.spec.ts/test-1.yml': `
+ 'a.spec.ts-snapshots/test-1.yml': `
- heading "foo"
`,
- '__snapshots__/a.spec.ts/test-2.yml': `
+ 'a.spec.ts-snapshots/test-2.yml': `
- heading "bar"
`,
'a.spec.ts': `
@@ -94,22 +79,17 @@ test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
}, { 'update-snapshots': 'all' });
expect(result.exitCode).toBe(0);
- expect(result.output).toContain(`A snapshot is generated at __snapshots__${path.sep}a.spec.ts${path.sep}test-1.yml`);
- expect(result.output).toContain(`A snapshot is generated at __snapshots__${path.sep}a.spec.ts${path.sep}test-2.yml`);
- const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml'), 'utf8');
+ expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-1.yml`);
+ expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-2.yml`);
+ const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.yml'), 'utf8');
expect(snapshot1).toBe('- heading "hello world" [level=1]');
- const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-2.yml'), 'utf8');
+ const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-2.yml'), 'utf8');
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
});
test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
- 'playwright.config.ts': `
- export default {
- snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
- };
- `,
- '__snapshots__/a.spec.ts/test.yml': `
+ 'a.spec.ts-snapshots/test.yml': `
- heading "hello world"
`,
'a.spec.ts': `
@@ -122,17 +102,12 @@ test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
}, { 'update-snapshots': 'changed' });
expect(result.exitCode).toBe(0);
- const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test.yml'), 'utf8');
+ const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test.yml'), 'utf8');
expect(snapshot1.trim()).toBe('- heading "hello world"');
});
test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
- 'playwright.config.ts': `
- export default {
- snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
- };
- `,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test name', async ({ page }) => {
@@ -145,11 +120,11 @@ test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
});
expect(result.exitCode).toBe(1);
- expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-name-1.yml, writing actual`);
- expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-name-2.yml, writing actual`);
- const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-name-1.yml'), 'utf8');
+ expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-name-1.yml, writing actual`);
+ expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-name-2.yml, writing actual`);
+ const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-1.yml'), 'utf8');
expect(snapshot1).toBe('- heading "hello world" [level=1]');
- const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-name-2.yml'), 'utf8');
+ const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-2.yml'), 'utf8');
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
});
@@ -158,7 +133,6 @@ for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
- snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
updateSnapshots: '${updateSnapshots}',
};
`,
@@ -169,13 +143,13 @@ for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
});
`,
- '__snapshots__/a.spec.ts/test-1.yml': '- heading "Old content" [level=1]',
+ 'a.spec.ts-snapshots/test-1.yml': '- heading "Old content" [level=1]',
});
const rebase = updateSnapshots === 'all' || updateSnapshots === 'changed';
expect(result.exitCode).toBe(rebase ? 0 : 1);
if (rebase) {
- const snapshotOutputPath = testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml');
+ const snapshotOutputPath = testInfo.outputPath('a.spec.ts-snapshots/test-1.yml');
expect(result.output).toContain(`A snapshot is generated at`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe('- heading "New content" [level=1]');
@@ -187,14 +161,6 @@ for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
test('should respect timeout', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
- 'playwright.config.ts': `
- export default {
- snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
- };
- `,
- 'test.yml': `
- - heading "hello world"
- `,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
import path from 'path';
@@ -203,9 +169,61 @@ test('should respect timeout', async ({ runInlineTest }, testInfo) => {
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
});
`,
- '__snapshots__/a.spec.ts/test-1.yml': '- heading "new world" [level=1]',
+ 'a.spec.ts-snapshots/test-1.yml': '- heading "new world" [level=1]',
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`Timed out 1ms waiting for`);
});
+
+test('should respect config.snapshotPathTemplate', async ({ runInlineTest }, testInfo) => {
+ const result = await runInlineTest({
+ 'playwright.config.ts': `
+ export default {
+ snapshotPathTemplate: 'my-snapshots/{testFilePath}/{arg}{ext}',
+ };
+ `,
+ 'my-snapshots/dir/a.spec.ts/test.yml': `
+ - heading "hello world"
+ `,
+ 'dir/a.spec.ts': `
+ import { test, expect } from '@playwright/test';
+ test('test', async ({ page }) => {
+ await page.setContent(\`hello world \`);
+ await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
+ });
+ `
+ });
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+});
+
+test('should respect config.expect.toMatchAriaSnapshot.pathTemplate', async ({ runInlineTest }, testInfo) => {
+ const result = await runInlineTest({
+ 'playwright.config.ts': `
+ export default {
+ snapshotPathTemplate: 'my-snapshots/{testFilePath}/{arg}{ext}',
+ expect: {
+ toMatchAriaSnapshot: {
+ pathTemplate: 'actual-snapshots/{testFilePath}/{arg}{ext}',
+ },
+ },
+ };
+ `,
+ 'my-snapshots/dir/a.spec.ts/test.yml': `
+ - heading "wrong one"
+ `,
+ 'actual-snapshots/dir/a.spec.ts/test.yml': `
+ - heading "hello world"
+ `,
+ 'dir/a.spec.ts': `
+ import { test, expect } from '@playwright/test';
+ test('test', async ({ page }) => {
+ await page.setContent(\`hello world \`);
+ await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
+ });
+ `
+ });
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+});
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index 57ef76a7ca..a4f41874aa 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -978,8 +978,8 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await test.step('step', async () => {
testInfo.attachments.push({ name: 'attachment', body: 'content', contentType: 'text/plain' });
- })
-
+ })
+
});
`,
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
@@ -1095,7 +1095,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
const result = await runInlineTest({
'a.spec.js': `
import { test as base, expect } from '@playwright/test';
-
+
const test = base.extend({
fixture1: [async ({}, use) => {
await use();
@@ -1141,144 +1141,97 @@ for (const useIntermediateMergeReport of [true, false] as const) {
]);
});
- test.describe('gitCommitInfo plugin', () => {
- test('should include metadata', async ({ runInlineTest, writeFiles, showReport, page }) => {
- const files = {
- 'uncommitted.txt': `uncommitted file`,
- 'playwright.config.ts': `
- import { gitCommitInfo } from 'playwright/lib/plugins';
- import { test, expect } from '@playwright/test';
- const plugins = [gitCommitInfo()];
- export default { '@playwright/test': { plugins } };
- `,
- 'example.spec.ts': `
- import { test, expect } from '@playwright/test';
- test('sample', async ({}) => { expect(2).toBe(2); });
- `,
- };
- const baseDir = await writeFiles(files);
+ test('should include metadata with populateGitInfo = true', async ({ runInlineTest, writeFiles, showReport, page }) => {
+ const files = {
+ 'uncommitted.txt': `uncommitted file`,
+ 'playwright.config.ts': `
+ export default {
+ populateGitInfo: true,
+ metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
+ };
+ `,
+ 'example.spec.ts': `
+ import { test, expect } from '@playwright/test';
+ test('sample', async ({}) => { expect(2).toBe(2); });
+ `,
+ };
+ const baseDir = await writeFiles(files);
- const execGit = async (args: string[]) => {
- const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir });
- if (!!code)
- throw new Error(`Non-zero exit of:\n$ git ${args.join(' ')}\nConsole:\nstdout:\n${stdout}\n\nstderr:\n${stderr}\n\n`);
- return;
- };
+ const execGit = async (args: string[]) => {
+ const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir });
+ if (!!code)
+ throw new Error(`Non-zero exit of:\n$ git ${args.join(' ')}\nConsole:\nstdout:\n${stdout}\n\nstderr:\n${stderr}\n\n`);
+ return;
+ };
- await execGit(['init']);
- await execGit(['config', '--local', 'user.email', 'shakespeare@example.local']);
- await execGit(['config', '--local', 'user.name', 'William']);
- await execGit(['add', '*.ts']);
- await execGit(['commit', '-m', 'awesome commit message']);
+ await execGit(['init']);
+ await execGit(['config', '--local', 'user.email', 'shakespeare@example.local']);
+ await execGit(['config', '--local', 'user.name', 'William']);
+ await execGit(['add', '*.ts']);
+ await execGit(['commit', '-m', 'chore(html): make this test look nice']);
- const result = await runInlineTest(files, { reporter: 'dot,html' }, {
- PLAYWRIGHT_HTML_OPEN: 'never',
- GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
- GITHUB_RUN_ID: 'example-run-id',
- GITHUB_SERVER_URL: 'https://playwright.dev',
- GITHUB_SHA: 'example-sha',
- });
-
- await showReport();
-
- expect(result.exitCode).toBe(0);
- await page.click('text=awesome commit message');
- await expect.soft(page.getByTestId('revision.id')).toContainText(/^[a-f\d]+$/i);
- await expect.soft(page.getByTestId('revision.id').locator('a')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/commit/example-sha');
- await expect.soft(page.getByTestId('revision.timestamp')).toContainText(/AM|PM/);
- await expect.soft(page.locator('text=awesome commit message')).toHaveCount(2);
- await expect.soft(page.locator('text=William')).toBeVisible();
- await expect.soft(page.locator('text=shakespeare@example.local')).toBeVisible();
- await expect.soft(page.locator('text=CI/CD Logs')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/actions/runs/example-run-id');
- await expect.soft(page.locator('text=Report generated on')).toContainText(/AM|PM/);
- await expect.soft(page.getByTestId('metadata-chip')).toBeVisible();
- await expect.soft(page.getByTestId('metadata-error')).not.toBeVisible();
+ const result = await runInlineTest(files, { reporter: 'dot,html' }, {
+ PLAYWRIGHT_HTML_OPEN: 'never',
+ GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
+ GITHUB_RUN_ID: 'example-run-id',
+ GITHUB_SERVER_URL: 'https://playwright.dev',
+ GITHUB_SHA: 'example-sha',
});
+ await showReport();
- test('should use explicitly supplied metadata', async ({ runInlineTest, showReport, page }) => {
- const result = await runInlineTest({
- 'uncommitted.txt': `uncommitted file`,
- 'playwright.config.ts': `
- import { gitCommitInfo } from 'playwright/lib/plugins';
- import { test, expect } from '@playwright/test';
- const plugin = gitCommitInfo({
- info: {
- 'revision.id': '1234567890',
- 'revision.subject': 'a better subject',
- 'revision.timestamp': new Date(),
- 'revision.author': 'William',
- 'revision.email': 'shakespeare@example.local',
- },
- });
- export default { '@playwright/test': { plugins: [plugin] } };
- `,
- 'example.spec.ts': `
- import { gitCommitInfo } from 'playwright/lib/plugins';
- import { test, expect } from '@playwright/test';
- test('sample', async ({}) => { expect(2).toBe(2); });
- `,
- }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never', GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test', GITHUB_RUN_ID: 'example-run-id', GITHUB_SERVER_URL: 'https://playwright.dev', GITHUB_SHA: 'example-sha' }, undefined);
+ expect(result.exitCode).toBe(0);
+ await page.getByRole('button', { name: 'Metadata' }).click();
+ await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
+ - 'link "chore(html): make this test look nice"'
+ - text: /^William on/
+ - link "logs"
+ - link /^[a-f0-9]{7}$/
+ - text: 'foo: value1 bar: {"prop":"value2"} baz: ["value3",123]'
+ `);
+ });
- await showReport();
+ test('should not include git metadata with populateGitInfo = false', async ({ runInlineTest, showReport, page }) => {
+ const result = await runInlineTest({
+ 'playwright.config.ts': `
+ export default { populateGitInfo: false };
+ `,
+ 'example.spec.ts': `
+ import { test, expect } from '@playwright/test';
+ test('my sample test', async ({}) => { expect(2).toBe(2); });
+ `,
+ }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }, undefined);
- expect(result.exitCode).toBe(0);
- await page.click('text=a better subject');
- await expect.soft(page.getByTestId('revision.id')).toContainText(/^[a-f\d]+$/i);
- await expect.soft(page.getByTestId('revision.id').locator('a')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/commit/example-sha');
- await expect.soft(page.getByTestId('revision.timestamp')).toContainText(/AM|PM/);
- await expect.soft(page.locator('text=a better subject')).toHaveCount(2);
- await expect.soft(page.locator('text=William')).toBeVisible();
- await expect.soft(page.locator('text=shakespeare@example.local')).toBeVisible();
- await expect.soft(page.locator('text=CI/CD Logs')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/actions/runs/example-run-id');
- await expect.soft(page.locator('text=Report generated on')).toContainText(/AM|PM/);
- await expect.soft(page.getByTestId('metadata-chip')).toBeVisible();
- await expect.soft(page.getByTestId('metadata-error')).not.toBeVisible();
- });
+ await showReport();
- test('should not have metadata by default', async ({ runInlineTest, showReport, page }) => {
- const result = await runInlineTest({
- 'uncommitted.txt': `uncommitted file`,
- 'playwright.config.ts': `
- export default {};
- `,
- 'example.spec.ts': `
- import { test, expect } from '@playwright/test';
- test('my sample test', async ({}) => { expect(2).toBe(2); });
- `,
- }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }, undefined);
+ expect(result.exitCode).toBe(0);
+ await expect.soft(page.getByRole('button', { name: 'Metadata' })).toBeHidden();
+ await expect.soft(page.locator('.metadata-view')).toBeHidden();
+ });
- await showReport();
+ test('should show an error when metadata has invalid fields', async ({ runInlineTest, showReport, page }) => {
+ const result = await runInlineTest({
+ 'uncommitted.txt': `uncommitted file`,
+ 'playwright.config.ts': `
+ export default {
+ metadata: {
+ 'git.commit.info': { 'revision.timestamp': 'hi' }
+ },
+ };
+ `,
+ 'example.spec.ts': `
+ import { test, expect } from '@playwright/test';
+ test('my sample test', async ({}) => { expect(2).toBe(2); });
+ `,
+ }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
- expect(result.exitCode).toBe(0);
- await expect.soft(page.locator('text="my sample test"')).toBeVisible();
- await expect.soft(page.getByTestId('metadata-error')).not.toBeVisible();
- await expect.soft(page.getByTestId('metadata-chip')).not.toBeVisible();
- });
+ await showReport();
- test('should not include metadata if user supplies invalid values via metadata field', async ({ runInlineTest, showReport, page }) => {
- const result = await runInlineTest({
- 'uncommitted.txt': `uncommitted file`,
- 'playwright.config.ts': `
- export default {
- metadata: {
- 'revision.timestamp': 'hi',
- },
- };
- `,
- 'example.spec.ts': `
- import { test, expect } from '@playwright/test';
- test('my sample test', async ({}) => { expect(2).toBe(2); });
- `,
- }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
-
- await showReport();
-
- expect(result.exitCode).toBe(0);
- await expect.soft(page.locator('text="my sample test"')).toBeVisible();
- await expect.soft(page.getByTestId('metadata-error')).toBeVisible();
- await expect.soft(page.getByTestId('metadata-chip')).not.toBeVisible();
- });
+ expect(result.exitCode).toBe(0);
+ await page.getByRole('button', { name: 'Metadata' }).click();
+ await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
+ - paragraph: An error was encountered when trying to render metadata.
+ `);
});
test('should report clashing folders', async ({ runInlineTest, useIntermediateMergeReport }) => {
diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts
index 3afa7a8d90..65e348cc52 100644
--- a/tests/playwright-test/to-have-screenshot.spec.ts
+++ b/tests/playwright-test/to-have-screenshot.spec.ts
@@ -740,6 +740,25 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
expect(comparePNGs(fs.readFileSync(snapshotOutputPath), whiteImage)).toBe(null);
});
+test('should respect config.expect.toHaveScreenshot.pathTemplate', async ({ runInlineTest }, testInfo) => {
+ const result = await runInlineTest({
+ ...playwrightConfig({
+ snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
+ expect: { toHaveScreenshot: { pathTemplate: 'actual-screenshots/{testFilePath}/{arg}{ext}' } },
+ }),
+ '__screenshots__/a.spec.js/snapshot.png': blueImage,
+ 'actual-screenshots/a.spec.js/snapshot.png': whiteImage,
+ 'a.spec.js': `
+ const { test, expect } = require('@playwright/test');
+ test('is a test', async ({ page }) => {
+ await expect(page).toHaveScreenshot('snapshot.png');
+ });
+ `
+ });
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+});
+
test('shouldn\'t update snapshot with the update-snapshots flag for negated matcher', async ({ runInlineTest }, testInfo) => {
const EXPECTED_SNAPSHOT = blueImage;
const result = await runInlineTest({
diff --git a/tests/playwright-test/ui-mode-metadata.spec.ts b/tests/playwright-test/ui-mode-metadata.spec.ts
new file mode 100644
index 0000000000..8f032ea987
--- /dev/null
+++ b/tests/playwright-test/ui-mode-metadata.spec.ts
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { test, expect } from './ui-mode-fixtures';
+
+test('should render html report git info metadata', async ({ runUITest }) => {
+ const { page } = await runUITest({
+ 'reporter.ts': `
+ module.exports = class Reporter {
+ onBegin(config, suite) {
+ console.log('ci.link:', config.metadata['git.commit.info']['ci.link']);
+ }
+ }
+ `,
+ 'playwright.config.ts': `
+ import { defineConfig } from '@playwright/test';
+ export default defineConfig({
+ populateGitInfo: true,
+ reporter: './reporter.ts',
+ });
+ `,
+ 'a.test.js': `
+ import { test, expect } from '@playwright/test';
+ test('should work', async ({}) => {});
+ `
+ }, {
+ BUILD_URL: 'https://playwright.dev',
+ });
+
+ await page.getByTitle('Run all').click();
+ await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
+ await page.getByTitle('Toggle output').click();
+
+ await expect(page.getByTestId('output')).toContainText('ci.link: https://playwright.dev');
+});