chore: fixtures-via-plugin implementation (#13950)

This commit is contained in:
Pavel Feldman 2022-05-05 09:14:00 -08:00 committed by GitHub
parent 7e6439d19c
commit 058f32caff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 297 additions and 35 deletions

View file

@ -319,6 +319,9 @@ The directory for each test can be accessed by [`property: TestInfo.snapshotDir`
This path will serve as the base directory for each test file snapshot directory. Setting `snapshotDir` to `'snapshots'`, the [`property: TestInfo.snapshotDir`] would resolve to `snapshots/a.spec.js-snapshots`. This path will serve as the base directory for each test file snapshot directory. Setting `snapshotDir` to `'snapshots'`, the [`property: TestInfo.snapshotDir`] would resolve to `snapshots/a.spec.js-snapshots`.
## property: TestConfig.plugins
- type: ?<[Array]<[TestPlugin]|[string]>>
## property: TestConfig.preserveOutput ## property: TestConfig.preserveOutput
- type: ?<[PreserveOutput]<"always"|"never"|"failures-only">> - type: ?<[PreserveOutput]<"always"|"never"|"failures-only">>

View file

@ -0,0 +1,20 @@
# class: TestPlugin
* langs: js
## property: TestPlugin.name
- type: <[string]>
## optional async method: TestPlugin.setup
### param: TestPlugin.setup.config
- `config` <[FullConfig]>
### param: TestPlugin.setup.configDir
- `configDir` <[string]>
### param: TestPlugin.setup.suite
- `suite` <[Suite]>
## optional async method: TestPlugin.teardown
## optional property: TestPlugin.fixtures
- `fixtures` <[any]>

6
package-lock.json generated
View file

@ -5707,7 +5707,7 @@
}, },
"packages/playwright-ct-react": { "packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "0.0.5", "version": "0.0.7",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@vitejs/plugin-react": "^1.0.7", "@vitejs/plugin-react": "^1.0.7",
@ -5722,7 +5722,7 @@
}, },
"packages/playwright-ct-svelte": { "packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "0.0.5", "version": "0.0.7",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.30", "@sveltejs/vite-plugin-svelte": "^1.0.0-next.30",
@ -5737,7 +5737,7 @@
}, },
"packages/playwright-ct-vue": { "packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "0.0.5", "version": "0.0.7",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^2.3.1", "@vitejs/plugin-vue": "^2.3.1",

View file

@ -15,7 +15,7 @@
*/ */
import { installTransform, setCurrentlyLoadingTestFile } from './transform'; import { installTransform, setCurrentlyLoadingTestFile } from './transform';
import type { Config, Project, ReporterDescription, FullProjectInternal, FullConfigInternal, Fixtures, FixturesWithLocation } from './types'; import type { Config, Project, ReporterDescription, FullProjectInternal, FullConfigInternal, Fixtures, FixturesWithLocation, TestPlugin } from './types';
import { getPackageJsonPath, mergeObjects, errorWithFile } from './util'; import { getPackageJsonPath, mergeObjects, errorWithFile } from './util';
import { setCurrentlyLoadingFileSuite } from './globals'; import { setCurrentlyLoadingFileSuite } from './globals';
import { Suite, type TestCase } from './test'; import { Suite, type TestCase } from './test';
@ -63,9 +63,7 @@ export class Loader {
async loadConfigFile(file: string): Promise<FullConfigInternal> { async loadConfigFile(file: string): Promise<FullConfigInternal> {
if (this._configFile) if (this._configFile)
throw new Error('Cannot load two config files'); throw new Error('Cannot load two config files');
let config = await this._requireOrImport(file) as Config; const config = await this._requireOrImportDefaultObject(file) as Config;
if (config && typeof config === 'object' && ('default' in config))
config = (config as any)['default'];
this._configFile = file; this._configFile = file;
await this._processConfigObject(config, path.dirname(file)); await this._processConfigObject(config, path.dirname(file));
return this._fullConfig; return this._fullConfig;
@ -125,6 +123,19 @@ export class Loader {
if (config.snapshotDir !== undefined) if (config.snapshotDir !== undefined)
config.snapshotDir = path.resolve(configDir, config.snapshotDir); config.snapshotDir = path.resolve(configDir, config.snapshotDir);
config.plugins = await Promise.all((config.plugins || []).map(async plugin => {
if (typeof plugin === 'string')
return (await this._requireOrImportDefaultObject(resolveScript(plugin, configDir))) as TestPlugin;
return plugin;
}));
for (const plugin of config.plugins || []) {
if (!plugin.fixtures)
continue;
if (typeof plugin.fixtures === 'string')
plugin.fixtures = await this._requireOrImportDefaultObject(resolveScript(plugin.fixtures, configDir));
}
this._fullConfig._configDir = configDir; this._fullConfig._configDir = configDir;
this._fullConfig.rootDir = config.testDir || this._configDir; this._fullConfig.rootDir = config.testDir || this._configDir;
this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir); this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir);
@ -144,8 +155,9 @@ export class Loader {
this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots); this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots);
this._fullConfig.workers = takeFirst(config.workers, baseFullConfig.workers); this._fullConfig.workers = takeFirst(config.workers, baseFullConfig.workers);
this._fullConfig.webServer = takeFirst(config.webServer, baseFullConfig.webServer); this._fullConfig.webServer = takeFirst(config.webServer, baseFullConfig.webServer);
this._fullConfig._plugins = takeFirst(config.plugins, baseFullConfig._plugins);
this._fullConfig.metadata = takeFirst(config.metadata, baseFullConfig.metadata); this._fullConfig.metadata = takeFirst(config.metadata, baseFullConfig.metadata);
this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, p, throwawayArtifactsPath)); this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, this._fullConfig, p, throwawayArtifactsPath));
} }
async loadTestFile(file: string, environment: 'runner' | 'worker') { async loadTestFile(file: string, environment: 'runner' | 'worker') {
@ -193,21 +205,11 @@ export class Loader {
} }
async loadGlobalHook(file: string, name: string): Promise<(config: FullConfigInternal) => any> { async loadGlobalHook(file: string, name: string): Promise<(config: FullConfigInternal) => any> {
let hook = await this._requireOrImport(file); return this._requireOrImportDefaultFunction(path.resolve(this._fullConfig.rootDir, file), false);
if (hook && typeof hook === 'object' && ('default' in hook))
hook = hook['default'];
if (typeof hook !== 'function')
throw errorWithFile(file, `${name} file must export a single function.`);
return hook;
} }
async loadReporter(file: string): Promise<new (arg?: any) => Reporter> { async loadReporter(file: string): Promise<new (arg?: any) => Reporter> {
let func = await this._requireOrImport(path.resolve(this._fullConfig.rootDir, file)); return this._requireOrImportDefaultFunction(path.resolve(this._fullConfig.rootDir, file), true);
if (func && typeof func === 'object' && ('default' in func))
func = func['default'];
if (typeof func !== 'function')
throw errorWithFile(file, `reporter file must export a single class.`);
return func;
} }
fullConfig(): FullConfigInternal { fullConfig(): FullConfigInternal {
@ -241,7 +243,7 @@ export class Loader {
projectConfig.use = mergeObjects(projectConfig.use, this._configCLIOverrides.use); projectConfig.use = mergeObjects(projectConfig.use, this._configCLIOverrides.use);
} }
private _resolveProject(config: Config, projectConfig: Project, throwawayArtifactsPath: string): FullProjectInternal { private _resolveProject(config: Config, fullConfig: FullConfigInternal, projectConfig: Project, throwawayArtifactsPath: string): FullProjectInternal {
// Resolve all config dirs relative to configDir. // Resolve all config dirs relative to configDir.
if (projectConfig.testDir !== undefined) if (projectConfig.testDir !== undefined)
projectConfig.testDir = path.resolve(this._configDir, projectConfig.testDir); projectConfig.testDir = path.resolve(this._configDir, projectConfig.testDir);
@ -259,6 +261,7 @@ export class Loader {
const name = takeFirst(projectConfig.name, config.name, ''); const name = takeFirst(projectConfig.name, config.name, '');
const screenshotsDir = takeFirst((projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name)); const screenshotsDir = takeFirst((projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name));
return { return {
_fullConfig: fullConfig,
_fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined), _fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined),
_expect: takeFirst(projectConfig.expect, config.expect, {}), _expect: takeFirst(projectConfig.expect, config.expect, {}),
grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep), grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep),
@ -308,22 +311,38 @@ ${'='.repeat(80)}\n`);
revertBabelRequire(); revertBabelRequire();
} }
} }
private async _requireOrImportDefaultFunction(file: string, expectConstructor: boolean) {
let func = await this._requireOrImport(file);
if (func && typeof func === 'object' && ('default' in func))
func = func['default'];
if (typeof func !== 'function')
throw errorWithFile(file, `file must export a single ${expectConstructor ? 'class' : 'function'}.`);
return func;
}
private async _requireOrImportDefaultObject(file: string) {
let object = await this._requireOrImport(file);
if (object && typeof object === 'object' && ('default' in object))
object = object['default'];
return object;
}
} }
class ProjectSuiteBuilder { class ProjectSuiteBuilder {
private _config: FullProjectInternal; private _project: FullProjectInternal;
private _index: number; private _index: number;
private _testTypePools = new Map<TestTypeImpl, FixturePool>(); private _testTypePools = new Map<TestTypeImpl, FixturePool>();
private _testPools = new Map<TestCase, FixturePool>(); private _testPools = new Map<TestCase, FixturePool>();
constructor(project: FullProjectInternal, index: number) { constructor(project: FullProjectInternal, index: number) {
this._config = project; this._project = project;
this._index = index; this._index = index;
} }
private _buildTestTypePool(testType: TestTypeImpl): FixturePool { private _buildTestTypePool(testType: TestTypeImpl): FixturePool {
if (!this._testTypePools.has(testType)) { if (!this._testTypePools.has(testType)) {
const fixtures = this._applyConfigUseOptions(testType, this._config.use || {}); const fixtures = this._applyConfigUseOptions(testType, this._project.use || {});
const pool = new FixturePool(fixtures); const pool = new FixturePool(fixtures);
this._testTypePools.set(testType, pool); this._testTypePools.set(testType, pool);
} }
@ -335,6 +354,16 @@ class ProjectSuiteBuilder {
if (!this._testPools.has(test)) { if (!this._testPools.has(test)) {
let pool = this._buildTestTypePool(test._testType); let pool = this._buildTestTypePool(test._testType);
for (const plugin of this._project._fullConfig._plugins) {
if (!plugin.fixtures)
continue;
const pluginFixturesWithLocation: FixturesWithLocation = {
fixtures: plugin.fixtures,
location: { file: '', line: 0, column: 0 },
};
pool = new FixturePool([pluginFixturesWithLocation], pool, false);
}
const parents: Suite[] = []; const parents: Suite[] = [];
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
parents.push(parent); parents.push(parent);
@ -366,7 +395,7 @@ class ProjectSuiteBuilder {
} }
} else { } else {
const test = entry._clone(); const test = entry._clone();
test.retries = this._config.retries; test.retries = this._project.retries;
// We rely upon relative paths being unique. // We rely upon relative paths being unique.
// See `getClashingTestsPerSuite()` in `runner.ts`. // See `getClashingTestsPerSuite()` in `runner.ts`.
test._id = `${calculateSha1(relativeTitlePath + ' ' + entry.title)}@${entry._requireFile}#run${this._index}-repeat${repeatEachIndex}`; test._id = `${calculateSha1(relativeTitlePath + ' ' + entry.title)}@${entry._requireFile}#run${this._index}-repeat${repeatEachIndex}`;
@ -624,6 +653,7 @@ export const baseFullConfig: FullConfigInternal = {
_globalOutputDir: path.resolve(process.cwd()), _globalOutputDir: path.resolve(process.cwd()),
_configDir: '', _configDir: '',
_testGroupsCount: 0, _testGroupsCount: 0,
_plugins: [],
}; };
function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined { function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined {

View file

@ -167,7 +167,7 @@ class RawReporter {
const project = suite.project(); const project = suite.project();
assert(project, 'Internal Error: Invalid project structure'); assert(project, 'Internal Error: Invalid project structure');
const report: JsonReport = { const report: JsonReport = {
config, config: filterOutPrivateFields(config),
project: { project: {
metadata: project.metadata, metadata: project.metadata,
name: project.name, name: project.name,
@ -317,4 +317,12 @@ function dedupeSteps(steps: JsonTestStep[]): JsonTestStep[] {
return result; return result;
} }
function filterOutPrivateFields(object: any): any {
if (!object || typeof object !== 'object')
return object;
if (Array.isArray(object))
return object.map(filterOutPrivateFields);
return Object.fromEntries(Object.entries(object).filter(entry => !entry[0].startsWith('_')).map(entry => [entry[0], filterOutPrivateFields(entry[1])]));
}
export default RawReporter; export default RawReporter;

View file

@ -434,7 +434,6 @@ export class Runner {
private async _performGlobalSetup(config: FullConfigInternal, rootSuite: Suite): Promise<(() => Promise<void>) | undefined> { private async _performGlobalSetup(config: FullConfigInternal, rootSuite: Suite): Promise<(() => Promise<void>) | undefined> {
const result: FullResult = { status: 'passed' }; const result: FullResult = { status: 'passed' };
const pluginTeardowns: (() => Promise<void>)[] = [];
let globalSetupResult: any; let globalSetupResult: any;
const tearDown = async () => { const tearDown = async () => {
@ -449,9 +448,9 @@ export class Runner {
await (await this._loader.loadGlobalHook(config.globalTeardown, 'globalTeardown'))(this._loader.fullConfig()); await (await this._loader.loadGlobalHook(config.globalTeardown, 'globalTeardown'))(this._loader.fullConfig());
}, result); }, result);
for (const teardown of pluginTeardowns) { for (const plugin of [...this._plugins, ...config._plugins].reverse()) {
await this._runAndReportError(async () => { await this._runAndReportError(async () => {
await teardown(); await plugin.teardown?.();
}, result); }, result);
} }
}; };
@ -463,11 +462,8 @@ export class Runner {
// First run the plugins, if plugin is a web server we want it to run before the // First run the plugins, if plugin is a web server we want it to run before the
// config's global setup. // config's global setup.
for (const plugin of this._plugins) { for (const plugin of [...this._plugins, ...config._plugins])
await plugin.setup?.(config, config._configDir, rootSuite); await plugin.setup?.(config, config._configDir, rootSuite);
if (plugin.teardown)
pluginTeardowns.unshift(plugin.teardown);
}
// The do global setup. // The do global setup.
if (config.globalSetup) if (config.globalSetup)

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Fixtures, TestError, Project } from '../types/test'; import type { Fixtures, TestError, Project, TestPlugin } from '../types/test';
import type { Location } from '../types/testReporter'; import type { Location } from '../types/testReporter';
import type { FullConfig as FullConfigPublic, FullProject as FullProjectPublic } from './types'; import type { FullConfig as FullConfigPublic, FullProject as FullProjectPublic } from './types';
export * from '../types/test'; export * from '../types/test';
@ -44,6 +44,7 @@ export interface FullConfigInternal extends FullConfigPublic {
_globalOutputDir: string; _globalOutputDir: string;
_configDir: string; _configDir: string;
_testGroupsCount: number; _testGroupsCount: number;
_plugins: TestPlugin[];
// Overrides the public field. // Overrides the public field.
projects: FullProjectInternal[]; projects: FullProjectInternal[];
@ -54,6 +55,7 @@ export interface FullConfigInternal extends FullConfigPublic {
* increasing the surface area of the public API type called FullProject. * increasing the surface area of the public API type called FullProject.
*/ */
export interface FullProjectInternal extends FullProjectPublic { export interface FullProjectInternal extends FullProjectPublic {
_fullConfig: FullConfigInternal;
_fullyParallel: boolean; _fullyParallel: boolean;
_expect: Project['expect']; _expect: Project['expect'];
_screenshotsDir: string; _screenshotsDir: string;

View file

@ -366,6 +366,22 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never }); type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never });
/**
*
*/
export interface TestPlugin {
fixtures?: Fixtures;
name: string;
/**
* @param config
* @param configDir
* @param suite
*/
setup?(config: FullConfig, configDir: string, suite: Suite): Promise<void>;
teardown?(): Promise<void>;}
/** /**
* Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or * Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or
* `testDir`. These options are described in the [TestConfig] object in the [configuration file](https://playwright.dev/docs/test-configuration). * `testDir`. These options are described in the [TestConfig] object in the [configuration file](https://playwright.dev/docs/test-configuration).
@ -459,6 +475,7 @@ interface TestConfig {
* *
*/ */
webServer?: TestConfigWebServer; webServer?: TestConfigWebServer;
plugins?: TestPlugin[],
/** /**
* Configuration for the `expect` assertion library. Learn more about [various timeouts](https://playwright.dev/docs/test-timeouts). * Configuration for the `expect` assertion library. Learn more about [various timeouts](https://playwright.dev/docs/test-timeouts).
* *

View file

@ -11,6 +11,8 @@
# production # production
/build /build
/dist-pw
# misc # misc
.DS_Store .DS_Store
.env.local .env.local

View file

@ -17026,6 +17026,22 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never }); type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never });
/**
*
*/
export interface TestPlugin {
fixtures?: Fixtures;
name: string;
/**
* @param config
* @param configDir
* @param suite
*/
setup?(config: FullConfig, configDir: string, suite: Suite): Promise<void>;
teardown?(): Promise<void>;}
/** /**
* Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or * Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or
* `testDir`. These options are described in the [TestConfig] object in the [configuration file](https://playwright.dev/docs/test-configuration). * `testDir`. These options are described in the [TestConfig] object in the [configuration file](https://playwright.dev/docs/test-configuration).
@ -17119,6 +17135,7 @@ interface TestConfig {
* *
*/ */
webServer?: TestConfigWebServer; webServer?: TestConfigWebServer;
plugins?: TestPlugin[],
/** /**
* Configuration for the `expect` assertion library. Learn more about [various timeouts](https://playwright.dev/docs/test-timeouts). * Configuration for the `expect` assertion library. Learn more about [various timeouts](https://playwright.dev/docs/test-timeouts).
* *

View file

@ -184,7 +184,7 @@ test('globalSetup should throw when passed non-function', async ({ runInlineTest
}); });
`, `,
}); });
expect(output).toContain(`globalSetup.ts: globalSetup file must export a single function.`); expect(output).toContain(`globalSetup.ts: 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 }) => {

View file

@ -0,0 +1,162 @@
/**
* 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 fs from 'fs';
import { test, expect } from './playwright-test-fixtures';
test('event order', async ({ runInlineTest }, testInfo) => {
const log = testInfo.outputPath('logs.txt');
const result = await runInlineTest({
'log.ts': `
import { appendFileSync } from 'fs';
const log = (...args) => appendFileSync('${log.replace(/\\/g, '\\\\')}', args.join(' ') + '\\n');
export default log;
`,
'test.spec.ts': `
import log from './log';
const { test } = pwt;
test('it works', async ({}) => {
});
`,
'playwright.config.ts': `
import { myPlugin } from './plugin.ts';
module.exports = {
plugins: [
myPlugin('a'),
myPlugin('b'),
],
globalSetup: 'globalSetup.ts',
globalTeardown: 'globalTeardown.ts',
};
`,
'globalSetup.ts': `
import log from './log';
const setup = async () => {
await new Promise(r => setTimeout(r, 100));
log('globalSetup');
}
export default setup;
`,
'globalTeardown.ts': `
import log from './log';
const teardown = async () => {
await new Promise(r => setTimeout(r, 100));
log('globalTeardown');
}
export default teardown;
`,
'plugin.ts': `
import log from './log';
export const myPlugin = (name: string) => ({
setup: async () => {
await new Promise(r => setTimeout(r, 100));
log(name, 'setup');
},
teardown: async () => {
await new Promise(r => setTimeout(r, 100));
log(name, 'teardown');
},
});
`,
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
const logLines = await fs.promises.readFile(log, 'utf8');
expect(logLines.split('\n')).toEqual([
'a setup',
'b setup',
'globalSetup',
'globalTeardown',
'b teardown',
'a teardown',
'',
]);
});
test('plugins via require', async ({ runInlineTest }) => {
const result = await runInlineTest({
'test.spec.ts': `
const { test } = pwt;
test('it works', async ({}) => {
expect(process.env.PW_CONFIG_DIR).toContain('plugins-via-require');
});
`,
'playwright.config.ts': `
export default { plugins: [ 'plugin.ts' ] };
`,
'plugin.ts': `
export function setup(config, configDir, suite) {
process.env.PW_CONFIG_DIR = configDir;
};
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('fixtures', async ({ runInlineTest }) => {
const result = await runInlineTest({
'test.spec.ts': `
const { test } = pwt;
test('it works', async ({ foo }) => {
expect(foo).toEqual(42);
});
test('it uses standard fixture', async ({ myBrowserName }) => {
expect(myBrowserName).toEqual('chromium');
});
`,
'playwright.config.ts': `
import plugin from './plugin.ts';
module.exports = {
plugins: [ plugin ],
};
`,
'plugin.ts': `
export default {
fixtures: {
foo: 42,
myBrowserName: async ({ browserName }, use) => { await use(browserName) }
}
};
`,
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
});
test('fixtures via require', async ({ runInlineTest }) => {
const result = await runInlineTest({
'test.spec.ts': `
const { test } = pwt;
test('it works', async ({ foo }) => {
expect(foo).toEqual(42);
});
`,
'playwright.config.ts': `
export default {
plugins: [ { fixtures: require.resolve('./fixtures.ts') } ],
};
`,
'fixtures.ts': `
//@no-header
export default {
foo: 42
};
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

View file

@ -56,9 +56,14 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never }); type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never });
export interface TestPlugin {
fixtures?: Fixtures;
}
interface TestConfig { interface TestConfig {
reporter?: LiteralUnion<'list'|'dot'|'line'|'github'|'json'|'junit'|'null'|'html', string> | ReporterDescription[]; reporter?: LiteralUnion<'list'|'dot'|'line'|'github'|'json'|'junit'|'null'|'html', string> | ReporterDescription[];
webServer?: TestConfigWebServer; webServer?: TestConfigWebServer;
plugins?: TestPlugin[],
} }
export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig { export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig {