chore: extract debugger model from inspector (#6261)

This commit is contained in:
Pavel Feldman 2021-04-21 20:46:45 -07:00 committed by GitHub
parent 34e03fc77d
commit fe4fba4a16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 179 additions and 93 deletions

View File

@ -144,7 +144,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
} }
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> { 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) { async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {

View File

@ -28,6 +28,7 @@ import { InspectorController } from './supplements/inspectorController';
import { WebKit } from './webkit/webkit'; import { WebKit } from './webkit/webkit';
import { Registry } from '../utils/registry'; import { Registry } from '../utils/registry';
import { InstrumentationListener, multiplexInstrumentation, SdkObject } from './instrumentation'; import { InstrumentationListener, multiplexInstrumentation, SdkObject } from './instrumentation';
import { Debugger } from './supplements/debugger';
export class Playwright extends SdkObject { export class Playwright extends SdkObject {
readonly selectors: Selectors; readonly selectors: Selectors;
@ -41,6 +42,7 @@ export class Playwright extends SdkObject {
constructor(isInternal: boolean) { constructor(isInternal: boolean) {
const listeners: InstrumentationListener[] = []; const listeners: InstrumentationListener[] = [];
if (!isInternal) { if (!isInternal) {
listeners.push(new Debugger());
listeners.push(new Tracer()); listeners.push(new Tracer());
listeners.push(new HarTracer()); listeners.push(new HarTracer());
listeners.push(new InspectorController()); listeners.push(new InspectorController());

View 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';
}

View File

@ -18,54 +18,36 @@ import { BrowserContext } from '../browserContext';
import { RecorderSupplement } from './recorderSupplement'; import { RecorderSupplement } from './recorderSupplement';
import { debugLogger } from '../../utils/debugLogger'; import { debugLogger } from '../../utils/debugLogger';
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation'; import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
import { debugMode, isUnderTest } from '../../utils/utils'; import { ContextDebugger } from './debugger';
import * as consoleApiSource from '../../generated/consoleApiSource';
export class InspectorController implements InstrumentationListener { export class InspectorController implements InstrumentationListener {
async onContextCreated(context: BrowserContext): Promise<void> { async onContextCreated(context: BrowserContext): Promise<void> {
if (debugMode() === 'inspector') const contextDebugger = ContextDebugger.lookup(context)!;
await RecorderSupplement.getOrCreate(context, { pauseOnNextStatement: true }); if (contextDebugger.isPaused())
else if (debugMode() === 'console') RecorderSupplement.show(context, {}).catch(() => {});
await context.extendInjectedScript(consoleApiSource.source); contextDebugger.on(ContextDebugger.Events.PausedStateChanged, () => {
RecorderSupplement.show(context, {}).catch(() => {});
});
} }
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
const context = sdkObject.attribution.context; const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
if (!context) recorder?.onBeforeCall(sdkObject, metadata);
return;
if (shouldOpenInspector(sdkObject, metadata))
await RecorderSupplement.getOrCreate(context, { pauseOnNextStatement: true });
const recorder = await RecorderSupplement.getNoCreate(context);
await recorder?.onBeforeCall(sdkObject, metadata);
} }
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.context) const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
return; recorder?.onAfterCall(sdkObject, metadata);
const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
await recorder?.onAfterCall(sdkObject, metadata);
} }
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.context) const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
return; recorder?.onBeforeInputAction(sdkObject, metadata);
const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
await recorder?.onBeforeInputAction(sdkObject, metadata);
} }
async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
debugLogger.log(logName as any, message); debugLogger.log(logName as any, message);
if (!sdkObject.attribution.context) const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
return;
const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
recorder?.updateCallLog([metadata]); recorder?.updateCallLog([metadata]);
} }
} }
function shouldOpenInspector(sdkObject: SdkObject, metadata: CallMetadata): boolean {
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
return false;
return metadata.method === 'pause';
}

View File

