chore: enable reused browser autoclose (#16363)

This commit is contained in:
Pavel Feldman 2022-08-08 17:16:13 -07:00 коммит произвёл GitHub
Родитель 0fa20d5d1e
Коммит c99d6cdd4c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 125 добавлений и 80 удалений

Просмотреть файл

@ -29,6 +29,7 @@ import { Recorder } from '../server/recorder';
import { EmptyRecorderApp } from '../server/recorder/recorderApp';
import type { BrowserContext } from '../server/browserContext';
import { serverSideCallMetadata } from '../server/instrumentation';
import type { Mode } from '../server/recorder/recorderTypes';
export function printApiJson() {
// Note: this file is generated by build-playwright-driver.sh
@ -83,55 +84,94 @@ function selfDestruct() {
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) {
process.send!({ method: 'ready', params: { wsEndpoint } });
const handler = new ProtocolHandler(playwright);
process.on('message', async message => {
try {
if (message.method === 'kill') {
selfDestruct();
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;
}
const result = await (handler as any)[message.method](message.params);
process.send!({ id: message.id, result });
} 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[]> {
const contexts = new Set<BrowserContext>();
for (const page of playwright.allPages())

Просмотреть файл

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

Просмотреть файл

@ -33,14 +33,14 @@ export class CodeGenerator extends EventEmitter {
private _enabled: boolean;
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();
// Make a copy of options to modify them later.
launchOptions = { headless: false, ...launchOptions };
contextOptions = { ...contextOptions };
this._enabled = generateHeaders;
this._options = { browserName, generateHeaders, launchOptions, contextOptions, deviceName, saveStorage };
this._enabled = enabled;
this._options = { browserName, launchOptions, contextOptions, deviceName, saveStorage };
this.restart();
}
@ -160,15 +160,13 @@ export class CodeGenerator extends EventEmitter {
generateText(languageGenerator: LanguageGenerator) {
const text = [];
if (this._options.generateHeaders)
text.push(languageGenerator.generateHeader(this._options));
text.push(languageGenerator.generateHeader(this._options));
for (const action of this._actions) {
const actionText = languageGenerator.generateAction(action);
if (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');
}
}

Просмотреть файл

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