mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: enable reused browser autoclose (#16363)
This commit is contained in:
parent
0fa20d5d1e
commit
c99d6cdd4c
@ -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())
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user