@ -32,9 +32,10 @@ import { RecorderApp } from './recorder/recorderApp';
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation'; import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
import { Point } from '../../common/types'; import { Point } from '../../common/types';
import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes'; 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 { InMemorySnapshotter } from '../snapshot/inMemorySnapshotter';
import { metadataToCallLog } from './recorder/recorderUtils'; import { metadataToCallLog } from './recorder/recorderUtils';
import { ContextDebugger } from './debugger';
type BindingSource = { frame: Frame, page: Page }; type BindingSource = { frame: Frame, page: Page };
@ -52,16 +53,15 @@ export class RecorderSupplement {
private _recorderApp: RecorderApp | null = null; private _recorderApp: RecorderApp | null = null;
private _params: channels.BrowserContextRecorderSupplementEnableParams; private _params: channels.BrowserContextRecorderSupplementEnableParams;
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>(); private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
private _pausedCallsMetadata = new Map<CallMetadata, () => void>();
private _pauseOnNextStatement: boolean;
private _recorderSources: Source[]; private _recorderSources: Source[];
private _userSources = new Map<string, Source>(); private _userSources = new Map<string, Source>();
private _snapshotter: InMemorySnapshotter; private _snapshotter: InMemorySnapshotter;
private _hoveredSnapshot: { callLogId: number, phase: 'before' | 'after' | 'action' } | undefined; private _hoveredSnapshot: { callLogId: number, phase: 'before' | 'after' | 'action' } | undefined;
private _snapshots = new Set<string>(); private _snapshots = new Set<string>();
private _allMetadatas = new Map<number, CallMetadata>(); 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>; let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
if (!recorderPromise) { if (!recorderPromise) {
const recorder = new RecorderSupplement(context, params); const recorder = new RecorderSupplement(context, params);
@ -71,15 +71,17 @@ export class RecorderSupplement {
return recorderPromise; 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; return (context as any)[symbol] as Promise<RecorderSupplement> | undefined;
} }
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
this._context = context; this._context = context;
this._contextDebugger = ContextDebugger.getOrCreate(context);
this._params = params; this._params = params;
this._mode = params.startRecording ? 'recording' : 'none'; this._mode = params.startRecording ? 'recording' : 'none';
this._pauseOnNextStatement = !!params.pauseOnNextStatement;
const language = params.language || context._options.sdkLanguage; const language = params.language || context._options.sdkLanguage;
const languages = new Set([ const languages = new Set([
@ -150,21 +152,21 @@ export class RecorderSupplement {
} }
if (data.event === 'callLogHovered') { if (data.event === 'callLogHovered') {
this._hoveredSnapshot = undefined; this._hoveredSnapshot = undefined;
if (this._isPaused() && data.params.callLogId) if (this._contextDebugger.isPaused() && data.params.callLogId)
this._hoveredSnapshot = data.params; this._hoveredSnapshot = data.params;
this._refreshOverlay(); this._refreshOverlay();
return; return;
} }
if (data.event === 'step') { if (data.event === 'step') {
this._resume(true); this._contextDebugger.resume(true);
return; return;
} }
if (data.event === 'resume') { if (data.event === 'resume') {
this._resume(false); this._contextDebugger.resume(false);
return; return;
} }
if (data.event === 'pause') { if (data.event === 'pause') {
this._pauseOnNextStatement = true; this._contextDebugger.pauseOnNextStatement();
return; return;
} }
if (data.event === 'clear') { if (data.event === 'clear') {
@ -175,7 +177,7 @@ export class RecorderSupplement {
await Promise.all([ await Promise.all([
recorderApp.setMode(this._mode), recorderApp.setMode(this._mode),
recorderApp.setPaused(!!this._pausedCallsMetadata.size), recorderApp.setPaused(this._contextDebugger.isPaused()),
this._pushAllSources() this._pushAllSources()
]); ]);
@ -231,28 +233,29 @@ export class RecorderSupplement {
}); });
await this._context.exposeBinding('_playwrightResume', false, () => { await this._context.exposeBinding('_playwrightResume', false, () => {
this._resume(false).catch(() => {}); this._contextDebugger.resume(false);
}); });
const snapshotBaseUrl = await this._snapshotter.initialize() + '/snapshot/'; const snapshotBaseUrl = await this._snapshotter.initialize() + '/snapshot/';
await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest(), snapshotBaseUrl }); await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest(), snapshotBaseUrl });
await this._context.extendInjectedScript(consoleApiSource.source); 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; (this._context as any).recorderAppForTest = recorderApp;
} }
async pause(metadata: CallMetadata) { _pausedStateChanged() {
const result = new Promise<void>(f => { // If we are called upon page.pause, we don't have metadatas, populate them.
this._pausedCallsMetadata.set(metadata, f); for (const { metadata, sdkObject } of this._contextDebugger.pausedDetails()) {
}); if (!this._currentCallsMetadata.has(metadata))
this._recorderApp!.setPaused(true); this.onBeforeCall(sdkObject, metadata);
metadata.pauseStartTime = monotonicTime();
this._updateUserSources();
this.updateCallLog([metadata]);
return result;
} }
this._recorderApp!.setPaused(this._contextDebugger.isPaused());
_isPaused(): boolean { this._updateUserSources();
return !!this._pausedCallsMetadata.size; this.updateCallLog([...this._currentCallsMetadata.keys()]);
} }
private _setMode(mode: Mode) { private _setMode(mode: Mode) {
@ -263,21 +266,6 @@ export class RecorderSupplement {
this._context.pages()[0].bringToFront().catch(() => {}); 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() { private _refreshOverlay() {
for (const page of this._context.pages()) for (const page of this._context.pages())
page.mainFrame().evaluateExpression('window._playwrightRefreshOverlay()', false, undefined, 'main').catch(() => {}); 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') if (this._mode === 'recording')
return; return;
this._captureSnapshot(sdkObject, metadata, 'before'); this._captureSnapshot(sdkObject, metadata, 'before');
@ -418,21 +406,18 @@ export class RecorderSupplement {
this._allMetadatas.set(metadata.id, metadata); this._allMetadatas.set(metadata.id, metadata);
this._updateUserSources(); this._updateUserSources();
this.updateCallLog([metadata]); this.updateCallLog([metadata]);
if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseOnStep(sdkObject, metadata)))
await this.pause(metadata);
if (metadata.params && metadata.params.selector) { if (metadata.params && metadata.params.selector) {
this._highlightedSelector = 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') if (this._mode === 'recording')
return; return;
this._captureSnapshot(sdkObject, metadata, 'after'); this._captureSnapshot(sdkObject, metadata, 'after');
if (!metadata.error) if (!metadata.error)
this._currentCallsMetadata.delete(metadata); this._currentCallsMetadata.delete(metadata);
this._pausedCallsMetadata.delete(metadata);
this._updateUserSources(); this._updateUserSources();
this.updateCallLog([metadata]); this.updateCallLog([metadata]);
} }
@ -456,7 +441,7 @@ export class RecorderSupplement {
this._userSources.set(file, source); this._userSources.set(file, source);
} }
if (line) { 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.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
source.revealLine = line; source.revealLine = line;
fileToSelect = source.file; fileToSelect = source.file;
@ -471,12 +456,10 @@ export class RecorderSupplement {
this._recorderApp?.setSources([...this._recorderSources, ...this._userSources.values()]); 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') if (this._mode === 'recording')
return; return;
this._captureSnapshot(sdkObject, metadata, 'action'); this._captureSnapshot(sdkObject, metadata, 'action');
if (this._pauseOnNextStatement)
await this.pause(metadata);
} }
updateCallLog(metadatas: CallMetadata[]) { updateCallLog(metadatas: CallMetadata[]) {
@ -489,7 +472,7 @@ export class RecorderSupplement {
let status: CallLogStatus = 'done'; let status: CallLogStatus = 'done';
if (this._currentCallsMetadata.has(metadata)) if (this._currentCallsMetadata.has(metadata))
status = 'in-progress'; status = 'in-progress';
if (this._pausedCallsMetadata.has(metadata)) if (this._contextDebugger.isPaused(metadata))
status = 'paused'; status = 'paused';
logs.push(metadataToCallLog(metadata, status, this._snapshots)); logs.push(metadataToCallLog(metadata, status, this._snapshots));
} }
@ -514,13 +497,3 @@ function languageForFile(file: string) {
return 'csharp'; return 'csharp';
return 'javascript'; 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';
}