Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/src/test-api/class-testconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,24 @@ export default defineConfig({
});
```

## property: TestConfig.importConditions
* since: v1.57
- type: ?<[Array]<[string]>>

Additional Node.js import conditions to use when resolving package exports. This is useful for packages that use [conditional exports](https://nodejs.org/api/packages.html#conditional-exports) to expose different entry points based on conditions.

These conditions are added to Node.js's default conditions (`node`, `import`, `require`, etc.) and apply only when running tests, not when loading the configuration file.

**Usage**

```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';

export default defineConfig({
importConditions: ['custom', 'development'],
});
```

## property: TestConfig.maxFailures
* since: v1.10
- type: ?<[int]>
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright/src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class FullConfigInternal {
readonly plugins: TestRunnerPluginRegistration[];
readonly projects: FullProjectInternal[] = [];
readonly singleTSConfigPath?: string;
readonly importConditions?: string[];
readonly captureGitInfo: Config['captureGitInfo'];
readonly failOnFlakyTests: boolean;
cliArgs: string[] = [];
Expand Down Expand Up @@ -79,6 +80,7 @@ export class FullConfigInternal {
const privateConfiguration = (userConfig as any)['@playwright/test'];
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig);
this.importConditions = userConfig.importConditions;
this.captureGitInfo = userConfig.captureGitInfo;
this.failOnFlakyTests = takeFirst(configCLIOverrides.failOnFlakyTests, userConfig.failOnFlakyTests, false);

Expand Down
9 changes: 9 additions & 0 deletions packages/playwright/src/common/configLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,15 @@ function validateConfig(file: string, config: Config) {
if (!fs.existsSync(path.resolve(file, '..', config.tsconfig)))
throw errorWithFile(file, `config.tsconfig does not exist`);
}

if ('importConditions' in config && config.importConditions !== undefined) {
if (!Array.isArray(config.importConditions))
throw errorWithFile(file, `config.importConditions must be an array`);
config.importConditions.forEach((condition, index) => {
if (typeof condition !== 'string')
throw errorWithFile(file, `config.importConditions[${index}] must be a string`);
});
}
}

function validateProject(file: string, project: Project, title: string) {
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright/src/runner/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,12 @@ export class Dispatcher {
_createWorker(testGroup: TestGroup, parallelIndex: number, loaderData: SerializedConfig) {
const projectConfig = this._config.projects.find(p => p.id === testGroup.projectId)!;
const outputDir = projectConfig.project.outputDir;
const execArgv = this._config.importConditions?.map(condition => `--conditions=${condition}`);
const worker = new WorkerHost(testGroup, {
parallelIndex,
config: loaderData,
extraEnv: this._extraEnvByProjectId.get(testGroup.projectId) || {},
execArgv,
outputDir,
pauseOnError: this._failureTracker.pauseOnError(),
pauseAtEnd: this._failureTracker.pauseAtEnd(projectConfig),
Expand Down
3 changes: 2 additions & 1 deletion packages/playwright/src/runner/processHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@ export class ProcessHost extends EventEmitter {
this._extraEnv = env;
}

async startRunner(runnerParams: any, options: { onStdOut?: (chunk: Buffer | string) => void, onStdErr?: (chunk: Buffer | string) => void } = {}): Promise<ProcessExitData | undefined> {
async startRunner(runnerParams: any, options: { onStdOut?: (chunk: Buffer | string) => void, onStdErr?: (chunk: Buffer | string) => void, execArgv?: string[] } = {}): Promise<ProcessExitData | undefined> {
assert(!this.process, 'Internal error: starting the same process twice');
this.process = child_process.fork(require.resolve('../common/process'), {
detached: false,
env: {
...process.env,
...this._extraEnv,
},
...(options.execArgv ? { execArgv: options.execArgv } : {}),
stdio: [
'ignore',
options.onStdOut ? 'pipe' : 'inherit',
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright/src/runner/workerHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type WorkerHostOptions = {
parallelIndex: number;
config: SerializedConfig;
extraEnv: Record<string, string | undefined>;
execArgv?: string[];
outputDir: string;
pauseOnError: boolean;
pauseAtEnd: boolean;
Expand All @@ -43,6 +44,7 @@ export class WorkerHost extends ProcessHost {
readonly workerIndex: number;
private _hash: string;
private _params: WorkerInitParams;
private _execArgv?: string[];
private _didFail = false;

constructor(testGroup: TestGroup, options: WorkerHostOptions) {
Expand All @@ -55,6 +57,7 @@ export class WorkerHost extends ProcessHost {
this.workerIndex = workerIndex;
this.parallelIndex = options.parallelIndex;
this._hash = testGroup.workerHash;
this._execArgv = options.execArgv;

this._params = {
workerIndex: this.workerIndex,
Expand All @@ -73,6 +76,7 @@ export class WorkerHost extends ProcessHost {
return await this.startRunner(this._params, {
onStdOut: chunk => this.emit('stdOut', stdioChunkToParams(chunk)),
onStdErr: chunk => this.emit('stdErr', stdioChunkToParams(chunk)),
execArgv: this._execArgv,
});
}

Expand Down
22 changes: 22 additions & 0 deletions packages/playwright/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,28 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
*/
ignoreSnapshots?: boolean;

/**
* Additional Node.js import conditions to use when resolving package exports. This is useful for packages that use
* [conditional exports](https://nodejs.org/api/packages.html#conditional-exports) to expose different entry points
* based on conditions.
*
* These conditions are added to Node.js's default conditions (`node`, `import`, `require`, etc.) and apply only when
* running tests, not when loading the configuration file.
*
* **Usage**
*
* ```js
* // playwright.config.ts
* import { defineConfig } from '@playwright/test';
*
* export default defineConfig({
* importConditions: ['custom', 'development'],
* });
* ```
*
*/
importConditions?: Array<string>;

/**
* The maximum number of test failures for the whole test suite run. After reaching this number, testing will stop and
* exit with an error. Setting to zero (default) disables this behavior.
Expand Down
223 changes: 223 additions & 0 deletions tests/playwright-test/importConditions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/**
* 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';

// Config Validation Tests
test('should validate importConditions is an array', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
importConditions: 'invalid',
};
`,
'a.test.ts': `
import { test } from '@playwright/test';
test('dummy', () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain('config.importConditions must be an array');
});

test('should validate importConditions array elements are strings', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
importConditions: ['valid', 123, 'another'],
};
`,
'a.test.ts': `
import { test } from '@playwright/test';
test('dummy', () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain('config.importConditions[1] must be a string');
});

test('should allow empty importConditions array', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
importConditions: [],
};
`,
'a.test.ts': `
import { test } from '@playwright/test';
test('passes', () => {});
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

test('should allow undefined importConditions', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
importConditions: undefined,
};
`,
'a.test.ts': `
import { test } from '@playwright/test';
test('passes', () => {});
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

// execArgv Passing Tests
test('should pass importConditions to worker process.execArgv', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
importConditions: ['custom', 'development'],
};
`,
'a.test.ts': `
import { test } from '@playwright/test';
test('verify execArgv', () => {
console.log('%%execArgv=' + JSON.stringify(process.execArgv));
});
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(result.output).toContain('--conditions=custom');
expect(result.output).toContain('--conditions=development');
});

test('should pass single importCondition to worker', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
importConditions: ['test-condition'],
};
`,
'a.test.ts': `
import { test } from '@playwright/test';
test('verify single condition', () => {
const hasCondition = process.execArgv.some(arg => arg === '--conditions=test-condition');
console.log('%%hasCondition=' + hasCondition);
});
`
});
expect(result.exitCode).toBe(0);
expect(result.output).toContain('hasCondition=true');
});

