feat(codegen): support --save-trace option (#8267)

This commit is contained in:
Dmitry Gozman 2021-08-18 07:27:45 -07:00 committed by GitHub
parent 93c0da6c07
commit 8d81890e47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 73 additions and 30 deletions

View file

@ -270,6 +270,7 @@ type Options = {
loadStorage?: string; loadStorage?: string;
proxyServer?: string; proxyServer?: string;
saveStorage?: string; saveStorage?: string;
saveTrace?: string;
timeout: string; timeout: string;
timezone?: string; timezone?: string;
viewportSize?: string; viewportSize?: string;
@ -386,6 +387,8 @@ async function launchContext(options: Options, headless: boolean, executablePath
if (closingBrowser) if (closingBrowser)
return; return;
closingBrowser = true; closingBrowser = true;
if (options.saveTrace)
await context.tracing.stop({ path: options.saveTrace });
if (options.saveStorage) if (options.saveStorage)
await context.storageState({ path: options.saveStorage }).catch(e => null); await context.storageState({ path: options.saveStorage }).catch(e => null);
await browser.close(); await browser.close();
@ -406,6 +409,9 @@ async function launchContext(options: Options, headless: boolean, executablePath
context.setDefaultNavigationTimeout(parseInt(options.timeout, 10)); context.setDefaultNavigationTimeout(parseInt(options.timeout, 10));
} }
if (options.saveTrace)
await context.tracing.start({ screenshots: true, snapshots: true });
// 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;
@ -548,6 +554,7 @@ function commandWithOpenOptions(command: string, description: string, options: a
.option('--lang <language>', 'specify language / locale, for example "en-GB"') .option('--lang <language>', 'specify language / locale, for example "en-GB"')
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"') .option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
.option('--save-storage <filename>', 'save context storage state at the end, for later use with --load-storage') .option('--save-storage <filename>', 'save context storage state at the end, for later use with --load-storage')
.option('--save-trace <filename>', 'record a trace for the session and save it to a file')
.option('--timezone <time zone>', 'time zone to emulate, for example "Europe/Rome"') .option('--timezone <time zone>', 'time zone to emulate, for example "Europe/Rome"')
.option('--timeout <timeout>', 'timeout for Playwright actions in milliseconds', '10000') .option('--timeout <timeout>', 'timeout for Playwright actions in milliseconds', '10000')
.option('--user-agent <ua string>', 'specify user agent string') .option('--user-agent <ua string>', 'specify user agent string')

View file

@ -29,10 +29,10 @@ import { PythonLanguageGenerator } from './recorder/python';
import * as recorderSource from '../../generated/recorderSource'; import * as recorderSource from '../../generated/recorderSource';
import * as consoleApiSource from '../../generated/consoleApiSource'; import * as consoleApiSource from '../../generated/consoleApiSource';
import { RecorderApp } from './recorder/recorderApp'; import { RecorderApp } from './recorder/recorderApp';
import { CallMetadata, InstrumentationListener, internalCallMetadata, SdkObject } from '../instrumentation'; import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
import { Point } from '../../common/types'; import { Point } from '../../common/types';
import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes'; import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
import { isUnderTest } from '../../utils/utils'; import { createGuid, isUnderTest, monotonicTime } from '../../utils/utils';
import { metadataToCallLog } from './recorder/recorderUtils'; import { metadataToCallLog } from './recorder/recorderUtils';
import { Debugger } from './debugger'; import { Debugger } from './debugger';
@ -308,36 +308,64 @@ export class RecorderSupplement implements InstrumentationListener {
...describeFrame(frame), ...describeFrame(frame),
action action
}; };
this._generator.willPerformAction(actionInContext);
const noCallMetadata = internalCallMetadata(); const perform = async (action: string, params: any, cb: (callMetadata: CallMetadata) => Promise<any>) => {
try { const callMetadata: CallMetadata = {
const kActionTimeout = 5000; id: `call@${createGuid()}`,
if (action.name === 'click') { apiName: 'frame.' + action,
const { options } = toClickOptions(action); objectId: frame.guid,
await frame.click(noCallMetadata, action.selector, { ...options, timeout: kActionTimeout }); pageId: frame._page.guid,
frameId: frame.guid,
startTime: monotonicTime(),
endTime: 0,
type: 'Frame',
method: action,
params,
log: [],
snapshots: [],
};
this._generator.willPerformAction(actionInContext);
try {
await frame.instrumentation.onBeforeCall(frame, callMetadata);
await cb(callMetadata);
} catch (e) {
callMetadata.endTime = monotonicTime();
await frame.instrumentation.onAfterCall(frame, callMetadata);
this._generator.performedActionFailed(actionInContext);
return;
} }
if (action.name === 'press') {
const modifiers = toModifiers(action.modifiers); callMetadata.endTime = monotonicTime();
const shortcut = [...modifiers, action.key].join('+'); await frame.instrumentation.onAfterCall(frame, callMetadata);
await frame.press(noCallMetadata, action.selector, shortcut, { timeout: kActionTimeout });
} const timer = setTimeout(() => {
if (action.name === 'check') // Commit the action after 5 seconds so that no further signals are added to it.
await frame.check(noCallMetadata, action.selector, { timeout: kActionTimeout }); actionInContext.committed = true;
if (action.name === 'uncheck') this._timers.delete(timer);
await frame.uncheck(noCallMetadata, action.selector, { timeout: kActionTimeout }); }, 5000);
if (action.name === 'select') this._generator.didPerformAction(actionInContext);
await frame.selectOption(noCallMetadata, action.selector, [], action.options.map(value => ({ value })), { timeout: kActionTimeout }); this._timers.add(timer);
} catch (e) { };
this._generator.performedActionFailed(actionInContext);
return; const kActionTimeout = 5000;
if (action.name === 'click') {
const { options } = toClickOptions(action);
await perform('click', { selector: action.selector }, callMetadata => frame.click(callMetadata, action.selector, { ...options, timeout: kActionTimeout }));
}
if (action.name === 'press') {
const modifiers = toModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+');
await perform('press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout }));
}
if (action.name === 'check')
await perform('check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout }));
if (action.name === 'uncheck')
await perform('uncheck', { selector: action.selector }, callMetadata => frame.uncheck(callMetadata, action.selector, { timeout: kActionTimeout }));
if (action.name === 'select') {
const values = action.options.map(value => ({ value }));
await perform('selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout }));
} }
const timer = setTimeout(() => {
// Commit the action after 5 seconds so that no further signals are added to it.
actionInContext.committed = true;
this._timers.delete(timer);
}, 5000);
this._generator.didPerformAction(actionInContext);
this._timers.add(timer);
} }
private async _recordAction(frame: Frame, action: actions.Action) { private async _recordAction(frame: Frame, action: actions.Action) {

View file

@ -16,6 +16,7 @@
import { test, expect } from './inspectorTest'; import { test, expect } from './inspectorTest';
import * as url from 'url'; import * as url from 'url';
import fs from 'fs';
test.describe('cli codegen', () => { test.describe('cli codegen', () => {
test.skip(({ mode }) => mode !== 'default'); test.skip(({ mode }) => mode !== 'default');
@ -637,4 +638,11 @@ test.describe('cli codegen', () => {
expect(sources.get('JavaScript').text).toContain(`page.waitForNavigation(/*{ url: '${server.EMPTY_PAGE}' }*/)`); expect(sources.get('JavaScript').text).toContain(`page.waitForNavigation(/*{ url: '${server.EMPTY_PAGE}' }*/)`);
}); });
test('should --save-trace', async ({ runCLI }, testInfo) => {
const traceFileName = testInfo.outputPath('trace.zip');
const cli = runCLI([`--save-trace=${traceFileName}`]);
await cli.exited;
expect(fs.existsSync(traceFileName)).toBeTruthy();
});
}); });