chore: enable reused browser autoclose (#16363)
This commit is contained in:
Родитель
0fa20d5d1e
Коммит
c99d6cdd4c
|
@ -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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче