diff --git a/packages/html-reporter/src/testErrorView.css b/packages/html-reporter/src/testErrorView.css
index d5a4534e7e..e29ea2a18b 100644
--- a/packages/html-reporter/src/testErrorView.css
+++ b/packages/html-reporter/src/testErrorView.css
@@ -14,6 +14,8 @@
limitations under the License.
*/
+@import '@web/third_party/vscode/colors.css';
+
.test-error-view {
white-space: pre;
overflow: auto;
diff --git a/packages/html-reporter/src/testErrorView.tsx b/packages/html-reporter/src/testErrorView.tsx
index 8d2bb13bd3..d63f5d7945 100644
--- a/packages/html-reporter/src/testErrorView.tsx
+++ b/packages/html-reporter/src/testErrorView.tsx
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import ansi2html from 'ansi-to-html';
+import { ansi2html } from '@web/ansi2html';
import * as React from 'react';
import './testErrorView.css';
import type { ImageDiff } from '@web/shared/imageDiffView';
@@ -43,33 +43,9 @@ export const TestScreenshotErrorView: React.FC<{
};
function ansiErrorToHtml(text?: string): string {
- const config: any = {
+ const defaultColors = {
bg: 'var(--color-canvas-subtle)',
fg: 'var(--color-fg-default)',
};
- config.colors = ansiColors;
- return new ansi2html(config).toHtml(escapeHTML(text || ''));
-}
-
-const ansiColors = {
- 0: '#000',
- 1: '#C00',
- 2: '#0C0',
- 3: '#C50',
- 4: '#00C',
- 5: '#C0C',
- 6: '#0CC',
- 7: '#CCC',
- 8: '#555',
- 9: '#F55',
- 10: '#5F5',
- 11: '#FF5',
- 12: '#55F',
- 13: '#F5F',
- 14: '#5FF',
- 15: '#FFF'
-};
-
-function escapeHTML(text: string): string {
- return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!));
+ return ansi2html(text || '', defaultColors);
}
diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx
index bd402a74de..4d6890ad33 100644
--- a/packages/html-reporter/src/testFileView.tsx
+++ b/packages/html-reporter/src/testFileView.tsx
@@ -14,24 +14,25 @@
limitations under the License.
*/
-import type { HTMLReport, TestCaseSummary, TestFileSummary } from './types';
+import type { TestCaseSummary, TestFileSummary } from './types';
import * as React from 'react';
import { hashStringToInt, msToString } from './utils';
import { Chip } from './chip';
-import { filterWithToken, type Filter } from './filter';
-import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
+import { filterWithToken } from './filter';
+import { generateTraceUrl, Link, navigate, ProjectLink, SearchParamsContext } from './links';
import { statusIcon } from './statusIcon';
import './testFileView.css';
import { video, image, trace } from './icons';
import { clsx } from '@web/uiUtils';
export const TestFileView: React.FC
boolean;
setFileExpanded: (fileId: string, expanded: boolean) => void;
- filter: Filter;
-}>> = ({ file, report, isFileExpanded, setFileExpanded, filter }) => {
+}>> = ({ file, projectNames, isFileExpanded, setFileExpanded }) => {
+ const searchParams = React.useContext(SearchParamsContext);
+ const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : '';
return
{file.fileName}
}>
- {file.tests.filter(t => filter.matches(t)).map(test =>
+ {file.tests.map(test =>
@@ -47,11 +48,11 @@ export const TestFileView: React.FC
-
+
{[...test.path, test.title].join(' › ')}
- {report.projectNames.length > 1 && !!test.projectName &&
- }
+ {projectNames.length > 1 && !!test.projectName &&
+ }
@@ -90,10 +91,10 @@ function traceBadge(test: TestCaseSummary): JSX.Element | undefined {
const LabelsClickView: React.FC
> = ({ labels }) => {
+ const searchParams = React.useContext(SearchParamsContext);
const onClickHandle = (e: React.MouseEvent, label: string) => {
e.preventDefault();
- const searchParams = new URLSearchParams(window.location.hash.slice(1));
const q = searchParams.get('q')?.toString() || '';
const tokens = q.split(' ');
navigate(filterWithToken(tokens, label, e.metaKey || e.ctrlKey));
diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx
index 30c80c075b..d21088f575 100644
--- a/packages/html-reporter/src/testFilesView.tsx
+++ b/packages/html-reporter/src/testFilesView.tsx
@@ -16,7 +16,6 @@
import type { FilteredStats, HTMLReport, TestFileSummary } from './types';
import * as React from 'react';
-import type { Filter } from './filter';
import { TestFileView } from './testFileView';
import './testFileView.css';
import { msToString } from './utils';
@@ -24,40 +23,26 @@ import { AutoChip } from './chip';
import { TestErrorView } from './testErrorView';
export const TestFilesView: React.FC<{
- report?: HTMLReport,
+ tests: TestFileSummary[],
expandedFiles: Map,
setExpandedFiles: (value: Map) => void,
- filter: Filter,
- filteredStats: FilteredStats,
projectNames: string[],
-}> = ({ report, filter, expandedFiles, setExpandedFiles, projectNames, filteredStats }) => {
+}> = ({ tests, expandedFiles, setExpandedFiles, projectNames }) => {
const filteredFiles = React.useMemo(() => {
const result: { file: TestFileSummary, defaultExpanded: boolean }[] = [];
let visibleTests = 0;
- for (const file of report?.files || []) {
- const tests = file.tests.filter(t => filter.matches(t));
- visibleTests += tests.length;
- if (tests.length)
- result.push({ file, defaultExpanded: visibleTests < 200 });
+ for (const file of tests) {
+ visibleTests += file.tests.length;
+ result.push({ file, defaultExpanded: visibleTests < 200 });
}
return result;
- }, [report, filter]);
+ }, [tests]);
return <>
-
- {projectNames.length === 1 && !!projectNames[0] &&
Project: {projectNames[0]}
}
- {!filter.empty() &&
Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}
}
-
-
{report ? new Date(report.startTime).toLocaleString() : ''}
-
Total time: {msToString(report?.duration ?? 0)}
-
- {report && !!report.errors.length &&
- {report.errors.map((error, index) => )}
- }
- {report && filteredFiles.map(({ file, defaultExpanded }) => {
+ {filteredFiles.map(({ file, defaultExpanded }) => {
return {
const value = expandedFiles.get(fileId);
if (value === undefined)
@@ -68,9 +53,28 @@ export const TestFilesView: React.FC<{
const newExpanded = new Map(expandedFiles);
newExpanded.set(fileId, expanded);
setExpandedFiles(newExpanded);
- }}
- filter={filter}>
+ }}>
;
})}
>;
};
+
+export const TestFilesHeader: React.FC<{
+ report: HTMLReport | undefined,
+ filteredStats?: FilteredStats,
+}> = ({ report, filteredStats }) => {
+ if (!report)
+ return;
+ return <>
+
+ {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)}
+
+ {!!report.errors.length &&
+ {report.errors.map((error, index) => )}
+ }
+ >;
+};
diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json
index 89f58ae146..c18df4aef3 100644
--- a/packages/playwright-core/browsers.json
+++ b/packages/playwright-core/browsers.json
@@ -3,9 +3,9 @@
"browsers": [
{
"name": "chromium",
- "revision": "1145",
+ "revision": "1146",
"installByDefault": true,
- "browserVersion": "131.0.6778.13"
+ "browserVersion": "131.0.6778.24"
},
{
"name": "chromium-tip-of-tree",
@@ -27,7 +27,7 @@
},
{
"name": "webkit",
- "revision": "2097",
+ "revision": "2102",
"installByDefault": true,
"revisionOverrides": {
"mac10.14": "1446",
diff --git a/packages/playwright-core/src/server/ariaSnapshot.ts b/packages/playwright-core/src/server/ariaSnapshot.ts
index 03b5a05e92..744167048a 100644
--- a/packages/playwright-core/src/server/ariaSnapshot.ts
+++ b/packages/playwright-core/src/server/ariaSnapshot.ts
@@ -14,39 +14,54 @@
* limitations under the License.
*/
-import type { AriaTemplateNode } from './injected/ariaSnapshot';
+import type { AriaTemplateNode, AriaTemplateRoleNode } from './injected/ariaSnapshot';
import { yaml } from '../utilsBundle';
-import type { AriaRole } from '@injected/roleUtils';
import { assert } from '../utils';
export function parseAriaSnapshot(text: string): AriaTemplateNode {
const fragment = yaml.parse(text) as any[];
- const result: AriaTemplateNode = { role: 'fragment' };
+ const result: AriaTemplateNode = { kind: 'role', role: 'fragment' };
populateNode(result, fragment);
return result;
}
-function populateNode(node: AriaTemplateNode, container: any[]) {
+function populateNode(node: AriaTemplateRoleNode, container: any[]) {
for (const object of container) {
if (typeof object === 'string') {
- const childNode = parseKey(object);
+ const childNode = KeyParser.parse(object);
node.children = node.children || [];
node.children.push(childNode);
continue;
}
for (const key of Object.keys(object)) {
- const childNode = parseKey(key);
- const value = object[key];
node.children = node.children || [];
+ const value = object[key];
- if (childNode.role === 'text') {
- node.children.push(valueOrRegex(value));
+ if (key === 'text') {
+ node.children.push({
+ kind: 'text',
+ text: valueOrRegex(value)
+ });
+ continue;
+ }
+
+ const childNode = KeyParser.parse(key);
+ if (childNode.kind === 'text') {
+ node.children.push({
+ kind: 'text',
+ text: valueOrRegex(value)
+ });
continue;
}
if (typeof value === 'string') {
- node.children.push({ ...childNode, children: [valueOrRegex(value)] });
+ node.children.push({
+ ...childNode, children: [{
+ kind: 'text',
+ text: valueOrRegex(value)
+ }]
+ });
continue;
}
@@ -56,7 +71,7 @@ function populateNode(node: AriaTemplateNode, container: any[]) {
}
}
-function applyAttribute(node: AriaTemplateNode, key: string, value: string) {
+function applyAttribute(node: AriaTemplateRoleNode, key: string, value: string) {
if (key === 'checked') {
assert(value === 'true' || value === 'false' || value === 'mixed', 'Value of "disabled" attribute must be a boolean or "mixed"');
node.checked = value === 'true' ? true : value === 'false' ? false : 'mixed';
@@ -90,47 +105,6 @@ function applyAttribute(node: AriaTemplateNode, key: string, value: string) {
throw new Error(`Unsupported attribute [${key}] `);
}
-function parseKey(key: string): AriaTemplateNode {
- const tokenRegex = /\s*([a-z]+|"(?:[^"]*)"|\/(?:[^\/]*)\/|\[.*?\])/g;
- let match;
- const tokens = [];
- while ((match = tokenRegex.exec(key)) !== null)
- tokens.push(match[1]);
-
- if (tokens.length === 0)
- throw new Error(`Invalid key ${key}`);
-
- const role = tokens[0] as AriaRole | 'text';
-
- let name: string | RegExp = '';
- let index = 1;
- if (tokens.length > 1 && (tokens[1].startsWith('"') || tokens[1].startsWith('/'))) {
- const nameToken = tokens[1];
- if (nameToken.startsWith('"')) {
- name = nameToken.slice(1, -1);
- } else {
- const pattern = nameToken.slice(1, -1);
- name = new RegExp(pattern);
- }
- index = 2;
- }
-
- const result: AriaTemplateNode = { role, name };
- for (; index < tokens.length; index++) {
- const attrToken = tokens[index];
- if (attrToken.startsWith('[') && attrToken.endsWith(']')) {
- const attrContent = attrToken.slice(1, -1).trim();
- const [attrName, attrValue] = attrContent.split('=', 2);
- const value = attrValue !== undefined ? attrValue.trim() : 'true';
- applyAttribute(result, attrName, value);
- } else {
- throw new Error(`Invalid attribute token ${attrToken} in key ${key}`);
- }
- }
-
- return result;
-}
-
function normalizeWhitespace(text: string) {
return text.replace(/[\r\n\s\t]+/g, ' ').trim();
}
@@ -138,3 +112,148 @@ function normalizeWhitespace(text: string) {
function valueOrRegex(value: string): string | RegExp {
return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : normalizeWhitespace(value);
}
+
+export class KeyParser {
+ private _input: string;
+ private _pos: number;
+ private _length: number;
+
+ static parse(input: string): AriaTemplateNode {
+ return new KeyParser(input)._parse();
+ }
+
+ constructor(input: string) {
+ this._input = input;
+ this._pos = 0;
+ this._length = input.length;
+ }
+
+ private _peek() {
+ return this._input[this._pos] || '';
+ }
+
+ private _next() {
+ if (this._pos < this._length)
+ return this._input[this._pos++];
+ return null;
+ }
+
+ private _eof() {
+ return this._pos >= this._length;
+ }
+
+ private _skipWhitespace() {
+ while (!this._eof() && /\s/.test(this._peek()))
+ this._pos++;
+ }
+
+ private _readIdentifier(): string {
+ if (this._eof())
+ throw new Error('Unexpected end of input when expecting identifier');
+ const start = this._pos;
+ while (!this._eof() && /[a-zA-Z]/.test(this._peek()))
+ this._pos++;
+ return this._input.slice(start, this._pos);
+ }
+
+ private _readString(): string {
+ let result = '';
+ let escaped = false;
+ while (!this._eof()) {
+ const ch = this._next();
+ if (escaped) {
+ result += ch;
+ escaped = false;
+ } else if (ch === '\\') {
+ escaped = true;
+ result += ch;
+ } else if (ch === '"') {
+ return result;
+ } else {
+ result += ch;
+ }
+ }
+ throw new Error('Unterminated string starting at position ' + this._pos);
+ }
+
+ private _readRegex(): string {
+ let result = '';
+ let escaped = false;
+ while (!this._eof()) {
+ const ch = this._next();
+ if (escaped) {
+ result += ch;
+ escaped = false;
+ } else if (ch === '\\') {
+ escaped = true;
+ result += ch;
+ } else if (ch === '/') {
+ return result;
+ } else {
+ result += ch;
+ }
+ }
+ throw new Error('Unterminated regex starting at position ' + this._pos);
+ }
+
+ private _readStringOrRegex(): string | RegExp | null {
+ const ch = this._peek();
+ if (ch === '"') {
+ this._next();
+ return this._readString();
+ }
+
+ if (ch === '/') {
+ this._next();
+ return new RegExp(this._readRegex());
+ }
+
+ return null;
+ }
+
+ private _readFlags(): Map {
+ const flags = new Map();
+ while (true) {
+ this._skipWhitespace();
+ if (this._peek() === '[') {
+ this._next();
+ this._skipWhitespace();
+ const flagName = this._readIdentifier();
+ this._skipWhitespace();
+ let flagValue = '';
+ if (this._peek() === '=') {
+ this._next();
+ this._skipWhitespace();
+ while (this._peek() !== ']' && !this._eof())
+ flagValue += this._next();
+ }
+ this._skipWhitespace();
+ if (this._peek() !== ']')
+ throw new Error('Expected ] at position ' + this._pos);
+
+ this._next(); // Consume ']'
+ flags.set(flagName, flagValue || 'true');
+ } else {
+ break;
+ }
+ }
+ return flags;
+ }
+
+ _parse(): AriaTemplateNode {
+ this._skipWhitespace();
+
+ const role = this._readIdentifier() as AriaTemplateRoleNode['role'];
+ this._skipWhitespace();
+ const name = this._readStringOrRegex() || '';
+ const result: AriaTemplateRoleNode = { kind: 'role', role, name };
+ const flags = this._readFlags();
+ for (const [name, value] of flags)
+ applyAttribute(result, name, value);
+ this._skipWhitespace();
+ if (!this._eof())
+ throw new Error('Unexpected input at position ' + this._pos);
+
+ return result;
+ }
+}
diff --git a/packages/playwright-core/src/server/codegen/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts
index c0e62d9df3..7a4b637a06 100644
--- a/packages/playwright-core/src/server/codegen/javascript.ts
+++ b/packages/playwright-core/src/server/codegen/javascript.ts
@@ -275,10 +275,13 @@ ${body}
}
export function quoteMultiline(text: string, indent = ' ') {
+ const escape = (text: string) => text.replace(/\\/g, '\\\\')
+ .replace(/`/g, '\\`')
+ .replace(/\$\{/g, '\\${');
const lines = text.split('\n');
if (lines.length === 1)
- return '`' + text.replace(/`/g, '\\`').replace(/\${/g, '\\${') + '`';
- return '`\n' + lines.map(line => indent + line.replace(/`/g, '\\`').replace(/\${/g, '\\${')).join('\n') + `\n${indent}\``;
+ return '`' + escape(text) + '`';
+ return '`\n' + lines.map(line => indent + escape(line).replace(/\${/g, '\\${')).join('\n') + `\n${indent}\``;
}
function isMultilineString(text: string) {
diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json
index 90608d052d..47544dc1fe 100644
--- a/packages/playwright-core/src/server/deviceDescriptorsSource.json
+++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json
@@ -110,7 +110,7 @@
"defaultBrowserType": "webkit"
},
"Galaxy S5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 740
@@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 740,
"height": 360
@@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 320,
"height": 658
@@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+ landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 658,
"height": 320
@@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Safari/537.36",
"viewport": {
"width": 712,
"height": 1138
@@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Safari/537.36",
"viewport": {
"width": 1138,
"height": 712
@@ -1098,7 +1098,7 @@
"defaultBrowserType": "webkit"
},
"LG Optimus L70": {
- "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@@ -1109,7 +1109,7 @@
"defaultBrowserType": "chromium"
},
"LG Optimus L70 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@@ -1120,7 +1120,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@@ -1131,7 +1131,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550 landscape": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@@ -1142,7 +1142,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@@ -1153,7 +1153,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950 landscape": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@@ -1164,7 +1164,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Safari/537.36",
"viewport": {
"width": 800,
"height": 1280
@@ -1175,7 +1175,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Safari/537.36",
"viewport": {
"width": 1280,
"height": 800
@@ -1186,7 +1186,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@@ -1197,7 +1197,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@@ -1208,7 +1208,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -1219,7 +1219,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -1230,7 +1230,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1241,7 +1241,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1252,7 +1252,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1263,7 +1263,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1274,7 +1274,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1285,7 +1285,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1296,7 +1296,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Safari/537.36",
"viewport": {
"width": 600,
"height": 960
@@ -1307,7 +1307,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Safari/537.36",
"viewport": {
"width": 960,
"height": 600
@@ -1362,7 +1362,7 @@
"defaultBrowserType": "webkit"
},
"Pixel 2": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 731
@@ -1373,7 +1373,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 731,
"height": 411
@@ -1384,7 +1384,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 823
@@ -1395,7 +1395,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 823,
"height": 411
@@ -1406,7 +1406,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3": {
- "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 393,
"height": 786
@@ -1417,7 +1417,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 786,
"height": 393
@@ -1428,7 +1428,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 353,
"height": 745
@@ -1439,7 +1439,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 745,
"height": 353
@@ -1450,7 +1450,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G)": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 892
@@ -1465,7 +1465,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G) landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"screen": {
"height": 892,
"width": 412
@@ -1480,7 +1480,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"screen": {
"width": 393,
"height": 851
@@ -1495,7 +1495,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"screen": {
"width": 851,
"height": 393
@@ -1510,7 +1510,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 915
@@ -1525,7 +1525,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"screen": {
"width": 915,
"height": 412
@@ -1540,7 +1540,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -1551,7 +1551,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -1562,7 +1562,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Chrome HiDPI": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Safari/537.36",
"screen": {
"width": 1792,
"height": 1120
@@ -1577,7 +1577,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge HiDPI": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36 Edg/131.0.6778.13",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Safari/537.36 Edg/131.0.6778.24",
"screen": {
"width": 1792,
"height": 1120
@@ -1622,7 +1622,7 @@
"defaultBrowserType": "webkit"
},
"Desktop Chrome": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Safari/537.36",
"screen": {
"width": 1920,
"height": 1080
@@ -1637,7 +1637,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36 Edg/131.0.6778.13",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.24 Safari/537.36 Edg/131.0.6778.24",
"screen": {
"width": 1920,
"height": 1080
diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts
index e14960b495..ce63891f4f 100644
--- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts
@@ -17,7 +17,7 @@
import { EventEmitter } from 'events';
import type * as channels from '@protocol/channels';
import { findValidator, ValidationError, createMetadataValidator, type ValidatorContext } from '../../protocol/validator';
-import { LongStandingScope, assert, isUnderTest, monotonicTime, rewriteErrorMessage } from '../../utils';
+import { LongStandingScope, assert, compressCallLog, isUnderTest, monotonicTime, rewriteErrorMessage } from '../../utils';
import { TargetClosedError, isTargetClosedError, serializeError } from '../errors';
import type { CallMetadata } from '../instrumentation';
import { SdkObject } from '../instrumentation';
@@ -357,7 +357,7 @@ export class DispatcherConnection {
}
if (response.error)
- response.log = callMetadata.log;
+ response.log = compressCallLog(callMetadata.log);
this.onmessage(response);
}
}
diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts
index 4bb301bffd..dea4a52e78 100644
--- a/packages/playwright-core/src/server/dom.ts
+++ b/packages/playwright-core/src/server/dom.ts
@@ -299,7 +299,7 @@ export class ElementHandle extends js.JSHandle {
while (progress.isRunning()) {
if (retry) {
- progress.log(`retrying ${actionName} action${options.trial ? ' (trial run)' : ''}, attempt #${retry}`);
+ progress.log(`retrying ${actionName} action${options.trial ? ' (trial run)' : ''}`);
const timeout = waitTime[Math.min(retry - 1, waitTime.length - 1)];
if (timeout) {
progress.log(` waiting ${timeout}ms`);
diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts
index 597ff35951..129484a027 100644
--- a/packages/playwright-core/src/server/frames.ts
+++ b/packages/playwright-core/src/server/frames.ts
@@ -29,7 +29,7 @@ import * as types from './types';
import { BrowserContext } from './browserContext';
import type { Progress } from './progress';
import { ProgressController } from './progress';
-import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime, asLocator } from '../utils';
+import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime, asLocator, compressCallLog } from '../utils';
import { ManualPromise } from '../utils/manualPromise';
import { debugLogger } from '../utils/debugLogger';
import type { CallMetadata } from './instrumentation';
@@ -1452,7 +1452,7 @@ export class Frame extends SdkObject {
timeout -= elapsed;
}
if (timeout < 0)
- return { matches: options.isNot, log: metadata.log, timedOut: true, received: lastIntermediateResult.received };
+ return { matches: options.isNot, log: compressCallLog(metadata.log), timedOut: true, received: lastIntermediateResult.received };
// Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time.
return await (new ProgressController(metadata, this)).run(async progress => {
@@ -1473,7 +1473,7 @@ export class Frame extends SdkObject {
// A: We want user to receive a friendly message containing the last intermediate result.
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
throw e;
- const result: { matches: boolean, received?: any, log?: string[], timedOut?: boolean } = { matches: options.isNot, log: metadata.log };
+ const result: { matches: boolean, received?: any, log?: string[], timedOut?: boolean } = { matches: options.isNot, log: compressCallLog(metadata.log) };
if (lastIntermediateResult.isSet)
result.received = lastIntermediateResult.received;
if (e instanceof TimeoutError)
diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts
index 82f3119d9c..fa0f14343c 100644
--- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts
+++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts
@@ -17,6 +17,8 @@
import * as roleUtils from './roleUtils';
import { getElementComputedStyle } from './domUtils';
import type { AriaRole } from './roleUtils';
+import { escapeRegExp, longestCommonSubstring } from '@isomorphic/stringUtils';
+import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded, yamlQuoteFragment } from './yaml';
type AriaProps = {
checked?: boolean | 'mixed';
@@ -33,14 +35,27 @@ type AriaNode = AriaProps & {
children: (AriaNode | string)[];
};
-export type AriaTemplateNode = AriaProps & {
- role: AriaRole | 'fragment' | 'text';
- name?: RegExp | string;
- children?: (AriaTemplateNode | string | RegExp)[];
+export type AriaTemplateTextNode = {
+ kind: 'text';
+ text: RegExp | string;
};
+export type AriaTemplateRoleNode = AriaProps & {
+ kind: 'role';
+ role: AriaRole | 'fragment';
+ name?: RegExp | string;
+ children?: AriaTemplateNode[];
+};
+
+export type AriaTemplateNode = AriaTemplateRoleNode | AriaTemplateTextNode;
+
export function generateAriaTree(rootElement: Element): AriaNode {
+ const visited = new Set();
const visit = (ariaNode: AriaNode, node: Node) => {
+ if (visited.has(node))
+ return;
+ visited.add(node);
+
if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
const text = node.nodeValue;
if (text)
@@ -55,13 +70,23 @@ export function generateAriaTree(rootElement: Element): AriaNode {
if (roleUtils.isElementHiddenForAria(element))
return;
+ const ariaChildren: Element[] = [];
+ if (element.hasAttribute('aria-owns')) {
+ const ids = element.getAttribute('aria-owns')!.split(/\s+/);
+ for (const id of ids) {
+ const ownedElement = rootElement.ownerDocument.getElementById(id);
+ if (ownedElement)
+ ariaChildren.push(ownedElement);
+ }
+ }
+
const childAriaNode = toAriaNode(element);
if (childAriaNode)
ariaNode.children.push(childAriaNode);
- processChildNodes(childAriaNode || ariaNode, element);
+ processElement(childAriaNode || ariaNode, element, ariaChildren);
};
- function processChildNodes(ariaNode: AriaNode, element: Element) {
+ function processElement(ariaNode: AriaNode, element: Element, ariaChildren: Element[] = []) {
// Surround every element with spaces for the sake of concatenated text nodes.
const display = getElementComputedStyle(element)?.display || 'inline';
const treatAsBlock = (display !== 'inline' || element.nodeName === 'BR') ? ' ' : '';
@@ -84,12 +109,15 @@ export function generateAriaTree(rootElement: Element): AriaNode {
}
}
+ for (const child of ariaChildren)
+ visit(ariaNode, child);
+
ariaNode.children.push(roleUtils.getPseudoContent(element, '::after'));
if (treatAsBlock)
ariaNode.children.push(treatAsBlock);
- if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0])
+ if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0])
ariaNode.children = [];
}
@@ -131,11 +159,14 @@ function toAriaNode(element: Element): AriaNode | null {
if (roleUtils.kAriaSelectedRoles.includes(role))
result.selected = roleUtils.getAriaSelected(element);
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)
+ result.children = [element.value];
+
return result;
}
-export function renderedAriaTree(rootElement: Element): string {
- return renderAriaTree(generateAriaTree(rootElement));
+export function renderedAriaTree(rootElement: Element, options?: { mode?: 'raw' | 'regex' }): string {
+ return renderAriaTree(generateAriaTree(rootElement), options);
}
function normalizeStringChildren(rootA11yNode: AriaNode) {
@@ -170,7 +201,7 @@ function normalizeStringChildren(rootA11yNode: AriaNode) {
const normalizeWhitespaceWithin = (text: string) => text.replace(/[\u200b\s\t\r\n]+/g, ' ');
-function matchesText(text: string | undefined, template: RegExp | string | undefined) {
+function matchesText(text: string, template: RegExp | string | undefined): boolean {
if (!template)
return true;
if (!text)
@@ -180,17 +211,36 @@ function matchesText(text: string | undefined, template: RegExp | string | undef
return !!text.match(template);
}
-export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } {
- const root = generateAriaTree(rootElement);
- const matches = matchesNodeDeep(root, template);
- return { matches, received: renderAriaTree(root) };
+function matchesTextNode(text: string, template: AriaTemplateTextNode) {
+ return matchesText(text, template.text);
}
-function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean {
- if (typeof node === 'string' && (typeof template === 'string' || template instanceof RegExp))
- return matchesText(node, template);
+function matchesName(text: string, template: AriaTemplateRoleNode) {
+ return matchesText(text, template.name);
+}
- if (typeof node === 'object' && typeof template === 'object' && !(template instanceof RegExp)) {
+export type MatcherReceived = {
+ raw: string;
+ regex: string;
+};
+
+export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: MatcherReceived } {
+ const root = generateAriaTree(rootElement);
+ const matches = matchesNodeDeep(root, template);
+ return {
+ matches,
+ received: {
+ raw: renderAriaTree(root, { mode: 'raw' }),
+ regex: renderAriaTree(root, { mode: 'regex' }),
+ }
+ };
+}
+
+function matchesNode(node: AriaNode | string, template: AriaTemplateNode, depth: number): boolean {
+ if (typeof node === 'string' && template.kind === 'text')
+ return matchesTextNode(node, template);
+
+ if (typeof node === 'object' && template.kind === 'role') {
if (template.role !== 'fragment' && template.role !== node.role)
return false;
if (template.checked !== undefined && template.checked !== node.checked)
@@ -205,7 +255,7 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegEx
return false;
if (template.selected !== undefined && template.selected !== node.selected)
return false;
- if (!matchesText(node.name, template.name))
+ if (!matchesName(node.name, template))
return false;
if (!containsList(node.children || [], template.children || [], depth))
return false;
@@ -214,7 +264,7 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegEx
return false;
}
-function containsList(children: (AriaNode | string)[], template: (AriaTemplateNode | RegExp | string)[], depth: number): boolean {
+function containsList(children: (AriaNode | string)[], template: AriaTemplateNode[], depth: number): boolean {
if (template.length > children.length)
return false;
const cc = children.slice();
@@ -251,62 +301,123 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean {
return !!results.length;
}
-export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean }): string {
+export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'regex' }): string {
const lines: string[] = [];
- const visit = (ariaNode: AriaNode | string, indent: string) => {
+ const includeText = options?.mode === 'regex' ? textContributesInfo : () => true;
+ const renderString = options?.mode === 'regex' ? convertToBestGuessRegex : (str: string) => str;
+ const visit = (ariaNode: AriaNode | string, parentAriaNode: AriaNode | null, indent: string) => {
if (typeof ariaNode === 'string') {
- if (!options?.noText)
- lines.push(indent + '- text: ' + quoteYamlString(ariaNode));
+ if (parentAriaNode && !includeText(parentAriaNode, ariaNode))
+ return;
+ const text = renderString(ariaNode);
+ if (text)
+ lines.push(indent + '- text: ' + text);
return;
}
- let line = `${indent}- ${ariaNode.role}`;
- if (ariaNode.name)
- line += ` ${quoteYamlString(ariaNode.name)}`;
+ let key = ariaNode.role;
+ if (ariaNode.name) {
+ const name = renderString(ariaNode.name);
+ if (name)
+ key += ' ' + (name.startsWith('/') && name.endsWith('/') ? name : yamlQuoteFragment(name));
+ }
if (ariaNode.checked === 'mixed')
- line += ` [checked=mixed]`;
+ key += ` [checked=mixed]`;
if (ariaNode.checked === true)
- line += ` [checked]`;
+ key += ` [checked]`;
if (ariaNode.disabled)
- line += ` [disabled]`;
+ key += ` [disabled]`;
if (ariaNode.expanded)
- line += ` [expanded]`;
+ key += ` [expanded]`;
if (ariaNode.level)
- line += ` [level=${ariaNode.level}]`;
+ key += ` [level=${ariaNode.level}]`;
if (ariaNode.pressed === 'mixed')
- line += ` [pressed=mixed]`;
+ key += ` [pressed=mixed]`;
if (ariaNode.pressed === true)
- line += ` [pressed]`;
+ key += ` [pressed]`;
if (ariaNode.selected === true)
- line += ` [selected]`;
+ key += ` [selected]`;
+ const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(key);
if (!ariaNode.children.length) {
- lines.push(line);
+ lines.push(escapedKey);
} else if (ariaNode.children.length === 1 && typeof ariaNode.children[0] === 'string') {
- if (!options?.noText)
- line += ': ' + quoteYamlString(ariaNode.children[0]);
- lines.push(line);
+ const text = includeText(ariaNode, ariaNode.children[0]) ? renderString(ariaNode.children[0] as string) : null;
+ if (text)
+ lines.push(escapedKey + ': ' + yamlEscapeValueIfNeeded(text));
+ else
+ lines.push(escapedKey);
} else {
- lines.push(line + ':');
+ lines.push(escapedKey + ':');
for (const child of ariaNode.children || [])
- visit(child, indent + ' ');
+ visit(child, ariaNode, indent + ' ');
}
};
if (ariaNode.role === 'fragment') {
// Render fragment.
for (const child of ariaNode.children || [])
- visit(child, '');
+ visit(child, ariaNode, '');
} else {
- visit(ariaNode, '');
+ visit(ariaNode, null, '');
}
return lines.join('\n');
}
-function quoteYamlString(str: string) {
- return `"${str
- .replace(/\\/g, '\\\\')
- .replace(/"/g, '\\"')
- .replace(/\n/g, '\\n')
- .replace(/\r/g, '\\r')}"`;
+function convertToBestGuessRegex(text: string): string {
+ const dynamicContent = [
+ // 2mb
+ { regex: /\b[\d,.]+[bkmBKM]+\b/, replacement: '[\\d,.]+[bkmBKM]+' },
+ // 2ms, 20s
+ { regex: /\b\d+[hmsp]+\b/, replacement: '\\d+[hmsp]+' },
+ { regex: /\b[\d,.]+[hmsp]+\b/, replacement: '[\\d,.]+[hmsp]+' },
+ // Do not replace single digits with regex by default.
+ // 2+ digits: [Issue 22, 22.3, 2.33, 2,333]
+ { regex: /\b\d+,\d+\b/, replacement: '\\d+,\\d+' },
+ { regex: /\b\d+\.\d{2,}\b/, replacement: '\\d+\\.\\d+' },
+ { regex: /\b\d{2,}\.\d+\b/, replacement: '\\d+\\.\\d+' },
+ { regex: /\b\d{2,}\b/, replacement: '\\d+' },
+ ];
+
+ let pattern = '';
+ let lastIndex = 0;
+
+ const combinedRegex = new RegExp(dynamicContent.map(r => '(' + r.regex.source + ')').join('|'), 'g');
+ text.replace(combinedRegex, (match, ...args) => {
+ const offset = args[args.length - 2];
+ const groups = args.slice(0, -2);
+ pattern += escapeRegExp(text.slice(lastIndex, offset));
+ for (let i = 0; i < groups.length; i++) {
+ if (groups[i]) {
+ const { replacement } = dynamicContent[i];
+ pattern += replacement;
+ break;
+ }
+ }
+ lastIndex = offset + match.length;
+ return match;
+ });
+ if (!pattern)
+ return text;
+
+ pattern += escapeRegExp(text.slice(lastIndex));
+ return String(new RegExp(pattern));
+}
+
+function textContributesInfo(node: AriaNode, text: string): boolean {
+ if (!text.length)
+ return false;
+
+ if (!node.name)
+ return true;
+
+ if (node.name.length > text.length)
+ return false;
+
+ // Figure out if text adds any value.
+ const substr = longestCommonSubstring(text, node.name);
+ let filtered = text;
+ while (substr && filtered.includes(substr))
+ filtered = filtered.replace(substr, '');
+ return filtered.trim().length / text.length > 0.1;
}
diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts
index 6446d16966..6f0d8ee473 100644
--- a/packages/playwright-core/src/server/injected/injectedScript.ts
+++ b/packages/playwright-core/src/server/injected/injectedScript.ts
@@ -212,10 +212,10 @@ export class InjectedScript {
return new Set(result.map(r => r.element));
}
- ariaSnapshot(node: Node): string {
+ ariaSnapshot(node: Node, options?: { mode?: 'raw' | 'regex' }): string {
if (node.nodeType !== Node.ELEMENT_NODE)
throw this.createStacklessError('Can only capture aria snapshot of Element nodes.');
- return renderedAriaTree(node as Element);
+ return renderedAriaTree(node as Element, options);
}
querySelectorAll(selector: ParsedSelector, root: Node): Element[] {
diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts
index 1d50495c77..389ec04276 100644
--- a/packages/playwright-core/src/server/injected/recorder/recorder.ts
+++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts
@@ -715,7 +715,7 @@ class TextAssertionTool implements RecorderTool {
name: 'assertSnapshot',
selector: this._hoverHighlight.selector,
signals: [],
- snapshot: this._recorder.injectedScript.ariaSnapshot(target),
+ snapshot: this._recorder.injectedScript.ariaSnapshot(target, { mode: 'regex' }),
};
} else {
this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true });
diff --git a/packages/playwright-core/src/server/injected/yaml.ts b/packages/playwright-core/src/server/injected/yaml.ts
new file mode 100644
index 0000000000..97bf3a070d
--- /dev/null
+++ b/packages/playwright-core/src/server/injected/yaml.ts
@@ -0,0 +1,107 @@
+/**
+ * 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 function yamlEscapeKeyIfNeeded(str: string): string {
+ if (!yamlStringNeedsQuotes(str))
+ return str;
+ return `'` + str.replace(/'/g, `''`) + `'`;
+}
+
+export function yamlEscapeValueIfNeeded(str: string): string {
+ if (!yamlStringNeedsQuotes(str))
+ return str;
+ return '"' + str.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, c => {
+ switch (c) {
+ case '\\':
+ return '\\\\';
+ case '"':
+ return '\\"';
+ case '\b':
+ return '\\b';
+ case '\f':
+ return '\\f';
+ case '\n':
+ return '\\n';
+ case '\r':
+ return '\\r';
+ case '\t':
+ return '\\t';
+ default:
+ const code = c.charCodeAt(0);
+ return '\\x' + code.toString(16).padStart(2, '0');
+ }
+ }) + '"';
+}
+
+export function yamlQuoteFragment(str: string, quote = '"'): string {
+ return quote + str.replace(/['"]/g, c => {
+ switch (c) {
+ case '"':
+ return quote === '"' ? '\\"' : '"';
+ case '\'':
+ return quote === '\'' ? '\\\'' : '\'';
+ default:
+ return c;
+ }
+ }) + quote;
+}
+
+function yamlStringNeedsQuotes(str: string): boolean {
+ if (str.length === 0)
+ return true;
+
+ // Strings with leading or trailing whitespace need quotes
+ if (/^\s|\s$/.test(str))
+ return true;
+
+ // Strings containing control characters need quotes
+ if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/.test(str))
+ return true;
+
+ // Strings starting with '-' followed by a space need quotes
+ if (/^-\s/.test(str))
+ return true;
+
+ // Strings that start with a special indicator character need quotes
+ if (/^[&*].*/.test(str))
+ return true;
+
+ // Strings containing ':' followed by a space or at the end need quotes
+ if (/:(\s|$)/.test(str))
+ return true;
+
+ // Strings containing '#' preceded by a space need quotes (comment indicator)
+ if (/\s#/.test(str))
+ return true;
+
+ // Strings that contain line breaks need quotes
+ if (/[\n\r]/.test(str))
+ return true;
+
+ // Strings starting with '?' or '!' (directives) need quotes
+ if (/^[?!]/.test(str))
+ return true;
+
+ // Strings starting with '>' or '|' (block scalar indicators) need quotes
+ if (/^[>|]/.test(str))
+ return true;
+
+ // Strings containing special characters that could cause ambiguity
+ if (/[{}`]/.test(str))
+ return true;
+
+ return false;
+}
diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts
index 55f99250c8..d626b1ed3c 100644
--- a/packages/playwright-core/src/server/page.ts
+++ b/packages/playwright-core/src/server/page.ts
@@ -31,7 +31,7 @@ import * as accessibility from './accessibility';
import { FileChooser } from './fileChooser';
import type { Progress } from './progress';
import { ProgressController } from './progress';
-import { LongStandingScope, assert, createGuid, trimStringWithEllipsis } from '../utils';
+import { LongStandingScope, assert, compressCallLog, createGuid, trimStringWithEllipsis } from '../utils';
import { ManualPromise } from '../utils/manualPromise';
import { debugLogger } from '../utils/debugLogger';
import type { ImageComparatorOptions } from '../utils/comparators';
@@ -676,7 +676,7 @@ export class Page extends SdkObject {
if (e instanceof TimeoutError && intermediateResult?.previous)
errorMessage = `Failed to take two consecutive stable screenshots.`;
return {
- log: e.message ? [...metadata.log, e.message] : metadata.log,
+ log: compressCallLog(e.message ? [...metadata.log, e.message] : metadata.log),
...intermediateResult,
errorMessage,
timedOut: (e instanceof TimeoutError),
diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
index 2d611f5cca..cc404c18ba 100644
--- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
+++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
@@ -172,6 +172,7 @@ export async function openTraceViewerApp(url: string, browserName: string, optio
...options?.persistentContextOptions,
useWebSocket: isUnderTest(),
headless: !!options?.headless,
+ colorScheme: isUnderTest() ? 'light' : undefined,
},
});
diff --git a/packages/playwright-core/src/utils/hostPlatform.ts b/packages/playwright-core/src/utils/hostPlatform.ts
index 8c20d65cce..9664418e3d 100644
--- a/packages/playwright-core/src/utils/hostPlatform.ts
+++ b/packages/playwright-core/src/utils/hostPlatform.ts
@@ -86,20 +86,15 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte
return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
}
- if (distroInfo?.id === 'debian' || distroInfo?.id === 'raspbian' || distroInfo?.id === 'devuan') {
+ if (distroInfo?.id === 'debian' || distroInfo?.id === 'raspbian') {
const isOfficiallySupportedPlatform = distroInfo?.id === 'debian';
- let debianVersion = distroInfo?.version;
- if (distroInfo.id === 'devuan') {
- // Devuan is debian-based but it's always 7 versions behind
- debianVersion = String(parseInt(distroInfo.version, 10) + 7);
- }
- if (debianVersion === '11')
+ if (distroInfo?.version === '11')
return { hostPlatform: ('debian11' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
- if (debianVersion === '12')
+ if (distroInfo?.version === '12')
return { hostPlatform: ('debian12' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
// use most recent supported release for 'debian testing' and 'unstable'.
// they never include a numeric version entry in /etc/os-release.
- if (debianVersion === '')
+ if (distroInfo?.version === '')
return { hostPlatform: ('debian12' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
}
return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
index 23c947cc49..ed81c9a033 100644
--- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
+++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
@@ -27,6 +27,13 @@ export function escapeWithQuotes(text: string, char: string = '\'') {
throw new Error('Invalid escape char');
}
+export function escapeTemplateString(text: string): string {
+ return text
+ .replace(/\\/g, '\\\\')
+ .replace(/`/g, '\\`')
+ .replace(/\$\{/g, '\\${');
+}
+
export function isString(obj: any): obj is string {
return typeof obj === 'string' || obj instanceof String;
}
@@ -140,3 +147,32 @@ export function escapeHTMLAttribute(s: string): string {
export function escapeHTML(s: string): string {
return s.replace(/[&<]/ug, char => (escaped as any)[char]);
}
+
+export function longestCommonSubstring(s1: string, s2: string): string {
+ const n = s1.length;
+ const m = s2.length;
+ let maxLen = 0;
+ let endingIndex = 0;
+
+ // Initialize a 2D array with zeros
+ const dp = Array(n + 1)
+ .fill(null)
+ .map(() => Array(m + 1).fill(0));
+
+ // Build the dp table
+ for (let i = 1; i <= n; i++) {
+ for (let j = 1; j <= m; j++) {
+ if (s1[i - 1] === s2[j - 1]) {
+ dp[i][j] = dp[i - 1][j - 1] + 1;
+
+ if (dp[i][j] > maxLen) {
+ maxLen = dp[i][j];
+ endingIndex = i;
+ }
+ }
+ }
+ }
+
+ // Extract the longest common substring
+ return s1.slice(endingIndex - maxLen, endingIndex);
+}
diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts
index 84d08b0184..2e40968ebc 100644
--- a/packages/playwright-core/src/utils/stackTrace.ts
+++ b/packages/playwright-core/src/utils/stackTrace.ts
@@ -131,7 +131,13 @@ export function splitErrorMessage(message: string): { name: string, message: str
export function formatCallLog(log: string[] | undefined): string {
if (!log || !log.some(l => !!l))
return '';
+ return `
+Call log:
+${colors.dim(log.join('\n'))}
+`;
+}
+export function compressCallLog(log: string[]): string[] {
const lines: string[] = [];
for (const block of findRepeatedSubsequences(log)) {
@@ -148,10 +154,7 @@ export function formatCallLog(log: string[] | undefined): string {
lines.push(whitespacePrefix + '- ' + line.trim());
}
}
- return `
-Call log:
-${colors.dim(lines.join('\n'))}
-`;
+ return lines;
}
export type ExpectZone = {
diff --git a/packages/playwright/ThirdPartyNotices.txt b/packages/playwright/ThirdPartyNotices.txt
index f2bb64d661..2931da55c9 100644
--- a/packages/playwright/ThirdPartyNotices.txt
+++ b/packages/playwright/ThirdPartyNotices.txt
@@ -112,6 +112,7 @@ This project incorporates components from the projects listed below. The origina
- escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp)
- fill-range@7.1.1 (https://github.com/jonschlinkert/fill-range)
- gensync@1.0.0-beta.2 (https://github.com/loganfsmyth/gensync)
+- get-east-asian-width@1.3.0 (https://github.com/sindresorhus/get-east-asian-width)
- glob-parent@5.1.2 (https://github.com/gulpjs/glob-parent)
- globals@11.12.0 (https://github.com/sindresorhus/globals)
- graceful-fs@4.2.11 (https://github.com/isaacs/node-graceful-fs)
@@ -3410,6 +3411,20 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
=========================================
END OF gensync@1.0.0-beta.2 AND INFORMATION
+%% get-east-asian-width@1.3.0 NOTICES AND INFORMATION BEGIN HERE
+=========================================
+MIT License
+
+Copyright (c) Sindre Sorhus (https://sindresorhus.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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.
+=========================================
+END OF get-east-asian-width@1.3.0 AND INFORMATION
+
%% glob-parent@5.1.2 NOTICES AND INFORMATION BEGIN HERE
=========================================
The ISC License
@@ -4399,6 +4414,6 @@ END OF yallist@3.1.1 AND INFORMATION
SUMMARY BEGIN HERE
=========================================
-Total Packages: 151
+Total Packages: 152
=========================================
END OF SUMMARY
\ No newline at end of file
diff --git a/packages/playwright/bundles/babel/src/babelBundleImpl.ts b/packages/playwright/bundles/babel/src/babelBundleImpl.ts
index 5e09be2a0c..78c3c0403e 100644
--- a/packages/playwright/bundles/babel/src/babelBundleImpl.ts
+++ b/packages/playwright/bundles/babel/src/babelBundleImpl.ts
@@ -23,7 +23,6 @@ import * as babel from '@babel/core';
export { codeFrameColumns } from '@babel/code-frame';
export { declare } from '@babel/helper-plugin-utils';
export { types } from '@babel/core';
-export { parse } from '@babel/parser';
import traverseFunction from '@babel/traverse';
export const traverse = traverseFunction;
@@ -114,16 +113,25 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins
let isTransforming = false;
-export function babelTransform(code: string, filename: string, isTypeScript: boolean, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][]): BabelFileResult {
+function isTypeScript(filename: string) {
+ return filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.mts') || filename.endsWith('.cts');
+}
+
+export function babelTransform(code: string, filename: string, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][]): BabelFileResult {
if (isTransforming)
return {};
// Prevent reentry while requiring plugins lazily.
isTransforming = true;
try {
- const options = babelTransformOptions(isTypeScript, isModule, pluginsPrologue, pluginsEpilogue);
+ const options = babelTransformOptions(isTypeScript(filename), isModule, pluginsPrologue, pluginsEpilogue);
return babel.transform(code, { filename, ...options })!;
} finally {
isTransforming = false;
}
}
+
+export function babelParse(code: string, filename: string, isModule: boolean): babel.ParseResult {
+ const options = babelTransformOptions(isTypeScript(filename), isModule, [], []);
+ return babel.parse(code, { filename, ...options })!;
+}
diff --git a/packages/playwright/bundles/utils/package-lock.json b/packages/playwright/bundles/utils/package-lock.json
index fcf9f972fe..5fd1392d33 100644
--- a/packages/playwright/bundles/utils/package-lock.json
+++ b/packages/playwright/bundles/utils/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"chokidar": "3.6.0",
"enquirer": "2.3.6",
+ "get-east-asian-width": "1.3.0",
"json5": "2.2.3",
"pirates": "4.0.4",
"source-map-support": "0.5.21",
@@ -146,6 +147,18 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/get-east-asian-width": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
+ "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -376,6 +389,11 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"optional": true
},
+ "get-east-asian-width": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
+ "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="
+ },
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
diff --git a/packages/playwright/bundles/utils/package.json b/packages/playwright/bundles/utils/package.json
index 69477909c5..3fb09c1d17 100644
--- a/packages/playwright/bundles/utils/package.json
+++ b/packages/playwright/bundles/utils/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"chokidar": "3.6.0",
"enquirer": "2.3.6",
+ "get-east-asian-width": "1.3.0",
"json5": "2.2.3",
"pirates": "4.0.4",
"source-map-support": "0.5.21",
diff --git a/packages/playwright/bundles/utils/src/utilsBundleImpl.ts b/packages/playwright/bundles/utils/src/utilsBundleImpl.ts
index 7c29c301a8..6cd35e2885 100644
--- a/packages/playwright/bundles/utils/src/utilsBundleImpl.ts
+++ b/packages/playwright/bundles/utils/src/utilsBundleImpl.ts
@@ -31,3 +31,6 @@ export const enquirer = enquirerLibrary;
import chokidarLibrary from 'chokidar';
export const chokidar = chokidarLibrary;
+
+import * as getEastAsianWidthLibrary from 'get-east-asian-width';
+export const getEastAsianWidth = getEastAsianWidthLibrary;
diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts
index 496c1b9a08..a475ec39d8 100644
--- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts
+++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts
@@ -23,6 +23,8 @@ import { EXPECTED_COLOR } from '../common/expectBundle';
import { callLogText } from '../util';
import { printReceivedStringContainExpectedSubstring } from './expect';
import { currentTestInfo } from '../common/globals';
+import type { MatcherReceived } from '@injected/ariaSnapshot';
+import { escapeTemplateString } from 'playwright-core/lib/utils';
export async function toMatchAriaSnapshot(
this: ExpectMatcherState,
@@ -70,28 +72,38 @@ export async function toMatchAriaSnapshot(
const timeout = options.timeout ?? this.timeout;
expected = unshift(expected);
const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout });
+ const typedReceived = received as MatcherReceived | typeof kNoElementsFoundError;
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
- const notFound = received === kNoElementsFoundError;
+ const notFound = typedReceived === kNoElementsFoundError;
+ if (notFound) {
+ return {
+ pass: this.isNot,
+ message: () => messagePrefix + `Expected: ${this.utils.printExpected(expected)}\nReceived: ${EXPECTED_COLOR('not found')}` + callLogText(log),
+ name: 'toMatchAriaSnapshot',
+ expected,
+ };
+ }
+
const escapedExpected = escapePrivateUsePoints(expected);
- const escapedReceived = escapePrivateUsePoints(received);
+ const escapedReceived = escapePrivateUsePoints(typedReceived.raw);
const message = () => {
if (pass) {
if (notFound)
return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived: ${escapedReceived}` + callLogText(log);
const printedReceived = printReceivedStringContainExpectedSubstring(escapedReceived, escapedReceived.indexOf(escapedExpected), escapedExpected.length);
- return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived string: ${printedReceived}` + callLogText(log);
+ return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived: ${printedReceived}` + callLogText(log);
} else {
const labelExpected = `Expected`;
if (notFound)
return messagePrefix + `${labelExpected}: ${this.utils.printExpected(escapedExpected)}\nReceived: ${escapedReceived}` + callLogText(log);
- return messagePrefix + this.utils.printDiffOrStringify(escapedExpected, escapedReceived, labelExpected, 'Received string', false) + callLogText(log);
+ return messagePrefix + this.utils.printDiffOrStringify(escapedExpected, escapedReceived, labelExpected, 'Received', false) + callLogText(log);
}
};
if (!this.isNot && pass === this.isNot && generateNewBaseline) {
// Only rebaseline failed snapshots.
- const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${indent(received, '${indent} ')}\n\${indent}\`)`;
+ const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
return { pass: this.isNot, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
}
@@ -107,7 +119,7 @@ export async function toMatchAriaSnapshot(
}
function escapePrivateUsePoints(str: string) {
- return str.replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
+ return escapeTemplateString(str).replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
}
function unshift(snapshot: string): string {
diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts
index 9bb5da81f6..3e452d6c44 100644
--- a/packages/playwright/src/reporters/base.ts
+++ b/packages/playwright/src/reporters/base.ts
@@ -18,6 +18,7 @@ import { colors as realColors, ms as milliseconds, parseStackTraceLine } from 'p
import path from 'path';
import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location } from '../../types/testReporter';
import { getPackageManagerExecCommand } from 'playwright-core/lib/utils';
+import { getEastAsianWidth } from '../utilsBundle';
import type { ReporterV2 } from './reporterV2';
import { resolveReporterOutputPath } from '../util';
export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
@@ -490,11 +491,35 @@ export function stripAnsiEscapes(str: string): string {
return str.replace(ansiRegex, '');
}
+function characterWidth(c: string) {
+ return getEastAsianWidth.eastAsianWidth(c.codePointAt(0)!);
+}
+
+function stringWidth(v: string) {
+ let width = 0;
+ for (const { segment } of new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(v))
+ width += characterWidth(segment);
+ return width;
+}
+
+function suffixOfWidth(v: string, width: number) {
+ const segments = [...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(v)];
+ let suffixBegin = v.length;
+ for (const { segment, index } of segments.reverse()) {
+ const segmentWidth = stringWidth(segment);
+ if (segmentWidth > width)
+ break;
+ width -= segmentWidth;
+ suffixBegin = index;
+ }
+ return v.substring(suffixBegin);
+}
+
// Leaves enough space for the "prefix" to also fit.
-function fitToWidth(line: string, width: number, prefix?: string): string {
+export function fitToWidth(line: string, width: number, prefix?: string): string {
const prefixLength = prefix ? stripAnsiEscapes(prefix).length : 0;
width -= prefixLength;
- if (line.length <= width)
+ if (stringWidth(line) <= width)
return line;
// Even items are plain text, odd items are control sequences.
@@ -505,13 +530,14 @@ function fitToWidth(line: string, width: number, prefix?: string): string {
// Include all control sequences to preserve formatting.
taken.push(parts[i]);
} else {
- let part = parts[i].substring(parts[i].length - width);
- if (part.length < parts[i].length && part.length > 0) {
+ let part = suffixOfWidth(parts[i], width);
+ const wasTruncated = part.length < parts[i].length;
+ if (wasTruncated && parts[i].length > 0) {
// Add ellipsis if we are truncating.
- part = '\u2026' + part.substring(1);
+ part = '\u2026' + suffixOfWidth(parts[i], width - 1);
}
taken.push(part);
- width -= part.length;
+ width -= stringWidth(part);
}
}
return taken.reverse().join('');
diff --git a/packages/playwright/src/runner/rebase.ts b/packages/playwright/src/runner/rebase.ts
index 17717e977e..7558ea0800 100644
--- a/packages/playwright/src/runner/rebase.ts
+++ b/packages/playwright/src/runner/rebase.ts
@@ -17,9 +17,10 @@
import path from 'path';
import fs from 'fs';
import type { T } from '../transform/babelBundle';
-import { types, traverse, parse } from '../transform/babelBundle';
+import { types, traverse, babelParse } from '../transform/babelBundle';
import { MultiMap } from 'playwright-core/lib/utils';
import { generateUnifiedDiff } from 'playwright-core/lib/utils';
+import { colors } from 'playwright-core/lib/utilsBundle';
import type { FullConfigInternal } from '../common/config';
import { filterProjects } from './projectUtils';
const t: typeof T = types;
@@ -45,15 +46,20 @@ export function addSuggestedRebaseline(location: Location, suggestedRebaseline:
export async function applySuggestedRebaselines(config: FullConfigInternal) {
if (config.config.updateSnapshots !== 'all' && config.config.updateSnapshots !== 'missing')
return;
+ if (!suggestedRebaselines.size)
+ return;
const [project] = filterProjects(config.projects, config.cliProjectFilter);
if (!project)
return;
- for (const fileName of suggestedRebaselines.keys()) {
+ const patches: string[] = [];
+ const files: string[] = [];
+
+ for (const fileName of [...suggestedRebaselines.keys()].sort()) {
const source = await fs.promises.readFile(fileName, 'utf8');
const lines = source.split('\n');
const replacements = suggestedRebaselines.get(fileName);
- const fileNode = parse(source, { sourceType: 'module' });
+ const fileNode = babelParse(source, fileName, true);
const ranges: { start: number, end: number, oldText: string, newText: string }[] = [];
traverse(fileNode, {
@@ -75,7 +81,7 @@ export async function applySuggestedRebaselines(config: FullConfigInternal) {
if (matcher.loc!.start.column + 1 !== replacement.location.column)
continue;
const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0];
- const newText = replacement.code.replace(/\$\{indent\}/g, indent);
+ const newText = replacement.code.replace(/\{indent\}/g, indent);
ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText });
}
}
@@ -87,9 +93,15 @@ export async function applySuggestedRebaselines(config: FullConfigInternal) {
result = result.substring(0, range.start) + range.newText + result.substring(range.end);
const relativeName = path.relative(process.cwd(), fileName);
-
- const patchFile = path.join(project.project.outputDir, 'rebaselines.patch');
- await fs.promises.mkdir(path.dirname(patchFile), { recursive: true });
- await fs.promises.writeFile(patchFile, generateUnifiedDiff(source, result, relativeName));
+ files.push(relativeName);
+ patches.push(generateUnifiedDiff(source, result, relativeName.replace(/\\/g, '/')));
}
+
+ const patchFile = path.join(project.project.outputDir, 'rebaselines.patch');
+ await fs.promises.mkdir(path.dirname(patchFile), { recursive: true });
+ await fs.promises.writeFile(patchFile, patches.join('\n'));
+
+ const fileList = files.map(file => ' ' + colors.dim(file)).join('\n');
+ // eslint-disable-next-line no-console
+ console.log(`New baselines created for:\n\n${fileList}\n\n ` + colors.cyan('git apply ' + path.relative(process.cwd(), patchFile)) + '\n');
}
diff --git a/packages/playwright/src/transform/babelBundle.ts b/packages/playwright/src/transform/babelBundle.ts
index 2806a05aec..d2f8b5919a 100644
--- a/packages/playwright/src/transform/babelBundle.ts
+++ b/packages/playwright/src/transform/babelBundle.ts
@@ -14,14 +14,15 @@
* limitations under the License.
*/
-import type { BabelFileResult } from '../../bundles/babel/node_modules/@types/babel__core';
+import type { BabelFileResult, ParseResult } from '../../bundles/babel/node_modules/@types/babel__core';
export const codeFrameColumns: typeof import('../../bundles/babel/node_modules/@types/babel__code-frame').codeFrameColumns = require('./babelBundleImpl').codeFrameColumns;
export const declare: typeof import('../../bundles/babel/node_modules/@types/babel__helper-plugin-utils').declare = require('./babelBundleImpl').declare;
export const types: typeof import('../../bundles/babel/node_modules/@types/babel__core').types = require('./babelBundleImpl').types;
-export const parse: typeof import('../../bundles/babel/node_modules/@babel/parser/typings/babel-parser').parse = require('./babelBundleImpl').parse;
export const traverse: typeof import('../../bundles/babel/node_modules/@types/babel__traverse').default = require('./babelBundleImpl').traverse;
export type BabelPlugin = [string, any?];
-export type BabelTransformFunction = (code: string, filename: string, isTypeScript: boolean, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult;
+export type BabelTransformFunction = (code: string, filename: string, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult;
export const babelTransform: BabelTransformFunction = require('./babelBundleImpl').babelTransform;
+export type BabelParseFunction = (code: string, filename: string, isModule: boolean) => ParseResult;
+export const babelParse: BabelParseFunction = require('./babelBundleImpl').babelParse;
export type { NodePath, types as T, PluginObj } from '../../bundles/babel/node_modules/@types/babel__core';
export type { BabelAPI } from '../../bundles/babel/node_modules/@types/babel__helper-plugin-utils';
diff --git a/packages/playwright/src/transform/transform.ts b/packages/playwright/src/transform/transform.ts
index f70f385b5b..549d83a168 100644
--- a/packages/playwright/src/transform/transform.ts
+++ b/packages/playwright/src/transform/transform.ts
@@ -215,7 +215,6 @@ export function setTransformData(pluginName: string, value: any) {
}
export function transformHook(originalCode: string, filename: string, moduleUrl?: string): { code: string, serializedCache?: any } {
- const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.mts') || filename.endsWith('.cts');
const hasPreprocessor =
process.env.PW_TEST_SOURCE_TRANSFORM &&
process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE &&
@@ -233,7 +232,7 @@ export function transformHook(originalCode: string, filename: string, moduleUrl?
const { babelTransform }: { babelTransform: BabelTransformFunction } = require('./babelBundle');
transformData = new Map();
- const { code, map } = babelTransform(originalCode, filename, isTypeScript, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
+ const { code, map } = babelTransform(originalCode, filename, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
if (!code)
return { code: '', serializedCache };
const added = addToCache!(code, map, transformData);
diff --git a/packages/playwright/src/utilsBundle.ts b/packages/playwright/src/utilsBundle.ts
index eec0cf9a5a..d0e2a37e77 100644
--- a/packages/playwright/src/utilsBundle.ts
+++ b/packages/playwright/src/utilsBundle.ts
@@ -20,4 +20,5 @@ export const sourceMapSupport: typeof import('../bundles/utils/node_modules/@typ
export const stoppable: typeof import('../bundles/utils/node_modules/@types/stoppable') = require('./utilsBundleImpl').stoppable;
export const enquirer: typeof import('../bundles/utils/node_modules/enquirer') = require('./utilsBundleImpl').enquirer;
export const chokidar: typeof import('../bundles/utils/node_modules/chokidar') = require('./utilsBundleImpl').chokidar;
+export const getEastAsianWidth: typeof import('../bundles/utils/node_modules/get-east-asian-width') = require('./utilsBundleImpl').getEastAsianWidth;
export type { RawSourceMap } from '../bundles/utils/node_modules/source-map';
diff --git a/packages/web/src/ansi2html.ts b/packages/web/src/ansi2html.ts
index e1d80ed073..827e081ae3 100644
--- a/packages/web/src/ansi2html.ts
+++ b/packages/web/src/ansi2html.ts
@@ -14,11 +14,16 @@
limitations under the License.
*/
-export function ansi2html(text: string): string {
+export function ansi2html(text: string, defaultColors?: { bg: string, fg: string }): string {
const regex = /(\x1b\[(\d+(;\d+)*)m)|([^\x1b]+)/g;
const tokens: string[] = [];
let match;
let style: any = {};
+
+ let reverse = false;
+ let fg: string | undefined = defaultColors?.fg;
+ let bg: string | undefined = defaultColors?.bg;
+
while ((match = regex.exec(text)) !== null) {
const [, , codeStr, , text] = match;
if (codeStr) {
@@ -29,11 +34,28 @@ export function ansi2html(text: string): string {
case 2: style['opacity'] = '0.8'; break;
case 3: style['font-style'] = 'italic'; break;
case 4: style['text-decoration'] = 'underline'; break;
+ case 7:
+ reverse = true;
+ break;
case 8: style.display = 'none'; break;
case 9: style['text-decoration'] = 'line-through'; break;
- case 22: style = { ...style, 'font-weight': undefined, 'font-style': undefined, 'opacity': undefined, 'text-decoration': undefined }; break;
- case 23: style = { ...style, 'font-weight': undefined, 'font-style': undefined, 'opacity': undefined }; break;
- case 24: style = { ...style, 'text-decoration': undefined }; break;
+ case 22:
+ delete style['font-weight'];
+ delete style['font-style'];
+ delete style['opacity'];
+ delete style['text-decoration'];
+ break;
+ case 23:
+ delete style['font-weight'];
+ delete style['font-style'];
+ delete style['opacity'];
+ break;
+ case 24:
+ delete style['text-decoration'];
+ break;
+ case 27:
+ reverse = false;
+ break;
case 30:
case 31:
case 32:
@@ -41,8 +63,12 @@ export function ansi2html(text: string): string {
case 34:
case 35:
case 36:
- case 37: style.color = ansiColors[code - 30]; break;
- case 39: style = { ...style, color: undefined }; break;
+ case 37:
+ fg = ansiColors[code - 30];
+ break;
+ case 39:
+ fg = defaultColors?.fg;
+ break;
case 40:
case 41:
case 42:
@@ -50,8 +76,12 @@ export function ansi2html(text: string): string {
case 44:
case 45:
case 46:
- case 47: style['background-color'] = ansiColors[code - 40]; break;
- case 49: style = { ...style, 'background-color': undefined }; break;
+ case 47:
+ bg = ansiColors[code - 40];
+ break;
+ case 49:
+ bg = defaultColors?.bg;
+ break;
case 53: style['text-decoration'] = 'overline'; break;
case 90:
case 91:
@@ -60,7 +90,9 @@ export function ansi2html(text: string): string {
case 94:
case 95:
case 96:
- case 97: style.color = brightAnsiColors[code - 90]; break;
+ case 97:
+ fg = brightAnsiColors[code - 90];
+ break;
case 100:
case 101:
case 102:
@@ -68,10 +100,19 @@ export function ansi2html(text: string): string {
case 104:
case 105:
case 106:
- case 107: style['background-color'] = brightAnsiColors[code - 100]; break;
+ case 107:
+ bg = brightAnsiColors[code - 100];
+ break;
}
} else if (text) {
- tokens.push(`${escapeHTML(text)}`);
+ const styleCopy = { ...style };
+ const color = reverse ? bg : fg;
+ if (color !== undefined)
+ styleCopy['color'] = color;
+ const backgroundColor = reverse ? fg : bg;
+ if (backgroundColor !== undefined)
+ styleCopy['background-color'] = backgroundColor;
+ tokens.push(`${escapeHTML(text)}`);
}
}
return tokens.join('');
diff --git a/packages/web/src/components/expandable.tsx b/packages/web/src/components/expandable.tsx
index 01e064373f..a837bd0a0c 100644
--- a/packages/web/src/components/expandable.tsx
+++ b/packages/web/src/components/expandable.tsx
@@ -24,14 +24,20 @@ export const Expandable: React.FunctionComponent> = ({ title, children, setExpanded, expanded, expandOnTitleClick }) => {
+ const id = React.useId();
return
-
expandOnTitleClick && setExpanded(!expanded)}>
+
expandOnTitleClick && setExpanded(!expanded)}>
!expandOnTitleClick && setExpanded(!expanded)} />
{title}
- { expanded &&
{children}
}
+ { expanded &&
{children}
}
;
};
diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx
index cb7ab7150d..e6cf557e99 100644
--- a/packages/web/src/components/treeView.tsx
+++ b/packages/web/src/components/treeView.tsx
@@ -231,6 +231,7 @@ export function TreeItemHeader
({
icon,
isKeyboardNavigation,
setIsKeyboardNavigation }: TreeItemHeaderProps) {
+ const groupId = React.useId();
const itemRef = React.useRef(null);
React.useEffect(() => {
@@ -251,7 +252,7 @@ export function TreeItemHeader({
const titled = title?.(item);
const iconed = icon?.(item) || 'codicon-blank';
- return
+ return
onAccepted?.(item)}
className={clsx(
@@ -281,7 +282,7 @@ export function TreeItemHeader
({
{icon && }
{typeof rendered === 'string' ? {rendered}
: rendered}
- {!!children.length &&
+ {!!children.length &&
{children.map(child => {
const itemData = treeItems.get(child);
return itemData &&
{
if (!options?.allowConnectRequests)
return;
+ if (kConnectHostsToIgnore.has(req.url))
+ return;
this.connectHosts.push(req.url);
req.url = `127.0.0.1:${port}`;
});
diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts
index a732985b5b..cd77a210d5 100644
--- a/tests/library/browsertype-connect.spec.ts
+++ b/tests/library/browsertype-connect.spec.ts
@@ -169,7 +169,8 @@ for (const kind of ['launchServer', 'run-server'] as const) {
await browser.close();
});
- test('should ignore page.pause when headed', async ({ connect, startRemoteServer, browserType }) => {
+ test('should ignore page.pause when headed', async ({ connect, startRemoteServer, browserType, channel }) => {
+ test.skip(channel === 'chromium-headless-shell', 'Headless Shell does not support headed mode');
const headless = (browserType as any)._defaultLaunchOptions.headless;
(browserType as any)._defaultLaunchOptions.headless = false;
const remoteServer = await startRemoteServer(kind);
diff --git a/tests/library/channels.spec.ts b/tests/library/channels.spec.ts
index 03becec063..314a55a3e5 100644
--- a/tests/library/channels.spec.ts
+++ b/tests/library/channels.spec.ts
@@ -239,7 +239,7 @@ it('should not generate dispatchers for subresources w/o listeners', async ({ pa
});
});
-it('should work with the domain module', async ({ browserType, server, browserName }) => {
+it('should work with the domain module', async ({ browserType, server, browserName, channel }) => {
const local = domain.create();
local.run(() => { });
let err;
@@ -262,7 +262,7 @@ it('should work with the domain module', async ({ browserType, server, browserNa
if (browserName === 'firefox')
expect(message).toBe('CLOSE_ABNORMAL');
else
- expect(message).toContain(': 400');
+ expect(message).toContain(channel?.includes('msedge') ? '' : ': 400');
await browser.close();
diff --git a/tests/library/chromium/oopif.spec.ts b/tests/library/chromium/oopif.spec.ts
index 3878aa5142..54cd3e1a46 100644
--- a/tests/library/chromium/oopif.spec.ts
+++ b/tests/library/chromium/oopif.spec.ts
@@ -232,7 +232,8 @@ it('should click a button when it overlays oopif', async function({ page, browse
expect(await page.evaluate(() => (window as any)['BUTTON_CLICKED'])).toBe(true);
});
-it('should report google.com frame with headed', async ({ browserType, server }) => {
+it('should report google.com frame with headed', async ({ browserType, server, channel }) => {
+ it.skip(channel === 'chromium-headless-shell', 'Headless Shell does not support headed mode');
// @see https://github.com/GoogleChrome/puppeteer/issues/2548
// https://google.com is isolated by default in Chromium embedder.
const browser = await browserType.launch({ headless: false });
diff --git a/tests/library/inspector/cli-codegen-aria.spec.ts b/tests/library/inspector/cli-codegen-aria.spec.ts
index f99f65fd6f..840725067b 100644
--- a/tests/library/inspector/cli-codegen-aria.spec.ts
+++ b/tests/library/inspector/cli-codegen-aria.spec.ts
@@ -39,4 +39,24 @@ test.describe(() => {
await expect.poll(() =>
recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button \\"Submit\\"");`);
});
+
+ test('should generate regex in aria snapshot', async ({ openRecorder }) => {
+ const { recorder } = await openRecorder();
+ await recorder.setContentAndWait(``);
+
+ await recorder.page.click('x-pw-tool-item.snapshot');
+ await recorder.page.hover('button');
+ await recorder.trustedClick();
+
+ await expect.poll(() =>
+ recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button /Submit \\\\d+/\`);`);
+ await expect.poll(() =>
+ recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button /Submit \\\\d+/")`);
+ await expect.poll(() =>
+ recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button /Submit \\\\d+/")`);
+ await expect.poll(() =>
+ recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button /Submit \\\\d+/");`);
+ await expect.poll(() =>
+ recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button /Submit \\\\d+/");`);
+ });
});
diff --git a/tests/library/modernizr.spec.ts b/tests/library/modernizr.spec.ts
index 7e3fc49f6f..c0a06f3714 100644
--- a/tests/library/modernizr.spec.ts
+++ b/tests/library/modernizr.spec.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import { hostPlatform } from '../../packages/playwright-core/src/utils/hostPlatform';
import { browserTest as it, expect } from '../config/browserTest';
import fs from 'fs';
import os from 'os';
@@ -33,6 +34,7 @@ async function checkFeatures(name: string, context: any, server: any) {
it('Safari Desktop', async ({ browser, browserName, platform, server, headless }) => {
it.skip(browserName !== 'webkit');
it.skip(browserName === 'webkit' && platform === 'darwin' && os.arch() === 'x64', 'Modernizr uses WebGL which is not available on Intel macOS - https://bugs.webkit.org/show_bug.cgi?id=278277');
+ it.skip(browserName === 'webkit' && hostPlatform.startsWith('ubuntu20.04'), 'Ubuntu 20.04 is frozen');
const context = await browser.newContext({
deviceScaleFactor: 2
});
@@ -52,7 +54,6 @@ it('Safari Desktop', async ({ browser, browserName, platform, server, headless }
actual.video = !!actual.video;
if (platform === 'linux') {
- expected.subpixelfont = false;
expected.speechrecognition = false;
expected.publickeycredential = false;
expected.mediastream = false;
@@ -96,6 +97,7 @@ it('Safari Desktop', async ({ browser, browserName, platform, server, headless }
it('Mobile Safari', async ({ playwright, browser, browserName, platform, server, headless }) => {
it.skip(browserName !== 'webkit');
it.skip(browserName === 'webkit' && platform === 'darwin' && os.arch() === 'x64', 'Modernizr uses WebGL which is not available on Intel macOS - https://bugs.webkit.org/show_bug.cgi?id=278277');
+ it.skip(browserName === 'webkit' && hostPlatform.startsWith('ubuntu20.04'), 'Ubuntu 20.04 is frozen');
const iPhone = playwright.devices['iPhone 12'];
const context = await browser.newContext(iPhone);
const { actual, expected } = await checkFeatures('mobile-safari-18', context, server);
@@ -119,7 +121,6 @@ it('Mobile Safari', async ({ playwright, browser, browserName, platform, server,
}
if (platform === 'linux') {
- expected.subpixelfont = false;
expected.speechrecognition = false;
expected.publickeycredential = false;
expected.mediastream = false;
diff --git a/tests/library/web-socket.spec.ts b/tests/library/web-socket.spec.ts
index 179c332307..94b8ff1b2b 100644
--- a/tests/library/web-socket.spec.ts
+++ b/tests/library/web-socket.spec.ts
@@ -137,7 +137,7 @@ it('should emit binary frame events', async ({ page, server }) => {
expect(sent[1][i]).toBe(i);
});
-it('should emit error', async ({ page, server, browserName }) => {
+it('should emit error', async ({ page, server, browserName, channel }) => {
let callback;
const result = new Promise(f => callback = f);
page.on('websocket', ws => ws.on('socketerror', callback));
@@ -148,7 +148,7 @@ it('should emit error', async ({ page, server, browserName }) => {
if (browserName === 'firefox')
expect(message).toBe('CLOSE_ABNORMAL');
else
- expect(message).toContain(': 400');
+ expect(message).toContain(channel?.includes('msedge') ? '' : ': 400');
});
it('should not have stray error events', async ({ page, server }) => {
diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts
index 86843d886a..e9156ebad8 100644
--- a/tests/page/page-aria-snapshot.spec.ts
+++ b/tests/page/page-aria-snapshot.spec.ts
@@ -64,8 +64,8 @@ it('should snapshot list with accessible name', async ({ page }) => {
`);
await checkAndMatchSnapshot(page.locator('body'), `
- list "my list":
- - listitem: "one"
- - listitem: "two"
+ - listitem: one
+ - listitem: two
`);
});
@@ -92,7 +92,7 @@ it('should allow text nodes', async ({ page }) => {
await checkAndMatchSnapshot(page.locator('body'), `
- heading "Microsoft" [level=1]
- - text: "Open source projects and samples from Microsoft"
+ - text: Open source projects and samples from Microsoft
`);
});
@@ -105,7 +105,7 @@ it('should snapshot details visibility', async ({ page }) => {
`);
await checkAndMatchSnapshot(page.locator('body'), `
- - group: "Summary"
+ - group: Summary
`);
});
@@ -145,10 +145,10 @@ it('should snapshot integration', async ({ page }) => {
await checkAndMatchSnapshot(page.locator('body'), `
- heading "Microsoft" [level=1]
- - text: "Open source projects and samples from Microsoft"
+ - text: Open source projects and samples from Microsoft
- list:
- listitem:
- - group: "Verified"
+ - group: Verified
- listitem:
- link "Sponsor"
`);
@@ -164,7 +164,7 @@ it('should support multiline text', async ({ page }) => {
`);
await checkAndMatchSnapshot(page.locator('body'), `
- - paragraph: "Line 1 Line 2 Line 3"
+ - paragraph: Line 1 Line 2 Line 3
`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- paragraph: |
@@ -180,7 +180,7 @@ it('should concatenate span text', async ({ page }) => {
`);
await checkAndMatchSnapshot(page.locator('body'), `
- - text: "One Two Three"
+ - text: One Two Three
`);
});
@@ -190,7 +190,7 @@ it('should concatenate span text 2', async ({ page }) => {
`);
await checkAndMatchSnapshot(page.locator('body'), `
- - text: "One Two Three"
+ - text: One Two Three
`);
});
@@ -200,7 +200,7 @@ it('should concatenate div text with spaces', async ({ page }) => {
`);
await checkAndMatchSnapshot(page.locator('body'), `
- - text: "One Two Three"
+ - text: One Two Three
`);
});
@@ -362,12 +362,12 @@ it('should snapshot inner text', async ({ page }) => {
await checkAndMatchSnapshot(page.locator('body'), `
- listitem:
- - text: "a.test.ts"
+ - text: a.test.ts
- button "Run"
- button "Show source"
- button "Watch"
- listitem:
- - text: "snapshot 30ms"
+ - text: snapshot 30ms
- button "Run"
- button "Show source"
- button "Watch"
@@ -382,12 +382,11 @@ it('should include pseudo codepoints', async ({ page, server }) => {
`);
await checkAndMatchSnapshot(page.locator('body'), `
- - paragraph: "\ueab2hello"
+ - paragraph: \ueab2hello
`);
});
-it('check aria-hidden text', async ({ page, server }) => {
- await page.goto(server.EMPTY_PAGE);
+it('check aria-hidden text', async ({ page }) => {
await page.setContent(`
hello
@@ -396,12 +395,11 @@ it('check aria-hidden text', async ({ page, server }) => {
`);
await checkAndMatchSnapshot(page.locator('body'), `
- - paragraph: "hello"
+ - paragraph: hello
`);
});
-it('should ignore presentation and none roles', async ({ page, server }) => {
- await page.goto(server.EMPTY_PAGE);
+it('should ignore presentation and none roles', async ({ page }) => {
await page.setContent(`
- hello
@@ -410,6 +408,54 @@ it('should ignore presentation and none roles', async ({ page, server }) => {
`);
await checkAndMatchSnapshot(page.locator('body'), `
- - list: "hello world"
+ - list: hello world
+ `);
+});
+
+it('should treat input value as text in templates', async ({ page }) => {
+ await page.setContent(`
+
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - textbox: hello world
+ `);
+});
+
+it('should respect aria-owns', async ({ page }) => {
+ await page.setContent(`
+
+ Link 1
+
+
+ Link 2
+
+
+ Paragraph
+ `);
+
+ // - Different from Chrome DevTools which attributes ownership to the last element.
+ // - CDT also does not include non-owned children in accessible name.
+ // - Disregarding these as aria-owns can't suggest multiple parts by spec.
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - link "Link 1 Value Paragraph":
+ - region: Link 1
+ - textbox: Value
+ - paragraph: Paragraph
+ - link "Link 2 Value Paragraph":
+ - region: Link 2
+ `);
+});
+
+it('should be ok with circular ownership', async ({ page }) => {
+ await page.setContent(`
+
+ Hello
+
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - link "Hello":
+ - region: Hello
`);
});
diff --git a/tests/page/page-autowaiting-basic.spec.ts b/tests/page/page-autowaiting-basic.spec.ts
index a2104530ef..dbfe482c33 100644
--- a/tests/page/page-autowaiting-basic.spec.ts
+++ b/tests/page/page-autowaiting-basic.spec.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
+import { stripAnsi } from 'tests/config/utils';
import type { TestServer } from '../config/testserver';
import { test as it, expect } from './pageTest';
@@ -139,3 +140,21 @@ it('should report navigation in the log when clicking anchor', async ({ page, se
expect(error.message).toContain('waiting for scheduled navigations to finish');
expect(error.message).toContain(`navigated to "${server.PREFIX + '/frames/one-frame.html'}"`);
});
+
+it('should report and collapse log in action', async ({ page, server, mode }) => {
+ await page.setContent(``);
+ const error = await page.locator('input').click({ timeout: 5000 }).catch(e => e);
+ const message = stripAnsi(error.message);
+ expect(message).toContain(`Call log:`);
+ expect(message).toMatch(/\d+ × waiting for/);
+ const logLines = message.substring(message.indexOf('Call log:')).split('\n');
+ expect(logLines.length).toBeLessThan(30);
+});
+
+it('should report and collapse log in expect', async ({ page, server, mode }) => {
+ await page.setContent(``);
+ const error = await expect(page.locator('input')).toBeVisible({ timeout: 5000 }).catch(e => e);
+ const message = stripAnsi(error.message);
+ expect(message).toContain(`Call log:`);
+ expect(message).toMatch(/\d+ × locator resolved to/);
+});
diff --git a/tests/page/to-match-aria-snapshot.spec.ts b/tests/page/to-match-aria-snapshot.spec.ts
index 8050c3b569..8c8b11525e 100644
--- a/tests/page/to-match-aria-snapshot.spec.ts
+++ b/tests/page/to-match-aria-snapshot.spec.ts
@@ -396,8 +396,8 @@ test('expected formatter', async ({ page }) => {
expect(stripAnsi(error.message)).toContain(`
Locator: locator('body')
-- Expected - 2
-+ Received string + 3
+- Expected - 2
++ Received + 3
- - heading "todos"
- - textbox "Wrong text"
@@ -405,3 +405,56 @@ Locator: locator('body')
+ - heading "todos" [level=1]
+ - textbox "What needs to be done?"`);
});
+
+test('should unpack escaped names', async ({ page }) => {
+ {
+ await page.setContent(`
+
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - 'button "Click: me"'
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - 'button /Click: me/'
+ `);
+ }
+
+ {
+ await page.setContent(`
+
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button "Click / me"
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button /Click \\/ me/
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - 'button /Click \\/ me/'
+ `);
+ }
+
+ {
+ await page.setContent(`
+
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button "Click \\ me"
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button /Click \\\\ me/
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - 'button /Click \\\\ me/'
+ `);
+ }
+
+ {
+ await page.setContent(`
+
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - 'button "Click '' me"'
+ `);
+ }
+});
diff --git a/tests/playwright-test/fit-to-width.spec.ts b/tests/playwright-test/fit-to-width.spec.ts
new file mode 100644
index 0000000000..94e73cb381
--- /dev/null
+++ b/tests/playwright-test/fit-to-width.spec.ts
@@ -0,0 +1,32 @@
+/**
+ * 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 { fitToWidth } from 'packages/playwright/lib/reporters/base';
+import { test, expect } from './playwright-test-fixtures';
+
+test('chinese characters', () => {
+ expect(fitToWidth('ä½ ä½ å¥½', 3)).toBe('…好');
+ expect(fitToWidth('ä½ å¥½ä½ å¥½', 4)).toBe('…好');
+});
+
+test('surrogate pairs', () => {
+ expect(fitToWidth('🫣🤗', 2)).toBe('…');
+ expect(fitToWidth('🫣🤗', 3)).toBe('…🤗');
+ expect(fitToWidth('🚄🚄', 1)).toBe('…');
+ expect(fitToWidth('🚄🚄', 2)).toBe('…');
+ expect(fitToWidth('🚄🚄', 3)).toBe('…🚄');
+ expect(fitToWidth('🚄🚄', 4)).toBe('🚄🚄');
+});
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index 51734be9ff..59d986857f 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -472,7 +472,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await showReport();
await page.click('text=fails');
- await expect(page.locator('.test-error-view span:has-text("received")').nth(1)).toHaveCSS('color', 'rgb(204, 0, 0)');
+ await expect(page.locator('.test-error-view span:has-text("true")').first()).toHaveCSS('color', 'rgb(205, 49, 49)');
});
test('should show trace source', async ({ runInlineTest, page, showReport }) => {
@@ -939,8 +939,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
expect(result.exitCode).toBe(1);
await showReport();
await page.click('text="is a test"');
- const stricken = await page.locator('css=strike').innerText();
- expect(stricken).toBe('old');
+
+ await expect(page.locator('.test-error-view').getByText('old')).toHaveCSS('text-decoration', 'line-through solid rgb(205, 49, 49)');
+ await expect(page.locator('.test-error-view').getByText('new', { exact: true })).toHaveCSS('text-decoration', 'none solid rgb(0, 188, 0)');
});
test('should strikethrough textual diff with commonalities', async ({ runInlineTest, showReport, page }) => {
@@ -966,8 +967,32 @@ for (const useIntermediateMergeReport of [true, false] as const) {
expect(result.exitCode).toBe(1);
await showReport();
await page.click('text="is a test"');
- const stricken = await page.locator('css=strike').innerText();
- expect(stricken).toBe('old');
+ await expect(page.locator('.test-error-view').getByText('old')).toHaveCSS('text-decoration', 'line-through solid rgb(205, 49, 49)');
+ await expect(page.locator('.test-error-view').getByText('new', { exact: true })).toHaveCSS('text-decoration', 'none solid rgb(0, 188, 0)');
+ await expect(page.locator('.test-error-view').getByText('common Expected:')).toHaveCSS('text-decoration', 'none solid rgb(36, 41, 47)');
+ });
+
+ test('should highlight inline textual diff in toHaveText', async ({ runInlineTest, showReport, page }) => {
+ const result = await runInlineTest({
+ 'a.spec.ts': `
+ import { test, expect } from '@playwright/test';
+ test('is a test', async ({ page }) => {
+ await page.setContent('begin inner end
');
+ await expect(page.locator('div')).toHaveText('inner', { timeout: 500 });
+ });
+ `
+ }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
+ expect(result.exitCode).toBe(1);
+ await showReport();
+ await page.click('text="is a test"');
+ await expect(page.locator('.test-error-view').getByText('begin ', { exact: true })).toHaveCSS('color', 'rgb(246, 248, 250)');
+ await expect(page.locator('.test-error-view').getByText('begin ', { exact: true })).toHaveCSS('background-color', 'rgb(205, 49, 49)');
+
+ await expect(page.locator('.test-error-view').getByText('inner', { exact: true })).toHaveCSS('color', 'rgb(205, 49, 49)');
+ await expect(page.locator('.test-error-view').getByText('inner', { exact: true })).toHaveCSS('background-color', 'rgb(246, 248, 250)');
+
+ await expect(page.locator('.test-error-view').getByText('end ', { exact: true })).toHaveCSS('color', 'rgb(246, 248, 250)');
+ await expect(page.locator('.test-error-view').getByText('end ', { exact: true })).toHaveCSS('background-color', 'rgb(205, 49, 49)');
});
test('should differentiate repeat-each test cases', async ({ runInlineTest, showReport, page }) => {
@@ -984,13 +1009,13 @@ for (const useIntermediateMergeReport of [true, false] as const) {
expect(result.exitCode).toBe(1);
await showReport();
- await page.locator('text=sample').first().click();
- await expect(page.locator('text=ouch')).toHaveCount(1);
- await page.locator('text=All').first().click();
+ await page.getByText('sample').first().click();
+ await expect(page.getByText('ouch')).toHaveCount(2);
+ await page.getByText('All').first().click();
- await page.locator('text=sample').nth(1).click();
- await expect(page.locator('text=Before Hooks')).toBeVisible();
- await expect(page.locator('text=ouch')).toBeHidden();
+ await page.getByText('sample').nth(1).click();
+ await expect(page.getByText('Before Hooks')).toBeVisible();
+ await expect(page.getByText('ouch')).toBeHidden();
});
test('should group similar / loop steps', async ({ runInlineTest, showReport, page }) => {
diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json
index 9b14a59c90..17cc3a9635 100644
--- a/tests/playwright-test/stable-test-runner/package-lock.json
+++ b/tests/playwright-test/stable-test-runner/package-lock.json
@@ -5,15 +5,15 @@
"packages": {
"": {
"dependencies": {
- "@playwright/test": "1.49.0-alpha-2024-10-26"
+ "@playwright/test": "1.49.0-alpha-2024-10-30"
}
},
"node_modules/@playwright/test": {
- "version": "1.49.0-alpha-2024-10-26",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-26.tgz",
- "integrity": "sha512-EUl8wIsAWVJJlX2ynKdY1KxRs44Yz9MPDmN8AH6HIdwazSRe1ML46kaM3V49gQvMVMo5JZfuXnRzbtYDMFpKYA==",
+ "version": "1.49.0-alpha-2024-10-30",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-30.tgz",
+ "integrity": "sha512-7pq4a+eDCkp6VmGGpr6KanL0gQ2SunC9dAjtP+VZLobdaY0ZL7XkmD2rL8UNANF2AkmKdOf+GmTS+wZ42qhvLg==",
"dependencies": {
- "playwright": "1.49.0-alpha-2024-10-26"
+ "playwright": "1.49.0-alpha-2024-10-30"
},
"bin": {
"playwright": "cli.js"
@@ -36,11 +36,11 @@
}
},
"node_modules/playwright": {
- "version": "1.49.0-alpha-2024-10-26",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-26.tgz",
- "integrity": "sha512-1qh/6z4UdWv7qMocNQmUMbvZAXzzS93jckUzjGr0mWMn9rs4QavHhuK0s2HIS0hLB+t5T1+NBUpHudWzeasudA==",
+ "version": "1.49.0-alpha-2024-10-30",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-30.tgz",
+ "integrity": "sha512-OJ++0IaaTyBHZuPMi7kNZ/ssyRvN4Fkh7NCpYBRyfPL8H90bEVwDe7j4Ab79HMBLxUZMg7D7aRIlimmYmVdbpQ==",
"dependencies": {
- "playwright-core": "1.49.0-alpha-2024-10-26"
+ "playwright-core": "1.49.0-alpha-2024-10-30"
},
"bin": {
"playwright": "cli.js"
@@ -53,9 +53,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.49.0-alpha-2024-10-26",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-26.tgz",
- "integrity": "sha512-ELIdRRHkdzkHP7siPcFSE9jBLRnDHE1l3UigIgEzVN9o34yGBgH8TAkC2uK1M8Jrkomc3jKQm5faiBsimu0XEQ==",
+ "version": "1.49.0-alpha-2024-10-30",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-30.tgz",
+ "integrity": "sha512-T1KDI5SQPqzVIahMOpCX7GE2Slv/5KEM+gSnj5mQZDi57Z8Ij5xnGz6ZX4KBdDrmkBRHLrRM4ijXfH1Q7zNkEg==",
"bin": {
"playwright-core": "cli.js"
},
@@ -66,11 +66,11 @@
},
"dependencies": {
"@playwright/test": {
- "version": "1.49.0-alpha-2024-10-26",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-26.tgz",
- "integrity": "sha512-EUl8wIsAWVJJlX2ynKdY1KxRs44Yz9MPDmN8AH6HIdwazSRe1ML46kaM3V49gQvMVMo5JZfuXnRzbtYDMFpKYA==",
+ "version": "1.49.0-alpha-2024-10-30",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-30.tgz",
+ "integrity": "sha512-7pq4a+eDCkp6VmGGpr6KanL0gQ2SunC9dAjtP+VZLobdaY0ZL7XkmD2rL8UNANF2AkmKdOf+GmTS+wZ42qhvLg==",
"requires": {
- "playwright": "1.49.0-alpha-2024-10-26"
+ "playwright": "1.49.0-alpha-2024-10-30"
}
},
"fsevents": {
@@ -80,18 +80,18 @@
"optional": true
},
"playwright": {
- "version": "1.49.0-alpha-2024-10-26",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-26.tgz",
- "integrity": "sha512-1qh/6z4UdWv7qMocNQmUMbvZAXzzS93jckUzjGr0mWMn9rs4QavHhuK0s2HIS0hLB+t5T1+NBUpHudWzeasudA==",
+ "version": "1.49.0-alpha-2024-10-30",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-30.tgz",
+ "integrity": "sha512-OJ++0IaaTyBHZuPMi7kNZ/ssyRvN4Fkh7NCpYBRyfPL8H90bEVwDe7j4Ab79HMBLxUZMg7D7aRIlimmYmVdbpQ==",
"requires": {
"fsevents": "2.3.2",
- "playwright-core": "1.49.0-alpha-2024-10-26"
+ "playwright-core": "1.49.0-alpha-2024-10-30"
}
},
"playwright-core": {
- "version": "1.49.0-alpha-2024-10-26",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-26.tgz",
- "integrity": "sha512-ELIdRRHkdzkHP7siPcFSE9jBLRnDHE1l3UigIgEzVN9o34yGBgH8TAkC2uK1M8Jrkomc3jKQm5faiBsimu0XEQ=="
+ "version": "1.49.0-alpha-2024-10-30",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-30.tgz",
+ "integrity": "sha512-T1KDI5SQPqzVIahMOpCX7GE2Slv/5KEM+gSnj5mQZDi57Z8Ij5xnGz6ZX4KBdDrmkBRHLrRM4ijXfH1Q7zNkEg=="
}
}
}
diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json
index 93ee2dba85..793663f7d5 100644
--- a/tests/playwright-test/stable-test-runner/package.json
+++ b/tests/playwright-test/stable-test-runner/package.json
@@ -1,6 +1,6 @@
{
"private": true,
"dependencies": {
- "@playwright/test": "1.49.0-alpha-2024-10-26"
+ "@playwright/test": "1.49.0-alpha-2024-10-30"
}
}
diff --git a/tests/playwright-test/ui-mode-test-output.spec.ts b/tests/playwright-test/ui-mode-test-output.spec.ts
index b10c02d08a..46cdc2e478 100644
--- a/tests/playwright-test/ui-mode-test-output.spec.ts
+++ b/tests/playwright-test/ui-mode-test-output.spec.ts
@@ -145,6 +145,17 @@ test('should format console messages in page', async ({ runUITest }, testInfo) =
'Failed to load resource: net::ERR_CONNECTION_REFUSED',
]);
+ await expect(page.locator('.console-tab')).toMatchAriaSnapshot(`
+ - list:
+ - listitem: "/:1 Object {a: 1}/"
+ - listitem: "/:4 Date/"
+ - listitem: "/:5 Regex \/a\//"
+ - listitem: "/:6 Number 0 one 2/"
+ - listitem: "/:7 Download the React DevTools for a better development experience: https:\/\/fb\.me\/react-devtools/"
+ - listitem: "/:8 Array of values/"
+ - listitem: "/Failed to load resource: net::ERR_CONNECTION_REFUSED/"
+ `);
+
const label = page.getByText('React DevTools');
await expect(label).toHaveCSS('color', 'rgb(255, 0, 0)');
await expect(label).toHaveCSS('font-weight', '700');
diff --git a/tests/playwright-test/update-aria-snapshot.spec.ts b/tests/playwright-test/update-aria-snapshot.spec.ts
index a5f56ea28e..c9c9b0056e 100644
--- a/tests/playwright-test/update-aria-snapshot.spec.ts
+++ b/tests/playwright-test/update-aria-snapshot.spec.ts
@@ -15,7 +15,8 @@
*/
import * as fs from 'fs';
-import { test, expect } from './playwright-test-fixtures';
+import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures';
+import { execSync } from 'child_process';
test.describe.configure({ mode: 'parallel' });
@@ -47,6 +48,10 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
});
`);
+
+ execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
+ const result2 = await runInlineTest({});
+ expect(result2.exitCode).toBe(0);
});
test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
@@ -76,4 +81,250 @@ test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
});
`);
+
+ execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
+ const result2 = await runInlineTest({});
+ expect(result2.exitCode).toBe(0);
+});
+
+test('should generate baseline with regex', async ({ runInlineTest }, testInfo) => {
+ const result = await runInlineTest({
+ 'a.spec.ts': `
+ import { test, expect } from '@playwright/test';
+ test('test', async ({ page }) => {
+ await page.setContent(\`
+ - Item 1
+ - Item 2
+ - Time 15:30
+ - Year 2022
+ - Duration 12ms
+ - 22,333
+ - 2,333.79
+ - Total 22
+ - /Regex 1/
+ - /Regex 22ms/
+
\`);
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
+ });
+ `
+ });
+
+ expect(result.exitCode).toBe(0);
+ const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
+ const data = fs.readFileSync(patchPath, 'utf-8');
+ expect(data).toBe(`--- a/a.spec.ts
++++ b/a.spec.ts
+@@ -13,6 +13,18 @@
+ - /Regex 1/
+ - /Regex 22ms/
+
\`);
+- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
++ await expect(page.locator('body')).toMatchAriaSnapshot(\`
++ - list:
++ - listitem: Item 1
++ - listitem: Item 2
++ - listitem: /Time \\\\d+:\\\\d+/
++ - listitem: /Year \\\\d+/
++ - listitem: /Duration \\\\d+[hmsp]+/
++ - listitem: /\\\\d+,\\\\d+/
++ - listitem: /\\\\d+,\\\\d+\\\\.\\\\d+/
++ - listitem: /Total \\\\d+/
++ - listitem: /Regex 1/
++ - listitem: /\\\\/Regex \\\\d+[hmsp]+\\\\//
++ \`);
+ });
+
+`);
+
+ execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
+ const result2 = await runInlineTest({});
+ expect(result2.exitCode).toBe(0);
+});
+
+test('should generate baseline with special characters', async ({ runInlineTest }, testInfo) => {
+ const result = await runInlineTest({
+ 'a.spec.ts': `
+ import { test, expect } from '@playwright/test';
+ test('test', async ({ page }) => {
+ await page.setContent(\`
+
+
+
+
+ - Item: 1
+ - Item {a: b}
+
\`);
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
+ });
+ `
+ });
+
+ expect(result.exitCode).toBe(0);
+ const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
+ const data = fs.readFileSync(patchPath, 'utf-8');
+ expect(data).toBe(`--- a/a.spec.ts
++++ b/a.spec.ts
+@@ -9,6 +9,14 @@
+ Item: 1
+ Item {a: b}
+ \`);
+- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
++ await expect(page.locator('body')).toMatchAriaSnapshot(\`
++ - list:
++ - 'button "Click: me"'
++ - 'button /Click: \\\\d+/'
++ - button "Click ' me"
++ - 'button "Click: '' me"'
++ - listitem: \"Item: 1\"
++ - listitem: \"Item {a: b}\"
++ \`);
+ });
+
+`);
+
+ execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
+ const result2 = await runInlineTest({});
+ expect(result2.exitCode).toBe(0);
+});
+
+test('should update missing snapshots in tsx', async ({ runInlineTest }, testInfo) => {
+ const result = await runInlineTest({
+ 'playwright.config.ts': playwrightCtConfigText,
+ 'playwright/index.html': ``,
+ 'playwright/index.ts': ``,
+
+ 'src/button.tsx': `
+ export const Button = () => ;
+ `,
+
+ 'src/button.test.tsx': `
+ import { test, expect } from '@playwright/experimental-ct-react';
+ import { Button } from './button.tsx';
+
+ test('pass', async ({ mount }) => {
+ const component = await mount();
+ await expect(component).toMatchAriaSnapshot(\`\`);
+ });
+ `,
+ });
+
+ expect(result.exitCode).toBe(0);
+ const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
+ const data = fs.readFileSync(patchPath, 'utf-8');
+ expect(data).toBe(`--- a/src/button.test.tsx
++++ b/src/button.test.tsx
+@@ -4,6 +4,8 @@
+
+ test('pass', async ({ mount }) => {
+ const component = await mount();
+- await expect(component).toMatchAriaSnapshot(\`\`);
++ await expect(component).toMatchAriaSnapshot(\`
++ - button \"Button\"
++ \`);
+ });
+
+`);
+
+ execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
+ const result2 = await runInlineTest({});
+ expect(result2.exitCode).toBe(0);
+});
+
+test('should update multiple files', async ({ runInlineTest }, testInfo) => {
+ const result = await runInlineTest({
+ 'playwright.config.ts': playwrightCtConfigText,
+ 'playwright/index.html': ``,
+ 'playwright/index.ts': ``,
+
+ 'src/button.tsx': `
+ export const Button = () => ;
+ `,
+
+ 'src/button-1.test.tsx': `
+ import { test, expect } from '@playwright/experimental-ct-react';
+ import { Button } from './button.tsx';
+
+ test('pass 1', async ({ mount }) => {
+ const component = await mount();
+ await expect(component).toMatchAriaSnapshot(\`\`);
+ });
+ `,
+
+ 'src/button-2.test.tsx': `
+ import { test, expect } from '@playwright/experimental-ct-react';
+ import { Button } from './button.tsx';
+
+ test('pass 2', async ({ mount }) => {
+ const component = await mount();
+ await expect(component).toMatchAriaSnapshot(\`\`);
+ });
+ `,
+ });
+
+ expect(result.exitCode).toBe(0);
+ const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
+ const data = fs.readFileSync(patchPath, 'utf-8');
+ expect(data).toBe(`--- a/src/button-1.test.tsx
++++ b/src/button-1.test.tsx
+@@ -4,6 +4,8 @@
+
+ test('pass 1', async ({ mount }) => {
+ const component = await mount();
+- await expect(component).toMatchAriaSnapshot(\`\`);
++ await expect(component).toMatchAriaSnapshot(\`
++ - button \"Button\"
++ \`);
+ });
+
+
+--- a/src/button-2.test.tsx
++++ b/src/button-2.test.tsx
+@@ -4,6 +4,8 @@
+
+ test('pass 2', async ({ mount }) => {
+ const component = await mount();
+- await expect(component).toMatchAriaSnapshot(\`\`);
++ await expect(component).toMatchAriaSnapshot(\`
++ - button \"Button\"
++ \`);
+ });
+
+`);
+
+ execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
+ const result2 = await runInlineTest({});
+ expect(result2.exitCode).toBe(0);
+});
+
+test('should generate baseline for input values', async ({ runInlineTest }, testInfo) => {
+ const result = await runInlineTest({
+ 'a.spec.ts': `
+ import { test, expect } from '@playwright/test';
+ test('test', async ({ page }) => {
+ await page.setContent(\`\`);
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
+ });
+ `
+ });
+
+ expect(result.exitCode).toBe(0);
+ const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
+ const data = fs.readFileSync(patchPath, 'utf-8');
+ expect(data).toBe(`--- a/a.spec.ts
++++ b/a.spec.ts
+@@ -2,6 +2,8 @@
+ import { test, expect } from '@playwright/test';
+ test('test', async ({ page }) => {
+ await page.setContent(\`\`);
+- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
++ await expect(page.locator('body')).toMatchAriaSnapshot(\`
++ - textbox: hello world
++ \`);
+ });
+
+`);
+
+ execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
+ const result2 = await runInlineTest({});
+ expect(result2.exitCode).toBe(0);
});
diff --git a/utils/build/build-playwright-driver.sh b/utils/build/build-playwright-driver.sh
index d587cdc94d..f89e770c77 100755
--- a/utils/build/build-playwright-driver.sh
+++ b/utils/build/build-playwright-driver.sh
@@ -4,7 +4,7 @@ set -x
trap "cd $(pwd -P)" EXIT
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
-NODE_VERSION="20.18.0" # autogenerated via ./update-playwright-driver-version.mjs
+NODE_VERSION="22.11.0" # autogenerated via ./update-playwright-driver-version.mjs
cd "$(dirname "$0")"
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy
index ff24c31c88..d4d0cbad8a 100644
--- a/utils/docker/Dockerfile.jammy
+++ b/utils/docker/Dockerfile.jammy
@@ -14,7 +14,7 @@ RUN apt-get update && \
apt-get install -y curl wget gpg ca-certificates && \
mkdir -p /etc/apt/keyrings && \
curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
- echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \
+ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \
apt-get update && \
apt-get install -y nodejs && \
# Feature-parity with node.js base images.
diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble
index 29ca98c4e4..62ec6232c2 100644
--- a/utils/docker/Dockerfile.noble
+++ b/utils/docker/Dockerfile.noble
@@ -14,7 +14,7 @@ RUN apt-get update && \
apt-get install -y curl wget gpg ca-certificates && \
mkdir -p /etc/apt/keyrings && \
curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
- echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \
+ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \
apt-get update && \
apt-get install -y nodejs && \
# Feature-parity with node.js base images.