Skip to content
Merged
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
6 changes: 3 additions & 3 deletions docs/src/test-api/class-testconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -704,9 +704,9 @@ export default defineConfig({
- `reuseExistingServer` ?<[boolean]> If true, it will re-use an existing server on the `port` or `url` when available. If no server is running on that `port` or `url`, it will run the command to start a new server. If `false`, it will throw if an existing process is listening on the `port` or `url`. This should be commonly set to `!process.env.CI` to allow the local dev server when running tests locally.
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
- `wait` ?<[Object]> Consider command started only when given output has been produced or a time in milliseconds has passed.
- `stdout` ?<[RegExp]>
- `stderr` ?<[RegExp]>
- `wait` ?<[Object]> Consider command started only when given output has been produced.
- `stdout` ?<[RegExp]> Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the environment, for example /Listening on port (?<my_server_port>\\d+)/ will store the port number in `process.env['MY_SERVER_PORT']`.
- `stderr` ?<[RegExp]> Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the environment, for example /Listening on port (?<my_server_port>\\d+)/ will store the port number in `process.env['MY_SERVER_PORT']`.
- `time` ?<[int]>
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.
Expand Down
56 changes: 28 additions & 28 deletions packages/playwright/src/plugins/webServerPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ import type { FullConfig } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
import type { ReporterV2 } from '../reporters/reporterV2';


export type WebServerPluginOptions = {
command: string;
url?: string;
wait?: { stdout?: RegExp, stderr?: RegExp, time?: number };
wait?: { stdout?: RegExp, stderr?: RegExp };
ignoreHTTPSErrors?: boolean;
timeout?: number;
gracefulShutdown?: { signal: 'SIGINT' | 'SIGTERM', timeout?: number };
Expand Down Expand Up @@ -143,43 +142,44 @@ export class WebServerPlugin implements TestRunnerPlugin {

if (this._options.wait?.stdout || this._options.wait?.stderr)
this._waitForStdioPromise = new ManualPromise();
let stdoutWaitCollector = this._options.wait?.stdout ? '' : undefined;
let stderrWaitCollector = this._options.wait?.stderr ? '' : undefined;

const resolveStdioPromise = () => {
stderrWaitCollector = undefined;
stdoutWaitCollector = undefined;
this._waitForStdioPromise?.resolve();
const stdioWaitCollectors = {
stdout: this._options.wait?.stdout ? '' : undefined,
stderr: this._options.wait?.stderr ? '' : undefined,
};

launchedProcess.stderr!.on('data', data => {
if (stderrWaitCollector !== undefined) {
stderrWaitCollector += data.toString();
if (this._options.wait?.stderr?.test(stderrWaitCollector))
resolveStdioPromise();
}
launchedProcess.stdout!.on('data', data => {
if (debugWebServer.enabled || this._options.stdout === 'pipe')
this._reporter!.onStdOut?.(prefixOutputLines(data.toString(), this._options.name));
});

launchedProcess.stderr!.on('data', data => {
if (debugWebServer.enabled || (this._options.stderr === 'pipe' || !this._options.stderr))
this._reporter!.onStdErr?.(prefixOutputLines(data.toString(), this._options.name));
});

launchedProcess.stdout!.on('data', data => {
if (stdoutWaitCollector !== undefined) {
stdoutWaitCollector += data.toString();
if (this._options.wait?.stdout?.test(stdoutWaitCollector))
resolveStdioPromise();
}
const resolveStdioPromise = () => {
stdioWaitCollectors.stdout = undefined;
stdioWaitCollectors.stderr = undefined;
this._waitForStdioPromise?.resolve();
};

if (debugWebServer.enabled || this._options.stdout === 'pipe')
this._reporter!.onStdOut?.(prefixOutputLines(data.toString(), this._options.name));
});
for (const stdio of ['stdout', 'stderr'] as const) {
launchedProcess[stdio]!.on('data', data => {
if (!this._options.wait?.[stdio] || stdioWaitCollectors[stdio] === undefined)
return;
stdioWaitCollectors[stdio] += data.toString();
this._options.wait[stdio].lastIndex = 0;
const result = this._options.wait[stdio].exec(stdioWaitCollectors[stdio]);
if (result) {
for (const [key, value] of Object.entries(result.groups || {}))
process.env[key.toUpperCase()] = value;
resolveStdioPromise();
}
});
}
}

private async _waitForProcess() {
// options.time is immune to the timeout.
if (this._options.wait?.time)
await new Promise(resolve => setTimeout(resolve, this._options.wait!.time));

if (!this._isAvailableCallback && !this._waitForStdioPromise) {
this._processExitedPromise.catch(() => {});
return;
Expand Down
12 changes: 11 additions & 1 deletion packages/playwright/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10193,11 +10193,21 @@ interface TestConfigWebServer {
stdout?: "pipe"|"ignore";

/**
* Consider command started only when given output has been produced or a time in milliseconds has passed.
* Consider command started only when given output has been produced.
*/
wait?: {
/**
* Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the
* environment, for example /Listening on port (?<my_server_port>\\d+)/ will store the port number in
* `process.env['MY_SERVER_PORT']`.
*/
stdout?: RegExp;

/**
* Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the
* environment, for example /Listening on port (?<my_server_port>\\d+)/ will store the port number in
* `process.env['MY_SERVER_PORT']`.
*/
stderr?: RegExp;

time?: number;
Expand Down
131 changes: 55 additions & 76 deletions tests/playwright-test/web-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,80 +947,59 @@ test('should throw helpful error when command is empty', async ({ runInlineTest
expect(result.output).toContain('config.webServer.command cannot be empty');
});

test('should wait for stdout', async ({ runInlineTest }) => {
const result = await runInlineTest({
'test.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({}) => {});
`,
'server.js': `
setTimeout(() => { console.log('server started'); }, 1000);
setTimeout(() => {}, 100000);
`,
'playwright.config.ts': `
module.exports = {
webServer: [
{
command: 'node server.js',
stdout: 'pipe',
wait: { stdout: /started/ },
}
],
};
`,
}, undefined);
expect(result.exitCode).toBe(0);
expect(result.output).toContain('server started');
});

test('should wait for stderr', async ({ runInlineTest }) => {
const result = await runInlineTest({
'test.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({}) => {});
`,
'server.js': `
setTimeout(() => { console.error('server started'); }, 1000);
setTimeout(() => {}, 100000);
`,
'playwright.config.ts': `
module.exports = {
webServer: [{
command: 'node server.js',
stdout: 'pipe',
wait: { stderr: /started/ },
}],
};
`,
}, undefined);
expect(result.exitCode).toBe(0);
expect(result.output).toContain('server started');
});
for (const stdio of ['stdout', 'stderr']) {
test(`should wait for ${stdio}`, async ({ runInlineTest }) => {
const result = await runInlineTest({
'test.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({}) => {});
`,
'server.js': `
setTimeout(() => { console.${stdio === 'stdout' ? 'log' : 'error'}('server started'); }, 1000);
setTimeout(() => {}, 100000);
`,
'playwright.config.ts': `
module.exports = {
webServer: [
{
command: 'node server.js',
stdout: 'pipe',
wait: { ${stdio}: /started/ },
}
],
};
`,
}, undefined);
expect(result.exitCode).toBe(0);
expect(result.output).toContain('server started');
});

test('should wait for time', async ({ runInlineTest }) => {
const result = await runInlineTest({
'test.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({}) => {
console.log('TEST: ' + Date.now());
});
`,
'server.js': `
console.log('SETUP: ' + Date.now());
setTimeout(() => {}, 100000);
`,
'playwright.config.ts': `
module.exports = {
webServer: [{
command: 'node server.js',
stdout: 'pipe',
wait: { time: 2000 },
}],
};
`,
}, undefined);
const [, setupTime] = /SETUP: (\d+)/.exec(result.output)!;
const [, testTime] = /TEST: (\d+)/.exec(result.output)!;
expect(+testTime - +setupTime).toBeGreaterThan(2000);
expect(result.exitCode).toBe(0);
});
test(`should wait for ${stdio} w/group`, async ({ runInlineTest }) => {
const result = await runInlineTest({
'test.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({}) => {
console.log('My server port is ' + process.env['MY_SERVER_PORT']);
});
`,
'server.js': `
setTimeout(() => { console.${stdio === 'stdout' ? 'log' : 'error'}('Listening on port 123'); }, 1000);
setTimeout(() => {}, 100000);
`,
'playwright.config.ts': `
module.exports = {
webServer: [
{
command: 'node server.js',
stdout: 'pipe',
wait: { ${stdio}: /Listening on port (?<my_server_port>\\d+)/ },
}
],
};
`,
}, undefined);
expect(result.exitCode).toBe(0);
expect(result.output).toContain('Listening on port 123');
expect(result.output).toContain('My server port is 123');
});
}
Loading