mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 11:46:42 +03:00
feat: add replay log (#5452)
This commit is contained in:
parent
6326d6f3ac
commit
3c877374c7
@ -27,7 +27,6 @@ declare global {
|
|||||||
_playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
|
_playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
|
||||||
_playwrightRecorderCommitAction: () => Promise<void>;
|
_playwrightRecorderCommitAction: () => Promise<void>;
|
||||||
_playwrightRecorderState: () => Promise<UIState>;
|
_playwrightRecorderState: () => Promise<UIState>;
|
||||||
_playwrightRecorderPrintSelector: (text: string) => Promise<void>;
|
|
||||||
_playwrightResume: () => Promise<void>;
|
_playwrightResume: () => Promise<void>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,10 +225,8 @@ export class Recorder {
|
|||||||
|
|
||||||
private _onClick(event: MouseEvent) {
|
private _onClick(event: MouseEvent) {
|
||||||
if (this._mode === 'inspecting') {
|
if (this._mode === 'inspecting') {
|
||||||
if (this._hoveredModel) {
|
if (this._hoveredModel)
|
||||||
copy(this._hoveredModel.selector);
|
copy(this._hoveredModel.selector);
|
||||||
window._playwrightRecorderPrintSelector(this._hoveredModel.selector);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (this._shouldIgnoreMouseEvent(event))
|
if (this._shouldIgnoreMouseEvent(event))
|
||||||
return;
|
return;
|
||||||
|
@ -49,20 +49,24 @@ export class InspectorController implements InstrumentationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||||
if (!sdkObject.attribution.page)
|
if (!sdkObject.attribution.context)
|
||||||
return;
|
return;
|
||||||
const recorder = await this._recorders.get(sdkObject.attribution.context!);
|
const recorder = await this._recorders.get(sdkObject.attribution.context!);
|
||||||
await recorder?.onAfterCall(sdkObject, metadata);
|
await recorder?.onAfterCall(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||||
if (!sdkObject.attribution.page)
|
if (!sdkObject.attribution.page)
|
||||||
return;
|
return;
|
||||||
const recorder = await this._recorders.get(sdkObject.attribution.context!);
|
const recorder = await this._recorders.get(sdkObject.attribution.context!);
|
||||||
await recorder?.onBeforeInputAction(sdkObject, metadata);
|
await recorder?.onBeforeInputAction(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCallLog(logName: string, message: string): void {
|
async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||||
debugLogger.log(logName as any, message);
|
debugLogger.log(logName as any, message);
|
||||||
|
if (!sdkObject.attribution.page)
|
||||||
|
return;
|
||||||
|
const recorder = await this._recorders.get(sdkObject.attribution.context!);
|
||||||
|
await recorder?.updateCallLog([metadata]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ 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 type { EventData, Mode, PauseDetails, 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';
|
||||||
|
|
||||||
@ -32,8 +32,9 @@ const readFileAsync = util.promisify(fs.readFile);
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
playwrightSetMode: (mode: Mode) => void;
|
playwrightSetMode: (mode: Mode) => void;
|
||||||
playwrightSetPaused: (details: PauseDetails | null) => void;
|
playwrightSetPaused: (paused: boolean) => void;
|
||||||
playwrightSetSource: (source: Source) => void;
|
playwrightSetSources: (sources: Source[]) => void;
|
||||||
|
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
|
||||||
dispatch(data: EventData): Promise<void>;
|
dispatch(data: EventData): Promise<void>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,27 +118,33 @@ export class RecorderApp extends EventEmitter {
|
|||||||
}).toString(), true, mode, 'main').catch(() => {});
|
}).toString(), true, mode, 'main').catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPaused(details: PauseDetails | null): Promise<void> {
|
async setPaused(paused: boolean): Promise<void> {
|
||||||
await this._page.mainFrame()._evaluateExpression(((details: PauseDetails | null) => {
|
await this._page.mainFrame()._evaluateExpression(((paused: boolean) => {
|
||||||
window.playwrightSetPaused(details);
|
window.playwrightSetPaused(paused);
|
||||||
}).toString(), true, details, 'main').catch(() => {});
|
}).toString(), true, paused, 'main').catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSource(text: string, language: string, highlightedLine?: number): Promise<void> {
|
async setSources(sources: Source[]): Promise<void> {
|
||||||
await this._page.mainFrame()._evaluateExpression(((source: Source) => {
|
await this._page.mainFrame()._evaluateExpression(((sources: Source[]) => {
|
||||||
window.playwrightSetSource(source);
|
window.playwrightSetSources(sources);
|
||||||
}).toString(), true, { text, language, highlightedLine }, 'main').catch(() => {});
|
}).toString(), true, sources, 'main').catch(() => {});
|
||||||
|
|
||||||
// Testing harness for runCLI mode.
|
// Testing harness for runCLI mode.
|
||||||
{
|
{
|
||||||
if (process.env.PWCLI_EXIT_FOR_TEST) {
|
if (process.env.PWCLI_EXIT_FOR_TEST) {
|
||||||
process.stdout.write('\n-------------8<-------------\n');
|
process.stdout.write('\n-------------8<-------------\n');
|
||||||
process.stdout.write(text);
|
process.stdout.write(sources[0].text);
|
||||||
process.stdout.write('\n-------------8<-------------\n');
|
process.stdout.write('\n-------------8<-------------\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateCallLogs(callLogs: CallLog[]): Promise<void> {
|
||||||
|
await this._page.mainFrame()._evaluateExpression(((callLogs: CallLog[]) => {
|
||||||
|
window.playwrightUpdateLogs(callLogs);
|
||||||
|
}).toString(), true, callLogs, 'main').catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
async bringToFront() {
|
async bringToFront() {
|
||||||
await this._page.bringToFront();
|
await this._page.bringToFront();
|
||||||
}
|
}
|
||||||
|
@ -19,18 +19,32 @@ import { Point } from '../../../common/types';
|
|||||||
export type Mode = 'inspecting' | 'recording' | 'none';
|
export type Mode = 'inspecting' | 'recording' | 'none';
|
||||||
|
|
||||||
export type EventData = {
|
export type EventData = {
|
||||||
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode',
|
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode';
|
||||||
params: any
|
params: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PauseDetails = {
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Source = { text: string, language: string, highlightedLine?: number };
|
|
||||||
|
|
||||||
export type UIState = {
|
export type UIState = {
|
||||||
mode: Mode,
|
mode: Mode;
|
||||||
actionPoint?: Point,
|
actionPoint?: Point;
|
||||||
actionSelector?: string
|
actionSelector?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CallLog = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
messages: string[];
|
||||||
|
status: 'in-progress' | 'done' | 'error' | 'paused';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SourceHighlight = {
|
||||||
|
line: number;
|
||||||
|
type: 'running' | 'paused';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Source = {
|
||||||
|
file: string;
|
||||||
|
text: string;
|
||||||
|
language: string;
|
||||||
|
highlight: SourceHighlight[];
|
||||||
|
revealLine?: number;
|
||||||
};
|
};
|
||||||
|
@ -32,7 +32,7 @@ import { BufferedOutput, FileOutput, OutputMultiplexer, RecorderOutput } from '.
|
|||||||
import { RecorderApp } from './recorder/recorderApp';
|
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 { EventData, Mode, PauseDetails, UIState } from './recorder/recorderTypes';
|
import { CallLog, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
|
||||||
|
|
||||||
type BindingSource = { frame: Frame, page: Page };
|
type BindingSource = { frame: Frame, page: Page };
|
||||||
|
|
||||||
@ -45,18 +45,17 @@ export class RecorderSupplement {
|
|||||||
private _lastDialogOrdinal = 0;
|
private _lastDialogOrdinal = 0;
|
||||||
private _timers = new Set<NodeJS.Timeout>();
|
private _timers = new Set<NodeJS.Timeout>();
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
private _resumeCallback: (() => void) | null = null;
|
|
||||||
private _mode: Mode;
|
private _mode: Mode;
|
||||||
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 _params: channels.BrowserContextRecorderSupplementEnableParams;
|
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
||||||
private _callMetadata: CallMetadata | null = null;
|
private _currentCallsMetadata = new Set<CallMetadata>();
|
||||||
|
private _pausedCallsMetadata = new Map<CallMetadata, () => void>();
|
||||||
private _pauseOnNextStatement = true;
|
private _pauseOnNextStatement = true;
|
||||||
private _sourceCache = new Map<string, string>();
|
|
||||||
private _sdkObject: SdkObject | null = null;
|
private _sdkObject: SdkObject | null = null;
|
||||||
|
private _recorderSource: Source;
|
||||||
|
private _userSources = new Map<string, Source>();
|
||||||
|
|
||||||
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>;
|
||||||
@ -73,7 +72,7 @@ export class RecorderSupplement {
|
|||||||
this._params = params;
|
this._params = params;
|
||||||
this._mode = params.startRecording ? 'recording' : 'none';
|
this._mode = params.startRecording ? 'recording' : 'none';
|
||||||
let languageGenerator: LanguageGenerator;
|
let languageGenerator: LanguageGenerator;
|
||||||
const language = params.language || context._options.sdkLanguage;
|
let language = params.language || context._options.sdkLanguage;
|
||||||
switch (language) {
|
switch (language) {
|
||||||
case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break;
|
case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break;
|
||||||
case 'csharp': languageGenerator = new CSharpLanguageGenerator(); break;
|
case 'csharp': languageGenerator = new CSharpLanguageGenerator(); break;
|
||||||
@ -81,14 +80,14 @@ export class RecorderSupplement {
|
|||||||
case 'python-async': languageGenerator = new PythonLanguageGenerator(params.language === 'python-async'); break;
|
case 'python-async': languageGenerator = new PythonLanguageGenerator(params.language === 'python-async'); break;
|
||||||
default: throw new Error(`Invalid target: '${params.language}'`);
|
default: throw new Error(`Invalid target: '${params.language}'`);
|
||||||
}
|
}
|
||||||
let highlighterType = language;
|
if (language === 'python-async')
|
||||||
if (highlighterType === 'python-async')
|
language = 'python';
|
||||||
highlighterType = 'python';
|
|
||||||
|
|
||||||
this._highlighterType = highlighterType;
|
this._recorderSource = { file: '<recorder>', text: '', language, highlight: [] };
|
||||||
this._bufferedOutput = new BufferedOutput(async text => {
|
this._bufferedOutput = new BufferedOutput(async text => {
|
||||||
if (this._recorderApp)
|
this._recorderSource.text = text;
|
||||||
this._recorderApp.setSource(text, highlighterType);
|
this._recorderSource.revealLine = text.split('\n').length - 1;
|
||||||
|
this._pushAllSources();
|
||||||
});
|
});
|
||||||
const outputs: RecorderOutput[] = [ this._bufferedOutput ];
|
const outputs: RecorderOutput[] = [ this._bufferedOutput ];
|
||||||
if (params.outputFile)
|
if (params.outputFile)
|
||||||
@ -136,8 +135,8 @@ export class RecorderSupplement {
|
|||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
recorderApp.setMode(this._mode),
|
recorderApp.setMode(this._mode),
|
||||||
recorderApp.setPaused(this._pauseDetails),
|
recorderApp.setPaused(!!this._pausedCallsMetadata.size),
|
||||||
recorderApp.setSource(this._bufferedOutput.buffer(), this._highlighterType)
|
this._pushAllSources()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this._context.on(BrowserContext.Events.Page, page => this._onPage(page));
|
this._context.on(BrowserContext.Events.Page, page => this._onPage(page));
|
||||||
@ -168,8 +167,11 @@ export class RecorderSupplement {
|
|||||||
let actionPoint: Point | undefined = undefined;
|
let actionPoint: Point | undefined = undefined;
|
||||||
let actionSelector: string | undefined = undefined;
|
let actionSelector: string | undefined = undefined;
|
||||||
if (source.page === this._sdkObject?.attribution?.page) {
|
if (source.page === this._sdkObject?.attribution?.page) {
|
||||||
actionPoint = this._callMetadata?.point;
|
if (this._currentCallsMetadata.size) {
|
||||||
actionSelector = this._callMetadata?.params.selector;
|
const metadata = this._currentCallsMetadata.values().next().value;
|
||||||
|
actionPoint = metadata.values().next().value;
|
||||||
|
actionSelector = metadata.params.selector;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const uiState: UIState = { mode: this._mode, actionPoint, actionSelector };
|
const uiState: UIState = { mode: this._mode, actionPoint, actionSelector };
|
||||||
return uiState;
|
return uiState;
|
||||||
@ -185,19 +187,26 @@ export class RecorderSupplement {
|
|||||||
(this._context as any).recorderAppForTest = recorderApp;
|
(this._context as any).recorderAppForTest = recorderApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause() {
|
async pause(metadata: CallMetadata) {
|
||||||
this._pauseDetails = { message: 'paused' };
|
const result = new Promise<void>(f => {
|
||||||
this._recorderApp!.setPaused(this._pauseDetails);
|
this._pausedCallsMetadata.set(metadata, f);
|
||||||
return new Promise<void>(f => this._resumeCallback = f);
|
});
|
||||||
|
this._recorderApp!.setPaused(true);
|
||||||
|
this._updateUserSources();
|
||||||
|
this.updateCallLog([metadata]);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _resume(step: boolean) {
|
private async _resume(step: boolean) {
|
||||||
this._pauseOnNextStatement = step;
|
this._pauseOnNextStatement = step;
|
||||||
if (this._resumeCallback)
|
|
||||||
this._resumeCallback();
|
for (const callback of this._pausedCallsMetadata.values())
|
||||||
this._resumeCallback = null;
|
callback();
|
||||||
this._pauseDetails = null;
|
this._pausedCallsMetadata.clear();
|
||||||
this._recorderApp?.setPaused(null);
|
|
||||||
|
this._recorderApp?.setPaused(false);
|
||||||
|
this._updateUserSources();
|
||||||
|
this.updateCallLog([...this._currentCallsMetadata]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _onPage(page: Page) {
|
private async _onPage(page: Page) {
|
||||||
@ -318,47 +327,90 @@ export class RecorderSupplement {
|
|||||||
|
|
||||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||||
this._sdkObject = sdkObject;
|
this._sdkObject = sdkObject;
|
||||||
this._callMetadata = metadata;
|
this._currentCallsMetadata.add(metadata);
|
||||||
const { source, line } = this._source(metadata);
|
this._updateUserSources();
|
||||||
this._recorderApp?.setSource(source, 'javascript', line);
|
this.updateCallLog([metadata]);
|
||||||
if (metadata.method === 'pause' || (this._pauseOnNextStatement && metadata.method === 'goto'))
|
if (metadata.method === 'pause' || (this._pauseOnNextStatement && metadata.method === 'goto'))
|
||||||
await this.pause();
|
await this.pause(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
async onAfterCall(metadata: CallMetadata): Promise<void> {
|
||||||
this._sdkObject = null;
|
this._sdkObject = null;
|
||||||
this._callMetadata = null;
|
this._currentCallsMetadata.delete(metadata);
|
||||||
|
this._pausedCallsMetadata.delete(metadata);
|
||||||
|
this._updateUserSources();
|
||||||
|
this.updateCallLog([metadata]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
private _updateUserSources() {
|
||||||
if (this._pauseOnNextStatement)
|
// Remove old decorations.
|
||||||
await this.pause();
|
for (const source of this._userSources.values()) {
|
||||||
}
|
source.highlight = [];
|
||||||
|
source.revealLine = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private _source(metadata: CallMetadata): { source: string, line: number | undefined } {
|
// Apply new decorations.
|
||||||
let source = '// No source available';
|
for (const metadata of this._currentCallsMetadata) {
|
||||||
let line: number | undefined = undefined;
|
if (!metadata.stack || !metadata.stack[0])
|
||||||
if (metadata.stack && metadata.stack.length) {
|
continue;
|
||||||
try {
|
const { file, line } = metadata.stack[0];
|
||||||
source = this._readAndCacheSource(metadata.stack[0].file);
|
let source = this._userSources.get(file);
|
||||||
line = metadata.stack[0].line ? metadata.stack[0].line - 1 : undefined;
|
if (!source) {
|
||||||
} catch (e) {
|
source = { file, text: this._readSource(file), highlight: [], language: languageForFile(file) };
|
||||||
source = metadata.stack.join('\n');
|
this._userSources.set(file, source);
|
||||||
|
}
|
||||||
|
if (line) {
|
||||||
|
const paused = this._pausedCallsMetadata.has(metadata);
|
||||||
|
source.highlight.push({ line, type: paused ? 'paused' : 'running' });
|
||||||
|
if (paused)
|
||||||
|
source.revealLine = line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { source, line };
|
this._pushAllSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _readAndCacheSource(fileName: string): string {
|
private _pushAllSources() {
|
||||||
let source = this._sourceCache.get(fileName);
|
this._recorderApp?.setSources([this._recorderSource, ...this._userSources.values()]);
|
||||||
if (source)
|
}
|
||||||
return source;
|
|
||||||
try {
|
async onBeforeInputAction(metadata: CallMetadata): Promise<void> {
|
||||||
source = fs.readFileSync(fileName, 'utf-8');
|
if (this._pauseOnNextStatement)
|
||||||
} catch (e) {
|
await this.pause(metadata);
|
||||||
source = '// No source available';
|
}
|
||||||
|
|
||||||
|
async updateCallLog(metadatas: CallMetadata[]): Promise<void> {
|
||||||
|
const logs: CallLog[] = [];
|
||||||
|
for (const metadata of metadatas) {
|
||||||
|
if (!metadata.method)
|
||||||
|
continue;
|
||||||
|
const title = metadata.stack?.[0]?.function || metadata.method;
|
||||||
|
let status: 'done' | 'in-progress' | 'paused' | 'error' = 'done';
|
||||||
|
if (this._currentCallsMetadata.has(metadata))
|
||||||
|
status = 'in-progress';
|
||||||
|
if (this._pausedCallsMetadata.has(metadata))
|
||||||
|
status = 'paused';
|
||||||
|
if (metadata.error)
|
||||||
|
status = 'error';
|
||||||
|
logs.push({ id: metadata.id, messages: metadata.log, title, status });
|
||||||
|
}
|
||||||
|
this._recorderApp?.updateCallLogs(logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _readSource(fileName: string): string {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(fileName, 'utf-8');
|
||||||
|
} catch (e) {
|
||||||
|
return '// No source available';
|
||||||
}
|
}
|
||||||
this._sourceCache.set(fileName, source);
|
|
||||||
return source;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function languageForFile(file: string) {
|
||||||
|
if (file.endsWith('.py'))
|
||||||
|
return 'python';
|
||||||
|
if (file.endsWith('.java'))
|
||||||
|
return 'java';
|
||||||
|
if (file.endsWith('.cs'))
|
||||||
|
return 'csharp';
|
||||||
|
return 'javascript';
|
||||||
|
}
|
@ -15,6 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--toolbar-bg-color: #fafafa;
|
||||||
|
--toolbar-color: #777;
|
||||||
|
|
||||||
--light-background: #f3f2f1;
|
--light-background: #f3f2f1;
|
||||||
--background: #edebe9;
|
--background: #edebe9;
|
||||||
--active-background: #333333;
|
--active-background: #333333;
|
||||||
@ -79,7 +82,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.codicon {
|
.codicon {
|
||||||
color: #C5C5C5;
|
color: var(--toolbar-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-line-highlighted {
|
.source-line-running {
|
||||||
background-color: #6fa8dc7f;
|
background-color: #6fa8dc7f;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
@ -19,18 +19,24 @@ import * as React from 'react';
|
|||||||
import * as highlightjs from '../../third_party/highlightjs/highlightjs';
|
import * as highlightjs from '../../third_party/highlightjs/highlightjs';
|
||||||
import '../../third_party/highlightjs/highlightjs/tomorrow.css';
|
import '../../third_party/highlightjs/highlightjs/tomorrow.css';
|
||||||
|
|
||||||
|
export type SourceHighlight = {
|
||||||
|
line: number;
|
||||||
|
type: 'running' | 'paused';
|
||||||
|
};
|
||||||
|
|
||||||
export interface SourceProps {
|
export interface SourceProps {
|
||||||
text: string,
|
text: string;
|
||||||
language: string,
|
language: string;
|
||||||
highlightedLine?: number,
|
// 1-based
|
||||||
paused?: boolean
|
highlight?: SourceHighlight[];
|
||||||
|
revealLine?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Source: React.FC<SourceProps> = ({
|
export const Source: React.FC<SourceProps> = ({
|
||||||
text,
|
text,
|
||||||
language,
|
language,
|
||||||
paused = false,
|
highlight = [],
|
||||||
highlightedLine = -1
|
revealLine
|
||||||
}) => {
|
}) => {
|
||||||
const lines = React.useMemo<string[]>(() => {
|
const lines = React.useMemo<string[]>(() => {
|
||||||
const result = [];
|
const result = [];
|
||||||
@ -43,20 +49,19 @@ export const Source: React.FC<SourceProps> = ({
|
|||||||
return result;
|
return result;
|
||||||
}, [text]);
|
}, [text]);
|
||||||
|
|
||||||
|
const revealedLineRef = React.createRef<HTMLDivElement>();
|
||||||
const highlightedLineRef = React.createRef<HTMLDivElement>();
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
if (highlightedLine && highlightedLineRef.current)
|
if (typeof revealLine === 'number' && revealedLineRef.current)
|
||||||
highlightedLineRef.current.scrollIntoView({ block: 'center', inline: 'nearest' });
|
revealedLineRef.current.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||||
}, [highlightedLineRef]);
|
}, [revealedLineRef]);
|
||||||
|
|
||||||
return <div className='source'>{
|
return <div className='source'>{
|
||||||
lines.map((markup, index) => {
|
lines.map((markup, index) => {
|
||||||
const isHighlighted = index === highlightedLine;
|
const lineNumber = index + 1;
|
||||||
const highlightType = paused && isHighlighted ? 'source-line-paused' : 'source-line-highlighted';
|
const lineHighlight = highlight.find(h => h.line === lineNumber);
|
||||||
const className = isHighlighted ? `source-line ${highlightType}` : 'source-line';
|
const lineClass = lineHighlight ? `source-line source-line-${lineHighlight.type}` : 'source-line';
|
||||||
return <div key={index} className={className} ref={isHighlighted ? highlightedLineRef : null}>
|
return <div key={lineNumber} className={lineClass} ref={revealLine === lineNumber ? revealedLineRef : null}>
|
||||||
<div className='source-line-number'>{index + 1}</div>
|
<div className='source-line-number'>{lineNumber}</div>
|
||||||
<div className='source-code' dangerouslySetInnerHTML={{ __html: markup }}></div>
|
<div className='source-code' dangerouslySetInnerHTML={{ __html: markup }}></div>
|
||||||
</div>;
|
</div>;
|
||||||
})
|
})
|
||||||
|
42
src/web/components/splitView.css
Normal file
42
src/web/components/splitView.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.split-view {
|
||||||
|
display: flex;
|
||||||
|
flex: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-view-main {
|
||||||
|
display: flex;
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-view-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-view-resizer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 12px;
|
||||||
|
cursor: resize;
|
||||||
|
cursor: ns-resize;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
45
src/web/components/splitView.tsx
Normal file
45
src/web/components/splitView.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
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 './splitView.css';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export interface SplitViewProps {
|
||||||
|
sidebarSize: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SplitView: React.FC<SplitViewProps> = ({
|
||||||
|
sidebarSize,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
let [size, setSize] = React.useState<number>(sidebarSize);
|
||||||
|
const [resizing, setResizing] = React.useState<{ offsetY: number } | null>(null);
|
||||||
|
if (size < 50)
|
||||||
|
size = 50;
|
||||||
|
|
||||||
|
const childrenArray = React.Children.toArray(children);
|
||||||
|
return <div className='split-view'>
|
||||||
|
<div className='split-view-main'>{childrenArray[0]}</div>
|
||||||
|
<div style={{flexBasis: size}} className='split-view-sidebar'>{childrenArray[1]}</div>
|
||||||
|
<div
|
||||||
|
style={{bottom: resizing ? 0 : size - 32, top: resizing ? 0 : undefined, height: resizing ? 'initial' : 32 }}
|
||||||
|
className='split-view-resizer'
|
||||||
|
onMouseDown={event => setResizing({ offsetY: event.clientY - (event.target as HTMLElement).getBoundingClientRect().y })}
|
||||||
|
onMouseUp={() => setResizing(null)}
|
||||||
|
onMouseMove={event => resizing ? setSize((event.target as HTMLElement).clientHeight - event.clientY + resizing.offsetY) : 0}
|
||||||
|
></div>
|
||||||
|
</div>;
|
||||||
|
};
|
@ -16,12 +16,13 @@
|
|||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
box-shadow: rgba(0, 0, 0, 0.1) 0px -1px 0px 0px inset;
|
box-shadow: var(--box-shadow);
|
||||||
background: rgb(255, 255, 255);
|
background-color: var(--toolbar-bg-color);
|
||||||
height: 40px;
|
height: 40px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-linewrap {
|
.toolbar-linewrap {
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
.toolbar-button {
|
.toolbar-button {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
color: #777;
|
color: var(--toolbar-color);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Playwright Recorder</title>
|
<title>Playwright Inspector</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id=root></div>
|
<div id=root></div>
|
||||||
|
@ -29,3 +29,52 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recorder-log {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: auto;
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: pre;
|
||||||
|
background: white;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recorder-log-message {
|
||||||
|
flex: none;
|
||||||
|
padding: 3px 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recorder-log-message-sub-level {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recorder-log-header {
|
||||||
|
color: var(--toolbar-color);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
background-color: var(--toolbar-bg-color);
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 9px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recorder-log-call {
|
||||||
|
color: var(--toolbar-color);
|
||||||
|
background-color: var(--toolbar-bg-color);
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 9px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recorder-log-call .codicon {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
@ -19,15 +19,17 @@ 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 as SourceView } from '../components/source';
|
import { Source as SourceView } from '../components/source';
|
||||||
import type { Mode, PauseDetails, Source } from '../../server/supplements/recorder/recorderTypes';
|
import type { CallLog, Mode, Source } from '../../server/supplements/recorder/recorderTypes';
|
||||||
|
import { SplitView } from '../components/splitView';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
playwrightSetMode: (mode: Mode) => void;
|
playwrightSetMode: (mode: Mode) => void;
|
||||||
playwrightSetPaused: (details: PauseDetails | null) => void;
|
playwrightSetPaused: (paused: boolean) => void;
|
||||||
playwrightSetSource: (source: Source) => void;
|
playwrightSetSources: (sources: Source[]) => void;
|
||||||
|
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
|
||||||
dispatch(data: any): Promise<void>;
|
dispatch(data: any): Promise<void>;
|
||||||
playwrightSourceEchoForTest?: (text: string) => Promise<void>;
|
playwrightSourceEchoForTest: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,42 +38,81 @@ export interface RecorderProps {
|
|||||||
|
|
||||||
export const Recorder: React.FC<RecorderProps> = ({
|
export const Recorder: React.FC<RecorderProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [source, setSource] = React.useState<Source>({ language: 'javascript', text: '' });
|
const [source, setSource] = React.useState<Source>({ file: '', language: 'javascript', text: '', highlight: [] });
|
||||||
const [paused, setPaused] = React.useState<PauseDetails | null>(null);
|
const [paused, setPaused] = React.useState(false);
|
||||||
|
const [log, setLog] = React.useState(new Map<number, CallLog>());
|
||||||
const [mode, setMode] = React.useState<Mode>('none');
|
const [mode, setMode] = React.useState<Mode>('none');
|
||||||
|
|
||||||
window.playwrightSetMode = setMode;
|
window.playwrightSetMode = setMode;
|
||||||
window.playwrightSetSource = setSource;
|
window.playwrightSetSources = sources => {
|
||||||
|
let s = sources.find(s => s.revealLine);
|
||||||
|
if (!s)
|
||||||
|
s = sources.find(s => s.file === source.file);
|
||||||
|
if (!s)
|
||||||
|
s = sources[0];
|
||||||
|
setSource(s);
|
||||||
|
};
|
||||||
window.playwrightSetPaused = setPaused;
|
window.playwrightSetPaused = setPaused;
|
||||||
if (window.playwrightSourceEchoForTest)
|
window.playwrightUpdateLogs = callLogs => {
|
||||||
window.playwrightSourceEchoForTest(source.text).catch(e => {});
|
const newLog = new Map<number, CallLog>(log);
|
||||||
|
for (const callLog of callLogs)
|
||||||
|
newLog.set(callLog.id, callLog);
|
||||||
|
setLog(newLog);
|
||||||
|
};
|
||||||
|
|
||||||
return <div className="recorder">
|
window.playwrightSourceEchoForTest = source.text;
|
||||||
|
|
||||||
|
const messagesEndRef = React.createRef<HTMLDivElement>();
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||||
|
}, [messagesEndRef]);
|
||||||
|
|
||||||
|
return <div className='recorder'>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarButton icon="record" title="Record" toggled={mode == 'recording'} onClick={() => {
|
<ToolbarButton icon='record' title='Record' toggled={mode == 'recording'} onClick={() => {
|
||||||
window.dispatch({ event: 'setMode', params: { mode: mode === 'recording' ? 'none' : 'recording' }}).catch(() => { });
|
window.dispatch({ event: 'setMode', params: { mode: mode === 'recording' ? 'none' : 'recording' }}).catch(() => { });
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<ToolbarButton icon="question" title="Inspect" toggled={mode == 'inspecting'} onClick={() => {
|
<ToolbarButton icon='question' title='Inspect' toggled={mode == 'inspecting'} onClick={() => {
|
||||||
window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' }}).catch(() => { });
|
window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' }}).catch(() => { });
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<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={() => {
|
<ToolbarButton icon='debug-continue' title='Resume' disabled={!paused} onClick={() => {
|
||||||
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={() => {
|
||||||
window.dispatch({ event: 'step' }).catch(() => {});
|
window.dispatch({ event: 'step' }).catch(() => {});
|
||||||
}}></ToolbarButton>
|
}}></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>
|
||||||
<SourceView text={source.text} language={source.language} highlightedLine={source.highlightedLine} paused={!!paused}></SourceView>
|
<SplitView sidebarSize={200}>
|
||||||
|
<SourceView text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine}></SourceView>
|
||||||
|
<div className='vbox'>
|
||||||
|
<div className='recorder-log-header' style={{flex: 'none'}}>Log</div>
|
||||||
|
<div className='recorder-log' style={{flex: 'auto'}}>
|
||||||
|
{[...log.values()].map(callLog => {
|
||||||
|
return <div className='vbox' style={{flex: 'none'}} key={callLog.id}>
|
||||||
|
<div className='recorder-log-call'>
|
||||||
|
<span className={'codicon ' + iconClass(callLog)}></span>{ callLog.title }
|
||||||
|
</div>
|
||||||
|
{ callLog.messages.map((message, i) => {
|
||||||
|
return <div className='recorder-log-message' key={i}>
|
||||||
|
{ message }
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
<div ref={messagesEndRef}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -85,3 +126,12 @@ function copy(text: string) {
|
|||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
textArea.remove();
|
textArea.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function iconClass(callLog: CallLog): string {
|
||||||
|
switch (callLog.status) {
|
||||||
|
case 'done': return 'codicon-check';
|
||||||
|
case 'in-progress': return 'codicon-clock';
|
||||||
|
case 'paused': return 'codicon-debug-pause';
|
||||||
|
case 'error': return 'codicon-error';
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new HtmlWebPackPlugin({
|
new HtmlWebPackPlugin({
|
||||||
title: 'Playwright Recorder',
|
title: 'Playwright Inspector',
|
||||||
template: path.join(__dirname, 'index.html'),
|
template: path.join(__dirname, 'index.html'),
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
@ -21,12 +21,12 @@ import * as url from 'url';
|
|||||||
const { it, describe, expect } = folio;
|
const { it, describe, expect } = folio;
|
||||||
|
|
||||||
describe('cli codegen', (suite, { mode, browserName, headful }) => {
|
describe('cli codegen', (suite, { mode, browserName, headful }) => {
|
||||||
suite.fixme(browserName === 'firefox' && headful, 'Focus is off');
|
// suite.fixme(browserName === 'firefox' && headful, 'Focus is off');
|
||||||
suite.skip(mode !== 'default');
|
suite.skip(mode !== 'default');
|
||||||
}, () => {
|
}, () => {
|
||||||
it('should contain open page', async ({ recorder }) => {
|
it('should contain open page', async ({ recorder }) => {
|
||||||
await recorder.setContentAndWait(``);
|
await recorder.setContentAndWait(``);
|
||||||
expect(recorder.output()).toContain(`const page = await context.newPage();`);
|
await recorder.waitForOutput(`const page = await context.newPage();`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain second page', async ({ context, recorder }) => {
|
it('should contain second page', async ({ context, recorder }) => {
|
||||||
@ -111,7 +111,7 @@ describe('cli codegen', (suite, { mode, browserName, headful }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should download files', (test, {browserName, headful}) => {
|
it('should download files', (test, {browserName, headful}) => {
|
||||||
test.fixme(browserName === 'webkit', 'Generated page.waitForNavigation next to page.waitForEvent(download)');
|
test.fixme(browserName === 'webkit' || browserName === 'firefox', 'Generated page.waitForNavigation next to page.waitForEvent(download)');
|
||||||
}, async ({ page, recorder, httpServer }) => {
|
}, async ({ page, recorder, httpServer }) => {
|
||||||
httpServer.setHandler((req: http.IncomingMessage, res: http.ServerResponse) => {
|
httpServer.setHandler((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
const pathName = url.parse(req.url!).path;
|
const pathName = url.parse(req.url!).path;
|
||||||
|
@ -37,10 +37,7 @@ export const fixtures = baseFolio.extend<TestFixtures, WorkerFixtures>();
|
|||||||
fixtures.recorder.init(async ({ page, recorderFrame }, runTest) => {
|
fixtures.recorder.init(async ({ page, recorderFrame }, 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 recorderFrameInstance = await recorderFrame();
|
||||||
const recorder = new Recorder(page, recorderFrameInstance);
|
await runTest(new Recorder(page, recorderFrameInstance));
|
||||||
await recorderFrameInstance._page.context().exposeBinding('playwrightSourceEchoForTest', false,
|
|
||||||
(_: any, text: string) => recorder.setText(text));
|
|
||||||
await runTest(recorder);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fixtures.httpServer.init(async ({testWorkerIndex}, runTest) => {
|
fixtures.httpServer.init(async ({testWorkerIndex}, runTest) => {
|
||||||
@ -69,8 +66,7 @@ class Recorder {
|
|||||||
_actionReporterInstalled: boolean
|
_actionReporterInstalled: boolean
|
||||||
_actionPerformedCallback: Function
|
_actionPerformedCallback: Function
|
||||||
recorderFrame: any;
|
recorderFrame: any;
|
||||||
private _text: string;
|
private _text: string = '';
|
||||||
private _waiters = [];
|
|
||||||
|
|
||||||
constructor(page: Page, recorderFrame: any) {
|
constructor(page: Page, recorderFrame: any) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
@ -101,16 +97,20 @@ class Recorder {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setText(text: string) {
|
|
||||||
this._text = text;
|
|
||||||
for (const waiter of this._waiters) {
|
|
||||||
if (text.includes(waiter.text))
|
|
||||||
waiter.fulfill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForOutput(text: string): Promise<void> {
|
async waitForOutput(text: string): Promise<void> {
|
||||||
return new Promise(fulfill => this._waiters.push({ text, fulfill }));
|
this._text = await this.recorderFrame._evaluateExpression(((text: string) => {
|
||||||
|
const w = window as any;
|
||||||
|
return new Promise(f => {
|
||||||
|
const poll = () => {
|
||||||
|
if (w.playwrightSourceEchoForTest && w.playwrightSourceEchoForTest.includes(text)) {
|
||||||
|
f(w.playwrightSourceEchoForTest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(poll, 300);
|
||||||
|
};
|
||||||
|
setTimeout(poll);
|
||||||
|
});
|
||||||
|
}).toString(), true, text, 'main');
|
||||||
}
|
}
|
||||||
|
|
||||||
output(): string {
|
output(): string {
|
||||||
|
@ -10,7 +10,8 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
"jsx": "react"
|
"jsx": "react",
|
||||||
|
"downlevelIteration": true,
|
||||||
},
|
},
|
||||||
"compileOnSave": true,
|
"compileOnSave": true,
|
||||||
"include": ["src/**/*.ts", "src/**/*.js"],
|
"include": ["src/**/*.ts", "src/**/*.js"],
|
||||||
|
Loading…
Reference in New Issue
Block a user