mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: pause on input in pwdebug mode (#5427)
This commit is contained in:
parent
55614c7cc8
commit
aef052aecc
@ -23,7 +23,6 @@ import { CRBrowserContext } from '../server/chromium/crBrowser';
|
|||||||
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
|
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
|
||||||
import { RecorderSupplement } from '../server/supplements/recorderSupplement';
|
import { RecorderSupplement } from '../server/supplements/recorderSupplement';
|
||||||
import { CallMetadata } from '../server/instrumentation';
|
import { CallMetadata } from '../server/instrumentation';
|
||||||
import { isUnderTest } from '../utils/utils';
|
|
||||||
|
|
||||||
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
|
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
@ -132,11 +131,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||||||
await RecorderSupplement.getOrCreate(this._context, params);
|
await RecorderSupplement.getOrCreate(this._context, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause() {
|
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
||||||
if (!this._context._browser.options.headful && !isUnderTest())
|
// Inspector controller will take care of this.
|
||||||
return;
|
|
||||||
const recorder = await RecorderSupplement.getOrCreate(this._context);
|
|
||||||
await recorder.pause();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise<channels.BrowserContextCrNewCDPSessionResult> {
|
async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise<channels.BrowserContextCrNewCDPSessionResult> {
|
||||||
|
@ -190,6 +190,7 @@ export class DispatcherConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const callMetadata: CallMetadata = {
|
const callMetadata: CallMetadata = {
|
||||||
|
id,
|
||||||
...validMetadata,
|
...validMetadata,
|
||||||
startTime: monotonicTime(),
|
startTime: monotonicTime(),
|
||||||
endTime: 0,
|
endTime: 0,
|
||||||
@ -199,9 +200,10 @@ export class DispatcherConnection {
|
|||||||
log: [],
|
log: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sdkObject = dispatcher._object instanceof SdkObject ? dispatcher._object : undefined;
|
||||||
try {
|
try {
|
||||||
if (dispatcher instanceof SdkObject)
|
if (sdkObject)
|
||||||
await dispatcher.instrumentation.onBeforeCall(dispatcher, callMetadata);
|
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
|
||||||
const result = await (dispatcher as any)[method](validParams, callMetadata);
|
const result = await (dispatcher as any)[method](validParams, callMetadata);
|
||||||
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
|
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -210,8 +212,8 @@ export class DispatcherConnection {
|
|||||||
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote);
|
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote);
|
||||||
this.onmessage({ id, error: serializeError(e) });
|
this.onmessage({ id, error: serializeError(e) });
|
||||||
} finally {
|
} finally {
|
||||||
if (dispatcher instanceof SdkObject)
|
if (sdkObject)
|
||||||
await dispatcher.instrumentation.onAfterCall(dispatcher, callMetadata);
|
await sdkObject.instrumentation.onAfterCall(sdkObject, callMetadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,6 +382,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||||||
if (options && options.modifiers)
|
if (options && options.modifiers)
|
||||||
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
|
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
|
||||||
progress.log(` performing ${actionName} action`);
|
progress.log(` performing ${actionName} action`);
|
||||||
|
progress.metadata.point = point;
|
||||||
await progress.beforeInputAction();
|
await progress.beforeInputAction();
|
||||||
await action(point);
|
await action(point);
|
||||||
progress.log(` ${actionName} action done`);
|
progress.log(` ${actionName} action done`);
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { StackFrame } from '../common/types';
|
import { Point, StackFrame } from '../common/types';
|
||||||
import type { Browser } from './browser';
|
import type { Browser } from './browser';
|
||||||
import type { BrowserContext } from './browserContext';
|
import type { BrowserContext } from './browserContext';
|
||||||
import type { BrowserType } from './browserType';
|
import type { BrowserType } from './browserType';
|
||||||
@ -31,6 +31,7 @@ export type Attribution = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CallMetadata = {
|
export type CallMetadata = {
|
||||||
|
id: number;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
type: string;
|
type: string;
|
||||||
@ -39,6 +40,7 @@ export type CallMetadata = {
|
|||||||
stack?: StackFrame[];
|
stack?: StackFrame[];
|
||||||
log: string[];
|
log: string[];
|
||||||
error?: Error;
|
error?: Error;
|
||||||
|
point?: Point;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SdkObject extends EventEmitter {
|
export class SdkObject extends EventEmitter {
|
||||||
@ -92,6 +94,7 @@ export function multiplexInstrumentation(listeners: InstrumentationListener[]):
|
|||||||
|
|
||||||
export function internalCallMetadata(): CallMetadata {
|
export function internalCallMetadata(): CallMetadata {
|
||||||
return {
|
return {
|
||||||
|
id: 0,
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
endTime: 0,
|
endTime: 0,
|
||||||
type: 'Internal',
|
type: 'Internal',
|
||||||
|
@ -27,6 +27,7 @@ export interface Progress {
|
|||||||
throwIfAborted(): void;
|
throwIfAborted(): void;
|
||||||
beforeInputAction(): Promise<void>;
|
beforeInputAction(): Promise<void>;
|
||||||
afterInputAction(): Promise<void>;
|
afterInputAction(): Promise<void>;
|
||||||
|
metadata: CallMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProgressController {
|
export class ProgressController {
|
||||||
@ -92,6 +93,7 @@ export class ProgressController {
|
|||||||
afterInputAction: async () => {
|
afterInputAction: async () => {
|
||||||
await this.instrumentation.onAfterInputAction(this.sdkObject, this.metadata);
|
await this.instrumentation.onAfterInputAction(this.sdkObject, this.metadata);
|
||||||
},
|
},
|
||||||
|
metadata: this.metadata
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeoutError = new TimeoutError(`Timeout ${this._timeout}ms exceeded.`);
|
const timeoutError = new TimeoutError(`Timeout ${this._timeout}ms exceeded.`);
|
||||||
|
@ -16,20 +16,17 @@
|
|||||||
|
|
||||||
import type * as actions from '../recorder/recorderActions';
|
import type * as actions from '../recorder/recorderActions';
|
||||||
import type InjectedScript from '../../injected/injectedScript';
|
import type InjectedScript from '../../injected/injectedScript';
|
||||||
import { generateSelector } from './selectorGenerator';
|
import { generateSelector, querySelector } from './selectorGenerator';
|
||||||
import { html } from './html';
|
import { html } from './html';
|
||||||
|
import type { Point } from '../../../common/types';
|
||||||
type Mode = 'inspecting' | 'recording' | 'none';
|
import type { UIState } from '../recorder/recorderTypes';
|
||||||
type State = {
|
|
||||||
mode: Mode,
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
_playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
|
_playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
|
||||||
_playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
|
_playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
|
||||||
_playwrightRecorderCommitAction: () => Promise<void>;
|
_playwrightRecorderCommitAction: () => Promise<void>;
|
||||||
_playwrightRecorderState: () => Promise<State>;
|
_playwrightRecorderState: () => Promise<UIState>;
|
||||||
_playwrightRecorderPrintSelector: (text: string) => Promise<void>;
|
_playwrightRecorderPrintSelector: (text: string) => Promise<void>;
|
||||||
_playwrightResume: () => Promise<void>;
|
_playwrightResume: () => Promise<void>;
|
||||||
}
|
}
|
||||||
@ -52,6 +49,9 @@ export class Recorder {
|
|||||||
private _expectProgrammaticKeyUp = false;
|
private _expectProgrammaticKeyUp = false;
|
||||||
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
||||||
private _mode: 'none' | 'inspecting' | 'recording' = 'none';
|
private _mode: 'none' | 'inspecting' | 'recording' = 'none';
|
||||||
|
private _actionPointElement: HTMLElement;
|
||||||
|
private _actionPoint: Point | undefined;
|
||||||
|
private _actionSelector: string | undefined;
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript) {
|
constructor(injectedScript: InjectedScript) {
|
||||||
this._injectedScript = injectedScript;
|
this._injectedScript = injectedScript;
|
||||||
@ -69,6 +69,7 @@ export class Recorder {
|
|||||||
</x-pw-glass>`;
|
</x-pw-glass>`;
|
||||||
|
|
||||||
this._tooltipElement = html`<x-pw-tooltip></x-pw-tooltip>`;
|
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`
|
this._innerGlassPaneElement = html`
|
||||||
<x-pw-glass-inner style="flex: auto">
|
<x-pw-glass-inner style="flex: auto">
|
||||||
@ -78,6 +79,7 @@ export class Recorder {
|
|||||||
// Use a closed shadow root to prevent selectors matching our internal previews.
|
// Use a closed shadow root to prevent selectors matching our internal previews.
|
||||||
this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: 'closed' });
|
this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: 'closed' });
|
||||||
this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
|
this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
|
||||||
|
this._glassPaneShadow.appendChild(this._actionPointElement);
|
||||||
this._glassPaneShadow.appendChild(html`
|
this._glassPaneShadow.appendChild(html`
|
||||||
<style>
|
<style>
|
||||||
x-pw-tooltip {
|
x-pw-tooltip {
|
||||||
@ -103,6 +105,19 @@ export class Recorder {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
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>
|
</style>
|
||||||
`);
|
`);
|
||||||
this._refreshListenersIfNeeded();
|
this._refreshListenersIfNeeded();
|
||||||
@ -114,11 +129,6 @@ export class Recorder {
|
|||||||
this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console
|
this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setMode(mode: Mode): void {
|
|
||||||
this._clearHighlight();
|
|
||||||
this._mode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _refreshListenersIfNeeded() {
|
private _refreshListenersIfNeeded() {
|
||||||
if ((document.documentElement as any)[scriptSymbol])
|
if ((document.documentElement as any)[scriptSymbol])
|
||||||
return;
|
return;
|
||||||
@ -136,6 +146,7 @@ export class Recorder {
|
|||||||
addEventListener(document, 'focus', () => this._onFocus(), true),
|
addEventListener(document, 'focus', () => this._onFocus(), true),
|
||||||
addEventListener(document, 'scroll', () => {
|
addEventListener(document, 'scroll', () => {
|
||||||
this._hoveredModel = null;
|
this._hoveredModel = null;
|
||||||
|
this._actionPointElement.hidden = true;
|
||||||
this._updateHighlight();
|
this._updateHighlight();
|
||||||
}, true),
|
}, true),
|
||||||
];
|
];
|
||||||
@ -152,11 +163,35 @@ export class Recorder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { mode } = state;
|
const { mode, actionPoint, actionSelector } = state;
|
||||||
if (mode !== this._mode) {
|
if (mode !== this._mode) {
|
||||||
this._mode = mode;
|
this._mode = mode;
|
||||||
this._clearHighlight();
|
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);
|
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,14 @@ type SelectorToken = {
|
|||||||
const cacheAllowText = new Map<Element, SelectorToken[] | null>();
|
const cacheAllowText = new Map<Element, SelectorToken[] | null>();
|
||||||
const cacheDisallowText = 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[] } {
|
export function generateSelector(injectedScript: InjectedScript, targetElement: Element): { selector: string, elements: Element[] } {
|
||||||
injectedScript._evaluator.begin();
|
injectedScript._evaluator.begin();
|
||||||
try {
|
try {
|
||||||
|
@ -15,15 +15,51 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
import { isDebugMode } from '../../utils/utils';
|
|
||||||
import { RecorderSupplement } from './recorderSupplement';
|
import { RecorderSupplement } from './recorderSupplement';
|
||||||
import { InstrumentationListener } from '../instrumentation';
|
|
||||||
import { debugLogger } from '../../utils/debugLogger';
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
|
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
|
||||||
|
import { isDebugMode, isUnderTest } from '../../utils/utils';
|
||||||
|
|
||||||
export class InspectorController implements InstrumentationListener {
|
export class InspectorController implements InstrumentationListener {
|
||||||
|
private _recorders = new Map<BrowserContext, Promise<RecorderSupplement>>();
|
||||||
|
|
||||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||||
if (isDebugMode())
|
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 {
|
onCallLog(logName: string, message: string): void {
|
||||||
|
@ -23,22 +23,17 @@ import { ProgressController } from '../../progress';
|
|||||||
import { createPlaywright } from '../../playwright';
|
import { createPlaywright } from '../../playwright';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { internalCallMetadata } from '../../instrumentation';
|
import { internalCallMetadata } from '../../instrumentation';
|
||||||
import { isUnderTest } from '../../../utils/utils';
|
import type { EventData, Mode, PauseDetails, Source } from './recorderTypes';
|
||||||
import { BrowserContext } from '../../browserContext';
|
import { BrowserContext } from '../../browserContext';
|
||||||
|
import { isUnderTest } from '../../../utils/utils';
|
||||||
|
|
||||||
const readFileAsync = util.promisify(fs.readFile);
|
const readFileAsync = util.promisify(fs.readFile);
|
||||||
|
|
||||||
export type Mode = 'inspecting' | 'recording' | 'none';
|
|
||||||
export type EventData = {
|
|
||||||
event: 'clear' | 'resume' | 'setMode',
|
|
||||||
params: any
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
playwrightSetMode: (mode: Mode) => void;
|
playwrightSetMode: (mode: Mode) => void;
|
||||||
playwrightSetPaused: (paused: boolean) => void;
|
playwrightSetPaused: (details: PauseDetails | null) => void;
|
||||||
playwrightSetSource: (params: { text: string, language: string }) => void;
|
playwrightSetSource: (source: Source) => void;
|
||||||
dispatch(data: EventData): Promise<void>;
|
dispatch(data: EventData): Promise<void>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,7 +97,7 @@ export class RecorderApp extends EventEmitter {
|
|||||||
'--window-position=1280,10',
|
'--window-position=1280,10',
|
||||||
],
|
],
|
||||||
noDefaultViewport: true,
|
noDefaultViewport: true,
|
||||||
headless: isUnderTest()
|
headless: isUnderTest() && !inspectedContext._browser.options.headful
|
||||||
});
|
});
|
||||||
|
|
||||||
const controller = new ProgressController(internalCallMetadata(), context._browser);
|
const controller = new ProgressController(internalCallMetadata(), context._browser);
|
||||||
@ -122,16 +117,16 @@ export class RecorderApp extends EventEmitter {
|
|||||||
}).toString(), true, mode, 'main').catch(() => {});
|
}).toString(), true, mode, 'main').catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPaused(paused: boolean): Promise<void> {
|
async setPaused(details: PauseDetails | null): Promise<void> {
|
||||||
await this._page.mainFrame()._evaluateExpression(((paused: boolean) => {
|
await this._page.mainFrame()._evaluateExpression(((details: PauseDetails | null) => {
|
||||||
window.playwrightSetPaused(paused);
|
window.playwrightSetPaused(details);
|
||||||
}).toString(), true, paused, 'main').catch(() => {});
|
}).toString(), true, details, 'main').catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSource(text: string, language: string): Promise<void> {
|
async setSource(text: string, language: string, highlightedLine?: number): Promise<void> {
|
||||||
await this._page.mainFrame()._evaluateExpression(((param: { text: string, language: string }) => {
|
await this._page.mainFrame()._evaluateExpression(((source: Source) => {
|
||||||
window.playwrightSetSource(param);
|
window.playwrightSetSource(source);
|
||||||
}).toString(), true, { text, language }, 'main').catch(() => {});
|
}).toString(), true, { text, language, highlightedLine }, 'main').catch(() => {});
|
||||||
|
|
||||||
// Testing harness for runCLI mode.
|
// Testing harness for runCLI mode.
|
||||||
{
|
{
|
||||||
|
36
src/server/supplements/recorder/recorderTypes.ts
Normal file
36
src/server/supplements/recorder/recorderTypes.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Point } from '../../../common/types';
|
||||||
|
|
||||||
|
export type Mode = 'inspecting' | 'recording' | 'none';
|
||||||
|
|
||||||
|
export type EventData = {
|
||||||
|
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode',
|
||||||
|
params: any
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PauseDetails = {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Source = { text: string, language: string, highlightedLine?: number };
|
||||||
|
|
||||||
|
export type UIState = {
|
||||||
|
mode: Mode,
|
||||||
|
actionPoint?: Point,
|
||||||
|
actionSelector?: string
|
||||||
|
};
|
@ -14,6 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
import * as actions from './recorder/recorderActions';
|
import * as actions from './recorder/recorderActions';
|
||||||
import type * as channels from '../../protocol/channels';
|
import type * as channels from '../../protocol/channels';
|
||||||
import { CodeGenerator, ActionInContext } from './recorder/codeGenerator';
|
import { CodeGenerator, ActionInContext } from './recorder/codeGenerator';
|
||||||
@ -28,8 +29,10 @@ import { PythonLanguageGenerator } from './recorder/python';
|
|||||||
import * as recorderSource from '../../generated/recorderSource';
|
import * as recorderSource from '../../generated/recorderSource';
|
||||||
import * as consoleApiSource from '../../generated/consoleApiSource';
|
import * as consoleApiSource from '../../generated/consoleApiSource';
|
||||||
import { BufferedOutput, FileOutput, OutputMultiplexer, RecorderOutput } from './recorder/outputs';
|
import { BufferedOutput, FileOutput, OutputMultiplexer, RecorderOutput } from './recorder/outputs';
|
||||||
import { EventData, Mode, RecorderApp } from './recorder/recorderApp';
|
import { RecorderApp } from './recorder/recorderApp';
|
||||||
import { internalCallMetadata } from '../instrumentation';
|
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
|
||||||
|
import { Point } from '../../common/types';
|
||||||
|
import { EventData, Mode, PauseDetails, UIState } from './recorder/recorderTypes';
|
||||||
|
|
||||||
type BindingSource = { frame: Frame, page: Page };
|
type BindingSource = { frame: Frame, page: Page };
|
||||||
|
|
||||||
@ -44,12 +47,16 @@ export class RecorderSupplement {
|
|||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
private _resumeCallback: (() => void) | null = null;
|
private _resumeCallback: (() => void) | null = null;
|
||||||
private _mode: Mode;
|
private _mode: Mode;
|
||||||
private _paused = false;
|
private _pauseDetails: PauseDetails | null = null;
|
||||||
private _output: OutputMultiplexer;
|
private _output: OutputMultiplexer;
|
||||||
private _bufferedOutput: BufferedOutput;
|
private _bufferedOutput: BufferedOutput;
|
||||||
private _recorderApp: RecorderApp | null = null;
|
private _recorderApp: RecorderApp | null = null;
|
||||||
private _highlighterType: string;
|
private _highlighterType: string;
|
||||||
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
||||||
|
private _callMetadata: CallMetadata | null = null;
|
||||||
|
private _pauseOnNextStatement = true;
|
||||||
|
private _sourceCache = new Map<string, string>();
|
||||||
|
private _sdkObject: SdkObject | null = null;
|
||||||
|
|
||||||
static getOrCreate(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<RecorderSupplement> {
|
static getOrCreate(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>;
|
||||||
@ -78,13 +85,12 @@ export class RecorderSupplement {
|
|||||||
if (highlighterType === 'python-async')
|
if (highlighterType === 'python-async')
|
||||||
highlighterType = 'python';
|
highlighterType = 'python';
|
||||||
|
|
||||||
const outputs: RecorderOutput[] = [];
|
|
||||||
this._highlighterType = highlighterType;
|
this._highlighterType = highlighterType;
|
||||||
this._bufferedOutput = new BufferedOutput(async text => {
|
this._bufferedOutput = new BufferedOutput(async text => {
|
||||||
if (this._recorderApp)
|
if (this._recorderApp)
|
||||||
this._recorderApp.setSource(text, highlighterType);
|
this._recorderApp.setSource(text, highlighterType);
|
||||||
});
|
});
|
||||||
outputs.push(this._bufferedOutput);
|
const outputs: RecorderOutput[] = [ this._bufferedOutput ];
|
||||||
if (params.outputFile)
|
if (params.outputFile)
|
||||||
outputs.push(new FileOutput(params.outputFile));
|
outputs.push(new FileOutput(params.outputFile));
|
||||||
this._output = new OutputMultiplexer(outputs);
|
this._output = new OutputMultiplexer(outputs);
|
||||||
@ -110,8 +116,16 @@ export class RecorderSupplement {
|
|||||||
this._context.pages()[0].bringToFront().catch(() => {});
|
this._context.pages()[0].bringToFront().catch(() => {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (data.event === 'step') {
|
||||||
|
this._resume(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data.event === 'resume') {
|
if (data.event === 'resume') {
|
||||||
this._resume();
|
this._resume(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.event === 'pause') {
|
||||||
|
this._pauseOnNextStatement = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.event === 'clear') {
|
if (data.event === 'clear') {
|
||||||
@ -122,7 +136,7 @@ export class RecorderSupplement {
|
|||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
recorderApp.setMode(this._mode),
|
recorderApp.setMode(this._mode),
|
||||||
recorderApp.setPaused(this._paused),
|
recorderApp.setPaused(this._pauseDetails),
|
||||||
recorderApp.setSource(this._bufferedOutput.buffer(), this._highlighterType)
|
recorderApp.setSource(this._bufferedOutput.buffer(), this._highlighterType)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -150,12 +164,19 @@ export class RecorderSupplement {
|
|||||||
await this._context.exposeBinding('_playwrightRecorderCommitAction', false,
|
await this._context.exposeBinding('_playwrightRecorderCommitAction', false,
|
||||||
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());
|
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());
|
||||||
|
|
||||||
await this._context.exposeBinding('_playwrightRecorderState', false, () => {
|
await this._context.exposeBinding('_playwrightRecorderState', false, source => {
|
||||||
return { mode: this._mode };
|
let actionPoint: Point | undefined = undefined;
|
||||||
|
let actionSelector: string | undefined = undefined;
|
||||||
|
if (source.page === this._sdkObject?.attribution?.page) {
|
||||||
|
actionPoint = this._callMetadata?.point;
|
||||||
|
actionSelector = this._callMetadata?.params.selector;
|
||||||
|
}
|
||||||
|
const uiState: UIState = { mode: this._mode, actionPoint, actionSelector };
|
||||||
|
return uiState;
|
||||||
});
|
});
|
||||||
|
|
||||||
await this._context.exposeBinding('_playwrightResume', false, () => {
|
await this._context.exposeBinding('_playwrightResume', false, () => {
|
||||||
this._resume().catch(() => {});
|
this._resume(false).catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
await this._context.extendInjectedScript(recorderSource.source);
|
await this._context.extendInjectedScript(recorderSource.source);
|
||||||
@ -165,18 +186,18 @@ export class RecorderSupplement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async pause() {
|
async pause() {
|
||||||
this._paused = true;
|
this._pauseDetails = { message: 'paused' };
|
||||||
this._recorderApp!.setPaused(true);
|
this._recorderApp!.setPaused(this._pauseDetails);
|
||||||
return new Promise<void>(f => this._resumeCallback = f);
|
return new Promise<void>(f => this._resumeCallback = f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _resume() {
|
private async _resume(step: boolean) {
|
||||||
|
this._pauseOnNextStatement = step;
|
||||||
if (this._resumeCallback)
|
if (this._resumeCallback)
|
||||||
this._resumeCallback();
|
this._resumeCallback();
|
||||||
this._resumeCallback = null;
|
this._resumeCallback = null;
|
||||||
this._paused = false;
|
this._pauseDetails = null;
|
||||||
if (this._recorderApp)
|
this._recorderApp?.setPaused(null);
|
||||||
this._recorderApp.setPaused(this._paused);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _onPage(page: Page) {
|
private async _onPage(page: Page) {
|
||||||
@ -294,4 +315,50 @@ export class RecorderSupplement {
|
|||||||
const pageAlias = this._pageAliases.get(page)!;
|
const pageAlias = this._pageAliases.get(page)!;
|
||||||
this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) });
|
this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||||
|
this._sdkObject = sdkObject;
|
||||||
|
this._callMetadata = metadata;
|
||||||
|
const { source, line } = this._source(metadata);
|
||||||
|
this._recorderApp?.setSource(source, 'javascript', line);
|
||||||
|
if (metadata.method === 'pause' || (this._pauseOnNextStatement && metadata.method === 'goto'))
|
||||||
|
await this.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||||
|
this._sdkObject = null;
|
||||||
|
this._callMetadata = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||||
|
if (this._pauseOnNextStatement)
|
||||||
|
await this.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _source(metadata: CallMetadata): { source: string, line: number | undefined } {
|
||||||
|
let source = '// No source available';
|
||||||
|
let line: number | undefined = undefined;
|
||||||
|
if (metadata.stack && metadata.stack.length) {
|
||||||
|
try {
|
||||||
|
source = this._readAndCacheSource(metadata.stack[0].file);
|
||||||
|
line = metadata.stack[0].line ? metadata.stack[0].line - 1 : undefined;
|
||||||
|
} catch (e) {
|
||||||
|
source = metadata.stack.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { source, line };
|
||||||
|
}
|
||||||
|
|
||||||
|
private _readAndCacheSource(fileName: string): string {
|
||||||
|
let source = this._sourceCache.get(fileName);
|
||||||
|
if (source)
|
||||||
|
return source;
|
||||||
|
try {
|
||||||
|
source = fs.readFileSync(fileName, 'utf-8');
|
||||||
|
} catch (e) {
|
||||||
|
source = '// No source available';
|
||||||
|
}
|
||||||
|
this._sourceCache.set(fileName, source);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { StackFrame } from '../common/types';
|
import { StackFrame } from '../common/types';
|
||||||
import StackUtils from 'stack-utils';
|
import StackUtils from 'stack-utils';
|
||||||
|
import { isUnderTest } from './utils';
|
||||||
|
|
||||||
const stackUtils = new StackUtils();
|
const stackUtils = new StackUtils();
|
||||||
|
|
||||||
@ -50,6 +51,8 @@ export function captureStackTrace(): { stack: string, frames: StackFrame[] } {
|
|||||||
// for tests.
|
// for tests.
|
||||||
if (fileName.includes(path.join('playwright', 'src')))
|
if (fileName.includes(path.join('playwright', 'src')))
|
||||||
continue;
|
continue;
|
||||||
|
if (isUnderTest() && fileName.includes(path.join('playwright', 'test', 'coverage.js')))
|
||||||
|
continue;
|
||||||
frames.push({
|
frames.push({
|
||||||
file: fileName,
|
file: fileName,
|
||||||
line: frame.line,
|
line: frame.line,
|
||||||
|
@ -45,5 +45,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.source-line-highlighted {
|
.source-line-highlighted {
|
||||||
background-color: #ffc0cb7f;
|
background-color: #6fa8dc7f;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-line-paused {
|
||||||
|
background-color: #ffc0cb7f;
|
||||||
|
outline: 1px solid red;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
@ -22,12 +22,14 @@ import '../../third_party/highlightjs/highlightjs/tomorrow.css';
|
|||||||
export interface SourceProps {
|
export interface SourceProps {
|
||||||
text: string,
|
text: string,
|
||||||
language: string,
|
language: string,
|
||||||
highlightedLine?: number
|
highlightedLine?: number,
|
||||||
|
paused?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Source: React.FC<SourceProps> = ({
|
export const Source: React.FC<SourceProps> = ({
|
||||||
text,
|
text,
|
||||||
language,
|
language,
|
||||||
|
paused = false,
|
||||||
highlightedLine = -1
|
highlightedLine = -1
|
||||||
}) => {
|
}) => {
|
||||||
const lines = React.useMemo<string[]>(() => {
|
const lines = React.useMemo<string[]>(() => {
|
||||||
@ -51,7 +53,8 @@ export const Source: React.FC<SourceProps> = ({
|
|||||||
return <div className='source'>{
|
return <div className='source'>{
|
||||||
lines.map((markup, index) => {
|
lines.map((markup, index) => {
|
||||||
const isHighlighted = index === highlightedLine;
|
const isHighlighted = index === highlightedLine;
|
||||||
const className = isHighlighted ? 'source-line source-line-highlighted' : 'source-line';
|
const highlightType = paused && isHighlighted ? 'source-line-paused' : 'source-line-highlighted';
|
||||||
|
const className = isHighlighted ? `source-line ${highlightType}` : 'source-line';
|
||||||
return <div key={index} className={className} ref={isHighlighted ? highlightedLineRef : null}>
|
return <div key={index} className={className} ref={isHighlighted ? highlightedLineRef : null}>
|
||||||
<div className='source-line-number'>{index + 1}</div>
|
<div className='source-line-number'>{index + 1}</div>
|
||||||
<div className='source-code' dangerouslySetInnerHTML={{ __html: markup }}></div>
|
<div className='source-code' dangerouslySetInnerHTML={{ __html: markup }}></div>
|
||||||
|
@ -41,10 +41,12 @@
|
|||||||
color: #fd1e1e;
|
color: #fd1e1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-button.codicon-run {
|
.toolbar-button.codicon-debug-continue,
|
||||||
color: #4bfd1e;
|
.toolbar-button.codicon-debug-step-over {
|
||||||
|
color: #01bb01;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-button.codicon-run:hover {
|
.toolbar-button.codicon-debug-continue:hover,
|
||||||
color: #0f0;
|
.toolbar-button.codicon-debug-step-over:hover {
|
||||||
|
color: #41ca1e;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
height: 24px;
|
line-height: 24px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex: none;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -18,15 +18,14 @@ import './recorder.css';
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Toolbar } from '../components/toolbar';
|
import { Toolbar } from '../components/toolbar';
|
||||||
import { ToolbarButton } from '../components/toolbarButton';
|
import { ToolbarButton } from '../components/toolbarButton';
|
||||||
import { Source } from '../components/source';
|
import { Source as SourceView } from '../components/source';
|
||||||
|
import type { Mode, PauseDetails, Source } from '../../server/supplements/recorder/recorderTypes';
|
||||||
type Mode = 'inspecting' | 'recording' | 'none';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
playwrightSetMode: (mode: Mode) => void;
|
playwrightSetMode: (mode: Mode) => void;
|
||||||
playwrightSetPaused: (paused: boolean) => void;
|
playwrightSetPaused: (details: PauseDetails | null) => void;
|
||||||
playwrightSetSource: (params: { text: string, language: string }) => void;
|
playwrightSetSource: (source: Source) => void;
|
||||||
dispatch(data: any): Promise<void>;
|
dispatch(data: any): Promise<void>;
|
||||||
playwrightSourceEchoForTest?: (text: string) => Promise<void>;
|
playwrightSourceEchoForTest?: (text: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
@ -37,8 +36,8 @@ export interface RecorderProps {
|
|||||||
|
|
||||||
export const Recorder: React.FC<RecorderProps> = ({
|
export const Recorder: React.FC<RecorderProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [source, setSource] = React.useState({ language: 'javascript', text: '' });
|
const [source, setSource] = React.useState<Source>({ language: 'javascript', text: '' });
|
||||||
const [paused, setPaused] = React.useState(false);
|
const [paused, setPaused] = React.useState<PauseDetails | null>(null);
|
||||||
const [mode, setMode] = React.useState<Mode>('none');
|
const [mode, setMode] = React.useState<Mode>('none');
|
||||||
|
|
||||||
window.playwrightSetMode = setMode;
|
window.playwrightSetMode = setMode;
|
||||||
@ -58,19 +57,21 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||||||
<ToolbarButton icon="files" title="Copy" disabled={!source.text} onClick={() => {
|
<ToolbarButton icon="files" title="Copy" disabled={!source.text} onClick={() => {
|
||||||
copy(source.text);
|
copy(source.text);
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
|
<ToolbarButton icon="debug-continue" title="Resume" disabled={!paused} onClick={() => {
|
||||||
|
window.dispatch({ event: 'resume' }).catch(() => {});
|
||||||
|
}}></ToolbarButton>
|
||||||
|
<ToolbarButton icon="debug-pause" title="Pause" disabled={!!paused} onClick={() => {
|
||||||
|
window.dispatch({ event: 'pause' }).catch(() => {});
|
||||||
|
}}></ToolbarButton>
|
||||||
|
<ToolbarButton icon="debug-step-over" title="Step over" disabled={!paused} onClick={() => {
|
||||||
|
window.dispatch({ event: 'step' }).catch(() => {});
|
||||||
|
}}></ToolbarButton>
|
||||||
<div style={{flex: "auto"}}></div>
|
<div style={{flex: "auto"}}></div>
|
||||||
<ToolbarButton icon="clear-all" title="Clear" disabled={!source.text} onClick={() => {
|
<ToolbarButton icon="clear-all" title="Clear" disabled={!source.text} onClick={() => {
|
||||||
window.dispatch({ event: 'clear' }).catch(() => {});
|
window.dispatch({ event: 'clear' }).catch(() => {});
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<div className="recorder-paused-infobar" hidden={!paused}>
|
<SourceView text={source.text} language={source.language} highlightedLine={source.highlightedLine} paused={!!paused}></SourceView>
|
||||||
<ToolbarButton icon="run" title="Resume" disabled={!paused} onClick={() => {
|
|
||||||
window.dispatch({ event: 'resume' }).catch(() => {});
|
|
||||||
}}></ToolbarButton>
|
|
||||||
<span style={{paddingLeft: 10}}>Paused due to <span className="code">page.pause()</span></span>
|
|
||||||
<div style={{flex: "auto"}}></div>
|
|
||||||
</div>
|
|
||||||
<Source text={source.text} language={source.language}></Source>
|
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,8 +14,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { expect } from 'folio';
|
||||||
import { folio } from './recorder.fixtures';
|
import { folio } from './recorder.fixtures';
|
||||||
const { it, expect, describe} = folio;
|
const { it, describe} = folio;
|
||||||
|
|
||||||
describe('pause', (suite, { mode }) => {
|
describe('pause', (suite, { mode }) => {
|
||||||
suite.skip(mode !== 'default');
|
suite.skip(mode !== 'default');
|
||||||
@ -36,16 +37,6 @@ describe('pause', (suite, { mode }) => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pause through a navigation', async ({page, server, recorderClick}) => {
|
|
||||||
let resolved = false;
|
|
||||||
const resumePromise = page.pause().then(() => resolved = true);
|
|
||||||
expect(resolved).toBe(false);
|
|
||||||
await page.goto(server.EMPTY_PAGE);
|
|
||||||
await recorderClick('[title=Resume]');
|
|
||||||
await resumePromise;
|
|
||||||
expect(resolved).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pause after a navigation', async ({page, server, recorderClick}) => {
|
it('should pause after a navigation', async ({page, server, recorderClick}) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -53,4 +44,16 @@ describe('pause', (suite, { mode }) => {
|
|||||||
recorderClick('[title=Resume]')
|
recorderClick('[title=Resume]')
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show source', async ({page, server, recorderClick, recorderFrame}) => {
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
const pausePromise = page.pause();
|
||||||
|
const frame = await recorderFrame();
|
||||||
|
const source = await frame._evaluateExpression((() => {
|
||||||
|
return document.querySelector('.source-line-paused .source-code').textContent;
|
||||||
|
}).toString(), true, undefined, 'main');
|
||||||
|
expect(source).toContain('page.pause()');
|
||||||
|
await recorderClick('[title=Resume]');
|
||||||
|
await pausePromise;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -155,7 +155,8 @@ DEPS['src/service.ts'] = ['src/remote/'];
|
|||||||
// CLI should only use client-side features.
|
// CLI should only use client-side features.
|
||||||
DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/', 'src/trace/**', 'src/utils/**'];
|
DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/', 'src/trace/**', 'src/utils/**'];
|
||||||
|
|
||||||
DEPS['src/server/supplements/recorder/recorderApp.ts'] = ['src/utils/', 'src/server/', 'src/server/chromium/'];
|
DEPS['src/server/supplements/recorder/recorderApp.ts'] = ['src/common/', 'src/utils/', 'src/server/', 'src/server/chromium/'];
|
||||||
|
DEPS['src/web/recorder/recorder.tsx'] = ['src/server/supplements/recorder/recorderTypes.ts'];
|
||||||
DEPS['src/utils/'] = ['src/common/'];
|
DEPS['src/utils/'] = ['src/common/'];
|
||||||
|
|
||||||
checkDeps().catch(e => {
|
checkDeps().catch(e => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user