fix(test runner): expose real stack traces and speed up locations (#7265)
Stop wrapping/prepending error messages so that we do not loose the stack trace. For this, update a few manually thrown errors with better messages (usually including a file path). Speed up locations by doing manual `sourceMapSupport.wrapCallSite()` for a single call site. Performance gain in the runner process with 100 files x 100 tests each: - 25% on the fresh run without babel cache; - 80% on the cached run where babel is almost instant. Also some obvious cleanups around stack traces (removing unused code).
This commit is contained in:
parent
f342d8b0b1
commit
4c6fa42810
|
|
@ -64,7 +64,7 @@ export function addTestCommand(program: commander.CommanderStatic) {
|
||||||
try {
|
try {
|
||||||
await runTests(args, opts);
|
await runTests(args, opts);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e.toString());
|
console.error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { errorWithCallLocation, formatLocation, prependErrorMessage, wrapInPromise } from './util';
|
import { wrapInPromise } from './util';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import { FixturesWithLocation, Location } from './types';
|
import { FixturesWithLocation, Location } from './types';
|
||||||
|
|
||||||
|
|
@ -56,9 +56,7 @@ class Fixture {
|
||||||
|
|
||||||
const params: { [key: string]: any } = {};
|
const params: { [key: string]: any } = {};
|
||||||
for (const name of this.registration.deps) {
|
for (const name of this.registration.deps) {
|
||||||
const registration = this.runner.pool!.resolveDependency(this.registration, name);
|
const registration = this.runner.pool!.resolveDependency(this.registration, name)!;
|
||||||
if (!registration)
|
|
||||||
throw errorWithCallLocation(`Unknown fixture "${name}"`);
|
|
||||||
const dep = await this.runner.setupFixtureForRegistration(registration, info);
|
const dep = await this.runner.setupFixtureForRegistration(registration, info);
|
||||||
dep.usages.add(this);
|
dep.usages.add(this);
|
||||||
params[name] = dep.value;
|
params[name] = dep.value;
|
||||||
|
|
@ -71,7 +69,7 @@ class Fixture {
|
||||||
const teardownFence = new Promise(f => this._teardownFenceCallback = f);
|
const teardownFence = new Promise(f => this._teardownFenceCallback = f);
|
||||||
this._tearDownComplete = wrapInPromise(this.registration.fn(params, async (value: any) => {
|
this._tearDownComplete = wrapInPromise(this.registration.fn(params, async (value: any) => {
|
||||||
if (called)
|
if (called)
|
||||||
throw errorWithCallLocation(`Cannot provide fixture value for the second time`);
|
throw new Error(`Cannot provide fixture value for the second time`);
|
||||||
called = true;
|
called = true;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
setupFenceFulfill();
|
setupFenceFulfill();
|
||||||
|
|
@ -111,40 +109,35 @@ export class FixturePool {
|
||||||
this.registrations = new Map(parentPool ? parentPool.registrations : []);
|
this.registrations = new Map(parentPool ? parentPool.registrations : []);
|
||||||
|
|
||||||
for (const { fixtures, location } of fixturesList) {
|
for (const { fixtures, location } of fixturesList) {
|
||||||
try {
|
for (const entry of Object.entries(fixtures)) {
|
||||||
for (const entry of Object.entries(fixtures)) {
|
const name = entry[0];
|
||||||
const name = entry[0];
|
let value = entry[1];
|
||||||
let value = entry[1];
|
let options: { auto: boolean, scope: FixtureScope } | undefined;
|
||||||
let options: { auto: boolean, scope: FixtureScope } | undefined;
|
if (Array.isArray(value) && typeof value[1] === 'object' && ('scope' in value[1] || 'auto' in value[1])) {
|
||||||
if (Array.isArray(value) && typeof value[1] === 'object' && ('scope' in value[1] || 'auto' in value[1])) {
|
options = {
|
||||||
options = {
|
auto: !!value[1].auto,
|
||||||
auto: !!value[1].auto,
|
scope: value[1].scope || 'test'
|
||||||
scope: value[1].scope || 'test'
|
};
|
||||||
};
|
value = value[0];
|
||||||
value = value[0];
|
|
||||||
}
|
|
||||||
const fn = value as (Function | any);
|
|
||||||
|
|
||||||
const previous = this.registrations.get(name);
|
|
||||||
if (previous && options) {
|
|
||||||
if (previous.scope !== options.scope)
|
|
||||||
throw errorWithLocations(`Fixture "${name}" has already been registered as a { scope: '${previous.scope}' } fixture.`, { location, name}, previous);
|
|
||||||
if (previous.auto !== options.auto)
|
|
||||||
throw errorWithLocations(`Fixture "${name}" has already been registered as a { auto: '${previous.scope}' } fixture.`, { location, name }, previous);
|
|
||||||
} else if (previous) {
|
|
||||||
options = { auto: previous.auto, scope: previous.scope };
|
|
||||||
} else if (!options) {
|
|
||||||
options = { auto: false, scope: 'test' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const deps = fixtureParameterNames(fn, location);
|
|
||||||
const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, deps, super: previous };
|
|
||||||
registrationId(registration);
|
|
||||||
this.registrations.set(name, registration);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
const fn = value as (Function | any);
|
||||||
prependErrorMessage(e, `Error processing fixtures at ${formatLocation(location)}:\n`);
|
|
||||||
throw e;
|
const previous = this.registrations.get(name);
|
||||||
|
if (previous && options) {
|
||||||
|
if (previous.scope !== options.scope)
|
||||||
|
throw errorWithLocations(`Fixture "${name}" has already been registered as a { scope: '${previous.scope}' } fixture.`, { location, name }, previous);
|
||||||
|
if (previous.auto !== options.auto)
|
||||||
|
throw errorWithLocations(`Fixture "${name}" has already been registered as a { auto: '${previous.scope}' } fixture.`, { location, name }, previous);
|
||||||
|
} else if (previous) {
|
||||||
|
options = { auto: previous.auto, scope: previous.scope };
|
||||||
|
} else if (!options) {
|
||||||
|
options = { auto: false, scope: 'test' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const deps = fixtureParameterNames(fn, location);
|
||||||
|
const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, deps, super: previous };
|
||||||
|
registrationId(registration);
|
||||||
|
this.registrations.set(name, registration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,9 +240,7 @@ export class FixtureRunner {
|
||||||
const names = fixtureParameterNames(fn, { file: '<unused>', line: 1, column: 1 });
|
const names = fixtureParameterNames(fn, { file: '<unused>', line: 1, column: 1 });
|
||||||
const params: { [key: string]: any } = {};
|
const params: { [key: string]: any } = {};
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
const registration = this.pool!.registrations.get(name);
|
const registration = this.pool!.registrations.get(name)!;
|
||||||
if (!registration)
|
|
||||||
throw errorWithCallLocation('Unknown fixture: ' + name);
|
|
||||||
const fixture = await this.setupFixtureForRegistration(registration, info);
|
const fixture = await this.setupFixtureForRegistration(registration, info);
|
||||||
params[name] = fixture.value;
|
params[name] = fixture.value;
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +346,9 @@ function errorWithLocations(message: string, ...defined: { location: Location, n
|
||||||
prefix = `"${name}" `;
|
prefix = `"${name}" `;
|
||||||
message += `\n ${prefix}defined at ${formatLocation(location)}`;
|
message += `\n ${prefix}defined at ${formatLocation(location)}`;
|
||||||
}
|
}
|
||||||
const error = new Error(message);
|
return new Error(message);
|
||||||
error.stack = 'Error: ' + message + '\n';
|
}
|
||||||
return error;
|
|
||||||
|
function formatLocation(location: Location) {
|
||||||
|
return location.file + ':' + location.line + ':' + location.column;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import { installTransform } from './transform';
|
import { installTransform } from './transform';
|
||||||
import type { FullConfig, Config, FullProject, Project, ReporterDescription, PreserveOutput } from './types';
|
import type { FullConfig, Config, FullProject, Project, ReporterDescription, PreserveOutput } from './types';
|
||||||
import { errorWithCallLocation, isRegExp, mergeObjects, prependErrorMessage } from './util';
|
import { isRegExp, mergeObjects } from './util';
|
||||||
import { setCurrentlyLoadingFileSuite } from './globals';
|
import { setCurrentlyLoadingFileSuite } from './globals';
|
||||||
import { Suite } from './test';
|
import { Suite } from './test';
|
||||||
import { SerializedLoaderData } from './ipc';
|
import { SerializedLoaderData } from './ipc';
|
||||||
|
|
@ -61,9 +61,6 @@ export class Loader {
|
||||||
const rawConfig = { ...config };
|
const rawConfig = { ...config };
|
||||||
this._processConfigObject(path.dirname(file));
|
this._processConfigObject(path.dirname(file));
|
||||||
return rawConfig;
|
return rawConfig;
|
||||||
} catch (e) {
|
|
||||||
prependErrorMessage(e, `Error while reading ${file}:\n`);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
} finally {
|
||||||
revertBabelRequire();
|
revertBabelRequire();
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +72,7 @@ export class Loader {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _processConfigObject(rootDir: string) {
|
private _processConfigObject(rootDir: string) {
|
||||||
validateConfig(this._config);
|
validateConfig(this._configFile || '<default config>', this._config);
|
||||||
|
|
||||||
// Resolve script hooks relative to the root dir.
|
// Resolve script hooks relative to the root dir.
|
||||||
if (this._config.globalSetup)
|
if (this._config.globalSetup)
|
||||||
|
|
@ -123,9 +120,6 @@ export class Loader {
|
||||||
require(file);
|
require(file);
|
||||||
this._fileSuites.set(file, suite);
|
this._fileSuites.set(file, suite);
|
||||||
return suite;
|
return suite;
|
||||||
} catch (e) {
|
|
||||||
prependErrorMessage(e, `Error while reading ${file}:\n`);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
} finally {
|
||||||
revertBabelRequire();
|
revertBabelRequire();
|
||||||
setCurrentlyLoadingFileSuite(undefined);
|
setCurrentlyLoadingFileSuite(undefined);
|
||||||
|
|
@ -139,11 +133,8 @@ export class Loader {
|
||||||
if (hook && typeof hook === 'object' && ('default' in hook))
|
if (hook && typeof hook === 'object' && ('default' in hook))
|
||||||
hook = hook['default'];
|
hook = hook['default'];
|
||||||
if (typeof hook !== 'function')
|
if (typeof hook !== 'function')
|
||||||
throw errorWithCallLocation(`${name} file must export a single function.`);
|
throw errorWithFile(file, `${name} file must export a single function.`);
|
||||||
return hook;
|
return hook;
|
||||||
} catch (e) {
|
|
||||||
prependErrorMessage(e, `Error while reading ${file}:\n`);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
} finally {
|
||||||
revertBabelRequire();
|
revertBabelRequire();
|
||||||
}
|
}
|
||||||
|
|
@ -156,11 +147,8 @@ export class Loader {
|
||||||
if (func && typeof func === 'object' && ('default' in func))
|
if (func && typeof func === 'object' && ('default' in func))
|
||||||
func = func['default'];
|
func = func['default'];
|
||||||
if (typeof func !== 'function')
|
if (typeof func !== 'function')
|
||||||
throw errorWithCallLocation(`Reporter file "${file}" must export a single class.`);
|
throw errorWithFile(file, `reporter file must export a single class.`);
|
||||||
return func;
|
return func;
|
||||||
} catch (e) {
|
|
||||||
prependErrorMessage(e, `Error while reading ${file}:\n`);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
} finally {
|
||||||
revertBabelRequire();
|
revertBabelRequire();
|
||||||
}
|
}
|
||||||
|
|
@ -227,40 +215,44 @@ function toReporters(reporters: 'dot' | 'line' | 'list' | 'junit' | 'json' | 'nu
|
||||||
return reporters;
|
return reporters;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateConfig(config: Config) {
|
function errorWithFile(file: string, message: string) {
|
||||||
if (typeof config !== 'object' || !config)
|
return new Error(`${file}: ${message}`);
|
||||||
throw new Error(`Configuration file must export a single object`);
|
}
|
||||||
|
|
||||||
validateProject(config, 'config');
|
function validateConfig(file: string, config: Config) {
|
||||||
|
if (typeof config !== 'object' || !config)
|
||||||
|
throw errorWithFile(file, `Configuration file must export a single object`);
|
||||||
|
|
||||||
|
validateProject(file, config, 'config');
|
||||||
|
|
||||||
if ('forbidOnly' in config && config.forbidOnly !== undefined) {
|
if ('forbidOnly' in config && config.forbidOnly !== undefined) {
|
||||||
if (typeof config.forbidOnly !== 'boolean')
|
if (typeof config.forbidOnly !== 'boolean')
|
||||||
throw new Error(`config.forbidOnly must be a boolean`);
|
throw errorWithFile(file, `config.forbidOnly must be a boolean`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('globalSetup' in config && config.globalSetup !== undefined) {
|
if ('globalSetup' in config && config.globalSetup !== undefined) {
|
||||||
if (typeof config.globalSetup !== 'string')
|
if (typeof config.globalSetup !== 'string')
|
||||||
throw new Error(`config.globalSetup must be a string`);
|
throw errorWithFile(file, `config.globalSetup must be a string`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('globalTeardown' in config && config.globalTeardown !== undefined) {
|
if ('globalTeardown' in config && config.globalTeardown !== undefined) {
|
||||||
if (typeof config.globalTeardown !== 'string')
|
if (typeof config.globalTeardown !== 'string')
|
||||||
throw new Error(`config.globalTeardown must be a string`);
|
throw errorWithFile(file, `config.globalTeardown must be a string`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('globalTimeout' in config && config.globalTimeout !== undefined) {
|
if ('globalTimeout' in config && config.globalTimeout !== undefined) {
|
||||||
if (typeof config.globalTimeout !== 'number' || config.globalTimeout < 0)
|
if (typeof config.globalTimeout !== 'number' || config.globalTimeout < 0)
|
||||||
throw new Error(`config.globalTimeout must be a non-negative number`);
|
throw errorWithFile(file, `config.globalTimeout must be a non-negative number`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('grep' in config && config.grep !== undefined) {
|
if ('grep' in config && config.grep !== undefined) {
|
||||||
if (Array.isArray(config.grep)) {
|
if (Array.isArray(config.grep)) {
|
||||||
config.grep.forEach((item, index) => {
|
config.grep.forEach((item, index) => {
|
||||||
if (!isRegExp(item))
|
if (!isRegExp(item))
|
||||||
throw new Error(`config.grep[${index}] must be a RegExp`);
|
throw errorWithFile(file, `config.grep[${index}] must be a RegExp`);
|
||||||
});
|
});
|
||||||
} else if (!isRegExp(config.grep)) {
|
} else if (!isRegExp(config.grep)) {
|
||||||
throw new Error(`config.grep must be a RegExp`);
|
throw errorWithFile(file, `config.grep must be a RegExp`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,115 +260,115 @@ function validateConfig(config: Config) {
|
||||||
if (Array.isArray(config.grepInvert)) {
|
if (Array.isArray(config.grepInvert)) {
|
||||||
config.grepInvert.forEach((item, index) => {
|
config.grepInvert.forEach((item, index) => {
|
||||||
if (!isRegExp(item))
|
if (!isRegExp(item))
|
||||||
throw new Error(`config.grepInvert[${index}] must be a RegExp`);
|
throw errorWithFile(file, `config.grepInvert[${index}] must be a RegExp`);
|
||||||
});
|
});
|
||||||
} else if (!isRegExp(config.grepInvert)) {
|
} else if (!isRegExp(config.grepInvert)) {
|
||||||
throw new Error(`config.grep must be a RegExp`);
|
throw errorWithFile(file, `config.grep must be a RegExp`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('maxFailures' in config && config.maxFailures !== undefined) {
|
if ('maxFailures' in config && config.maxFailures !== undefined) {
|
||||||
if (typeof config.maxFailures !== 'number' || config.maxFailures < 0)
|
if (typeof config.maxFailures !== 'number' || config.maxFailures < 0)
|
||||||
throw new Error(`config.maxFailures must be a non-negative number`);
|
throw errorWithFile(file, `config.maxFailures must be a non-negative number`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('preserveOutput' in config && config.preserveOutput !== undefined) {
|
if ('preserveOutput' in config && config.preserveOutput !== undefined) {
|
||||||
if (typeof config.preserveOutput !== 'string' || !['always', 'never', 'failures-only'].includes(config.preserveOutput))
|
if (typeof config.preserveOutput !== 'string' || !['always', 'never', 'failures-only'].includes(config.preserveOutput))
|
||||||
throw new Error(`config.preserveOutput must be one of "always", "never" or "failures-only"`);
|
throw errorWithFile(file, `config.preserveOutput must be one of "always", "never" or "failures-only"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('projects' in config && config.projects !== undefined) {
|
if ('projects' in config && config.projects !== undefined) {
|
||||||
if (!Array.isArray(config.projects))
|
if (!Array.isArray(config.projects))
|
||||||
throw new Error(`config.projects must be an array`);
|
throw errorWithFile(file, `config.projects must be an array`);
|
||||||
config.projects.forEach((project, index) => {
|
config.projects.forEach((project, index) => {
|
||||||
validateProject(project, `config.projects[${index}]`);
|
validateProject(file, project, `config.projects[${index}]`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('quiet' in config && config.quiet !== undefined) {
|
if ('quiet' in config && config.quiet !== undefined) {
|
||||||
if (typeof config.quiet !== 'boolean')
|
if (typeof config.quiet !== 'boolean')
|
||||||
throw new Error(`config.quiet must be a boolean`);
|
throw errorWithFile(file, `config.quiet must be a boolean`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('reporter' in config && config.reporter !== undefined) {
|
if ('reporter' in config && config.reporter !== undefined) {
|
||||||
if (Array.isArray(config.reporter)) {
|
if (Array.isArray(config.reporter)) {
|
||||||
config.reporter.forEach((item, index) => {
|
config.reporter.forEach((item, index) => {
|
||||||
if (!Array.isArray(item) || item.length <= 0 || item.length > 2 || typeof item[0] !== 'string')
|
if (!Array.isArray(item) || item.length <= 0 || item.length > 2 || typeof item[0] !== 'string')
|
||||||
throw new Error(`config.reporter[${index}] must be a tuple [name, optionalArgument]`);
|
throw errorWithFile(file, `config.reporter[${index}] must be a tuple [name, optionalArgument]`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const builtinReporters = ['dot', 'line', 'list', 'junit', 'json', 'null'];
|
const builtinReporters = ['dot', 'line', 'list', 'junit', 'json', 'null'];
|
||||||
if (typeof config.reporter !== 'string' || !builtinReporters.includes(config.reporter))
|
if (typeof config.reporter !== 'string' || !builtinReporters.includes(config.reporter))
|
||||||
throw new Error(`config.reporter must be one of ${builtinReporters.map(name => `"${name}"`).join(', ')}`);
|
throw errorWithFile(file, `config.reporter must be one of ${builtinReporters.map(name => `"${name}"`).join(', ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('reportSlowTests' in config && config.reportSlowTests !== undefined && config.reportSlowTests !== null) {
|
if ('reportSlowTests' in config && config.reportSlowTests !== undefined && config.reportSlowTests !== null) {
|
||||||
if (!config.reportSlowTests || typeof config.reportSlowTests !== 'object')
|
if (!config.reportSlowTests || typeof config.reportSlowTests !== 'object')
|
||||||
throw new Error(`config.reportSlowTests must be an object`);
|
throw errorWithFile(file, `config.reportSlowTests must be an object`);
|
||||||
if (!('max' in config.reportSlowTests) || typeof config.reportSlowTests.max !== 'number' || config.reportSlowTests.max < 0)
|
if (!('max' in config.reportSlowTests) || typeof config.reportSlowTests.max !== 'number' || config.reportSlowTests.max < 0)
|
||||||
throw new Error(`config.reportSlowTests.max must be a non-negative number`);
|
throw errorWithFile(file, `config.reportSlowTests.max must be a non-negative number`);
|
||||||
if (!('threshold' in config.reportSlowTests) || typeof config.reportSlowTests.threshold !== 'number' || config.reportSlowTests.threshold < 0)
|
if (!('threshold' in config.reportSlowTests) || typeof config.reportSlowTests.threshold !== 'number' || config.reportSlowTests.threshold < 0)
|
||||||
throw new Error(`config.reportSlowTests.threshold must be a non-negative number`);
|
throw errorWithFile(file, `config.reportSlowTests.threshold must be a non-negative number`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('shard' in config && config.shard !== undefined && config.shard !== null) {
|
if ('shard' in config && config.shard !== undefined && config.shard !== null) {
|
||||||
if (!config.shard || typeof config.shard !== 'object')
|
if (!config.shard || typeof config.shard !== 'object')
|
||||||
throw new Error(`config.shard must be an object`);
|
throw errorWithFile(file, `config.shard must be an object`);
|
||||||
if (!('total' in config.shard) || typeof config.shard.total !== 'number' || config.shard.total < 1)
|
if (!('total' in config.shard) || typeof config.shard.total !== 'number' || config.shard.total < 1)
|
||||||
throw new Error(`config.shard.total must be a positive number`);
|
throw errorWithFile(file, `config.shard.total must be a positive number`);
|
||||||
if (!('current' in config.shard) || typeof config.shard.current !== 'number' || config.shard.current < 1 || config.shard.current > config.shard.total)
|
if (!('current' in config.shard) || typeof config.shard.current !== 'number' || config.shard.current < 1 || config.shard.current > config.shard.total)
|
||||||
throw new Error(`config.shard.current must be a positive number, not greater than config.shard.total`);
|
throw errorWithFile(file, `config.shard.current must be a positive number, not greater than config.shard.total`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('updateSnapshots' in config && config.updateSnapshots !== undefined) {
|
if ('updateSnapshots' in config && config.updateSnapshots !== undefined) {
|
||||||
if (typeof config.updateSnapshots !== 'string' || !['all', 'none', 'missing'].includes(config.updateSnapshots))
|
if (typeof config.updateSnapshots !== 'string' || !['all', 'none', 'missing'].includes(config.updateSnapshots))
|
||||||
throw new Error(`config.updateSnapshots must be one of "all", "none" or "missing"`);
|
throw errorWithFile(file, `config.updateSnapshots must be one of "all", "none" or "missing"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('workers' in config && config.workers !== undefined) {
|
if ('workers' in config && config.workers !== undefined) {
|
||||||
if (typeof config.workers !== 'number' || config.workers <= 0)
|
if (typeof config.workers !== 'number' || config.workers <= 0)
|
||||||
throw new Error(`config.workers must be a positive number`);
|
throw errorWithFile(file, `config.workers must be a positive number`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateProject(project: Project, title: string) {
|
function validateProject(file: string, project: Project, title: string) {
|
||||||
if (typeof project !== 'object' || !project)
|
if (typeof project !== 'object' || !project)
|
||||||
throw new Error(`${title} must be an object`);
|
throw errorWithFile(file, `${title} must be an object`);
|
||||||
|
|
||||||
if ('define' in project && project.define !== undefined) {
|
if ('define' in project && project.define !== undefined) {
|
||||||
if (Array.isArray(project.define)) {
|
if (Array.isArray(project.define)) {
|
||||||
project.define.forEach((item, index) => {
|
project.define.forEach((item, index) => {
|
||||||
validateDefine(item, `${title}.define[${index}]`);
|
validateDefine(file, item, `${title}.define[${index}]`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
validateDefine(project.define, `${title}.define`);
|
validateDefine(file, project.define, `${title}.define`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('name' in project && project.name !== undefined) {
|
if ('name' in project && project.name !== undefined) {
|
||||||
if (typeof project.name !== 'string')
|
if (typeof project.name !== 'string')
|
||||||
throw new Error(`${title}.name must be a string`);
|
throw errorWithFile(file, `${title}.name must be a string`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('outputDir' in project && project.outputDir !== undefined) {
|
if ('outputDir' in project && project.outputDir !== undefined) {
|
||||||
if (typeof project.outputDir !== 'string')
|
if (typeof project.outputDir !== 'string')
|
||||||
throw new Error(`${title}.outputDir must be a string`);
|
throw errorWithFile(file, `${title}.outputDir must be a string`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('repeatEach' in project && project.repeatEach !== undefined) {
|
if ('repeatEach' in project && project.repeatEach !== undefined) {
|
||||||
if (typeof project.repeatEach !== 'number' || project.repeatEach < 0)
|
if (typeof project.repeatEach !== 'number' || project.repeatEach < 0)
|
||||||
throw new Error(`${title}.repeatEach must be a non-negative number`);
|
throw errorWithFile(file, `${title}.repeatEach must be a non-negative number`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('retries' in project && project.retries !== undefined) {
|
if ('retries' in project && project.retries !== undefined) {
|
||||||
if (typeof project.retries !== 'number' || project.retries < 0)
|
if (typeof project.retries !== 'number' || project.retries < 0)
|
||||||
throw new Error(`${title}.retries must be a non-negative number`);
|
throw errorWithFile(file, `${title}.retries must be a non-negative number`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('testDir' in project && project.testDir !== undefined) {
|
if ('testDir' in project && project.testDir !== undefined) {
|
||||||
if (typeof project.testDir !== 'string')
|
if (typeof project.testDir !== 'string')
|
||||||
throw new Error(`${title}.testDir must be a string`);
|
throw errorWithFile(file, `${title}.testDir must be a string`);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const prop of ['testIgnore', 'testMatch'] as const) {
|
for (const prop of ['testIgnore', 'testMatch'] as const) {
|
||||||
|
|
@ -385,28 +377,28 @@ function validateProject(project: Project, title: string) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach((item, index) => {
|
value.forEach((item, index) => {
|
||||||
if (typeof item !== 'string' && !isRegExp(item))
|
if (typeof item !== 'string' && !isRegExp(item))
|
||||||
throw new Error(`${title}.${prop}[${index}] must be a string or a RegExp`);
|
throw errorWithFile(file, `${title}.${prop}[${index}] must be a string or a RegExp`);
|
||||||
});
|
});
|
||||||
} else if (typeof value !== 'string' && !isRegExp(value)) {
|
} else if (typeof value !== 'string' && !isRegExp(value)) {
|
||||||
throw new Error(`${title}.${prop} must be a string or a RegExp`);
|
throw errorWithFile(file, `${title}.${prop} must be a string or a RegExp`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('timeout' in project && project.timeout !== undefined) {
|
if ('timeout' in project && project.timeout !== undefined) {
|
||||||
if (typeof project.timeout !== 'number' || project.timeout < 0)
|
if (typeof project.timeout !== 'number' || project.timeout < 0)
|
||||||
throw new Error(`${title}.timeout must be a non-negative number`);
|
throw errorWithFile(file, `${title}.timeout must be a non-negative number`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('use' in project && project.use !== undefined) {
|
if ('use' in project && project.use !== undefined) {
|
||||||
if (!project.use || typeof project.use !== 'object')
|
if (!project.use || typeof project.use !== 'object')
|
||||||
throw new Error(`${title}.use must be an object`);
|
throw errorWithFile(file, `${title}.use must be an object`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateDefine(define: any, title: string) {
|
function validateDefine(file: string, define: any, title: string) {
|
||||||
if (!define || typeof define !== 'object' || !define.test || !define.fixtures)
|
if (!define || typeof define !== 'object' || !define.test || !define.fixtures)
|
||||||
throw new Error(`${title} must be an object with "test" and "fixtures" properties`);
|
throw errorWithFile(file, `${title} must be an object with "test" and "fixtures" properties`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseFullConfig: FullConfig = {
|
const baseFullConfig: FullConfig = {
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,10 @@
|
||||||
import { expect } from './expect';
|
import { expect } from './expect';
|
||||||
import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuite } from './globals';
|
import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuite } from './globals';
|
||||||
import { Spec, Suite } from './test';
|
import { Spec, Suite } from './test';
|
||||||
import { callLocation, errorWithCallLocation } from './util';
|
import { wrapFunctionWithLocation } from './transform';
|
||||||
import { Fixtures, FixturesWithLocation, Location, TestInfo, TestType } from './types';
|
import { Fixtures, FixturesWithLocation, Location, TestInfo, TestType } from './types';
|
||||||
import { inheritFixtureParameterNames } from './fixtures';
|
import { inheritFixtureParameterNames } from './fixtures';
|
||||||
|
|
||||||
Error.stackTraceLimit = 15;
|
|
||||||
|
|
||||||
const countByFile = new Map<string, number>();
|
const countByFile = new Map<string, number>();
|
||||||
|
|
||||||
export class DeclaredFixtures {
|
export class DeclaredFixtures {
|
||||||
|
|
@ -37,31 +35,30 @@ export class TestTypeImpl {
|
||||||
constructor(fixtures: (FixturesWithLocation | DeclaredFixtures)[]) {
|
constructor(fixtures: (FixturesWithLocation | DeclaredFixtures)[]) {
|
||||||
this.fixtures = fixtures;
|
this.fixtures = fixtures;
|
||||||
|
|
||||||
const test: any = this._spec.bind(this, 'default');
|
const test: any = wrapFunctionWithLocation(this._spec.bind(this, 'default'));
|
||||||
test.expect = expect;
|
test.expect = expect;
|
||||||
test.only = this._spec.bind(this, 'only');
|
test.only = wrapFunctionWithLocation(this._spec.bind(this, 'only'));
|
||||||
test.describe = this._describe.bind(this, 'default');
|
test.describe = wrapFunctionWithLocation(this._describe.bind(this, 'default'));
|
||||||
test.describe.only = this._describe.bind(this, 'only');
|
test.describe.only = wrapFunctionWithLocation(this._describe.bind(this, 'only'));
|
||||||
test.beforeEach = this._hook.bind(this, 'beforeEach');
|
test.beforeEach = wrapFunctionWithLocation(this._hook.bind(this, 'beforeEach'));
|
||||||
test.afterEach = this._hook.bind(this, 'afterEach');
|
test.afterEach = wrapFunctionWithLocation(this._hook.bind(this, 'afterEach'));
|
||||||
test.beforeAll = this._hook.bind(this, 'beforeAll');
|
test.beforeAll = wrapFunctionWithLocation(this._hook.bind(this, 'beforeAll'));
|
||||||
test.afterAll = this._hook.bind(this, 'afterAll');
|
test.afterAll = wrapFunctionWithLocation(this._hook.bind(this, 'afterAll'));
|
||||||
test.skip = this._modifier.bind(this, 'skip');
|
test.skip = wrapFunctionWithLocation(this._modifier.bind(this, 'skip'));
|
||||||
test.fixme = this._modifier.bind(this, 'fixme');
|
test.fixme = wrapFunctionWithLocation(this._modifier.bind(this, 'fixme'));
|
||||||
test.fail = this._modifier.bind(this, 'fail');
|
test.fail = wrapFunctionWithLocation(this._modifier.bind(this, 'fail'));
|
||||||
test.slow = this._modifier.bind(this, 'slow');
|
test.slow = wrapFunctionWithLocation(this._modifier.bind(this, 'slow'));
|
||||||
test.setTimeout = this._setTimeout.bind(this);
|
test.setTimeout = this._setTimeout.bind(this);
|
||||||
test.use = this._use.bind(this);
|
test.use = wrapFunctionWithLocation(this._use.bind(this));
|
||||||
test.extend = this._extend.bind(this);
|
test.extend = wrapFunctionWithLocation(this._extend.bind(this));
|
||||||
test.declare = this._declare.bind(this);
|
test.declare = wrapFunctionWithLocation(this._declare.bind(this));
|
||||||
this.test = test;
|
this.test = test;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _spec(type: 'default' | 'only', title: string, fn: Function) {
|
private _spec(type: 'default' | 'only', location: Location, title: string, fn: Function) {
|
||||||
const suite = currentlyLoadingFileSuite();
|
const suite = currentlyLoadingFileSuite();
|
||||||
if (!suite)
|
if (!suite)
|
||||||
throw errorWithCallLocation(`test() can only be called in a test file`);
|
throw new Error(`test() can only be called in a test file`);
|
||||||
const location = callLocation(suite.file);
|
|
||||||
|
|
||||||
const ordinalInFile = countByFile.get(suite._requireFile) || 0;
|
const ordinalInFile = countByFile.get(suite._requireFile) || 0;
|
||||||
countByFile.set(suite._requireFile, ordinalInFile + 1);
|
countByFile.set(suite._requireFile, ordinalInFile + 1);
|
||||||
|
|
@ -77,11 +74,10 @@ export class TestTypeImpl {
|
||||||
spec._only = true;
|
spec._only = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _describe(type: 'default' | 'only', title: string, fn: Function) {
|
private _describe(type: 'default' | 'only', location: Location, title: string, fn: Function) {
|
||||||
const suite = currentlyLoadingFileSuite();
|
const suite = currentlyLoadingFileSuite();
|
||||||
if (!suite)
|
if (!suite)
|
||||||
throw errorWithCallLocation(`describe() can only be called in a test file`);
|
throw new Error(`describe() can only be called in a test file`);
|
||||||
const location = callLocation(suite.file);
|
|
||||||
|
|
||||||
const child = new Suite(title);
|
const child = new Suite(title);
|
||||||
child._requireFile = suite._requireFile;
|
child._requireFile = suite._requireFile;
|
||||||
|
|
@ -98,17 +94,16 @@ export class TestTypeImpl {
|
||||||
setCurrentlyLoadingFileSuite(suite);
|
setCurrentlyLoadingFileSuite(suite);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _hook(name: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', fn: Function) {
|
private _hook(name: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', location: Location, fn: Function) {
|
||||||
const suite = currentlyLoadingFileSuite();
|
const suite = currentlyLoadingFileSuite();
|
||||||
if (!suite)
|
if (!suite)
|
||||||
throw errorWithCallLocation(`${name} hook can only be called in a test file`);
|
throw new Error(`${name} hook can only be called in a test file`);
|
||||||
suite._hooks.push({ type: name, fn, location: callLocation() });
|
suite._hooks.push({ type: name, fn, location });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', ...modiferAgs: [arg?: any | Function, description?: string]) {
|
private _modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', location: Location, ...modiferAgs: [arg?: any | Function, description?: string]) {
|
||||||
const suite = currentlyLoadingFileSuite();
|
const suite = currentlyLoadingFileSuite();
|
||||||
if (suite) {
|
if (suite) {
|
||||||
const location = callLocation();
|
|
||||||
if (typeof modiferAgs[0] === 'function') {
|
if (typeof modiferAgs[0] === 'function') {
|
||||||
const [conditionFn, description] = modiferAgs;
|
const [conditionFn, description] = modiferAgs;
|
||||||
const fn = (args: any, testInfo: TestInfo) => testInfo[type](conditionFn(args), description!);
|
const fn = (args: any, testInfo: TestInfo) => testInfo[type](conditionFn(args), description!);
|
||||||
|
|
@ -136,24 +131,21 @@ export class TestTypeImpl {
|
||||||
testInfo.setTimeout(timeout);
|
testInfo.setTimeout(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _use(fixtures: Fixtures) {
|
private _use(location: Location, fixtures: Fixtures) {
|
||||||
const suite = currentlyLoadingFileSuite();
|
const suite = currentlyLoadingFileSuite();
|
||||||
if (!suite)
|
if (!suite)
|
||||||
throw errorWithCallLocation(`test.use() can only be called in a test file`);
|
throw new Error(`test.use() can only be called in a test file`);
|
||||||
suite._fixtureOverrides = { ...suite._fixtureOverrides, ...fixtures };
|
suite._fixtureOverrides = { ...suite._fixtureOverrides, ...fixtures };
|
||||||
}
|
}
|
||||||
|
|
||||||
private _extend(fixtures: Fixtures) {
|
private _extend(location: Location, fixtures: Fixtures) {
|
||||||
const fixturesWithLocation = {
|
const fixturesWithLocation = { fixtures, location };
|
||||||
fixtures,
|
|
||||||
location: callLocation(),
|
|
||||||
};
|
|
||||||
return new TestTypeImpl([...this.fixtures, fixturesWithLocation]).test;
|
return new TestTypeImpl([...this.fixtures, fixturesWithLocation]).test;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _declare() {
|
private _declare(location: Location) {
|
||||||
const declared = new DeclaredFixtures();
|
const declared = new DeclaredFixtures();
|
||||||
declared.location = callLocation();
|
declared.location = location;
|
||||||
const child = new TestTypeImpl([...this.fixtures, declared]);
|
const child = new TestTypeImpl([...this.fixtures, declared]);
|
||||||
declared.testType = child;
|
declared.testType = child;
|
||||||
return child.test;
|
return child.test;
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,15 @@ import * as fs from 'fs';
|
||||||
import * as pirates from 'pirates';
|
import * as pirates from 'pirates';
|
||||||
import * as babel from '@babel/core';
|
import * as babel from '@babel/core';
|
||||||
import * as sourceMapSupport from 'source-map-support';
|
import * as sourceMapSupport from 'source-map-support';
|
||||||
|
import type { Location } from './types';
|
||||||
|
|
||||||
const version = 4;
|
const version = 4;
|
||||||
const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwright-transform-cache');
|
const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwright-transform-cache');
|
||||||
const sourceMaps: Map<string, string> = new Map();
|
const sourceMaps: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
const kStackTraceLimit = 15;
|
||||||
|
Error.stackTraceLimit = kStackTraceLimit;
|
||||||
|
|
||||||
sourceMapSupport.install({
|
sourceMapSupport.install({
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
handleUncaughtExceptions: false,
|
handleUncaughtExceptions: false,
|
||||||
|
|
@ -97,3 +101,24 @@ export function installTransform(): () => void {
|
||||||
exts: ['.ts']
|
exts: ['.ts']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function wrapFunctionWithLocation<A extends any[], R>(func: (location: Location, ...args: A) => R): (...args: A) => R {
|
||||||
|
return (...args) => {
|
||||||
|
const oldPrepareStackTrace = Error.prepareStackTrace;
|
||||||
|
Error.prepareStackTrace = (error, stackFrames) => {
|
||||||
|
const frame: NodeJS.CallSite = sourceMapSupport.wrapCallSite(stackFrames[1]);
|
||||||
|
return {
|
||||||
|
file: frame.getFileName(),
|
||||||
|
line: frame.getLineNumber(),
|
||||||
|
column: frame.getColumnNumber(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
Error.stackTraceLimit = 2;
|
||||||
|
const obj: { stack: Location } = {} as any;
|
||||||
|
Error.captureStackTrace(obj);
|
||||||
|
const location = obj.stack;
|
||||||
|
Error.stackTraceLimit = kStackTraceLimit;
|
||||||
|
Error.prepareStackTrace = oldPrepareStackTrace;
|
||||||
|
return func(location, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
import StackUtils from 'stack-utils';
|
import type { TestError } from './types';
|
||||||
import type { Location, TestError } from './types';
|
|
||||||
import { default as minimatch } from 'minimatch';
|
import { default as minimatch } from 'minimatch';
|
||||||
|
|
||||||
const TEST_RUNNER_DIRS = [
|
|
||||||
path.join('@playwright', 'test', 'lib'),
|
|
||||||
path.join(__dirname, '..', '..', 'src', 'test'),
|
|
||||||
];
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const stackUtils = new StackUtils({ cwd });
|
|
||||||
|
|
||||||
export class DeadlineRunner<T> {
|
export class DeadlineRunner<T> {
|
||||||
private _timer: NodeJS.Timer | undefined;
|
private _timer: NodeJS.Timer | undefined;
|
||||||
private _done = false;
|
private _done = false;
|
||||||
|
|
@ -89,50 +80,11 @@ export function serializeError(error: Error | any): TestError {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function callFrames(): string[] {
|
|
||||||
const obj = { stack: '' };
|
|
||||||
Error.captureStackTrace(obj);
|
|
||||||
const frames = obj.stack.split('\n').slice(1);
|
|
||||||
while (frames.length && TEST_RUNNER_DIRS.some(dir => frames[0].includes(dir)))
|
|
||||||
frames.shift();
|
|
||||||
return frames;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function callLocation(fallbackFile?: string): Location {
|
|
||||||
const frames = callFrames();
|
|
||||||
if (!frames.length)
|
|
||||||
return {file: fallbackFile || '<unknown>', line: 1, column: 1};
|
|
||||||
const location = stackUtils.parseLine(frames[0])!;
|
|
||||||
return {
|
|
||||||
file: path.resolve(cwd, location.file || ''),
|
|
||||||
line: location.line || 0,
|
|
||||||
column: location.column || 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function errorWithCallLocation(message: string): Error {
|
|
||||||
const frames = callFrames();
|
|
||||||
const error = new Error(message);
|
|
||||||
error.stack = 'Error: ' + message + '\n' + frames.join('\n');
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function monotonicTime(): number {
|
export function monotonicTime(): number {
|
||||||
const [seconds, nanoseconds] = process.hrtime();
|
const [seconds, nanoseconds] = process.hrtime();
|
||||||
return seconds * 1000 + (nanoseconds / 1000000 | 0);
|
return seconds * 1000 + (nanoseconds / 1000000 | 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prependErrorMessage(e: Error, message: string) {
|
|
||||||
let stack = e.stack || '';
|
|
||||||
if (stack.includes(e.message))
|
|
||||||
stack = stack.substring(stack.indexOf(e.message) + e.message.length);
|
|
||||||
let m = e.message;
|
|
||||||
if (m.startsWith('Error:'))
|
|
||||||
m = m.substring('Error:'.length);
|
|
||||||
e.message = message + m;
|
|
||||||
e.stack = e.message + stack;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRegExp(e: any): e is RegExp {
|
export function isRegExp(e: any): e is RegExp {
|
||||||
return e && typeof e === 'object' && (e instanceof RegExp || Object.prototype.toString.call(e) === '[object RegExp]');
|
return e && typeof e === 'object' && (e instanceof RegExp || Object.prototype.toString.call(e) === '[object RegExp]');
|
||||||
}
|
}
|
||||||
|
|
@ -184,10 +136,6 @@ export async function wrapInPromise(value: any) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatLocation(location: Location) {
|
|
||||||
return location.file + ':' + location.line + ':' + location.column;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function forceRegExp(pattern: string): RegExp {
|
export function forceRegExp(pattern: string): RegExp {
|
||||||
const match = pattern.match(/^\/(.*)\/([gi]*)$/);
|
const match = pattern.match(/^\/(.*)\/([gi]*)$/);
|
||||||
if (match)
|
if (match)
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,6 @@ import { isUnderTest } from './utils';
|
||||||
|
|
||||||
const stackUtils = new StackUtils();
|
const stackUtils = new StackUtils();
|
||||||
|
|
||||||
export function getCallerFilePath(ignorePrefix: string): string | null {
|
|
||||||
const frame = captureStackTrace().frames.find(f => !f.file.startsWith(ignorePrefix));
|
|
||||||
return frame ? frame.file : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rewriteErrorMessage(e: Error, newMessage: string): Error {
|
export function rewriteErrorMessage(e: Error, newMessage: string): Error {
|
||||||
if (e.stack) {
|
if (e.stack) {
|
||||||
const index = e.stack.indexOf(e.message);
|
const index = e.stack.indexOf(e.message);
|
||||||
|
|
|
||||||
|
|
@ -175,23 +175,6 @@ test('should support different testDirs', async ({ runInlineTest }) => {
|
||||||
expect(result.report.suites[1].specs[0].title).toBe('runs twice');
|
expect(result.report.suites[1].specs[0].title).toBe('runs twice');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow export default form the config file', async ({ runInlineTest }) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'playwright.config.ts': `
|
|
||||||
export default { timeout: 1000 };
|
|
||||||
`,
|
|
||||||
'a.test.ts': `
|
|
||||||
const { test } = pwt;
|
|
||||||
test('fails', async ({}, testInfo) => {
|
|
||||||
await new Promise(f => setTimeout(f, 2000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.exitCode).toBe(1);
|
|
||||||
expect(result.failed).toBe(1);
|
|
||||||
expect(result.output).toContain('Timeout of 1000ms exceeded.');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow root testDir and use it for relative paths', async ({ runInlineTest }) => {
|
test('should allow root testDir and use it for relative paths', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
|
|
|
||||||
|
|
@ -367,7 +367,7 @@ test('should print nice error message for problematic fixtures', async ({ runInl
|
||||||
});
|
});
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.output).toContain('oh my!');
|
expect(result.output).toContain('oh my!');
|
||||||
expect(result.output).toContain('x.spec.ts:5:29');
|
expect(result.output).toContain('x.spec.ts:6:49');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should exit with timeout when fixture causes an exception in the test', async ({ runInlineTest }) => {
|
test('should exit with timeout when fixture causes an exception in the test', async ({ runInlineTest }) => {
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ test('globalSetup should throw when passed non-function', async ({ runInlineTest
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
expect(output).toContain(`globalSetup file must export a single function.`);
|
expect(output).toContain(`globalSetup.ts: globalSetup file must export a single function.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('globalSetup should work with default export and run the returned fn', async ({ runInlineTest }) => {
|
test('globalSetup should work with default export and run the returned fn', async ({ runInlineTest }) => {
|
||||||
|
|
|
||||||
106
tests/playwright-test/loader.spec.ts
Normal file
106
tests/playwright-test/loader.spec.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from './playwright-test-fixtures';
|
||||||
|
|
||||||
|
test('should return the location of a syntax error', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'error.spec.js': `
|
||||||
|
const x = {
|
||||||
|
foo: 'bar';
|
||||||
|
};
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(0);
|
||||||
|
expect(result.failed).toBe(0);
|
||||||
|
expect(result.output).toContain('error.spec.js:6');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should print an improper error', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'error.spec.js': `
|
||||||
|
throw 123;
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(0);
|
||||||
|
expect(result.failed).toBe(0);
|
||||||
|
expect(result.output).toContain('123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should print a null error', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'error.spec.js': `
|
||||||
|
throw null;
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(0);
|
||||||
|
expect(result.failed).toBe(0);
|
||||||
|
expect(result.output).toContain('null');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the location of a syntax error in typescript', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'error.spec.ts': `
|
||||||
|
const x = {
|
||||||
|
foo: 'bar';
|
||||||
|
};
|
||||||
|
`
|
||||||
|
}, {}, {
|
||||||
|
FORCE_COLOR: '0'
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(0);
|
||||||
|
expect(result.failed).toBe(0);
|
||||||
|
expect(result.output).toContain('error.spec.ts');
|
||||||
|
expect(result.output).toContain(`'bar';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow export default form the config file', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
export default { timeout: 1000 };
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('fails', async ({}, testInfo) => {
|
||||||
|
await new Promise(f => setTimeout(f, 2000));
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.output).toContain('Timeout of 1000ms exceeded.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should validate configuration object', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
export default { timeout: '1000' };
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('works', () => {});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(0);
|
||||||
|
expect(result.output).toContain('playwright.config.ts: config.timeout must be a non-negative number');
|
||||||
|
});
|
||||||
|
|
@ -225,7 +225,7 @@ export const test = base.extend<Fixtures>({
|
||||||
runResult = await runPlaywrightTest(baseDir, params, env);
|
runResult = await runPlaywrightTest(baseDir, params, env);
|
||||||
return runResult;
|
return runResult;
|
||||||
});
|
});
|
||||||
if (testInfo.status !== testInfo.expectedStatus && runResult)
|
if (testInfo.status !== testInfo.expectedStatus && runResult && !process.env.PW_RUNNER_DEBUG)
|
||||||
console.log(runResult.output);
|
console.log(runResult.output);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -236,7 +236,7 @@ export const test = base.extend<Fixtures>({
|
||||||
tscResult = await runTSC(baseDir);
|
tscResult = await runTSC(baseDir);
|
||||||
return tscResult;
|
return tscResult;
|
||||||
});
|
});
|
||||||
if (testInfo.status !== testInfo.expectedStatus && tscResult)
|
if (testInfo.status !== testInfo.expectedStatus && tscResult && !process.env.PW_RUNNER_DEBUG)
|
||||||
console.log(tscResult.output);
|
console.log(tscResult.output);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2019 Google Inc. All rights reserved.
|
|
||||||
* Modifications copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { playwrightTest as test, expect } from './config/browserTest';
|
|
||||||
import path from 'path';
|
|
||||||
import * as stackTrace from '../src/utils/stackTrace';
|
|
||||||
import { setUnderTest } from '../src/utils/utils';
|
|
||||||
|
|
||||||
test('caller file path', async ({}) => {
|
|
||||||
setUnderTest();
|
|
||||||
const callme = require('./assets/callback');
|
|
||||||
const filePath = callme(() => {
|
|
||||||
return stackTrace.getCallerFilePath(path.join(__dirname, 'assets') + path.sep);
|
|
||||||
});
|
|
||||||
expect(filePath).toBe(__filename);
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue