2021-06-07 02:09:53 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
|
|
|
|
|
2021-07-20 22:13:40 +02:00
|
|
|
import * as fs from 'fs';
|
2022-10-11 01:42:48 +02:00
|
|
|
import * as path from 'path';
|
2023-01-18 02:16:36 +01:00
|
|
|
import { isRegExp } from 'playwright-core/lib/utils';
|
2023-01-27 02:26:47 +01:00
|
|
|
import type { ConfigCLIOverrides, SerializedConfig } from './ipc';
|
2023-01-18 02:16:36 +01:00
|
|
|
import { requireOrImport } from './transform';
|
2023-04-07 18:54:01 +02:00
|
|
|
import type { Config, Project } from '../../types/test';
|
2023-04-08 02:46:47 +02:00
|
|
|
import { errorWithFile } from '../util';
|
2023-02-17 01:48:28 +01:00
|
|
|
import { setCurrentConfig } from './globals';
|
2023-04-08 02:46:47 +02:00
|
|
|
import { FullConfigInternal } from './config';
|
2023-04-19 23:20:53 +02:00
|
|
|
import { setBabelPlugins } from './babelBundle';
|
2021-06-07 02:09:53 +02:00
|
|
|
|
2023-03-02 00:47:05 +01:00
|
|
|
const kDefineConfigWasUsed = Symbol('defineConfigWasUsed');
|
|
|
|
|
export const defineConfig = (config: any) => {
|
|
|
|
|
config[kDefineConfigWasUsed] = true;
|
|
|
|
|
return config;
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-18 02:16:36 +01:00
|
|
|
export class ConfigLoader {
|
2023-04-07 18:54:01 +02:00
|
|
|
private _configCLIOverrides: ConfigCLIOverrides;
|
|
|
|
|
private _fullConfig: FullConfigInternal | undefined;
|
2021-06-07 02:09:53 +02:00
|
|
|
|
2022-04-29 22:32:39 +02:00
|
|
|
constructor(configCLIOverrides?: ConfigCLIOverrides) {
|
2023-04-07 18:54:01 +02:00
|
|
|
this._configCLIOverrides = configCLIOverrides || {};
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
2023-04-07 18:54:01 +02:00
|
|
|
static async deserialize(data: SerializedConfig): Promise<FullConfigInternal> {
|
2023-01-18 02:16:36 +01:00
|
|
|
const loader = new ConfigLoader(data.configCLIOverrides);
|
2023-04-19 23:20:53 +02:00
|
|
|
setBabelPlugins(data.babelTransformPlugins);
|
|
|
|
|
|
2022-05-03 23:25:56 +02:00
|
|
|
if (data.configFile)
|
2023-04-07 18:54:01 +02:00
|
|
|
return await loader.loadConfigFile(data.configFile);
|
|
|
|
|
return await loader.loadEmptyConfig(data.configDir);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
2023-04-07 18:54:01 +02:00
|
|
|
async loadConfigFile(file: string, ignoreProjectDependencies = false): Promise<FullConfigInternal> {
|
|
|
|
|
if (this._fullConfig)
|
2021-06-07 02:09:53 +02:00
|
|
|
throw new Error('Cannot load two config files');
|
2023-01-27 21:44:15 +01:00
|
|
|
const config = await requireOrImportDefaultObject(file) as Config;
|
2023-04-07 18:54:01 +02:00
|
|
|
const fullConfig = await this._loadConfig(config, path.dirname(file), file);
|
|
|
|
|
setCurrentConfig(fullConfig);
|
|
|
|
|
if (ignoreProjectDependencies) {
|
|
|
|
|
for (const project of fullConfig.projects)
|
|
|
|
|
project.deps = [];
|
|
|
|
|
}
|
|
|
|
|
this._fullConfig = fullConfig;
|
|
|
|
|
return fullConfig;
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
2023-04-07 18:54:01 +02:00
|
|
|
async loadEmptyConfig(configDir: string): Promise<FullConfigInternal> {
|
|
|
|
|
const fullConfig = await this._loadConfig({}, configDir);
|
|
|
|
|
setCurrentConfig(fullConfig);
|
|
|
|
|
return fullConfig;
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
2023-04-07 18:54:01 +02:00
|
|
|
private async _loadConfig(config: Config, configDir: string, configFile?: string): Promise<FullConfigInternal> {
|
2022-04-29 22:32:39 +02:00
|
|
|
// 1. Validate data provided in the config file.
|
2023-01-27 21:44:15 +01:00
|
|
|
validateConfig(configFile || '<default config>', config);
|
2023-04-08 02:46:47 +02:00
|
|
|
const fullConfig = new FullConfigInternal(configDir, configFile, config, this._configCLIOverrides);
|
2023-04-07 18:54:01 +02:00
|
|
|
fullConfig.defineConfigWasUsed = !!(config as any)[kDefineConfigWasUsed];
|
|
|
|
|
return fullConfig;
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
2023-01-27 21:44:15 +01:00
|
|
|
}
|
2021-07-12 18:59:58 +02:00
|
|
|
|
2023-01-27 21:44:15 +01:00
|
|
|
async function requireOrImportDefaultObject(file: string) {
|
|
|
|
|
let object = await requireOrImport(file);
|
|
|
|
|
if (object && typeof object === 'object' && ('default' in object))
|
|
|
|
|
object = object['default'];
|
|
|
|
|
return object;
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
2021-06-23 19:30:54 +02:00
|
|
|
function validateConfig(file: string, config: Config) {
|
2021-06-07 02:09:53 +02:00
|
|
|
if (typeof config !== 'object' || !config)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `Configuration file must export a single object`);
|
2021-06-07 02:09:53 +02:00
|
|
|
|
2021-06-23 19:30:54 +02:00
|
|
|
validateProject(file, config, 'config');
|
2021-06-07 02:09:53 +02:00
|
|
|
|
|
|
|
|
if ('forbidOnly' in config && config.forbidOnly !== undefined) {
|
|
|
|
|
if (typeof config.forbidOnly !== 'boolean')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.forbidOnly must be a boolean`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('globalSetup' in config && config.globalSetup !== undefined) {
|
|
|
|
|
if (typeof config.globalSetup !== 'string')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.globalSetup must be a string`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('globalTeardown' in config && config.globalTeardown !== undefined) {
|
|
|
|
|
if (typeof config.globalTeardown !== 'string')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.globalTeardown must be a string`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('globalTimeout' in config && config.globalTimeout !== undefined) {
|
|
|
|
|
if (typeof config.globalTimeout !== 'number' || config.globalTimeout < 0)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.globalTimeout must be a non-negative number`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('grep' in config && config.grep !== undefined) {
|
|
|
|
|
if (Array.isArray(config.grep)) {
|
|
|
|
|
config.grep.forEach((item, index) => {
|
|
|
|
|
if (!isRegExp(item))
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.grep[${index}] must be a RegExp`);
|
2021-06-07 02:09:53 +02:00
|
|
|
});
|
|
|
|
|
} else if (!isRegExp(config.grep)) {
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.grep must be a RegExp`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-19 02:56:59 +02:00
|
|
|
if ('grepInvert' in config && config.grepInvert !== undefined) {
|
|
|
|
|
if (Array.isArray(config.grepInvert)) {
|
|
|
|
|
config.grepInvert.forEach((item, index) => {
|
|
|
|
|
if (!isRegExp(item))
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.grepInvert[${index}] must be a RegExp`);
|
2021-06-19 02:56:59 +02:00
|
|
|
});
|
|
|
|
|
} else if (!isRegExp(config.grepInvert)) {
|
2022-09-29 03:45:01 +02:00
|
|
|
throw errorWithFile(file, `config.grepInvert must be a RegExp`);
|
2021-06-19 02:56:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-07 02:09:53 +02:00
|
|
|
if ('maxFailures' in config && config.maxFailures !== undefined) {
|
|
|
|
|
if (typeof config.maxFailures !== 'number' || config.maxFailures < 0)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.maxFailures must be a non-negative number`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('preserveOutput' in config && config.preserveOutput !== undefined) {
|
|
|
|
|
if (typeof config.preserveOutput !== 'string' || !['always', 'never', 'failures-only'].includes(config.preserveOutput))
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.preserveOutput must be one of "always", "never" or "failures-only"`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('projects' in config && config.projects !== undefined) {
|
|
|
|
|
if (!Array.isArray(config.projects))
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.projects must be an array`);
|
2021-06-07 02:09:53 +02:00
|
|
|
config.projects.forEach((project, index) => {
|
2021-06-23 19:30:54 +02:00
|
|
|
validateProject(file, project, `config.projects[${index}]`);
|
2021-06-07 02:09:53 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('quiet' in config && config.quiet !== undefined) {
|
|
|
|
|
if (typeof config.quiet !== 'boolean')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.quiet must be a boolean`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('reporter' in config && config.reporter !== undefined) {
|
|
|
|
|
if (Array.isArray(config.reporter)) {
|
|
|
|
|
config.reporter.forEach((item, index) => {
|
|
|
|
|
if (!Array.isArray(item) || item.length <= 0 || item.length > 2 || typeof item[0] !== 'string')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.reporter[${index}] must be a tuple [name, optionalArgument]`);
|
2021-06-07 02:09:53 +02:00
|
|
|
});
|
2021-07-20 22:03:01 +02:00
|
|
|
} else if (typeof config.reporter !== 'string') {
|
|
|
|
|
throw errorWithFile(file, `config.reporter must be a string`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-15 07:45:58 +02:00
|
|
|
if ('reportSlowTests' in config && config.reportSlowTests !== undefined && config.reportSlowTests !== null) {
|
|
|
|
|
if (!config.reportSlowTests || typeof config.reportSlowTests !== 'object')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.reportSlowTests must be an object`);
|
2021-06-15 07:45:58 +02:00
|
|
|
if (!('max' in config.reportSlowTests) || typeof config.reportSlowTests.max !== 'number' || config.reportSlowTests.max < 0)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.reportSlowTests.max must be a non-negative number`);
|
2021-06-15 07:45:58 +02:00
|
|
|
if (!('threshold' in config.reportSlowTests) || typeof config.reportSlowTests.threshold !== 'number' || config.reportSlowTests.threshold < 0)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.reportSlowTests.threshold must be a non-negative number`);
|
2021-06-15 07:45:58 +02:00
|
|
|
}
|
|
|
|
|
|
2021-06-07 02:09:53 +02:00
|
|
|
if ('shard' in config && config.shard !== undefined && config.shard !== null) {
|
|
|
|
|
if (!config.shard || typeof config.shard !== 'object')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.shard must be an object`);
|
2021-06-07 02:09:53 +02:00
|
|
|
if (!('total' in config.shard) || typeof config.shard.total !== 'number' || config.shard.total < 1)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.shard.total must be a positive number`);
|
2021-06-07 02:09:53 +02:00
|
|
|
if (!('current' in config.shard) || typeof config.shard.current !== 'number' || config.shard.current < 1 || config.shard.current > config.shard.total)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.shard.current must be a positive number, not greater than config.shard.total`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
2022-09-01 14:34:36 +02:00
|
|
|
if ('ignoreSnapshots' in config && config.ignoreSnapshots !== undefined) {
|
|
|
|
|
if (typeof config.ignoreSnapshots !== 'boolean')
|
|
|
|
|
throw errorWithFile(file, `config.ignoreSnapshots must be a boolean`);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-07 02:09:53 +02:00
|
|
|
if ('updateSnapshots' in config && config.updateSnapshots !== undefined) {
|
|
|
|
|
if (typeof config.updateSnapshots !== 'string' || !['all', 'none', 'missing'].includes(config.updateSnapshots))
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.updateSnapshots must be one of "all", "none" or "missing"`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('workers' in config && config.workers !== undefined) {
|
2022-09-21 20:17:36 +02:00
|
|
|
if (typeof config.workers === 'number' && config.workers <= 0)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `config.workers must be a positive number`);
|
2022-09-21 20:17:36 +02:00
|
|
|
else if (typeof config.workers === 'string' && !config.workers.endsWith('%'))
|
|
|
|
|
throw errorWithFile(file, `config.workers must be a number or percentage`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-23 19:30:54 +02:00
|
|
|
function validateProject(file: string, project: Project, title: string) {
|
2021-06-07 02:09:53 +02:00
|
|
|
if (typeof project !== 'object' || !project)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `${title} must be an object`);
|
2021-06-07 02:09:53 +02:00
|
|
|
|
|
|
|
|
if ('name' in project && project.name !== undefined) {
|
|
|
|
|
if (typeof project.name !== 'string')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `${title}.name must be a string`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('outputDir' in project && project.outputDir !== undefined) {
|
|
|
|
|
if (typeof project.outputDir !== 'string')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `${title}.outputDir must be a string`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('repeatEach' in project && project.repeatEach !== undefined) {
|
|
|
|
|
if (typeof project.repeatEach !== 'number' || project.repeatEach < 0)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `${title}.repeatEach must be a non-negative number`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('retries' in project && project.retries !== undefined) {
|
|
|
|
|
if (typeof project.retries !== 'number' || project.retries < 0)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `${title}.retries must be a non-negative number`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('testDir' in project && project.testDir !== undefined) {
|
|
|
|
|
if (typeof project.testDir !== 'string')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `${title}.testDir must be a string`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
2023-01-18 21:56:03 +01:00
|
|
|
for (const prop of ['testIgnore', 'testMatch'] as const) {
|
2021-06-07 02:09:53 +02:00
|
|
|
if (prop in project && project[prop] !== undefined) {
|
|
|
|
|
const value = project[prop];
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
value.forEach((item, index) => {
|
|
|
|
|
if (typeof item !== 'string' && !isRegExp(item))
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `${title}.${prop}[${index}] must be a string or a RegExp`);
|
2021-06-07 02:09:53 +02:00
|
|
|
});
|
|
|
|
|
} else if (typeof value !== 'string' && !isRegExp(value)) {
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `${title}.${prop} must be a string or a RegExp`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('timeout' in project && project.timeout !== undefined) {
|
|
|
|
|
if (typeof project.timeout !== 'number' || project.timeout < 0)
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `${title}.timeout must be a non-negative number`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('use' in project && project.use !== undefined) {
|
|
|
|
|
if (!project.use || typeof project.use !== 'object')
|
2021-06-23 19:30:54 +02:00
|
|
|
throw errorWithFile(file, `${title}.use must be an object`);
|
2021-06-07 02:09:53 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-26 22:20:05 +01:00
|
|
|
export const kDefaultConfigFiles = ['playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs'];
|
|
|
|
|
|
|
|
|
|
export function resolveConfigFile(configFileOrDirectory: string): string | null {
|
|
|
|
|
const resolveConfig = (configFile: string) => {
|
|
|
|
|
if (fs.existsSync(configFile))
|
|
|
|
|
return configFile;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resolveConfigFileFromDirectory = (directory: string) => {
|
|
|
|
|
for (const configName of kDefaultConfigFiles) {
|
|
|
|
|
const configFile = resolveConfig(path.resolve(directory, configName));
|
|
|
|
|
if (configFile)
|
|
|
|
|
return configFile;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(configFileOrDirectory))
|
|
|
|
|
throw new Error(`${configFileOrDirectory} does not exist`);
|
|
|
|
|
if (fs.statSync(configFileOrDirectory).isDirectory()) {
|
|
|
|
|
// When passed a directory, look for a config file inside.
|
|
|
|
|
const configFile = resolveConfigFileFromDirectory(configFileOrDirectory);
|
|
|
|
|
if (configFile)
|
|
|
|
|
return configFile;
|
|
|
|
|
// If there is no config, assume this as a root testing directory.
|
|
|
|
|
return null;
|
|
|
|
|
} else {
|
|
|
|
|
// When passed a file, it must be a config file.
|
|
|
|
|
const configFile = resolveConfig(configFileOrDirectory);
|
|
|
|
|
return configFile!;
|
|
|
|
|
}
|
|
|
|
|
}
|