build(deps): bump playwright from 1.49.1 to 1.50.1

This commit is contained in:
2025-02-21 17:22:03 -07:00
parent 79c9869e65
commit dc6d9c68a9
174 changed files with 3064 additions and 1955 deletions

6
node_modules/playwright/README.md generated vendored
View File

@@ -1,6 +1,6 @@
# 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-131.0.6778.33-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-132.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-133.0.6943.16-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-134.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->131.0.6778.33<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->133.0.6943.16<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->132.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->134.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.

View File

@@ -18,4 +18,4 @@ import jsxRuntime from './jsx-runtime.js';
export const jsx = jsxRuntime.jsx;
export const jsxs = jsxRuntime.jsxs;
export const Fragment = jsxRuntime.Fragment;
export const Fragment = jsxRuntime.Fragment;

View File

@@ -49,6 +49,7 @@ class FullConfigInternal {
this.cliFailOnFlakyTests = void 0;
this.cliLastFailed = void 0;
this.testIdMatcher = void 0;
this.lastFailedTestIdMatcher = void 0;
this.defineConfigWasUsed = false;
this.globalSetups = [];
this.globalTeardowns = [];
@@ -90,6 +91,7 @@ class FullConfigInternal {
projects: [],
shard: takeFirst(configCLIOverrides.shard, userConfig.shard, null),
updateSnapshots: takeFirst(configCLIOverrides.updateSnapshots, userConfig.updateSnapshots, 'missing'),
updateSourceMethod: takeFirst(configCLIOverrides.updateSourceMethod, userConfig.updateSourceMethod, 'patch'),
version: require('../../package.json').version,
workers: 0,
webServer: null
@@ -159,8 +161,7 @@ class FullProjectInternal {
this.teardown = void 0;
this.fullConfig = fullConfig;
const testDir = takeFirst(pathResolve(configDir, projectConfig.testDir), pathResolve(configDir, config.testDir), fullConfig.configDir);
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate);
this.project = {
grep: takeFirst(projectConfig.grep, config.grep, defaultGrep),
grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, null),

View File

@@ -206,7 +206,7 @@ function validateConfig(file, config) {
if (!('current' in config.shard) || typeof config.shard.current !== 'number' || config.shard.current < 1 || config.shard.current > config.shard.total) throw (0, _util.errorWithFile)(file, `config.shard.current must be a positive number, not greater than config.shard.total`);
}
if ('updateSnapshots' in config && config.updateSnapshots !== undefined) {
if (typeof config.updateSnapshots !== 'string' || !['all', 'none', 'missing'].includes(config.updateSnapshots)) throw (0, _util.errorWithFile)(file, `config.updateSnapshots must be one of "all", "none" or "missing"`);
if (typeof config.updateSnapshots !== 'string' || !['all', 'changed', 'missing', 'none'].includes(config.updateSnapshots)) throw (0, _util.errorWithFile)(file, `config.updateSnapshots must be one of "all", "changed", "missing" or "none"`);
}
if ('workers' in config && config.workers !== undefined) {
if (typeof config.workers === 'number' && config.workers <= 0) throw (0, _util.errorWithFile)(file, `config.workers must be a positive number`);else if (typeof config.workers === 'string' && !config.workers.endsWith('%')) throw (0, _util.errorWithFile)(file, `config.workers must be a number or percentage`);

View File

@@ -44,7 +44,6 @@ function registerESMLoader() {
} = new MessageChannel();
// register will wait until the loader is initialized.
require('node:module').register(_url.default.pathToFileURL(require.resolve('../transform/esmLoader')), {
parentURL: _url.default.pathToFileURL(__filename),
data: {
port: port2
},

View File

@@ -11,6 +11,7 @@ var _globals = require("./globals");
var _test = require("./test");
var _transform = require("../transform/transform");
var _utils = require("playwright-core/lib/utils");
var _playwrightCore = require("playwright-core");
/**
* Copyright (c) Microsoft Corporation.
*
@@ -56,7 +57,8 @@ class TestTypeImpl {
test.fail.only = (0, _transform.wrapFunctionWithLocation)(this._createTest.bind(this, 'fail.only'));
test.slow = (0, _transform.wrapFunctionWithLocation)(this._modifier.bind(this, 'slow'));
test.setTimeout = (0, _transform.wrapFunctionWithLocation)(this._setTimeout.bind(this));
test.step = this._step.bind(this);
test.step = this._step.bind(this, 'pass');
test.step.skip = this._step.bind(this, 'skip');
test.use = (0, _transform.wrapFunctionWithLocation)(this._use.bind(this));
test.extend = (0, _transform.wrapFunctionWithLocation)(this._extend.bind(this));
test.info = () => {
@@ -222,9 +224,19 @@ class TestTypeImpl {
location
});
}
async _step(title, body, options = {}) {
async _step(expectation, title, body, options = {}) {
const testInfo = (0, _globals.currentTestInfo)();
if (!testInfo) throw new Error(`test.step() can only be called from a test`);
if (expectation === 'skip') {
const step = testInfo._addStep({
category: 'test.step.skip',
title,
location: options.location,
box: options.box
});
step.complete({});
return undefined;
}
const step = testInfo._addStep({
category: 'test.step',
title,
@@ -233,9 +245,21 @@ class TestTypeImpl {
});
return await _utils.zones.run('stepZone', step, async () => {
try {
const result = await body();
let result = undefined;
result = await (0, _utils.raceAgainstDeadline)(async () => {
try {
return await body();
} catch (e) {
var _result;
// If the step timed out, the test fixtures will tear down, which in turn
// will abort unfinished actions in the step body. Record such errors here.
if ((_result = result) !== null && _result !== void 0 && _result.timedOut) testInfo._failWithError(e);
throw e;
}
}, options.timeout ? (0, _utils.monotonicTime)() + options.timeout : 0);
if (result.timedOut) throw new _playwrightCore.errors.TimeoutError(`Step timeout of ${options.timeout}ms exceeded.`);
step.complete({});
return result;
return result.result;
} catch (error) {
step.complete({
error

44
node_modules/playwright/lib/index.js generated vendored
View File

@@ -437,34 +437,43 @@ const playwrightFixtures = {
await artifactsRecorder.willStartTest(testInfo);
const tracingGroupSteps = [];
const csiListener = {
onApiCallBegin: (apiName, params, frames, userData, out) => {
userData.apiName = apiName;
onApiCallBegin: data => {
const testInfo = (0, _globals.currentTestInfo)();
if (!testInfo || apiName.includes('setTestIdAttribute') || apiName === 'tracing.groupEnd') return;
// Some special calls do not get into steps.
if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd') return;
const zone = _utils.zones.zoneData('stepZone');
if (zone && zone.category === 'expect') {
// Display the internal locator._expect call under the name of the enclosing expect call,
// and connect it to the existing expect step.
data.apiName = zone.title;
data.stepId = zone.stepId;
return;
}
// In the general case, create a step for each api call and connect them through the stepId.
const step = testInfo._addStep({
location: frames[0],
location: data.frames[0],
category: 'pw:api',
title: renderApiCall(apiName, params),
apiName,
params
title: renderApiCall(data.apiName, data.params),
apiName: data.apiName,
params: data.params
}, tracingGroupSteps[tracingGroupSteps.length - 1]);
userData.step = step;
out.stepId = step.stepId;
if (apiName === 'tracing.group') tracingGroupSteps.push(step);
data.userData = step;
data.stepId = step.stepId;
if (data.apiName === 'tracing.group') tracingGroupSteps.push(step);
},
onApiCallEnd: (userData, error) => {
onApiCallEnd: data => {
// "tracing.group" step will end later, when "tracing.groupEnd" finishes.
if (userData.apiName === 'tracing.group') return;
if (userData.apiName === 'tracing.groupEnd') {
if (data.apiName === 'tracing.group') return;
if (data.apiName === 'tracing.groupEnd') {
const step = tracingGroupSteps.pop();
step === null || step === void 0 || step.complete({
error
error: data.error
});
return;
}
const step = userData.step;
const step = data.userData;
step === null || step === void 0 || step.complete({
error
error: data.error
});
},
onWillPause: ({
@@ -781,8 +790,9 @@ class ArtifactsRecorder {
async didFinishTest() {
const captureScreenshots = this._shouldCaptureScreenshotUponFinish();
if (captureScreenshots) await this._screenshotOnTestFailure();
const leftoverContexts = [];
let leftoverContexts = [];
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit]) leftoverContexts.push(...browserType._contexts);
leftoverContexts = leftoverContexts.filter(context => !this._reusedContexts.has(context));
const leftoverApiRequests = Array.from(this._playwright.request._contexts);
// Collect traces/screenshots for remaining contexts.

View File

@@ -137,7 +137,7 @@ class TeleReporterReceiver {
const result = test.results.find(r => r._id === resultId);
const parentStep = payload.parentStepId ? result._stepMap.get(payload.parentStepId) : undefined;
const location = this._absoluteLocation(payload.location);
const step = new TeleTestStep(payload, parentStep, location);
const step = new TeleTestStep(payload, parentStep, location, result);
if (parentStep) parentStep.steps.push(step);else result.steps.push(step);
result._stepMap.set(payload.id, step);
(_this$_reporter$onSte = (_this$_reporter5 = this._reporter).onStepBegin) === null || _this$_reporter$onSte === void 0 || _this$_reporter$onSte.call(_this$_reporter5, test, result, step);
@@ -147,6 +147,7 @@ class TeleReporterReceiver {
const test = this._tests.get(testId);
const result = test.results.find(r => r._id === resultId);
const step = result._stepMap.get(payload.id);
step._endPayload = payload;
step.duration = payload.duration;
step.error = payload.error;
(_this$_reporter$onSte2 = (_this$_reporter6 = this._reporter).onStepEnd) === null || _this$_reporter$onSte2 === void 0 || _this$_reporter$onSte2.call(_this$_reporter6, test, result, step);
@@ -360,19 +361,23 @@ class TeleTestCase {
}
exports.TeleTestCase = TeleTestCase;
class TeleTestStep {
constructor(payload, parentStep, location) {
constructor(payload, parentStep, location, result) {
this.title = void 0;
this.category = void 0;
this.location = void 0;
this.parent = void 0;
this.duration = -1;
this.steps = [];
this.error = void 0;
this._result = void 0;
this._endPayload = void 0;
this._startTime = 0;
this.title = payload.title;
this.category = payload.category;
this.location = location;
this.parent = parentStep;
this._startTime = payload.startTime;
this._result = result;
}
titlePath() {
var _this$parent2;
@@ -385,6 +390,10 @@ class TeleTestStep {
set startTime(value) {
this._startTime = +value;
}
get attachments() {
var _this$_endPayload$att, _this$_endPayload;
return (_this$_endPayload$att = (_this$_endPayload = this._endPayload) === null || _this$_endPayload === void 0 || (_this$_endPayload = _this$_endPayload.attachments) === null || _this$_endPayload === void 0 ? void 0 : _this$_endPayload.map(index => this._result.attachments[index])) !== null && _this$_endPayload$att !== void 0 ? _this$_endPayload$att : [];
}
}
class TeleTestResult {
constructor(retry, id) {
@@ -438,6 +447,7 @@ const baseFullConfig = exports.baseFullConfig = {
quiet: false,
shard: null,
updateSnapshots: 'missing',
updateSourceMethod: 'patch',
version: '',
workers: 0,
webServer: null

View File

@@ -52,15 +52,19 @@ class TestTree {
this._treeItemById.set(rootFolder, this.rootItem);
const visitSuite = (project, parentSuite, parentGroup) => {
for (const suite of parentSuite.suites) {
const title = suite.title || '<anonymous>';
let group = parentGroup.children.find(item => item.kind === 'group' && item.title === title);
if (!suite.title) {
// Flatten anonymous describes.
visitSuite(project, suite, parentGroup);
continue;
}
let group = parentGroup.children.find(item => item.kind === 'group' && item.title === suite.title);
if (!group) {
group = {
kind: 'group',
subKind: 'describe',
id: 'suite:' + parentSuite.titlePath().join('\x1e') + '\x1e' + title,
id: 'suite:' + parentSuite.titlePath().join('\x1e') + '\x1e' + suite.title,
// account for anonymous suites
title,
title: suite.title,
location: suite.location,
duration: 0,
parent: parentGroup,

View File

@@ -170,6 +170,7 @@ const customAsyncMatchers = {
toContainText: _matchers.toContainText,
toHaveAccessibleDescription: _matchers.toHaveAccessibleDescription,
toHaveAccessibleName: _matchers.toHaveAccessibleName,
toHaveAccessibleErrorMessage: _matchers.toHaveAccessibleErrorMessage,
toHaveAttribute: _matchers.toHaveAttribute,
toHaveClass: _matchers.toHaveClass,
toHaveCount: _matchers.toHaveCount,
@@ -234,9 +235,10 @@ class ExpectMetaInfoProxyHandler {
// out all the frames that belong to the test runner from caught runtime errors.
const stackFrames = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)());
// Enclose toPass in a step to maintain async stacks, toPass matcher is always async.
// toPass and poll matchers can contain other steps, expects and API calls,
// so they behave like a retriable step.
const stepInfo = {
category: 'expect',
category: matcherName === 'toPass' || this._info.poll ? 'step' : 'expect',
title: (0, _util.trimLongString)(title, 1024),
params: args[0] ? {
expected: args[0]
@@ -248,6 +250,8 @@ class ExpectMetaInfoProxyHandler {
const jestError = (0, _matcherHint.isJestError)(e) ? e : null;
const error = jestError ? new _matcherHint.ExpectError(jestError, customMessage, stackFrames) : e;
if (jestError !== null && jestError !== void 0 && jestError.matcherResult.suggestedRebaseline) {
// NOTE: this is a workaround for the fact that we can't pass the suggested rebaseline
// for passing matchers. See toMatchAriaSnapshot for a counterpart.
step.complete({
suggestedRebaseline: jestError === null || jestError === void 0 ? void 0 : jestError.matcherResult.suggestedRebaseline
});
@@ -263,12 +267,7 @@ class ExpectMetaInfoProxyHandler {
};
try {
const callback = () => matcher.call(target, ...args);
// toPass and poll matchers can contain other steps, expects and API calls,
// so they behave like a retriable step.
const result = matcherName === 'toPass' || this._info.poll ? _utils.zones.run('stepZone', step, callback) : _utils.zones.run('expectZone', {
title,
stepId: step.stepId
}, callback);
const result = _utils.zones.run('stepZone', step, callback);
if (result instanceof Promise) return result.then(finalizer).catch(reportStepError);
finalizer();
return result;

View File

@@ -16,6 +16,7 @@ exports.toBeOK = toBeOK;
exports.toBeVisible = toBeVisible;
exports.toContainText = toContainText;
exports.toHaveAccessibleDescription = toHaveAccessibleDescription;
exports.toHaveAccessibleErrorMessage = toHaveAccessibleErrorMessage;
exports.toHaveAccessibleName = toHaveAccessibleName;
exports.toHaveAttribute = toHaveAttribute;
exports.toHaveCSS = toHaveCSS;
@@ -58,9 +59,8 @@ var _config = require("../common/config");
function toBeAttached(locator, options) {
const attached = !options || options.attached === undefined || options.attached;
const expected = attached ? 'attached' : 'detached';
const unexpected = attached ? 'detached' : 'attached';
const arg = attached ? '' : '{ attached: false }';
return _toBeTruthy.toBeTruthy.call(this, 'toBeAttached', locator, 'Locator', expected, unexpected, arg, async (isNot, timeout) => {
return _toBeTruthy.toBeTruthy.call(this, 'toBeAttached', locator, 'Locator', expected, arg, async (isNot, timeout) => {
return await locator._expect(attached ? 'to.be.attached' : 'to.be.detached', {
isNot,
timeout
@@ -68,19 +68,31 @@ function toBeAttached(locator, options) {
}, options);
}
function toBeChecked(locator, options) {
const checked = !options || options.checked === undefined || options.checked;
const expected = checked ? 'checked' : 'unchecked';
const unexpected = checked ? 'unchecked' : 'checked';
const arg = checked ? '' : '{ checked: false }';
return _toBeTruthy.toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', expected, unexpected, arg, async (isNot, timeout) => {
return await locator._expect(checked ? 'to.be.checked' : 'to.be.unchecked', {
const checked = options === null || options === void 0 ? void 0 : options.checked;
const indeterminate = options === null || options === void 0 ? void 0 : options.indeterminate;
const expectedValue = {
checked,
indeterminate
};
let expected;
let arg;
if (options !== null && options !== void 0 && options.indeterminate) {
expected = 'indeterminate';
arg = `{ indeterminate: true }`;
} else {
expected = (options === null || options === void 0 ? void 0 : options.checked) === false ? 'unchecked' : 'checked';
arg = (options === null || options === void 0 ? void 0 : options.checked) === false ? `{ checked: false }` : '';
}
return _toBeTruthy.toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', expected, arg, async (isNot, timeout) => {
return await locator._expect('to.be.checked', {
isNot,
timeout
timeout,
expectedValue
});
}, options);
}
function toBeDisabled(locator, options) {
return _toBeTruthy.toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', 'disabled', 'enabled', '', async (isNot, timeout) => {
return _toBeTruthy.toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', 'disabled', '', async (isNot, timeout) => {
return await locator._expect('to.be.disabled', {
isNot,
timeout
@@ -90,9 +102,8 @@ function toBeDisabled(locator, options) {
function toBeEditable(locator, options) {
const editable = !options || options.editable === undefined || options.editable;
const expected = editable ? 'editable' : 'readOnly';
const unexpected = editable ? 'readOnly' : 'editable';
const arg = editable ? '' : '{ editable: false }';
return _toBeTruthy.toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', expected, unexpected, arg, async (isNot, timeout) => {
return _toBeTruthy.toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', expected, arg, async (isNot, timeout) => {
return await locator._expect(editable ? 'to.be.editable' : 'to.be.readonly', {
isNot,
timeout
@@ -100,7 +111,7 @@ function toBeEditable(locator, options) {
}, options);
}
function toBeEmpty(locator, options) {
return _toBeTruthy.toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', 'empty', 'notEmpty', '', async (isNot, timeout) => {
return _toBeTruthy.toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', 'empty', '', async (isNot, timeout) => {
return await locator._expect('to.be.empty', {
isNot,
timeout
@@ -110,9 +121,8 @@ function toBeEmpty(locator, options) {
function toBeEnabled(locator, options) {
const enabled = !options || options.enabled === undefined || options.enabled;
const expected = enabled ? 'enabled' : 'disabled';
const unexpected = enabled ? 'disabled' : 'enabled';
const arg = enabled ? '' : '{ enabled: false }';
return _toBeTruthy.toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', expected, unexpected, arg, async (isNot, timeout) => {
return _toBeTruthy.toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', expected, arg, async (isNot, timeout) => {
return await locator._expect(enabled ? 'to.be.enabled' : 'to.be.disabled', {
isNot,
timeout
@@ -120,7 +130,7 @@ function toBeEnabled(locator, options) {
}, options);
}
function toBeFocused(locator, options) {
return _toBeTruthy.toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', 'focused', 'inactive', '', async (isNot, timeout) => {
return _toBeTruthy.toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', 'focused', '', async (isNot, timeout) => {
return await locator._expect('to.be.focused', {
isNot,
timeout
@@ -128,7 +138,7 @@ function toBeFocused(locator, options) {
}, options);
}
function toBeHidden(locator, options) {
return _toBeTruthy.toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', 'hidden', 'visible', '', async (isNot, timeout) => {
return _toBeTruthy.toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', 'hidden', '', async (isNot, timeout) => {
return await locator._expect('to.be.hidden', {
isNot,
timeout
@@ -138,9 +148,8 @@ function toBeHidden(locator, options) {
function toBeVisible(locator, options) {
const visible = !options || options.visible === undefined || options.visible;
const expected = visible ? 'visible' : 'hidden';
const unexpected = visible ? 'hidden' : 'visible';
const arg = visible ? '' : '{ visible: false }';
return _toBeTruthy.toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', expected, unexpected, arg, async (isNot, timeout) => {
return _toBeTruthy.toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', expected, arg, async (isNot, timeout) => {
return await locator._expect(visible ? 'to.be.visible' : 'to.be.hidden', {
isNot,
timeout
@@ -148,7 +157,7 @@ function toBeVisible(locator, options) {
}, options);
}
function toBeInViewport(locator, options) {
return _toBeTruthy.toBeTruthy.call(this, 'toBeInViewport', locator, 'Locator', 'in viewport', 'outside viewport', '', async (isNot, timeout) => {
return _toBeTruthy.toBeTruthy.call(this, 'toBeInViewport', locator, 'Locator', 'in viewport', '', async (isNot, timeout) => {
return await locator._expect('to.be.in.viewport', {
isNot,
expectedNumber: options === null || options === void 0 ? void 0 : options.ratio,
@@ -216,6 +225,19 @@ function toHaveAccessibleName(locator, expected, options) {
});
}, expected, options);
}
function toHaveAccessibleErrorMessage(locator, expected, options) {
return _toMatchText.toMatchText.call(this, 'toHaveAccessibleErrorMessage', locator, 'Locator', async (isNot, timeout) => {
const expectedText = (0, _utils.serializeExpectedTextValues)([expected], {
ignoreCase: options === null || options === void 0 ? void 0 : options.ignoreCase,
normalizeWhiteSpace: true
});
return await locator._expect('to.have.accessible.error.message', {
expectedText: expectedText,
isNot,
timeout
});
}, expected, options);
}
function toHaveAttribute(locator, name, expected, options) {
if (!options) {
// Update params for the case toHaveAttribute(name, options);
@@ -225,7 +247,7 @@ function toHaveAttribute(locator, name, expected, options) {
}
}
if (expected === undefined) {
return _toBeTruthy.toBeTruthy.call(this, 'toHaveAttribute', locator, 'Locator', 'have attribute', 'not have attribute', '', async (isNot, timeout) => {
return _toBeTruthy.toBeTruthy.call(this, 'toHaveAttribute', locator, 'Locator', 'have attribute', '', async (isNot, timeout) => {
return await locator._expect('to.have.attribute', {
expressionArg: name,
isNot,

View File

@@ -22,7 +22,7 @@ var _matcherHint = require("./matcherHint");
* limitations under the License.
*/
async function toBeTruthy(matcherName, receiver, receiverType, expected, unexpected, arg, query, options = {}) {
async function toBeTruthy(matcherName, receiver, receiverType, expected, arg, query, options = {}) {
var _options$timeout;
(0, _util.expectTypes)(receiver, [receiverType], matcherName);
const matcherOptions = {
@@ -45,7 +45,6 @@ async function toBeTruthy(matcherName, receiver, receiverType, expected, unexpec
};
}
const notFound = received === _matcherHint.kNoElementsFoundError ? received : undefined;
const actual = pass ? expected : unexpected;
let printedReceived;
let printedExpected;
if (pass) {
@@ -53,7 +52,7 @@ async function toBeTruthy(matcherName, receiver, receiverType, expected, unexpec
printedReceived = `Received: ${notFound ? _matcherHint.kNoElementsFoundError : expected}`;
} else {
printedExpected = `Expected: ${expected}`;
printedReceived = `Received: ${notFound ? _matcherHint.kNoElementsFoundError : unexpected}`;
printedReceived = `Received: ${notFound ? _matcherHint.kNoElementsFoundError : received}`;
}
const message = () => {
const header = (0, _matcherHint.matcherHint)(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined);
@@ -63,7 +62,7 @@ async function toBeTruthy(matcherName, receiver, receiverType, expected, unexpec
return {
message,
pass,
actual,
actual: received,
name: matcherName,
expected,
log,

View File

@@ -5,12 +5,14 @@ Object.defineProperty(exports, "__esModule", {
});
exports.toMatchAriaSnapshot = toMatchAriaSnapshot;
var _matcherHint = require("./matcherHint");
var _utilsBundle = require("playwright-core/lib/utilsBundle");
var _expectBundle = require("../common/expectBundle");
var _util = require("../util");
var _expect = require("./expect");
var _globals = require("../common/globals");
var _utils = require("playwright-core/lib/utils");
var _fs = _interopRequireDefault(require("fs"));
var _path = _interopRequireDefault(require("path"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Copyright Microsoft Corporation. All rights reserved.
*
@@ -27,8 +29,8 @@ var _utils = require("playwright-core/lib/utils");
* limitations under the License.
*/
async function toMatchAriaSnapshot(receiver, expected, options = {}) {
var _options$timeout;
async function toMatchAriaSnapshot(receiver, expectedParam, options = {}) {
var _testInfo$_projectInt;
const matcherName = 'toMatchAriaSnapshot';
const testInfo = (0, _globals.currentTestInfo)();
if (!testInfo) throw new Error(`toMatchAriaSnapshot() must be called during the test`);
@@ -36,18 +38,41 @@ async function toMatchAriaSnapshot(receiver, expected, options = {}) {
pass: !this.isNot,
message: () => '',
name: 'toMatchAriaSnapshot',
expected
expected: ''
};
const updateSnapshots = testInfo.config.updateSnapshots;
const pathTemplate = (_testInfo$_projectInt = testInfo._projectInternal.expect) === null || _testInfo$_projectInt === void 0 || (_testInfo$_projectInt = _testInfo$_projectInt.toMatchAriaSnapshot) === null || _testInfo$_projectInt === void 0 ? void 0 : _testInfo$_projectInt.pathTemplate;
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}';
const matcherOptions = {
isNot: this.isNot,
promise: this.promise
};
if (typeof expected !== 'string') {
throw new Error([(0, _matcherHint.matcherHint)(this, receiver, matcherName, receiver, expected, matcherOptions), `${_utilsBundle.colors.bold('Matcher error')}: ${(0, _expectBundle.EXPECTED_COLOR)('expected')} value must be a string`, this.utils.printWithType('Expected', expected, this.utils.printExpected)].join('\n\n'));
let expected;
let timeout;
let expectedPath;
if ((0, _utils.isString)(expectedParam)) {
var _options$timeout;
expected = expectedParam;
timeout = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : this.timeout;
} else {
var _expectedParam$timeou;
if (expectedParam !== null && expectedParam !== void 0 && expectedParam.name) {
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [(0, _util.sanitizeFilePathBeforeExtension)(expectedParam.name)]);
} else {
let snapshotNames = testInfo[snapshotNamesSymbol];
if (!snapshotNames) {
snapshotNames = {
anonymousSnapshotIndex: 0
};
testInfo[snapshotNamesSymbol] = snapshotNames;
}
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [(0, _utils.sanitizeForFilePath)((0, _util.trimLongString)(fullTitleWithoutSpec)) + '.yml']);
}
expected = await _fs.default.promises.readFile(expectedPath, 'utf8').catch(() => '');
timeout = (_expectedParam$timeou = expectedParam === null || expectedParam === void 0 ? void 0 : expectedParam.timeout) !== null && _expectedParam$timeou !== void 0 ? _expectedParam$timeou : this.timeout;
}
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`;
@@ -61,7 +86,6 @@ async function toMatchAriaSnapshot(receiver, expected, options = {}) {
expected = `- none "Generating new baseline"`;
}
}
const timeout = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : this.timeout;
expected = unshift(expected);
const {
matches: pass,
@@ -96,15 +120,46 @@ async function toMatchAriaSnapshot(receiver, expected, options = {}) {
return messagePrefix + this.utils.printDiffOrStringify(expected, receivedText, labelExpected, 'Received', false) + (0, _util.callLogText)(log);
}
};
if (!this.isNot && pass === this.isNot && generateNewBaseline) {
// Only rebaseline failed snapshots.
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${(0, _utils.escapeTemplateString)(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
return {
pass: this.isNot,
message: () => '',
name: 'toMatchAriaSnapshot',
suggestedRebaseline
};
if (!this.isNot) {
if (updateSnapshots === 'all' || updateSnapshots === 'changed' && pass === this.isNot || generateMissingBaseline) {
if (expectedPath) {
await _fs.default.promises.mkdir(_path.default.dirname(expectedPath), {
recursive: true
});
await _fs.default.promises.writeFile(expectedPath, typedReceived.regex, 'utf8');
const relativePath = _path.default.relative(process.cwd(), expectedPath);
if (updateSnapshots === 'missing') {
const message = `A snapshot doesn't exist at ${relativePath}, writing actual.`;
testInfo._hasNonRetriableError = true;
testInfo._failWithError(new Error(message));
} else {
const message = `A snapshot is generated at ${relativePath}.`;
/* eslint-disable no-console */
console.log(message);
}
return {
pass: true,
message: () => '',
name: 'toMatchAriaSnapshot'
};
} else {
const suggestedRebaseline = `\`\n${(0, _utils.escapeTemplateString)(indent(typedReceived.regex, '{indent} '))}\n{indent}\``;
if (updateSnapshots === 'missing') {
const message = 'A snapshot is not provided, generating new baseline.';
testInfo._hasNonRetriableError = true;
testInfo._failWithError(new Error(message));
}
// TODO: ideally, we should return "pass: true" here because this matcher passes
// when regenerating baselines. However, we can only access suggestedRebaseline in case
// of an error, so we fail here and workaround it in the expect implementation.
return {
pass: false,
message: () => '',
name: 'toMatchAriaSnapshot',
suggestedRebaseline
};
}
}
}
return {
name: matcherName,
@@ -123,10 +178,10 @@ function unshift(snapshot) {
if (!line.trim()) continue;
const match = line.match(/^(\s*)/);
if (match && match[1].length < whitespacePrefixLength) whitespacePrefixLength = match[1].length;
break;
}
return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n');
}
function indent(snapshot, indent) {
return snapshot.split('\n').map(line => indent + line).join('\n');
}
}
const snapshotNamesSymbol = Symbol('snapshotNames');

View File

@@ -102,7 +102,8 @@ class SnapshotHelper {
outputBasePath = testInfo._getOutputPath(sanitizedName);
this.attachmentBaseName = sanitizedName;
}
this.expectedPath = testInfo.snapshotPath(...expectedPathSegments);
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
this.expectedPath = testInfo._resolveSnapshotPath(configOptions.pathTemplate, defaultTemplate, expectedPathSegments);
this.legacyExpectedPath = (0, _util.addSuffixToFilePath)(outputBasePath, '-expected');
this.previousPath = (0, _util.addSuffixToFilePath)(outputBasePath, '-previous');
this.actualPath = (0, _util.addSuffixToFilePath)(outputBasePath, '-actual');
@@ -144,7 +145,7 @@ class SnapshotHelper {
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined));
}
handleMissingNegated() {
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
const isWriteMissingMode = this.updateSnapshots !== 'none';
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`;
// NOTE: 'isNot' matcher implies inversed value.
return this.createMatcherResult(message, true);
@@ -159,7 +160,7 @@ class SnapshotHelper {
return this.createMatcherResult(message, true);
}
handleMissing(actual) {
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
const isWriteMissingMode = this.updateSnapshots !== 'none';
if (isWriteMissingMode) writeFileSync(this.expectedPath, actual);
this.testInfo.attachments.push({
name: (0, _util.addSuffixToFilePath)(this.attachmentBaseName, '-expected'),
@@ -173,7 +174,7 @@ class SnapshotHelper {
path: this.actualPath
});
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', writing actual.' : '.'}`;
if (this.updateSnapshots === 'all') {
if (this.updateSnapshots === 'all' || this.updateSnapshots === 'changed') {
/* eslint-disable no-console */
console.log(message);
return this.createMatcherResult(message, true);
@@ -252,14 +253,23 @@ function toMatchSnapshot(received, nameOrOptions = {}, optOptions = {}) {
}
if (!_fs.default.existsSync(helper.expectedPath)) return helper.handleMissing(received);
const expected = _fs.default.readFileSync(helper.expectedPath);
const result = helper.comparator(received, expected, helper.options);
if (!result) return helper.handleMatching();
if (helper.updateSnapshots === 'all') {
if (!(0, _utils.compareBuffersOrStrings)(received, expected)) return helper.handleMatching();
writeFileSync(helper.expectedPath, received);
/* eslint-disable no-console */
console.log(helper.expectedPath + ' is not the same, writing actual.');
return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
}
if (helper.updateSnapshots === 'changed') {
const result = helper.comparator(received, expected, helper.options);
if (!result) return helper.handleMatching();
writeFileSync(helper.expectedPath, received);
/* eslint-disable no-console */
console.log(helper.expectedPath + ' does not match, writing actual.');
return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
}
const result = helper.comparator(received, expected, helper.options);
if (!result) return helper.handleMatching();
const receiver = (0, _utils.isString)(received) ? 'string' : 'Buffer';
const header = (0, _matcherHint.matcherHint)(this, undefined, 'toMatchSnapshot', receiver, undefined, undefined);
return helper.handleDifferent(received, expected, undefined, result.diff, header, result.errorMessage, undefined);
@@ -344,8 +354,8 @@ async function toHaveScreenshot(pageOrLocator, nameOrOptions = {}, optOptions =
// General case:
// - snapshot exists
// - regular matcher (i.e. not a `.not`)
// - perhaps an 'all' flag to update non-matching screenshots
expectScreenshotOptions.expected = await _fs.default.promises.readFile(helper.expectedPath);
const expected = await _fs.default.promises.readFile(helper.expectedPath);
expectScreenshotOptions.expected = helper.updateSnapshots === 'all' ? undefined : expected;
const {
actual,
previous,
@@ -354,14 +364,22 @@ async function toHaveScreenshot(pageOrLocator, nameOrOptions = {}, optOptions =
log,
timedOut
} = await page._expectScreenshot(expectScreenshotOptions);
if (!errorMessage) return helper.handleMatching();
if (helper.updateSnapshots === 'all') {
const writeFiles = () => {
writeFileSync(helper.expectedPath, actual);
writeFileSync(helper.actualPath, actual);
/* eslint-disable no-console */
console.log(helper.expectedPath + ' is re-generated, writing actual.');
return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
};
if (!errorMessage) {
// Screenshot is matching, but is not necessarily the same as the expected.
if (helper.updateSnapshots === 'all' && actual && (0, _utils.compareBuffersOrStrings)(actual, expected)) {
console.log(helper.expectedPath + ' is re-generated, writing actual.');
return writeFiles();
}
return helper.handleMatching();
}
if (helper.updateSnapshots === 'changed' || helper.updateSnapshots === 'all') return writeFiles();
const header = (0, _matcherHint.matcherHint)(this, undefined, 'toHaveScreenshot', receiver, undefined, undefined, timedOut ? timeout : undefined);
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log);
}

View File

@@ -78,7 +78,7 @@ class WebServerPlugin {
debugWebServer(`Starting WebServer process ${this._options.command}...`);
const {
launchedProcess,
kill
gracefullyClose
} = await (0, _utils.launchProcess)({
command: this._options.command,
env: {
@@ -89,14 +89,30 @@ class WebServerPlugin {
cwd: this._options.cwd,
stdio: 'stdin',
shell: true,
// Reject to indicate that we cannot close the web server gracefully
// and should fallback to non-graceful shutdown.
attemptToGracefullyClose: () => Promise.reject(),
attemptToGracefullyClose: async () => {
if (process.platform === 'win32') throw new Error('Graceful shutdown is not supported on Windows');
if (!this._options.gracefulShutdown) throw new Error('skip graceful shutdown');
const {
signal,
timeout = 0
} = this._options.gracefulShutdown;
// proper usage of SIGINT is to send it to the entire process group, see https://www.cons.org/cracauer/sigint.html
// there's no such convention for SIGTERM, so we decide what we want. signaling the process group for consistency.
process.kill(-launchedProcess.pid, signal);
return new Promise((resolve, reject) => {
const timer = timeout !== 0 ? setTimeout(() => reject(new Error(`process didn't close gracefully within timeout`)), timeout) : undefined;
launchedProcess.once('close', (...args) => {
clearTimeout(timer);
resolve();
});
});
},
log: () => {},
onExit: code => processExitedReject(new Error(code ? `Process from config.webServer was not able to start. Exit code: ${code}` : 'Process from config.webServer exited early.')),
tempDirectories: []
});
this._killProcess = kill;
this._killProcess = gracefullyClose;
debugWebServer(`Process started`);
launchedProcess.stderr.on('data', data => {
var _onStdErr, _ref;

View File

@@ -268,6 +268,8 @@ async function mergeReports(reportDir, opts) {
}
function overridesFromOptions(options) {
const shardPair = options.shard ? options.shard.split('/').map(t => parseInt(t, 10)) : undefined;
let updateSnapshots;
if (['all', 'changed', 'missing', 'none'].includes(options.updateSnapshots)) updateSnapshots = options.updateSnapshots;else updateSnapshots = 'updateSnapshots' in options ? 'changed' : undefined;
const overrides = {
forbidOnly: options.forbidOnly ? true : undefined,
fullyParallel: options.fullyParallel ? true : undefined,
@@ -285,7 +287,8 @@ function overridesFromOptions(options) {
timeout: options.timeout ? parseInt(options.timeout, 10) : undefined,
tsconfig: options.tsconfig ? _path.default.resolve(process.cwd(), options.tsconfig) : undefined,
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined,
updateSnapshots: options.updateSnapshots ? 'all' : undefined,
updateSnapshots,
updateSourceMethod: options.updateSourceMethod,
workers: options.workers
};
if (options.browser) {
@@ -328,7 +331,10 @@ function resolveReporter(id) {
});
}
const kTraceModes = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure', 'retain-on-first-failure'];
const testOptions = [['--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`], ['-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`], ['--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options`], ['--fail-on-flaky-tests', `Fail if any test is flagged as flaky (default: false)`], ['--forbid-only', `Fail if test.only is called (default: false)`], ['--fully-parallel', `Run all tests in parallel (default: false)`], ['--global-timeout <timeout>', `Maximum time this test suite can run in milliseconds (default: unlimited)`], ['-g, --grep <grep>', `Only run tests matching this regular expression (default: ".*")`], ['-gv, --grep-invert <grep>', `Only run tests that do not match this regular expression`], ['--headed', `Run tests in headed browsers (default: headless)`], ['--ignore-snapshots', `Ignore screenshot and snapshot expectations`], ['--last-failed', `Only re-run the failures`], ['--list', `Collect all the tests and report them, but do not run`], ['--max-failures <N>', `Stop after the first N failures`], ['--no-deps', 'Do not run project dependencies'], ['--output <dir>', `Folder for output artifacts (default: "test-results")`], ['--only-changed [ref]', `Only run test files that have been changed between 'HEAD' and 'ref'. Defaults to running all uncommitted changes. Only supports Git.`], ['--pass-with-no-tests', `Makes test run succeed even if no tests were found`], ['--project <project-name...>', `Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects)`], ['--quiet', `Suppress stdio`], ['--repeat-each <N>', `Run each test N times (default: 1)`], ['--reporter <reporter>', `Reporter to use, comma-separated, can be ${_config.builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${_config.defaultReporter}")`], ['--retries <retries>', `Maximum retry count for flaky tests, zero for no retries (default: no retries)`], ['--shard <shard>', `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"`], ['--timeout <timeout>', `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${_config.defaultTimeout})`], ['--trace <mode>', `Force tracing mode, can be ${kTraceModes.map(mode => `"${mode}"`).join(', ')}`], ['--tsconfig <path>', `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)`], ['--ui', `Run tests in interactive UI mode`], ['--ui-host <host>', 'Host to serve UI on; specifying this option opens UI in a browser tab'], ['--ui-port <port>', 'Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab'], ['-u, --update-snapshots', `Update snapshots with actual results (default: only create missing snapshots)`], ['-j, --workers <workers>', `Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%)`], ['-x', `Stop after the first failure`]];
// Note: update docs/src/test-cli-js.md when you update this, program is the source of truth.
const testOptions = [/* deprecated */['--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`], ['-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`], ['--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options`], ['--fail-on-flaky-tests', `Fail if any test is flagged as flaky (default: false)`], ['--forbid-only', `Fail if test.only is called (default: false)`], ['--fully-parallel', `Run all tests in parallel (default: false)`], ['--global-timeout <timeout>', `Maximum time this test suite can run in milliseconds (default: unlimited)`], ['-g, --grep <grep>', `Only run tests matching this regular expression (default: ".*")`], ['-gv, --grep-invert <grep>', `Only run tests that do not match this regular expression`], ['--headed', `Run tests in headed browsers (default: headless)`], ['--ignore-snapshots', `Ignore screenshot and snapshot expectations`], ['--last-failed', `Only re-run the failures`], ['--list', `Collect all the tests and report them, but do not run`], ['--max-failures <N>', `Stop after the first N failures`], ['--no-deps', 'Do not run project dependencies'], ['--output <dir>', `Folder for output artifacts (default: "test-results")`], ['--only-changed [ref]', `Only run test files that have been changed between 'HEAD' and 'ref'. Defaults to running all uncommitted changes. Only supports Git.`], ['--pass-with-no-tests', `Makes test run succeed even if no tests were found`], ['--project <project-name...>', `Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects)`], ['--quiet', `Suppress stdio`], ['--repeat-each <N>', `Run each test N times (default: 1)`], ['--reporter <reporter>', `Reporter to use, comma-separated, can be ${_config.builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${_config.defaultReporter}")`], ['--retries <retries>', `Maximum retry count for flaky tests, zero for no retries (default: no retries)`], ['--shard <shard>', `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"`], ['--timeout <timeout>', `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${_config.defaultTimeout})`], ['--trace <mode>', `Force tracing mode, can be ${kTraceModes.map(mode => `"${mode}"`).join(', ')}`], ['--tsconfig <path>', `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)`], ['--ui', `Run tests in interactive UI mode`], ['--ui-host <host>', 'Host to serve UI on; specifying this option opens UI in a browser tab'], ['--ui-port <port>', 'Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab'], ['-u, --update-snapshots [mode]', `Update snapshots with actual results. Possible values are 'all', 'changed', 'missing' and 'none'. Not passing defaults to 'missing', passing without value defaults to 'changed'`], ['--update-source-method <method>', `Chooses the way source is updated. Possible values are 'overwrite', '3way' and 'patch'. Defaults to 'patch'`], ['-j, --workers <workers>', `Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%)`], ['-x', `Stop after the first failure`]];
addTestCommand(_program.program);
addShowReportCommand(_program.program);
addListFilesCommand(_program.program);

View File

@@ -3,22 +3,20 @@
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.colors = exports.BaseReporter = void 0;
exports.TerminalReporter = void 0;
exports.fitToWidth = fitToWidth;
exports.formatError = formatError;
exports.formatFailure = formatFailure;
exports.formatResultFailure = formatResultFailure;
exports.formatRetry = formatRetry;
exports.formatTestHeader = formatTestHeader;
exports.formatTestTitle = formatTestTitle;
exports.kOutputSymbol = exports.isTTY = void 0;
exports.nonTerminalScreen = exports.noColors = exports.kOutputSymbol = exports.internalScreen = void 0;
exports.prepareErrorStack = prepareErrorStack;
exports.relativeFilePath = relativeFilePath;
exports.resolveOutputFile = resolveOutputFile;
exports.separator = separator;
exports.stepSuffix = stepSuffix;
exports.stripAnsiEscapes = stripAnsiEscapes;
exports.ttyWidth = void 0;
exports.terminalScreen = void 0;
var _utilsBundle = require("playwright-core/lib/utilsBundle");
var _path = _interopRequireDefault(require("path"));
var _utils = require("playwright-core/lib/utils");
@@ -42,11 +40,49 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
*/
const kOutputSymbol = exports.kOutputSymbol = Symbol('output');
const {
isTTY,
ttyWidth,
colors
} = (() => {
const noColors = exports.noColors = {
bold: t => t,
cyan: t => t,
dim: t => t,
gray: t => t,
green: t => t,
red: t => t,
yellow: t => t,
black: t => t,
blue: t => t,
magenta: t => t,
white: t => t,
grey: t => t,
bgBlack: t => t,
bgRed: t => t,
bgGreen: t => t,
bgYellow: t => t,
bgBlue: t => t,
bgMagenta: t => t,
bgCyan: t => t,
bgWhite: t => t,
strip: t => t,
stripColors: t => t,
reset: t => t,
italic: t => t,
underline: t => t,
inverse: t => t,
hidden: t => t,
strikethrough: t => t,
rainbow: t => t,
zebra: t => t,
america: t => t,
trap: t => t,
random: t => t,
zalgo: t => t,
enabled: false,
enable: () => {},
disable: () => {},
setTheme: () => {}
};
// Output goes to terminal.
const terminalScreen = exports.terminalScreen = (() => {
let isTTY = !!process.stdout.isTTY;
let ttyWidth = process.stdout.columns || 0;
if (process.env.PLAYWRIGHT_FORCE_TTY === 'false' || process.env.PLAYWRIGHT_FORCE_TTY === '0') {
@@ -62,27 +98,33 @@ const {
}
let useColors = isTTY;
if (process.env.DEBUG_COLORS === '0' || process.env.DEBUG_COLORS === 'false' || process.env.FORCE_COLOR === '0' || process.env.FORCE_COLOR === 'false') useColors = false;else if (process.env.DEBUG_COLORS || process.env.FORCE_COLOR) useColors = true;
const colors = useColors ? _utilsBundle.colors : {
bold: t => t,
cyan: t => t,
dim: t => t,
gray: t => t,
green: t => t,
red: t => t,
yellow: t => t,
enabled: false
};
const colors = useColors ? _utilsBundle.colors : noColors;
return {
resolveFiles: 'cwd',
isTTY,
ttyWidth,
colors
};
})();
exports.colors = colors;
exports.ttyWidth = ttyWidth;
exports.isTTY = isTTY;
class BaseReporter {
// Output does not go to terminal, but colors are controlled with terminal env vars.
const nonTerminalScreen = exports.nonTerminalScreen = {
colors: terminalScreen.colors,
isTTY: false,
ttyWidth: 0,
resolveFiles: 'rootDir'
};
// Internal output for post-processing, should always contain real colors.
const internalScreen = exports.internalScreen = {
colors: _utilsBundle.colors,
isTTY: false,
ttyWidth: 0,
resolveFiles: 'rootDir'
};
class TerminalReporter {
constructor(options = {}) {
this.screen = terminalScreen;
this.config = void 0;
this.suite = void 0;
this.totalTestCount = 0;
@@ -123,7 +165,7 @@ class BaseReporter {
onTestEnd(test, result) {
if (result.status !== 'skipped' && result.status !== test.expectedStatus) ++this._failureCount;
const projectName = test.titlePath()[1];
const relativePath = relativeTestPath(this.config, test);
const relativePath = relativeTestPath(this.screen, this.config, test);
const fileAndProject = (projectName ? `[${projectName}] ` : '') + relativePath;
const entry = this.fileDurations.get(fileAndProject) || {
duration: 0,
@@ -140,18 +182,18 @@ class BaseReporter {
this.result = result;
}
fitToScreen(line, prefix) {
if (!ttyWidth) {
if (!this.screen.ttyWidth) {
// Guard against the case where we cannot determine available width.
return line;
}
return fitToWidth(line, ttyWidth, prefix);
return fitToWidth(line, this.screen.ttyWidth, prefix);
}
generateStartingMessage() {
var _this$config$metadata;
const jobs = (_this$config$metadata = this.config.metadata.actualWorkers) !== null && _this$config$metadata !== void 0 ? _this$config$metadata : this.config.workers;
const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : '';
if (!this.totalTestCount) return '';
return '\n' + colors.dim('Running ') + this.totalTestCount + colors.dim(` test${this.totalTestCount !== 1 ? 's' : ''} using `) + jobs + colors.dim(` worker${jobs !== 1 ? 's' : ''}${shardDetails}`);
return '\n' + this.screen.colors.dim('Running ') + this.totalTestCount + this.screen.colors.dim(` test${this.totalTestCount !== 1 ? 's' : ''} using `) + jobs + this.screen.colors.dim(` worker${jobs !== 1 ? 's' : ''}${shardDetails}`);
}
getSlowTests() {
if (!this.config.reportSlowTests) return [];
@@ -173,27 +215,27 @@ class BaseReporter {
}) {
const tokens = [];
if (unexpected.length) {
tokens.push(colors.red(` ${unexpected.length} failed`));
for (const test of unexpected) tokens.push(colors.red(formatTestHeader(this.config, test, {
tokens.push(this.screen.colors.red(` ${unexpected.length} failed`));
for (const test of unexpected) tokens.push(this.screen.colors.red(this.formatTestHeader(test, {
indent: ' '
})));
}
if (interrupted.length) {
tokens.push(colors.yellow(` ${interrupted.length} interrupted`));
for (const test of interrupted) tokens.push(colors.yellow(formatTestHeader(this.config, test, {
tokens.push(this.screen.colors.yellow(` ${interrupted.length} interrupted`));
for (const test of interrupted) tokens.push(this.screen.colors.yellow(this.formatTestHeader(test, {
indent: ' '
})));
}
if (flaky.length) {
tokens.push(colors.yellow(` ${flaky.length} flaky`));
for (const test of flaky) tokens.push(colors.yellow(formatTestHeader(this.config, test, {
tokens.push(this.screen.colors.yellow(` ${flaky.length} flaky`));
for (const test of flaky) tokens.push(this.screen.colors.yellow(this.formatTestHeader(test, {
indent: ' '
})));
}
if (skipped) tokens.push(colors.yellow(` ${skipped} skipped`));
if (didNotRun) tokens.push(colors.yellow(` ${didNotRun} did not run`));
if (expected) tokens.push(colors.green(` ${expected} passed`) + colors.dim(` (${(0, _utilsBundle.ms)(this.result.duration)})`));
if (fatalErrors.length && expected + unexpected.length + interrupted.length + flaky.length > 0) tokens.push(colors.red(` ${fatalErrors.length === 1 ? '1 error was not a part of any test' : fatalErrors.length + ' errors were not a part of any test'}, see above for details`));
if (skipped) tokens.push(this.screen.colors.yellow(` ${skipped} skipped`));
if (didNotRun) tokens.push(this.screen.colors.yellow(` ${didNotRun} did not run`));
if (expected) tokens.push(this.screen.colors.green(` ${expected} passed`) + this.screen.colors.dim(` (${(0, _utilsBundle.ms)(this.result.duration)})`));
if (fatalErrors.length && expected + unexpected.length + interrupted.length + flaky.length > 0) tokens.push(this.screen.colors.red(` ${fatalErrors.length === 1 ? '1 error was not a part of any test' : fatalErrors.length + ' errors were not a part of any test'}, see above for details`));
return tokens.join('\n');
}
generateSummary() {
@@ -251,15 +293,15 @@ class BaseReporter {
_printFailures(failures) {
console.log('');
failures.forEach((test, index) => {
console.log(formatFailure(this.config, test, index + 1));
console.log(this.formatFailure(test, index + 1));
});
}
_printSlowTests() {
const slowTests = this.getSlowTests();
slowTests.forEach(([file, duration]) => {
console.log(colors.yellow(' Slow test file: ') + file + colors.yellow(` (${(0, _utilsBundle.ms)(duration)})`));
console.log(this.screen.colors.yellow(' Slow test file: ') + file + this.screen.colors.yellow(` (${(0, _utilsBundle.ms)(duration)})`));
});
if (slowTests.length) console.log(colors.yellow(' Consider splitting slow test files to speed up parallel execution'));
if (slowTests.length) console.log(this.screen.colors.yellow(' Consider running tests from slow files in parallel, see https://playwright.dev/docs/test-parallel.'));
}
_printSummary(summary) {
if (summary.trim()) console.log(summary);
@@ -267,24 +309,36 @@ class BaseReporter {
willRetry(test) {
return test.outcome() === 'unexpected' && test.results.length <= test.retries;
}
formatTestTitle(test, step, omitLocation = false) {
return formatTestTitle(this.screen, this.config, test, step, omitLocation);
}
formatTestHeader(test, options = {}) {
return formatTestHeader(this.screen, this.config, test, options);
}
formatFailure(test, index) {
return formatFailure(this.screen, this.config, test, index);
}
formatError(error) {
return formatError(this.screen, error);
}
}
exports.BaseReporter = BaseReporter;
function formatFailure(config, test, index) {
exports.TerminalReporter = TerminalReporter;
function formatFailure(screen, config, test, index) {
const lines = [];
const header = formatTestHeader(config, test, {
const header = formatTestHeader(screen, config, test, {
indent: ' ',
index,
mode: 'error'
});
lines.push(colors.red(header));
lines.push(screen.colors.red(header));
for (const result of test.results) {
const resultLines = [];
const errors = formatResultFailure(test, result, ' ', colors.enabled);
const errors = formatResultFailure(screen, test, result, ' ');
if (!errors.length) continue;
const retryLines = [];
if (result.retry) {
retryLines.push('');
retryLines.push(colors.gray(separator(` Retry #${result.retry}`)));
retryLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`)));
}
resultLines.push(...retryLines);
resultLines.push(...errors.map(error => '\n' + error.message));
@@ -293,37 +347,37 @@ function formatFailure(config, test, index) {
const hasPrintableContent = attachment.contentType.startsWith('text/');
if (!attachment.path && !hasPrintableContent) continue;
resultLines.push('');
resultLines.push(colors.cyan(separator(` attachment #${i + 1}: ${attachment.name} (${attachment.contentType})`)));
resultLines.push(screen.colors.cyan(separator(screen, ` attachment #${i + 1}: ${attachment.name} (${attachment.contentType})`)));
if (attachment.path) {
const relativePath = _path.default.relative(process.cwd(), attachment.path);
resultLines.push(colors.cyan(` ${relativePath}`));
resultLines.push(screen.colors.cyan(` ${relativePath}`));
// Make this extensible
if (attachment.name === 'trace') {
const packageManagerCommand = (0, _utils.getPackageManagerExecCommand)();
resultLines.push(colors.cyan(` Usage:`));
resultLines.push(screen.colors.cyan(` Usage:`));
resultLines.push('');
resultLines.push(colors.cyan(` ${packageManagerCommand} playwright show-trace ${quotePathIfNeeded(relativePath)}`));
resultLines.push(screen.colors.cyan(` ${packageManagerCommand} playwright show-trace ${quotePathIfNeeded(relativePath)}`));
resultLines.push('');
}
} else {
if (attachment.contentType.startsWith('text/') && attachment.body) {
let text = attachment.body.toString();
if (text.length > 300) text = text.slice(0, 300) + '...';
for (const line of text.split('\n')) resultLines.push(colors.cyan(` ${line}`));
for (const line of text.split('\n')) resultLines.push(screen.colors.cyan(` ${line}`));
}
}
resultLines.push(colors.cyan(separator(' ')));
resultLines.push(screen.colors.cyan(separator(screen, ' ')));
}
lines.push(...resultLines);
}
lines.push('');
return lines.join('\n');
}
function formatRetry(result) {
function formatRetry(screen, result) {
const retryLines = [];
if (result.retry) {
retryLines.push('');
retryLines.push(colors.gray(separator(` Retry #${result.retry}`)));
retryLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`)));
}
return retryLines;
}
@@ -331,20 +385,20 @@ function quotePathIfNeeded(path) {
if (/\s/.test(path)) return `"${path}"`;
return path;
}
function formatResultFailure(test, result, initialIndent, highlightCode) {
function formatResultFailure(screen, test, result, initialIndent) {
const errorDetails = [];
if (result.status === 'passed' && test.expectedStatus === 'failed') {
errorDetails.push({
message: indent(colors.red(`Expected to fail, but passed.`), initialIndent)
message: indent(screen.colors.red(`Expected to fail, but passed.`), initialIndent)
});
}
if (result.status === 'interrupted') {
errorDetails.push({
message: indent(colors.red(`Test was interrupted.`), initialIndent)
message: indent(screen.colors.red(`Test was interrupted.`), initialIndent)
});
}
for (const error of result.errors) {
const formattedError = formatError(error, highlightCode);
const formattedError = formatError(screen, error);
errorDetails.push({
message: indent(formattedError.message, initialIndent),
location: formattedError.location
@@ -352,29 +406,29 @@ function formatResultFailure(test, result, initialIndent, highlightCode) {
}
return errorDetails;
}
function relativeFilePath(config, file) {
return _path.default.relative(config.rootDir, file) || _path.default.basename(file);
function relativeFilePath(screen, config, file) {
if (screen.resolveFiles === 'cwd') return _path.default.relative(process.cwd(), file);
return _path.default.relative(config.rootDir, file);
}
function relativeTestPath(config, test) {
return relativeFilePath(config, test.location.file);
function relativeTestPath(screen, config, test) {
return relativeFilePath(screen, config, test.location.file);
}
function stepSuffix(step) {
const stepTitles = step ? step.titlePath() : [];
return stepTitles.map(t => t.split('\n')[0]).map(t => ' ' + t).join('');
}
function formatTestTitle(config, test, step, omitLocation = false) {
var _step$location$line, _step$location, _step$location$column, _step$location2;
function formatTestTitle(screen, config, test, step, omitLocation = false) {
// root, project, file, ...describes, test
const [, projectName,, ...titles] = test.titlePath();
let location;
if (omitLocation) location = `${relativeTestPath(config, test)}`;else location = `${relativeTestPath(config, test)}:${(_step$location$line = step === null || step === void 0 || (_step$location = step.location) === null || _step$location === void 0 ? void 0 : _step$location.line) !== null && _step$location$line !== void 0 ? _step$location$line : test.location.line}:${(_step$location$column = step === null || step === void 0 || (_step$location2 = step.location) === null || _step$location2 === void 0 ? void 0 : _step$location2.column) !== null && _step$location$column !== void 0 ? _step$location$column : test.location.column}`;
if (omitLocation) location = `${relativeTestPath(screen, config, test)}`;else location = `${relativeTestPath(screen, config, test)}:${test.location.line}:${test.location.column}`;
const projectTitle = projectName ? `[${projectName}] ` : '';
const testTitle = `${projectTitle}${location} ${titles.join(' ')}`;
const extraTags = test.tags.filter(t => !testTitle.includes(t));
return `${testTitle}${stepSuffix(step)}${extraTags.length ? ' ' + extraTags.join(' ') : ''}`;
}
function formatTestHeader(config, test, options = {}) {
const title = formatTestTitle(config, test);
function formatTestHeader(screen, config, test, options = {}) {
const title = formatTestTitle(screen, config, test);
const header = `${options.indent || ''}${options.index ? options.index + ') ' : ''}${title}`;
let fullHeader = header;
@@ -396,9 +450,9 @@ function formatTestHeader(config, test, options = {}) {
}
fullHeader = header + (stepPaths.size === 1 ? stepPaths.values().next().value : '');
}
return separator(fullHeader);
return separator(screen, fullHeader);
}
function formatError(error, highlightCode) {
function formatError(screen, error) {
const message = error.message || error.value || '';
const stack = error.stack;
if (!stack && !error.location) return {
@@ -412,23 +466,23 @@ function formatError(error, highlightCode) {
tokens.push((parsedStack === null || parsedStack === void 0 ? void 0 : parsedStack.message) || message);
if (error.snippet) {
let snippet = error.snippet;
if (!highlightCode) snippet = stripAnsiEscapes(snippet);
if (!screen.colors.enabled) snippet = stripAnsiEscapes(snippet);
tokens.push('');
tokens.push(snippet);
}
if (parsedStack && parsedStack.stackLines.length) tokens.push(colors.dim(parsedStack.stackLines.join('\n')));
if (parsedStack && parsedStack.stackLines.length) tokens.push(screen.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);
if (error.cause) tokens.push(screen.colors.dim('[cause]: ') + formatError(screen, error.cause).message);
return {
location,
message: tokens.join('\n')
};
}
function separator(text = '') {
function separator(screen, text = '') {
if (text) text += ' ';
const columns = Math.min(100, ttyWidth || 100);
return text + colors.dim('─'.repeat(Math.max(0, columns - text.length)));
const columns = Math.min(100, screen.ttyWidth || 100);
return text + screen.colors.dim('─'.repeat(Math.max(0, columns - text.length)));
}
function indent(lines, tab) {
return lines.replace(/^(?=.+$)/gm, tab);

View File

@@ -21,7 +21,7 @@ var _base = require("./base");
* limitations under the License.
*/
class DotReporter extends _base.BaseReporter {
class DotReporter extends _base.TerminalReporter {
constructor(...args) {
super(...args);
this._counter = 0;
@@ -46,28 +46,28 @@ class DotReporter extends _base.BaseReporter {
}
++this._counter;
if (result.status === 'skipped') {
process.stdout.write(_base.colors.yellow('°'));
process.stdout.write(this.screen.colors.yellow('°'));
return;
}
if (this.willRetry(test)) {
process.stdout.write(_base.colors.gray('×'));
process.stdout.write(this.screen.colors.gray('×'));
return;
}
switch (test.outcome()) {
case 'expected':
process.stdout.write(_base.colors.green('·'));
process.stdout.write(this.screen.colors.green('·'));
break;
case 'unexpected':
process.stdout.write(_base.colors.red(result.status === 'timedOut' ? 'T' : 'F'));
process.stdout.write(this.screen.colors.red(result.status === 'timedOut' ? 'T' : 'F'));
break;
case 'flaky':
process.stdout.write(_base.colors.yellow('±'));
process.stdout.write(this.screen.colors.yellow('±'));
break;
}
}
onError(error) {
super.onError(error);
console.log('\n' + (0, _base.formatError)(error, _base.colors.enabled).message);
console.log('\n' + this.formatError(error).message);
this._counter = 0;
}
async onEnd(result) {

View File

@@ -43,10 +43,14 @@ class GitHubLogger {
this._log(message, 'warning', options);
}
}
class GitHubReporter extends _base.BaseReporter {
constructor(...args) {
super(...args);
class GitHubReporter extends _base.TerminalReporter {
constructor(options = {}) {
super(options);
this.githubLogger = new GitHubLogger();
this.screen = {
...this.screen,
colors: _base.noColors
};
}
printsToStdio() {
return false;
@@ -56,7 +60,7 @@ class GitHubReporter extends _base.BaseReporter {
this._printAnnotations();
}
onError(error) {
const errorMessage = (0, _base.formatError)(error, false).message;
const errorMessage = this.formatError(error).message;
this.githubLogger.error(errorMessage);
}
_printAnnotations() {
@@ -82,14 +86,14 @@ class GitHubReporter extends _base.BaseReporter {
}
_printFailureAnnotations(failures) {
failures.forEach((test, index) => {
const title = (0, _base.formatTestTitle)(this.config, test);
const header = (0, _base.formatTestHeader)(this.config, test, {
const title = this.formatTestTitle(test);
const header = this.formatTestHeader(test, {
indent: ' ',
index: index + 1,
mode: 'error'
});
for (const result of test.results) {
const errors = (0, _base.formatResultFailure)(test, result, ' ', _base.colors.enabled);
const errors = (0, _base.formatResultFailure)(this.screen, test, result, ' ');
for (const error of errors) {
var _error$location;
const options = {
@@ -100,7 +104,7 @@ class GitHubReporter extends _base.BaseReporter {
options.line = error.location.line;
options.col = error.location.column;
}
const message = [header, ...(0, _base.formatRetry)(result), error.message].join('\n');
const message = [header, ...(0, _base.formatRetry)(this.screen, result), error.message].join('\n');
this.githubLogger.error(message, options);
}
}

View File

@@ -78,10 +78,10 @@ class HtmlReporter {
const key = outputFolder + '|' + project.outputDir;
if (reportedWarnings.has(key)) continue;
reportedWarnings.add(key);
console.log(_base.colors.red(`Configuration Error: HTML reporter output folder clashes with the tests output folder:`));
console.log(_utilsBundle.colors.red(`Configuration Error: HTML reporter output folder clashes with the tests output folder:`));
console.log(`
html reporter folder: ${_base.colors.bold(outputFolder)}
test results folder: ${_base.colors.bold(project.outputDir)}`);
html reporter folder: ${_utilsBundle.colors.bold(outputFolder)}
test results folder: ${_utilsBundle.colors.bold(project.outputDir)}`);
console.log('');
console.log(`HTML reporter will clear its output directory prior to being generated, which will lead to the artifact loss.
`);
@@ -129,7 +129,7 @@ class HtmlReporter {
const portArg = this._port ? ` --port ${this._port}` : '';
console.log('');
console.log('To open last HTML report run:');
console.log(_base.colors.cyan(`
console.log(_utilsBundle.colors.cyan(`
${packageManagerCommand} playwright show-report${relativeReportPath}${hostArg}${portArg}
`));
}
@@ -145,7 +145,7 @@ function getHtmlReportOptionProcessEnv() {
const htmlOpenEnv = process.env.PLAYWRIGHT_HTML_OPEN || process.env.PW_TEST_HTML_REPORT_OPEN;
if (!htmlOpenEnv) return undefined;
if (!isHtmlReportOption(htmlOpenEnv)) {
console.log(_base.colors.red(`Configuration Error: HTML reporter Invalid value for PLAYWRIGHT_HTML_OPEN: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(', ')}`));
console.log(_utilsBundle.colors.red(`Configuration Error: HTML reporter Invalid value for PLAYWRIGHT_HTML_OPEN: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(', ')}`));
return undefined;
}
return htmlOpenEnv;
@@ -159,7 +159,7 @@ async function showHTMLReport(reportFolder, host = 'localhost', port, testId) {
try {
(0, _utils.assert)(_fs.default.statSync(folder).isDirectory());
} catch (e) {
console.log(_base.colors.red(`No report found at "${folder}"`));
console.log(_utilsBundle.colors.red(`No report found at "${folder}"`));
(0, _utils.gracefullyProcessExitDoNotHang)(1);
return;
}
@@ -171,7 +171,7 @@ async function showHTMLReport(reportFolder, host = 'localhost', port, testId) {
});
let url = server.urlPrefix('human-readable');
console.log('');
console.log(_base.colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
console.log(_utilsBundle.colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
if (testId) url += `#?testId=${testId}`;
url = url.replace('0.0.0.0', 'localhost');
await (0, _utilsBundle.open)(url, {
@@ -217,12 +217,9 @@ class HtmlBuilder {
async build(metadata, projectSuites, result, topLevelErrors) {
const data = new Map();
for (const projectSuite of projectSuites) {
const testDir = projectSuite.project().testDir;
for (const fileSuite of projectSuite.suites) {
const fileName = this._relativeLocation(fileSuite.location).file;
// Preserve file ids computed off the testDir.
const relativeFile = _path.default.relative(testDir, fileSuite.location.file);
const fileId = (0, _utils.calculateSha1)((0, _utils.toPosixPath)(relativeFile)).slice(0, 20);
const fileId = (0, _utils.calculateSha1)((0, _utils.toPosixPath)(fileName)).slice(0, 20);
let fileEntry = data.get(fileId);
if (!fileEntry) {
fileEntry = {
@@ -285,7 +282,7 @@ class HtmlBuilder {
stats: {
...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats())
},
errors: topLevelErrors.map(error => (0, _base.formatError)(error, true).message)
errors: topLevelErrors.map(error => (0, _base.formatError)(_base.internalScreen, error).message)
};
htmlReport.files.sort((f1, f2) => {
const w1 = f1.stats.unexpected * 1000 + f1.stats.flaky;
@@ -490,30 +487,37 @@ class HtmlBuilder {
duration: result.duration,
startTime: result.startTime.toISOString(),
retry: result.retry,
steps: dedupeSteps(result.steps).map(s => this._createTestStep(s)),
errors: (0, _base.formatResultFailure)(test, result, '', true).map(error => error.message),
steps: dedupeSteps(result.steps).map(s => this._createTestStep(s, result)),
errors: (0, _base.formatResultFailure)(_base.internalScreen, test, result, '').map(error => error.message),
status: result.status,
attachments: this._serializeAttachments([...result.attachments, ...result.stdout.map(m => stdioAttachment(m, 'stdout')), ...result.stderr.map(m => stdioAttachment(m, 'stderr'))])
};
}
_createTestStep(dedupedStep) {
_createTestStep(dedupedStep, result) {
var _step$error;
const {
step,
duration,
count
} = dedupedStep;
const result = {
const skipped = dedupedStep.step.category === 'test.step.skip';
const testStep = {
title: step.title,
startTime: step.startTime.toISOString(),
duration,
steps: dedupeSteps(step.steps).map(s => this._createTestStep(s)),
steps: dedupeSteps(step.steps).map(s => this._createTestStep(s, result)),
attachments: step.attachments.map(s => {
const index = result.attachments.indexOf(s);
if (index === -1) throw new Error('Unexpected, attachment not found');
return index;
}),
location: this._relativeLocation(step.location),
error: (_step$error = step.error) === null || _step$error === void 0 ? void 0 : _step$error.message,
count
count,
skipped
};
if (step.location) this._stepsInFile.set(step.location.file, result);
return result;
if (step.location) this._stepsInFile.set(step.location.file, testStep);
return testStep;
}
_relativeLocation(location) {
if (!location) return undefined;
@@ -572,17 +576,10 @@ function isTextContentType(contentType) {
return contentType.startsWith('text/') || contentType.startsWith('application/json');
}
function stdioAttachment(chunk, type) {
if (typeof chunk === 'string') {
return {
name: type,
contentType: 'text/plain',
body: chunk
};
}
return {
name: type,
contentType: 'application/octet-stream',
body: chunk
contentType: 'text/plain',
body: typeof chunk === 'string' ? chunk : chunk.toString('utf-8')
};
}
function dedupeSteps(steps) {

View File

@@ -123,7 +123,7 @@ function addLocationAndSnippetToError(config, error, file) {
});
// Convert /var/folders to /private/var/folders on Mac.
if (!file || _fs.default.realpathSync(file) !== location.file) {
tokens.push(_base.colors.gray(` at `) + `${(0, _base.relativeFilePath)(config, location.file)}:${location.line}`);
tokens.push(_base.internalScreen.colors.gray(` at `) + `${(0, _base.relativeFilePath)(_base.internalScreen, config, location.file)}:${location.line}`);
tokens.push('');
}
tokens.push(codeFrame);

View File

@@ -203,7 +203,7 @@ class JSONReporter {
return jsonResult;
}
_serializeError(error) {
return (0, _base.formatError)(error, true);
return (0, _base.formatError)(_base.nonTerminalScreen, error);
}
_serializeTestStep(step) {
const steps = step.steps.filter(s => s.category === 'test.step');

View File

@@ -167,7 +167,7 @@ class JUnitReporter {
message: `${_path.default.basename(test.location.file)}:${test.location.line}:${test.location.column} ${test.title}`,
type: 'FAILURE'
},
text: (0, _base.stripAnsiEscapes)((0, _base.formatFailure)(this.config, test))
text: (0, _base.stripAnsiEscapes)((0, _base.formatFailure)(_base.nonTerminalScreen, this.config, test))
});
}
const systemOut = [];

View File

@@ -21,7 +21,7 @@ var _base = require("./base");
* limitations under the License.
*/
class LineReporter extends _base.BaseReporter {
class LineReporter extends _base.TerminalReporter {
constructor(...args) {
super(...args);
this._current = 0;
@@ -51,7 +51,7 @@ class LineReporter extends _base.BaseReporter {
if (!process.env.PW_TEST_DEBUG_REPORTERS) stream.write(`\u001B[1A\u001B[2K`);
if (test && this._lastTest !== test) {
// Write new header for the output.
const title = _base.colors.dim((0, _base.formatTestTitle)(this.config, test));
const title = this.screen.colors.dim(this.formatTestTitle(test));
stream.write(this.fitToScreen(title) + `\n`);
this._lastTest = test;
}
@@ -73,20 +73,20 @@ class LineReporter extends _base.BaseReporter {
super.onTestEnd(test, result);
if (!this.willRetry(test) && (test.outcome() === 'flaky' || test.outcome() === 'unexpected' || result.status === 'interrupted')) {
if (!process.env.PW_TEST_DEBUG_REPORTERS) process.stdout.write(`\u001B[1A\u001B[2K`);
console.log((0, _base.formatFailure)(this.config, test, ++this._failures));
console.log(this.formatFailure(test, ++this._failures));
console.log();
}
}
_updateLine(test, result, step) {
const retriesPrefix = this.totalTestCount < this._current ? ` (retries)` : ``;
const prefix = `[${this._current}/${this.totalTestCount}]${retriesPrefix} `;
const currentRetrySuffix = result.retry ? _base.colors.yellow(` (retry #${result.retry})`) : '';
const title = (0, _base.formatTestTitle)(this.config, test, step) + currentRetrySuffix;
const currentRetrySuffix = result.retry ? this.screen.colors.yellow(` (retry #${result.retry})`) : '';
const title = this.formatTestTitle(test, step) + currentRetrySuffix;
if (process.env.PW_TEST_DEBUG_REPORTERS) process.stdout.write(`${prefix + title}\n`);else process.stdout.write(`\u001B[1A\u001B[2K${prefix + this.fitToScreen(title, prefix)}\n`);
}
onError(error) {
super.onError(error);
const message = (0, _base.formatError)(error, _base.colors.enabled).message + '\n';
const message = this.formatError(error).message + '\n';
if (!process.env.PW_TEST_DEBUG_REPORTERS && this._didBegin) process.stdout.write(`\u001B[1A\u001B[2K`);
process.stdout.write(message);
console.log();

View File

@@ -27,7 +27,7 @@ var _utils = require("playwright-core/lib/utils");
const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION;
const POSITIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? 'ok' : '✓';
const NEGATIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? 'x' : '✘';
class ListReporter extends _base.BaseReporter {
class ListReporter extends _base.TerminalReporter {
constructor(options = {}) {
super();
this._lastRow = 0;
@@ -51,11 +51,11 @@ class ListReporter extends _base.BaseReporter {
onTestBegin(test, result) {
const index = String(this._resultIndex.size + 1);
this._resultIndex.set(result, index);
if (!_base.isTTY) return;
if (!this.screen.isTTY) return;
this._maybeWriteNewLine();
this._testRows.set(test, this._lastRow);
const prefix = this._testPrefix(index, '');
const line = _base.colors.dim((0, _base.formatTestTitle)(this.config, test)) + this._retrySuffix(result);
const line = this.screen.colors.dim(this.formatTestTitle(test)) + this._retrySuffix(result);
this._appendLine(line, prefix);
}
onStdOut(chunk, test, result) {
@@ -77,41 +77,43 @@ class ListReporter extends _base.BaseReporter {
onStepBegin(test, result, step) {
if (step.category !== 'test.step') return;
const testIndex = this._resultIndex.get(result) || '';
if (!_base.isTTY) return;
if (!this.screen.isTTY) return;
if (this._printSteps) {
this._maybeWriteNewLine();
this._stepRows.set(step, this._lastRow);
const prefix = this._testPrefix(this.getStepIndex(testIndex, result, step), '');
const line = test.title + _base.colors.dim((0, _base.stepSuffix)(step));
const line = test.title + this.screen.colors.dim((0, _base.stepSuffix)(step));
this._appendLine(line, prefix);
} else {
this._updateLine(this._testRows.get(test), _base.colors.dim((0, _base.formatTestTitle)(this.config, test, step)) + this._retrySuffix(result), this._testPrefix(testIndex, ''));
this._updateLine(this._testRows.get(test), this.screen.colors.dim(this.formatTestTitle(test, step)) + this._retrySuffix(result), this._testPrefix(testIndex, ''));
}
}
onStepEnd(test, result, step) {
if (step.category !== 'test.step') return;
const testIndex = this._resultIndex.get(result) || '';
if (!this._printSteps) {
if (_base.isTTY) this._updateLine(this._testRows.get(test), _base.colors.dim((0, _base.formatTestTitle)(this.config, test, step.parent)) + this._retrySuffix(result), this._testPrefix(testIndex, ''));
if (this.screen.isTTY) this._updateLine(this._testRows.get(test), this.screen.colors.dim(this.formatTestTitle(test, step.parent)) + this._retrySuffix(result), this._testPrefix(testIndex, ''));
return;
}
const index = this.getStepIndex(testIndex, result, step);
const title = _base.isTTY ? test.title + _base.colors.dim((0, _base.stepSuffix)(step)) : (0, _base.formatTestTitle)(this.config, test, step);
const title = this.screen.isTTY ? test.title + this.screen.colors.dim((0, _base.stepSuffix)(step)) : this.formatTestTitle(test, step);
const prefix = this._testPrefix(index, '');
let text = '';
if (step.error) text = _base.colors.red(title);else text = title;
text += _base.colors.dim(` (${(0, _utilsBundle.ms)(step.duration)})`);
if (step.error) text = this.screen.colors.red(title);else text = title;
text += this.screen.colors.dim(` (${(0, _utilsBundle.ms)(step.duration)})`);
this._updateOrAppendLine(this._stepRows.get(step), text, prefix);
}
_maybeWriteNewLine() {
if (this._needNewLine) {
this._needNewLine = false;
process.stdout.write('\n');
++this._lastRow;
this._lastColumn = 0;
}
}
_updateLineCountAndNewLineFlagForOutput(text) {
this._needNewLine = text[text.length - 1] !== '\n';
if (!_base.ttyWidth) return;
if (!this.screen.ttyWidth) return;
for (const ch of text) {
if (ch === '\n') {
this._lastColumn = 0;
@@ -119,7 +121,7 @@ class ListReporter extends _base.BaseReporter {
continue;
}
++this._lastColumn;
if (this._lastColumn > _base.ttyWidth) {
if (this._lastColumn > this.screen.ttyWidth) {
this._lastColumn = 0;
++this._lastRow;
}
@@ -133,7 +135,7 @@ class ListReporter extends _base.BaseReporter {
}
onTestEnd(test, result) {
super.onTestEnd(test, result);
const title = (0, _base.formatTestTitle)(this.config, test);
const title = this.formatTestTitle(test);
let prefix = '';
let text = '';
@@ -145,24 +147,24 @@ class ListReporter extends _base.BaseReporter {
this._resultIndex.set(result, index);
}
if (result.status === 'skipped') {
prefix = this._testPrefix(index, _base.colors.green('-'));
prefix = this._testPrefix(index, this.screen.colors.green('-'));
// Do not show duration for skipped.
text = _base.colors.cyan(title) + this._retrySuffix(result);
text = this.screen.colors.cyan(title) + this._retrySuffix(result);
} else {
const statusMark = result.status === 'passed' ? POSITIVE_STATUS_MARK : NEGATIVE_STATUS_MARK;
if (result.status === test.expectedStatus) {
prefix = this._testPrefix(index, _base.colors.green(statusMark));
prefix = this._testPrefix(index, this.screen.colors.green(statusMark));
text = title;
} else {
prefix = this._testPrefix(index, _base.colors.red(statusMark));
text = _base.colors.red(title);
prefix = this._testPrefix(index, this.screen.colors.red(statusMark));
text = this.screen.colors.red(title);
}
text += this._retrySuffix(result) + _base.colors.dim(` (${(0, _utilsBundle.ms)(result.duration)})`);
text += this._retrySuffix(result) + this.screen.colors.dim(` (${(0, _utilsBundle.ms)(result.duration)})`);
}
this._updateOrAppendLine(this._testRows.get(test), text, prefix);
}
_updateOrAppendLine(row, text, prefix) {
if (_base.isTTY) {
if (this.screen.isTTY) {
this._updateLine(row, text, prefix);
} else {
this._maybeWriteNewLine();
@@ -178,6 +180,7 @@ class ListReporter extends _base.BaseReporter {
process.stdout.write('\n');
}
++this._lastRow;
this._lastColumn = 0;
}
_updateLine(row, text, prefix) {
const line = prefix + this.fitToScreen(text, prefix);
@@ -194,15 +197,15 @@ class ListReporter extends _base.BaseReporter {
}
_testPrefix(index, statusMark) {
const statusMarkLength = (0, _base.stripAnsiEscapes)(statusMark).length;
return ' ' + statusMark + ' '.repeat(3 - statusMarkLength) + _base.colors.dim(index + ' ');
return ' ' + statusMark + ' '.repeat(3 - statusMarkLength) + this.screen.colors.dim(index + ' ');
}
_retrySuffix(result) {
return result.retry ? _base.colors.yellow(` (retry #${result.retry})`) : '';
return result.retry ? this.screen.colors.yellow(` (retry #${result.retry})`) : '';
}
onError(error) {
super.onError(error);
this._maybeWriteNewLine();
const message = (0, _base.formatError)(error, _base.colors.enabled).message + '\n';
const message = this.formatError(error).message + '\n';
this._updateLineCountAndNewLineFlagForOutput(message);
process.stdout.write(message);
}

View File

@@ -25,7 +25,7 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
* limitations under the License.
*/
class MarkdownReporter extends _base.BaseReporter {
class MarkdownReporter extends _base.TerminalReporter {
constructor(options) {
super();
this._options = void 0;
@@ -69,7 +69,7 @@ class MarkdownReporter extends _base.BaseReporter {
await _fs.default.promises.writeFile(reportFile, lines.join('\n'));
}
_printTestList(prefix, tests, lines, suffix) {
for (const test of tests) lines.push(`${prefix} ${(0, _base.formatTestTitle)(this.config, test)}${suffix || ''}`);
for (const test of tests) lines.push(`${prefix} ${this.formatTestTitle(test)}${suffix || ''}`);
lines.push(``);
}
}

View File

@@ -102,7 +102,7 @@ class TeleReporterEmitter {
params: {
testId: test.id,
resultId: result[this._idSymbol],
step: this._serializeStepEnd(step)
step: this._serializeStepEnd(step, result)
}
});
}
@@ -245,11 +245,12 @@ class TeleReporterEmitter {
location: this._relativeLocation(step.location)
};
}
_serializeStepEnd(step) {
_serializeStepEnd(step, result) {
return {
id: step[this._idSymbol],
duration: step.duration,
error: step.error
error: step.error,
attachments: step.attachments.map(a => result.attachments.indexOf(a))
};
}
_relativeLocation(location) {

View File

@@ -321,6 +321,7 @@ class JobDispatcher {
startTime: new Date(params.wallTime),
duration: -1,
steps: [],
attachments: [],
location: params.location
};
steps.set(params.stepId, step);
@@ -364,6 +365,11 @@ class JobDispatcher {
body: params.body !== undefined ? Buffer.from(params.body, 'base64') : undefined
};
data.result.attachments.push(attachment);
if (params.stepId) {
var _this$_reporter$onStd4, _this$_reporter8;
const step = data.steps.get(params.stepId);
if (step) step.attachments.push(attachment);else (_this$_reporter$onStd4 = (_this$_reporter8 = this._reporter).onStdErr) === null || _this$_reporter$onStd4 === void 0 || _this$_reporter$onStd4.call(_this$_reporter8, 'Internal error: step id not found: ' + params.stepId);
}
}
_failTestWithErrors(test, errors) {
const runData = this._dataByTestId.get(test.id);
@@ -372,9 +378,9 @@ class JobDispatcher {
if (runData) {
result = runData.result;
} else {
var _this$_reporter$onTes2, _this$_reporter8;
var _this$_reporter$onTes2, _this$_reporter9;
result = test._appendTestResult();
(_this$_reporter$onTes2 = (_this$_reporter8 = this._reporter).onTestBegin) === null || _this$_reporter$onTes2 === void 0 || _this$_reporter$onTes2.call(_this$_reporter8, test, result);
(_this$_reporter$onTes2 = (_this$_reporter9 = this._reporter).onTestBegin) === null || _this$_reporter$onTes2 === void 0 || _this$_reporter$onTes2.call(_this$_reporter9, test, result);
}
result.errors = [...errors];
result.error = result.errors[0];
@@ -396,8 +402,8 @@ class JobDispatcher {
// Let's just fail the test run.
this._failureTracker.onWorkerError();
for (const error of errors) {
var _this$_reporter$onErr2, _this$_reporter9;
(_this$_reporter$onErr2 = (_this$_reporter9 = this._reporter).onError) === null || _this$_reporter$onErr2 === void 0 || _this$_reporter$onErr2.call(_this$_reporter9, error);
var _this$_reporter$onErr2, _this$_reporter10;
(_this$_reporter$onErr2 = (_this$_reporter10 = this._reporter).onError) === null || _this$_reporter$onErr2 === void 0 || _this$_reporter$onErr2.call(_this$_reporter10, error);
}
}
}
@@ -517,9 +523,9 @@ class JobDispatcher {
const allTestsSkipped = this._job.tests.every(test => test.expectedStatus === 'skipped');
if (allTestsSkipped && !this._failureTracker.hasReachedMaxFailures()) {
for (const test of this._job.tests) {
var _this$_reporter$onTes3, _this$_reporter10;
var _this$_reporter$onTes3, _this$_reporter11;
const result = test._appendTestResult();
(_this$_reporter$onTes3 = (_this$_reporter10 = this._reporter).onTestBegin) === null || _this$_reporter$onTes3 === void 0 || _this$_reporter$onTes3.call(_this$_reporter10, test, result);
(_this$_reporter$onTes3 = (_this$_reporter11 = this._reporter).onTestBegin) === null || _this$_reporter$onTes3 === void 0 || _this$_reporter$onTes3.call(_this$_reporter11, test, result);
result.status = 'skipped';
this._reportTestEnd(test, result);
}
@@ -531,14 +537,14 @@ class JobDispatcher {
return this._currentlyRunning;
}
_reportTestEnd(test, result) {
var _this$_reporter$onTes4, _this$_reporter11;
(_this$_reporter$onTes4 = (_this$_reporter11 = this._reporter).onTestEnd) === null || _this$_reporter$onTes4 === void 0 || _this$_reporter$onTes4.call(_this$_reporter11, test, result);
var _this$_reporter$onTes4, _this$_reporter12;
(_this$_reporter$onTes4 = (_this$_reporter12 = this._reporter).onTestEnd) === null || _this$_reporter$onTes4 === void 0 || _this$_reporter$onTes4.call(_this$_reporter12, test, result);
const hadMaxFailures = this._failureTracker.hasReachedMaxFailures();
this._failureTracker.onTestEnd(test, result);
if (this._failureTracker.hasReachedMaxFailures()) {
var _this$_reporter$onErr3, _this$_reporter12;
var _this$_reporter$onErr3, _this$_reporter13;
this._stopCallback();
if (!hadMaxFailures) (_this$_reporter$onErr3 = (_this$_reporter12 = this._reporter).onError) === null || _this$_reporter$onErr3 === void 0 || _this$_reporter$onErr3.call(_this$_reporter12, {
if (!hadMaxFailures) (_this$_reporter$onErr3 = (_this$_reporter13 = this._reporter).onError) === null || _this$_reporter$onErr3 === void 0 || _this$_reporter$onErr3.call(_this$_reporter13, {
message: _utilsBundle.colors.red(`Testing stopped early after ${this._failureTracker.maxFailures()} maximum allowed failures.`)
});
}

View File

@@ -37,7 +37,7 @@ class LastRunReporter {
if (!this._lastRunFile) return;
try {
const lastRunInfo = JSON.parse(await _fs.default.promises.readFile(this._lastRunFile, 'utf8'));
this._config.testIdMatcher = id => lastRunInfo.failedTests.includes(id);
this._config.lastFailedTestIdMatcher = id => lastRunInfo.failedTests.includes(id);
} catch {}
}
version() {

View File

@@ -196,6 +196,9 @@ async function createRootSuite(testRun, errors, shouldFilterOnly, additionalFile
(0, _suiteUtils.filterTestsRemoveEmptySuites)(rootSuite, test => testsInThisShard.has(test));
}
// Explicitly apply --last-failed filter after sharding.
if (config.lastFailedTestIdMatcher) (0, _suiteUtils.filterByTestIds)(rootSuite, config.lastFailedTestIdMatcher);
// Now prepend dependency projects without filtration.
{
// Filtering 'only' and sharding might have reduced the number of top-level projects.

View File

@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
});
exports.addSuggestedRebaseline = addSuggestedRebaseline;
exports.applySuggestedRebaselines = applySuggestedRebaselines;
exports.clearSuggestedRebaselines = clearSuggestedRebaselines;
var _path = _interopRequireDefault(require("path"));
var _fs = _interopRequireDefault(require("fs"));
var _babelBundle = require("../transform/babelBundle");
@@ -36,13 +37,18 @@ function addSuggestedRebaseline(location, suggestedRebaseline) {
code: suggestedRebaseline
});
}
function clearSuggestedRebaselines() {
suggestedRebaselines.clear();
}
async function applySuggestedRebaselines(config, reporter) {
if (config.config.updateSnapshots !== 'all' && config.config.updateSnapshots !== 'missing') return;
if (config.config.updateSnapshots === 'none') return;
if (!suggestedRebaselines.size) return;
const [project] = (0, _projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
if (!project) return;
const patches = [];
const files = [];
const gitCache = new Map();
const patchFile = _path.default.join(project.project.outputDir, 'rebaselines.patch');
for (const fileName of [...suggestedRebaselines.keys()].sort()) {
const source = await _fs.default.promises.readFile(fileName, 'utf8');
const lines = source.split('\n');
@@ -52,21 +58,24 @@ async function applySuggestedRebaselines(config, reporter) {
(0, _babelBundle.traverse)(fileNode, {
CallExpression: path => {
const node = path.node;
if (node.arguments.length !== 1) return;
if (node.arguments.length < 1) return;
if (!t.isMemberExpression(node.callee)) return;
const argument = node.arguments[0];
if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument)) return;
const matcher = node.callee.property;
const prop = node.callee.property;
if (!prop.loc || !argument.start || !argument.end) return;
// Replacements are anchored by the location of the call expression.
// However, replacement text is meant to only replace the first argument.
for (const replacement of replacements) {
// In Babel, rows are 1-based, columns are 0-based.
if (matcher.loc.start.line !== replacement.location.line) continue;
if (matcher.loc.start.column + 1 !== replacement.location.column) continue;
const indent = lines[matcher.loc.start.line - 1].match(/^\s*/)[0];
if (prop.loc.start.line !== replacement.location.line) continue;
if (prop.loc.start.column + 1 !== replacement.location.column) continue;
const indent = lines[prop.loc.start.line - 1].match(/^\s*/)[0];
const newText = replacement.code.replace(/\{indent\}/g, indent);
ranges.push({
start: matcher.start,
end: node.end,
oldText: source.substring(matcher.start, node.end),
start: argument.start,
end: argument.end,
oldText: source.substring(argument.start, argument.end),
newText
});
// We can have multiple, hopefully equal, replacements for the same location,
@@ -81,15 +90,25 @@ async function applySuggestedRebaselines(config, reporter) {
for (const range of ranges) result = result.substring(0, range.start) + range.newText + result.substring(range.end);
const relativeName = _path.default.relative(process.cwd(), fileName);
files.push(relativeName);
patches.push(createPatch(relativeName, source, result));
if (config.config.updateSourceMethod === 'overwrite') {
await _fs.default.promises.writeFile(fileName, result);
} else if (config.config.updateSourceMethod === '3way') {
await _fs.default.promises.writeFile(fileName, applyPatchWithConflictMarkers(source, result));
} else {
const gitFolder = findGitRoot(_path.default.dirname(fileName), gitCache);
const relativeToGit = _path.default.relative(gitFolder || process.cwd(), fileName);
patches.push(createPatch(relativeToGit, source, result));
}
}
const patchFile = _path.default.join(project.project.outputDir, 'rebaselines.patch');
await _fs.default.promises.mkdir(_path.default.dirname(patchFile), {
recursive: true
});
await _fs.default.promises.writeFile(patchFile, patches.join('\n'));
const fileList = files.map(file => ' ' + _utilsBundle.colors.dim(file)).join('\n');
reporter.onStdErr(`\nNew baselines created for:\n\n${fileList}\n\n ` + _utilsBundle.colors.cyan('git apply ' + _path.default.relative(process.cwd(), patchFile)) + '\n');
reporter.onStdErr(`\nNew baselines created for:\n\n${fileList}\n`);
if (config.config.updateSourceMethod === 'patch') {
await _fs.default.promises.mkdir(_path.default.dirname(patchFile), {
recursive: true
});
await _fs.default.promises.writeFile(patchFile, patches.join('\n'));
reporter.onStdErr(`\n ` + _utilsBundle.colors.cyan('git apply ' + _path.default.relative(process.cwd(), patchFile)) + '\n');
}
}
function createPatch(fileName, before, after) {
const file = fileName.replace(/\\/g, '/');
@@ -97,4 +116,53 @@ function createPatch(fileName, before, after) {
context: 3
});
return ['diff --git a/' + file + ' b/' + file, '--- a/' + file, '+++ b/' + file, ...text.split('\n').slice(4)].join('\n');
}
function findGitRoot(dir, cache) {
const result = cache.get(dir);
if (result !== undefined) return result;
const gitPath = _path.default.join(dir, '.git');
if (_fs.default.existsSync(gitPath) && _fs.default.lstatSync(gitPath).isDirectory()) {
cache.set(dir, dir);
return dir;
}
const parentDir = _path.default.dirname(dir);
if (dir === parentDir) {
cache.set(dir, null);
return null;
}
const parentResult = findGitRoot(parentDir, cache);
cache.set(dir, parentResult);
return parentResult;
}
function applyPatchWithConflictMarkers(oldText, newText) {
const diffResult = _utilsBundle.diff.diffLines(oldText, newText);
let result = '';
let conflict = false;
diffResult.forEach(part => {
if (part.added) {
if (conflict) {
result += part.value;
result += '>>>>>>> SNAPSHOT\n';
conflict = false;
} else {
result += '<<<<<<< HEAD\n';
result += part.value;
result += '=======\n';
conflict = true;
}
} else if (part.removed) {
result += '<<<<<<< HEAD\n';
result += part.value;
result += '=======\n';
conflict = true;
} else {
if (conflict) {
result += '>>>>>>> SNAPSHOT\n';
conflict = false;
}
result += part.value;
}
});
if (conflict) result += '>>>>>>> SNAPSHOT\n';
return result;
}

View File

@@ -87,13 +87,13 @@ async function createReporterForTestServer(file, messageSink) {
_send: messageSink
}));
}
function createErrorCollectingReporter(writeToConsole) {
function createErrorCollectingReporter(screen, writeToConsole) {
const errors = [];
return {
version: () => 'v2',
onError(error) {
errors.push(error);
if (writeToConsole) process.stdout.write((0, _base.formatError)(error, _base.colors.enabled).message + '\n');
if (writeToConsole) process.stdout.write((0, _base.formatError)(screen, error).message + '\n');
},
errors: () => errors
};
@@ -147,6 +147,6 @@ class ListModeReporter {
}
onError(error) {
// eslint-disable-next-line no-console
console.error('\n' + (0, _base.formatError)(error, false).message);
console.error('\n' + (0, _base.formatError)(_base.terminalScreen, error).message);
}
}

View File

@@ -11,6 +11,7 @@ var _tasks = require("./tasks");
var _compilationCache = require("../transform/compilationCache");
var _internalReporter = require("../reporters/internalReporter");
var _lastRun = require("./lastRun");
var _base = require("../reporters/base");
/**
* Copyright 2019 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
@@ -79,7 +80,7 @@ class Runner {
return status;
}
async findRelatedTestFiles(files) {
const errorReporter = (0, _reporters.createErrorCollectingReporter)();
const errorReporter = (0, _reporters.createErrorCollectingReporter)(_base.terminalScreen);
const reporter = new _internalReporter.InternalReporter([errorReporter]);
const status = await (0, _tasks.runTasks)(new _tasks.TestRun(this._config, reporter), [...(0, _tasks.createPluginSetupTasks)(this._config), (0, _tasks.createLoadTask)('in-process', {
failOnLoadErrors: true,
@@ -95,7 +96,7 @@ class Runner {
};
}
async runDevServer() {
const reporter = new _internalReporter.InternalReporter([(0, _reporters.createErrorCollectingReporter)(true)]);
const reporter = new _internalReporter.InternalReporter([(0, _reporters.createErrorCollectingReporter)(_base.terminalScreen, true)]);
const status = await (0, _tasks.runTasks)(new _tasks.TestRun(this._config, reporter), [...(0, _tasks.createPluginSetupTasks)(this._config), (0, _tasks.createLoadTask)('in-process', {
failOnLoadErrors: true,
filterOnly: false
@@ -108,7 +109,7 @@ class Runner {
};
}
async clearCache() {
const reporter = new _internalReporter.InternalReporter([(0, _reporters.createErrorCollectingReporter)(true)]);
const reporter = new _internalReporter.InternalReporter([(0, _reporters.createErrorCollectingReporter)(_base.terminalScreen, true)]);
const status = await (0, _tasks.runTasks)(new _tasks.TestRun(this._config, reporter), [...(0, _tasks.createPluginSetupTasks)(this._config), (0, _tasks.createClearCacheTask)(this._config)]);
return {
status

View File

@@ -267,6 +267,9 @@ function createLoadTask(mode, options) {
function createApplyRebaselinesTask() {
return {
title: 'apply rebaselines',
setup: async () => {
(0, _rebase.clearSuggestedRebaselines)();
},
teardown: async ({
config,
reporter

View File

@@ -23,6 +23,7 @@ var _webServerPlugin = require("../plugins/webServerPlugin");
var _util = require("../util");
var _teleReceiver = require("../isomorphic/teleReceiver");
var _internalReporter = require("../reporters/internalReporter");
var _base = require("../reporters/base");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Copyright Microsoft Corporation. All rights reserved.
@@ -353,6 +354,9 @@ class TestServerDispatcher {
...(params.updateSnapshots ? {
updateSnapshots: params.updateSnapshots
} : {}),
...(params.updateSourceMethod ? {
updateSourceMethod: params.updateSourceMethod
} : {}),
...(params.workers ? {
workers: params.workers
} : {})
@@ -400,7 +404,7 @@ class TestServerDispatcher {
await this._updateWatcher(true);
}
async findRelatedTestFiles(params) {
const errorReporter = (0, _reporters.createErrorCollectingReporter)();
const errorReporter = (0, _reporters.createErrorCollectingReporter)(_base.internalScreen);
const reporter = new _internalReporter.InternalReporter([errorReporter]);
const config = await this._loadConfigOrReportError(reporter);
if (!config) return {
@@ -528,7 +532,7 @@ async function innerRunTestServer(configLocation, configCLIOverrides, options, o
return sigintWatcher.hadSignal() ? 'interrupted' : 'passed';
}
function chunkToPayload(type, chunk) {
if (chunk instanceof Buffer) return {
if (chunk instanceof Uint8Array) return {
type,
buffer: chunk.toString('base64')
};

View File

@@ -330,7 +330,7 @@ function readCommand() {
const name = key === null || key === void 0 ? void 0 : key.name;
if (name === 'q') return 'exit';
if (name === 'h') {
process.stdout.write(`${(0, _base.separator)()}
process.stdout.write(`${(0, _base.separator)(_base.terminalScreen)}
Run tests
${_utilsBundle.colors.bold('enter')} ${_utilsBundle.colors.dim('run tests')}
${_utilsBundle.colors.bold('f')} ${_utilsBundle.colors.dim('run failed tests')}
@@ -379,14 +379,14 @@ function printConfiguration(options, title) {
if (title) tokens.push(_utilsBundle.colors.dim(`(${title})`));
tokens.push(_utilsBundle.colors.dim(`#${seq++}`));
const lines = [];
const sep = (0, _base.separator)();
const sep = (0, _base.separator)(_base.terminalScreen);
lines.push('\x1Bc' + sep);
lines.push(`${tokens.join(' ')}`);
lines.push(`${_utilsBundle.colors.dim('Show & reuse browser:')} ${_utilsBundle.colors.bold(showBrowserServer ? 'on' : 'off')}`);
process.stdout.write(lines.join('\n'));
}
function printBufferPrompt(dirtyTestFiles, rootDir) {
const sep = (0, _base.separator)();
const sep = (0, _base.separator)(_base.terminalScreen);
process.stdout.write('\x1Bc');
process.stdout.write(`${sep}\n`);
if (dirtyTestFiles.size === 0) {
@@ -398,7 +398,7 @@ function printBufferPrompt(dirtyTestFiles, rootDir) {
process.stdout.write(`\n${_utilsBundle.colors.dim(`Press`)} ${_utilsBundle.colors.bold('enter')} ${_utilsBundle.colors.dim('to run')}, ${_utilsBundle.colors.bold('q')} ${_utilsBundle.colors.dim('to quit or')} ${_utilsBundle.colors.bold('h')} ${_utilsBundle.colors.dim('for more options.')}\n\n`);
}
function printPrompt() {
const sep = (0, _base.separator)();
const sep = (0, _base.separator)(_base.terminalScreen);
process.stdout.write(`
${sep}
${_utilsBundle.colors.dim('Waiting for file changes. Press')} ${_utilsBundle.colors.bold('enter')} ${_utilsBundle.colors.dim('to run tests')}, ${_utilsBundle.colors.bold('q')} ${_utilsBundle.colors.dim('to quit or')} ${_utilsBundle.colors.bold('h')} ${_utilsBundle.colors.dim('for more options.')}

View File

@@ -62,13 +62,14 @@ class Fixture {
};
}
async setup(testInfo, runnable) {
var _this$registration$cu;
this.runner.instanceForId.set(this.registration.id, this);
if (typeof this.registration.fn !== 'function') {
this.value = this.registration.fn;
return;
}
await testInfo._runAsStage({
title: `fixture: ${this.registration.name}`,
title: `fixture: ${(_this$registration$cu = this.registration.customTitle) !== null && _this$registration$cu !== void 0 ? _this$registration$cu : this.registration.name}`,
runnable: {
...runnable,
fixture: this._setupDescription
@@ -135,8 +136,9 @@ class Fixture {
// Do not even start the teardown for a fixture that does not have any
// time remaining in the time slot. This avoids cascading timeouts.
if (!testInfo._timeoutManager.isTimeExhaustedFor(fixtureRunnable)) {
var _this$registration$cu2;
await testInfo._runAsStage({
title: `fixture: ${this.registration.name}`,
title: `fixture: ${(_this$registration$cu2 = this.registration.customTitle) !== null && _this$registration$cu2 !== void 0 ? _this$registration$cu2 : this.registration.name}`,
runnable: fixtureRunnable,
stepInfo: this._stepInfo
}, async () => {

View File

@@ -74,6 +74,7 @@ class TestInfoImpl {
this._projectInternal = void 0;
this._configInternal = void 0;
this._steps = [];
this._stepMap = new Map();
this._onDidFinishTestFunction = void 0;
this._hasNonRetriableError = false;
this._hasUnhandledError = false;
@@ -144,7 +145,10 @@ class TestInfoImpl {
})();
this._attachmentsPush = this.attachments.push.bind(this.attachments);
this.attachments.push = (...attachments) => {
for (const a of attachments) this._attach(a.name, a);
for (const a of attachments) {
var _this$_parentStep;
this._attach(a, (_this$_parentStep = this._parentStep()) === null || _this$_parentStep === void 0 ? void 0 : _this$_parentStep.stepId);
}
return this.attachments.length;
};
this._tracing = new _testTracing.TestTracing(this, workerParams.artifactsDir);
@@ -176,6 +180,10 @@ class TestInfoImpl {
if (steps[i].isStage && !steps[i].endWallTime) return steps[i];
}
}
_parentStep() {
var _zones$zoneData;
return (_zones$zoneData = _utils.zones.zoneData('stepZone')) !== null && _zones$zoneData !== void 0 ? _zones$zoneData : this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
}
_addStep(data, parentStep) {
var _parentStep, _parentStep2;
const stepId = `${data.category}@${++this._lastStepId}`;
@@ -183,11 +191,7 @@ class TestInfoImpl {
// Predefined stages form a fixed hierarchy - use the current one as parent.
parentStep = this._findLastStageStep(this._steps);
} else {
if (!parentStep) parentStep = _utils.zones.zoneData('stepZone');
if (!parentStep) {
// If no parent step on stack, assume the current stage as parent.
parentStep = this._findLastStageStep(this._steps);
}
if (!parentStep) parentStep = this._parentStep();
}
const filteredStack = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)());
data.boxedStack = (_parentStep = parentStep) === null || _parentStep === void 0 ? void 0 : _parentStep.boxedStack;
@@ -196,10 +200,12 @@ class TestInfoImpl {
data.location = data.location || data.boxedStack[0];
}
data.location = data.location || filteredStack[0];
const attachmentIndices = [];
const step = {
stepId,
...data,
steps: [],
attachmentIndices,
complete: result => {
if (step.endWallTime) return;
step.endWallTime = Date.now();
@@ -235,11 +241,13 @@ class TestInfoImpl {
message: step.error.message || '',
stack: step.error.stack
} : undefined;
this._tracing.appendAfterActionForStep(stepId, errorForTrace, result.attachments);
const attachments = attachmentIndices.map(i => this.attachments[i]);
this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments);
}
};
const parentStepList = parentStep ? parentStep.steps : this._steps;
parentStepList.push(step);
this._stepMap.set(stepId, step);
const payload = {
testId: this.testId,
stepId,
@@ -250,7 +258,7 @@ class TestInfoImpl {
location: data.location
};
this._onStepBegin(payload);
this._tracing.appendBeforeActionForStep(stepId, (_parentStep2 = parentStep) === null || _parentStep2 === void 0 ? void 0 : _parentStep2.stepId, data.apiName || data.title, data.params, data.location ? [data.location] : []);
this._tracing.appendBeforeActionForStep(stepId, (_parentStep2 = parentStep) === null || _parentStep2 === void 0 ? void 0 : _parentStep2.stepId, data.category, data.apiName || data.title, data.params, data.location ? [data.location] : []);
return step;
}
_interrupt() {
@@ -330,24 +338,32 @@ class TestInfoImpl {
// ------------ TestInfo methods ------------
async attach(name, options = {}) {
this._attach(name, await (0, _util.normalizeAndSaveAttachment)(this.outputPath(), name, options));
}
_attach(name, attachment) {
var _attachment$body;
const step = this._addStep({
title: `attach "${name}"`,
category: 'attach'
});
this._attachmentsPush(attachment);
this._attach(await (0, _util.normalizeAndSaveAttachment)(this.outputPath(), name, options), step.stepId);
step.complete({});
}
_attach(attachment, stepId) {
var _attachment$body;
const index = this._attachmentsPush(attachment) - 1;
if (stepId) {
this._stepMap.get(stepId).attachmentIndices.push(index);
} else {
var _this$_findLastStageS;
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action
const callId = `attach@${++this._lastStepId}`;
this._tracing.appendBeforeActionForStep(callId, (_this$_findLastStageS = this._findLastStageStep(this._steps)) === null || _this$_findLastStageS === void 0 ? void 0 : _this$_findLastStageS.stepId, 'attach', `attach "${attachment.name}"`, undefined, []);
this._tracing.appendAfterActionForStep(callId, undefined, [attachment]);
}
this._onAttach({
testId: this.testId,
name: attachment.name,
contentType: attachment.contentType,
path: attachment.path,
body: (_attachment$body = attachment.body) === null || _attachment$body === void 0 ? void 0 : _attachment$body.toString('base64')
});
step.complete({
attachments: [attachment]
body: (_attachment$body = attachment.body) === null || _attachment$body === void 0 ? void 0 : _attachment$body.toString('base64'),
stepId
});
}
outputPath(...pathSegments) {
@@ -367,15 +383,20 @@ class TestInfoImpl {
const fullTitleWithoutSpec = this.titlePath.slice(1).join(' ');
return (0, _utils.sanitizeForFilePath)((0, _util.trimLongString)(fullTitleWithoutSpec));
}
snapshotPath(...pathSegments) {
_resolveSnapshotPath(template, defaultTemplate, pathSegments) {
const subPath = _path.default.join(...pathSegments);
const parsedSubPath = _path.default.parse(subPath);
const relativeTestFilePath = _path.default.relative(this.project.testDir, this._requireFile);
const parsedRelativeTestFilePath = _path.default.parse(relativeTestFilePath);
const projectNamePathSegment = (0, _utils.sanitizeForFilePath)(this.project.name);
const snapshotPath = (this._projectInternal.snapshotPathTemplate || '').replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir).replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir).replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '').replace(/\{(.)?testFileDir\}/g, '$1' + parsedRelativeTestFilePath.dir).replace(/\{(.)?platform\}/g, '$1' + process.platform).replace(/\{(.)?projectName\}/g, projectNamePathSegment ? '$1' + projectNamePathSegment : '').replace(/\{(.)?testName\}/g, '$1' + this._fsSanitizedTestName()).replace(/\{(.)?testFileName\}/g, '$1' + parsedRelativeTestFilePath.base).replace(/\{(.)?testFilePath\}/g, '$1' + relativeTestFilePath).replace(/\{(.)?arg\}/g, '$1' + _path.default.join(parsedSubPath.dir, parsedSubPath.name)).replace(/\{(.)?ext\}/g, parsedSubPath.ext ? '$1' + parsedSubPath.ext : '');
const actualTemplate = template || this._projectInternal.snapshotPathTemplate || defaultTemplate;
const snapshotPath = actualTemplate.replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir).replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir).replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '').replace(/\{(.)?testFileDir\}/g, '$1' + parsedRelativeTestFilePath.dir).replace(/\{(.)?platform\}/g, '$1' + process.platform).replace(/\{(.)?projectName\}/g, projectNamePathSegment ? '$1' + projectNamePathSegment : '').replace(/\{(.)?testName\}/g, '$1' + this._fsSanitizedTestName()).replace(/\{(.)?testFileName\}/g, '$1' + parsedRelativeTestFilePath.base).replace(/\{(.)?testFilePath\}/g, '$1' + relativeTestFilePath).replace(/\{(.)?arg\}/g, '$1' + _path.default.join(parsedSubPath.dir, parsedSubPath.name)).replace(/\{(.)?ext\}/g, parsedSubPath.ext ? '$1' + parsedSubPath.ext : '');
return _path.default.normalize(_path.default.resolve(this._configInternal.configDir, snapshotPath));
}
snapshotPath(...pathSegments) {
const legacyTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
return this._resolveSnapshotPath(undefined, legacyTemplate, pathSegments);
}
skip(...args) {
this._modifier('skip', args);
}

View File

@@ -212,14 +212,14 @@ class TestTracing {
base64: typeof chunk === 'string' ? undefined : chunk.toString('base64')
});
}
appendBeforeActionForStep(callId, parentId, apiName, params, stack) {
appendBeforeActionForStep(callId, parentId, category, apiName, params, stack) {
this._appendTraceEvent({
type: 'before',
callId,
parentId,
startTime: (0, _utils.monotonicTime)(),
class: 'Test',
method: 'step',
method: category,
apiName,
params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])),
stack

View File

@@ -71,17 +71,19 @@ class WorkerMain extends _process.ProcessRunner {
this._runFinished.resolve();
process.on('unhandledRejection', reason => this.unhandledError(reason));
process.on('uncaughtException', error => this.unhandledError(error));
process.stdout.write = chunk => {
process.stdout.write = (chunk, cb) => {
var _this$_currentTest;
this.dispatchEvent('stdOut', (0, _ipc.stdioChunkToParams)(chunk));
(_this$_currentTest = this._currentTest) === null || _this$_currentTest === void 0 || _this$_currentTest._tracing.appendStdioToTrace('stdout', chunk);
if (typeof cb === 'function') process.nextTick(cb);
return true;
};
if (!process.env.PW_RUNNER_DEBUG) {
process.stderr.write = chunk => {
process.stderr.write = (chunk, cb) => {
var _this$_currentTest2;
this.dispatchEvent('stdErr', (0, _ipc.stdioChunkToParams)(chunk));
(_this$_currentTest2 = this._currentTest) === null || _this$_currentTest2 === void 0 || _this$_currentTest2._tracing.appendStdioToTrace('stderr', chunk);
if (typeof cb === 'function') process.nextTick(cb);
return true;
};
}

View File

@@ -1,6 +1,6 @@
{
"name": "playwright",
"version": "1.49.1",
"version": "1.50.1",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
@@ -56,7 +56,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.1"
"playwright-core": "1.50.1"
},
"optionalDependencies": {
"fsevents": "2.3.2"

View File

@@ -214,6 +214,27 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
* [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot).
*/
stylePath?: string|Array<string>;
/**
* A template controlling location of the screenshots. See
* [testProject.snapshotPathTemplate](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-path-template)
* for details.
*/
pathTemplate?: string;
};
/**
* Configuration for the
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* method.
*/
toMatchAriaSnapshot?: {
/**
* A template controlling location of the aria snapshots. See
* [testProject.snapshotPathTemplate](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-path-template)
* for details.
*/
pathTemplate?: string;
};
/**
@@ -404,10 +425,14 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
/**
* This option configures a template controlling location of snapshots generated by
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1)
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1),
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* and
* [expect(value).toMatchSnapshot(name[, options])](https://playwright.dev/docs/api/class-snapshotassertions#snapshot-assertions-to-match-snapshot-1).
*
* You can configure templates for each assertion separately in
* [testConfig.expect](https://playwright.dev/docs/api/class-testconfig#test-config-expect).
*
* **Usage**
*
* ```js
@@ -416,7 +441,19 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
*
* export default defineConfig({
* testDir: './tests',
*
* // Single template for all assertions
* snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
*
* // Assertion-specific templates
* expect: {
* toHaveScreenshot: {
* pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
* },
* toMatchAriaSnapshot: {
* pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
* },
* },
* });
* ```
*
@@ -447,27 +484,27 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
* ```
*
* The list of supported tokens:
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
* snapshot name.
* - `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to
* `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be
* an auto-generated snapshot name.
* - Value: `foo/bar/baz`
* - `{ext}` - snapshot extension (with dots)
* - `{ext}` - Snapshot extension (with the leading dot).
* - Value: `.png`
* - `{platform}` - The value of `process.platform`.
* - `{projectName}` - Project's file-system-sanitized name, if any.
* - Value: `''` (empty string).
* - `{snapshotDir}` - Project's
* [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
* [testProject.snapshotDir](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-dir).
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* - `{testDir}` - Project's
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
* - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
* - Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with
* config)
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* - Value: `page`
* - `{testFileName}` - Test file name with extension.
* - Value: `page-click.spec.ts`
* - `{testFilePath}` - Relative path from `testDir` to **test file**
* - `{testFilePath}` - Relative path from `testDir` to **test file**.
* - Value: `page/page-click.spec.ts`
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* - Value: `suite-test-should-work`
@@ -991,6 +1028,27 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
*/
threshold?: number;
/**
* A template controlling location of the screenshots. See
* [testConfig.snapshotPathTemplate](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template)
* for details.
*/
pathTemplate?: string;
};
/**
* Configuration for the
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* method.
*/
toMatchAriaSnapshot?: {
/**
* A template controlling location of the aria snapshots. See
* [testConfig.snapshotPathTemplate](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template)
* for details.
*/
pathTemplate?: string;
};
/**
@@ -1468,10 +1526,14 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
/**
* This option configures a template controlling location of snapshots generated by
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1)
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1),
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* and
* [expect(value).toMatchSnapshot(name[, options])](https://playwright.dev/docs/api/class-snapshotassertions#snapshot-assertions-to-match-snapshot-1).
*
* You can configure templates for each assertion separately in
* [testConfig.expect](https://playwright.dev/docs/api/class-testconfig#test-config-expect).
*
* **Usage**
*
* ```js
@@ -1480,7 +1542,19 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
*
* export default defineConfig({
* testDir: './tests',
*
* // Single template for all assertions
* snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
*
* // Assertion-specific templates
* expect: {
* toHaveScreenshot: {
* pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
* },
* toMatchAriaSnapshot: {
* pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
* },
* },
* });
* ```
*
@@ -1511,27 +1585,27 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* ```
*
* The list of supported tokens:
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
* snapshot name.
* - `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to
* `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be
* an auto-generated snapshot name.
* - Value: `foo/bar/baz`
* - `{ext}` - snapshot extension (with dots)
* - `{ext}` - Snapshot extension (with the leading dot).
* - Value: `.png`
* - `{platform}` - The value of `process.platform`.
* - `{projectName}` - Project's file-system-sanitized name, if any.
* - Value: `''` (empty string).
* - `{snapshotDir}` - Project's
* [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
* [testProject.snapshotDir](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-dir).
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* - `{testDir}` - Project's
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
* - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
* - Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with
* config)
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* - Value: `page`
* - `{testFileName}` - Test file name with extension.
* - Value: `page-click.spec.ts`
* - `{testFilePath}` - Relative path from `testDir` to **test file**
* - `{testFilePath}` - Relative path from `testDir` to **test file**.
* - Value: `page/page-click.spec.ts`
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* - Value: `suite-test-should-work`
@@ -1665,11 +1739,12 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
/**
* Whether to update expected snapshots with the actual results produced by the test run. Defaults to `'missing'`.
* - `'all'` - All tests that are executed will update snapshots that did not match. Matching snapshots will not be
* updated.
* - `'none'` - No snapshots are updated.
* - `'all'` - All tests that are executed will update snapshots.
* - `'changed'` - All tests that are executed will update snapshots that did not match. Matching snapshots will not
* be updated.
* - `'missing'` - Missing snapshots are created, for example when authoring a new test and running it for the first
* time. This is the default.
* - `'none'` - No snapshots are updated.
*
* Learn more about [snapshots](https://playwright.dev/docs/test-snapshots).
*
@@ -1685,7 +1760,16 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* ```
*
*/
updateSnapshots?: "all"|"none"|"missing";
updateSnapshots?: "all"|"changed"|"missing"|"none";
/**
* Defines how to update snapshots in the source code.
* - `'patch'` - Create a unified diff file that can be used to update the source code later. This is the default.
* - `'3way'` - Generate merge conflict markers in source code. This allows user to manually pick relevant changes,
* as if they are resolving a merge conflict in the IDE.
* - `'overwrite'` - Overwrite the source code with the new snapshot values.
*/
updateSourceMethod?: "overwrite"|"3way"|"patch";
/**
* The maximum number of concurrent worker processes to use for parallelizing tests. Can also be set as percentage of
@@ -1834,7 +1918,13 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
/**
* See [testConfig.updateSnapshots](https://playwright.dev/docs/api/class-testconfig#test-config-update-snapshots).
*/
updateSnapshots: "all"|"none"|"missing";
updateSnapshots: "all"|"changed"|"missing"|"none";
/**
* See
* [testConfig.updateSourceMethod](https://playwright.dev/docs/api/class-testconfig#test-config-update-source-method).
*/
updateSourceMethod: "overwrite"|"3way"|"patch";
/**
* Playwright version.
@@ -1849,7 +1939,7 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted';
type TestDetailsAnnotation = {
export type TestDetailsAnnotation = {
type: string;
description?: string;
};
@@ -1876,7 +1966,7 @@ type ConditionBody<TestArgs> = (args: TestArgs) => boolean;
* ```
*
*/
export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue> {
export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
/**
* Declares a test.
* - `test(title, body)`
@@ -5536,7 +5626,191 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
* @param body Step body.
* @param options
*/
step<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location }): Promise<T>;
step: {
/**
* Declares a test step that is shown in the report.
*
* **Usage**
*
* ```js
* import { test, expect } from '@playwright/test';
*
* test('test', async ({ page }) => {
* await test.step('Log in', async () => {
* // ...
* });
*
* await test.step('Outer step', async () => {
* // ...
* // You can nest steps inside each other.
* await test.step('Inner step', async () => {
* // ...
* });
* });
* });
* ```
*
* **Details**
*
* The method returns the value returned by the step callback.
*
* ```js
* import { test, expect } from '@playwright/test';
*
* test('test', async ({ page }) => {
* const user = await test.step('Log in', async () => {
* // ...
* return 'john';
* });
* expect(user).toBe('john');
* });
* ```
*
* **Decorator**
*
* You can use TypeScript method decorators to turn a method into a step. Each call to the decorated method will show
* up as a step in the report.
*
* ```js
* function step(target: Function, context: ClassMethodDecoratorContext) {
* return function replacementMethod(...args: any) {
* const name = this.constructor.name + '.' + (context.name as string);
* return test.step(name, async () => {
* return await target.call(this, ...args);
* });
* };
* }
*
* class LoginPage {
* constructor(readonly page: Page) {}
*
* @step
* async login() {
* const account = { username: 'Alice', password: 's3cr3t' };
* await this.page.getByLabel('Username or email address').fill(account.username);
* await this.page.getByLabel('Password').fill(account.password);
* await this.page.getByRole('button', { name: 'Sign in' }).click();
* await expect(this.page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
* }
* }
*
* test('example', async ({ page }) => {
* const loginPage = new LoginPage(page);
* await loginPage.login();
* });
* ```
*
* **Boxing**
*
* When something inside a step fails, you would usually see the error pointing to the exact action that failed. For
* example, consider the following login step:
*
* ```js
* async function login(page) {
* await test.step('login', async () => {
* const account = { username: 'Alice', password: 's3cr3t' };
* await page.getByLabel('Username or email address').fill(account.username);
* await page.getByLabel('Password').fill(account.password);
* await page.getByRole('button', { name: 'Sign in' }).click();
* await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
* });
* }
*
* test('example', async ({ page }) => {
* await page.goto('https://github.com/login');
* await login(page);
* });
* ```
*
* ```txt
* Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
* ... error details omitted ...
*
* 8 | await page.getByRole('button', { name: 'Sign in' }).click();
* > 9 | await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
* | ^
* 10 | });
* ```
*
* As we see above, the test may fail with an error pointing inside the step. If you would like the error to highlight
* the "login" step instead of its internals, use the `box` option. An error inside a boxed step points to the step
* call site.
*
* ```js
* async function login(page) {
* await test.step('login', async () => {
* // ...
* }, { box: true }); // Note the "box" option here.
* }
* ```
*
* ```txt
* Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
* ... error details omitted ...
*
* 14 | await page.goto('https://github.com/login');
* > 15 | await login(page);
* | ^
* 16 | });
* ```
*
* You can also create a TypeScript decorator for a boxed step, similar to a regular step decorator above:
*
* ```js
* function boxedStep(target: Function, context: ClassMethodDecoratorContext) {
* return function replacementMethod(...args: any) {
* const name = this.constructor.name + '.' + (context.name as string);
* return test.step(name, async () => {
* return await target.call(this, ...args);
* }, { box: true }); // Note the "box" option here.
* };
* }
*
* class LoginPage {
* constructor(readonly page: Page) {}
*
* @boxedStep
* async login() {
* // ....
* }
* }
*
* test('example', async ({ page }) => {
* const loginPage = new LoginPage(page);
* await loginPage.login(); // <-- Error will be reported on this line.
* });
* ```
*
* @param title Step name.
* @param body Step body.
* @param options
*/
<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
/**
* Mark a test step as "skip" to temporarily disable its execution, useful for steps that are currently failing and
* planned for a near-term fix. Playwright will not run the step.
*
* **Usage**
*
* You can declare a skipped step, and Playwright will not run it.
*
* ```js
* import { test, expect } from '@playwright/test';
*
* test('my test', async ({ page }) => {
* // ...
* await test.step.skip('not yet ready', async () => {
* // ...
* });
* });
* ```
*
* @param title Step name.
* @param body Step body.
* @param options
*/
skip(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
}
/**
* `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions).
*
@@ -5617,7 +5891,7 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
* Learn more about [fixtures](https://playwright.dev/docs/test-fixtures) and [parametrizing tests](https://playwright.dev/docs/test-parameterize).
* @param fixtures An object containing fixtures and/or options. Learn more about [fixtures format](https://playwright.dev/docs/test-fixtures).
*/
extend<T extends KeyValue, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
extend<T extends {}, W extends {} = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
/**
* Returns information about the currently running test. This method can only be called during the test execution,
* otherwise it throws.
@@ -5638,19 +5912,18 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
info(): TestInfo;
}
type KeyValue = { [key: string]: any };
export type TestFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) => Promise<void>, testInfo: TestInfo) => any;
export type WorkerFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) => Promise<void>, workerInfo: WorkerInfo) => any;
type TestFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | TestFixture<R, Args>;
type WorkerFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | WorkerFixture<R, Args>;
export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extends KeyValue = {}, PW extends KeyValue = {}> = {
export type TestFixture<R, Args extends {}> = (args: Args, use: (r: R) => Promise<void>, testInfo: TestInfo) => any;
export type WorkerFixture<R, Args extends {}> = (args: Args, use: (r: R) => Promise<void>, workerInfo: WorkerInfo) => any;
type TestFixtureValue<R, Args extends {}> = Exclude<R, Function> | TestFixture<R, Args>;
type WorkerFixtureValue<R, Args extends {}> = Exclude<R, Function> | WorkerFixture<R, Args>;
export type Fixtures<T extends {} = {}, W extends {} = {}, PT extends {} = {}, PW extends {} = {}> = {
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }];
} & {
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
} & {
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
[K in Exclude<keyof W, keyof PW | keyof PT>]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
} & {
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
[K in Exclude<keyof T, keyof PW | keyof PT>]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
};
type BrowserName = 'chromium' | 'firefox' | 'webkit';
@@ -5778,7 +6051,7 @@ export interface PlaywrightWorkerOptions {
/**
* Browser distribution channel.
*
* Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode).
* Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#chromium-new-headless-mode).
*
* Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).
@@ -6232,8 +6505,8 @@ export interface PlaywrightTestOptions {
javaScriptEnabled: boolean;
/**
* Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value,
* `Accept-Language` request header value as well as number and date formatting rules. Defaults to the system default
* locale. Learn more about emulation in our [emulation guide](https://playwright.dev/docs/emulation#locale--timezone).
* `Accept-Language` request header value as well as number and date formatting rules. Defaults to `en-US`. Learn more
* about emulation in our [emulation guide](https://playwright.dev/docs/emulation#locale--timezone).
*
* **Usage**
*
@@ -7472,8 +7745,8 @@ export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T, W>;
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig<T>[]): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig<T, W>[]): PlaywrightTestConfig<T, W>;
type MergedT<List> = List extends [TestType<infer T, any>, ...(infer Rest)] ? T & MergedT<Rest> : {};
type MergedW<List> = List extends [TestType<any, infer W>, ...(infer Rest)] ? W & MergedW<Rest> : {};
@@ -7589,8 +7862,21 @@ interface LocatorAssertions {
* @param options
*/
toBeChecked(options?: {
/**
* Provides state to assert for. Asserts for input to be checked by default. This option can't be used when
* [`indeterminate`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-be-checked-option-indeterminate)
* is set to true.
*/
checked?: boolean;
/**
* Asserts that the element is in the indeterminate (mixed) state. Only supported for checkboxes and radio buttons.
* This option can't be true when
* [`checked`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-be-checked-option-checked)
* is provided.
*/
indeterminate?: boolean;
/**
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/
@@ -7888,6 +8174,34 @@ interface LocatorAssertions {
timeout?: number;
}): Promise<void>;
/**
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with a given
* [aria errormessage](https://w3c.github.io/aria/#aria-errormessage).
*
* **Usage**
*
* ```js
* const locator = page.getByTestId('username-input');
* await expect(locator).toHaveAccessibleErrorMessage('Username is required.');
* ```
*
* @param errorMessage Expected accessible error message.
* @param options
*/
toHaveAccessibleErrorMessage(errorMessage: string|RegExp, options?: {
/**
* Whether to perform case-insensitive match.
* [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-error-message-option-ignore-case)
* option takes precedence over the corresponding regular expression flag if specified.
*/
ignoreCase?: boolean;
/**
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/
timeout?: number;
}): Promise<void>;
/**
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with a given
* [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
@@ -7967,21 +8281,24 @@ interface LocatorAssertions {
/**
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with given CSS classes.
* This needs to be a full match or using a relaxed regular expression.
* When a string is provided, it must fully match the element's `class` attribute. To match individual classes or
* perform partial matches, use a regular expression:
*
* **Usage**
*
* ```html
* <div class='selected row' id='component'></div>
* <div class='middle selected row' id='component'></div>
* ```
*
* ```js
* const locator = page.locator('#component');
* await expect(locator).toHaveClass(/selected/);
* await expect(locator).toHaveClass('selected row');
* await expect(locator).toHaveClass('middle selected row');
* await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/);
* ```
*
* Note that if array is passed as an expected value, entire lists of elements can be asserted:
* When an array is passed, the method asserts that the list of elements located matches the corresponding list of
* expected class values. Each element's class attribute is matched against the corresponding string or regular
* expression in the array:
*
* ```js
* const locator = page.locator('list > .component');
@@ -8439,6 +8756,34 @@ interface LocatorAssertions {
timeout?: number;
}): Promise<void>;
/**
* Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots).
*
* Snapshot is stored in a separate `.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate`
* and/or `snapshotPathTemplate` properties in the configuration file.
*
* **Usage**
*
* ```js
* await expect(page.locator('body')).toMatchAriaSnapshot();
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
* ```
*
* @param options
*/
toMatchAriaSnapshot(options?: {
/**
* Name of the snapshot to store in the snapshot folder corresponding to this test. Generates sequential names if not
* specified.
*/
name?: string;
/**
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/
timeout?: number;
}): Promise<void>;
/**
* Makes the assertion check for the opposite condition. For example, this code tests that the Locator doesn't contain
* text `"error"`:
@@ -9374,6 +9719,19 @@ interface TestConfigWebServer {
*/
timeout?: number;
/**
* How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal:
* 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit
* within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent.
* Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting
* down a Docker container requires `SIGTERM`.
*/
gracefulShutdown?: {
signal: "SIGINT"|"SIGTERM";
timeout: number;
};
/**
* The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the
* server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is

View File

@@ -691,6 +691,33 @@ export interface TestStep {
*/
titlePath(): Array<string>;
/**
* The list of files or buffers attached in the step execution through
* [testInfo.attach(name[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach).
*/
attachments: Array<{
/**
* Attachment name.
*/
name: string;
/**
* Content type of this attachment to properly present in the report, for example `'application/json'` or
* `'image/png'`.
*/
contentType: string;
/**
* Optional path on the filesystem to the attached file.
*/
path?: string;
/**
* Optional attachment body used instead of a file.
*/
body?: Buffer;
}>;
/**
* Step category to differentiate steps with different origin and verbosity. Built-in categories are:
* - `hook` for fixtures and hooks initialization and teardown