mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(inspector): render errors (#5459)
This commit is contained in:
parent
ae2ffb3fb9
commit
8b9a2afd3d
1
index.js
1
index.js
@ -14,5 +14,4 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { setUnderTest } = require('./lib/utils/utils');
|
|
||||||
module.exports = require('./lib/inprocess');
|
module.exports = require('./lib/inprocess');
|
||||||
|
@ -214,6 +214,7 @@ export class DispatcherConnection {
|
|||||||
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
|
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Dispatching error
|
// Dispatching error
|
||||||
|
callMetadata.error = e.message;
|
||||||
if (callMetadata.log.length)
|
if (callMetadata.log.length)
|
||||||
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) });
|
||||||
|
@ -345,8 +345,8 @@ export abstract class BrowserContext extends SdkObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async extendInjectedScript(source: string) {
|
async extendInjectedScript(source: string, arg?: any) {
|
||||||
const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source).catch(e => {});
|
const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source, arg).catch(() => {});
|
||||||
const installInPage = (page: Page) => {
|
const installInPage = (page: Page) => {
|
||||||
page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame);
|
page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame);
|
||||||
return Promise.all(page.frames().map(installInFrame));
|
return Promise.all(page.frames().map(installInFrame));
|
||||||
|
@ -39,7 +39,7 @@ export type CallMetadata = {
|
|||||||
params: any;
|
params: any;
|
||||||
stack?: StackFrame[];
|
stack?: StackFrame[];
|
||||||
log: string[];
|
log: string[];
|
||||||
error?: Error;
|
error?: string;
|
||||||
point?: Point;
|
point?: Point;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,8 +51,10 @@ export class Recorder {
|
|||||||
private _actionPointElement: HTMLElement;
|
private _actionPointElement: HTMLElement;
|
||||||
private _actionPoint: Point | undefined;
|
private _actionPoint: Point | undefined;
|
||||||
private _actionSelector: string | undefined;
|
private _actionSelector: string | undefined;
|
||||||
|
private _params: { isUnderTest: boolean; };
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript) {
|
constructor(injectedScript: InjectedScript, params: { isUnderTest: boolean }) {
|
||||||
|
this._params = params;
|
||||||
this._injectedScript = injectedScript;
|
this._injectedScript = injectedScript;
|
||||||
this._outerGlassPaneElement = html`
|
this._outerGlassPaneElement = html`
|
||||||
<x-pw-glass style="
|
<x-pw-glass style="
|
||||||
@ -76,7 +78,7 @@ export class Recorder {
|
|||||||
</x-pw-glass-inner>`;
|
</x-pw-glass-inner>`;
|
||||||
|
|
||||||
// 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: this._params.isUnderTest ? 'open' : 'closed' });
|
||||||
this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
|
this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
|
||||||
this._glassPaneShadow.appendChild(this._actionPointElement);
|
this._glassPaneShadow.appendChild(this._actionPointElement);
|
||||||
this._glassPaneShadow.appendChild(html`
|
this._glassPaneShadow.appendChild(html`
|
||||||
|
@ -26,6 +26,7 @@ import { internalCallMetadata } from '../../instrumentation';
|
|||||||
import type { CallLog, EventData, Mode, Source } from './recorderTypes';
|
import type { CallLog, EventData, Mode, Source } from './recorderTypes';
|
||||||
import { BrowserContext } from '../../browserContext';
|
import { BrowserContext } from '../../browserContext';
|
||||||
import { isUnderTest } from '../../../utils/utils';
|
import { isUnderTest } from '../../../utils/utils';
|
||||||
|
import { RecentLogsCollector } from '../../../utils/debugLogger';
|
||||||
|
|
||||||
const readFileAsync = util.promisify(fs.readFile);
|
const readFileAsync = util.promisify(fs.readFile);
|
||||||
|
|
||||||
@ -41,11 +42,13 @@ declare global {
|
|||||||
|
|
||||||
export class RecorderApp extends EventEmitter {
|
export class RecorderApp extends EventEmitter {
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
|
readonly wsEndpoint: string | undefined;
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page, wsEndpoint: string | undefined) {
|
||||||
super();
|
super();
|
||||||
this.setMaxListeners(0);
|
this.setMaxListeners(0);
|
||||||
this._page = page;
|
this._page = page;
|
||||||
|
this.wsEndpoint = wsEndpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
@ -90,28 +93,45 @@ export class RecorderApp extends EventEmitter {
|
|||||||
|
|
||||||
static async open(inspectedContext: BrowserContext): Promise<RecorderApp> {
|
static async open(inspectedContext: BrowserContext): Promise<RecorderApp> {
|
||||||
const recorderPlaywright = createPlaywright(true);
|
const recorderPlaywright = createPlaywright(true);
|
||||||
const context = await recorderPlaywright.chromium.launchPersistentContext(internalCallMetadata(), '', {
|
const args = [
|
||||||
sdkLanguage: inspectedContext._options.sdkLanguage,
|
|
||||||
args: [
|
|
||||||
'--app=data:text/html,',
|
'--app=data:text/html,',
|
||||||
'--window-size=600,600',
|
'--window-size=600,600',
|
||||||
'--window-position=1280,10',
|
'--window-position=1280,10',
|
||||||
],
|
];
|
||||||
|
if (isUnderTest())
|
||||||
|
args.push(`--remote-debugging-port=0`);
|
||||||
|
const context = await recorderPlaywright.chromium.launchPersistentContext(internalCallMetadata(), '', {
|
||||||
|
sdkLanguage: inspectedContext._options.sdkLanguage,
|
||||||
|
args,
|
||||||
noDefaultViewport: true,
|
noDefaultViewport: true,
|
||||||
headless: isUnderTest() && !inspectedContext._browser.options.headful
|
headless: isUnderTest() && !inspectedContext._browser.options.headful
|
||||||
});
|
});
|
||||||
|
const wsEndpoint = isUnderTest() ? await this._parseWsEndpoint(context._browser.options.browserLogsCollector) : undefined;
|
||||||
const controller = new ProgressController(internalCallMetadata(), context._browser);
|
const controller = new ProgressController(internalCallMetadata(), context._browser);
|
||||||
await controller.run(async progress => {
|
await controller.run(async progress => {
|
||||||
await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
|
await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [page] = context.pages();
|
const [page] = context.pages();
|
||||||
const result = new RecorderApp(page);
|
const result = new RecorderApp(page, wsEndpoint);
|
||||||
await result._init();
|
await result._init();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async _parseWsEndpoint(recentLogs: RecentLogsCollector): Promise<string> {
|
||||||
|
let callback: ((log: string) => void) | undefined;
|
||||||
|
const result = new Promise<string>(f => callback = f);
|
||||||
|
const check = (log: string) => {
|
||||||
|
const match = log.match(/DevTools listening on (.*)/);
|
||||||
|
if (match)
|
||||||
|
callback!(match[1]);
|
||||||
|
};
|
||||||
|
for (const log of recentLogs.recentLogs())
|
||||||
|
check(log);
|
||||||
|
recentLogs.on('log', check);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void> {
|
async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void> {
|
||||||
await this._page.mainFrame()._evaluateExpression(((mode: Mode) => {
|
await this._page.mainFrame()._evaluateExpression(((mode: Mode) => {
|
||||||
window.playwrightSetMode(mode);
|
window.playwrightSetMode(mode);
|
||||||
|
@ -34,11 +34,12 @@ export type CallLog = {
|
|||||||
title: string;
|
title: string;
|
||||||
messages: string[];
|
messages: string[];
|
||||||
status: 'in-progress' | 'done' | 'error' | 'paused';
|
status: 'in-progress' | 'done' | 'error' | 'paused';
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SourceHighlight = {
|
export type SourceHighlight = {
|
||||||
line: number;
|
line: number;
|
||||||
type: 'running' | 'paused';
|
type: 'running' | 'paused' | 'error';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Source = {
|
export type Source = {
|
||||||
|
@ -33,6 +33,7 @@ import { RecorderApp } from './recorder/recorderApp';
|
|||||||
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
|
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
|
||||||
import { Point } from '../../common/types';
|
import { Point } from '../../common/types';
|
||||||
import { CallLog, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
|
import { CallLog, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
|
||||||
|
import { isUnderTest } from '../../utils/utils';
|
||||||
|
|
||||||
type BindingSource = { frame: Frame, page: Page };
|
type BindingSource = { frame: Frame, page: Page };
|
||||||
|
|
||||||
@ -179,7 +180,7 @@ export class RecorderSupplement {
|
|||||||
this._resume(false).catch(() => {});
|
this._resume(false).catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
await this._context.extendInjectedScript(recorderSource.source);
|
await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest() });
|
||||||
await this._context.extendInjectedScript(consoleApiSource.source);
|
await this._context.extendInjectedScript(consoleApiSource.source);
|
||||||
|
|
||||||
(this._context as any).recorderAppForTest = recorderApp;
|
(this._context as any).recorderAppForTest = recorderApp;
|
||||||
@ -332,6 +333,7 @@ export class RecorderSupplement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onAfterCall(metadata: CallMetadata): Promise<void> {
|
async onAfterCall(metadata: CallMetadata): Promise<void> {
|
||||||
|
if (!metadata.error)
|
||||||
this._currentCallsMetadata.delete(metadata);
|
this._currentCallsMetadata.delete(metadata);
|
||||||
this._pausedCallsMetadata.delete(metadata);
|
this._pausedCallsMetadata.delete(metadata);
|
||||||
this._updateUserSources();
|
this._updateUserSources();
|
||||||
@ -357,7 +359,7 @@ export class RecorderSupplement {
|
|||||||
}
|
}
|
||||||
if (line) {
|
if (line) {
|
||||||
const paused = this._pausedCallsMetadata.has(metadata);
|
const paused = this._pausedCallsMetadata.has(metadata);
|
||||||
source.highlight.push({ line, type: paused ? 'paused' : 'running' });
|
source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
|
||||||
if (paused)
|
if (paused)
|
||||||
source.revealLine = line;
|
source.revealLine = line;
|
||||||
}
|
}
|
||||||
@ -387,7 +389,7 @@ export class RecorderSupplement {
|
|||||||
status = 'paused';
|
status = 'paused';
|
||||||
if (metadata.error)
|
if (metadata.error)
|
||||||
status = 'error';
|
status = 'error';
|
||||||
logs.push({ id: metadata.id, messages: metadata.log, title, status });
|
logs.push({ id: metadata.id, messages: metadata.log, title, status, error: metadata.error });
|
||||||
}
|
}
|
||||||
this._recorderApp?.updateCallLogs(logs);
|
this._recorderApp?.updateCallLogs(logs);
|
||||||
}
|
}
|
||||||
|
@ -180,7 +180,7 @@ class ContextTracer implements SnapshotterDelegate {
|
|||||||
startTime: metadata.startTime,
|
startTime: metadata.startTime,
|
||||||
endTime: metadata.endTime,
|
endTime: metadata.endTime,
|
||||||
logs: metadata.log.slice(),
|
logs: metadata.log.slice(),
|
||||||
error: metadata.error ? metadata.error.stack : undefined,
|
error: metadata.error,
|
||||||
snapshots: snapshotsForMetadata(metadata),
|
snapshots: snapshotsForMetadata(metadata),
|
||||||
};
|
};
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
const debugLoggerColorMap = {
|
const debugLoggerColorMap = {
|
||||||
'api': 45, // cyan
|
'api': 45, // cyan
|
||||||
@ -63,10 +64,11 @@ class DebugLogger {
|
|||||||
export const debugLogger = new DebugLogger();
|
export const debugLogger = new DebugLogger();
|
||||||
|
|
||||||
const kLogCount = 50;
|
const kLogCount = 50;
|
||||||
export class RecentLogsCollector {
|
export class RecentLogsCollector extends EventEmitter {
|
||||||
private _logs: string[] = [];
|
private _logs: string[] = [];
|
||||||
|
|
||||||
log(message: string) {
|
log(message: string) {
|
||||||
|
this.emit('log', message);
|
||||||
this._logs.push(message);
|
this._logs.push(message);
|
||||||
if (this._logs.length === kLogCount * 2)
|
if (this._logs.length === kLogCount * 2)
|
||||||
this._logs.splice(0, kLogCount);
|
this._logs.splice(0, kLogCount);
|
||||||
|
@ -45,12 +45,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.source-line-running {
|
.source-line-running {
|
||||||
background-color: #6fa8dc7f;
|
background-color: #b3dbff7f;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-line-paused {
|
.source-line-paused {
|
||||||
background-color: #ffc0cb7f;
|
background-color: #b3dbff7f;
|
||||||
outline: 1px solid red;
|
outline: 1px solid #009aff;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-line-error {
|
||||||
|
background-color: #fff0f0;
|
||||||
|
outline: 1px solid #ffd6d6;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import '../../third_party/highlightjs/highlightjs/tomorrow.css';
|
|||||||
|
|
||||||
export type SourceHighlight = {
|
export type SourceHighlight = {
|
||||||
line: number;
|
line: number;
|
||||||
type: 'running' | 'paused';
|
type: 'running' | 'paused' | 'error';
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SourceProps {
|
export interface SourceProps {
|
||||||
|
@ -42,15 +42,11 @@
|
|||||||
|
|
||||||
.recorder-log-message {
|
.recorder-log-message {
|
||||||
flex: none;
|
flex: none;
|
||||||
padding: 3px 12px;
|
padding: 3px 0 3px 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recorder-log-message-sub-level {
|
|
||||||
padding-left: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder-log-header {
|
.recorder-log-header {
|
||||||
color: var(--toolbar-color);
|
color: var(--toolbar-color);
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
@ -63,18 +59,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.recorder-log-call {
|
.recorder-log-call {
|
||||||
color: var(--toolbar-color);
|
display: flex;
|
||||||
background-color: var(--toolbar-bg-color);
|
flex: none;
|
||||||
border-top: 1px solid #ddd;
|
flex-direction: column;
|
||||||
border-bottom: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recorder-log-call-header {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 9px;
|
padding: 0 2px;
|
||||||
margin-bottom: 3px;
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recorder-log-call .codicon {
|
.recorder-log-call .codicon {
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recorder-log .codicon-check {
|
||||||
|
color: #21a945;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recorder-log-call.error {
|
||||||
|
background-color: #fff0f0;
|
||||||
|
border-top: 1px solid #ffd6d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recorder-log-call.error .recorder-log-call-header,
|
||||||
|
.recorder-log-message.error,
|
||||||
|
.recorder-log .codicon-error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
@ -79,12 +79,14 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||||||
copy(source.text);
|
copy(source.text);
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<ToolbarButton icon='debug-continue' title='Resume' disabled={!paused} onClick={() => {
|
<ToolbarButton icon='debug-continue' title='Resume' disabled={!paused} onClick={() => {
|
||||||
|
setPaused(false);
|
||||||
window.dispatch({ event: 'resume' }).catch(() => {});
|
window.dispatch({ event: 'resume' }).catch(() => {});
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<ToolbarButton icon='debug-pause' title='Pause' disabled={paused} onClick={() => {
|
<ToolbarButton icon='debug-pause' title='Pause' disabled={paused} onClick={() => {
|
||||||
window.dispatch({ event: 'pause' }).catch(() => {});
|
window.dispatch({ event: 'pause' }).catch(() => {});
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<ToolbarButton icon='debug-step-over' title='Step over' disabled={!paused} onClick={() => {
|
<ToolbarButton icon='debug-step-over' title='Step over' disabled={!paused} onClick={() => {
|
||||||
|
setPaused(false);
|
||||||
window.dispatch({ event: 'step' }).catch(() => {});
|
window.dispatch({ event: 'step' }).catch(() => {});
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<div style={{flex: 'auto'}}></div>
|
<div style={{flex: 'auto'}}></div>
|
||||||
@ -98,15 +100,18 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||||||
<div className='recorder-log-header' style={{flex: 'none'}}>Log</div>
|
<div className='recorder-log-header' style={{flex: 'none'}}>Log</div>
|
||||||
<div className='recorder-log' style={{flex: 'auto'}}>
|
<div className='recorder-log' style={{flex: 'auto'}}>
|
||||||
{[...log.values()].map(callLog => {
|
{[...log.values()].map(callLog => {
|
||||||
return <div className='vbox' style={{flex: 'none'}} key={callLog.id}>
|
return <div className={`recorder-log-call ${callLog.status}`} key={callLog.id}>
|
||||||
<div className='recorder-log-call'>
|
<div className='recorder-log-call-header'>
|
||||||
<span className={'codicon ' + iconClass(callLog)}></span>{ callLog.title }
|
<span className={'codicon ' + iconClass(callLog)}></span>{ callLog.title }
|
||||||
</div>
|
</div>
|
||||||
{ callLog.messages.map((message, i) => {
|
{ callLog.messages.map((message, i) => {
|
||||||
return <div className='recorder-log-message' key={i}>
|
return <div className='recorder-log-message' key={i}>
|
||||||
{ message }
|
{ message.trim() }
|
||||||
</div>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
|
{ callLog.error ? <div className='recorder-log-message error'>
|
||||||
|
{ callLog.error }
|
||||||
|
</div> : undefined }
|
||||||
</div>
|
</div>
|
||||||
})}
|
})}
|
||||||
<div ref={messagesEndRef}></div>
|
<div ref={messagesEndRef}></div>
|
||||||
|
@ -34,10 +34,10 @@ type TestFixtures = {
|
|||||||
|
|
||||||
export const fixtures = baseFolio.extend<TestFixtures, WorkerFixtures>();
|
export const fixtures = baseFolio.extend<TestFixtures, WorkerFixtures>();
|
||||||
|
|
||||||
fixtures.recorder.init(async ({ page, recorderFrame }, runTest) => {
|
fixtures.recorder.init(async ({ page, recorderPageGetter }, runTest) => {
|
||||||
await (page.context() as any)._enableRecorder({ language: 'javascript', startRecording: true });
|
await (page.context() as any)._enableRecorder({ language: 'javascript', startRecording: true });
|
||||||
const recorderFrameInstance = await recorderFrame();
|
const recorderPage = await recorderPageGetter();
|
||||||
await runTest(new Recorder(page, recorderFrameInstance));
|
await runTest(new Recorder(page, recorderPage));
|
||||||
});
|
});
|
||||||
|
|
||||||
fixtures.httpServer.init(async ({testWorkerIndex}, runTest) => {
|
fixtures.httpServer.init(async ({testWorkerIndex}, runTest) => {
|
||||||
@ -65,12 +65,12 @@ class Recorder {
|
|||||||
_highlightInstalled: boolean
|
_highlightInstalled: boolean
|
||||||
_actionReporterInstalled: boolean
|
_actionReporterInstalled: boolean
|
||||||
_actionPerformedCallback: Function
|
_actionPerformedCallback: Function
|
||||||
recorderFrame: any;
|
recorderPage: Page;
|
||||||
private _text: string = '';
|
private _text: string = '';
|
||||||
|
|
||||||
constructor(page: Page, recorderFrame: any) {
|
constructor(page: Page, recorderPage: Page) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.recorderFrame = recorderFrame;
|
this.recorderPage = recorderPage;
|
||||||
this._highlightCallback = () => { };
|
this._highlightCallback = () => { };
|
||||||
this._highlightInstalled = false;
|
this._highlightInstalled = false;
|
||||||
this._actionReporterInstalled = false;
|
this._actionReporterInstalled = false;
|
||||||
@ -98,7 +98,7 @@ class Recorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async waitForOutput(text: string): Promise<void> {
|
async waitForOutput(text: string): Promise<void> {
|
||||||
this._text = await this.recorderFrame._evaluateExpression(((text: string) => {
|
this._text = await this.recorderPage.evaluate((text: string) => {
|
||||||
const w = window as any;
|
const w = window as any;
|
||||||
return new Promise(f => {
|
return new Promise(f => {
|
||||||
const poll = () => {
|
const poll = () => {
|
||||||
@ -110,7 +110,7 @@ class Recorder {
|
|||||||
};
|
};
|
||||||
setTimeout(poll);
|
setTimeout(poll);
|
||||||
});
|
});
|
||||||
}).toString(), true, text, 'main');
|
}, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
output(): string {
|
output(): string {
|
||||||
|
@ -21,39 +21,102 @@ const { it, describe} = folio;
|
|||||||
describe('pause', (suite, { mode }) => {
|
describe('pause', (suite, { mode }) => {
|
||||||
suite.skip(mode !== 'default');
|
suite.skip(mode !== 'default');
|
||||||
}, () => {
|
}, () => {
|
||||||
it('should pause and resume the script', async ({ page, recorderClick }) => {
|
it('should pause and resume the script', async ({ page, recorderPageGetter }) => {
|
||||||
await Promise.all([
|
const scriptPromise = (async () => {
|
||||||
page.pause(),
|
await page.pause();
|
||||||
recorderClick('[title=Resume]')
|
})();
|
||||||
]);
|
const recorderPage = await recorderPageGetter();
|
||||||
|
await recorderPage.click('[title=Resume]');
|
||||||
|
await scriptPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resume from console', async ({page}) => {
|
it('should resume from console', async ({page}) => {
|
||||||
|
const scriptPromise = (async () => {
|
||||||
|
await page.pause();
|
||||||
|
})();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.pause(),
|
|
||||||
page.waitForFunction(() => (window as any).playwright && (window as any).playwright.resume).then(() => {
|
page.waitForFunction(() => (window as any).playwright && (window as any).playwright.resume).then(() => {
|
||||||
return page.evaluate('window.playwright.resume()');
|
return page.evaluate('window.playwright.resume()');
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
await scriptPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pause after a navigation', async ({page, server, recorderClick}) => {
|
it('should pause after a navigation', async ({page, server, recorderPageGetter}) => {
|
||||||
|
const scriptPromise = (async () => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
await Promise.all([
|
await page.pause();
|
||||||
page.pause(),
|
})();
|
||||||
recorderClick('[title=Resume]')
|
const recorderPage = await recorderPageGetter();
|
||||||
]);
|
await recorderPage.click('[title=Resume]');
|
||||||
|
await scriptPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show source', async ({page, server, recorderClick, recorderFrame}) => {
|
it('should show source', async ({page, recorderPageGetter}) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
const scriptPromise = (async () => {
|
||||||
const pausePromise = page.pause();
|
await page.pause();
|
||||||
const frame = await recorderFrame();
|
})();
|
||||||
const source = await frame._evaluateExpression((() => {
|
const recorderPage = await recorderPageGetter();
|
||||||
return document.querySelector('.source-line-paused .source-code').textContent;
|
const source = await recorderPage.textContent('.source-line-paused .source-code');
|
||||||
}).toString(), true, undefined, 'main');
|
|
||||||
expect(source).toContain('page.pause()');
|
expect(source).toContain('page.pause()');
|
||||||
await recorderClick('[title=Resume]');
|
await recorderPage.click('[title=Resume]');
|
||||||
await pausePromise;
|
await scriptPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pause on next pause', async ({page, recorderPageGetter}) => {
|
||||||
|
const scriptPromise = (async () => {
|
||||||
|
await page.pause(); // 1
|
||||||
|
await page.pause(); // 2
|
||||||
|
})();
|
||||||
|
const recorderPage = await recorderPageGetter();
|
||||||
|
const source = await recorderPage.textContent('.source-line-paused');
|
||||||
|
expect(source).toContain('page.pause(); // 1');
|
||||||
|
await recorderPage.click('[title=Resume]');
|
||||||
|
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")');
|
||||||
|
await recorderPage.click('[title=Resume]');
|
||||||
|
await scriptPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should step', async ({page, recorderPageGetter}) => {
|
||||||
|
await page.setContent('<button>Submit</button>');
|
||||||
|
const scriptPromise = (async () => {
|
||||||
|
await page.pause();
|
||||||
|
await page.click('button');
|
||||||
|
})();
|
||||||
|
const recorderPage = await recorderPageGetter();
|
||||||
|
const source = await recorderPage.textContent('.source-line-paused');
|
||||||
|
expect(source).toContain('page.pause();');
|
||||||
|
|
||||||
|
await recorderPage.click('[title="Step over"]');
|
||||||
|
await recorderPage.waitForSelector('.source-line-paused :has-text("page.click")');
|
||||||
|
|
||||||
|
await recorderPage.click('[title=Resume]');
|
||||||
|
await scriptPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should highlight pointer', async ({page, recorderPageGetter}) => {
|
||||||
|
await page.setContent('<button>Submit</button>');
|
||||||
|
const scriptPromise = (async () => {
|
||||||
|
await page.pause();
|
||||||
|
await page.click('button');
|
||||||
|
})();
|
||||||
|
const recorderPage = await recorderPageGetter();
|
||||||
|
await recorderPage.click('[title="Step over"]');
|
||||||
|
|
||||||
|
const point = await page.waitForSelector('x-pw-action-point');
|
||||||
|
const button = await page.waitForSelector('button');
|
||||||
|
const box1 = await button.boundingBox();
|
||||||
|
const box2 = await point.boundingBox();
|
||||||
|
|
||||||
|
const x1 = box1.x + box1.width / 2;
|
||||||
|
const y1 = box1.y + box1.height / 2;
|
||||||
|
const x2 = box2.x + box2.width / 2;
|
||||||
|
const y2 = box2.y + box2.height / 2;
|
||||||
|
|
||||||
|
expect(Math.abs(x1 - x2) < 2).toBeTruthy();
|
||||||
|
expect(Math.abs(y1 - y2) < 2).toBeTruthy();
|
||||||
|
|
||||||
|
await recorderPage.click('[title="Step over"]');
|
||||||
|
await scriptPromise;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,25 +15,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { folio as baseFolio } from './fixtures';
|
import { folio as baseFolio } from './fixtures';
|
||||||
import { internalCallMetadata } from '../lib/server/instrumentation';
|
import { Page } from '..';
|
||||||
|
import { chromium } from '../index';
|
||||||
|
|
||||||
const fixtures = baseFolio.extend<{
|
const fixtures = baseFolio.extend<{
|
||||||
recorderFrame: () => Promise<any>,
|
recorderPageGetter: () => Promise<Page>,
|
||||||
recorderClick: (selector: string) => Promise<void>
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
fixtures.recorderFrame.init(async ({context, toImpl}, runTest) => {
|
fixtures.recorderPageGetter.init(async ({context, toImpl}, runTest) => {
|
||||||
await runTest(async () => {
|
await runTest(async () => {
|
||||||
while (!toImpl(context).recorderAppForTest)
|
while (!toImpl(context).recorderAppForTest)
|
||||||
await new Promise(f => setTimeout(f, 100));
|
await new Promise(f => setTimeout(f, 100));
|
||||||
return toImpl(context).recorderAppForTest._page.mainFrame();
|
const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpoint;
|
||||||
});
|
const browser = await chromium.connectOverCDP({ wsEndpoint });
|
||||||
});
|
const c = browser.contexts()[0];
|
||||||
|
return c.pages()[0] || await c.waitForEvent('page');
|
||||||
fixtures.recorderClick.init(async ({ recorderFrame }, runTest) => {
|
|
||||||
await runTest(async (selector: string) => {
|
|
||||||
const frame = await recorderFrame();
|
|
||||||
await frame.click(internalCallMetadata(), selector, {});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user