=
- T extends new (...angs: any) => { $props: infer P; } ? NonNullable :
- T extends (props: infer P, ...args: any) => any ? P :
- {};
-
-export interface MountOptions {
- props?: ComponentProps;
- slots?: ComponentSlots;
- on?: ComponentEvents;
- hooksConfig?: HooksConfig;
-}
-
-export interface MountOptionsJsx {
- hooksConfig?: HooksConfig;
-}
-
-export interface MountResult extends Locator {
- unmount(): Promise;
- update(options: {
- props?: Partial>;
- slots?: Partial;
- on?: Partial;
- }): Promise;
-}
-
-export interface MountResultJsx extends Locator {
- unmount(): Promise;
- update(component: JSX.Element): Promise;
-}
-
-export const test: TestType<{
- mount(
- component: JSX.Element,
- options?: MountOptionsJsx
- ): Promise;
- mount(
- component: Component,
- options?: MountOptions
- ): Promise>;
-}>;
-
-export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core';
diff --git a/packages/playwright-ct-vue2/index.js b/packages/playwright-ct-vue2/index.js
deleted file mode 100644
index 2eeabb0d08..0000000000
--- a/packages/playwright-ct-vue2/index.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * 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.
- */
-
-const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
-const path = require('path');
-
-const defineConfig = (config, ...configs) => {
- return originalDefineConfig({
- ...config,
- '@playwright/test': {
- packageJSON: require.resolve('./package.json'),
- },
- '@playwright/experimental-ct-core': {
- registerSourceFile: path.join(__dirname, 'registerSource.mjs'),
- frameworkPluginFactory: () => import('@vitejs/plugin-vue2').then(plugin => plugin.default()),
- },
- }, ...configs);
-};
-
-module.exports = { test, expect, devices, defineConfig };
diff --git a/packages/playwright-ct-vue2/package.json b/packages/playwright-ct-vue2/package.json
deleted file mode 100644
index 9ec30191ad..0000000000
--- a/packages/playwright-ct-vue2/package.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "name": "@playwright/experimental-ct-vue2",
- "version": "1.49.0-next",
- "description": "Playwright Component Testing for Vue2",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/microsoft/playwright.git"
- },
- "homepage": "https://playwright.dev",
- "engines": {
- "node": ">=18"
- },
- "author": {
- "name": "Microsoft Corporation"
- },
- "license": "Apache-2.0",
- "exports": {
- ".": {
- "types": "./index.d.ts",
- "default": "./index.js"
- },
- "./register": {
- "types": "./register.d.ts",
- "default": "./register.mjs"
- },
- "./hooks": {
- "types": "./hooks.d.ts",
- "default": "./hooks.mjs"
- },
- "./package.json": "./package.json"
- },
- "dependencies": {
- "@playwright/experimental-ct-core": "1.49.0-next",
- "@vitejs/plugin-vue2": "^2.2.0"
- },
- "devDependencies": {
- "vue": "^2.7.14"
- },
- "bin": {
- "playwright": "cli.js"
- }
-}
diff --git a/packages/playwright-ct-vue2/register.d.ts b/packages/playwright-ct-vue2/register.d.ts
deleted file mode 100644
index f88e9be59d..0000000000
--- a/packages/playwright-ct-vue2/register.d.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * 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 default function pwRegister(
- components: Record,
- options?: {
- createApp: any,
- setDevtoolsHook: any,
- h: any,
- }
-): void;
diff --git a/packages/playwright-ct-vue2/register.mjs b/packages/playwright-ct-vue2/register.mjs
deleted file mode 100644
index ca6a6a12d9..0000000000
--- a/packages/playwright-ct-vue2/register.mjs
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * 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 { pwRegister } from './registerSource.mjs';
-
-export default components => {
- pwRegister(components);
-};
diff --git a/packages/playwright-ct-vue2/registerSource.mjs b/packages/playwright-ct-vue2/registerSource.mjs
deleted file mode 100644
index 19b4d41c08..0000000000
--- a/packages/playwright-ct-vue2/registerSource.mjs
+++ /dev/null
@@ -1,212 +0,0 @@
-/**
- * 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.
- */
-
-// @ts-check
-
-// This file is injected into the registry as text, no dependencies are allowed.
-
-import __pwVue, { h as __pwH } from 'vue';
-
-/** @typedef {import('../playwright-ct-core/types/component').Component} Component */
-/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
-/** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */
-/** @typedef {import('vue').Component} FrameworkComponent */
-
-/**
- * @param {any} component
- * @returns {component is ObjectComponent}
- */
-function isObjectComponent(component) {
- return typeof component === 'object' && component && component.__pw_type === 'object-component';
-}
-
-/**
- * @param {any} component
- * @returns {component is JsxComponent}
- */
-function isJsxComponent(component) {
- return typeof component === 'object' && component && component.__pw_type === 'jsx';
-}
-
-/**
- * @param {any} child
- */
-function __pwCreateChild(child) {
- if (Array.isArray(child))
- return child.map(grandChild => __pwCreateChild(grandChild));
- if (isJsxComponent(child) || isObjectComponent(child))
- return __pwCreateWrapper(child);
- return child;
-}
-
-/**
- * Exists to support fallthrough attributes:
- * https://vuejs.org/guide/components/attrs.html#fallthrough-attributes
- * @param {any} Component
- * @param {string} key
- * @return {boolean}
- */
-function __pwComponentHasKeyInProps(Component, key) {
- return typeof Component.props === 'object' && Component.props && key in Component.props;
-}
-
-/**
- * @param {JsxComponent} component
- * @returns {any[] | undefined}
- */
-function __pwJsxChildArray(component) {
- if (!component.props.children)
- return;
- if (Array.isArray(component.props.children))
- return component.props.children;
- return [component.props.children];
-}
-
-/**
- * @param {Component} component
- */
-function __pwCreateComponent(component) {
- const isVueComponent = typeof component.type !== 'string';
-
- /**
- * @type {(import('vue').VNode | string)[]}
- */
- const children = [];
-
- /** @type {import('vue').VNodeData} */
- const nodeData = {};
- nodeData.attrs = {};
- nodeData.props = {};
- nodeData.scopedSlots = {};
- nodeData.on = {};
-
- if (component.__pw_type === 'jsx') {
- for (const child of __pwJsxChildArray(component) || []) {
- if (isJsxComponent(child) && child.type === 'template') {
- const slotProperty = Object.keys(child.props).find(k => k.startsWith('v-slot:'));
- const slot = slotProperty ? slotProperty.substring('v-slot:'.length) : 'default';
- nodeData.scopedSlots[slot] = () => __pwJsxChildArray(child)?.map(c => __pwCreateChild(c));
- } else {
- children.push(__pwCreateChild(child));
- }
- }
-
- for (const [key, value] of Object.entries(component.props)) {
- if (key.startsWith('v-on:')) {
- const event = key.substring('v-on:'.length);
- nodeData.on[event] = value;
- } else {
- if (isVueComponent && __pwComponentHasKeyInProps(component.type, key))
- nodeData.props[key] = value;
- else
- nodeData.attrs[key] = value;
- }
- }
- }
-
- if (component.__pw_type === 'object-component') {
- // Vue test util syntax.
- for (const [key, value] of Object.entries(component.slots || {})) {
- const list = (Array.isArray(value) ? value : [value]).map(v => __pwCreateChild(v));
- if (key === 'default')
- children.push(...list);
- else
- nodeData.scopedSlots[key] = () => list;
- }
- nodeData.props = component.props || {};
- for (const [key, value] of Object.entries(component.on || {}))
- nodeData.on[key] = value;
- }
-
- /** @type {(string|import('vue').VNode)[] | undefined} */
- let lastArg;
- if (Object.entries(nodeData.scopedSlots).length) {
- if (children.length)
- nodeData.scopedSlots.default = () => children;
- } else if (children.length) {
- lastArg = children;
- }
-
- return { Component: component.type, nodeData, slots: lastArg };
-}
-
-/**
- * @param {Component} component
- * @returns {import('vue').VNode}
- */
-function __pwCreateWrapper(component) {
- const { Component, nodeData, slots } = __pwCreateComponent(component);
- const wrapper = __pwH(Component, nodeData, slots);
- return wrapper;
-}
-
-const instanceKey = Symbol('instanceKey');
-const wrapperKey = Symbol('wrapperKey');
-
-window.playwrightMount = async (component, rootElement, hooksConfig) => {
- let options = {};
- for (const hook of window.__pw_hooks_before_mount || [])
- options = await hook({ hooksConfig, Vue: __pwVue });
-
- const instance = new __pwVue({
- ...options,
- render: () => {
- const wrapper = __pwCreateWrapper(component);
- /** @type {any} */ (rootElement)[wrapperKey] = wrapper;
- return wrapper;
- },
- }).$mount();
- rootElement.appendChild(instance.$el);
- /** @type {any} */ (rootElement)[instanceKey] = instance;
-
- for (const hook of window.__pw_hooks_after_mount || [])
- await hook({ hooksConfig, instance });
-};
-
-window.playwrightUnmount = async rootElement => {
- const component = rootElement[instanceKey];
- if (!component)
- throw new Error('Component was not mounted');
- component.$destroy();
- component.$el.remove();
- delete rootElement[instanceKey];
-};
-
-window.playwrightUpdate = async (element, options) => {
- const wrapper = /** @type {any} */(element)[wrapperKey];
- if (!wrapper)
- throw new Error('Component was not mounted');
-
- const component = wrapper.componentInstance;
- if (!component)
- throw new Error('Updating a native HTML element is not supported');
-
- const { nodeData, slots } = __pwCreateComponent(options);
-
- for (const [name, value] of Object.entries(nodeData.on || {})) {
- component.$on(name, value);
- component.$listeners[name] = value;
- }
-
- Object.assign(component.$scopedSlots, nodeData.scopedSlots);
- component.$slots.default = slots;
-
- for (const [key, value] of Object.entries(nodeData.props || {}))
- component[key] = value;
-
- if (!Object.keys(nodeData.props || {}).length)
- component.$forceUpdate();
-};
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/build.js b/packages/playwright/bundles/utils/build.js
index 8c6bd89cf2..6fca8d3e0a 100644
--- a/packages/playwright/bundles/utils/build.js
+++ b/packages/playwright/bundles/utils/build.js
@@ -16,35 +16,14 @@
// @ts-check
const path = require('path');
-const fs = require('fs');
const esbuild = require('esbuild');
-// Can be removed once source-map-support was is fixed.
-/** @type{import('esbuild').Plugin} */
-let patchSource = {
- name: 'patch-source-map-support-deprecation',
- setup(build) {
- build.onResolve({ filter: /^source-map-support$/ }, () => {
- const originalPath = require.resolve('source-map-support');
- const patchedPath = path.join(path.dirname(originalPath), path.basename(originalPath, '.js') + '.pw-patched.js');
- let sourceFileContent = fs.readFileSync(originalPath, 'utf8');
- // source-map-support is overwriting __PW_ZONE__ with func in core if source maps are present.
- const original = `return state.nextPosition.name || originalFunctionName();`;
- const insertedLine = `if (state.nextPosition.name === 'func') return originalFunctionName() || 'func';`;
- sourceFileContent = sourceFileContent.replace(original, insertedLine + original);
- fs.writeFileSync(patchedPath, sourceFileContent);
- return { path: patchedPath }
- });
- },
-};
-
(async () => {
const ctx = await esbuild.context({
entryPoints: [path.join(__dirname, 'src/utilsBundleImpl.ts')],
external: ['fsevents'],
bundle: true,
outdir: path.join(__dirname, '../../lib'),
- plugins: [patchSource],
format: 'cjs',
platform: 'node',
target: 'ES2019',
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/common/configLoader.ts b/packages/playwright/src/common/configLoader.ts
index 37a886d3e8..13d7eac187 100644
--- a/packages/playwright/src/common/configLoader.ts
+++ b/packages/playwright/src/common/configLoader.ts
@@ -378,11 +378,6 @@ export function restartWithExperimentalTsEsm(configFile: string | undefined, for
// Now check for the newer API presence.
if (!require('node:module').register) {
- // Older API is experimental, only supported on Node 16+.
- const nodeVersion = +process.versions.node.split('.')[0];
- if (nodeVersion < 16)
- return false;
-
// With older API requiring a process restart, do so conditionally on the config.
const configIsModule = !!configFile && fileIsModule(configFile);
if (!force && !configIsModule)
diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts
index c4f5afd4b4..dc4264d56b 100644
--- a/packages/playwright/src/matchers/matcherHint.ts
+++ b/packages/playwright/src/matchers/matcherHint.ts
@@ -33,16 +33,12 @@ export function matcherHint(state: ExpectMatcherState, locator: Locator | undefi
export type MatcherResult = {
name: string;
- expected: E;
+ expected?: E;
message: () => string;
pass: boolean;
actual?: A;
log?: string[];
timeout?: number;
- locator?: string;
- printedReceived?: string;
- printedExpected?: string;
- printedDiff?: string;
suggestedRebaseline?: string;
};
diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts
index c0319e371f..f7c68b4544 100644
--- a/packages/playwright/src/matchers/matchers.ts
+++ b/packages/playwright/src/matchers/matchers.ts
@@ -15,7 +15,7 @@
*/
import type { Locator, Page, APIResponse } from 'playwright-core';
-import type { FrameExpectOptions } from 'playwright-core/lib/client/types';
+import type { FrameExpectParams } from 'playwright-core/lib/client/types';
import { colors } from 'playwright-core/lib/utilsBundle';
import { expectTypes, callLogText } from '../util';
import { toBeTruthy } from './toBeTruthy';
@@ -28,7 +28,7 @@ import type { ExpectMatcherState } from '../../types/test';
import { takeFirst } from '../common/config';
export interface LocatorEx extends Locator {
- _expect(expression: string, options: Omit & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
+ _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
}
interface APIResponseEx extends APIResponse {
diff --git a/packages/playwright/src/matchers/toBeTruthy.ts b/packages/playwright/src/matchers/toBeTruthy.ts
index 8902a14eea..abe00a8852 100644
--- a/packages/playwright/src/matchers/toBeTruthy.ts
+++ b/packages/playwright/src/matchers/toBeTruthy.ts
@@ -73,7 +73,5 @@ export async function toBeTruthy(
expected,
log,
timeout: timedOut ? timeout : undefined,
- ...(printedReceived ? { printedReceived } : {}),
- ...(printedExpected ? { printedExpected } : {}),
};
}
diff --git a/packages/playwright/src/matchers/toEqual.ts b/packages/playwright/src/matchers/toEqual.ts
index f75caf87f5..bb0f4e147e 100644
--- a/packages/playwright/src/matchers/toEqual.ts
+++ b/packages/playwright/src/matchers/toEqual.ts
@@ -83,8 +83,5 @@ export async function toEqual(
pass,
log,
timeout: timedOut ? timeout : undefined,
- ...(printedReceived ? { printedReceived } : {}),
- ...(printedExpected ? { printedExpected } : {}),
- ...(printedDiff ? { printedDiff } : {}),
};
}
diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts
index 2c92d562d6..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,
@@ -34,10 +36,10 @@ export async function toMatchAriaSnapshot(
const testInfo = currentTestInfo();
if (!testInfo)
- throw new Error(`toMatchSnapshot() must be called during the test`);
+ throw new Error(`toMatchAriaSnapshot() must be called during the test`);
if (testInfo._projectInternal.ignoreSnapshots)
- return { pass: !this.isNot, message: () => '', name: 'toMatchSnapshot', expected };
+ return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected };
const updateSnapshots = testInfo.config.updateSnapshots;
@@ -54,31 +56,55 @@ export async function toMatchAriaSnapshot(
].join('\n\n'));
}
+ const generateMissingBaseline = updateSnapshots === 'missing' && !expected;
+ const generateNewBaseline = updateSnapshots === 'all' || generateMissingBaseline;
+
+ if (generateMissingBaseline) {
+ if (this.isNot) {
+ const message = `Matchers using ".not" can't generate new baselines`;
+ return { pass: this.isNot, message: () => message, name: 'toMatchAriaSnapshot' };
+ } else {
+ // When generating new baseline, run entire pipeline against impossible match.
+ expected = `- none "Generating new baseline"`;
+ }
+ }
+
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 escapedExpected = unshift(escapePrivateUsePoints(expected));
- const escapedReceived = unshift(escapePrivateUsePoints(received));
+ 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(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);
}
};
- let suggestedRebaseline: string | undefined;
- if (!this.isNot && pass === this.isNot) {
- if (updateSnapshots === 'all' || (updateSnapshots === 'missing' && !expected.trim()))
- suggestedRebaseline = `toMatchAriaSnapshot(\`\n${unshift(received, '${indent} ')}\n\${indent}\`)`;
+ if (!this.isNot && pass === this.isNot && generateNewBaseline) {
+ // Only rebaseline failed snapshots.
+ const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
+ return { pass: this.isNot, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
}
return {
@@ -88,16 +114,15 @@ export async function toMatchAriaSnapshot(
pass,
actual: received,
log,
- suggestedRebaseline,
timeout: timedOut ? timeout : undefined,
};
}
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, indent: string = ''): string {
+function unshift(snapshot: string): string {
const lines = snapshot.split('\n');
let whitespacePrefixLength = 100;
for (const line of lines) {
@@ -108,5 +133,9 @@ function unshift(snapshot: string, indent: string = ''): string {
whitespacePrefixLength = match[1].length;
break;
}
- return lines.filter(t => t.trim()).map(line => indent + line.substring(whitespacePrefixLength)).join('\n');
+ return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n');
+}
+
+function indent(snapshot: string, indent: string): string {
+ return snapshot.split('\n').map(line => indent + line).join('\n');
}
diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts
index 86504062d3..374fba3db5 100644
--- a/packages/playwright/src/matchers/toMatchSnapshot.ts
+++ b/packages/playwright/src/matchers/toMatchSnapshot.ts
@@ -18,7 +18,7 @@ import type { Locator, Page } from 'playwright-core';
import type { ExpectScreenshotOptions, Page as PageEx } from 'playwright-core/lib/client/page';
import { currentTestInfo } from '../common/globals';
import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils';
-import { getComparator, sanitizeForFilePath } from 'playwright-core/lib/utils';
+import { getComparator, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
import {
addSuffixToFilePath,
trimLongString, callLogText,
@@ -31,7 +31,7 @@ import path from 'path';
import { mime } from 'playwright-core/lib/utilsBundle';
import type { TestInfoImpl } from '../worker/testInfo';
import type { ExpectMatcherState } from '../../types/test';
-import type { MatcherResult } from './matcherHint';
+import { matcherHint, type MatcherResult } from './matcherHint';
import type { FullProjectInternal } from '../common/config';
type NameOrSegments = string | string[];
@@ -194,10 +194,6 @@ class SnapshotHelper {
pass,
message: () => message,
log,
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- ...(this.locator ? { locator: this.locator.toString() } : {}),
- printedExpected: this.expectedPath,
- printedReceived: this.actualPath,
};
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult;
}
@@ -250,16 +246,10 @@ class SnapshotHelper {
expected: Buffer | string | undefined,
previous: Buffer | string | undefined,
diff: Buffer | string | undefined,
- diffError: string | undefined,
- log: string[] | undefined,
- title = `${this.kind} comparison failed:`): ImageMatcherResult {
- const output = [
- colors.red(title),
- '',
- ];
- if (diffError)
- output.push(indent(diffError, ' '));
-
+ header: string,
+ diffError: string,
+ log: string[] | undefined): ImageMatcherResult {
+ const output = [`${header}${indent(diffError, ' ')}`];
if (expected !== undefined) {
// Copy the expectation inside the `test-results/` folder for backwards compatibility,
// so that one can upload `test-results/` directory and have all the data inside.
@@ -338,7 +328,9 @@ export function toMatchSnapshot(
return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
}
- return helper.handleDifferent(received, expected, undefined, result.diff, result.errorMessage, undefined);
+ const receiver = isString(received) ? 'string' : 'Buffer';
+ const header = matcherHint(this, undefined, 'toMatchSnapshot', receiver, undefined, undefined);
+ return helper.handleDifferent(received, expected, undefined, result.diff, header, result.errorMessage, undefined);
}
export function toHaveScreenshotStepTitle(
@@ -374,6 +366,7 @@ export async function toHaveScreenshot(
throw new Error(`Screenshot name "${path.basename(helper.expectedPath)}" must have '.png' extension`);
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
const style = await loadScreenshotStyles(helper.options.stylePath);
+ const timeout = helper.options.timeout ?? this.timeout;
const expectScreenshotOptions: ExpectScreenshotOptions = {
locator,
animations: helper.options.animations ?? 'disabled',
@@ -386,7 +379,7 @@ export async function toHaveScreenshot(
scale: helper.options.scale ?? 'css',
style,
isNot: !!this.isNot,
- timeout: helper.options.timeout ?? this.timeout,
+ timeout,
comparator: helper.options.comparator,
maxDiffPixels: helper.options.maxDiffPixels,
maxDiffPixelRatio: helper.options.maxDiffPixelRatio,
@@ -410,13 +403,16 @@ export async function toHaveScreenshot(
if (helper.updateSnapshots === 'none' && !hasSnapshot)
return helper.createMatcherResult(`A snapshot doesn't exist at ${helper.expectedPath}.`, false);
+ const receiver = locator ? 'locator' : 'page';
if (!hasSnapshot) {
// Regenerate a new screenshot by waiting until two screenshots are the same.
- const { actual, previous, diff, errorMessage, log } = await page._expectScreenshot(expectScreenshotOptions);
+ const { actual, previous, diff, errorMessage, log, timedOut } = await page._expectScreenshot(expectScreenshotOptions);
// We tried re-generating new snapshot but failed.
// This can be due to e.g. spinning animation, so we want to show it as a diff.
- if (errorMessage)
- return helper.handleDifferent(actual, undefined, previous, diff, undefined, log, errorMessage);
+ if (errorMessage) {
+ const header = matcherHint(this, locator, 'toHaveScreenshot', receiver, undefined, undefined, timedOut ? timeout : undefined);
+ return helper.handleDifferent(actual, undefined, previous, diff, header, errorMessage, log);
+ }
// We successfully generated new screenshot.
return helper.handleMissing(actual!);
@@ -427,7 +423,7 @@ export async function toHaveScreenshot(
// - regular matcher (i.e. not a `.not`)
// - perhaps an 'all' flag to update non-matching screenshots
expectScreenshotOptions.expected = await fs.promises.readFile(helper.expectedPath);
- const { actual, previous, diff, errorMessage, log } = await page._expectScreenshot(expectScreenshotOptions);
+ const { actual, previous, diff, errorMessage, log, timedOut } = await page._expectScreenshot(expectScreenshotOptions);
if (!errorMessage)
return helper.handleMatching();
@@ -440,7 +436,8 @@ export async function toHaveScreenshot(
return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
}
- return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, errorMessage, log);
+ const header = matcherHint(this, undefined, 'toHaveScreenshot', receiver, undefined, undefined, timedOut ? timeout : undefined);
+ return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log);
}
function writeFileSync(aPath: string, content: Buffer | string) {
diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts
index 76ed48af1e..2f8bc34b21 100644
--- a/packages/playwright/src/matchers/toMatchText.ts
+++ b/packages/playwright/src/matchers/toMatchText.ts
@@ -118,10 +118,5 @@ export async function toMatchText(
actual: received,
log,
timeout: timedOut ? timeout : undefined,
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- locator: receiver.toString(),
- ...(printedReceived ? { printedReceived } : {}),
- ...(printedExpected ? { printedExpected } : {}),
- ...(printedDiff ? { printedDiff } : {}),
};
}
diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts
index 66f78bfdc9..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' };
@@ -26,13 +27,6 @@ export const kOutputSymbol = Symbol('output');
type ErrorDetails = {
message: string;
location?: Location;
- timeout?: number;
- matcherName?: string;
- locator?: string;
- expected?: string;
- received?: string;
- log?: string[];
- snippet?: string;
};
type TestSummary = {
@@ -362,13 +356,6 @@ export function formatResultFailure(test: TestCase, result: TestResult, initialI
errorDetails.push({
message: indent(formattedError.message, initialIndent),
location: formattedError.location,
- timeout: error.timeout,
- matcherName: error.matcherName,
- locator: error.locator,
- expected: error.expected,
- received: error.received,
- log: error.log,
- snippet: error.snippet,
});
}
return errorDetails;
@@ -448,15 +435,16 @@ export function formatError(error: TestError, highlightCode: boolean): ErrorDeta
tokens.push(snippet);
}
- if (parsedStack && parsedStack.stackLines.length) {
- tokens.push('');
+ if (parsedStack && parsedStack.stackLines.length)
tokens.push(colors.dim(parsedStack.stackLines.join('\n')));
- }
let location = error.location;
if (parsedStack && !location)
location = parsedStack.location;
+ if (error.cause)
+ tokens.push(colors.dim('[cause]: ') + formatError(error.cause, highlightCode).message);
+
return {
location,
message: tokens.join('\n'),
@@ -503,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.
@@ -518,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/loadUtils.ts b/packages/playwright/src/runner/loadUtils.ts
index e2c6d0c530..1315eea6e4 100644
--- a/packages/playwright/src/runner/loadUtils.ts
+++ b/packages/playwright/src/runner/loadUtils.ts
@@ -30,7 +30,7 @@ import { applyRepeatEachIndex, bindFileSuiteToProject, filterByFocusedLine, filt
import { createTestGroups, filterForShard, type TestGroup } from './testGroups';
import { dependenciesForTestFile } from '../transform/compilationCache';
import { sourceMapSupport } from '../utilsBundle';
-import type { RawSourceMap } from 'source-map';
+import type { RawSourceMap } from '../utilsBundle';
export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean) {
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/util.ts b/packages/playwright/src/util.ts
index 4046809433..f7f91d3198 100644
--- a/packages/playwright/src/util.ts
+++ b/packages/playwright/src/util.ts
@@ -29,15 +29,17 @@ import type { TestInfoErrorImpl } from './common/ipc';
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json'));
-export function filterStackTrace(e: Error): { message: string, stack: string } {
+export function filterStackTrace(e: Error): { message: string, stack: string, cause?: ReturnType } {
const name = e.name ? e.name + ': ' : '';
+ const cause = e.cause instanceof Error ? filterStackTrace(e.cause) : undefined;
if (process.env.PWDEBUGIMPL)
- return { message: name + e.message, stack: e.stack || '' };
+ return { message: name + e.message, stack: e.stack || '', cause };
const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || []));
return {
message: name + e.message,
- stack: `${name}${e.message}${stackLines.map(line => '\n' + line).join('')}`
+ stack: `${name}${e.message}${stackLines.map(line => '\n' + line).join('')}`,
+ cause,
};
}
diff --git a/packages/playwright/src/utilsBundle.ts b/packages/playwright/src/utilsBundle.ts
index 072e16bb03..d0e2a37e77 100644
--- a/packages/playwright/src/utilsBundle.ts
+++ b/packages/playwright/src/utilsBundle.ts
@@ -20,3 +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/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts
index 5e7a3d80db..eb0ce9d807 100644
--- a/packages/playwright/src/worker/testTracing.ts
+++ b/packages/playwright/src/worker/testTracing.ts
@@ -224,11 +224,18 @@ export class TestTracing {
const stack = rawStack ? filteredStackTrace(rawStack) : [];
this._appendTraceEvent({
type: 'error',
- message: error.message || String(error.value),
+ message: this._formatError(error),
stack,
});
}
+ _formatError(error: TestInfoErrorImpl) {
+ const parts: string[] = [error.message || String(error.value)];
+ if (error.cause)
+ parts.push('[cause]: ' + this._formatError(error.cause));
+ return parts.join('\n');
+ }
+
appendStdioToTrace(type: 'stdout' | 'stderr', chunk: string | Buffer) {
this._appendTraceEvent({
type,
diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts
index 5db30f72e2..706d567dcb 100644
--- a/packages/playwright/types/test.d.ts
+++ b/packages/playwright/types/test.d.ts
@@ -9152,6 +9152,13 @@ export interface TestInfo {
* Information about an error thrown during test execution.
*/
export interface TestInfoError {
+ /**
+ * Error cause. Set when there is a
+ * [cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) for the
+ * error. Will be `undefined` if there is no cause or if the cause is not an instance of [Error].
+ */
+ cause?: TestInfoError;
+
/**
* Error message. Set when [Error] (or its subclass) has been thrown.
*/
diff --git a/packages/playwright/types/testReporter.d.ts b/packages/playwright/types/testReporter.d.ts
index 5663f92f98..04cf03287f 100644
--- a/packages/playwright/types/testReporter.d.ts
+++ b/packages/playwright/types/testReporter.d.ts
@@ -555,40 +555,22 @@ export interface TestCase {
*/
export interface TestError {
/**
- * Expected value formatted as a human-readable string.
+ * Error cause. Set when there is a
+ * [cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) for the
+ * error. Will be `undefined` if there is no cause or if the cause is not an instance of [Error].
*/
- expected?: string;
+ cause?: TestError;
/**
* Error location in the source code.
*/
location?: Location;
- /**
- * Receiver's locator.
- */
- locator?: string;
-
- /**
- * Call log.
- */
- log?: Array;
-
- /**
- * Expect matcher name.
- */
- matcherName?: string;
-
/**
* Error message. Set when [Error] (or its subclass) has been thrown.
*/
message?: string;
- /**
- * Received value formatted as a human-readable string.
- */
- received?: string;
-
/**
* Source code snippet with highlighted error.
*/
@@ -599,11 +581,6 @@ export interface TestError {
*/
stack?: string;
- /**
- * Timeout in milliseconds, if the error was caused by a timeout.
- */
- timeout?: number;
-
/**
* The value that was thrown. Set when anything except the [Error] (or its subclass) has been thrown.
*/
diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts
index 7fcb815468..5b37b33bcf 100644
--- a/packages/protocol/src/channels.ts
+++ b/packages/protocol/src/channels.ts
@@ -2141,7 +2141,7 @@ export type PageReloadResult = {
};
export type PageExpectScreenshotParams = {
expected?: Binary,
- timeout?: number,
+ timeout: number,
isNot: boolean,
locator?: {
frame: FrameChannel,
@@ -2166,7 +2166,6 @@ export type PageExpectScreenshotParams = {
};
export type PageExpectScreenshotOptions = {
expected?: Binary,
- timeout?: number,
locator?: {
frame: FrameChannel,
selector: string,
@@ -2193,6 +2192,7 @@ export type PageExpectScreenshotResult = {
errorMessage?: string,
actual?: Binary,
previous?: Binary,
+ timedOut?: boolean,
log?: string[],
};
export type PageScreenshotParams = {
@@ -3162,7 +3162,7 @@ export type FrameExpectParams = {
expectedValue?: SerializedArgument,
useInnerText?: boolean,
isNot: boolean,
- timeout?: number,
+ timeout: number,
};
export type FrameExpectOptions = {
expressionArg?: any,
@@ -3170,7 +3170,6 @@ export type FrameExpectOptions = {
expectedNumber?: number,
expectedValue?: SerializedArgument,
useInnerText?: boolean,
- timeout?: number,
};
export type FrameExpectResult = {
matches: boolean,
diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml
index 98428cae2f..82de53be24 100644
--- a/packages/protocol/src/protocol.yml
+++ b/packages/protocol/src/protocol.yml
@@ -1482,7 +1482,7 @@ Page:
expectScreenshot:
parameters:
expected: binary?
- timeout: number?
+ timeout: number
isNot: boolean
locator:
type: object?
@@ -1501,6 +1501,7 @@ Page:
errorMessage: string?
actual: binary?
previous: binary?
+ timedOut: boolean?
log:
type: array?
items: string
@@ -2387,7 +2388,7 @@ Frame:
expectedValue: SerializedArgument?
useInnerText: boolean?
isNot: boolean
- timeout: number?
+ timeout: number
returns:
matches: boolean
received: SerializedValue?
diff --git a/packages/trace-viewer/src/ui/sourceTab.tsx b/packages/trace-viewer/src/ui/sourceTab.tsx
index d130499207..1dd9170f67 100644
--- a/packages/trace-viewer/src/ui/sourceTab.tsx
+++ b/packages/trace-viewer/src/ui/sourceTab.tsx
@@ -111,7 +111,7 @@ export const SourceTab: React.FunctionComponent<{
{location && }
}
-
+