2022-09-15 15:53:18 -07:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2022-09-20 14:32:21 -07:00
|
|
|
import type { Mode, Source } from '@recorder/recorderTypes';
|
2023-07-24 08:29:29 -07:00
|
|
|
import { gracefullyProcessExitDoNotHang } from '../utils/processLauncher';
|
2022-09-15 15:53:18 -07:00
|
|
|
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';
|
2023-03-06 18:49:14 -08:00
|
|
|
import { asLocator } from '../utils/isomorphic/locatorGenerators';
|
|
|
|
import type { Language } from '../utils/isomorphic/locatorGenerators';
|
2022-09-15 15:53:18 -07:00
|
|
|
|
|
|
|
const internalMetadata = serverSideCallMetadata();
|
|
|
|
|
2022-09-21 14:35:52 -08:00
|
|
|
export class DebugController extends SdkObject {
|
2022-09-15 15:53:18 -07:00
|
|
|
static Events = {
|
|
|
|
BrowsersChanged: 'browsersChanged',
|
2022-10-21 20:57:22 -04:00
|
|
|
StateChanged: 'stateChanged',
|
2022-09-20 14:32:21 -07:00
|
|
|
InspectRequested: 'inspectRequested',
|
2022-10-25 12:55:20 -04:00
|
|
|
SourceChanged: 'sourceChanged',
|
2022-11-10 12:15:29 -08:00
|
|
|
Paused: 'paused',
|
2022-09-15 15:53:18 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
private _autoCloseTimer: NodeJS.Timeout | undefined;
|
|
|
|
// TODO: remove in 1.27
|
|
|
|
private _autoCloseAllowed = false;
|
|
|
|
private _trackHierarchyListener: InstrumentationListener | undefined;
|
|
|
|
private _playwright: Playwright;
|
2022-10-25 12:55:20 -04:00
|
|
|
_sdkLanguage: Language = 'javascript';
|
|
|
|
_codegenId: string = 'playwright-test';
|
2022-09-15 15:53:18 -07:00
|
|
|
|
|
|
|
constructor(playwright: Playwright) {
|
2022-09-21 14:35:52 -08:00
|
|
|
super({ attribution: { isInternalPlaywright: true }, instrumentation: createInstrumentation() } as any, undefined, 'DebugController');
|
2022-09-15 15:53:18 -07:00
|
|
|
this._playwright = playwright;
|
|
|
|
}
|
|
|
|
|
2022-10-25 12:55:20 -04:00
|
|
|
initialize(codegenId: string, sdkLanguage: Language) {
|
|
|
|
this._codegenId = codegenId;
|
|
|
|
this._sdkLanguage = sdkLanguage;
|
2022-11-10 12:15:29 -08:00
|
|
|
Recorder.setAppFactory(async () => new InspectingRecorderApp(this));
|
2022-10-25 12:55:20 -04:00
|
|
|
}
|
|
|
|
|
2022-09-15 15:53:18 -07:00
|
|
|
setAutoCloseAllowed(allowed: boolean) {
|
|
|
|
this._autoCloseAllowed = allowed;
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
2022-10-21 20:57:22 -04:00
|
|
|
this.setReportStateChanged(false);
|
2022-09-15 15:53:18 -07:00
|
|
|
this.setAutoCloseAllowed(false);
|
2022-11-10 12:15:29 -08:00
|
|
|
Recorder.setAppFactory(undefined);
|
2022-09-15 15:53:18 -07:00
|
|
|
}
|
|
|
|
|
2022-10-21 20:57:22 -04:00
|
|
|
setReportStateChanged(enabled: boolean) {
|
2022-09-15 15:53:18 -07:00
|
|
|
if (enabled && !this._trackHierarchyListener) {
|
|
|
|
this._trackHierarchyListener = {
|
|
|
|
onPageOpen: () => this._emitSnapshot(),
|
|
|
|
onPageClose: () => this._emitSnapshot(),
|
|
|
|
};
|
2022-09-20 14:32:21 -07:00
|
|
|
this._playwright.instrumentation.addListener(this._trackHierarchyListener, null);
|
2022-09-15 15:53:18 -07:00
|
|
|
} else if (!enabled && this._trackHierarchyListener) {
|
2022-09-20 14:32:21 -07:00
|
|
|
this._playwright.instrumentation.removeListener(this._trackHierarchyListener);
|
2022-09-15 15:53:18 -07:00
|
|
|
this._trackHierarchyListener = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-10-24 19:19:58 -04:00
|
|
|
async navigate(url: string) {
|
2022-09-15 15:53:18 -07:00
|
|
|
for (const p of this._playwright.allPages())
|
|
|
|
await p.mainFrame().goto(internalMetadata, url);
|
|
|
|
}
|
|
|
|
|
2022-11-08 12:04:43 -08:00
|
|
|
async setRecorderMode(params: { mode: Mode, file?: string, testIdAttributeName?: string }) {
|
2022-10-25 12:55:20 -04:00
|
|
|
// TODO: |file| is only used in the legacy mode.
|
2022-09-15 15:53:18 -07:00
|
|
|
await this._closeBrowsersWithoutPages();
|
|
|
|
|
|
|
|
if (params.mode === 'none') {
|
|
|
|
for (const recorder of await this._allRecorders()) {
|
2023-10-04 22:56:42 -04:00
|
|
|
recorder.hideHighlightedSelector();
|
2022-09-15 15:53:18 -07:00
|
|
|
recorder.setMode('none');
|
|
|
|
}
|
|
|
|
this.setAutoCloseEnabled(true);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this._playwright.allBrowsers().length)
|
2022-10-24 19:19:58 -04:00
|
|
|
await this._playwright.chromium.launch(internalMetadata, { headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS });
|
2022-09-15 15:53:18 -07:00
|
|
|
// 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);
|
|
|
|
}
|
2022-11-29 11:43:47 -08:00
|
|
|
// Update test id attribute.
|
|
|
|
if (params.testIdAttributeName) {
|
|
|
|
for (const page of this._playwright.allPages())
|
|
|
|
page.context().selectors().setTestIdAttributeName(params.testIdAttributeName);
|
|
|
|
}
|
2022-09-15 15:53:18 -07:00
|
|
|
// Toggle the mode.
|
|
|
|
for (const recorder of await this._allRecorders()) {
|
2023-10-04 22:56:42 -04:00
|
|
|
recorder.hideHighlightedSelector();
|
2022-11-29 11:43:47 -08:00
|
|
|
if (params.mode === 'recording')
|
2022-10-25 12:55:20 -04:00
|
|
|
recorder.setOutput(this._codegenId, params.file);
|
2022-09-15 15:53:18 -07:00
|
|
|
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)
|
2023-07-24 08:29:29 -07:00
|
|
|
gracefullyProcessExitDoNotHang(0);
|
2022-09-15 15:53:18 -07:00
|
|
|
else
|
|
|
|
this._autoCloseTimer = setTimeout(heartBeat, 5000);
|
|
|
|
};
|
|
|
|
this._autoCloseTimer = setTimeout(heartBeat, 30000);
|
|
|
|
}
|
|
|
|
|
2022-10-24 19:19:58 -04:00
|
|
|
async highlight(selector: string) {
|
2022-09-15 15:53:18 -07:00
|
|
|
for (const recorder of await this._allRecorders())
|
2022-11-03 15:17:08 -07:00
|
|
|
recorder.setHighlightedSelector(this._sdkLanguage, selector);
|
2022-09-15 15:53:18 -07:00
|
|
|
}
|
|
|
|
|
2022-10-24 19:19:58 -04:00
|
|
|
async hideHighlight() {
|
|
|
|
// Hide all active recorder highlights.
|
|
|
|
for (const recorder of await this._allRecorders())
|
2023-10-04 22:56:42 -04:00
|
|
|
recorder.hideHighlightedSelector();
|
2022-10-24 19:19:58 -04:00
|
|
|
// Hide all locator.highlight highlights.
|
2022-09-15 15:53:18 -07:00
|
|
|
await this._playwright.hideHighlight();
|
|
|
|
}
|
|
|
|
|
|
|
|
allBrowsers(): Browser[] {
|
|
|
|
return [...this._playwright.allBrowsers()];
|
|
|
|
}
|
|
|
|
|
2022-11-10 12:15:29 -08:00
|
|
|
async resume() {
|
|
|
|
for (const recorder of await this._allRecorders())
|
|
|
|
recorder.resume();
|
|
|
|
}
|
|
|
|
|
2022-09-15 15:53:18 -07:00
|
|
|
async kill() {
|
2023-07-24 08:29:29 -07:00
|
|
|
gracefullyProcessExitDoNotHang(0);
|
2022-09-15 15:53:18 -07:00
|
|
|
}
|
|
|
|
|
2022-09-15 20:38:28 -07:00
|
|
|
async closeAllBrowsers() {
|
|
|
|
await Promise.all(this.allBrowsers().map(browser => browser.close()));
|
|
|
|
}
|
|
|
|
|
2022-09-15 15:53:18 -07:00
|
|
|
private _emitSnapshot() {
|
|
|
|
const browsers = [];
|
2022-10-21 20:57:22 -04:00
|
|
|
let pageCount = 0;
|
2022-09-15 15:53:18 -07:00
|
|
|
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());
|
2022-10-21 20:57:22 -04:00
|
|
|
pageCount += context.pages().length;
|
2022-09-15 15:53:18 -07:00
|
|
|
}
|
|
|
|
}
|
2022-10-21 20:57:22 -04:00
|
|
|
// TODO: browsers is deprecated, remove it.
|
2022-09-21 14:35:52 -08:00
|
|
|
this.emit(DebugController.Events.BrowsersChanged, browsers);
|
2022-10-21 20:57:22 -04:00
|
|
|
this.emit(DebugController.Events.StateChanged, { pageCount });
|
2022-09-15 15:53:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
private async _allRecorders(): Promise<Recorder[]> {
|
|
|
|
const contexts = new Set<BrowserContext>();
|
|
|
|
for (const page of this._playwright.allPages())
|
|
|
|
contexts.add(page.context());
|
2022-11-10 12:15:29 -08:00
|
|
|
const result = await Promise.all([...contexts].map(c => Recorder.show(c, { omitCallTracking: true })));
|
2022-09-15 15:53:18 -07:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class InspectingRecorderApp extends EmptyRecorderApp {
|
2022-09-21 14:35:52 -08:00
|
|
|
private _debugController: DebugController;
|
2022-09-15 15:53:18 -07:00
|
|
|
|
2022-09-21 14:35:52 -08:00
|
|
|
constructor(debugController: DebugController) {
|
2022-09-15 15:53:18 -07:00
|
|
|
super();
|
2022-09-21 14:35:52 -08:00
|
|
|
this._debugController = debugController;
|
2022-09-15 15:53:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
override async setSelector(selector: string): Promise<void> {
|
2022-10-25 12:55:20 -04:00
|
|
|
const locator: string = asLocator(this._debugController._sdkLanguage, selector);
|
|
|
|
this._debugController.emit(DebugController.Events.InspectRequested, { selector, locator });
|
2022-09-15 15:53:18 -07:00
|
|
|
}
|
2022-09-20 14:32:21 -07:00
|
|
|
|
|
|
|
override async setSources(sources: Source[]): Promise<void> {
|
2022-10-25 12:55:20 -04:00
|
|
|
const source = sources.find(s => s.id === this._debugController._codegenId);
|
2022-11-01 18:02:14 -07:00
|
|
|
const { text, header, footer, actions } = source || { text: '' };
|
|
|
|
this._debugController.emit(DebugController.Events.SourceChanged, { text, header, footer, actions });
|
2022-09-20 14:32:21 -07:00
|
|
|
}
|
2022-11-10 12:15:29 -08:00
|
|
|
|
|
|
|
override async setPaused(paused: boolean) {
|
|
|
|
this._debugController.emit(DebugController.Events.Paused, { paused });
|
|
|
|
}
|
2022-09-15 15:53:18 -07:00
|
|
|
}
|