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> {
await RecorderSupplement.getOrCreate(this._context, params);
await RecorderSupplement.show(this._context, params);
}
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {

View File

@ -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());

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

View File

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