chore: expose driver protocol on the pw object (#17363)
This commit is contained in:
parent
b09ea69024
commit
872bcc9734
|
|
@ -21,15 +21,12 @@ import * as playwright from '../..';
|
||||||
import type { BrowserType } from '../client/browserType';
|
import type { BrowserType } from '../client/browserType';
|
||||||
import type { LaunchServerOptions } from '../client/types';
|
import type { LaunchServerOptions } from '../client/types';
|
||||||
import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from '../server';
|
import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from '../server';
|
||||||
import type { Playwright } from '../server';
|
import type { Playwright } from '../server/playwright';
|
||||||
import { IpcTransport, PipeTransport } from '../protocol/transport';
|
import { IpcTransport, PipeTransport } from '../protocol/transport';
|
||||||
import { PlaywrightServer } from '../remote/playwrightServer';
|
import { PlaywrightServer } from '../remote/playwrightServer';
|
||||||
import { gracefullyCloseAll } from '../utils/processLauncher';
|
import { gracefullyCloseAll } from '../utils/processLauncher';
|
||||||
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';
|
import type { Mode } from '../server/recorder/recorderTypes';
|
||||||
|
import { ReuseController } from '../server/reuseController';
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -60,8 +57,8 @@ export async function runServer(port: number | undefined, path = '/', maxClients
|
||||||
process.on('exit', () => server.close().catch(console.error));
|
process.on('exit', () => server.close().catch(console.error));
|
||||||
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
|
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
|
||||||
process.stdin.on('close', () => selfDestruct());
|
process.stdin.on('close', () => selfDestruct());
|
||||||
if (process.send && server.preLaunchedPlaywright())
|
if (reuseBrowser && process.send)
|
||||||
wireController(server.preLaunchedPlaywright()!, wsEndpoint);
|
wireController(server.preLaunchedPlaywright(), wsEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function launchBrowserServer(browserName: string, configFile?: string) {
|
export async function launchBrowserServer(browserName: string, configFile?: string) {
|
||||||
|
|
@ -82,118 +79,48 @@ function selfDestruct() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalMetadata = serverSideCallMetadata();
|
|
||||||
|
|
||||||
class ProtocolHandler {
|
class ProtocolHandler {
|
||||||
private _playwright: Playwright;
|
private _controller: ReuseController;
|
||||||
private _autoCloseTimer: NodeJS.Timeout | undefined;
|
|
||||||
|
|
||||||
constructor(playwright: Playwright) {
|
constructor(playwright: Playwright) {
|
||||||
this._playwright = playwright;
|
this._controller = playwright.reuseController;
|
||||||
playwright.instrumentation.addListener({
|
this._controller.setAutoCloseAllowed(true);
|
||||||
onPageOpen: () => this._sendSnapshot(),
|
this._controller.setTrackHierarcy(true);
|
||||||
onPageNavigated: () => this._sendSnapshot(),
|
this._controller.setReuseBrowser(true);
|
||||||
onPageClose: () => this._sendSnapshot(),
|
this._controller.on(ReuseController.Events.BrowsersChanged, browsers => {
|
||||||
}, null);
|
process.send!({ method: 'browsersChanged', params: { browsers } });
|
||||||
}
|
});
|
||||||
|
this._controller.on(ReuseController.Events.InspectRequested, selector => {
|
||||||
private _sendSnapshot() {
|
process.send!({ method: 'inspectRequested', params: { selector } });
|
||||||
const browsers = [];
|
});
|
||||||
for (const browser of this._playwright.allBrowsers()) {
|
|
||||||
const b = {
|
|
||||||
name: browser.options.name,
|
|
||||||
guid: browser.guid,
|
|
||||||
contexts: [] as any[]
|
|
||||||
};
|
|
||||||
browsers.push(b);
|
|
||||||
for (const context of browser.contexts()) {
|
|
||||||
const c = {
|
|
||||||
guid: context.guid,
|
|
||||||
pages: [] as any[]
|
|
||||||
};
|
|
||||||
b.contexts.push(c);
|
|
||||||
for (const page of context.pages()) {
|
|
||||||
const p = {
|
|
||||||
guid: page.guid,
|
|
||||||
url: page.mainFrame().url()
|
|
||||||
};
|
|
||||||
c.pages.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
process.send!({ method: 'browsersChanged', params: { browsers } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetForReuse() {
|
async resetForReuse() {
|
||||||
const contexts = new Set<BrowserContext>();
|
await this._controller.resetForReuse();
|
||||||
for (const page of this._playwright.allPages())
|
|
||||||
contexts.add(page.context());
|
|
||||||
for (const context of contexts)
|
|
||||||
await context.resetForReuse(internalMetadata, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigate(params: { url: string }) {
|
async navigate(params: { url: string }) {
|
||||||
for (const p of this._playwright.allPages())
|
await this._controller.navigateAll(params.url);
|
||||||
await p.mainFrame().goto(internalMetadata, params.url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMode(params: { mode: Mode, language?: string, file?: string }) {
|
async setMode(params: { mode: Mode, language?: string, file?: string }) {
|
||||||
await gc(this._playwright);
|
await this._controller.setRecorderMode(params);
|
||||||
|
|
||||||
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 }) {
|
async setAutoClose(params: { enabled: boolean }) {
|
||||||
if (this._autoCloseTimer)
|
await this._controller.setAutoCloseEnabled(params.enabled);
|
||||||
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 }) {
|
async highlight(params: { selector: string }) {
|
||||||
for (const recorder of await allRecorders(this._playwright))
|
await this._controller.highlightAll(params.selector);
|
||||||
recorder.setHighlightedSelector(params.selector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async hideHighlight() {
|
async hideHighlight() {
|
||||||
await this._playwright.hideHighlight();
|
await this._controller.hideHighlightAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
async kill() {
|
async kill() {
|
||||||
selfDestruct();
|
await this._controller.kill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,28 +136,3 @@ function wireController(playwright: Playwright, wsEndpoint: string) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
|
||||||
contexts.add(page.context());
|
|
||||||
const result = await Promise.all([...contexts].map(c => Recorder.show(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp()))));
|
|
||||||
return result.filter(Boolean) as Recorder[];
|
|
||||||
}
|
|
||||||
|
|
||||||
class InspectingRecorderApp extends EmptyRecorderApp {
|
|
||||||
override async setSelector(selector: string): Promise<void> {
|
|
||||||
process.send!({ method: 'inspectRequested', params: { selector } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: str
|
||||||
const log = debug(`pw:grid:worker:${workerId}`);
|
const log = debug(`pw:grid:worker:${workerId}`);
|
||||||
log('created');
|
log('created');
|
||||||
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
|
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
|
||||||
new PlaywrightConnection(Promise.resolve(), 'auto', ws, { enableSocksProxy: true, browserName, launchOptions: {} }, { playwright: null, browser: null }, log, async () => {
|
new PlaywrightConnection(Promise.resolve(), 'auto', ws, false, { enableSocksProxy: true, browserName, launchOptions: {} }, { playwright: null, browser: null }, log, async () => {
|
||||||
log('exiting process');
|
log('exiting process');
|
||||||
setTimeout(() => process.exit(0), 30000);
|
setTimeout(() => process.exit(0), 30000);
|
||||||
// Meanwhile, try to gracefully close all browsers.
|
// Meanwhile, try to gracefully close all browsers.
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ export type InitializerTraits<T> =
|
||||||
T extends BrowserTypeChannel ? BrowserTypeInitializer :
|
T extends BrowserTypeChannel ? BrowserTypeInitializer :
|
||||||
T extends SelectorsChannel ? SelectorsInitializer :
|
T extends SelectorsChannel ? SelectorsInitializer :
|
||||||
T extends SocksSupportChannel ? SocksSupportInitializer :
|
T extends SocksSupportChannel ? SocksSupportInitializer :
|
||||||
|
T extends ReuseControllerChannel ? ReuseControllerInitializer :
|
||||||
T extends PlaywrightChannel ? PlaywrightInitializer :
|
T extends PlaywrightChannel ? PlaywrightInitializer :
|
||||||
T extends RootChannel ? RootInitializer :
|
T extends RootChannel ? RootInitializer :
|
||||||
T extends LocalUtilsChannel ? LocalUtilsInitializer :
|
T extends LocalUtilsChannel ? LocalUtilsInitializer :
|
||||||
|
|
@ -89,6 +90,7 @@ export type EventsTraits<T> =
|
||||||
T extends BrowserTypeChannel ? BrowserTypeEvents :
|
T extends BrowserTypeChannel ? BrowserTypeEvents :
|
||||||
T extends SelectorsChannel ? SelectorsEvents :
|
T extends SelectorsChannel ? SelectorsEvents :
|
||||||
T extends SocksSupportChannel ? SocksSupportEvents :
|
T extends SocksSupportChannel ? SocksSupportEvents :
|
||||||
|
T extends ReuseControllerChannel ? ReuseControllerEvents :
|
||||||
T extends PlaywrightChannel ? PlaywrightEvents :
|
T extends PlaywrightChannel ? PlaywrightEvents :
|
||||||
T extends RootChannel ? RootEvents :
|
T extends RootChannel ? RootEvents :
|
||||||
T extends LocalUtilsChannel ? LocalUtilsEvents :
|
T extends LocalUtilsChannel ? LocalUtilsEvents :
|
||||||
|
|
@ -126,6 +128,7 @@ export type EventTargetTraits<T> =
|
||||||
T extends BrowserTypeChannel ? BrowserTypeEventTarget :
|
T extends BrowserTypeChannel ? BrowserTypeEventTarget :
|
||||||
T extends SelectorsChannel ? SelectorsEventTarget :
|
T extends SelectorsChannel ? SelectorsEventTarget :
|
||||||
T extends SocksSupportChannel ? SocksSupportEventTarget :
|
T extends SocksSupportChannel ? SocksSupportEventTarget :
|
||||||
|
T extends ReuseControllerChannel ? ReuseControllerEventTarget :
|
||||||
T extends PlaywrightChannel ? PlaywrightEventTarget :
|
T extends PlaywrightChannel ? PlaywrightEventTarget :
|
||||||
T extends RootChannel ? RootEventTarget :
|
T extends RootChannel ? RootEventTarget :
|
||||||
T extends LocalUtilsChannel ? LocalUtilsEventTarget :
|
T extends LocalUtilsChannel ? LocalUtilsEventTarget :
|
||||||
|
|
@ -555,6 +558,94 @@ export type PlaywrightHideHighlightResult = void;
|
||||||
export interface PlaywrightEvents {
|
export interface PlaywrightEvents {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------- ReuseController -----------
|
||||||
|
export type ReuseControllerInitializer = {};
|
||||||
|
export interface ReuseControllerEventTarget {
|
||||||
|
on(event: 'inspectRequested', callback: (params: ReuseControllerInspectRequestedEvent) => void): this;
|
||||||
|
on(event: 'browsersChanged', callback: (params: ReuseControllerBrowsersChangedEvent) => void): this;
|
||||||
|
}
|
||||||
|
export interface ReuseControllerChannel extends ReuseControllerEventTarget, Channel {
|
||||||
|
_type_ReuseController: boolean;
|
||||||
|
setTrackHierarchy(params: ReuseControllerSetTrackHierarchyParams, metadata?: Metadata): Promise<ReuseControllerSetTrackHierarchyResult>;
|
||||||
|
setReuseBrowser(params: ReuseControllerSetReuseBrowserParams, metadata?: Metadata): Promise<ReuseControllerSetReuseBrowserResult>;
|
||||||
|
resetForReuse(params?: ReuseControllerResetForReuseParams, metadata?: Metadata): Promise<ReuseControllerResetForReuseResult>;
|
||||||
|
navigateAll(params: ReuseControllerNavigateAllParams, metadata?: Metadata): Promise<ReuseControllerNavigateAllResult>;
|
||||||
|
setRecorderMode(params: ReuseControllerSetRecorderModeParams, metadata?: Metadata): Promise<ReuseControllerSetRecorderModeResult>;
|
||||||
|
setAutoClose(params: ReuseControllerSetAutoCloseParams, metadata?: Metadata): Promise<ReuseControllerSetAutoCloseResult>;
|
||||||
|
highlightAll(params: ReuseControllerHighlightAllParams, metadata?: Metadata): Promise<ReuseControllerHighlightAllResult>;
|
||||||
|
hideHighlightAll(params?: ReuseControllerHideHighlightAllParams, metadata?: Metadata): Promise<ReuseControllerHideHighlightAllResult>;
|
||||||
|
kill(params?: ReuseControllerKillParams, metadata?: Metadata): Promise<ReuseControllerKillResult>;
|
||||||
|
}
|
||||||
|
export type ReuseControllerInspectRequestedEvent = {
|
||||||
|
selector: string,
|
||||||
|
};
|
||||||
|
export type ReuseControllerBrowsersChangedEvent = {
|
||||||
|
browsers: {
|
||||||
|
contexts: {
|
||||||
|
pages: string[],
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
|
};
|
||||||
|
export type ReuseControllerSetTrackHierarchyParams = {
|
||||||
|
enabled: boolean,
|
||||||
|
};
|
||||||
|
export type ReuseControllerSetTrackHierarchyOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type ReuseControllerSetTrackHierarchyResult = void;
|
||||||
|
export type ReuseControllerSetReuseBrowserParams = {
|
||||||
|
enabled: boolean,
|
||||||
|
};
|
||||||
|
export type ReuseControllerSetReuseBrowserOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type ReuseControllerSetReuseBrowserResult = void;
|
||||||
|
export type ReuseControllerResetForReuseParams = {};
|
||||||
|
export type ReuseControllerResetForReuseOptions = {};
|
||||||
|
export type ReuseControllerResetForReuseResult = void;
|
||||||
|
export type ReuseControllerNavigateAllParams = {
|
||||||
|
url: string,
|
||||||
|
};
|
||||||
|
export type ReuseControllerNavigateAllOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type ReuseControllerNavigateAllResult = void;
|
||||||
|
export type ReuseControllerSetRecorderModeParams = {
|
||||||
|
mode: 'inspecting' | 'recording' | 'none',
|
||||||
|
language?: string,
|
||||||
|
file?: string,
|
||||||
|
};
|
||||||
|
export type ReuseControllerSetRecorderModeOptions = {
|
||||||
|
language?: string,
|
||||||
|
file?: string,
|
||||||
|
};
|
||||||
|
export type ReuseControllerSetRecorderModeResult = void;
|
||||||
|
export type ReuseControllerSetAutoCloseParams = {
|
||||||
|
enabled: boolean,
|
||||||
|
};
|
||||||
|
export type ReuseControllerSetAutoCloseOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type ReuseControllerSetAutoCloseResult = void;
|
||||||
|
export type ReuseControllerHighlightAllParams = {
|
||||||
|
selector: string,
|
||||||
|
};
|
||||||
|
export type ReuseControllerHighlightAllOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type ReuseControllerHighlightAllResult = void;
|
||||||
|
export type ReuseControllerHideHighlightAllParams = {};
|
||||||
|
export type ReuseControllerHideHighlightAllOptions = {};
|
||||||
|
export type ReuseControllerHideHighlightAllResult = void;
|
||||||
|
export type ReuseControllerKillParams = {};
|
||||||
|
export type ReuseControllerKillOptions = {};
|
||||||
|
export type ReuseControllerKillResult = void;
|
||||||
|
|
||||||
|
export interface ReuseControllerEvents {
|
||||||
|
'inspectRequested': ReuseControllerInspectRequestedEvent;
|
||||||
|
'browsersChanged': ReuseControllerBrowsersChangedEvent;
|
||||||
|
}
|
||||||
|
|
||||||
// ----------- SocksSupport -----------
|
// ----------- SocksSupport -----------
|
||||||
export type SocksSupportInitializer = {};
|
export type SocksSupportInitializer = {};
|
||||||
export interface SocksSupportEventTarget {
|
export interface SocksSupportEventTarget {
|
||||||
|
|
|
||||||
|
|
@ -622,6 +622,68 @@ Playwright:
|
||||||
|
|
||||||
hideHighlight:
|
hideHighlight:
|
||||||
|
|
||||||
|
ReuseController:
|
||||||
|
type: interface
|
||||||
|
|
||||||
|
commands:
|
||||||
|
setTrackHierarchy:
|
||||||
|
parameters:
|
||||||
|
enabled: boolean
|
||||||
|
|
||||||
|
setReuseBrowser:
|
||||||
|
parameters:
|
||||||
|
enabled: boolean
|
||||||
|
|
||||||
|
resetForReuse:
|
||||||
|
|
||||||
|
navigateAll:
|
||||||
|
parameters:
|
||||||
|
url: string
|
||||||
|
|
||||||
|
setRecorderMode:
|
||||||
|
parameters:
|
||||||
|
mode:
|
||||||
|
type: enum
|
||||||
|
literals:
|
||||||
|
- inspecting
|
||||||
|
- recording
|
||||||
|
- none
|
||||||
|
language: string?
|
||||||
|
file: string?
|
||||||
|
|
||||||
|
setAutoClose:
|
||||||
|
parameters:
|
||||||
|
enabled: boolean
|
||||||
|
|
||||||
|
highlightAll:
|
||||||
|
parameters:
|
||||||
|
selector: string
|
||||||
|
|
||||||
|
hideHighlightAll:
|
||||||
|
|
||||||
|
kill:
|
||||||
|
|
||||||
|
events:
|
||||||
|
inspectRequested:
|
||||||
|
parameters:
|
||||||
|
selector: string
|
||||||
|
|
||||||
|
browsersChanged:
|
||||||
|
parameters:
|
||||||
|
browsers:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
contexts:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pages:
|
||||||
|
type: array
|
||||||
|
items: string
|
||||||
|
|
||||||
SocksSupport:
|
SocksSupport:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,49 @@ scheme.PlaywrightNewRequestResult = tObject({
|
||||||
});
|
});
|
||||||
scheme.PlaywrightHideHighlightParams = tOptional(tObject({}));
|
scheme.PlaywrightHideHighlightParams = tOptional(tObject({}));
|
||||||
scheme.PlaywrightHideHighlightResult = tOptional(tObject({}));
|
scheme.PlaywrightHideHighlightResult = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerInitializer = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerInspectRequestedEvent = tObject({
|
||||||
|
selector: tString,
|
||||||
|
});
|
||||||
|
scheme.ReuseControllerBrowsersChangedEvent = tObject({
|
||||||
|
browsers: tArray(tObject({
|
||||||
|
contexts: tArray(tObject({
|
||||||
|
pages: tArray(tString),
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
scheme.ReuseControllerSetTrackHierarchyParams = tObject({
|
||||||
|
enabled: tBoolean,
|
||||||
|
});
|
||||||
|
scheme.ReuseControllerSetTrackHierarchyResult = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerSetReuseBrowserParams = tObject({
|
||||||
|
enabled: tBoolean,
|
||||||
|
});
|
||||||
|
scheme.ReuseControllerSetReuseBrowserResult = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerResetForReuseParams = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerResetForReuseResult = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerNavigateAllParams = tObject({
|
||||||
|
url: tString,
|
||||||
|
});
|
||||||
|
scheme.ReuseControllerNavigateAllResult = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerSetRecorderModeParams = tObject({
|
||||||
|
mode: tEnum(['inspecting', 'recording', 'none']),
|
||||||
|
language: tOptional(tString),
|
||||||
|
file: tOptional(tString),
|
||||||
|
});
|
||||||
|
scheme.ReuseControllerSetRecorderModeResult = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerSetAutoCloseParams = tObject({
|
||||||
|
enabled: tBoolean,
|
||||||
|
});
|
||||||
|
scheme.ReuseControllerSetAutoCloseResult = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerHighlightAllParams = tObject({
|
||||||
|
selector: tString,
|
||||||
|
});
|
||||||
|
scheme.ReuseControllerHighlightAllResult = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerHideHighlightAllParams = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerHideHighlightAllResult = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerKillParams = tOptional(tObject({}));
|
||||||
|
scheme.ReuseControllerKillResult = tOptional(tObject({}));
|
||||||
scheme.SocksSupportInitializer = tOptional(tObject({}));
|
scheme.SocksSupportInitializer = tOptional(tObject({}));
|
||||||
scheme.SocksSupportSocksRequestedEvent = tObject({
|
scheme.SocksSupportSocksRequestedEvent = tObject({
|
||||||
uid: tString,
|
uid: tString,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { WebSocket } from '../utilsBundle';
|
import type { WebSocket } from '../utilsBundle';
|
||||||
import type { Playwright } from '../server';
|
import type { DispatcherScope, Playwright } from '../server';
|
||||||
import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from '../server';
|
import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from '../server';
|
||||||
import { Browser } from '../server/browser';
|
import { Browser } from '../server/browser';
|
||||||
import { serverSideCallMetadata } from '../server/instrumentation';
|
import { serverSideCallMetadata } from '../server/instrumentation';
|
||||||
|
|
@ -24,6 +24,7 @@ import { SocksProxy } from '../common/socksProxy';
|
||||||
import type { Mode } from './playwrightServer';
|
import type { Mode } from './playwrightServer';
|
||||||
import { assert } from '../utils';
|
import { assert } from '../utils';
|
||||||
import type { LaunchOptions } from '../server/types';
|
import type { LaunchOptions } from '../server/types';
|
||||||
|
import { ReuseControllerDispatcher } from '../server/dispatchers/reuseControllerDispatcher';
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
enableSocksProxy: boolean,
|
enableSocksProxy: boolean,
|
||||||
|
|
@ -45,9 +46,9 @@ export class PlaywrightConnection {
|
||||||
private _disconnected = false;
|
private _disconnected = false;
|
||||||
private _preLaunched: PreLaunched;
|
private _preLaunched: PreLaunched;
|
||||||
private _options: Options;
|
private _options: Options;
|
||||||
private _root: RootDispatcher;
|
private _root: DispatcherScope;
|
||||||
|
|
||||||
constructor(lock: Promise<void>, mode: Mode, ws: WebSocket, options: Options, preLaunched: PreLaunched, log: (m: string) => void, onClose: () => void) {
|
constructor(lock: Promise<void>, mode: Mode, ws: WebSocket, isReuseControllerClient: boolean, options: Options, preLaunched: PreLaunched, log: (m: string) => void, onClose: () => void) {
|
||||||
this._ws = ws;
|
this._ws = ws;
|
||||||
this._preLaunched = preLaunched;
|
this._preLaunched = preLaunched;
|
||||||
this._options = options;
|
this._options = options;
|
||||||
|
|
@ -72,6 +73,11 @@ export class PlaywrightConnection {
|
||||||
ws.on('close', () => this._onDisconnect());
|
ws.on('close', () => this._onDisconnect());
|
||||||
ws.on('error', error => this._onDisconnect(error));
|
ws.on('error', error => this._onDisconnect(error));
|
||||||
|
|
||||||
|
if (isReuseControllerClient) {
|
||||||
|
this._root = this._initReuseControllerMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._root = new RootDispatcher(this._dispatcherConnection, async scope => {
|
this._root = new RootDispatcher(this._dispatcherConnection, async scope => {
|
||||||
if (mode === 'reuse-browser')
|
if (mode === 'reuse-browser')
|
||||||
return await this._initReuseBrowsersMode(scope);
|
return await this._initReuseBrowsersMode(scope);
|
||||||
|
|
@ -130,6 +136,14 @@ export class PlaywrightConnection {
|
||||||
return playwrightDispatcher;
|
return playwrightDispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _initReuseControllerMode(): ReuseControllerDispatcher {
|
||||||
|
this._debugLog(`engaged reuse controller mode`);
|
||||||
|
const playwright = this._preLaunched.playwright!;
|
||||||
|
this._cleanups.push(() => gracefullyCloseAll());
|
||||||
|
// Always create new instance based on the reused Playwright instance.
|
||||||
|
return new ReuseControllerDispatcher(this._dispatcherConnection, playwright.reuseController);
|
||||||
|
}
|
||||||
|
|
||||||
private async _initReuseBrowsersMode(scope: RootDispatcher) {
|
private async _initReuseBrowsersMode(scope: RootDispatcher) {
|
||||||
this._debugLog(`engaged reuse browsers mode for ${this._options.browserName}`);
|
this._debugLog(`engaged reuse browsers mode for ${this._options.browserName}`);
|
||||||
const playwright = this._preLaunched.playwright!;
|
const playwright = this._preLaunched.playwright!;
|
||||||
|
|
@ -150,7 +164,7 @@ export class PlaywrightConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!browser) {
|
if (!browser) {
|
||||||
browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), {
|
browser = await playwright[(this._options.browserName || 'chromium') as 'chromium'].launch(serverSideCallMetadata(), {
|
||||||
...this._options.launchOptions,
|
...this._options.launchOptions,
|
||||||
headless: false,
|
headless: false,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ function newLogger() {
|
||||||
return (message: string) => debugLog(`[id=${id}] ${message}`);
|
return (message: string) => debugLog(`[id=${id}] ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: replace 'reuse-browser' with 'allow-reuse' in 1.27.
|
||||||
export type Mode = 'use-pre-launched-browser' | 'reuse-browser' | 'auto';
|
export type Mode = 'use-pre-launched-browser' | 'reuse-browser' | 'auto';
|
||||||
|
|
||||||
type ServerOptions = {
|
type ServerOptions = {
|
||||||
|
|
@ -58,11 +59,11 @@ export class PlaywrightServer {
|
||||||
assert(options.preLaunchedBrowser);
|
assert(options.preLaunchedBrowser);
|
||||||
this._preLaunchedPlaywright = options.preLaunchedBrowser.options.rootSdkObject as Playwright;
|
this._preLaunchedPlaywright = options.preLaunchedBrowser.options.rootSdkObject as Playwright;
|
||||||
}
|
}
|
||||||
if (mode === 'reuse-browser')
|
|
||||||
this._preLaunchedPlaywright = createPlaywright('javascript');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preLaunchedPlaywright(): Playwright | null {
|
preLaunchedPlaywright(): Playwright {
|
||||||
|
if (!this._preLaunchedPlaywright)
|
||||||
|
this._preLaunchedPlaywright = createPlaywright('javascript');
|
||||||
return this._preLaunchedPlaywright;
|
return this._preLaunchedPlaywright;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,9 +88,10 @@ export class PlaywrightServer {
|
||||||
debugLog('Listening at ' + wsEndpoint);
|
debugLog('Listening at ' + wsEndpoint);
|
||||||
|
|
||||||
this._wsServer = new wsServer({ server, path: this._options.path });
|
this._wsServer = new wsServer({ server, path: this._options.path });
|
||||||
const semaphore = new Semaphore(this._options.maxConcurrentConnections);
|
const browserSemaphore = new Semaphore(this._options.maxConcurrentConnections);
|
||||||
|
const controllerSemaphore = new Semaphore(1);
|
||||||
this._wsServer.on('connection', (ws, request) => {
|
this._wsServer.on('connection', (ws, request) => {
|
||||||
if (semaphore.requested() >= this._options.maxIncomingConnections) {
|
if (browserSemaphore.requested() >= this._options.maxIncomingConnections) {
|
||||||
ws.close(1013, 'Playwright Server is busy');
|
ws.close(1013, 'Playwright Server is busy');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -109,9 +111,27 @@ export class PlaywrightServer {
|
||||||
|
|
||||||
const log = newLogger();
|
const log = newLogger();
|
||||||
log(`serving connection: ${request.url}`);
|
log(`serving connection: ${request.url}`);
|
||||||
|
const isReuseControllerClient = !!request.headers['x-playwright-reuse-controller'];
|
||||||
|
const semaphore = isReuseControllerClient ? controllerSemaphore : browserSemaphore;
|
||||||
|
|
||||||
|
// If we started in the legacy reuse-browser mode, create this._preLaunchedPlaywright.
|
||||||
|
// If we get a reuse-controller request, create this._preLaunchedPlaywright.
|
||||||
|
if (isReuseControllerClient || (this._mode === 'reuse-browser') && !this._preLaunchedPlaywright)
|
||||||
|
this.preLaunchedPlaywright();
|
||||||
|
|
||||||
|
// If we have a playwright to reuse, consult controller for reuse mode.
|
||||||
|
let mode = this._mode;
|
||||||
|
if (mode === 'auto' && this._preLaunchedPlaywright?.reuseController.reuseBrowser())
|
||||||
|
mode = 'reuse-browser';
|
||||||
|
|
||||||
|
if (mode === 'reuse-browser')
|
||||||
|
semaphore.setMax(1);
|
||||||
|
else
|
||||||
|
semaphore.setMax(this._options.maxConcurrentConnections);
|
||||||
|
|
||||||
const connection = new PlaywrightConnection(
|
const connection = new PlaywrightConnection(
|
||||||
semaphore.aquire(),
|
semaphore.aquire(),
|
||||||
this._mode, ws,
|
mode, ws, isReuseControllerClient,
|
||||||
{ enableSocksProxy, browserName, launchOptions },
|
{ enableSocksProxy, browserName, launchOptions },
|
||||||
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null },
|
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null },
|
||||||
log, () => semaphore.release());
|
log, () => semaphore.release());
|
||||||
|
|
@ -149,10 +169,15 @@ export class Semaphore {
|
||||||
private _max: number;
|
private _max: number;
|
||||||
private _aquired = 0;
|
private _aquired = 0;
|
||||||
private _queue: ManualPromise[] = [];
|
private _queue: ManualPromise[] = [];
|
||||||
|
|
||||||
constructor(max: number) {
|
constructor(max: number) {
|
||||||
this._max = max;
|
this._max = max;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMax(max: number) {
|
||||||
|
this._max = max;
|
||||||
|
}
|
||||||
|
|
||||||
aquire(): Promise<void> {
|
aquire(): Promise<void> {
|
||||||
const lock = new ManualPromise();
|
const lock = new ManualPromise();
|
||||||
this._queue.push(lock);
|
this._queue.push(lock);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as channels from '../../protocol/channels';
|
||||||
|
import { ReuseController } from '../reuseController';
|
||||||
|
import type { DispatcherConnection, RootDispatcher } from './dispatcher';
|
||||||
|
import { Dispatcher } from './dispatcher';
|
||||||
|
|
||||||
|
export class ReuseControllerDispatcher extends Dispatcher<ReuseController, channels.ReuseControllerChannel, RootDispatcher> implements channels.ReuseControllerChannel {
|
||||||
|
_type_ReuseController;
|
||||||
|
|
||||||
|
constructor(connection: DispatcherConnection, reuseController: ReuseController) {
|
||||||
|
super(connection, reuseController, 'ReuseController', {});
|
||||||
|
this._type_ReuseController = true;
|
||||||
|
this._object.on(ReuseController.Events.BrowsersChanged, browsers => {
|
||||||
|
this._dispatchEvent('browsersChanged', { browsers });
|
||||||
|
});
|
||||||
|
this._object.on(ReuseController.Events.InspectRequested, selector => {
|
||||||
|
this._dispatchEvent('inspectRequested', { selector });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTrackHierarchy(params: channels.ReuseControllerSetTrackHierarchyParams) {
|
||||||
|
this._object.setTrackHierarcy(params.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setReuseBrowser(params: channels.ReuseControllerSetReuseBrowserParams) {
|
||||||
|
this._object.setReuseBrowser(params.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetForReuse() {
|
||||||
|
await this._object.resetForReuse();
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateAll(params: channels.ReuseControllerNavigateAllParams) {
|
||||||
|
await this._object.navigateAll(params.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRecorderMode(params: channels.ReuseControllerSetRecorderModeParams) {
|
||||||
|
await this._object.setRecorderMode(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAutoClose(params: channels.ReuseControllerSetAutoCloseParams) {
|
||||||
|
await this._object.setAutoCloseEnabled(params.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
async highlightAll(params: channels.ReuseControllerHighlightAllParams) {
|
||||||
|
await this._object.highlightAll(params.selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
async hideHighlightAll() {
|
||||||
|
await this._object.hideHighlightAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async kill() {
|
||||||
|
await this._object.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
override _dispose() {
|
||||||
|
super._dispose();
|
||||||
|
this._object.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ import type { CallMetadata } from './instrumentation';
|
||||||
import { createInstrumentation, SdkObject } from './instrumentation';
|
import { createInstrumentation, SdkObject } from './instrumentation';
|
||||||
import { debugLogger } from '../common/debugLogger';
|
import { debugLogger } from '../common/debugLogger';
|
||||||
import type { Page } from './page';
|
import type { Page } from './page';
|
||||||
|
import { ReuseController } from './reuseController';
|
||||||
|
|
||||||
export class Playwright extends SdkObject {
|
export class Playwright extends SdkObject {
|
||||||
readonly selectors: Selectors;
|
readonly selectors: Selectors;
|
||||||
|
|
@ -35,6 +36,7 @@ export class Playwright extends SdkObject {
|
||||||
readonly firefox: Firefox;
|
readonly firefox: Firefox;
|
||||||
readonly webkit: WebKit;
|
readonly webkit: WebKit;
|
||||||
readonly options: PlaywrightOptions;
|
readonly options: PlaywrightOptions;
|
||||||
|
readonly reuseController: ReuseController;
|
||||||
private _allPages = new Set<Page>();
|
private _allPages = new Set<Page>();
|
||||||
private _allBrowsers = new Set<Browser>();
|
private _allBrowsers = new Set<Browser>();
|
||||||
|
|
||||||
|
|
@ -60,6 +62,7 @@ export class Playwright extends SdkObject {
|
||||||
this.electron = new Electron(this.options);
|
this.electron = new Electron(this.options);
|
||||||
this.android = new Android(new AdbBackend(), this.options);
|
this.android = new Android(new AdbBackend(), this.options);
|
||||||
this.selectors = this.options.selectors;
|
this.selectors = this.options.selectors;
|
||||||
|
this.reuseController = new ReuseController(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hideHighlight() {
|
async hideHighlight() {
|
||||||
|
|
|
||||||
215
packages/playwright-core/src/server/reuseController.ts
Normal file
215
packages/playwright-core/src/server/reuseController.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Mode } from '../server/recorder/recorderTypes';
|
||||||
|
import { gracefullyCloseAll } from '../utils/processLauncher';
|
||||||
|
import type { Browser } from './browser';
|
||||||
|
import type { BrowserContext } from './browserContext';
|
||||||
|
import { createInstrumentation, SdkObject, serverSideCallMetadata } from './instrumentation';
|
||||||
|
import type { InstrumentationListener } from './instrumentation';
|
||||||
|
import type { Playwright } from './playwright';
|
||||||
|
import { Recorder } from './recorder';
|
||||||
|
import { EmptyRecorderApp } from './recorder/recorderApp';
|
||||||
|
|
||||||
|
const internalMetadata = serverSideCallMetadata();
|
||||||
|
|
||||||
|
export class ReuseController extends SdkObject {
|
||||||
|
static Events = {
|
||||||
|
BrowsersChanged: 'browsersChanged',
|
||||||
|
InspectRequested: 'inspectRequested'
|
||||||
|
};
|
||||||
|
|
||||||
|
private _autoCloseTimer: NodeJS.Timeout | undefined;
|
||||||
|
// TODO: remove in 1.27
|
||||||
|
private _autoCloseAllowed = false;
|
||||||
|
private _trackHierarchyListener: InstrumentationListener | undefined;
|
||||||
|
private _playwright: Playwright;
|
||||||
|
private _reuseBrowser = false;
|
||||||
|
|
||||||
|
constructor(playwright: Playwright) {
|
||||||
|
super({ attribution: { isInternalPlaywright: true }, instrumentation: createInstrumentation() } as any, undefined, 'ReuseController');
|
||||||
|
this._playwright = playwright;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAutoCloseAllowed(allowed: boolean) {
|
||||||
|
this._autoCloseAllowed = allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.setTrackHierarcy(false);
|
||||||
|
this.setAutoCloseAllowed(false);
|
||||||
|
this.setReuseBrowser(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTrackHierarcy(enabled: boolean) {
|
||||||
|
if (enabled && !this._trackHierarchyListener) {
|
||||||
|
this._trackHierarchyListener = {
|
||||||
|
onPageOpen: () => this._emitSnapshot(),
|
||||||
|
onPageNavigated: () => this._emitSnapshot(),
|
||||||
|
onPageClose: () => this._emitSnapshot(),
|
||||||
|
};
|
||||||
|
this.instrumentation.addListener(this._trackHierarchyListener, null);
|
||||||
|
} else if (!enabled && this._trackHierarchyListener) {
|
||||||
|
this.instrumentation.removeListener(this._trackHierarchyListener);
|
||||||
|
this._trackHierarchyListener = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reuseBrowser(): boolean {
|
||||||
|
return this._reuseBrowser;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReuseBrowser(enabled: boolean) {
|
||||||
|
this._reuseBrowser = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetForReuse() {
|
||||||
|
const contexts = new Set<BrowserContext>();
|
||||||
|
for (const page of this._playwright.allPages())
|
||||||
|
contexts.add(page.context());
|
||||||
|
for (const context of contexts)
|
||||||
|
await context.resetForReuse(internalMetadata, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateAll(url: string) {
|
||||||
|
for (const p of this._playwright.allPages())
|
||||||
|
await p.mainFrame().goto(internalMetadata, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRecorderMode(params: { mode: Mode, language?: string, file?: string }) {
|
||||||
|
await this._closeBrowsersWithoutPages();
|
||||||
|
|
||||||
|
if (params.mode === 'none') {
|
||||||
|
for (const recorder of await this._allRecorders()) {
|
||||||
|
recorder.setHighlightedSelector('');
|
||||||
|
recorder.setMode('none');
|
||||||
|
}
|
||||||
|
this.setAutoCloseEnabled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._playwright.allBrowsers().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 this._allRecorders()) {
|
||||||
|
recorder.setHighlightedSelector('');
|
||||||
|
if (params.mode === 'recording')
|
||||||
|
recorder.setOutput(params.language!, params.file);
|
||||||
|
recorder.setMode(params.mode);
|
||||||
|
}
|
||||||
|
this.setAutoCloseEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAutoCloseEnabled(enabled: boolean) {
|
||||||
|
if (!this._autoCloseAllowed)
|
||||||
|
return;
|
||||||
|
if (this._autoCloseTimer)
|
||||||
|
clearTimeout(this._autoCloseTimer);
|
||||||
|
if (!enabled)
|
||||||
|
return;
|
||||||
|
const heartBeat = () => {
|
||||||
|
if (!this._playwright.allPages().length)
|
||||||
|
selfDestruct();
|
||||||
|
else
|
||||||
|
this._autoCloseTimer = setTimeout(heartBeat, 5000);
|
||||||
|
};
|
||||||
|
this._autoCloseTimer = setTimeout(heartBeat, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async highlightAll(selector: string) {
|
||||||
|
for (const recorder of await this._allRecorders())
|
||||||
|
recorder.setHighlightedSelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
async hideHighlightAll() {
|
||||||
|
await this._playwright.hideHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
allBrowsers(): Browser[] {
|
||||||
|
return [...this._playwright.allBrowsers()];
|
||||||
|
}
|
||||||
|
|
||||||
|
async kill() {
|
||||||
|
selfDestruct();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _emitSnapshot() {
|
||||||
|
const browsers = [];
|
||||||
|
for (const browser of this._playwright.allBrowsers()) {
|
||||||
|
const b = {
|
||||||
|
contexts: [] as any[]
|
||||||
|
};
|
||||||
|
browsers.push(b);
|
||||||
|
for (const context of browser.contexts()) {
|
||||||
|
const c = {
|
||||||
|
pages: [] as any[]
|
||||||
|
};
|
||||||
|
b.contexts.push(c);
|
||||||
|
for (const page of context.pages())
|
||||||
|
c.pages.push(page.mainFrame().url());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.emit(ReuseController.Events.BrowsersChanged, browsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _allRecorders(): Promise<Recorder[]> {
|
||||||
|
const contexts = new Set<BrowserContext>();
|
||||||
|
for (const page of this._playwright.allPages())
|
||||||
|
contexts.add(page.context());
|
||||||
|
const result = await Promise.all([...contexts].map(c => Recorder.show(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp(this)))));
|
||||||
|
return result.filter(Boolean) as Recorder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _closeBrowsersWithoutPages() {
|
||||||
|
for (const browser of this._playwright.allBrowsers()) {
|
||||||
|
for (const context of browser.contexts()) {
|
||||||
|
if (!context.pages().length)
|
||||||
|
await context.close(serverSideCallMetadata());
|
||||||
|
}
|
||||||
|
if (!browser.contexts())
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selfDestruct() {
|
||||||
|
// Force exit after 30 seconds.
|
||||||
|
setTimeout(() => process.exit(0), 30000);
|
||||||
|
// Meanwhile, try to gracefully close all browsers.
|
||||||
|
gracefullyCloseAll().then(() => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class InspectingRecorderApp extends EmptyRecorderApp {
|
||||||
|
private _reuseController: ReuseController;
|
||||||
|
|
||||||
|
constructor(reuseController: ReuseController) {
|
||||||
|
super();
|
||||||
|
this._reuseController = reuseController;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async setSelector(selector: string): Promise<void> {
|
||||||
|
this._reuseController.emit(ReuseController.Events.InspectRequested, selector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -218,6 +218,7 @@ export class Runner {
|
||||||
};
|
};
|
||||||
for (const [project, files] of filesByProject) {
|
for (const [project, files] of filesByProject) {
|
||||||
report.projects.push({
|
report.projects.push({
|
||||||
|
docker: process.env.PLAYWRIGHT_DOCKER,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
testDir: path.resolve(configFile, project.testDir),
|
testDir: path.resolve(configFile, project.testDir),
|
||||||
files: files
|
files: files
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue