chore: enable debug controller testing (#18270)

This commit is contained in:
Pavel Feldman 2022-10-24 19:19:58 -04:00 committed by GitHub
parent 9a684d39ab
commit d3948d1308
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 374 additions and 62 deletions

View File

@ -100,7 +100,7 @@ class ProtocolHandler {
} }
async navigate(params: { url: string }) { async navigate(params: { url: string }) {
await this._controller.navigateAll(params.url); await this._controller.navigate(params.url);
} }
async setMode(params: { mode: Mode, language?: string, file?: string }) { async setMode(params: { mode: Mode, language?: string, file?: string }) {
@ -112,11 +112,11 @@ class ProtocolHandler {
} }
async highlight(params: { selector: string }) { async highlight(params: { selector: string }) {
await this._controller.highlightAll(params.selector); await this._controller.highlight(params.selector);
} }
async hideHighlight() { async hideHighlight() {
await this._controller.hideHighlightAll(); await this._controller.hideHighlight();
} }
async closeAllBrowsers() { async closeAllBrowsers() {

View File

@ -73,10 +73,6 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
(global as any)._playwrightInstance = this; (global as any)._playwrightInstance = this;
} }
async _hideHighlight() {
await this._channel.hideHighlight();
}
_setSelectors(selectors: Selectors) { _setSelectors(selectors: Selectors) {
const selectorsOwner = SelectorsOwner.from(this._initializer.selectors); const selectorsOwner = SelectorsOwner.from(this._initializer.selectors);
this.selectors._removeChannel(selectorsOwner); this.selectors._removeChannel(selectorsOwner);

View File

@ -316,8 +316,6 @@ scheme.PlaywrightNewRequestParams = tObject({
scheme.PlaywrightNewRequestResult = tObject({ scheme.PlaywrightNewRequestResult = tObject({
request: tChannel(['APIRequestContext']), request: tChannel(['APIRequestContext']),
}); });
scheme.PlaywrightHideHighlightParams = tOptional(tObject({}));
scheme.PlaywrightHideHighlightResult = tOptional(tObject({}));
scheme.RecorderSource = tObject({ scheme.RecorderSource = tObject({
isRecorded: tBoolean, isRecorded: tBoolean,
id: tString, id: tString,
@ -355,22 +353,22 @@ scheme.DebugControllerSetReportStateChangedParams = tObject({
scheme.DebugControllerSetReportStateChangedResult = tOptional(tObject({})); scheme.DebugControllerSetReportStateChangedResult = tOptional(tObject({}));
scheme.DebugControllerResetForReuseParams = tOptional(tObject({})); scheme.DebugControllerResetForReuseParams = tOptional(tObject({}));
scheme.DebugControllerResetForReuseResult = tOptional(tObject({})); scheme.DebugControllerResetForReuseResult = tOptional(tObject({}));
scheme.DebugControllerNavigateAllParams = tObject({ scheme.DebugControllerNavigateParams = tObject({
url: tString, url: tString,
}); });
scheme.DebugControllerNavigateAllResult = tOptional(tObject({})); scheme.DebugControllerNavigateResult = tOptional(tObject({}));
scheme.DebugControllerSetRecorderModeParams = tObject({ scheme.DebugControllerSetRecorderModeParams = tObject({
mode: tEnum(['inspecting', 'recording', 'none']), mode: tEnum(['inspecting', 'recording', 'none']),
language: tOptional(tString), language: tOptional(tString),
file: tOptional(tString), file: tOptional(tString),
}); });
scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({})); scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({}));
scheme.DebugControllerHighlightAllParams = tObject({ scheme.DebugControllerHighlightParams = tObject({
selector: tString, selector: tString,
}); });
scheme.DebugControllerHighlightAllResult = tOptional(tObject({})); scheme.DebugControllerHighlightResult = tOptional(tObject({}));
scheme.DebugControllerHideHighlightAllParams = tOptional(tObject({})); scheme.DebugControllerHideHighlightParams = tOptional(tObject({}));
scheme.DebugControllerHideHighlightAllResult = tOptional(tObject({})); scheme.DebugControllerHideHighlightResult = tOptional(tObject({}));
scheme.DebugControllerKillParams = tOptional(tObject({})); scheme.DebugControllerKillParams = tOptional(tObject({}));
scheme.DebugControllerKillResult = tOptional(tObject({})); scheme.DebugControllerKillResult = tOptional(tObject({}));
scheme.DebugControllerCloseAllBrowsersParams = tOptional(tObject({})); scheme.DebugControllerCloseAllBrowsersParams = tOptional(tObject({}));

View File

@ -142,7 +142,6 @@ export class PlaywrightConnection {
private _initDebugControllerMode(): DebugControllerDispatcher { private _initDebugControllerMode(): DebugControllerDispatcher {
this._debugLog(`engaged reuse controller mode`); this._debugLog(`engaged reuse controller mode`);
const playwright = this._preLaunched.playwright!; const playwright = this._preLaunched.playwright!;
this._cleanups.push(() => gracefullyCloseAll());
// Always create new instance based on the reused Playwright instance. // Always create new instance based on the reused Playwright instance.
return new DebugControllerDispatcher(this._dispatcherConnection, playwright.debugController); return new DebugControllerDispatcher(this._dispatcherConnection, playwright.debugController);
} }
@ -169,7 +168,7 @@ export class PlaywrightConnection {
if (!browser) { if (!browser) {
browser = await playwright[(this._options.browserName || 'chromium') as 'chromium'].launch(serverSideCallMetadata(), { browser = await playwright[(this._options.browserName || 'chromium') as 'chromium'].launch(serverSideCallMetadata(), {
...this._options.launchOptions, ...this._options.launchOptions,
headless: false, headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS,
}); });
browser.on(Browser.Events.Disconnected, () => { browser.on(Browser.Events.Disconnected, () => {
// Underlying browser did close for some reason - force disconnect the client. // Underlying browser did close for some reason - force disconnect the client.

View File

@ -670,4 +670,5 @@ const defaultNewContextParamValues: channels.BrowserNewContextForReuseParams = {
acceptDownloads: true, acceptDownloads: true,
strictSelectors: false, strictSelectors: false,
serviceWorkers: 'allow', serviceWorkers: 'allow',
locale: 'en-US',
}; };

View File

@ -42,7 +42,6 @@ export class DebugController extends SdkObject {
private _autoCloseAllowed = false; private _autoCloseAllowed = false;
private _trackHierarchyListener: InstrumentationListener | undefined; private _trackHierarchyListener: InstrumentationListener | undefined;
private _playwright: Playwright; private _playwright: Playwright;
private _reuseBrowser = false;
constructor(playwright: Playwright) { constructor(playwright: Playwright) {
super({ attribution: { isInternalPlaywright: true }, instrumentation: createInstrumentation() } as any, undefined, 'DebugController'); super({ attribution: { isInternalPlaywright: true }, instrumentation: createInstrumentation() } as any, undefined, 'DebugController');
@ -62,7 +61,6 @@ export class DebugController extends SdkObject {
if (enabled && !this._trackHierarchyListener) { if (enabled && !this._trackHierarchyListener) {
this._trackHierarchyListener = { this._trackHierarchyListener = {
onPageOpen: () => this._emitSnapshot(), onPageOpen: () => this._emitSnapshot(),
onPageNavigated: () => this._emitSnapshot(),
onPageClose: () => this._emitSnapshot(), onPageClose: () => this._emitSnapshot(),
}; };
this._playwright.instrumentation.addListener(this._trackHierarchyListener, null); this._playwright.instrumentation.addListener(this._trackHierarchyListener, null);
@ -80,7 +78,7 @@ export class DebugController extends SdkObject {
await context.resetForReuse(internalMetadata, null); await context.resetForReuse(internalMetadata, null);
} }
async navigateAll(url: string) { async navigate(url: string) {
for (const p of this._playwright.allPages()) for (const p of this._playwright.allPages())
await p.mainFrame().goto(internalMetadata, url); await p.mainFrame().goto(internalMetadata, url);
} }
@ -98,7 +96,7 @@ export class DebugController extends SdkObject {
} }
if (!this._playwright.allBrowsers().length) if (!this._playwright.allBrowsers().length)
await this._playwright.chromium.launch(internalMetadata, { headless: false }); await this._playwright.chromium.launch(internalMetadata, { headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS });
// Create page if none. // Create page if none.
const pages = this._playwright.allPages(); const pages = this._playwright.allPages();
if (!pages.length) { if (!pages.length) {
@ -132,12 +130,16 @@ export class DebugController extends SdkObject {
this._autoCloseTimer = setTimeout(heartBeat, 30000); this._autoCloseTimer = setTimeout(heartBeat, 30000);
} }
async highlightAll(selector: string) { async highlight(selector: string) {
for (const recorder of await this._allRecorders()) for (const recorder of await this._allRecorders())
recorder.setHighlightedSelector(selector); recorder.setHighlightedSelector(selector);
} }
async hideHighlightAll() { async hideHighlight() {
// Hide all active recorder highlights.
for (const recorder of await this._allRecorders())
recorder.setHighlightedSelector('');
// Hide all locator.highlight highlights.
await this._playwright.hideHighlight(); await this._playwright.hideHighlight();
} }

View File

@ -44,20 +44,20 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
await this._object.resetForReuse(); await this._object.resetForReuse();
} }
async navigateAll(params: channels.DebugControllerNavigateAllParams) { async navigate(params: channels.DebugControllerNavigateParams) {
await this._object.navigateAll(params.url); await this._object.navigate(params.url);
} }
async setRecorderMode(params: channels.DebugControllerSetRecorderModeParams) { async setRecorderMode(params: channels.DebugControllerSetRecorderModeParams) {
await this._object.setRecorderMode(params); await this._object.setRecorderMode(params);
} }
async highlightAll(params: channels.DebugControllerHighlightAllParams) { async highlight(params: channels.DebugControllerHighlightParams) {
await this._object.highlightAll(params.selector); await this._object.highlight(params.selector);
} }
async hideHighlightAll() { async hideHighlight() {
await this._object.hideHighlightAll(); await this._object.hideHighlight();
} }
async kill() { async kill() {

View File

@ -62,10 +62,6 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
return { request: APIRequestContextDispatcher.from(this.parentScope(), request) }; return { request: APIRequestContextDispatcher.from(this.parentScope(), request) };
} }
async hideHighlight(params: channels.PlaywrightHideHighlightParams, metadata?: channels.Metadata): Promise<channels.PlaywrightHideHighlightResult> {
await this._object.hideHighlight();
}
async cleanup() { async cleanup() {
// Cleanup contexts upon disconnect. // Cleanup contexts upon disconnect.
await this._browserDispatcher?.cleanupContexts(); await this._browserDispatcher?.cleanupContexts();

View File

@ -462,8 +462,6 @@ export class FrameManager {
private _fireInternalFrameNavigation(frame: Frame, event: NavigationEvent) { private _fireInternalFrameNavigation(frame: Frame, event: NavigationEvent) {
frame.emit(Frame.Events.InternalNavigation, event); frame.emit(Frame.Events.InternalNavigation, event);
if (event.isPublic && !frame.parentFrame())
frame.instrumentation.onPageNavigated(frame._page, event.url);
} }
} }

View File

@ -63,7 +63,6 @@ export interface Instrumentation {
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>; onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onEvent(sdkObject: SdkObject, metadata: CallMetadata): void; onEvent(sdkObject: SdkObject, metadata: CallMetadata): void;
onPageOpen(page: Page): void; onPageOpen(page: Page): void;
onPageNavigated(page: Page, url: string): void;
onPageClose(page: Page): void; onPageClose(page: Page): void;
onBrowserOpen(browser: Browser): void; onBrowserOpen(browser: Browser): void;
onBrowserClose(browser: Browser): void; onBrowserClose(browser: Browser): void;
@ -76,7 +75,6 @@ export interface InstrumentationListener {
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>; onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void; onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void;
onPageOpen?(page: Page): void; onPageOpen?(page: Page): void;
onPageNavigated?(page: Page, url: string): void;
onPageClose?(page: Page): void; onPageClose?(page: Page): void;
onBrowserOpen?(browser: Browser): void; onBrowserOpen?(browser: Browser): void;
onBrowserClose?(browser: Browser): void; onBrowserClose?(browser: Browser): void;

View File

@ -192,7 +192,6 @@ export class Page extends SdkObject {
this.pdf = delegate.pdf.bind(delegate); this.pdf = delegate.pdf.bind(delegate);
this.coverage = delegate.coverage ? delegate.coverage() : null; this.coverage = delegate.coverage ? delegate.coverage() : null;
this.selectors = browserContext.selectors(); this.selectors = browserContext.selectors();
this.instrumentation.onPageOpen(this);
} }
async initOpener(opener: PageDelegate | null) { async initOpener(opener: PageDelegate | null) {
@ -218,6 +217,7 @@ export class Page extends SdkObject {
// corresponding Close event after it is reported on the context. // corresponding Close event after it is reported on the context.
if (this.isClosed()) if (this.isClosed())
this.emit(Page.Events.Close); this.emit(Page.Events.Close);
this.instrumentation.onPageOpen(this);
} }
initializedOrUndefined() { initializedOrUndefined() {
@ -261,25 +261,24 @@ export class Page extends SdkObject {
} }
_didClose() { _didClose() {
this.instrumentation.onPageClose(this);
this._frameManager.dispose(); this._frameManager.dispose();
this._frameThrottler.dispose(); this._frameThrottler.dispose();
assert(this._closedState !== 'closed', 'Page closed twice'); assert(this._closedState !== 'closed', 'Page closed twice');
this._closedState = 'closed'; this._closedState = 'closed';
this.emit(Page.Events.Close); this.emit(Page.Events.Close);
this._closedPromise.resolve(); this._closedPromise.resolve();
this.instrumentation.onPageClose(this);
} }
_didCrash() { _didCrash() {
this.instrumentation.onPageClose(this);
this._frameManager.dispose(); this._frameManager.dispose();
this._frameThrottler.dispose(); this._frameThrottler.dispose();
this.emit(Page.Events.Crash); this.emit(Page.Events.Crash);
this._crashedPromise.resolve(new Error('Page crashed')); this._crashedPromise.resolve(new Error('Page crashed'));
this.instrumentation.onPageClose(this);
} }
_didDisconnect() { _didDisconnect() {
this.instrumentation.onPageClose(this);
this._frameManager.dispose(); this._frameManager.dispose();
this._frameThrottler.dispose(); this._frameThrottler.dispose();
assert(!this._disconnected, 'Page disconnected twice'); assert(!this._disconnected, 'Page disconnected twice');

View File

@ -519,7 +519,6 @@ export interface PlaywrightEventTarget {
export interface PlaywrightChannel extends PlaywrightEventTarget, Channel { export interface PlaywrightChannel extends PlaywrightEventTarget, Channel {
_type_Playwright: boolean; _type_Playwright: boolean;
newRequest(params: PlaywrightNewRequestParams, metadata?: Metadata): Promise<PlaywrightNewRequestResult>; newRequest(params: PlaywrightNewRequestParams, metadata?: Metadata): Promise<PlaywrightNewRequestResult>;
hideHighlight(params?: PlaywrightHideHighlightParams, metadata?: Metadata): Promise<PlaywrightHideHighlightResult>;
} }
export type PlaywrightNewRequestParams = { export type PlaywrightNewRequestParams = {
baseURL?: string, baseURL?: string,
@ -568,9 +567,6 @@ export type PlaywrightNewRequestOptions = {
export type PlaywrightNewRequestResult = { export type PlaywrightNewRequestResult = {
request: APIRequestContextChannel, request: APIRequestContextChannel,
}; };
export type PlaywrightHideHighlightParams = {};
export type PlaywrightHideHighlightOptions = {};
export type PlaywrightHideHighlightResult = void;
export interface PlaywrightEvents { export interface PlaywrightEvents {
} }
@ -601,10 +597,10 @@ export interface DebugControllerChannel extends DebugControllerEventTarget, Chan
_type_DebugController: boolean; _type_DebugController: boolean;
setReportStateChanged(params: DebugControllerSetReportStateChangedParams, metadata?: Metadata): Promise<DebugControllerSetReportStateChangedResult>; setReportStateChanged(params: DebugControllerSetReportStateChangedParams, metadata?: Metadata): Promise<DebugControllerSetReportStateChangedResult>;
resetForReuse(params?: DebugControllerResetForReuseParams, metadata?: Metadata): Promise<DebugControllerResetForReuseResult>; resetForReuse(params?: DebugControllerResetForReuseParams, metadata?: Metadata): Promise<DebugControllerResetForReuseResult>;
navigateAll(params: DebugControllerNavigateAllParams, metadata?: Metadata): Promise<DebugControllerNavigateAllResult>; navigate(params: DebugControllerNavigateParams, metadata?: Metadata): Promise<DebugControllerNavigateResult>;
setRecorderMode(params: DebugControllerSetRecorderModeParams, metadata?: Metadata): Promise<DebugControllerSetRecorderModeResult>; setRecorderMode(params: DebugControllerSetRecorderModeParams, metadata?: Metadata): Promise<DebugControllerSetRecorderModeResult>;
highlightAll(params: DebugControllerHighlightAllParams, metadata?: Metadata): Promise<DebugControllerHighlightAllResult>; highlight(params: DebugControllerHighlightParams, metadata?: Metadata): Promise<DebugControllerHighlightResult>;
hideHighlightAll(params?: DebugControllerHideHighlightAllParams, metadata?: Metadata): Promise<DebugControllerHideHighlightAllResult>; hideHighlight(params?: DebugControllerHideHighlightParams, metadata?: Metadata): Promise<DebugControllerHideHighlightResult>;
kill(params?: DebugControllerKillParams, metadata?: Metadata): Promise<DebugControllerKillResult>; kill(params?: DebugControllerKillParams, metadata?: Metadata): Promise<DebugControllerKillResult>;
closeAllBrowsers(params?: DebugControllerCloseAllBrowsersParams, metadata?: Metadata): Promise<DebugControllerCloseAllBrowsersResult>; closeAllBrowsers(params?: DebugControllerCloseAllBrowsersParams, metadata?: Metadata): Promise<DebugControllerCloseAllBrowsersResult>;
} }
@ -635,13 +631,13 @@ export type DebugControllerSetReportStateChangedResult = void;
export type DebugControllerResetForReuseParams = {}; export type DebugControllerResetForReuseParams = {};
export type DebugControllerResetForReuseOptions = {}; export type DebugControllerResetForReuseOptions = {};
export type DebugControllerResetForReuseResult = void; export type DebugControllerResetForReuseResult = void;
export type DebugControllerNavigateAllParams = { export type DebugControllerNavigateParams = {
url: string, url: string,
}; };
export type DebugControllerNavigateAllOptions = { export type DebugControllerNavigateOptions = {
}; };
export type DebugControllerNavigateAllResult = void; export type DebugControllerNavigateResult = void;
export type DebugControllerSetRecorderModeParams = { export type DebugControllerSetRecorderModeParams = {
mode: 'inspecting' | 'recording' | 'none', mode: 'inspecting' | 'recording' | 'none',
language?: string, language?: string,
@ -652,16 +648,16 @@ export type DebugControllerSetRecorderModeOptions = {
file?: string, file?: string,
}; };
export type DebugControllerSetRecorderModeResult = void; export type DebugControllerSetRecorderModeResult = void;
export type DebugControllerHighlightAllParams = { export type DebugControllerHighlightParams = {
selector: string, selector: string,
}; };
export type DebugControllerHighlightAllOptions = { export type DebugControllerHighlightOptions = {
}; };
export type DebugControllerHighlightAllResult = void; export type DebugControllerHighlightResult = void;
export type DebugControllerHideHighlightAllParams = {}; export type DebugControllerHideHighlightParams = {};
export type DebugControllerHideHighlightAllOptions = {}; export type DebugControllerHideHighlightOptions = {};
export type DebugControllerHideHighlightAllResult = void; export type DebugControllerHideHighlightResult = void;
export type DebugControllerKillParams = {}; export type DebugControllerKillParams = {};
export type DebugControllerKillOptions = {}; export type DebugControllerKillOptions = {};
export type DebugControllerKillResult = void; export type DebugControllerKillResult = void;

View File

@ -636,8 +636,6 @@ Playwright:
returns: returns:
request: APIRequestContext request: APIRequestContext
hideHighlight:
RecorderSource: RecorderSource:
type: object type: object
properties: properties:
@ -666,7 +664,7 @@ DebugController:
resetForReuse: resetForReuse:
navigateAll: navigate:
parameters: parameters:
url: string url: string
@ -681,11 +679,11 @@ DebugController:
language: string? language: string?
file: string? file: string?
highlightAll: highlight:
parameters: parameters:
selector: string selector: string
hideHighlightAll: hideHighlight:
kill: kill:

View File

@ -0,0 +1,176 @@
/**
* 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 WebSocket from 'ws';
import { EventEmitter } from 'events';
export type ProtocolRequest = {
id: number;
method: string;
params: any;
};
export type ProtocolResponse = {
id?: number;
method?: string;
error?: { message: string; data: any; };
params?: any;
result?: any;
};
export interface ConnectionTransport {
send(s: ProtocolRequest): void;
close(): void; // Note: calling close is expected to issue onclose at some point.
isClosed(): boolean,
onmessage?: (message: ProtocolResponse) => void,
onclose?: () => void,
}
class WebSocketTransport implements ConnectionTransport {
private _ws: WebSocket;
onmessage?: (message: ProtocolResponse) => void;
onclose?: () => void;
readonly wsEndpoint: string;
static async connect(url: string, headers: Record<string, string> = {}): Promise<WebSocketTransport> {
const transport = new WebSocketTransport(url, headers);
await new Promise<WebSocketTransport>((fulfill, reject) => {
transport._ws.addEventListener('open', async () => {
fulfill(transport);
});
transport._ws.addEventListener('error', event => {
reject(new Error('WebSocket error: ' + event.message));
transport._ws.close();
});
});
return transport;
}
constructor(url: string, headers: Record<string, string> = {}) {
this.wsEndpoint = url;
this._ws = new WebSocket(url, [], {
perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb,
handshakeTimeout: 30000,
headers
});
this._ws.addEventListener('message', event => {
try {
if (this.onmessage)
this.onmessage.call(null, JSON.parse(event.data.toString()));
} catch (e) {
this._ws.close();
}
});
this._ws.addEventListener('close', event => {
if (this.onclose)
this.onclose.call(null);
});
// Prevent Error: read ECONNRESET.
this._ws.addEventListener('error', () => {});
}
isClosed() {
return this._ws.readyState === WebSocket.CLOSING || this._ws.readyState === WebSocket.CLOSED;
}
send(message: ProtocolRequest) {
this._ws.send(JSON.stringify(message));
}
close() {
this._ws.close();
}
async closeAndWait() {
const promise = new Promise(f => this._ws.once('close', f));
this.close();
await promise; // Make sure to await the actual disconnect.
}
}
export class Backend extends EventEmitter {
private static _lastId = 0;
private _callbacks = new Map<number, { fulfill: (a: any) => void, reject: (e: Error) => void }>();
private _transport!: WebSocketTransport;
constructor() {
super();
}
async connect(wsEndpoint: string) {
this._transport = await WebSocketTransport.connect(wsEndpoint, {
'x-playwright-debug-controller': 'true'
});
this._transport.onmessage = (message: any) => {
if (!message.id) {
this.emit(message.method, message.params);
return;
}
const pair = this._callbacks.get(message.id);
if (!pair)
return;
this._callbacks.delete(message.id);
if (message.error) {
const error = new Error(message.error.error?.message || message.error.value);
error.stack = message.error.error?.stack;
pair.reject(error);
} else {
pair.fulfill(message.result);
}
};
}
async resetForReuse() {
await this._send('resetForReuse');
}
async navigate(params: { url: string }) {
await this._send('navigate', params);
}
async setMode(params: { mode: 'none' | 'inspecting' | 'recording', language?: string, file?: string }) {
await this._send('setRecorderMode', params);
}
async setReportStateChanged(params: { enabled: boolean }) {
await this._send('setReportStateChanged', params);
}
async highlight(params: { selector: string }) {
await this._send('highlight', params);
}
async hideHighlight() {
await this._send('hideHighlight');
}
async kill() {
this._send('kill');
}
private _send(method: string, params: any = {}): Promise<any> {
return new Promise((fulfill, reject) => {
const id = ++Backend._lastId;
const command = { id, guid: 'DebugController', method, params, metadata: {} };
this._transport.send(command as any);
this._callbacks.set(id, { fulfill, reject });
});
}
}

View File

@ -0,0 +1,155 @@
/**
* 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 { expect, playwrightTest as baseTest } from '../config/browserTest';
import { PlaywrightServer } from '../../packages/playwright-core/lib/remote/playwrightServer';
import { createGuid } from '../../packages/playwright-core/lib/utils';
import { Backend } from '../config/debugControllerBackend';
import type { Browser, BrowserContext } from '@playwright/test';
type Fixtures = {
wsEndpoint: string;
backend: Backend;
connectedBrowser: Browser & { _newContextForReuse: () => Promise<BrowserContext> };
};
const test = baseTest.extend<Fixtures>({
wsEndpoint: async ({ }, use) => {
process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1';
const server = new PlaywrightServer({ path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false });
const wsEndpoint = await server.listen();
await use(wsEndpoint);
await server.close();
},
backend: async ({ wsEndpoint }, use) => {
const backend = new Backend();
await backend.connect(wsEndpoint);
await use(backend);
},
connectedBrowser: async ({ playwright, wsEndpoint }, use) => {
(playwright.chromium as any)._defaultConnectOptions = {
wsEndpoint,
headers: { 'x-playwright-reuse-context': '1', },
};
const browser = await playwright.chromium.launch();
await use(browser as any);
await browser.close();
},
});
test.slow(true, 'All controller tests are slow');
test('should pick element', async ({ backend, connectedBrowser }) => {
const events = [];
backend.on('inspectRequested', event => events.push(event));
await backend.setMode({ mode: 'inspecting' });
const context = await connectedBrowser._newContextForReuse();
const [page] = context.pages();
await page.locator('body').click();
await page.locator('body').click();
expect(events).toEqual([
{
selector: 'body',
locators: [
{ name: 'javascript', value: 'locator(\'body\')' },
{ name: 'python', value: 'locator("body")' },
{ name: 'java', value: 'locator("body")' },
{ name: 'csharp', value: 'Locator("body")' }
]
}, {
selector: 'body',
locators: [
{ name: 'javascript', value: 'locator(\'body\')' },
{ name: 'python', value: 'locator("body")' },
{ name: 'java', value: 'locator("body")' },
{ name: 'csharp', value: 'Locator("body")' }
]
},
]);
// No events after mode disabled
await backend.setMode({ mode: 'none' });
await page.locator('body').click();
expect(events).toHaveLength(2);
});
test('should report pages', async ({ backend, connectedBrowser }) => {
const events = [];
backend.on('stateChanged', event => events.push(event));
await backend.setReportStateChanged({ enabled: true });
const context = await connectedBrowser._newContextForReuse();
const page1 = await context.newPage();
const page2 = await context.newPage();
await page1.close();
await page2.close();
await backend.setReportStateChanged({ enabled: false });
const page3 = await context.newPage();
await page3.close();
expect(events).toEqual([
{
pageCount: 1,
}, {
pageCount: 2,
}, {
pageCount: 1,
}, {
pageCount: 0,
}
]);
});
test('should navigate all', async ({ backend, connectedBrowser }) => {
const context = await connectedBrowser._newContextForReuse();
const page1 = await context.newPage();
const page2 = await context.newPage();
await backend.navigate({ url: 'data:text/plain,Hello world' });
expect(await page1.evaluate(() => window.location.href)).toBe('data:text/plain,Hello world');
expect(await page2.evaluate(() => window.location.href)).toBe('data:text/plain,Hello world');
});
test('should reset for reuse', async ({ backend, connectedBrowser }) => {
const context = await connectedBrowser._newContextForReuse();
const page1 = await context.newPage();
const page2 = await context.newPage();
await backend.navigate({ url: 'data:text/plain,Hello world' });
const context2 = await connectedBrowser._newContextForReuse();
expect(await context2.pages()[0].evaluate(() => window.location.href)).toBe('about:blank');
expect(await page1.evaluate(() => window.location.href)).toBe('about:blank');
expect(await page2.evaluate(() => window.location.href).catch(e => e.message)).toContain('Target page, context or browser has been closed');
});
test('should highlight all', async ({ backend, connectedBrowser }) => {
const context = await connectedBrowser._newContextForReuse();
const page1 = await context.newPage();
const page2 = await context.newPage();
await backend.navigate({ url: 'data:text/html,<button>Submit</button>' });
await backend.highlight({ selector: 'button' });
await expect(page1.getByText('locator(\'button\')')).toBeVisible();
await expect(page2.getByText('locator(\'button\')')).toBeVisible();
await backend.hideHighlight();
await expect(page1.getByText('locator(\'button\')')).toBeHidden({ timeout: 1000000 });
await expect(page2.getByText('locator(\'button\')')).toBeHidden();
});