Skip to content

Commit

Permalink
chore: pause on input in pwdebug mode (#5427)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Feb 12, 2021
1 parent 55614c7 commit aef052a
Show file tree
Hide file tree
Showing 19 changed files with 299 additions and 96 deletions.
8 changes: 2 additions & 6 deletions src/dispatchers/browserContextDispatcher.ts
Expand Up @@ -23,7 +23,6 @@ import { CRBrowserContext } from '../server/chromium/crBrowser';
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { RecorderSupplement } from '../server/supplements/recorderSupplement';
import { CallMetadata } from '../server/instrumentation';
import { isUnderTest } from '../utils/utils';

export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
private _context: BrowserContext;
Expand Down Expand Up @@ -132,11 +131,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await RecorderSupplement.getOrCreate(this._context, params);
}

async pause() {
if (!this._context._browser.options.headful && !isUnderTest())
return;
const recorder = await RecorderSupplement.getOrCreate(this._context);
await recorder.pause();
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
// Inspector controller will take care of this.
}

async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise<channels.BrowserContextCrNewCDPSessionResult> {
Expand Down
10 changes: 6 additions & 4 deletions src/dispatchers/dispatcher.ts
Expand Up @@ -190,6 +190,7 @@ export class DispatcherConnection {
}

const callMetadata: CallMetadata = {
id,
...validMetadata,
startTime: monotonicTime(),
endTime: 0,
Expand All @@ -199,9 +200,10 @@ export class DispatcherConnection {
log: [],
};

const sdkObject = dispatcher._object instanceof SdkObject ? dispatcher._object : undefined;
try {
if (dispatcher instanceof SdkObject)
await dispatcher.instrumentation.onBeforeCall(dispatcher, callMetadata);
if (sdkObject)
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
const result = await (dispatcher as any)[method](validParams, callMetadata);
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
} catch (e) {
Expand All @@ -210,8 +212,8 @@ export class DispatcherConnection {
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote);
this.onmessage({ id, error: serializeError(e) });
} finally {
if (dispatcher instanceof SdkObject)
await dispatcher.instrumentation.onAfterCall(dispatcher, callMetadata);
if (sdkObject)
await sdkObject.instrumentation.onAfterCall(sdkObject, callMetadata);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/server/dom.ts
Expand Up @@ -382,6 +382,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (options && options.modifiers)
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
progress.log(` performing ${actionName} action`);
progress.metadata.point = point;
await progress.beforeInputAction();
await action(point);
progress.log(` ${actionName} action done`);
Expand Down
5 changes: 4 additions & 1 deletion src/server/instrumentation.ts
Expand Up @@ -15,7 +15,7 @@
*/

import { EventEmitter } from 'events';
import { StackFrame } from '../common/types';
import { Point, StackFrame } from '../common/types';
import type { Browser } from './browser';
import type { BrowserContext } from './browserContext';
import type { BrowserType } from './browserType';
Expand All @@ -31,6 +31,7 @@ export type Attribution = {
};

export type CallMetadata = {
id: number;
startTime: number;
endTime: number;
type: string;
Expand All @@ -39,6 +40,7 @@ export type CallMetadata = {
stack?: StackFrame[];
log: string[];
error?: Error;
point?: Point;
};

export class SdkObject extends EventEmitter {
Expand Down Expand Up @@ -92,6 +94,7 @@ export function multiplexInstrumentation(listeners: InstrumentationListener[]):

export function internalCallMetadata(): CallMetadata {
return {
id: 0,
startTime: 0,
endTime: 0,
type: 'Internal',
Expand Down
2 changes: 2 additions & 0 deletions src/server/progress.ts
Expand Up @@ -27,6 +27,7 @@ export interface Progress {
throwIfAborted(): void;
beforeInputAction(): Promise<void>;
afterInputAction(): Promise<void>;
metadata: CallMetadata;
}

export class ProgressController {
Expand Down Expand Up @@ -92,6 +93,7 @@ export class ProgressController {
afterInputAction: async () => {
await this.instrumentation.onAfterInputAction(this.sdkObject, this.metadata);
},
metadata: this.metadata
};

const timeoutError = new TimeoutError(`Timeout ${this._timeout}ms exceeded.`);
Expand Down
61 changes: 48 additions & 13 deletions src/server/supplements/injected/recorder.ts
Expand Up @@ -16,20 +16,17 @@

import type * as actions from '../recorder/recorderActions';
import type InjectedScript from '../../injected/injectedScript';
import { generateSelector } from './selectorGenerator';
import { generateSelector, querySelector } from './selectorGenerator';
import { html } from './html';

type Mode = 'inspecting' | 'recording' | 'none';
type State = {
mode: Mode,
};
import type { Point } from '../../../common/types';
import type { UIState } from '../recorder/recorderTypes';

declare global {
interface Window {
_playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
_playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
_playwrightRecorderCommitAction: () => Promise<void>;
_playwrightRecorderState: () => Promise<State>;
_playwrightRecorderState: () => Promise<UIState>;
_playwrightRecorderPrintSelector: (text: string) => Promise<void>;
_playwrightResume: () => Promise<void>;
}
Expand All @@ -52,6 +49,9 @@ export class Recorder {
private _expectProgrammaticKeyUp = false;
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
private _mode: 'none' | 'inspecting' | 'recording' = 'none';
private _actionPointElement: HTMLElement;
private _actionPoint: Point | undefined;
private _actionSelector: string | undefined;

constructor(injectedScript: InjectedScript) {
this._injectedScript = injectedScript;
Expand All @@ -69,6 +69,7 @@ export class Recorder {
</x-pw-glass>`;

this._tooltipElement = html`<x-pw-tooltip></x-pw-tooltip>`;
this._actionPointElement = html`<x-pw-action-point hidden=true></x-pw-action-point>`;

this._innerGlassPaneElement = html`
<x-pw-glass-inner style="flex: auto">
Expand All @@ -78,6 +79,7 @@ export class Recorder {
// Use a closed shadow root to prevent selectors matching our internal previews.
this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: 'closed' });
this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
this._glassPaneShadow.appendChild(this._actionPointElement);
this._glassPaneShadow.appendChild(html`
<style>
x-pw-tooltip {
Expand All @@ -103,6 +105,19 @@ export class Recorder {
position: absolute;
top: 0;
}
x-pw-action-point {
position: absolute;
width: 20px;
height: 20px;
background: red;
border-radius: 10px;
pointer-events: none;
margin: -10px 0 0 -10px;
z-index: 2;
}
*[hidden] {
display: none !important;
}
</style>
`);
this._refreshListenersIfNeeded();
Expand All @@ -114,11 +129,6 @@ export class Recorder {
this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console
}

private _setMode(mode: Mode): void {
this._clearHighlight();
this._mode = mode;
}

private _refreshListenersIfNeeded() {
if ((document.documentElement as any)[scriptSymbol])
return;
Expand All @@ -136,6 +146,7 @@ export class Recorder {
addEventListener(document, 'focus', () => this._onFocus(), true),
addEventListener(document, 'scroll', () => {
this._hoveredModel = null;
this._actionPointElement.hidden = true;
this._updateHighlight();
}, true),
];
Expand All @@ -152,11 +163,35 @@ export class Recorder {
return;
}

const { mode } = state;
const { mode, actionPoint, actionSelector } = state;
if (mode !== this._mode) {
this._mode = mode;
this._clearHighlight();
}
if (actionPoint && this._actionPoint && actionPoint.x === this._actionPoint.x && actionPoint.y === this._actionPoint.y) {
// All good.
} else if (!actionPoint && !this._actionPoint) {
// All good.
} else {
if (actionPoint) {
this._actionPointElement.style.top = actionPoint.y + 'px';
this._actionPointElement.style.left = actionPoint.x + 'px';
this._actionPointElement.hidden = false;
} else {
this._actionPointElement.hidden = true;
}
this._actionPoint = actionPoint;
}

// Race or scroll.
if (this._actionSelector && !this._hoveredModel?.elements.length)
this._actionSelector = undefined;

if (actionSelector !== this._actionSelector) {
this._hoveredModel = actionSelector ? querySelector(this._injectedScript, actionSelector, document) : null;
this._updateHighlight();
this._actionSelector = actionSelector;
}
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
}

Expand Down
8 changes: 8 additions & 0 deletions src/server/supplements/injected/selectorGenerator.ts
Expand Up @@ -26,6 +26,14 @@ type SelectorToken = {
const cacheAllowText = new Map<Element, SelectorToken[] | null>();
const cacheDisallowText = new Map<Element, SelectorToken[] | null>();

export function querySelector(injectedScript: InjectedScript, selector: string, ownerDocument: Document): { selector: string, elements: Element[] } {
const parsedSelector = injectedScript.parseSelector(selector);
return {
selector,
elements: injectedScript.querySelectorAll(parsedSelector, ownerDocument)
};
}

export function generateSelector(injectedScript: InjectedScript, targetElement: Element): { selector: string, elements: Element[] } {
injectedScript._evaluator.begin();
try {
Expand Down
42 changes: 39 additions & 3 deletions src/server/supplements/inspectorController.ts
Expand Up @@ -15,15 +15,51 @@
*/

import { BrowserContext } from '../browserContext';
import { isDebugMode } from '../../utils/utils';
import { RecorderSupplement } from './recorderSupplement';
import { InstrumentationListener } from '../instrumentation';
import { debugLogger } from '../../utils/debugLogger';
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
import { isDebugMode, isUnderTest } from '../../utils/utils';

export class InspectorController implements InstrumentationListener {
private _recorders = new Map<BrowserContext, Promise<RecorderSupplement>>();

async onContextCreated(context: BrowserContext): Promise<void> {
if (isDebugMode())
RecorderSupplement.getOrCreate(context);
this._recorders.set(context, RecorderSupplement.getOrCreate(context));
}

async onContextDidDestroy(context: BrowserContext): Promise<void> {
this._recorders.delete(context);
}

async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
const context = sdkObject.attribution.context;
if (!context)
return;

if (metadata.method === 'pause') {
// Force create recorder on pause.
if (!context._browser.options.headful && !isUnderTest())
return;
this._recorders.set(context, RecorderSupplement.getOrCreate(context));
}

const recorder = await this._recorders.get(context);
await recorder?.onBeforeCall(sdkObject, metadata);
}

async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.page)
return;
const recorder = await this._recorders.get(sdkObject.attribution.context!);
await recorder?.onAfterCall(sdkObject, metadata);
}

async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.page)
return;
const recorder = await this._recorders.get(sdkObject.attribution.context!);
await recorder?.onBeforeInputAction(sdkObject, metadata);
}

onCallLog(logName: string, message: string): void {
Expand Down
31 changes: 13 additions & 18 deletions src/server/supplements/recorder/recorderApp.ts
Expand Up @@ -23,22 +23,17 @@ import { ProgressController } from '../../progress';
import { createPlaywright } from '../../playwright';
import { EventEmitter } from 'events';
import { internalCallMetadata } from '../../instrumentation';
import { isUnderTest } from '../../../utils/utils';
import type { EventData, Mode, PauseDetails, Source } from './recorderTypes';
import { BrowserContext } from '../../browserContext';
import { isUnderTest } from '../../../utils/utils';

const readFileAsync = util.promisify(fs.readFile);

export type Mode = 'inspecting' | 'recording' | 'none';
export type EventData = {
event: 'clear' | 'resume' | 'setMode',
params: any
};

declare global {
interface Window {
playwrightSetMode: (mode: Mode) => void;
playwrightSetPaused: (paused: boolean) => void;
playwrightSetSource: (params: { text: string, language: string }) => void;
playwrightSetPaused: (details: PauseDetails | null) => void;
playwrightSetSource: (source: Source) => void;
dispatch(data: EventData): Promise<void>;
}
}
Expand Down Expand Up @@ -102,7 +97,7 @@ export class RecorderApp extends EventEmitter {
'--window-position=1280,10',
],
noDefaultViewport: true,
headless: isUnderTest()
headless: isUnderTest() && !inspectedContext._browser.options.headful
});

const controller = new ProgressController(internalCallMetadata(), context._browser);
Expand All @@ -122,16 +117,16 @@ export class RecorderApp extends EventEmitter {
}).toString(), true, mode, 'main').catch(() => {});
}

async setPaused(paused: boolean): Promise<void> {
await this._page.mainFrame()._evaluateExpression(((paused: boolean) => {
window.playwrightSetPaused(paused);
}).toString(), true, paused, 'main').catch(() => {});
async setPaused(details: PauseDetails | null): Promise<void> {
await this._page.mainFrame()._evaluateExpression(((details: PauseDetails | null) => {
window.playwrightSetPaused(details);
}).toString(), true, details, 'main').catch(() => {});
}

async setSource(text: string, language: string): Promise<void> {
await this._page.mainFrame()._evaluateExpression(((param: { text: string, language: string }) => {
window.playwrightSetSource(param);
}).toString(), true, { text, language }, 'main').catch(() => {});
async setSource(text: string, language: string, highlightedLine?: number): Promise<void> {
await this._page.mainFrame()._evaluateExpression(((source: Source) => {
window.playwrightSetSource(source);
}).toString(), true, { text, language, highlightedLine }, 'main').catch(() => {});

// Testing harness for runCLI mode.
{
Expand Down

0 comments on commit aef052a

Please sign in to comment.