test: unflake inspector-cli tests (#22347)

This patch:
- changes the `childProcess` fixture to reliably SIGKILL all descendants
  (children and grand-children, regardless of their process group).
This is achieved using the `ps` command to build the process tree, and
then send
  `SIGKILL` to the descendant process groups.
- changes the `runCLI` fixture to **not** auto-close codegen by default;
  the `childProcess` fixture will clean up all processes. This makes
sure that all `runCLI.waitFor()` commands actually wait until the
necessary
  output.
- for a handful of tests that do actually want to auto-close codegen,
  introduce an optional `autoCloseWhen` flag for the `runCLI` fixture
that makes sure to close the codegen once a certain output was reached.
This commit is contained in:
Andrey Lushnikov 2023-04-12 16:37:24 +00:00 committed by GitHub
parent 56dcab844a
commit 8bb708be70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 178 additions and 155 deletions

View file

@ -414,7 +414,16 @@ async function launchContext(options: Options, headless: boolean, executablePath
const browser = await browserType.launch(launchOptions);
if (process.env.PWTEST_CLI_EXIT) {
if (process.env.PWTEST_CLI_IS_UNDER_TEST) {
(process as any)._didSetSourcesForTest = (text: string) => {
process.stdout.write('\n-------------8<-------------\n');
process.stdout.write(text);
process.stdout.write('\n-------------8<-------------\n');
const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN;
if (autoExitCondition && text.includes(autoExitCondition))
Promise.all(context.pages().map(async p => p.close()));
};
// Make sure we exit abnormally when browser crashes.
const logs: string[] = [];
require('playwright-core/lib/utilsBundle').debug.log = (...args: any[]) => {
const line = require('util').format(...args) + '\n';
@ -425,7 +434,6 @@ async function launchContext(options: Options, headless: boolean, executablePath
const hasCrashLine = logs.some(line => line.includes('process did exit:') && !line.includes('process did exit: exitCode=0, signal=null'));
if (hasCrashLine) {
process.stderr.write('Detected browser crash.\n');
// Make sure we exit abnormally when browser crashes.
process.exit(1);
}
});
@ -552,7 +560,14 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi
url = 'file://' + path.resolve(url);
else if (!url.startsWith('http') && !url.startsWith('file://') && !url.startsWith('about:') && !url.startsWith('data:'))
url = 'http://' + url;
await page.goto(url);
await page.goto(url).catch(error => {
if (process.env.PWTEST_CLI_AUTO_EXIT_WHEN && error.message.includes('Navigation failed because page was closed')) {
// Tests with PWTEST_CLI_AUTO_EXIT_WHEN might close page too fast, resulting
// in a stray navigation aborted error. We should ignore it.
} else {
throw error;
}
});
}
return page;
}
@ -567,8 +582,6 @@ async function open(options: Options, url: string | undefined, language: string)
saveStorage: options.saveStorage,
});
await openPage(context, url);
if (process.env.PWTEST_CLI_EXIT)
await Promise.all(context.pages().map(p => p.close()));
}
async function codegen(options: Options, url: string | undefined, language: string, outputFile?: string) {
@ -584,8 +597,6 @@ async function codegen(options: Options, url: string | undefined, language: stri
handleSIGINT: false,
});
await openPage(context, url);
if (process.env.PWTEST_CLI_EXIT)
await Promise.all(context.pages().map(p => p.close()));
}
async function waitForPage(page: Page, captureOptions: CaptureOptions) {

View file

@ -170,13 +170,8 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
}).toString(), true, sources, 'main').catch(() => {});
// Testing harness for runCLI mode.
{
if ((process.env.PWTEST_CLI_IS_UNDER_TEST || process.env.PWTEST_CLI_EXIT) && sources.length) {
process.stdout.write('\n-------------8<-------------\n');
process.stdout.write(sources[0].text);
process.stdout.write('\n-------------8<-------------\n');
}
}
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length)
(process as any)._didSetSourcesForTest(sources[0].text);
}
async setSelector(selector: string, focus?: boolean): Promise<void> {

View file

@ -28,6 +28,38 @@ type TestChildParams = {
onOutput?: () => void;
};
import childProcess from 'child_process';
type ProcessData = {
pid: number, // process ID
pgid: number, // process groupd ID
children: Set<ProcessData>, // direct children of the process
};
function buildProcessTreePosix(pid: number): ProcessData {
const processTree = childProcess.spawnSync('ps', ['-eo', 'pid,pgid,ppid']);
const lines = processTree.stdout.toString().trim().split('\n');
const pidToProcess = new Map<number, ProcessData>();
const edges: { pid: number, ppid: number }[] = [];
for (const line of lines) {
const [pid, pgid, ppid] = line.trim().split(/\s+/).map(token => +token);
// On linux, the very first line of `ps` is the header with "PID PGID PPID".
if (isNaN(pid) || isNaN(pgid) || isNaN(ppid))
continue;
pidToProcess.set(pid, { pid, pgid, children: new Set() });
edges.push({ pid, ppid });
}
for (const { pid, ppid } of edges) {
const parent = pidToProcess.get(ppid);
const child = pidToProcess.get(pid);
// On POSIX, certain processes might not have parent (e.g. PID=1 and occasionally PID=2).
if (parent && child)
parent.children.add(child);
}
return pidToProcess.get(pid);
}
export class TestChildProcess {
params: TestChildParams;
process: ChildProcess;
@ -72,7 +104,7 @@ export class TestChildProcess {
this.process.stderr.on('data', appendChunk);
this.process.stdout.on('data', appendChunk);
const killProcessGroup = this._killProcessGroup.bind(this);
const killProcessGroup = this._killProcessTree.bind(this, 'SIGKILL');
process.on('exit', killProcessGroup);
this.exited = new Promise(f => {
this.process.on('exit', (exitCode, signal) => f({ exitCode, signal }));
@ -86,29 +118,50 @@ export class TestChildProcess {
return strippedOutput.split('\n').filter(line => line.startsWith('%%')).map(line => line.substring(2).trim());
}
async close() {
if (this.process.kill(0))
this._killProcessGroup('SIGINT');
async kill(signal: 'SIGINT' | 'SIGKILL' = 'SIGKILL') {
this._killProcessTree(signal);
return this.exited;
}
async kill() {
if (this.process.kill(0))
this._killProcessGroup('SIGKILL');
return this.exited;
}
private _killProcessGroup(signal: 'SIGINT' | 'SIGKILL') {
private _killProcessTree(signal: 'SIGINT' | 'SIGKILL') {
if (!this.process.pid || !this.process.kill(0))
return;
// On Windows, we always call `taskkill` no matter signal.
if (process.platform === 'win32') {
try {
if (process.platform === 'win32')
execSync(`taskkill /pid ${this.process.pid} /T /F /FI "MEMUSAGE gt 0"`, { stdio: 'ignore' });
else
process.kill(-this.process.pid, signal);
} catch (e) {
// the process might have already stopped
}
return;
}
// In case of POSIX and `SIGINT` signal, send it to the main process group only.
if (signal === 'SIGINT') {
try {
process.kill(-this.process.pid, 'SIGINT');
} catch (e) {
// the process might have already stopped
}
return;
}
// In case of POSIX and `SIGKILL` signal, we should send it to all descendant process groups.
const rootProcess = buildProcessTreePosix(this.process.pid);
const descendantProcessGroups = (function flatten(processData: ProcessData, result: Set<number> = new Set()) {
// Process can nullify its own process group with `setpgid`. Use its PID instead.
result.add(processData.pgid || processData.pid);
processData.children.forEach(child => flatten(child, result));
return result;
})(rootProcess);
for (const pgid of descendantProcessGroups) {
try {
process.kill(-pgid, 'SIGKILL');
} catch (e) {
// the process might have already stopped
}
}
}
async cleanExit() {
@ -150,16 +203,7 @@ export const commonFixtures: Fixtures<CommonFixtures, CommonWorkerFixtures> = {
processes.push(process);
return process;
});
await Promise.all(processes.map(async child => {
await Promise.race([
child.exited,
new Promise(f => setTimeout(f, 3_000)),
]);
if (child.process.kill(0)) {
await child.kill();
throw new Error(`Process ${child.params.command.join(' ')} is still running. Leaking process?\nOutput:${child.output}`);
}
}));
await Promise.all(processes.map(async child => child.kill()));
if (testInfo.status !== 'passed' && testInfo.status !== 'skipped' && !process.env.PWTEST_DEBUG) {
for (const process of processes) {
console.log('====== ' + process.params.command.join(' '));
@ -176,7 +220,7 @@ export const commonFixtures: Fixtures<CommonFixtures, CommonWorkerFixtures> = {
processes.push(process);
return process;
});
await Promise.all(processes.map(child => child.close()));
await Promise.all(processes.map(child => child.kill('SIGINT')));
}, { scope: 'worker' }],
waitForPort: async ({}, use) => {

View file

@ -49,8 +49,7 @@ export class RunServer implements PlaywrightServer {
}
async close() {
await this._process.close();
await this._process.exitCode;
await this._process.kill('SIGINT');
}
}
@ -150,7 +149,7 @@ export class RemoteServer implements PlaywrightServer {
await this._browser.close();
this._browser = undefined;
}
await this._process.close();
await this._process.kill('SIGINT');
await this.childExitCode();
}
}

View file

@ -19,20 +19,32 @@ test('codegen should work', async ({ exec }) => {
await exec('npm i --foreground-scripts playwright');
await test.step('codegen without arguments', async () => {
const result = await exec('npx playwright codegen', { env: { PWTEST_CLI_EXIT: '1' } });
expect(result).toContain(`@playwright/test`);
const result = await exec('npx playwright codegen', {
env: {
PWTEST_CLI_IS_UNDER_TEST: '1',
PWTEST_CLI_AUTO_EXIT_WHEN: '@playwright/test',
}
});
expect(result).toContain(`{ page }`);
});
await test.step('codegen --target=javascript', async () => {
const result = await exec('npx playwright codegen --target=javascript', { env: { PWTEST_CLI_EXIT: '1' } });
const result = await exec('npx playwright codegen --target=javascript', {
env: {
PWTEST_CLI_IS_UNDER_TEST: '1',
PWTEST_CLI_AUTO_EXIT_WHEN: 'context.close',
}
});
expect(result).toContain(`playwright`);
expect(result).toContain(`page.close`);
});
await test.step('codegen --target=python', async () => {
const result = await exec('npx playwright codegen --target=python', { env: { PWTEST_CLI_EXIT: '1' } });
expect(result).toContain(`chromium.launch`);
const result = await exec('npx playwright codegen --target=python', {
env: {
PWTEST_CLI_IS_UNDER_TEST: '1',
PWTEST_CLI_AUTO_EXIT_WHEN: 'chromium.launch',
},
});
expect(result).toContain(`browser.close`);
});
});

View file

@ -481,8 +481,10 @@ test.describe('cli codegen', () => {
test('should --save-trace', async ({ runCLI }, testInfo) => {
const traceFileName = testInfo.outputPath('trace.zip');
const cli = runCLI([`--save-trace=${traceFileName}`]);
await cli.exited;
const cli = runCLI([`--save-trace=${traceFileName}`], {
autoExitWhen: ' ',
});
await cli.waitForCleanExit();
expect(fs.existsSync(traceFileName)).toBeTruthy();
});
@ -492,11 +494,9 @@ test.describe('cli codegen', () => {
const traceFileName = testInfo.outputPath('trace.zip');
const storageFileName = testInfo.outputPath('auth.json');
const harFileName = testInfo.outputPath('har.har');
const cli = runCLI([`--save-trace=${traceFileName}`, `--save-storage=${storageFileName}`, `--save-har=${harFileName}`], {
noAutoExit: true,
});
const cli = runCLI([`--save-trace=${traceFileName}`, `--save-storage=${storageFileName}`, `--save-har=${harFileName}`]);
await cli.waitFor(`import { test, expect } from '@playwright/test'`);
cli.exit('SIGINT');
cli.process.kill('SIGINT');
const { exitCode } = await cli.process.exited;
expect(exitCode).toBe(130);
expect(fs.existsSync(traceFileName)).toBeTruthy();

View file

@ -43,8 +43,7 @@ class Program
${launchOptions(channel)}
});
var context = await browser.NewContextAsync();`;
await cli.waitFor(expectedResult).catch(e => e);
expect(cli.text()).toContain(expectedResult);
await cli.waitFor(expectedResult);
});
test('should print the correct context options for custom settings', async ({ browserName, channel, runCLI }) => {
@ -87,7 +86,6 @@ test('should print the correct context options for custom settings', async ({ br
},
});`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options when using a device', async ({ browserName, channel, runCLI }) => {
@ -102,7 +100,6 @@ test('should print the correct context options when using a device', async ({ br
});
var context = await browser.NewContextAsync(playwright.Devices["Pixel 2"]);`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, channel, runCLI }) => {
@ -147,9 +144,7 @@ test('should print the correct context options when using a device and additiona
Width = 1280,
},
});`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print load/save storageState', async ({ browserName, channel, runCLI }, testInfo) => {
@ -179,7 +174,6 @@ test('should print load/save storageState', async ({ browserName, channel, runCL
test('should work with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const cli = runCLI(['--target=csharp', `--save-har=${harFileName}`]);
const expectedResult = `
var context = await browser.NewContextAsync(new BrowserNewContextOptions
{
@ -187,9 +181,10 @@ test('should work with --save-har', async ({ runCLI }, testInfo) => {
RecordHarPath = ${JSON.stringify(harFileName)},
ServiceWorkers = ServiceWorkerPolicy.Block,
});`;
await cli.waitFor(expectedResult).catch(e => e);
expect(cli.text()).toContain(expectedResult);
await cli.exited;
const cli = runCLI(['--target=csharp', `--save-har=${harFileName}`], {
autoExitWhen: expectedResult,
});
await cli.waitForCleanExit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});

View file

@ -37,7 +37,6 @@ public class Example {
${launchOptions(channel)});
BrowserContext context = browser.newContext();`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options for custom settings', async ({ runCLI, browserName }) => {
@ -45,7 +44,6 @@ test('should print the correct context options for custom settings', async ({ ru
const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setColorScheme(ColorScheme.LIGHT));`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options when using a device', async ({ browserName, runCLI }) => {
@ -93,14 +91,15 @@ test('should print load/save storage_state', async ({ runCLI, browserName }, tes
test('should work with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const cli = runCLI(['--target=java', `--save-har=${harFileName}`]);
const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setRecordHarMode(HarMode.MINIMAL)
.setRecordHarPath(Paths.get(${JSON.stringify(harFileName)}))
.setServiceWorkers(ServiceWorkerPolicy.BLOCK));`;
await cli.waitFor(expectedResult).catch(e => e);
expect(cli.text()).toContain(expectedResult);
await cli.exited;
const cli = runCLI(['--target=java', `--save-har=${harFileName}`], {
autoExitWhen: expectedResult,
});
await cli.waitForCleanExit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});

View file

@ -34,7 +34,6 @@ test('should print the correct imports and context options', async ({ browserNam
});
const context = await browser.newContext();`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options for custom settings', async ({ browserName, channel, runCLI }) => {
@ -49,7 +48,6 @@ test('should print the correct context options for custom settings', async ({ br
colorScheme: 'light'
});`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
@ -67,7 +65,6 @@ test('should print the correct context options when using a device', async ({ br
...devices['Pixel 2'],
});`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, channel, runCLI }) => {
@ -85,13 +82,14 @@ test('should print the correct context options when using a device and additiona
colorScheme: 'light'
});`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should save the codegen output to a file if specified', async ({ browserName, channel, runCLI }, testInfo) => {
const tmpFile = testInfo.outputPath('script.js');
const cli = runCLI(['--output', tmpFile, '--target=javascript', emptyHTML]);
await cli.exited;
const cli = runCLI(['--output', tmpFile, '--target=javascript', emptyHTML], {
autoExitWhen: 'await page.goto', // We have to wait for the initial navigation to be recorded.
});
await cli.waitForCleanExit();
const content = fs.readFileSync(tmpFile);
expect(content.toString()).toBe(`const { ${browserName} } = require('playwright');

View file

@ -27,15 +27,16 @@ test('should print the correct imports and context options', async ({ runCLI })
def test_example(page: Page) -> None:`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options when using a device and lang', async ({ browserName, runCLI }, testInfo) => {
test.skip(browserName !== 'webkit');
const tmpFile = testInfo.outputPath('script.js');
const cli = runCLI(['--target=python-pytest', '--device=iPhone 11', '--lang=en-US', '--output', tmpFile, emptyHTML]);
await cli.exited;
const cli = runCLI(['--target=python-pytest', '--device=iPhone 11', '--lang=en-US', '--output', tmpFile, emptyHTML], {
autoExitWhen: 'page.goto',
});
await cli.waitForCleanExit();
const content = fs.readFileSync(tmpFile);
expect(content.toString()).toBe(`import pytest
@ -54,8 +55,10 @@ def test_example(page: Page) -> None:
test('should save the codegen output to a file if specified', async ({ runCLI }, testInfo) => {
const tmpFile = testInfo.outputPath('test_example.py');
const cli = runCLI(['--target=python-pytest', '--output', tmpFile, emptyHTML]);
await cli.exited;
const cli = runCLI(['--target=python-pytest', '--output', tmpFile, emptyHTML], {
autoExitWhen: 'page.goto',
});
await cli.waitForCleanExit();
const content = fs.readFileSync(tmpFile);
expect(content.toString()).toBe(`from playwright.sync_api import Page, expect

View file

@ -34,7 +34,6 @@ async def run(playwright: Playwright) -> None:
browser = await playwright.${browserName}.launch(${launchOptions(channel)})
context = await browser.new_context()`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options for custom settings', async ({ browserName, channel, runCLI }) => {
@ -48,7 +47,6 @@ async def run(playwright: Playwright) -> None:
browser = await playwright.${browserName}.launch(${launchOptions(channel)})
context = await browser.new_context(color_scheme="light")`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options when using a device', async ({ browserName, channel, runCLI }) => {
@ -64,7 +62,6 @@ async def run(playwright: Playwright) -> None:
browser = await playwright.chromium.launch(${launchOptions(channel)})
context = await browser.new_context(**playwright.devices["Pixel 2"])`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, channel, runCLI }) => {
@ -80,13 +77,14 @@ async def run(playwright: Playwright) -> None:
browser = await playwright.webkit.launch(${launchOptions(channel)})
context = await browser.new_context(**playwright.devices["iPhone 11"], color_scheme="light")`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should save the codegen output to a file if specified', async ({ browserName, channel, runCLI }, testInfo) => {
const tmpFile = testInfo.outputPath('example.py');
const cli = runCLI(['--target=python-async', '--output', tmpFile, emptyHTML]);
await cli.exited;
const cli = runCLI(['--target=python-async', '--output', tmpFile, emptyHTML], {
autoExitWhen: 'page.goto',
});
await cli.waitForCleanExit();
const content = fs.readFileSync(tmpFile);
expect(content.toString()).toBe(`import asyncio
@ -148,11 +146,11 @@ asyncio.run(main())
test('should work with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`]);
const expectedResult = `context = await browser.new_context(record_har_mode="minimal", record_har_path=${JSON.stringify(harFileName)}, service_workers="block")`;
await cli.waitFor(expectedResult).catch(e => e);
expect(cli.text()).toContain(expectedResult);
await cli.exited;
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`], {
autoExitWhen: expectedResult,
});
await cli.waitForCleanExit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});

View file

@ -32,7 +32,6 @@ def run(playwright: Playwright) -> None:
browser = playwright.${browserName}.launch(${launchOptions(channel)})
context = browser.new_context()`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options for custom settings', async ({ runCLI, channel, browserName }) => {
@ -44,7 +43,6 @@ def run(playwright: Playwright) -> None:
browser = playwright.${browserName}.launch(${launchOptions(channel)})
context = browser.new_context(color_scheme="light")`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options when using a device', async ({ browserName, channel, runCLI }) => {
@ -58,7 +56,6 @@ def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(${launchOptions(channel)})
context = browser.new_context(**playwright.devices["Pixel 2"])`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, channel, runCLI }) => {
@ -72,13 +69,14 @@ def run(playwright: Playwright) -> None:
browser = playwright.webkit.launch(${launchOptions(channel)})
context = browser.new_context(**playwright.devices["iPhone 11"], color_scheme="light")`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should save the codegen output to a file if specified', async ({ runCLI, channel, browserName }, testInfo) => {
const tmpFile = testInfo.outputPath('example.py');
const cli = runCLI(['--target=python', '--output', tmpFile, emptyHTML]);
await cli.exited;
const cli = runCLI(['--target=python', '--output', tmpFile, emptyHTML], {
autoExitWhen: 'page.goto',
});
await cli.waitForCleanExit();
const content = fs.readFileSync(tmpFile);
expect(content.toString()).toBe(`from playwright.sync_api import Playwright, sync_playwright, expect

View file

@ -27,7 +27,6 @@ test('should print the correct imports and context options', async ({ runCLI })
test('test', async ({ page }) => {
});`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options for custom settings', async ({ browserName, channel, runCLI }) => {
@ -40,7 +39,6 @@ test.use({
test('test', async ({ page }) => {`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
@ -56,7 +54,6 @@ test.use({
test('test', async ({ page }) => {`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, channel, runCLI }) => {
@ -72,7 +69,6 @@ test.use({
test('test', async ({ page }) => {`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
test('should print load storageState', async ({ browserName, channel, runCLI }, testInfo) => {
@ -86,21 +82,20 @@ test.use({
});
test('test', async ({ page }) => {`;
await cli.waitFor(expectedResult);
});
test('should work with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const cli = runCLI(['--target=playwright-test', `--save-har=${harFileName}`]);
const expectedResult = `
recordHar: {
mode: 'minimal',
path: '${harFileName.replace(/\\/g, '\\\\')}'
}`;
await cli.waitFor(expectedResult).catch(e => e);
expect(cli.text()).toContain(expectedResult);
await cli.exited;
const cli = runCLI(['--target=playwright-test', `--save-har=${harFileName}`], {
autoExitWhen: expectedResult,
});
await cli.waitForCleanExit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});

View file

@ -16,9 +16,11 @@
import { contextTest } from '../../config/browserTest';
import type { Page } from 'playwright-core';
import { step } from '../../config/baseTest';
import * as path from 'path';
import type { Source } from '../../../packages/recorder/src/recorderTypes';
import type { CommonFixtures, TestChildProcess } from '../../config/commonFixtures';
import { stripAnsi } from '../../config/utils';
import { expect } from '@playwright/test';
export { expect } from '@playwright/test';
@ -26,7 +28,7 @@ type CLITestArgs = {
recorderPageGetter: () => Promise<Page>;
closeRecorder: () => Promise<void>;
openRecorder: () => Promise<Recorder>;
runCLI: (args: string[], options?: { noAutoExit?: boolean }) => CLIMock;
runCLI: (args: string[], options?: { autoExitWhen?: string }) => CLIMock;
};
const codegenLang2Id: Map<string, string> = new Map([
@ -68,13 +70,9 @@ export const test = contextTest.extend<CLITestArgs>({
process.env.PWTEST_RECORDER_PORT = String(10907 + testInfo.workerIndex);
testInfo.skip(mode === 'service');
let cli: CLIMock | undefined;
await run((cliArgs, { noAutoExit } = {}) => {
cli = new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath, noAutoExit);
return cli;
await run((cliArgs, { autoExitWhen } = {}) => {
return new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath, autoExitWhen);
});
// Discard any exit error and let childProcess fixture report leaking processes (processwes which do not exit).
cli?.exited.catch(() => {});
},
openRecorder: async ({ page, recorderPageGetter }, run) => {
@ -135,7 +133,7 @@ class Recorder {
const w = window as any;
const source = (w.playwrightSourcesEchoForTest || []).find((s: Source) => s.id === params.languageId);
return source && source.text.includes(params.text) ? w.playwrightSourcesEchoForTest : null;
}, { text, languageId: codegenLang2Id.get(file) }, { timeout: 8000, polling: 300 });
}, { text, languageId: codegenLang2Id.get(file) }, { timeout: 0, polling: 300 });
const sources: Source[] = await handle.jsonValue();
for (const source of sources) {
if (!codegenLangId2lang.has(source.id))
@ -199,11 +197,8 @@ class Recorder {
class CLIMock {
process: TestChildProcess;
private waitForText: string;
private waitForCallback: () => void;
exited: Promise<void>;
constructor(childProcess: CommonFixtures['childProcess'], browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, noAutoExit: boolean | undefined) {
constructor(childProcess: CommonFixtures['childProcess'], browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined) {
const nodeArgs = [
'node',
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'lib', 'cli', 'cli.js'),
@ -216,47 +211,28 @@ class CLIMock {
this.process = childProcess({
command: nodeArgs,
env: {
PWTEST_CLI_AUTO_EXIT_WHEN: autoExitWhen,
PWTEST_CLI_IS_UNDER_TEST: '1',
PWTEST_CLI_EXIT: !noAutoExit ? '1' : undefined,
PWTEST_CLI_HEADLESS: headless ? '1' : undefined,
PWTEST_CLI_EXECUTABLE_PATH: executablePath,
DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*',
},
});
this.process.onOutput = () => {
if (this.waitForCallback && this.process.output.includes(this.waitForText))
this.waitForCallback();
};
this.exited = this.process.cleanExit();
}
async waitFor(text: string, timeout = 10_000): Promise<void> {
if (this.process.output.includes(text))
return Promise.resolve();
this.waitForText = text;
return new Promise((f, r) => {
this.waitForCallback = f;
if (timeout) {
setTimeout(() => {
r(new Error('Timed out waiting for text:\n' + text + '\n\nReceived:\n' + this.text()));
}, timeout);
@step
async waitFor(text: string): Promise<void> {
await expect(() => {
expect(this.text()).toContain(text);
}).toPass();
}
});
@step
async waitForCleanExit() {
return this.process.cleanExit();
}
text() {
return removeAnsiColors(this.process.output);
}
exit(signal: NodeJS.Signals | number) {
this.process.process.kill(signal);
return stripAnsi(this.process.output);
}
}
function removeAnsiColors(input: string): string {
const pattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
].join('|');
return input.replace(new RegExp(pattern, 'g'), '');
}

View file

@ -109,7 +109,7 @@ export const test = base
return { page, testProcess };
});
await browser?.close();
await testProcess?.close();
await testProcess?.kill('SIGINT');
await removeFolderAsync(cacheDir);
},
createLatch: async ({}, use, testInfo) => {

View file

@ -50,6 +50,6 @@ export const webView2Test = baseTest.extend<TraceViewerFixtures>(traceViewerFixt
const browser = await playwright.chromium.connectOverCDP(`http://127.0.0.1:${cdpPort}`);
await use(browser);
await browser.close();
await spawnedProcess.close();
await spawnedProcess.kill('SIGINT');
}, { scope: 'worker' }],
});