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..7b52904bdc 100644
--- a/packages/playwright-core/browsers.json
+++ b/packages/playwright-core/browsers.json
@@ -3,15 +3,15 @@
"browsers": [
{
"name": "chromium",
- "revision": "1145",
+ "revision": "1146",
"installByDefault": true,
- "browserVersion": "131.0.6778.13"
+ "browserVersion": "131.0.6778.24"
},
{
"name": "chromium-tip-of-tree",
- "revision": "1271",
+ "revision": "1274",
"installByDefault": false,
- "browserVersion": "132.0.6791.0"
+ "browserVersion": "132.0.6809.0"
},
{
"name": "firefox",
@@ -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/bidi/bidiChromium.ts b/packages/playwright-core/src/server/bidi/bidiChromium.ts
index 32751bd51a..9572ac71ed 100644
--- a/packages/playwright-core/src/server/bidi/bidiChromium.ts
+++ b/packages/playwright-core/src/server/bidi/bidiChromium.ts
@@ -112,10 +112,7 @@ export class BidiChromium extends BrowserType {
if (options.devtools)
chromeArguments.push('--auto-open-devtools-for-tabs');
if (options.headless) {
- if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
- chromeArguments.push('--headless=new');
- else
- chromeArguments.push('--headless=old');
+ chromeArguments.push('--headless');
chromeArguments.push(
'--hide-scrollbars',
diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts
index 023ee0b7d1..2bae0eaf5a 100644
--- a/packages/playwright-core/src/server/chromium/chromium.ts
+++ b/packages/playwright-core/src/server/chromium/chromium.ts
@@ -309,10 +309,7 @@ export class Chromium extends BrowserType {
if (options.devtools)
chromeArguments.push('--auto-open-devtools-for-tabs');
if (options.headless) {
- if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
- chromeArguments.push('--headless=new');
- else
- chromeArguments.push('--headless=old');
+ chromeArguments.push('--headless');
chromeArguments.push(
'--hide-scrollbars',
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/recorder.ts b/packages/playwright-core/src/server/recorder.ts
index a2be2992e6..8b026c789a 100644
--- a/packages/playwright-core/src/server/recorder.ts
+++ b/packages/playwright-core/src/server/recorder.ts
@@ -30,6 +30,8 @@ import type { IRecorderAppFactory, IRecorderApp, IRecorder } from './recorder/re
import { metadataToCallLog } from './recorder/recorderUtils';
import type * as actions from '@recorder/actions';
import { buildFullSelector } from '../utils/isomorphic/recorderUtils';
+import { stringifySelector } from '../utils/isomorphic/selectorParser';
+import type { Frame } from './frames';
const recorderSymbol = Symbol('recorderSymbol');
@@ -146,12 +148,12 @@ export class Recorder implements InstrumentationListener, IRecorder {
this._pushAllSources();
});
- await this._context.exposeBinding('__pw_recorderState', false, source => {
+ await this._context.exposeBinding('__pw_recorderState', false, async source => {
let actionSelector = '';
let actionPoint: Point | undefined;
const hasActiveScreenshotCommand = [...this._currentCallsMetadata.keys()].some(isScreenshotCommand);
if (!hasActiveScreenshotCommand) {
- actionSelector = this._highlightedSelector;
+ actionSelector = await this._scopeHighlightedSelectorToFrame(source.frame);
for (const [metadata, sdkObject] of this._currentCallsMetadata) {
if (source.page === sdkObject.attribution.page) {
actionPoint = metadata.point || actionPoint;
@@ -243,13 +245,38 @@ export class Recorder implements InstrumentationListener, IRecorder {
this._refreshOverlay();
}
+ private async _scopeHighlightedSelectorToFrame(frame: Frame): Promise {
+ try {
+ const mainFrame = frame._page.mainFrame();
+ const resolved = await mainFrame.selectors.resolveFrameForSelector(this._highlightedSelector);
+ // selector couldn't be found, don't highlight anything
+ if (!resolved)
+ return '';
+
+ // selector points to no specific frame, highlight in all frames
+ if (resolved?.frame === mainFrame)
+ return stringifySelector(resolved.info.parsed);
+
+ // selector points to this frame, highlight it
+ if (resolved?.frame === frame)
+ return stringifySelector(resolved.info.parsed);
+
+ // selector points to a different frame, highlight nothing
+ return '';
+ } catch {
+ return '';
+ }
+ }
+
setOutput(codegenId: string, outputFile: string | undefined) {
this._contextRecorder.setOutput(codegenId, outputFile);
}
private _refreshOverlay() {
- for (const page of this._context.pages())
- page.mainFrame().evaluateExpression('window.__pw_refreshOverlay()').catch(() => {});
+ for (const page of this._context.pages()) {
+ for (const frame of page.frames())
+ frame.evaluateExpression('window.__pw_refreshOverlay()').catch(() => {});
+ }
}
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
index 5de0cc3f07..7fccbc3ec2 100644
--- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
+++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
@@ -112,6 +112,7 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[
continue;
}
+ // /trace/file?path=/path/to/trace.zip
const url = new URL('/trace/file', server.urlPrefix('precise'));
url.searchParams.set('path', traceUrl);
params.append('trace', url.toString());
@@ -178,6 +179,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/trace-viewer/src/sw/traceModelBackends.ts b/packages/trace-viewer/src/sw/traceModelBackends.ts
index 70fbc90dd2..1318b298a7 100644
--- a/packages/trace-viewer/src/sw/traceModelBackends.ts
+++ b/packages/trace-viewer/src/sw/traceModelBackends.ts
@@ -30,7 +30,6 @@ export class ZipTraceModelBackend implements TraceModelBackend {
constructor(traceURL: string, progress: Progress) {
this._traceURL = traceURL;
- zipjs.configure({ baseURL: self.location.href } as any);
this._zipReader = new zipjs.ZipReader(
new zipjs.HttpReader(traceURL, { mode: 'cors', preventHeadRequest: true } as any),
{ useWebWorkers: false });
@@ -82,19 +81,16 @@ export class ZipTraceModelBackend implements TraceModelBackend {
}
export class FetchTraceModelBackend implements TraceModelBackend {
- private _entriesPromise: Promise