chore: make instrumentation per-context (#6302)
This commit is contained in:
Родитель
10c76ff56f
Коммит
97cf86d20a
|
@ -27,7 +27,11 @@ import { Progress } from './progress';
|
|||
import { Selectors, serverSelectors } from './selectors';
|
||||
import * as types from './types';
|
||||
import path from 'path';
|
||||
import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation';
|
||||
import { CallMetadata, internalCallMetadata, createInstrumentation, SdkObject } from './instrumentation';
|
||||
import { Debugger } from './supplements/debugger';
|
||||
import { Tracer } from './trace/recorder/tracer';
|
||||
import { HarTracer } from './supplements/har/harTracer';
|
||||
import { RecorderSupplement } from './supplements/recorderSupplement';
|
||||
|
||||
export abstract class BrowserContext extends SdkObject {
|
||||
static Events = {
|
||||
|
@ -51,6 +55,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
readonly _browserContextId: string | undefined;
|
||||
private _selectors?: Selectors;
|
||||
private _origins = new Set<string>();
|
||||
private _harTracer: HarTracer | undefined;
|
||||
|
||||
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
|
||||
super(browser, 'browser-context');
|
||||
|
@ -60,6 +65,9 @@ export abstract class BrowserContext extends SdkObject {
|
|||
this._browserContextId = browserContextId;
|
||||
this._isPersistentContext = !browserContextId;
|
||||
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
||||
|
||||
if (this._options.recordHar)
|
||||
this._harTracer = new HarTracer(this, this._options.recordHar);
|
||||
}
|
||||
|
||||
_setSelectors(selectors: Selectors) {
|
||||
|
@ -71,7 +79,31 @@ export abstract class BrowserContext extends SdkObject {
|
|||
}
|
||||
|
||||
async _initialize() {
|
||||
await this.instrumentation.onContextCreated(this);
|
||||
if (this.attribution.isInternal)
|
||||
return;
|
||||
// Create instrumentation per context.
|
||||
this.instrumentation = createInstrumentation();
|
||||
|
||||
// Debugger will pause execution upon page.pause in headed mode.
|
||||
const contextDebugger = new Debugger(this);
|
||||
this.instrumentation.addListener(contextDebugger);
|
||||
|
||||
if (this._options._traceDir)
|
||||
this.instrumentation.addListener(new Tracer(this, this._options._traceDir));
|
||||
|
||||
|
||||
// When PWDEBUG=1, show inspector for each context.
|
||||
if (debugMode() === 'inspector')
|
||||
await RecorderSupplement.show(this, { pauseOnNextStatement: true });
|
||||
|
||||
// When paused, show inspector.
|
||||
if (contextDebugger.isPaused())
|
||||
RecorderSupplement.showInspector(this);
|
||||
contextDebugger.on(Debugger.Events.PausedStateChanged, () => {
|
||||
RecorderSupplement.showInspector(this);
|
||||
});
|
||||
|
||||
await this.instrumentation.onContextCreated();
|
||||
}
|
||||
|
||||
async _ensureVideosPath() {
|
||||
|
@ -231,7 +263,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
this.emit(BrowserContext.Events.BeforeClose);
|
||||
this._closedStatus = 'closing';
|
||||
|
||||
await this.instrumentation.onContextWillDestroy(this);
|
||||
await this._harTracer?.flush();
|
||||
|
||||
// Cleanup.
|
||||
const promises: Promise<void>[] = [];
|
||||
|
@ -260,7 +292,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
await this._browser.close();
|
||||
|
||||
// Bookkeeping.
|
||||
await this.instrumentation.onContextDidDestroy(this);
|
||||
await this.instrumentation.onContextDestroyed();
|
||||
this._didCloseInternal();
|
||||
}
|
||||
await this._closePromise;
|
||||
|
|
|
@ -25,6 +25,7 @@ import type { Frame } from './frames';
|
|||
import type { Page } from './page';
|
||||
|
||||
export type Attribution = {
|
||||
isInternal: boolean,
|
||||
browserType?: BrowserType;
|
||||
browser?: Browser;
|
||||
context?: BrowserContext;
|
||||
|
@ -67,34 +68,32 @@ export class SdkObject extends EventEmitter {
|
|||
}
|
||||
|
||||
export interface Instrumentation {
|
||||
onContextCreated(context: BrowserContext): Promise<void>;
|
||||
onContextWillDestroy(context: BrowserContext): Promise<void>;
|
||||
onContextDidDestroy(context: BrowserContext): Promise<void>;
|
||||
|
||||
addListener(listener: InstrumentationListener): void;
|
||||
onContextCreated(): Promise<void>;
|
||||
onContextDestroyed(): Promise<void>;
|
||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
||||
onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
|
||||
onEvent(sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
}
|
||||
|
||||
export interface InstrumentationListener {
|
||||
onContextCreated?(context: BrowserContext): Promise<void>;
|
||||
onContextWillDestroy?(context: BrowserContext): Promise<void>;
|
||||
onContextDidDestroy?(context: BrowserContext): Promise<void>;
|
||||
|
||||
onContextCreated?(): Promise<void>;
|
||||
onContextDestroyed?(): Promise<void>;
|
||||
onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
||||
onCallLog?(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
|
||||
onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
}
|
||||
|
||||
export function multiplexInstrumentation(listeners: InstrumentationListener[]): Instrumentation {
|
||||
export function createInstrumentation(): Instrumentation {
|
||||
const listeners: InstrumentationListener[] = [];
|
||||
return new Proxy({}, {
|
||||
get: (obj: any, prop: string) => {
|
||||
if (prop === 'addListener')
|
||||
return (listener: InstrumentationListener) => listeners.push(listener);
|
||||
if (!prop.startsWith('on'))
|
||||
return obj[prop];
|
||||
return async (...params: any[]) => {
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { Tracer } from './trace/recorder/tracer';
|
||||
import { Android } from './android/android';
|
||||
import { AdbBackend } from './android/backendAdb';
|
||||
import { PlaywrightOptions } from './browser';
|
||||
|
@ -23,12 +22,9 @@ import { Chromium } from './chromium/chromium';
|
|||
import { Electron } from './electron/electron';
|
||||
import { Firefox } from './firefox/firefox';
|
||||
import { Selectors, serverSelectors } from './selectors';
|
||||
import { HarTracer } from './supplements/har/harTracer';
|
||||
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';
|
||||
import { createInstrumentation, SdkObject } from './instrumentation';
|
||||
|
||||
export class Playwright extends SdkObject {
|
||||
readonly selectors: Selectors;
|
||||
|
@ -40,15 +36,7 @@ export class Playwright extends SdkObject {
|
|||
readonly options: PlaywrightOptions;
|
||||
|
||||
constructor(isInternal: boolean) {
|
||||
const listeners: InstrumentationListener[] = [];
|
||||
if (!isInternal) {
|
||||
listeners.push(new Debugger());
|
||||
listeners.push(new Tracer());
|
||||
listeners.push(new HarTracer());
|
||||
listeners.push(new InspectorController());
|
||||
}
|
||||
const instrumentation = multiplexInstrumentation(listeners);
|
||||
super({ attribution: {}, instrumentation } as any, undefined, 'Playwright');
|
||||
super({ attribution: { isInternal }, instrumentation: createInstrumentation() } as any, undefined, 'Playwright');
|
||||
this.options = {
|
||||
registry: new Registry(path.join(__dirname, '..', '..')),
|
||||
rootSdkObject: this,
|
||||
|
|
|
@ -19,54 +19,38 @@ import { debugMode, isUnderTest, monotonicTime } from '../../utils/utils';
|
|||
import { BrowserContext } from '../browserContext';
|
||||
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
|
||||
import * as consoleApiSource from '../../generated/consoleApiSource';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
|
||||
export class Debugger implements InstrumentationListener {
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
ContextDebugger.getOrCreate(context);
|
||||
if (debugMode() === 'console')
|
||||
await context.extendInjectedScript(consoleApiSource.source);
|
||||
}
|
||||
const symbol = Symbol('Debugger');
|
||||
|
||||
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 {
|
||||
export class Debugger extends EventEmitter implements InstrumentationListener {
|
||||
private _pauseOnNextStatement = false;
|
||||
private _pausedCallsMetadata = new Map<CallMetadata, { resolve: () => void, sdkObject: SdkObject }>();
|
||||
private _enabled: boolean;
|
||||
private _context: BrowserContext;
|
||||
|
||||
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() {
|
||||
constructor(context: BrowserContext) {
|
||||
super();
|
||||
this._context = context;
|
||||
(this._context as any)[symbol] = this;
|
||||
this._enabled = debugMode() === 'inspector';
|
||||
if (this._enabled)
|
||||
this.pauseOnNextStatement();
|
||||
}
|
||||
|
||||
static lookup(context?: BrowserContext): ContextDebugger | undefined {
|
||||
static lookup(context?: BrowserContext): Debugger | undefined {
|
||||
if (!context)
|
||||
return;
|
||||
return (context as any)[symbol] as ContextDebugger | undefined;
|
||||
return (context as any)[symbol] as Debugger | undefined;
|
||||
}
|
||||
|
||||
async onContextCreated() {
|
||||
if (debugMode() === 'console')
|
||||
await this._context.extendInjectedScript(consoleApiSource.source);
|
||||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
|
@ -79,13 +63,17 @@ export class ContextDebugger extends EventEmitter {
|
|||
await this.pause(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
debugLogger.log(logName as any, message);
|
||||
}
|
||||
|
||||
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);
|
||||
this.emit(Debugger.Events.PausedStateChanged);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -97,7 +85,7 @@ export class ContextDebugger extends EventEmitter {
|
|||
resolve();
|
||||
}
|
||||
this._pausedCallsMetadata.clear();
|
||||
this.emit(ContextDebugger.Events.PausedStateChanged);
|
||||
this.emit(Debugger.Events.PausedStateChanged);
|
||||
}
|
||||
|
||||
pauseOnNextStatement() {
|
||||
|
|
|
@ -20,36 +20,16 @@ import { BrowserContext } from '../../browserContext';
|
|||
import { helper } from '../../helper';
|
||||
import * as network from '../../network';
|
||||
import { Page } from '../../page';
|
||||
import { InstrumentationListener } from '../../instrumentation';
|
||||
import * as har from './har';
|
||||
|
||||
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
|
||||
|
||||
export class HarTracer implements InstrumentationListener {
|
||||
private _contextTracers = new Map<BrowserContext, HarContextTracer>();
|
||||
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
if (!context._options.recordHar)
|
||||
return;
|
||||
const contextTracer = new HarContextTracer(context, context._options.recordHar);
|
||||
this._contextTracers.set(context, contextTracer);
|
||||
}
|
||||
|
||||
async onContextWillDestroy(context: BrowserContext): Promise<void> {
|
||||
const contextTracer = this._contextTracers.get(context);
|
||||
if (contextTracer) {
|
||||
this._contextTracers.delete(context);
|
||||
await contextTracer.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type HarOptions = {
|
||||
path: string;
|
||||
omitContent?: boolean;
|
||||
};
|
||||
|
||||
class HarContextTracer {
|
||||
export class HarTracer {
|
||||
private _options: HarOptions;
|
||||
private _log: har.Log;
|
||||
private _pageEntries = new Map<Page, har.Page>();
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/**
|
||||
* 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 { BrowserContext } from '../browserContext';
|
||||
import { RecorderSupplement } from './recorderSupplement';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
|
||||
import { ContextDebugger } from './debugger';
|
||||
|
||||
export class InspectorController implements InstrumentationListener {
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
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 recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
|
||||
recorder?.onBeforeCall(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
|
||||
recorder?.onAfterCall(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
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);
|
||||
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
|
||||
recorder?.updateCallLog([metadata]);
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toS
|
|||
import { ActionInContext } from './codeGenerator';
|
||||
import { actionTitle, Action } from './recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from './utils';
|
||||
import deviceDescriptors = require('../../deviceDescriptors');
|
||||
import deviceDescriptors from '../../deviceDescriptors';
|
||||
|
||||
export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||
id = 'csharp';
|
||||
|
|
|
@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, toSignalMap } from './lang
|
|||
import { ActionInContext } from './codeGenerator';
|
||||
import { Action, actionTitle } from './recorderActions';
|
||||
import { toModifiers } from './utils';
|
||||
import deviceDescriptors = require('../../deviceDescriptors');
|
||||
import deviceDescriptors from '../../deviceDescriptors';
|
||||
import { JavaScriptFormatter } from './javascript';
|
||||
|
||||
export class JavaLanguageGenerator implements LanguageGenerator {
|
||||
|
|
|
@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toS
|
|||
import { ActionInContext } from './codeGenerator';
|
||||
import { Action, actionTitle } from './recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from './utils';
|
||||
import deviceDescriptors = require('../../deviceDescriptors');
|
||||
import deviceDescriptors from '../../deviceDescriptors';
|
||||
|
||||
export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||
id = 'javascript';
|
||||
|
|
|
@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toS
|
|||
import { ActionInContext } from './codeGenerator';
|
||||
import { actionTitle, Action } from './recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from './utils';
|
||||
import deviceDescriptors = require('../../deviceDescriptors');
|
||||
import deviceDescriptors from '../../deviceDescriptors';
|
||||
|
||||
export class PythonLanguageGenerator implements LanguageGenerator {
|
||||
id = 'python';
|
||||
|
|
|
@ -29,19 +29,19 @@ import { PythonLanguageGenerator } from './recorder/python';
|
|||
import * as recorderSource from '../../generated/recorderSource';
|
||||
import * as consoleApiSource from '../../generated/consoleApiSource';
|
||||
import { RecorderApp } from './recorder/recorderApp';
|
||||
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
|
||||
import { CallMetadata, InstrumentationListener, internalCallMetadata, SdkObject } from '../instrumentation';
|
||||
import { Point } from '../../common/types';
|
||||
import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
|
||||
import { isUnderTest } from '../../utils/utils';
|
||||
import { InMemorySnapshotter } from '../snapshot/inMemorySnapshotter';
|
||||
import { metadataToCallLog } from './recorder/recorderUtils';
|
||||
import { ContextDebugger } from './debugger';
|
||||
import { Debugger } from './debugger';
|
||||
|
||||
type BindingSource = { frame: Frame, page: Page };
|
||||
|
||||
const symbol = Symbol('RecorderSupplement');
|
||||
|
||||
export class RecorderSupplement {
|
||||
export class RecorderSupplement implements InstrumentationListener {
|
||||
private _generator: CodeGenerator;
|
||||
private _pageAliases = new Map<Page, string>();
|
||||
private _lastPopupOrdinal = 0;
|
||||
|
@ -59,7 +59,11 @@ export class RecorderSupplement {
|
|||
private _hoveredSnapshot: { callLogId: string, phase: 'before' | 'after' | 'action' } | undefined;
|
||||
private _snapshots = new Set<string>();
|
||||
private _allMetadatas = new Map<string, CallMetadata>();
|
||||
private _contextDebugger: ContextDebugger;
|
||||
private _debugger: Debugger;
|
||||
|
||||
static showInspector(context: BrowserContext) {
|
||||
RecorderSupplement.show(context, {}).catch(() => {});
|
||||
}
|
||||
|
||||
static show(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<RecorderSupplement> {
|
||||
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
|
||||
|
@ -71,15 +75,10 @@ export class RecorderSupplement {
|
|||
return recorderPromise;
|
||||
}
|
||||
|
||||
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._debugger = Debugger.lookup(context)!;
|
||||
context.instrumentation.addListener(this);
|
||||
this._params = params;
|
||||
this._mode = params.startRecording ? 'recording' : 'none';
|
||||
const language = params.language || context._options.sdkLanguage;
|
||||
|
@ -152,21 +151,21 @@ export class RecorderSupplement {
|
|||
}
|
||||
if (data.event === 'callLogHovered') {
|
||||
this._hoveredSnapshot = undefined;
|
||||
if (this._contextDebugger.isPaused() && data.params.callLogId)
|
||||
if (this._debugger.isPaused() && data.params.callLogId)
|
||||
this._hoveredSnapshot = data.params;
|
||||
this._refreshOverlay();
|
||||
return;
|
||||
}
|
||||
if (data.event === 'step') {
|
||||
this._contextDebugger.resume(true);
|
||||
this._debugger.resume(true);
|
||||
return;
|
||||
}
|
||||
if (data.event === 'resume') {
|
||||
this._contextDebugger.resume(false);
|
||||
this._debugger.resume(false);
|
||||
return;
|
||||
}
|
||||
if (data.event === 'pause') {
|
||||
this._contextDebugger.pauseOnNextStatement();
|
||||
this._debugger.pauseOnNextStatement();
|
||||
return;
|
||||
}
|
||||
if (data.event === 'clear') {
|
||||
|
@ -177,7 +176,7 @@ export class RecorderSupplement {
|
|||
|
||||
await Promise.all([
|
||||
recorderApp.setMode(this._mode),
|
||||
recorderApp.setPaused(this._contextDebugger.isPaused()),
|
||||
recorderApp.setPaused(this._debugger.isPaused()),
|
||||
this._pushAllSources()
|
||||
]);
|
||||
|
||||
|
@ -233,27 +232,27 @@ export class RecorderSupplement {
|
|||
});
|
||||
|
||||
await this._context.exposeBinding('_playwrightResume', false, () => {
|
||||
this._contextDebugger.resume(false);
|
||||
this._debugger.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())
|
||||
if (this._debugger.isPaused())
|
||||
this._pausedStateChanged();
|
||||
this._contextDebugger.on(ContextDebugger.Events.PausedStateChanged, () => this._pausedStateChanged());
|
||||
this._debugger.on(Debugger.Events.PausedStateChanged, () => this._pausedStateChanged());
|
||||
|
||||
(this._context as any).recorderAppForTest = recorderApp;
|
||||
}
|
||||
|
||||
_pausedStateChanged() {
|
||||
// If we are called upon page.pause, we don't have metadatas, populate them.
|
||||
for (const { metadata, sdkObject } of this._contextDebugger.pausedDetails()) {
|
||||
for (const { metadata, sdkObject } of this._debugger.pausedDetails()) {
|
||||
if (!this._currentCallsMetadata.has(metadata))
|
||||
this.onBeforeCall(sdkObject, metadata);
|
||||
}
|
||||
this._recorderApp!.setPaused(this._contextDebugger.isPaused());
|
||||
this._recorderApp!.setPaused(this._debugger.isPaused());
|
||||
this._updateUserSources();
|
||||
this.updateCallLog([...this._currentCallsMetadata.keys()]);
|
||||
}
|
||||
|
@ -398,7 +397,7 @@ export class RecorderSupplement {
|
|||
}
|
||||
}
|
||||
|
||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._mode === 'recording')
|
||||
return;
|
||||
this._captureSnapshot(sdkObject, metadata, 'before');
|
||||
|
@ -412,7 +411,7 @@ export class RecorderSupplement {
|
|||
}
|
||||
}
|
||||
|
||||
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._mode === 'recording')
|
||||
return;
|
||||
this._captureSnapshot(sdkObject, metadata, 'after');
|
||||
|
@ -441,7 +440,7 @@ export class RecorderSupplement {
|
|||
this._userSources.set(file, source);
|
||||
}
|
||||
if (line) {
|
||||
const paused = this._contextDebugger.isPaused(metadata);
|
||||
const paused = this._debugger.isPaused(metadata);
|
||||
source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
|
||||
source.revealLine = line;
|
||||
fileToSelect = source.file;
|
||||
|
@ -456,12 +455,16 @@ export class RecorderSupplement {
|
|||
this._recorderApp?.setSources([...this._recorderSources, ...this._userSources.values()]);
|
||||
}
|
||||
|
||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._mode === 'recording')
|
||||
return;
|
||||
this._captureSnapshot(sdkObject, metadata, 'action');
|
||||
}
|
||||
|
||||
async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
this.updateCallLog([metadata]);
|
||||
}
|
||||
|
||||
updateCallLog(metadatas: CallMetadata[]) {
|
||||
if (this._mode === 'recording')
|
||||
return;
|
||||
|
@ -472,7 +475,7 @@ export class RecorderSupplement {
|
|||
let status: CallLogStatus = 'done';
|
||||
if (this._currentCallsMetadata.has(metadata))
|
||||
status = 'in-progress';
|
||||
if (this._contextDebugger.isPaused(metadata))
|
||||
if (this._debugger.isPaused(metadata))
|
||||
status = 'paused';
|
||||
logs.push(metadataToCallLog(metadata, status, this._snapshots));
|
||||
}
|
||||
|
|
|
@ -63,7 +63,6 @@ export type ActionTraceEvent = {
|
|||
type: 'action' | 'event',
|
||||
contextId: string,
|
||||
metadata: CallMetadata,
|
||||
snapshots?: { title: string, snapshotName: string }[],
|
||||
};
|
||||
|
||||
export type DialogOpenedEvent = {
|
||||
|
|
|
@ -32,53 +32,18 @@ const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
|
|||
const envTrace = getFromENV('PWTRACE_RESOURCE_DIR');
|
||||
|
||||
export class Tracer implements InstrumentationListener {
|
||||
private _contextTracers = new Map<BrowserContext, ContextTracer>();
|
||||
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
const traceDir = context._options._traceDir;
|
||||
if (!traceDir)
|
||||
return;
|
||||
const resourcesDir = envTrace || path.join(traceDir, 'resources');
|
||||
const tracePath = path.join(traceDir, context._options._debugName!);
|
||||
const contextTracer = new ContextTracer(context, resourcesDir, tracePath);
|
||||
await contextTracer.start();
|
||||
this._contextTracers.set(context, contextTracer);
|
||||
}
|
||||
|
||||
async onContextDidDestroy(context: BrowserContext): Promise<void> {
|
||||
const contextTracer = this._contextTracers.get(context);
|
||||
if (contextTracer) {
|
||||
await contextTracer.dispose().catch(e => {});
|
||||
this._contextTracers.delete(context);
|
||||
}
|
||||
}
|
||||
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void> {
|
||||
this._contextTracers.get(sdkObject.attribution.context!)?.onBeforeInputAction(sdkObject, metadata, element);
|
||||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
this._contextTracers.get(sdkObject.attribution.context!)?.onBeforeCall(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
this._contextTracers.get(sdkObject.attribution.context!)?.onAfterCall(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onEvent(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
this._contextTracers.get(sdkObject.attribution.context!)?.onEvent(sdkObject, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextTracer {
|
||||
private _contextId: string;
|
||||
private _appendEventChain: Promise<string>;
|
||||
private _snapshotter: PersistentSnapshotter;
|
||||
private _eventListeners: RegisteredListener[];
|
||||
private _disposed = false;
|
||||
private _pendingCalls = new Map<string, { sdkObject: SdkObject, metadata: CallMetadata }>();
|
||||
private _context: BrowserContext;
|
||||
|
||||
constructor(context: BrowserContext, resourcesDir: string, tracePrefix: string) {
|
||||
constructor(context: BrowserContext, traceDir: string) {
|
||||
this._context = context;
|
||||
const resourcesDir = envTrace || path.join(traceDir, 'resources');
|
||||
const tracePrefix = path.join(traceDir, context._options._debugName!);
|
||||
const traceFile = tracePrefix + '-actions.trace';
|
||||
this._contextId = 'context@' + createGuid();
|
||||
this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile);
|
||||
|
@ -99,10 +64,6 @@ class ContextTracer {
|
|||
];
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this._snapshotter.start(false);
|
||||
}
|
||||
|
||||
_captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) {
|
||||
if (!sdkObject.attribution.page)
|
||||
return;
|
||||
|
@ -111,16 +72,20 @@ class ContextTracer {
|
|||
this._snapshotter.captureSnapshot(sdkObject.attribution.page, snapshotName, element);
|
||||
}
|
||||
|
||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
async onContextCreated(): Promise<void> {
|
||||
await this._snapshotter.start(false);
|
||||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
this._captureSnapshot('before', sdkObject, metadata);
|
||||
this._pendingCalls.set(metadata.id, { sdkObject, metadata });
|
||||
}
|
||||
|
||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
||||
this._captureSnapshot('action', sdkObject, metadata, element);
|
||||
}
|
||||
|
||||
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
this._captureSnapshot('after', sdkObject, metadata);
|
||||
if (!sdkObject.attribution.page)
|
||||
return;
|
||||
|
@ -239,7 +204,7 @@ class ContextTracer {
|
|||
});
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
async onContextDestroyed() {
|
||||
this._disposed = true;
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
await this._snapshotter.dispose();
|
||||
|
|
|
@ -30,7 +30,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
let [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
||||
|
||||
const snapshotMap = new Map<string, { title: string, snapshotName: string }>();
|
||||
for (const snapshot of actionEntry?.snapshots || [])
|
||||
for (const snapshot of actionEntry?.metadata.snapshots || [])
|
||||
snapshotMap.set(snapshot.title, snapshot);
|
||||
const actionSnapshot = snapshotMap.get('action') || snapshotMap.get('after');
|
||||
const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[];
|
||||
|
|
|
@ -140,7 +140,8 @@ DEPS['src/server/injected/'] = ['src/server/common/'];
|
|||
DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/'];
|
||||
DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
||||
|
||||
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/trace/recorder/tracer.ts', 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/android/', 'src/server/electron/'];
|
||||
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/android/', 'src/server/electron/'];
|
||||
DEPS['src/server/browserContext.ts'] = [...DEPS['src/server/'], 'src/server/trace/recorder/tracer.ts'];
|
||||
DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**'];
|
||||
|
||||
// Tracing is a client/server plugin, nothing should depend on it.
|
||||
|
|
Загрузка…
Ссылка в новой задаче