test('should not pass conditions when importConditions is empty', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
importConditions: [],
};
`,
'a.test.ts': `
import { test } from '@playwright/test';
test('verify no conditions', () => {
const hasConditions = process.execArgv.some(arg => arg.startsWith('--conditions='));
console.log('%%hasConditions=' + hasConditions);
});
`
});
expect(result.exitCode).toBe(0);
expect(result.output).toContain('hasConditions=false');
});

// Integration Tests
test('should run tests successfully with importConditions', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
importConditions: ['custom'],
};
`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('should pass', () => {
expect(1 + 1).toBe(2);
});
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

test('should work with multiple workers', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
importConditions: ['test'],
workers: 2,
};
`,
'a.test.ts': `
import { test } from '@playwright/test';
test('test 1', () => {
console.log('%%worker-' + process.env.TEST_WORKER_INDEX);
});
`,
'b.test.ts': `
import { test } from '@playwright/test';
test('test 2', () => {
console.log('%%worker-' + process.env.TEST_WORKER_INDEX);
});
`
}, { workers: 2 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
});

// Edge Cases
test('should handle many conditions', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
importConditions: ['cond1', 'cond2', 'cond3', 'cond4', 'cond5'],
};
`,
'a.test.ts': `
import { test } from '@playwright/test';
test('verify all conditions', () => {
const conditions = process.execArgv.filter(arg => arg.startsWith('--conditions='));
console.log('%%conditionCount=' + conditions.length);
});
`
});
expect(result.exitCode).toBe(0);
expect(result.output).toContain('conditionCount=5');
});

test('should not interfere with existing tests when omitted', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
workers: 1,
};
`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('normal test', () => {
expect(true).toBe(true);
});
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});