mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-15 06:02:57 +03:00
chore: extract debugger model from inspector (#6261)
This commit is contained in:
parent
34e03fc77d
commit
fe4fba4a16
@ -144,7 +144,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||
}
|
||||
|
||||
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
|
||||
await RecorderSupplement.getOrCreate(this._context, params);
|
||||
await RecorderSupplement.show(this._context, params);
|
||||
}
|
||||
|
||||
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
||||
|
@ -28,6 +28,7 @@ import { InspectorController } from './supplements/inspectorController';
|
||||
import { WebKit } from './webkit/webkit';
|
||||
import { Registry } from '../utils/registry';
|
||||
import { InstrumentationListener, multiplexInstrumentation, SdkObject } from './instrumentation';
|
||||
import { Debugger } from './supplements/debugger';
|
||||
|
||||
export class Playwright extends SdkObject {
|
||||
readonly selectors: Selectors;
|
||||
@ -41,6 +42,7 @@ export class Playwright extends SdkObject {
|
||||
constructor(isInternal: boolean) {
|
||||
const listeners: InstrumentationListener[] = [];
|
||||
if (!isInternal) {
|
||||
listeners.push(new Debugger());
|
||||
listeners.push(new Tracer());
|
||||
listeners.push(new HarTracer());
|
||||
listeners.push(new InspectorController());
|
||||
|
129
src/server/supplements/debugger.ts
Normal file
129
src/server/supplements/debugger.ts
Normal file
@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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 { EventEmitter } from 'events';
|
||||
import { debugMode, isUnderTest, monotonicTime } from '../../utils/utils';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
|
||||
import * as consoleApiSource from '../../generated/consoleApiSource';
|
||||
|
||||
export class Debugger implements InstrumentationListener {
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
ContextDebugger.getOrCreate(context);
|
||||
if (debugMode() === 'console')
|
||||
await context.extendInjectedScript(consoleApiSource.source);
|
||||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
await ContextDebugger.lookup(sdkObject.attribution.context!)?.onBeforeCall(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
await ContextDebugger.lookup(sdkObject.attribution.context!)?.onBeforeInputAction(sdkObject, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
const symbol = Symbol('ContextDebugger');
|
||||
|
||||
export class ContextDebugger extends EventEmitter {
|
||||
private _pauseOnNextStatement = false;
|
||||
private _pausedCallsMetadata = new Map<CallMetadata, { resolve: () => void, sdkObject: SdkObject }>();
|
||||
private _enabled: boolean;
|
||||
|
||||
static Events = {
|
||||
PausedStateChanged: 'pausedstatechanged'
|
||||
};
|
||||
|
||||
static getOrCreate(context: BrowserContext): ContextDebugger {
|
||||
let contextDebugger = (context as any)[symbol] as ContextDebugger;
|
||||
if (!contextDebugger) {
|
||||
contextDebugger = new ContextDebugger();
|
||||
(context as any)[symbol] = contextDebugger;
|
||||
}
|
||||
return contextDebugger;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._enabled = debugMode() === 'inspector';
|
||||
if (this._enabled)
|
||||
this.pauseOnNextStatement();
|
||||
}
|
||||
|
||||
static lookup(context?: BrowserContext): ContextDebugger | undefined {
|
||||
if (!context)
|
||||
return;
|
||||
return (context as any)[symbol] as ContextDebugger | undefined;
|
||||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseOnStep(sdkObject, metadata)))
|
||||
await this.pause(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
if (this._enabled && this._pauseOnNextStatement)
|
||||
await this.pause(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async pause(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
this._enabled = true;
|
||||
metadata.pauseStartTime = monotonicTime();
|
||||
const result = new Promise<void>(resolve => {
|
||||
this._pausedCallsMetadata.set(metadata, { resolve, sdkObject });
|
||||
});
|
||||
this.emit(ContextDebugger.Events.PausedStateChanged);
|
||||
return result;
|
||||
}
|
||||
|
||||
resume(step: boolean) {
|
||||
this._pauseOnNextStatement = step;
|
||||
const endTime = monotonicTime();
|
||||
for (const [metadata, { resolve }] of this._pausedCallsMetadata) {
|
||||
metadata.pauseEndTime = endTime;
|
||||
resolve();
|
||||
}
|
||||
this._pausedCallsMetadata.clear();
|
||||
this.emit(ContextDebugger.Events.PausedStateChanged);
|
||||
}
|
||||
|
||||
pauseOnNextStatement() {
|
||||
this._pauseOnNextStatement = true;
|
||||
}
|
||||
|
||||
isPaused(metadata?: CallMetadata): boolean {
|
||||
if (metadata)
|
||||
return this._pausedCallsMetadata.has(metadata);
|
||||
return !!this._pausedCallsMetadata.size;
|
||||
}
|
||||
|
||||
pausedDetails(): { metadata: CallMetadata, sdkObject: SdkObject }[] {
|
||||
const result: { metadata: CallMetadata, sdkObject: SdkObject }[] = [];
|
||||
for (const [metadata, { sdkObject }] of this._pausedCallsMetadata)
|
||||
result.push({ metadata, sdkObject });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
||||
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
|
||||
return false;
|
||||
return metadata.method === 'pause';
|
||||
}
|
||||
|
||||
function shouldPauseOnStep(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
||||
return metadata.method === 'goto' || metadata.method === 'close';
|
||||
}
|
@ -18,54 +18,36 @@ import { BrowserContext } from '../browserContext';
|
||||
import { RecorderSupplement } from './recorderSupplement';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
|
||||
import { debugMode, isUnderTest } from '../../utils/utils';
|
||||
import * as consoleApiSource from '../../generated/consoleApiSource';
|
||||
import { ContextDebugger } from './debugger';
|
||||
|
||||
export class InspectorController implements InstrumentationListener {
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
if (debugMode() === 'inspector')
|
||||
await RecorderSupplement.getOrCreate(context, { pauseOnNextStatement: true });
|
||||
else if (debugMode() === 'console')
|
||||
await context.extendInjectedScript(consoleApiSource.source);
|
||||
const contextDebugger = ContextDebugger.lookup(context)!;
|
||||
if (contextDebugger.isPaused())
|
||||
RecorderSupplement.show(context, {}).catch(() => {});
|
||||
contextDebugger.on(ContextDebugger.Events.PausedStateChanged, () => {
|
||||
RecorderSupplement.show(context, {}).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
const context = sdkObject.attribution.context;
|
||||
if (!context)
|
||||
return;
|
||||
|
||||
if (shouldOpenInspector(sdkObject, metadata))
|
||||
await RecorderSupplement.getOrCreate(context, { pauseOnNextStatement: true });
|
||||
|
||||
const recorder = await RecorderSupplement.getNoCreate(context);
|
||||
await recorder?.onBeforeCall(sdkObject, metadata);
|
||||
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
|
||||
recorder?.onBeforeCall(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
if (!sdkObject.attribution.context)
|
||||
return;
|
||||
const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
|
||||
await recorder?.onAfterCall(sdkObject, metadata);
|
||||
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
|
||||
recorder?.onAfterCall(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
if (!sdkObject.attribution.context)
|
||||
return;
|
||||
const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
|
||||
await recorder?.onBeforeInputAction(sdkObject, metadata);
|
||||
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
|
||||
recorder?.onBeforeInputAction(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
debugLogger.log(logName as any, message);
|
||||
if (!sdkObject.attribution.context)
|
||||
return;
|
||||
const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
|
||||
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
|
||||
recorder?.updateCallLog([metadata]);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldOpenInspector(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
||||
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
|
||||
return false;
|
||||
return metadata.method === 'pause';
|
||||
}
|
||||
|
@ -32,9 +32,10 @@ import { RecorderApp } from './recorder/recorderApp';
|
||||
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
|
||||
import { Point } from '../../common/types';
|
||||
import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
|
||||
import { isUnderTest, monotonicTime } from '../../utils/utils';
|
||||
import { isUnderTest } from '../../utils/utils';
|
||||
import { InMemorySnapshotter } from '../snapshot/inMemorySnapshotter';
|
||||
import { metadataToCallLog } from './recorder/recorderUtils';
|
||||
import { ContextDebugger } from './debugger';
|
||||
|
||||
type BindingSource = { frame: Frame, page: Page };
|
||||
|
||||
@ -52,16 +53,15 @@ export class RecorderSupplement {
|
||||
private _recorderApp: RecorderApp | null = null;
|
||||
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
||||
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
||||
private _pausedCallsMetadata = new Map<CallMetadata, () => void>();
|
||||
private _pauseOnNextStatement: boolean;
|
||||
private _recorderSources: Source[];
|
||||
private _userSources = new Map<string, Source>();
|
||||
private _snapshotter: InMemorySnapshotter;
|
||||
private _hoveredSnapshot: { callLogId: number, phase: 'before' | 'after' | 'action' } | undefined;
|
||||
private _snapshots = new Set<string>();
|
||||
private _allMetadatas = new Map<number, CallMetadata>();
|
||||
private _contextDebugger: ContextDebugger;
|
||||
|
||||
static getOrCreate(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<RecorderSupplement> {
|
||||
static show(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<RecorderSupplement> {
|
||||
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
|
||||
if (!recorderPromise) {
|
||||
const recorder = new RecorderSupplement(context, params);
|
||||
@ -71,15 +71,17 @@ export class RecorderSupplement {
|
||||
return recorderPromise;
|
||||
}
|
||||
|
||||
static getNoCreate(context: BrowserContext): Promise<RecorderSupplement> | undefined {
|
||||
static lookup(context: BrowserContext | undefined): Promise<RecorderSupplement> | undefined {
|
||||
if (!context)
|
||||
return;
|
||||
return (context as any)[symbol] as Promise<RecorderSupplement> | undefined;
|
||||
}
|
||||
|
||||
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
|
||||
this._context = context;
|
||||
this._contextDebugger = ContextDebugger.getOrCreate(context);
|
||||
this._params = params;
|
||||
this._mode = params.startRecording ? 'recording' : 'none';
|
||||
this._pauseOnNextStatement = !!params.pauseOnNextStatement;
|
||||
const language = params.language || context._options.sdkLanguage;
|
||||
|
||||
const languages = new Set([
|
||||
@ -150,21 +152,21 @@ export class RecorderSupplement {
|
||||
}
|
||||
if (data.event === 'callLogHovered') {
|
||||
this._hoveredSnapshot = undefined;
|
||||
if (this._isPaused() && data.params.callLogId)
|
||||
if (this._contextDebugger.isPaused() && data.params.callLogId)
|
||||
this._hoveredSnapshot = data.params;
|
||||
this._refreshOverlay();
|
||||
return;
|
||||
}
|
||||
if (data.event === 'step') {
|
||||
this._resume(true);
|
||||
this._contextDebugger.resume(true);
|
||||
return;
|
||||
}
|
||||
if (data.event === 'resume') {
|
||||
this._resume(false);
|
||||
this._contextDebugger.resume(false);
|
||||
return;
|
||||
}
|
||||
if (data.event === 'pause') {
|
||||
this._pauseOnNextStatement = true;
|
||||
this._contextDebugger.pauseOnNextStatement();
|
||||
return;
|
||||
}
|
||||
if (data.event === 'clear') {
|
||||
@ -175,7 +177,7 @@ export class RecorderSupplement {
|
||||
|
||||
await Promise.all([
|
||||
recorderApp.setMode(this._mode),
|
||||
recorderApp.setPaused(!!this._pausedCallsMetadata.size),
|
||||
recorderApp.setPaused(this._contextDebugger.isPaused()),
|
||||
this._pushAllSources()
|
||||
]);
|
||||
|
||||
@ -231,28 +233,29 @@ export class RecorderSupplement {
|
||||
});
|
||||
|
||||
await this._context.exposeBinding('_playwrightResume', false, () => {
|
||||
this._resume(false).catch(() => {});
|
||||
this._contextDebugger.resume(false);
|
||||
});
|
||||
|
||||
const snapshotBaseUrl = await this._snapshotter.initialize() + '/snapshot/';
|
||||
await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest(), snapshotBaseUrl });
|
||||
await this._context.extendInjectedScript(consoleApiSource.source);
|
||||
|
||||
if (this._contextDebugger.isPaused())
|
||||
this._pausedStateChanged();
|
||||
this._contextDebugger.on(ContextDebugger.Events.PausedStateChanged, () => this._pausedStateChanged());
|
||||
|
||||
(this._context as any).recorderAppForTest = recorderApp;
|
||||
}
|
||||
|
||||
async pause(metadata: CallMetadata) {
|
||||
const result = new Promise<void>(f => {
|
||||
this._pausedCallsMetadata.set(metadata, f);
|
||||
});
|
||||
this._recorderApp!.setPaused(true);
|
||||
metadata.pauseStartTime = monotonicTime();
|
||||
_pausedStateChanged() {
|
||||
// If we are called upon page.pause, we don't have metadatas, populate them.
|
||||
for (const { metadata, sdkObject } of this._contextDebugger.pausedDetails()) {
|
||||
if (!this._currentCallsMetadata.has(metadata))
|
||||
this.onBeforeCall(sdkObject, metadata);
|
||||
}
|
||||
this._recorderApp!.setPaused(this._contextDebugger.isPaused());
|
||||
this._updateUserSources();
|
||||
this.updateCallLog([metadata]);
|
||||
return result;
|
||||
}
|
||||
|
||||
_isPaused(): boolean {
|
||||
return !!this._pausedCallsMetadata.size;
|
||||
this.updateCallLog([...this._currentCallsMetadata.keys()]);
|
||||
}
|
||||
|
||||
private _setMode(mode: Mode) {
|
||||
@ -263,21 +266,6 @@ export class RecorderSupplement {
|
||||
this._context.pages()[0].bringToFront().catch(() => {});
|
||||
}
|
||||
|
||||
private async _resume(step: boolean) {
|
||||
this._pauseOnNextStatement = step;
|
||||
this._recorderApp?.setPaused(false);
|
||||
|
||||
const endTime = monotonicTime();
|
||||
for (const [metadata, callback] of this._pausedCallsMetadata) {
|
||||
metadata.pauseEndTime = endTime;
|
||||
callback();
|
||||
}
|
||||
this._pausedCallsMetadata.clear();
|
||||
|
||||
this._updateUserSources();
|
||||
this.updateCallLog([...this._currentCallsMetadata.keys()]);
|
||||
}
|
||||
|
||||
private _refreshOverlay() {
|
||||
for (const page of this._context.pages())
|
||||
page.mainFrame().evaluateExpression('window._playwrightRefreshOverlay()', false, undefined, 'main').catch(() => {});
|
||||
@ -410,7 +398,7 @@ export class RecorderSupplement {
|
||||
}
|
||||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._mode === 'recording')
|
||||
return;
|
||||
this._captureSnapshot(sdkObject, metadata, 'before');
|
||||
@ -418,21 +406,18 @@ export class RecorderSupplement {
|
||||
this._allMetadatas.set(metadata.id, metadata);
|
||||
this._updateUserSources();
|
||||
this.updateCallLog([metadata]);
|
||||
if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseOnStep(sdkObject, metadata)))
|
||||
await this.pause(metadata);
|
||||
if (metadata.params && metadata.params.selector) {
|
||||
this._highlightedSelector = metadata.params.selector;
|
||||
await this._recorderApp?.setSelector(this._highlightedSelector);
|
||||
this._recorderApp?.setSelector(this._highlightedSelector).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._mode === 'recording')
|
||||
return;
|
||||
this._captureSnapshot(sdkObject, metadata, 'after');
|
||||
if (!metadata.error)
|
||||
this._currentCallsMetadata.delete(metadata);
|
||||
this._pausedCallsMetadata.delete(metadata);
|
||||
this._updateUserSources();
|
||||
this.updateCallLog([metadata]);
|
||||
}
|
||||
@ -456,7 +441,7 @@ export class RecorderSupplement {
|
||||
this._userSources.set(file, source);
|
||||
}
|
||||
if (line) {
|
||||
const paused = this._pausedCallsMetadata.has(metadata);
|
||||
const paused = this._contextDebugger.isPaused(metadata);
|
||||
source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
|
||||
source.revealLine = line;
|
||||
fileToSelect = source.file;
|
||||
@ -471,12 +456,10 @@ export class RecorderSupplement {
|
||||
this._recorderApp?.setSources([...this._recorderSources, ...this._userSources.values()]);
|
||||
}
|
||||
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._mode === 'recording')
|
||||
return;
|
||||
this._captureSnapshot(sdkObject, metadata, 'action');
|
||||
if (this._pauseOnNextStatement)
|
||||
await this.pause(metadata);
|
||||
}
|
||||
|
||||
updateCallLog(metadatas: CallMetadata[]) {
|
||||
@ -489,7 +472,7 @@ export class RecorderSupplement {
|
||||
let status: CallLogStatus = 'done';
|
||||
if (this._currentCallsMetadata.has(metadata))
|
||||
status = 'in-progress';
|
||||
if (this._pausedCallsMetadata.has(metadata))
|
||||
if (this._contextDebugger.isPaused(metadata))
|
||||
status = 'paused';
|
||||
logs.push(metadataToCallLog(metadata, status, this._snapshots));
|
||||
}
|
||||
@ -514,13 +497,3 @@ function languageForFile(file: string) {
|
||||
return 'csharp';
|
||||
return 'javascript';
|
||||
}
|
||||
|
||||
function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
||||
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
|
||||
return false;
|
||||
return metadata.method === 'pause';
|
||||
}
|
||||
|
||||
function shouldPauseOnStep(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
||||
return metadata.method === 'goto' || metadata.method === 'close';
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user