chore: enable reused browser autoclose (#16363)

This commit is contained in:
Pavel Feldman 2022-08-08 17:16:13 -07:00 committed by GitHub
parent 0fa20d5d1e
commit c99d6cdd4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 125 additions and 80 deletions

View File

@ -29,6 +29,7 @@ import { Recorder } from '../server/recorder';
import { EmptyRecorderApp } from '../server/recorder/recorderApp'; import { EmptyRecorderApp } from '../server/recorder/recorderApp';
import type { BrowserContext } from '../server/browserContext'; import type { BrowserContext } from '../server/browserContext';
import { serverSideCallMetadata } from '../server/instrumentation'; import { serverSideCallMetadata } from '../server/instrumentation';
import type { Mode } from '../server/recorder/recorderTypes';
export function printApiJson() { export function printApiJson() {
// Note: this file is generated by build-playwright-driver.sh // Note: this file is generated by build-playwright-driver.sh
@ -83,55 +84,94 @@ function selfDestruct() {
const internalMetadata = serverSideCallMetadata(); const internalMetadata = serverSideCallMetadata();
class ProtocolHandler {
private _playwright: Playwright;
private _autoCloseTimer: NodeJS.Timeout | undefined;
constructor(playwright: Playwright) {
this._playwright = playwright;
}
async setMode(params: { mode: Mode, language?: string, file?: string }) {
await gc(this._playwright);
if (params.mode === 'none') {
for (const recorder of await allRecorders(this._playwright)) {
recorder.setHighlightedSelector('');
recorder.setMode('none');
}
this.setAutoClose({ enabled: true });
return;
}
const browsers = this._playwright.allBrowsers();
if (!browsers.length)
await this._playwright.chromium.launch(internalMetadata, { headless: false });
// Create page if none.
const pages = this._playwright.allPages();
if (!pages.length) {
const [browser] = this._playwright.allBrowsers();
const { context } = await browser.newContextForReuse({}, internalMetadata);
await context.newPage(internalMetadata);
}
// Toggle the mode.
for (const recorder of await allRecorders(this._playwright)) {
recorder.setHighlightedSelector('');
if (params.mode === 'recording')
recorder.setOutput(params.language!, params.file);
recorder.setMode(params.mode);
}
this.setAutoClose({ enabled: true });
}
async setAutoClose(params: { enabled: boolean }) {
if (this._autoCloseTimer)
clearTimeout(this._autoCloseTimer);
if (!params.enabled)
return;
const heartBeat = () => {
if (!this._playwright.allPages().length)
selfDestruct();
else
this._autoCloseTimer = setTimeout(heartBeat, 5000);
};
this._autoCloseTimer = setTimeout(heartBeat, 30000);
}
async highlight(params: { selector: string }) {
for (const recorder of await allRecorders(this._playwright))
recorder.setHighlightedSelector(params.selector);
}
async kill() {
selfDestruct();
}
}
function wireController(playwright: Playwright, wsEndpoint: string) { function wireController(playwright: Playwright, wsEndpoint: string) {
process.send!({ method: 'ready', params: { wsEndpoint } }); process.send!({ method: 'ready', params: { wsEndpoint } });
const handler = new ProtocolHandler(playwright);
process.on('message', async message => { process.on('message', async message => {
try { try {
if (message.method === 'kill') { const result = await (handler as any)[message.method](message.params);
selfDestruct(); process.send!({ id: message.id, result });
return;
}
if (message.method === 'inspect') {
if (!message.params.enabled) {
for (const recorder of await allRecorders(playwright)) {
recorder.setHighlightedSelector('');
recorder.setMode('none');
}
return;
}
// Create browser if none.
const browsers = playwright.allBrowsers();
if (!browsers.length)
await playwright.chromium.launch(internalMetadata, { headless: false });
// Create page if none.
const pages = playwright.allPages();
if (!pages.length) {
const [browser] = playwright.allBrowsers();
const { context } = await browser.newContextForReuse({}, internalMetadata);
await context.newPage(internalMetadata);
}
// Toggle inspect mode.
for (const recorder of await allRecorders(playwright)) {
recorder.setHighlightedSelector('');
recorder.setMode('inspecting');
}
return;
}
if (message.method === 'highlight') {
for (const recorder of await allRecorders(playwright))
recorder.setHighlightedSelector(message.params.selector);
return;
}
} catch (e) { } catch (e) {
process.send!({ method: 'error', params: { error: e.toString() } }); process.send!({ id: message.id, error: e.toString() });
} }
}); });
} }
async function gc(playwright: Playwright) {
for (const browser of playwright.allBrowsers()) {
for (const context of browser.contexts()) {
if (!context.pages().length)
await context.close(serverSideCallMetadata());
}
if (!browser.contexts())
await browser.close();
}
}
async function allRecorders(playwright: Playwright): Promise<Recorder[]> { async function allRecorders(playwright: Playwright): Promise<Recorder[]> {
const contexts = new Set<BrowserContext>(); const contexts = new Set<BrowserContext>();
for (const page of playwright.allPages()) for (const page of playwright.allPages())

View File

@ -40,6 +40,7 @@ import { metadataToCallLog } from './recorder/recorderUtils';
import { Debugger } from './debugger'; import { Debugger } from './debugger';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { raceAgainstTimeout } from '../utils/timeoutRunner'; import { raceAgainstTimeout } from '../utils/timeoutRunner';
import type { LanguageGenerator } from './recorder/language';
type BindingSource = { frame: Frame, page: Page }; type BindingSource = { frame: Frame, page: Page };
@ -204,6 +205,10 @@ export class Recorder implements InstrumentationListener {
this._refreshOverlay(); this._refreshOverlay();
} }
setOutput(language: string, outputFile: string | undefined) {
this._contextRecorder.setOutput(language, outputFile);
}
private _refreshOverlay() { private _refreshOverlay() {
for (const page of this._context.pages()) for (const page of this._context.pages())
page.mainFrame().evaluateExpression('window.__pw_refreshOverlay()', false, undefined, 'main').catch(() => {}); page.mainFrame().evaluateExpression('window.__pw_refreshOverlay()', false, undefined, 'main').catch(() => {});
@ -312,35 +317,20 @@ class ContextRecorder extends EventEmitter {
private _context: BrowserContext; private _context: BrowserContext;
private _params: channels.BrowserContextRecorderSupplementEnableParams; private _params: channels.BrowserContextRecorderSupplementEnableParams;
private _recorderSources: Source[]; private _recorderSources: Source[];
private _throttledOutputFile: ThrottledFile | null = null;
private _orderedLanguages: LanguageGenerator[] = [];
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
super(); super();
this._context = context; this._context = context;
this._params = params; this._params = params;
const language = params.language || context._browser.options.sdkLanguage;
const languages = new Set([
new JavaLanguageGenerator(),
new JavaScriptLanguageGenerator(false),
new JavaScriptLanguageGenerator(true),
new PythonLanguageGenerator(false, false),
new PythonLanguageGenerator(true, false),
new PythonLanguageGenerator(false, true),
new CSharpLanguageGenerator(),
]);
const primaryLanguage = [...languages].find(l => l.id === language)!;
if (!primaryLanguage)
throw new Error(`\n===============================\nUnsupported language: '${language}'\n===============================\n`);
languages.delete(primaryLanguage);
const orderedLanguages = [primaryLanguage, ...languages];
this._recorderSources = []; this._recorderSources = [];
const language = params.language || context._browser.options.sdkLanguage;
this.setOutput(language, params.outputFile);
const generator = new CodeGenerator(context._browser.options.name, params.mode === 'recording', params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage); const generator = new CodeGenerator(context._browser.options.name, params.mode === 'recording', params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage);
const throttledOutputFile = params.outputFile ? new ThrottledFile(params.outputFile) : null;
generator.on('change', () => { generator.on('change', () => {
this._recorderSources = []; this._recorderSources = [];
for (const languageGenerator of orderedLanguages) { for (const languageGenerator of this._orderedLanguages) {
const source: Source = { const source: Source = {
isRecorded: true, isRecorded: true,
file: languageGenerator.fileName, file: languageGenerator.fileName,
@ -350,25 +340,43 @@ class ContextRecorder extends EventEmitter {
}; };
source.revealLine = source.text.split('\n').length - 1; source.revealLine = source.text.split('\n').length - 1;
this._recorderSources.push(source); this._recorderSources.push(source);
if (languageGenerator === orderedLanguages[0]) if (languageGenerator === this._orderedLanguages[0])
throttledOutputFile?.setContent(source.text); this._throttledOutputFile?.setContent(source.text);
} }
this.emit(ContextRecorder.Events.Change, { this.emit(ContextRecorder.Events.Change, {
sources: this._recorderSources, sources: this._recorderSources,
primaryFileName: primaryLanguage.fileName primaryFileName: this._orderedLanguages[0].fileName
}); });
}); });
if (throttledOutputFile) { context.on(BrowserContext.Events.BeforeClose, () => {
context.on(BrowserContext.Events.BeforeClose, () => { this._throttledOutputFile?.flush();
throttledOutputFile.flush(); });
}); process.on('exit', () => {
process.on('exit', () => { this._throttledOutputFile?.flush();
throttledOutputFile.flush(); });
});
}
this._generator = generator; this._generator = generator;
} }
setOutput(language: string, outputFile: string | undefined) {
const languages = new Set([
new JavaLanguageGenerator(),
new JavaScriptLanguageGenerator(false),
new JavaScriptLanguageGenerator(true),
new PythonLanguageGenerator(false, false),
new PythonLanguageGenerator(true, false),
new PythonLanguageGenerator(false, true),
new CSharpLanguageGenerator(),
]);
const primaryLanguage = [...languages].find(l => l.id === language);
if (!primaryLanguage)
throw new Error(`\n===============================\nUnsupported language: '${language}'\n===============================\n`);
languages.delete(primaryLanguage);
this._orderedLanguages = [primaryLanguage, ...languages];
this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null;
this._generator?.restart();
}
async install() { async install() {
this._context.on(BrowserContext.Events.Page, page => this._onPage(page)); this._context.on(BrowserContext.Events.Page, page => this._onPage(page));
for (const page of this._context.pages()) for (const page of this._context.pages())
@ -625,7 +633,7 @@ class ThrottledFile {
setContent(text: string) { setContent(text: string) {
this._text = text; this._text = text;
if (!this._timer) if (!this._timer)
this._timer = setTimeout(() => this.flush(), 1000); this._timer = setTimeout(() => this.flush(), 250);
} }
flush(): void { flush(): void {

View File

@ -33,14 +33,14 @@ export class CodeGenerator extends EventEmitter {
private _enabled: boolean; private _enabled: boolean;
private _options: LanguageGeneratorOptions; private _options: LanguageGeneratorOptions;
constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName: string | undefined, saveStorage: string | undefined) { constructor(browserName: string, enabled: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName: string | undefined, saveStorage: string | undefined) {
super(); super();
// Make a copy of options to modify them later. // Make a copy of options to modify them later.
launchOptions = { headless: false, ...launchOptions }; launchOptions = { headless: false, ...launchOptions };
contextOptions = { ...contextOptions }; contextOptions = { ...contextOptions };
this._enabled = generateHeaders; this._enabled = enabled;
this._options = { browserName, generateHeaders, launchOptions, contextOptions, deviceName, saveStorage }; this._options = { browserName, launchOptions, contextOptions, deviceName, saveStorage };
this.restart(); this.restart();
} }
@ -160,15 +160,13 @@ export class CodeGenerator extends EventEmitter {
generateText(languageGenerator: LanguageGenerator) { generateText(languageGenerator: LanguageGenerator) {
const text = []; const text = [];
if (this._options.generateHeaders) text.push(languageGenerator.generateHeader(this._options));
text.push(languageGenerator.generateHeader(this._options));
for (const action of this._actions) { for (const action of this._actions) {
const actionText = languageGenerator.generateAction(action); const actionText = languageGenerator.generateAction(action);
if (actionText) if (actionText)
text.push(actionText); text.push(actionText);
} }
if (this._options.generateHeaders) text.push(languageGenerator.generateFooter(this._options.saveStorage));
text.push(languageGenerator.generateFooter(this._options.saveStorage));
return text.join('\n'); return text.join('\n');
} }
} }

View File

@ -20,7 +20,6 @@ import type { Action, DialogSignal, DownloadSignal, NavigationSignal, PopupSigna
export type LanguageGeneratorOptions = { export type LanguageGeneratorOptions = {
browserName: string; browserName: string;
generateHeaders: boolean;
launchOptions: LaunchOptions; launchOptions: LaunchOptions;
contextOptions: BrowserContextOptions; contextOptions: BrowserContextOptions;
deviceName?: string; deviceName?: string;