fix(cli): store trace/storage/har on SIGINT (#16339)
This commit is contained in:
parent
57fcb590f7
commit
1ca6635bb8
|
|
@ -376,6 +376,7 @@ async function launchContext(options: Options, headless: boolean, executablePath
|
||||||
const launchOptions: LaunchOptions = { headless, executablePath };
|
const launchOptions: LaunchOptions = { headless, executablePath };
|
||||||
if (options.channel)
|
if (options.channel)
|
||||||
launchOptions.channel = options.channel as any;
|
launchOptions.channel = options.channel as any;
|
||||||
|
launchOptions.handleSIGINT = false;
|
||||||
|
|
||||||
const contextOptions: BrowserContextOptions =
|
const contextOptions: BrowserContextOptions =
|
||||||
// Copy the device descriptor since we have to compare and modify the options.
|
// 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);
|
closeBrowser().catch(e => null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
await closeBrowser();
|
||||||
|
process.exit(130);
|
||||||
|
});
|
||||||
|
|
||||||
const timeout = options.timeout ? parseInt(options.timeout, 10) : 0;
|
const timeout = options.timeout ? parseInt(options.timeout, 10) : 0;
|
||||||
context.setDefaultTimeout(timeout);
|
context.setDefaultTimeout(timeout);
|
||||||
context.setDefaultNavigationTimeout(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.
|
// Omit options that we add automatically for presentation purpose.
|
||||||
delete launchOptions.headless;
|
delete launchOptions.headless;
|
||||||
delete launchOptions.executablePath;
|
delete launchOptions.executablePath;
|
||||||
|
delete launchOptions.handleSIGINT;
|
||||||
delete contextOptions.deviceScaleFactor;
|
delete contextOptions.deviceScaleFactor;
|
||||||
return { browser, browserName: browserType.name(), context, contextOptions, launchOptions };
|
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,
|
device: options.device,
|
||||||
saveStorage: options.saveStorage,
|
saveStorage: options.saveStorage,
|
||||||
mode: 'recording',
|
mode: 'recording',
|
||||||
outputFile: outputFile ? path.resolve(outputFile) : undefined
|
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
||||||
|
handleSIGINT: false,
|
||||||
});
|
});
|
||||||
await openPage(context, url);
|
await openPage(context, url);
|
||||||
if (process.env.PWTEST_CLI_EXIT)
|
if (process.env.PWTEST_CLI_EXIT)
|
||||||
|
|
|
||||||
|
|
@ -374,7 +374,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
device?: string,
|
device?: string,
|
||||||
saveStorage?: string,
|
saveStorage?: string,
|
||||||
mode?: 'recording' | 'inspecting',
|
mode?: 'recording' | 'inspecting',
|
||||||
outputFile?: string
|
outputFile?: string,
|
||||||
|
handleSIGINT?: boolean,
|
||||||
}) {
|
}) {
|
||||||
await this._channel.recorderSupplementEnable(params);
|
await this._channel.recorderSupplementEnable(params);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1423,6 +1423,7 @@ export type BrowserContextRecorderSupplementEnableParams = {
|
||||||
device?: string,
|
device?: string,
|
||||||
saveStorage?: string,
|
saveStorage?: string,
|
||||||
outputFile?: string,
|
outputFile?: string,
|
||||||
|
handleSIGINT?: boolean,
|
||||||
};
|
};
|
||||||
export type BrowserContextRecorderSupplementEnableOptions = {
|
export type BrowserContextRecorderSupplementEnableOptions = {
|
||||||
language?: string,
|
language?: string,
|
||||||
|
|
@ -1433,6 +1434,7 @@ export type BrowserContextRecorderSupplementEnableOptions = {
|
||||||
device?: string,
|
device?: string,
|
||||||
saveStorage?: string,
|
saveStorage?: string,
|
||||||
outputFile?: string,
|
outputFile?: string,
|
||||||
|
handleSIGINT?: boolean,
|
||||||
};
|
};
|
||||||
export type BrowserContextRecorderSupplementEnableResult = void;
|
export type BrowserContextRecorderSupplementEnableResult = void;
|
||||||
export type BrowserContextNewCDPSessionParams = {
|
export type BrowserContextNewCDPSessionParams = {
|
||||||
|
|
|
||||||
|
|
@ -948,6 +948,7 @@ BrowserContext:
|
||||||
device: string?
|
device: string?
|
||||||
saveStorage: string?
|
saveStorage: string?
|
||||||
outputFile: string?
|
outputFile: string?
|
||||||
|
handleSIGINT: boolean?
|
||||||
|
|
||||||
newCDPSession:
|
newCDPSession:
|
||||||
parameters:
|
parameters:
|
||||||
|
|
|
||||||
|
|
@ -772,6 +772,7 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
||||||
device: tOptional(tString),
|
device: tOptional(tString),
|
||||||
saveStorage: tOptional(tString),
|
saveStorage: tOptional(tString),
|
||||||
outputFile: tOptional(tString),
|
outputFile: tOptional(tString),
|
||||||
|
handleSIGINT: tOptional(tBoolean),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
|
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextNewCDPSessionParams = tObject({
|
scheme.BrowserContextNewCDPSessionParams = tObject({
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ export class Recorder implements InstrumentationListener {
|
||||||
private _allMetadatas = new Map<string, CallMetadata>();
|
private _allMetadatas = new Map<string, CallMetadata>();
|
||||||
private _debugger: Debugger;
|
private _debugger: Debugger;
|
||||||
private _contextRecorder: ContextRecorder;
|
private _contextRecorder: ContextRecorder;
|
||||||
|
private _handleSIGINT: boolean | undefined;
|
||||||
private _recorderAppFactory: (recorder: Recorder) => Promise<IRecorderApp>;
|
private _recorderAppFactory: (recorder: Recorder) => Promise<IRecorderApp>;
|
||||||
|
|
||||||
static showInspector(context: BrowserContext) {
|
static showInspector(context: BrowserContext) {
|
||||||
|
|
@ -78,13 +79,14 @@ export class Recorder implements InstrumentationListener {
|
||||||
this._contextRecorder = new ContextRecorder(context, params);
|
this._contextRecorder = new ContextRecorder(context, params);
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._debugger = Debugger.lookup(context)!;
|
this._debugger = Debugger.lookup(context)!;
|
||||||
|
this._handleSIGINT = params.handleSIGINT;
|
||||||
context.instrumentation.addListener(this, context);
|
context.instrumentation.addListener(this, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async defaultRecorderAppFactory(recorder: Recorder) {
|
private static async defaultRecorderAppFactory(recorder: Recorder) {
|
||||||
if (process.env.PW_CODEGEN_NO_INSPECTOR)
|
if (process.env.PW_CODEGEN_NO_INSPECTOR)
|
||||||
return new EmptyRecorderApp();
|
return new EmptyRecorderApp();
|
||||||
return await RecorderApp.open(recorder, recorder._context);
|
return await RecorderApp.open(recorder, recorder._context, recorder._handleSIGINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
async install() {
|
async install() {
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
||||||
await mainFrame.goto(serverSideCallMetadata(), 'https://playwright/index.html');
|
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 sdkLanguage = inspectedContext._browser.options.sdkLanguage;
|
||||||
const headed = !!inspectedContext._browser.options.headful;
|
const headed = !!inspectedContext._browser.options.headful;
|
||||||
const recorderPlaywright = (require('../playwright').createPlaywright as typeof import('../playwright').createPlaywright)('javascript', true);
|
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,
|
noDefaultViewport: true,
|
||||||
ignoreDefaultArgs: ['--enable-automation'],
|
ignoreDefaultArgs: ['--enable-automation'],
|
||||||
headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed),
|
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);
|
const controller = new ProgressController(serverSideCallMetadata(), context._browser);
|
||||||
await controller.run(async progress => {
|
await controller.run(async progress => {
|
||||||
|
|
@ -165,7 +166,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
||||||
|
|
||||||
// Testing harness for runCLI mode.
|
// 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('\n-------------8<-------------\n');
|
||||||
process.stdout.write(sources[0].text);
|
process.stdout.write(sources[0].text);
|
||||||
process.stdout.write('\n-------------8<-------------\n');
|
process.stdout.write('\n-------------8<-------------\n');
|
||||||
|
|
|
||||||
|
|
@ -544,6 +544,24 @@ test.describe('cli codegen', () => {
|
||||||
expect(fs.existsSync(traceFileName)).toBeTruthy();
|
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 }) => {
|
test('should fill tricky characters', async ({ page, openRecorder }) => {
|
||||||
const recorder = await openRecorder();
|
const recorder = await openRecorder();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ type CLITestArgs = {
|
||||||
recorderPageGetter: () => Promise<Page>;
|
recorderPageGetter: () => Promise<Page>;
|
||||||
closeRecorder: () => Promise<void>;
|
closeRecorder: () => Promise<void>;
|
||||||
openRecorder: () => Promise<Recorder>;
|
openRecorder: () => Promise<Recorder>;
|
||||||
runCLI: (args: string[]) => CLIMock;
|
runCLI: (args: string[], options?: { noAutoExit?: boolean }) => CLIMock;
|
||||||
};
|
};
|
||||||
|
|
||||||
const playwrightToAutomateInspector = require('../../../packages/playwright-core/lib/inProcessFactory').createInProcessPlaywright();
|
const playwrightToAutomateInspector = require('../../../packages/playwright-core/lib/inProcessFactory').createInProcessPlaywright();
|
||||||
|
|
@ -55,8 +55,8 @@ export const test = contextTest.extend<CLITestArgs>({
|
||||||
testInfo.skip(mode === 'service');
|
testInfo.skip(mode === 'service');
|
||||||
|
|
||||||
let cli: CLIMock | undefined;
|
let cli: CLIMock | undefined;
|
||||||
await run(cliArgs => {
|
await run((cliArgs, { noAutoExit } = {}) => {
|
||||||
cli = new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath);
|
cli = new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath, noAutoExit);
|
||||||
return cli;
|
return cli;
|
||||||
});
|
});
|
||||||
if (cli)
|
if (cli)
|
||||||
|
|
@ -178,12 +178,12 @@ class Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
class CLIMock {
|
class CLIMock {
|
||||||
private process: TestChildProcess;
|
process: TestChildProcess;
|
||||||
private waitForText: string;
|
private waitForText: string;
|
||||||
private waitForCallback: () => void;
|
private waitForCallback: () => void;
|
||||||
exited: Promise<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 = [
|
const nodeArgs = [
|
||||||
'node',
|
'node',
|
||||||
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'lib', 'cli', 'cli.js'),
|
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'lib', 'cli', 'cli.js'),
|
||||||
|
|
@ -196,7 +196,8 @@ class CLIMock {
|
||||||
this.process = childProcess({
|
this.process = childProcess({
|
||||||
command: nodeArgs,
|
command: nodeArgs,
|
||||||
env: {
|
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_HEADLESS: headless ? '1' : undefined,
|
||||||
PWTEST_CLI_EXECUTABLE_PATH: executablePath,
|
PWTEST_CLI_EXECUTABLE_PATH: executablePath,
|
||||||
DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*',
|
DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*',
|
||||||
|
|
@ -226,6 +227,10 @@ class CLIMock {
|
||||||
text() {
|
text() {
|
||||||
return removeAnsiColors(this.process.output);
|
return removeAnsiColors(this.process.output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exit(signal: NodeJS.Signals | number) {
|
||||||
|
this.process.process.kill(signal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAnsiColors(input: string): string {
|
function removeAnsiColors(input: string): string {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue