feat: use package.json directory as a default for outputDir (#12942)

This patch:
- starts using directory of `package.json` to resolve default
  output directory path
- starts using either `package.json` directory or configuration
  directory to resolve all relative paths

References #12970
This commit is contained in:
Andrey Lushnikov 2022-03-23 17:05:49 -06:00 committed by GitHub
parent 39376cccd8
commit e1700bd167
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 142 additions and 89 deletions

View File

@ -256,7 +256,7 @@ Any JSON-serializable metadata that will be put directly to the test report.
## property: TestConfig.outputDir
- type: <[string]>
The output directory for files created during test execution. Defaults to `test-results`.
The output directory for files created during test execution. Defaults to `<package.json-directory>/test-results`.
```js js-flavor=js
// playwright.config.js

View File

@ -202,7 +202,7 @@ This path will serve as the base directory for each test file snapshot directory
## property: TestProject.outputDir
- type: <[string]>
The output directory for files created during test execution. Defaults to `test-results`.
The output directory for files created during test execution. Defaults to `<package.json-directory>/test-results`.
This directory is cleaned at the start. When running a test, a unique subdirectory inside the [`property: TestProject.outputDir`] is created, guaranteeing that test running in parallel do not conflict. This directory can be accessed by [`property: TestInfo.outputDir`] and [`method: TestInfo.outputPath`].

View File

@ -20,7 +20,7 @@ import type { Config, TestStatus } from './types';
export type SerializedLoaderData = {
defaultConfig: Config;
overrides: Config;
configFile: { file: string } | { rootDir: string };
configFile: { file: string } | { configDir: string };
};
export type WorkerInitParams = {
workerIndex: number;

View File

@ -37,7 +37,7 @@ export class Loader {
private _defaultConfig: Config;
private _configOverrides: Config;
private _fullConfig: FullConfig;
private _config: Config = {};
private _configDir: string = '';
private _configFile: string | undefined;
private _projects: ProjectImpl[] = [];
@ -52,7 +52,7 @@ export class Loader {
if ('file' in data.configFile)
await loader.loadConfigFile(data.configFile.file);
else
loader.loadEmptyConfig(data.configFile.rootDir);
loader.loadEmptyConfig(data.configFile.configDir);
return loader;
}
@ -62,55 +62,63 @@ export class Loader {
let config = await this._requireOrImport(file);
if (config && typeof config === 'object' && ('default' in config))
config = config['default'];
this._config = config;
this._configFile = file;
const rawConfig = { ...config };
this._processConfigObject(path.dirname(file));
this._processConfigObject(config, path.dirname(file));
return rawConfig;
}
loadEmptyConfig(rootDir: string): Config {
this._config = {};
this._processConfigObject(rootDir);
loadEmptyConfig(configDir: string): Config {
this._processConfigObject({}, configDir);
return {};
}
private _processConfigObject(rootDir: string) {
validateConfig(this._configFile || '<default config>', this._config);
private _processConfigObject(config: Config, configDir: string) {
this._configDir = configDir;
const packageJsonPath = getPackageJsonPath(configDir);
const packageJsonDir = packageJsonPath ? path.dirname(packageJsonPath) : undefined;
const throwawayArtifactsPath = packageJsonDir || process.cwd();
validateConfig(this._configFile || '<default config>', config);
// Resolve script hooks relative to the root dir.
if (this._config.globalSetup)
this._config.globalSetup = resolveScript(this._config.globalSetup, rootDir);
if (this._config.globalTeardown)
this._config.globalTeardown = resolveScript(this._config.globalTeardown, rootDir);
if (config.globalSetup)
config.globalSetup = resolveScript(config.globalSetup, configDir);
if (config.globalTeardown)
config.globalTeardown = resolveScript(config.globalTeardown, configDir);
// Resolve all config dirs relative to configDir.
if (config.testDir !== undefined)
config.testDir = path.resolve(configDir, config.testDir);
if (config.outputDir !== undefined)
config.outputDir = path.resolve(configDir, config.outputDir);
if (config.screenshotsDir !== undefined)
config.screenshotsDir = path.resolve(configDir, config.screenshotsDir);
if (config.snapshotDir !== undefined)
config.snapshotDir = path.resolve(configDir, config.snapshotDir);
const configUse = mergeObjects(this._defaultConfig.use, this._config.use);
this._config = mergeObjects(mergeObjects(this._defaultConfig, this._config), { use: configUse });
const configUse = mergeObjects(this._defaultConfig.use, config.use);
config = mergeObjects(mergeObjects(this._defaultConfig, config), { use: configUse });
if (this._config.testDir !== undefined)
this._config.testDir = path.resolve(rootDir, this._config.testDir);
const projects: Project[] = ('projects' in this._config) && this._config.projects !== undefined ? this._config.projects : [this._config];
this._fullConfig.rootDir = this._config.testDir || rootDir;
this._fullConfig.forbidOnly = takeFirst(this._configOverrides.forbidOnly, this._config.forbidOnly, baseFullConfig.forbidOnly);
this._fullConfig.fullyParallel = takeFirst(this._configOverrides.fullyParallel, this._config.fullyParallel, baseFullConfig.fullyParallel);
this._fullConfig.globalSetup = takeFirst(this._configOverrides.globalSetup, this._config.globalSetup, baseFullConfig.globalSetup);
this._fullConfig.globalTeardown = takeFirst(this._configOverrides.globalTeardown, this._config.globalTeardown, baseFullConfig.globalTeardown);
this._fullConfig.globalTimeout = takeFirst(this._configOverrides.globalTimeout, this._configOverrides.globalTimeout, this._config.globalTimeout, baseFullConfig.globalTimeout);
this._fullConfig.grep = takeFirst(this._configOverrides.grep, this._config.grep, baseFullConfig.grep);
this._fullConfig.grepInvert = takeFirst(this._configOverrides.grepInvert, this._config.grepInvert, baseFullConfig.grepInvert);
this._fullConfig.maxFailures = takeFirst(this._configOverrides.maxFailures, this._config.maxFailures, baseFullConfig.maxFailures);
this._fullConfig.preserveOutput = takeFirst<PreserveOutput>(this._configOverrides.preserveOutput, this._config.preserveOutput, baseFullConfig.preserveOutput);
this._fullConfig.reporter = takeFirst(toReporters(this._configOverrides.reporter as any), resolveReporters(this._config.reporter, rootDir), baseFullConfig.reporter);
this._fullConfig.reportSlowTests = takeFirst(this._configOverrides.reportSlowTests, this._config.reportSlowTests, baseFullConfig.reportSlowTests);
this._fullConfig.quiet = takeFirst(this._configOverrides.quiet, this._config.quiet, baseFullConfig.quiet);
this._fullConfig.shard = takeFirst(this._configOverrides.shard, this._config.shard, baseFullConfig.shard);
this._fullConfig.updateSnapshots = takeFirst(this._configOverrides.updateSnapshots, this._config.updateSnapshots, baseFullConfig.updateSnapshots);
this._fullConfig.workers = takeFirst(this._configOverrides.workers, this._config.workers, baseFullConfig.workers);
this._fullConfig.webServer = takeFirst(this._configOverrides.webServer, this._config.webServer, baseFullConfig.webServer);
this._fullConfig.rootDir = config.testDir || this._configDir;
this._fullConfig.forbidOnly = takeFirst(this._configOverrides.forbidOnly, config.forbidOnly, baseFullConfig.forbidOnly);
this._fullConfig.fullyParallel = takeFirst(this._configOverrides.fullyParallel, config.fullyParallel, baseFullConfig.fullyParallel);
this._fullConfig.globalSetup = takeFirst(this._configOverrides.globalSetup, config.globalSetup, baseFullConfig.globalSetup);
this._fullConfig.globalTeardown = takeFirst(this._configOverrides.globalTeardown, config.globalTeardown, baseFullConfig.globalTeardown);
this._fullConfig.globalTimeout = takeFirst(this._configOverrides.globalTimeout, this._configOverrides.globalTimeout, config.globalTimeout, baseFullConfig.globalTimeout);
this._fullConfig.grep = takeFirst(this._configOverrides.grep, config.grep, baseFullConfig.grep);
this._fullConfig.grepInvert = takeFirst(this._configOverrides.grepInvert, config.grepInvert, baseFullConfig.grepInvert);
this._fullConfig.maxFailures = takeFirst(this._configOverrides.maxFailures, config.maxFailures, baseFullConfig.maxFailures);
this._fullConfig.preserveOutput = takeFirst<PreserveOutput>(this._configOverrides.preserveOutput, config.preserveOutput, baseFullConfig.preserveOutput);
this._fullConfig.reporter = takeFirst(toReporters(this._configOverrides.reporter as any), resolveReporters(config.reporter, configDir), baseFullConfig.reporter);
this._fullConfig.reportSlowTests = takeFirst(this._configOverrides.reportSlowTests, config.reportSlowTests, baseFullConfig.reportSlowTests);
this._fullConfig.quiet = takeFirst(this._configOverrides.quiet, config.quiet, baseFullConfig.quiet);
this._fullConfig.shard = takeFirst(this._configOverrides.shard, config.shard, baseFullConfig.shard);
this._fullConfig.updateSnapshots = takeFirst(this._configOverrides.updateSnapshots, config.updateSnapshots, baseFullConfig.updateSnapshots);
this._fullConfig.workers = takeFirst(this._configOverrides.workers, config.workers, baseFullConfig.workers);
this._fullConfig.webServer = takeFirst(this._configOverrides.webServer, config.webServer, baseFullConfig.webServer);
const projects: Project[] = ('projects' in config) && config.projects !== undefined ? config.projects : [config];
for (const project of projects)
this._addProject(project, this._fullConfig.rootDir, rootDir);
this._addProject(config, project, throwawayArtifactsPath);
this._fullConfig.projects = this._projects.map(p => p.config);
}
@ -187,47 +195,49 @@ export class Loader {
serialize(): SerializedLoaderData {
return {
defaultConfig: this._defaultConfig,
configFile: this._configFile ? { file: this._configFile } : { rootDir: this._fullConfig.rootDir },
configFile: this._configFile ? { file: this._configFile } : { configDir: this._configDir },
overrides: this._configOverrides,
};
}
private _addProject(projectConfig: Project, rootDir: string, configDir: string) {
let testDir = takeFirst(projectConfig.testDir, rootDir);
if (!path.isAbsolute(testDir))
testDir = path.resolve(configDir, testDir);
let outputDir = takeFirst(this._configOverrides.outputDir, projectConfig.outputDir, this._config.outputDir, path.resolve(rootDir, 'test-results'));
if (!path.isAbsolute(outputDir))
outputDir = path.resolve(configDir, outputDir);
let snapshotDir = takeFirst(this._configOverrides.snapshotDir, projectConfig.snapshotDir, this._config.snapshotDir, testDir);
if (!path.isAbsolute(snapshotDir))
snapshotDir = path.resolve(configDir, snapshotDir);
const name = takeFirst(this._configOverrides.name, projectConfig.name, this._config.name, '');
let screenshotsDir = takeFirst(this._configOverrides.screenshotsDir, projectConfig.screenshotsDir, this._config.screenshotsDir, path.join(rootDir, '__screenshots__', process.platform, name));
if (!path.isAbsolute(screenshotsDir))
screenshotsDir = path.resolve(configDir, screenshotsDir);
private _addProject(config: Config, projectConfig: Project, throwawayArtifactsPath: string) {
// Resolve all config dirs relative to configDir.
if (projectConfig.testDir !== undefined)
projectConfig.testDir = path.resolve(this._configDir, projectConfig.testDir);
if (projectConfig.outputDir !== undefined)
projectConfig.outputDir = path.resolve(this._configDir, projectConfig.outputDir);
if (projectConfig.screenshotsDir !== undefined)
projectConfig.screenshotsDir = path.resolve(this._configDir, projectConfig.screenshotsDir);
if (projectConfig.snapshotDir !== undefined)
projectConfig.snapshotDir = path.resolve(this._configDir, projectConfig.snapshotDir);
const testDir = takeFirst(this._configOverrides.testDir, projectConfig.testDir, config.testDir, this._configDir);
const outputDir = takeFirst(this._configOverrides.outputDir, projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results'));
const snapshotDir = takeFirst(this._configOverrides.snapshotDir, projectConfig.snapshotDir, config.snapshotDir, testDir);
const name = takeFirst(this._configOverrides.name, projectConfig.name, config.name, '');
const screenshotsDir = takeFirst(this._configOverrides.screenshotsDir, projectConfig.screenshotsDir, config.screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name));
const fullProject: FullProject = {
fullyParallel: takeFirst(this._configOverrides.fullyParallel, projectConfig.fullyParallel, this._config.fullyParallel, undefined),
expect: takeFirst(this._configOverrides.expect, projectConfig.expect, this._config.expect, undefined),
grep: takeFirst(this._configOverrides.grep, projectConfig.grep, this._config.grep, baseFullConfig.grep),
grepInvert: takeFirst(this._configOverrides.grepInvert, projectConfig.grepInvert, this._config.grepInvert, baseFullConfig.grepInvert),
fullyParallel: takeFirst(this._configOverrides.fullyParallel, projectConfig.fullyParallel, config.fullyParallel, undefined),
expect: takeFirst(this._configOverrides.expect, projectConfig.expect, config.expect, undefined),
grep: takeFirst(this._configOverrides.grep, projectConfig.grep, config.grep, baseFullConfig.grep),
grepInvert: takeFirst(this._configOverrides.grepInvert, projectConfig.grepInvert, config.grepInvert, baseFullConfig.grepInvert),
outputDir,
repeatEach: takeFirst(this._configOverrides.repeatEach, projectConfig.repeatEach, this._config.repeatEach, 1),
retries: takeFirst(this._configOverrides.retries, projectConfig.retries, this._config.retries, 0),
metadata: takeFirst(this._configOverrides.metadata, projectConfig.metadata, this._config.metadata, undefined),
repeatEach: takeFirst(this._configOverrides.repeatEach, projectConfig.repeatEach, config.repeatEach, 1),
retries: takeFirst(this._configOverrides.retries, projectConfig.retries, config.retries, 0),
metadata: takeFirst(this._configOverrides.metadata, projectConfig.metadata, config.metadata, undefined),
name,
testDir,
snapshotDir,
screenshotsDir,
testIgnore: takeFirst(this._configOverrides.testIgnore, projectConfig.testIgnore, this._config.testIgnore, []),
testMatch: takeFirst(this._configOverrides.testMatch, projectConfig.testMatch, this._config.testMatch, '**/?(*.)@(spec|test).*'),
timeout: takeFirst(this._configOverrides.timeout, projectConfig.timeout, this._config.timeout, 10000),
use: mergeObjects(mergeObjects(this._config.use, projectConfig.use), this._configOverrides.use),
testIgnore: takeFirst(this._configOverrides.testIgnore, projectConfig.testIgnore, config.testIgnore, []),
testMatch: takeFirst(this._configOverrides.testMatch, projectConfig.testMatch, config.testMatch, '**/?(*.)@(spec|test).*'),
timeout: takeFirst(this._configOverrides.timeout, projectConfig.timeout, config.timeout, 10000),
use: mergeObjects(mergeObjects(config.use, projectConfig.use), this._configOverrides.use),
};
this._projects.push(new ProjectImpl(fullProject, this._projects.length));
}
private async _requireOrImport(file: string) {
const revertBabelRequire = installTransform();
const isModule = fileIsModule(file);
@ -482,25 +492,35 @@ export function fileIsModule(file: string): boolean {
return folderIsModule(folder);
}
const folderToIsModuleCache = new Map<string, { isModule: boolean }>();
const folderToPackageJsonPath = new Map<string, string>();
function getPackageJsonPath(folderPath: string): string {
const cached = folderToPackageJsonPath.get(folderPath);
if (cached !== undefined)
return cached;
const packageJsonPath = path.join(folderPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
folderToPackageJsonPath.set(folderPath, packageJsonPath);
return packageJsonPath;
}
const parentFolder = path.dirname(folderPath);
if (folderPath === parentFolder) {
folderToPackageJsonPath.set(folderPath, '');
return '';
}
const result = getPackageJsonPath(parentFolder);
folderToPackageJsonPath.set(folderPath, result);
return result;
}
export function folderIsModule(folder: string): boolean {
// Fast track.
const cached = folderToIsModuleCache.get(folder);
if (cached)
return cached.isModule;
const packageJson = path.join(folder, 'package.json');
let isModule = false;
if (fs.existsSync(packageJson)) {
isModule = require(packageJson).type === 'module';
} else {
const parentFolder = path.dirname(folder);
if (parentFolder !== folder)
isModule = folderIsModule(parentFolder);
else
isModule = false;
}
folderToIsModuleCache.set(folder, { isModule });
return isModule;
const packageJsonPath = getPackageJsonPath(folder);
if (!packageJsonPath)
return false;
// Rely on `require` internal caching logic.
return require(packageJsonPath).type === 'module';
}

View File

@ -239,7 +239,7 @@ interface TestProject {
*/
screenshotsDir?: string;
/**
* The output directory for files created during test execution. Defaults to `test-results`.
* The output directory for files created during test execution. Defaults to `<package.json-directory>/test-results`.
*
* This directory is cleaned at the start. When running a test, a unique subdirectory inside the
* [testProject.outputDir](https://playwright.dev/docs/api/class-testproject#test-project-output-dir) is created,
@ -828,7 +828,7 @@ interface TestConfig {
*/
screenshotsDir?: string;
/**
* The output directory for files created during test execution. Defaults to `test-results`.
* The output directory for files created during test execution. Defaults to `<package.json-directory>/test-results`.
*
* ```ts
* // playwright.config.ts

View File

@ -69,6 +69,12 @@ async function writeFiles(testInfo: TestInfo, files: Files) {
`,
};
}
if (!Object.keys(files).some(name => name.includes('package.json'))) {
files = {
...files,
'package.json': `{ "name": "test-project" }`,
};
}
await Promise.all(Object.keys(files).map(async name => {
const fullName = path.join(baseDir, name);
@ -142,7 +148,7 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
NODE_OPTIONS: undefined,
...env,
},
cwd: baseDir,
cwd: options.cwd ? path.resolve(baseDir, options.cwd) : baseDir,
});
let didSendSigint = false;
testProcess.onOutput = () => {
@ -206,6 +212,7 @@ type RunOptions = {
sendSIGINTAfter?: number;
usesCustomOutputDir?: boolean;
additionalArgs?: string[];
cwd?: string,
};
type Fixtures = {
writeFiles: (files: Files) => Promise<string>;

View File

@ -83,6 +83,32 @@ test('should include repeat token', async ({ runInlineTest }) => {
expect(result.passed).toBe(3);
});
test('should default to package.json directory', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'foo/package.json': `{ "name": "foo" }`,
'foo/bar/playwright.config.js': `
module.exports = { projects: [ {} ] };
`,
'foo/bar/baz/tests/a.spec.js': `
const { test } = pwt;
const fs = require('fs');
test('pass', ({}, testInfo) => {
expect(process.cwd()).toBe(__dirname);
fs.writeFileSync(testInfo.outputPath('foo.ts'), 'foobar');
});
`
}, { 'reporter': '' }, {}, {
cwd: 'foo/bar/baz/tests',
usesCustomOutputDir: true
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(fs.existsSync(testInfo.outputPath('test-results'))).toBe(false);
expect(fs.existsSync(testInfo.outputPath('foo', 'test-results'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'test-results'))).toBe(false);
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'tests', 'test-results'))).toBe(false);
});
test('should be unique for beforeAll hook from different workers', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'a.spec.js': `