452 lines
20 KiB
JavaScript
452 lines
20 KiB
JavaScript
"use strict";
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.TestStepInfoImpl = exports.TestSkipError = exports.TestInfoImpl = exports.StepSkipError = void 0;
|
|
var _fs = _interopRequireDefault(require("fs"));
|
|
var _path = _interopRequireDefault(require("path"));
|
|
var _utils = require("playwright-core/lib/utils");
|
|
var _timeoutManager = require("./timeoutManager");
|
|
var _util = require("../util");
|
|
var _testTracing = require("./testTracing");
|
|
var _util2 = require("./util");
|
|
var _floatingPromiseScope = require("./floatingPromiseScope");
|
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
/**
|
|
* Copyright Microsoft Corporation. All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
class TestInfoImpl {
|
|
get error() {
|
|
return this.errors[0];
|
|
}
|
|
set error(e) {
|
|
if (e === undefined) throw new Error('Cannot assign testInfo.error undefined value!');
|
|
this.errors[0] = e;
|
|
}
|
|
get timeout() {
|
|
return this._timeoutManager.defaultSlot().timeout;
|
|
}
|
|
set timeout(timeout) {
|
|
// Ignored.
|
|
}
|
|
_deadlineForMatcher(timeout) {
|
|
const startTime = (0, _utils.monotonicTime)();
|
|
const matcherDeadline = timeout ? startTime + timeout : _timeoutManager.kMaxDeadline;
|
|
const testDeadline = this._timeoutManager.currentSlotDeadline() - 250;
|
|
const matcherMessage = `Timeout ${timeout}ms exceeded while waiting on the predicate`;
|
|
const testMessage = `Test timeout of ${this.timeout}ms exceeded`;
|
|
return {
|
|
deadline: Math.min(testDeadline, matcherDeadline),
|
|
timeoutMessage: testDeadline < matcherDeadline ? testMessage : matcherMessage
|
|
};
|
|
}
|
|
static _defaultDeadlineForMatcher(timeout) {
|
|
return {
|
|
deadline: timeout ? (0, _utils.monotonicTime)() + timeout : 0,
|
|
timeoutMessage: `Timeout ${timeout}ms exceeded while waiting on the predicate`
|
|
};
|
|
}
|
|
constructor(configInternal, projectInternal, workerParams, test, retry, onStepBegin, onStepEnd, onAttach) {
|
|
var _test$id, _test$_requireFile, _test$title, _test$titlePath, _test$location$file, _test$location$line, _test$location$column, _test$tags, _test$fn, _test$expectedStatus;
|
|
this._onStepBegin = void 0;
|
|
this._onStepEnd = void 0;
|
|
this._onAttach = void 0;
|
|
this._timeoutManager = void 0;
|
|
this._startTime = void 0;
|
|
this._startWallTime = void 0;
|
|
this._tracing = void 0;
|
|
this._floatingPromiseScope = new _floatingPromiseScope.FloatingPromiseScope();
|
|
this._wasInterrupted = false;
|
|
this._lastStepId = 0;
|
|
this._requireFile = void 0;
|
|
this._projectInternal = void 0;
|
|
this._configInternal = void 0;
|
|
this._steps = [];
|
|
this._stepMap = new Map();
|
|
this._onDidFinishTestFunction = void 0;
|
|
this._hasNonRetriableError = false;
|
|
this._hasUnhandledError = false;
|
|
this._allowSkips = false;
|
|
// ------------ TestInfo fields ------------
|
|
this.testId = void 0;
|
|
this.repeatEachIndex = void 0;
|
|
this.retry = void 0;
|
|
this.workerIndex = void 0;
|
|
this.parallelIndex = void 0;
|
|
this.project = void 0;
|
|
this.config = void 0;
|
|
this.title = void 0;
|
|
this.titlePath = void 0;
|
|
this.file = void 0;
|
|
this.line = void 0;
|
|
this.tags = void 0;
|
|
this.column = void 0;
|
|
this.fn = void 0;
|
|
this.expectedStatus = void 0;
|
|
this.duration = 0;
|
|
this.annotations = [];
|
|
this.attachments = [];
|
|
this.status = 'passed';
|
|
this.snapshotSuffix = '';
|
|
this.outputDir = void 0;
|
|
this.snapshotDir = void 0;
|
|
this.errors = [];
|
|
this._attachmentsPush = void 0;
|
|
this.testId = (_test$id = test === null || test === void 0 ? void 0 : test.id) !== null && _test$id !== void 0 ? _test$id : '';
|
|
this._onStepBegin = onStepBegin;
|
|
this._onStepEnd = onStepEnd;
|
|
this._onAttach = onAttach;
|
|
this._startTime = (0, _utils.monotonicTime)();
|
|
this._startWallTime = Date.now();
|
|
this._requireFile = (_test$_requireFile = test === null || test === void 0 ? void 0 : test._requireFile) !== null && _test$_requireFile !== void 0 ? _test$_requireFile : '';
|
|
this.repeatEachIndex = workerParams.repeatEachIndex;
|
|
this.retry = retry;
|
|
this.workerIndex = workerParams.workerIndex;
|
|
this.parallelIndex = workerParams.parallelIndex;
|
|
this._projectInternal = projectInternal;
|
|
this.project = projectInternal.project;
|
|
this._configInternal = configInternal;
|
|
this.config = configInternal.config;
|
|
this.title = (_test$title = test === null || test === void 0 ? void 0 : test.title) !== null && _test$title !== void 0 ? _test$title : '';
|
|
this.titlePath = (_test$titlePath = test === null || test === void 0 ? void 0 : test.titlePath()) !== null && _test$titlePath !== void 0 ? _test$titlePath : [];
|
|
this.file = (_test$location$file = test === null || test === void 0 ? void 0 : test.location.file) !== null && _test$location$file !== void 0 ? _test$location$file : '';
|
|
this.line = (_test$location$line = test === null || test === void 0 ? void 0 : test.location.line) !== null && _test$location$line !== void 0 ? _test$location$line : 0;
|
|
this.column = (_test$location$column = test === null || test === void 0 ? void 0 : test.location.column) !== null && _test$location$column !== void 0 ? _test$location$column : 0;
|
|
this.tags = (_test$tags = test === null || test === void 0 ? void 0 : test.tags) !== null && _test$tags !== void 0 ? _test$tags : [];
|
|
this.fn = (_test$fn = test === null || test === void 0 ? void 0 : test.fn) !== null && _test$fn !== void 0 ? _test$fn : () => {};
|
|
this.expectedStatus = (_test$expectedStatus = test === null || test === void 0 ? void 0 : test.expectedStatus) !== null && _test$expectedStatus !== void 0 ? _test$expectedStatus : 'skipped';
|
|
this._timeoutManager = new _timeoutManager.TimeoutManager(this.project.timeout);
|
|
if (configInternal.configCLIOverrides.debug) this._setDebugMode();
|
|
this.outputDir = (() => {
|
|
const relativeTestFilePath = _path.default.relative(this.project.testDir, this._requireFile.replace(/\.(spec|test)\.(js|ts|jsx|tsx|mjs|mts|cjs|cts)$/, ''));
|
|
const sanitizedRelativePath = relativeTestFilePath.replace(process.platform === 'win32' ? new RegExp('\\\\', 'g') : new RegExp('/', 'g'), '-');
|
|
const fullTitleWithoutSpec = this.titlePath.slice(1).join(' ');
|
|
let testOutputDir = (0, _util.trimLongString)(sanitizedRelativePath + '-' + (0, _utils.sanitizeForFilePath)(fullTitleWithoutSpec), _util.windowsFilesystemFriendlyLength);
|
|
if (projectInternal.id) testOutputDir += '-' + (0, _utils.sanitizeForFilePath)(projectInternal.id);
|
|
if (this.retry) testOutputDir += '-retry' + this.retry;
|
|
if (this.repeatEachIndex) testOutputDir += '-repeat' + this.repeatEachIndex;
|
|
return _path.default.join(this.project.outputDir, testOutputDir);
|
|
})();
|
|
this.snapshotDir = (() => {
|
|
const relativeTestFilePath = _path.default.relative(this.project.testDir, this._requireFile);
|
|
return _path.default.join(this.project.snapshotDir, relativeTestFilePath + '-snapshots');
|
|
})();
|
|
this._attachmentsPush = this.attachments.push.bind(this.attachments);
|
|
this.attachments.push = (...attachments) => {
|
|
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);
|
|
}
|
|
_modifier(type, modifierArgs) {
|
|
if (typeof modifierArgs[1] === 'function') {
|
|
throw new Error(['It looks like you are calling test.skip() inside the test and pass a callback.', 'Pass a condition instead and optional description instead:', `test('my test', async ({ page, isMobile }) => {`, ` test.skip(isMobile, 'This test is not applicable on mobile');`, `});`].join('\n'));
|
|
}
|
|
if (modifierArgs.length >= 1 && !modifierArgs[0]) return;
|
|
const description = modifierArgs[1];
|
|
this.annotations.push({
|
|
type,
|
|
description
|
|
});
|
|
if (type === 'slow') {
|
|
this._timeoutManager.slow();
|
|
} else if (type === 'skip' || type === 'fixme') {
|
|
this.expectedStatus = 'skipped';
|
|
throw new TestSkipError('Test is skipped: ' + (description || ''));
|
|
} else if (type === 'fail') {
|
|
if (this.expectedStatus !== 'skipped') this.expectedStatus = 'failed';
|
|
}
|
|
}
|
|
_findLastPredefinedStep(steps) {
|
|
// Find the deepest predefined step that has not finished yet.
|
|
for (let i = steps.length - 1; i >= 0; i--) {
|
|
const child = this._findLastPredefinedStep(steps[i].steps);
|
|
if (child) return child;
|
|
if ((steps[i].category === 'hook' || steps[i].category === 'fixture') && !steps[i].endWallTime) return steps[i];
|
|
}
|
|
}
|
|
_parentStep() {
|
|
var _currentZone$data;
|
|
return (_currentZone$data = (0, _utils.currentZone)().data('stepZone')) !== null && _currentZone$data !== void 0 ? _currentZone$data : this._findLastPredefinedStep(this._steps);
|
|
}
|
|
_addStep(data, parentStep) {
|
|
var _parentStep, _parentStep2;
|
|
const stepId = `${data.category}@${++this._lastStepId}`;
|
|
if (data.category === 'hook' || data.category === 'fixture') {
|
|
// Predefined steps form a fixed hierarchy - use the current one as parent.
|
|
parentStep = this._findLastPredefinedStep(this._steps);
|
|
} else {
|
|
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;
|
|
if (!data.boxedStack && data.box) {
|
|
data.boxedStack = filteredStack.slice(1);
|
|
data.location = data.location || data.boxedStack[0];
|
|
}
|
|
data.location = data.location || filteredStack[0];
|
|
const attachmentIndices = [];
|
|
const step = {
|
|
stepId,
|
|
...data,
|
|
steps: [],
|
|
attachmentIndices,
|
|
info: new TestStepInfoImpl(this, stepId),
|
|
complete: result => {
|
|
if (step.endWallTime) return;
|
|
step.endWallTime = Date.now();
|
|
if (result.error) {
|
|
var _result$error;
|
|
if (typeof result.error === 'object' && !((_result$error = result.error) !== null && _result$error !== void 0 && _result$error[stepSymbol])) result.error[stepSymbol] = step;
|
|
const error = (0, _util2.testInfoError)(result.error);
|
|
if (data.boxedStack) error.stack = `${error.message}\n${(0, _utils.stringifyStackFrames)(data.boxedStack).join('\n')}`;
|
|
step.error = error;
|
|
}
|
|
if (!step.error) {
|
|
// Soft errors inside try/catch will make the test fail.
|
|
// In order to locate the failing step, we are marking all the parent
|
|
// steps as failing unconditionally.
|
|
for (const childStep of step.steps) {
|
|
if (childStep.error && childStep.infectParentStepsWithError) {
|
|
step.error = childStep.error;
|
|
step.infectParentStepsWithError = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
const payload = {
|
|
testId: this.testId,
|
|
stepId,
|
|
wallTime: step.endWallTime,
|
|
error: step.error,
|
|
suggestedRebaseline: result.suggestedRebaseline,
|
|
annotations: step.info.annotations
|
|
};
|
|
this._onStepEnd(payload);
|
|
const errorForTrace = step.error ? {
|
|
name: '',
|
|
message: step.error.message || '',
|
|
stack: step.error.stack
|
|
} : undefined;
|
|
const attachments = attachmentIndices.map(i => this.attachments[i]);
|
|
this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments, step.info.annotations);
|
|
}
|
|
};
|
|
const parentStepList = parentStep ? parentStep.steps : this._steps;
|
|
parentStepList.push(step);
|
|
this._stepMap.set(stepId, step);
|
|
const payload = {
|
|
testId: this.testId,
|
|
stepId,
|
|
parentStepId: parentStep ? parentStep.stepId : undefined,
|
|
title: data.title,
|
|
category: data.category,
|
|
wallTime: Date.now(),
|
|
location: data.location
|
|
};
|
|
this._onStepBegin(payload);
|
|
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() {
|
|
// Mark as interrupted so we can ignore TimeoutError thrown by interrupt() call.
|
|
this._wasInterrupted = true;
|
|
this._timeoutManager.interrupt();
|
|
// Do not overwrite existing failure (for example, unhandled rejection) with "interrupted".
|
|
if (this.status === 'passed') this.status = 'interrupted';
|
|
}
|
|
_failWithError(error) {
|
|
if (this.status === 'passed' || this.status === 'skipped') this.status = error instanceof _timeoutManager.TimeoutManagerError ? 'timedOut' : 'failed';
|
|
const serialized = (0, _util2.testInfoError)(error);
|
|
const step = typeof error === 'object' ? error === null || error === void 0 ? void 0 : error[stepSymbol] : undefined;
|
|
if (step && step.boxedStack) serialized.stack = `${error.name}: ${error.message}\n${(0, _utils.stringifyStackFrames)(step.boxedStack).join('\n')}`;
|
|
this.errors.push(serialized);
|
|
this._tracing.appendForError(serialized);
|
|
}
|
|
async _runAsStep(stepInfo, cb) {
|
|
const step = this._addStep(stepInfo);
|
|
try {
|
|
await cb();
|
|
step.complete({});
|
|
} catch (error) {
|
|
step.complete({
|
|
error
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
async _runWithTimeout(runnable, cb) {
|
|
try {
|
|
await this._timeoutManager.withRunnable(runnable, async () => {
|
|
try {
|
|
await cb();
|
|
} catch (e) {
|
|
if (this._allowSkips && e instanceof TestSkipError) {
|
|
if (this.status === 'passed') this.status = 'skipped';
|
|
} else {
|
|
// Unfortunately, we have to handle user errors and timeout errors differently.
|
|
// Consider the following scenario:
|
|
// - locator.click times out
|
|
// - all steps containing the test function finish with TimeoutManagerError
|
|
// - test finishes, the page is closed and this triggers locator.click error
|
|
// - we would like to present the locator.click error to the user
|
|
// - therefore, we need a try/catch inside the "run with timeout" block and capture the error
|
|
this._failWithError(e);
|
|
}
|
|
throw e;
|
|
}
|
|
});
|
|
} catch (error) {
|
|
// When interrupting, we arrive here with a TimeoutManagerError, but we should not
|
|
// consider it a timeout.
|
|
if (!this._wasInterrupted && error instanceof _timeoutManager.TimeoutManagerError) this._failWithError(error);
|
|
throw error;
|
|
}
|
|
}
|
|
_isFailure() {
|
|
return this.status !== 'skipped' && this.status !== this.expectedStatus;
|
|
}
|
|
_currentHookType() {
|
|
const type = this._timeoutManager.currentSlotType();
|
|
return ['beforeAll', 'afterAll', 'beforeEach', 'afterEach'].includes(type) ? type : undefined;
|
|
}
|
|
_setDebugMode() {
|
|
this._timeoutManager.setIgnoreTimeouts();
|
|
}
|
|
|
|
// ------------ TestInfo methods ------------
|
|
|
|
async attach(name, options = {}) {
|
|
const step = this._addStep({
|
|
title: `attach "${name}"`,
|
|
category: 'attach'
|
|
});
|
|
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 this._tracing.appendTopLevelAttachment(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'),
|
|
stepId
|
|
});
|
|
}
|
|
outputPath(...pathSegments) {
|
|
const outputPath = this._getOutputPath(...pathSegments);
|
|
_fs.default.mkdirSync(this.outputDir, {
|
|
recursive: true
|
|
});
|
|
return outputPath;
|
|
}
|
|
_getOutputPath(...pathSegments) {
|
|
const joinedPath = _path.default.join(...pathSegments);
|
|
const outputPath = (0, _util.getContainedPath)(this.outputDir, joinedPath);
|
|
if (outputPath) return outputPath;
|
|
throw new Error(`The outputPath is not allowed outside of the parent directory. Please fix the defined path.\n\n\toutputPath: ${joinedPath}`);
|
|
}
|
|
_fsSanitizedTestName() {
|
|
const fullTitleWithoutSpec = this.titlePath.slice(1).join(' ');
|
|
return (0, _utils.sanitizeForFilePath)((0, _util.trimLongString)(fullTitleWithoutSpec));
|
|
}
|
|
_resolveSnapshotPath(template, defaultTemplate, pathSegments, extension) {
|
|
const subPath = _path.default.join(...pathSegments);
|
|
const dir = _path.default.dirname(subPath);
|
|
const ext = extension !== null && extension !== void 0 ? extension : _path.default.extname(subPath);
|
|
const name = _path.default.basename(subPath, ext);
|
|
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 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(dir, name)).replace(/\{(.)?ext\}/g, ext ? '$1' + 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);
|
|
}
|
|
fixme(...args) {
|
|
this._modifier('fixme', args);
|
|
}
|
|
fail(...args) {
|
|
this._modifier('fail', args);
|
|
}
|
|
slow(...args) {
|
|
this._modifier('slow', args);
|
|
}
|
|
setTimeout(timeout) {
|
|
this._timeoutManager.setTimeout(timeout);
|
|
}
|
|
}
|
|
exports.TestInfoImpl = TestInfoImpl;
|
|
class TestStepInfoImpl {
|
|
constructor(testInfo, stepId) {
|
|
this.annotations = [];
|
|
this._testInfo = void 0;
|
|
this._stepId = void 0;
|
|
this._testInfo = testInfo;
|
|
this._stepId = stepId;
|
|
}
|
|
async _runStepBody(skip, body) {
|
|
if (skip) {
|
|
this.annotations.push({
|
|
type: 'skip'
|
|
});
|
|
return undefined;
|
|
}
|
|
try {
|
|
return await body(this);
|
|
} catch (e) {
|
|
if (e instanceof StepSkipError) return undefined;
|
|
throw e;
|
|
}
|
|
}
|
|
_attachToStep(attachment) {
|
|
this._testInfo._attach(attachment, this._stepId);
|
|
}
|
|
async attach(name, options) {
|
|
this._attachToStep(await (0, _util.normalizeAndSaveAttachment)(this._testInfo.outputPath(), name, options));
|
|
}
|
|
skip(...args) {
|
|
// skip();
|
|
// skip(condition: boolean, description: string);
|
|
if (args.length > 0 && !args[0]) return;
|
|
const description = args[1];
|
|
this.annotations.push({
|
|
type: 'skip',
|
|
description
|
|
});
|
|
throw new StepSkipError(description);
|
|
}
|
|
}
|
|
exports.TestStepInfoImpl = TestStepInfoImpl;
|
|
class TestSkipError extends Error {}
|
|
exports.TestSkipError = TestSkipError;
|
|
class StepSkipError extends Error {}
|
|
exports.StepSkipError = StepSkipError;
|
|
const stepSymbol = Symbol('step'); |