fix(cli): store trace/storage/har on SIGINT (#16339)

This commit is contained in:
Max Schmitt 2022-08-09 00:13:38 +02:00 committed by GitHub
parent 57fcb590f7
commit 1ca6635bb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 51 additions and 12 deletions

View file

@ -376,6 +376,7 @@ async function launchContext(options: Options, headless: boolean, executablePath
const launchOptions: LaunchOptions = { headless, executablePath };
if (options.channel)
launchOptions.channel = options.channel as any;
launchOptions.handleSIGINT = false;
const contextOptions: BrowserContextOptions =
// Copy the device descriptor since we have to compare and modify the options.
@ -522,6 +523,11 @@ async function launchContext(options: Options, headless: boolean, executablePath
closeBrowser().catch(e => null);
});
});
process.on('SIGINT', async () => {
await closeBrowser();
process.exit(130);
});
const timeout = options.timeout ? parseInt(options.timeout, 10) : 0;
context.setDefaultTimeout(timeout);
context.setDefaultNavigationTimeout(timeout);
@ -532,6 +538,7 @@ async function launchContext(options: Options, headless: boolean, executablePath
// Omit options that we add automatically for presentation purpose.
delete launchOptions.headless;
delete launchOptions.executablePath;
delete launchOptions.handleSIGINT;
delete contextOptions.deviceScaleFactor;
return { browser, browserName: browserType.name(), context, contextOptions, launchOptions };
}
@ -571,7 +578,8 @@ async function codegen(options: Options, url: string | undefined, language: stri
device: options.device,
saveStorage: options.saveStorage,
mode: 'recording',
outputFile: outputFile ? path.resolve(outputFile) : undefined
outputFile: outputFile ? path.resolve(outputFile) : undefined,
handleSIGINT: false,
});
await openPage(context, url);
if (process.env.PWTEST_CLI_EXIT)

View file

@ -374,7 +374,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
device?: string,
saveStorage?: string,
mode?: 'recording' | 'inspecting',
outputFile?: string
outputFile?: string,
handleSIGINT?: boolean,
}) {
await this._channel.recorderSupplementEnable(params);
}

View file

@ -1423,6 +1423,7 @@ export type BrowserContextRecorderSupplementEnableParams = {
device?: string,
saveStorage?: string,
outputFile?: string,
handleSIGINT?: boolean,
};
export type BrowserContextRecorderSupplementEnableOptions = {
language?: string,
@ -1433,6 +1434,7 @@ export type BrowserContextRecorderSupplementEnableOptions = {
device?: string,
saveStorage?: string,
outputFile?: string,
handleSIGINT?: boolean,
};
export type BrowserContextRecorderSupplementEnableResult = void;
export type BrowserContextNewCDPSessionParams = {

View file

@ -948,6 +948,7 @@ BrowserContext:
device: string?
saveStorage: string?
outputFile: string?
handleSIGINT: boolean?
newCDPSession:
parameters:

View file

@ -772,6 +772,7 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
device: tOptional(tString),
saveStorage: tOptional(tString),
outputFile: tOptional(tString),
handleSIGINT: tOptional(tBoolean),
});
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
scheme.BrowserContextNewCDPSessionParams = tObject({

View file

@ -56,6 +56,7 @@ export class Recorder implements InstrumentationListener {
private _allMetadatas = new Map<string, CallMetadata>();
private _debugger: Debugger;
private _contextRecorder: ContextRecorder;
private _handleSIGINT: boolean | undefined;
private _recorderAppFactory: (recorder: Recorder) => Promise<IRecorderApp>;
static showInspector(context: BrowserContext) {
@ -78,13 +79,14 @@ export class Recorder implements InstrumentationListener {
this._contextRecorder = new ContextRecorder(context, params);
this._context = context;
this._debugger = Debugger.lookup(context)!;
this._handleSIGINT = params.handleSIGINT;
context.instrumentation.addListener(this, context);
}
private static async defaultRecorderAppFactory(recorder: Recorder) {
if (process.env.PW_CODEGEN_NO_INSPECTOR)
return new EmptyRecorderApp();
return await RecorderApp.open(recorder, recorder._context);
return await RecorderApp.open(recorder, recorder._context, recorder._handleSIGINT);
}
async install() {

View file

@ -109,7 +109,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
await mainFrame.goto(serverSideCallMetadata(), 'https://playwright/index.html');
}
static async open(recorder: Recorder, inspectedContext: BrowserContext): Promise<IRecorderApp> {
static async open(recorder: Recorder, inspectedContext: BrowserContext, handleSIGINT: boolean | undefined): Promise<IRecorderApp> {
const sdkLanguage = inspectedContext._browser.options.sdkLanguage;
const headed = !!inspectedContext._browser.options.headful;
const recorderPlaywright = (require('../playwright').createPlaywright as typeof import('../playwright').createPlaywright)('javascript', true);
@ -127,7 +127,8 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
noDefaultViewport: true,
ignoreDefaultArgs: ['--enable-automation'],
headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed),
useWebSocket: !!process.env.PWTEST_RECORDER_PORT
useWebSocket: !!process.env.PWTEST_RECORDER_PORT,
handleSIGINT,
});
const controller = new ProgressController(serverSideCallMetadata(), context._browser);
await controller.run(async progress => {
@ -165,7 +166,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
// Testing harness for runCLI mode.
{
if (process.env.PWTEST_CLI_EXIT && sources.length) {
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');

View file

@ -544,6 +544,24 @@ test.describe('cli codegen', () => {
expect(fs.existsSync(traceFileName)).toBeTruthy();
});
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
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,
});
await cli.waitFor(`import { test, expect } from '@playwright/test'`);
cli.exit('SIGINT');
const { exitCode } = await cli.process.exited;
expect(exitCode).toBe(130);
expect(fs.existsSync(traceFileName)).toBeTruthy();
expect(fs.existsSync(storageFileName)).toBeTruthy();
expect(fs.existsSync(harFileName)).toBeTruthy();
});
test('should fill tricky characters', async ({ page, openRecorder }) => {
const recorder = await openRecorder();

View file

@ -25,7 +25,7 @@ type CLITestArgs = {
recorderPageGetter: () => Promise<Page>;
closeRecorder: () => Promise<void>;
openRecorder: () => Promise<Recorder>;
runCLI: (args: string[]) => CLIMock;
runCLI: (args: string[], options?: { noAutoExit?: boolean }) => CLIMock;
};
const playwrightToAutomateInspector = require('../../../packages/playwright-core/lib/inProcessFactory').createInProcessPlaywright();
@ -55,8 +55,8 @@ export const test = contextTest.extend<CLITestArgs>({
testInfo.skip(mode === 'service');
let cli: CLIMock | undefined;
await run(cliArgs => {
cli = new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath);
await run((cliArgs, { noAutoExit } = {}) => {
cli = new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath, noAutoExit);
return cli;
});
if (cli)
@ -178,12 +178,12 @@ class Recorder {
}
class CLIMock {
private process: TestChildProcess;
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) {
constructor(childProcess: CommonFixtures['childProcess'], browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, noAutoExit: boolean | undefined) {
const nodeArgs = [
'node',
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'lib', 'cli', 'cli.js'),
@ -196,7 +196,8 @@ class CLIMock {
this.process = childProcess({
command: nodeArgs,
env: {
PWTEST_CLI_EXIT: '1',
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*',
@ -226,6 +227,10 @@ class CLIMock {
text() {
return removeAnsiColors(this.process.output);
}
exit(signal: NodeJS.Signals | number) {
this.process.process.kill(signal);
}
}
function removeAnsiColors(input: string): string {