mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: enable debug controller testing (#18270)
This commit is contained in:
parent
9a684d39ab
commit
d3948d1308
@ -100,7 +100,7 @@ class ProtocolHandler {
|
||||
}
|
||||
|
||||
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 }) {
|
||||
@ -112,11 +112,11 @@ class ProtocolHandler {
|
||||
}
|
||||
|
||||
async highlight(params: { selector: string }) {
|
||||
await this._controller.highlightAll(params.selector);
|
||||
await this._controller.highlight(params.selector);
|
||||
}
|
||||
|
||||
async hideHighlight() {
|
||||
await this._controller.hideHighlightAll();
|
||||
await this._controller.hideHighlight();
|
||||
}
|
||||
|
||||
async closeAllBrowsers() {
|
||||
|
||||
@ -73,10 +73,6 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||
(global as any)._playwrightInstance = this;
|
||||
}
|
||||
|
||||
async _hideHighlight() {
|
||||
await this._channel.hideHighlight();
|
||||
}
|
||||
|
||||
_setSelectors(selectors: Selectors) {
|
||||
const selectorsOwner = SelectorsOwner.from(this._initializer.selectors);
|
||||
this.selectors._removeChannel(selectorsOwner);
|
||||
|
||||
@ -316,8 +316,6 @@ scheme.PlaywrightNewRequestParams = tObject({
|
||||
scheme.PlaywrightNewRequestResult = tObject({
|
||||
request: tChannel(['APIRequestContext']),
|
||||
});
|
||||
scheme.PlaywrightHideHighlightParams = tOptional(tObject({}));
|
||||
scheme.PlaywrightHideHighlightResult = tOptional(tObject({}));
|
||||
scheme.RecorderSource = tObject({
|
||||
isRecorded: tBoolean,
|
||||
id: tString,
|
||||
@ -355,22 +353,22 @@ scheme.DebugControllerSetReportStateChangedParams = tObject({
|
||||
scheme.DebugControllerSetReportStateChangedResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerResetForReuseParams = tOptional(tObject({}));
|
||||
scheme.DebugControllerResetForReuseResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerNavigateAllParams = tObject({
|
||||
scheme.DebugControllerNavigateParams = tObject({
|
||||
url: tString,
|
||||
});
|
||||
scheme.DebugControllerNavigateAllResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerNavigateResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerSetRecorderModeParams = tObject({
|
||||
mode: tEnum(['inspecting', 'recording', 'none']),
|
||||
language: tOptional(tString),
|
||||
file: tOptional(tString),
|
||||
});
|
||||
scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerHighlightAllParams = tObject({
|
||||
scheme.DebugControllerHighlightParams = tObject({
|
||||
selector: tString,
|
||||
});
|
||||
scheme.DebugControllerHighlightAllResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerHideHighlightAllParams = tOptional(tObject({}));
|
||||
scheme.DebugControllerHideHighlightAllResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerHighlightResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerHideHighlightParams = tOptional(tObject({}));
|
||||
scheme.DebugControllerHideHighlightResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerKillParams = tOptional(tObject({}));
|
||||
scheme.DebugControllerKillResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerCloseAllBrowsersParams = tOptional(tObject({}));
|
||||
|
||||
@ -142,7 +142,6 @@ export class PlaywrightConnection {
|
||||
private _initDebugControllerMode(): DebugControllerDispatcher {
|
||||
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 DebugControllerDispatcher(this._dispatcherConnection, playwright.debugController);
|
||||
}
|
||||
@ -169,7 +168,7 @@ export class PlaywrightConnection {
|
||||
if (!browser) {
|
||||
browser = await playwright[(this._options.browserName || 'chromium') as 'chromium'].launch(serverSideCallMetadata(), {
|
||||
...this._options.launchOptions,
|
||||
headless: false,
|
||||
headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS,
|
||||
});
|
||||
browser.on(Browser.Events.Disconnected, () => {
|
||||
// Underlying browser did close for some reason - force disconnect the client.
|
||||
|
||||
@ -670,4 +670,5 @@ const defaultNewContextParamValues: channels.BrowserNewContextForReuseParams = {
|
||||
acceptDownloads: true,
|
||||
strictSelectors: false,
|
||||
serviceWorkers: 'allow',
|
||||
locale: 'en-US',
|
||||
};
|
||||
|
||||
@ -42,7 +42,6 @@ export class DebugController extends SdkObject {
|
||||
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, 'DebugController');
|
||||
@ -62,7 +61,6 @@ export class DebugController extends SdkObject {
|
||||
if (enabled && !this._trackHierarchyListener) {
|
||||
this._trackHierarchyListener = {
|
||||
onPageOpen: () => this._emitSnapshot(),
|
||||
onPageNavigated: () => this._emitSnapshot(),
|
||||
onPageClose: () => this._emitSnapshot(),
|
||||
};
|
||||
this._playwright.instrumentation.addListener(this._trackHierarchyListener, null);
|
||||
@ -80,7 +78,7 @@ export class DebugController extends SdkObject {
|
||||
await context.resetForReuse(internalMetadata, null);
|
||||
}
|
||||
|
||||
async navigateAll(url: string) {
|
||||
async navigate(url: string) {
|
||||
for (const p of this._playwright.allPages())
|
||||
await p.mainFrame().goto(internalMetadata, url);
|
||||
}
|
||||
@ -98,7 +96,7 @@ export class DebugController extends SdkObject {
|
||||
}
|
||||
|
||||
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.
|
||||
const pages = this._playwright.allPages();
|
||||
if (!pages.length) {
|
||||
@ -132,12 +130,16 @@ export class DebugController extends SdkObject {
|
||||
this._autoCloseTimer = setTimeout(heartBeat, 30000);
|
||||
}
|
||||
|
||||
async highlightAll(selector: string) {
|
||||
async highlight(selector: string) {
|
||||
for (const recorder of await this._allRecorders())
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@ -44,20 +44,20 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
|
||||
await this._object.resetForReuse();
|
||||
}
|
||||
|
||||
async navigateAll(params: channels.DebugControllerNavigateAllParams) {
|
||||
await this._object.navigateAll(params.url);
|
||||
async navigate(params: channels.DebugControllerNavigateParams) {
|
||||
await this._object.navigate(params.url);
|
||||
}
|
||||
|
||||
async setRecorderMode(params: channels.DebugControllerSetRecorderModeParams) {
|
||||
await this._object.setRecorderMode(params);
|
||||
}
|
||||
|
||||
async highlightAll(params: channels.DebugControllerHighlightAllParams) {
|
||||
await this._object.highlightAll(params.selector);
|
||||
async highlight(params: channels.DebugControllerHighlightParams) {
|
||||
await this._object.highlight(params.selector);
|
||||
}
|
||||
|
||||
async hideHighlightAll() {
|
||||
await this._object.hideHighlightAll();
|
||||
async hideHighlight() {
|
||||
await this._object.hideHighlight();
|
||||
}
|
||||
|
||||
async kill() {
|
||||
|
||||
@ -62,10 +62,6 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
||||
return { request: APIRequestContextDispatcher.from(this.parentScope(), request) };
|
||||
}
|
||||
|
||||
async hideHighlight(params: channels.PlaywrightHideHighlightParams, metadata?: channels.Metadata): Promise<channels.PlaywrightHideHighlightResult> {
|
||||
await this._object.hideHighlight();
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
// Cleanup contexts upon disconnect.
|
||||
await this._browserDispatcher?.cleanupContexts();
|
||||
|
||||
@ -462,8 +462,6 @@ export class FrameManager {
|
||||
|
||||
private _fireInternalFrameNavigation(frame: Frame, event: NavigationEvent) {
|
||||
frame.emit(Frame.Events.InternalNavigation, event);
|
||||
if (event.isPublic && !frame.parentFrame())
|
||||
frame.instrumentation.onPageNavigated(frame._page, event.url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -63,7 +63,6 @@ export interface Instrumentation {
|
||||
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onEvent(sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onPageOpen(page: Page): void;
|
||||
onPageNavigated(page: Page, url: string): void;
|
||||
onPageClose(page: Page): void;
|
||||
onBrowserOpen(browser: Browser): void;
|
||||
onBrowserClose(browser: Browser): void;
|
||||
@ -76,7 +75,6 @@ export interface InstrumentationListener {
|
||||
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onPageOpen?(page: Page): void;
|
||||
onPageNavigated?(page: Page, url: string): void;
|
||||
onPageClose?(page: Page): void;
|
||||
onBrowserOpen?(browser: Browser): void;
|
||||
onBrowserClose?(browser: Browser): void;
|
||||
|
||||
@ -192,7 +192,6 @@ export class Page extends SdkObject {
|
||||
this.pdf = delegate.pdf.bind(delegate);
|
||||
this.coverage = delegate.coverage ? delegate.coverage() : null;
|
||||
this.selectors = browserContext.selectors();
|
||||
this.instrumentation.onPageOpen(this);
|
||||
}
|
||||
|
||||
async initOpener(opener: PageDelegate | null) {
|
||||
@ -218,6 +217,7 @@ export class Page extends SdkObject {
|
||||
// corresponding Close event after it is reported on the context.
|
||||
if (this.isClosed())
|
||||
this.emit(Page.Events.Close);
|
||||
this.instrumentation.onPageOpen(this);
|
||||
}
|
||||
|
||||
initializedOrUndefined() {
|
||||
@ -261,25 +261,24 @@ export class Page extends SdkObject {
|
||||
}
|
||||
|
||||
_didClose() {
|
||||
this.instrumentation.onPageClose(this);
|
||||
this._frameManager.dispose();
|
||||
this._frameThrottler.dispose();
|
||||
assert(this._closedState !== 'closed', 'Page closed twice');
|
||||
this._closedState = 'closed';
|
||||
this.emit(Page.Events.Close);
|
||||
this._closedPromise.resolve();
|
||||
this.instrumentation.onPageClose(this);
|
||||
}
|
||||
|
||||
_didCrash() {
|
||||
this.instrumentation.onPageClose(this);
|
||||
this._frameManager.dispose();
|
||||
this._frameThrottler.dispose();
|
||||
this.emit(Page.Events.Crash);
|
||||
this._crashedPromise.resolve(new Error('Page crashed'));
|
||||
this.instrumentation.onPageClose(this);
|
||||
}
|
||||
|
||||
_didDisconnect() {
|
||||
this.instrumentation.onPageClose(this);
|
||||
this._frameManager.dispose();
|
||||
this._frameThrottler.dispose();
|
||||
assert(!this._disconnected, 'Page disconnected twice');
|
||||
|
||||
@ -519,7 +519,6 @@ export interface PlaywrightEventTarget {
|
||||
export interface PlaywrightChannel extends PlaywrightEventTarget, Channel {
|
||||
_type_Playwright: boolean;
|
||||
newRequest(params: PlaywrightNewRequestParams, metadata?: Metadata): Promise<PlaywrightNewRequestResult>;
|
||||
hideHighlight(params?: PlaywrightHideHighlightParams, metadata?: Metadata): Promise<PlaywrightHideHighlightResult>;
|
||||
}
|
||||
export type PlaywrightNewRequestParams = {
|
||||
baseURL?: string,
|
||||
@ -568,9 +567,6 @@ export type PlaywrightNewRequestOptions = {
|
||||
export type PlaywrightNewRequestResult = {
|
||||
request: APIRequestContextChannel,
|
||||
};
|
||||
export type PlaywrightHideHighlightParams = {};
|
||||
export type PlaywrightHideHighlightOptions = {};
|
||||
export type PlaywrightHideHighlightResult = void;
|
||||
|
||||
export interface PlaywrightEvents {
|
||||
}
|
||||
@ -601,10 +597,10 @@ export interface DebugControllerChannel extends DebugControllerEventTarget, Chan
|
||||
_type_DebugController: boolean;
|
||||
setReportStateChanged(params: DebugControllerSetReportStateChangedParams, metadata?: Metadata): Promise<DebugControllerSetReportStateChangedResult>;
|
||||
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>;
|
||||
highlightAll(params: DebugControllerHighlightAllParams, metadata?: Metadata): Promise<DebugControllerHighlightAllResult>;
|
||||
hideHighlightAll(params?: DebugControllerHideHighlightAllParams, metadata?: Metadata): Promise<DebugControllerHideHighlightAllResult>;
|
||||
highlight(params: DebugControllerHighlightParams, metadata?: Metadata): Promise<DebugControllerHighlightResult>;
|
||||
hideHighlight(params?: DebugControllerHideHighlightParams, metadata?: Metadata): Promise<DebugControllerHideHighlightResult>;
|
||||
kill(params?: DebugControllerKillParams, metadata?: Metadata): Promise<DebugControllerKillResult>;
|
||||
closeAllBrowsers(params?: DebugControllerCloseAllBrowsersParams, metadata?: Metadata): Promise<DebugControllerCloseAllBrowsersResult>;
|
||||
}
|
||||
@ -635,13 +631,13 @@ export type DebugControllerSetReportStateChangedResult = void;
|
||||
export type DebugControllerResetForReuseParams = {};
|
||||
export type DebugControllerResetForReuseOptions = {};
|
||||
export type DebugControllerResetForReuseResult = void;
|
||||
export type DebugControllerNavigateAllParams = {
|
||||
export type DebugControllerNavigateParams = {
|
||||
url: string,
|
||||
};
|
||||
export type DebugControllerNavigateAllOptions = {
|
||||
export type DebugControllerNavigateOptions = {
|
||||
|
||||
};
|
||||
export type DebugControllerNavigateAllResult = void;
|
||||
export type DebugControllerNavigateResult = void;
|
||||
export type DebugControllerSetRecorderModeParams = {
|
||||
mode: 'inspecting' | 'recording' | 'none',
|
||||
language?: string,
|
||||
@ -652,16 +648,16 @@ export type DebugControllerSetRecorderModeOptions = {
|
||||
file?: string,
|
||||
};
|
||||
export type DebugControllerSetRecorderModeResult = void;
|
||||
export type DebugControllerHighlightAllParams = {
|
||||
export type DebugControllerHighlightParams = {
|
||||
selector: string,
|
||||
};
|
||||
export type DebugControllerHighlightAllOptions = {
|
||||
export type DebugControllerHighlightOptions = {
|
||||
|
||||
};
|
||||
export type DebugControllerHighlightAllResult = void;
|
||||
export type DebugControllerHideHighlightAllParams = {};
|
||||
export type DebugControllerHideHighlightAllOptions = {};
|
||||
export type DebugControllerHideHighlightAllResult = void;
|
||||
export type DebugControllerHighlightResult = void;
|
||||
export type DebugControllerHideHighlightParams = {};
|
||||
export type DebugControllerHideHighlightOptions = {};
|
||||
export type DebugControllerHideHighlightResult = void;
|
||||
export type DebugControllerKillParams = {};
|
||||
export type DebugControllerKillOptions = {};
|
||||
export type DebugControllerKillResult = void;
|
||||
|
||||
@ -636,8 +636,6 @@ Playwright:
|
||||
returns:
|
||||
request: APIRequestContext
|
||||
|
||||
hideHighlight:
|
||||
|
||||
RecorderSource:
|
||||
type: object
|
||||
properties:
|
||||
@ -666,7 +664,7 @@ DebugController:
|
||||
|
||||
resetForReuse:
|
||||
|
||||
navigateAll:
|
||||
navigate:
|
||||
parameters:
|
||||
url: string
|
||||
|
||||
@ -681,11 +679,11 @@ DebugController:
|
||||
language: string?
|
||||
file: string?
|
||||
|
||||
highlightAll:
|
||||
highlight:
|
||||
parameters:
|
||||
selector: string
|
||||
|
||||
hideHighlightAll:
|
||||
hideHighlight:
|
||||
|
||||
kill:
|
||||
|
||||
|
||||
176
tests/config/debugControllerBackend.ts
Normal file
176
tests/config/debugControllerBackend.ts
Normal 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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
155
tests/library/debug-controller.spec.ts
Normal file
155
tests/library/debug-controller.spec.ts
Normal 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();
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user