2020-12-28 14:50:12 -08:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2021-02-12 10:11:30 -08:00
|
|
|
import * as fs from 'fs';
|
2021-01-24 19:21:19 -08:00
|
|
|
import * as actions from './recorder/recorderActions';
|
2021-01-25 14:49:26 -08:00
|
|
|
import type * as channels from '../../protocol/channels';
|
|
|
|
import { CodeGenerator, ActionInContext } from './recorder/codeGenerator';
|
2021-01-27 13:19:36 -08:00
|
|
|
import { describeFrame, toClickOptions, toModifiers } from './recorder/utils';
|
2021-01-24 19:21:19 -08:00
|
|
|
import { Page } from '../page';
|
|
|
|
import { Frame } from '../frames';
|
|
|
|
import { BrowserContext } from '../browserContext';
|
2021-03-03 14:32:09 -08:00
|
|
|
import { JavaLanguageGenerator } from './recorder/java';
|
2021-01-24 19:21:19 -08:00
|
|
|
import { JavaScriptLanguageGenerator } from './recorder/javascript';
|
|
|
|
import { CSharpLanguageGenerator } from './recorder/csharp';
|
|
|
|
import { PythonLanguageGenerator } from './recorder/python';
|
|
|
|
import * as recorderSource from '../../generated/recorderSource';
|
2021-01-25 14:49:26 -08:00
|
|
|
import * as consoleApiSource from '../../generated/consoleApiSource';
|
2021-02-12 10:11:30 -08:00
|
|
|
import { RecorderApp } from './recorder/recorderApp';
|
|
|
|
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
|
|
|
|
import { Point } from '../../common/types';
|
2021-03-11 11:22:59 -08:00
|
|
|
import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
|
2021-02-17 22:10:13 -08:00
|
|
|
import { isUnderTest, monotonicTime } from '../../utils/utils';
|
2021-02-26 14:16:32 -08:00
|
|
|
import { InMemorySnapshotter } from '../snapshot/inMemorySnapshotter';
|
2021-03-11 11:22:59 -08:00
|
|
|
import { metadataToCallLog } from './recorder/recorderUtils';
|
2020-12-28 14:50:12 -08:00
|
|
|
|
|
|
|
type BindingSource = { frame: Frame, page: Page };
|
2021-01-25 14:49:26 -08:00
|
|
|
|
|
|
|
const symbol = Symbol('RecorderSupplement');
|
|
|
|
|
2021-01-24 19:21:19 -08:00
|
|
|
export class RecorderSupplement {
|
2020-12-28 14:50:12 -08:00
|
|
|
private _generator: CodeGenerator;
|
|
|
|
private _pageAliases = new Map<Page, string>();
|
|
|
|
private _lastPopupOrdinal = 0;
|
|
|
|
private _lastDialogOrdinal = 0;
|
|
|
|
private _timers = new Set<NodeJS.Timeout>();
|
2021-01-24 19:21:19 -08:00
|
|
|
private _context: BrowserContext;
|
2021-02-05 14:24:27 -08:00
|
|
|
private _mode: Mode;
|
2021-02-19 07:25:08 -08:00
|
|
|
private _highlightedSelector = '';
|
2021-02-05 14:24:27 -08:00
|
|
|
private _recorderApp: RecorderApp | null = null;
|
2021-02-03 16:01:51 -08:00
|
|
|
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
2021-02-12 22:06:45 -08:00
|
|
|
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
2021-02-12 18:53:46 -08:00
|
|
|
private _pausedCallsMetadata = new Map<CallMetadata, () => void>();
|
2021-02-19 09:33:24 -08:00
|
|
|
private _pauseOnNextStatement: boolean;
|
2021-02-16 18:13:26 -08:00
|
|
|
private _recorderSources: Source[];
|
2021-02-12 18:53:46 -08:00
|
|
|
private _userSources = new Map<string, Source>();
|
2021-02-26 14:16:32 -08:00
|
|
|
private _snapshotter: InMemorySnapshotter;
|
2021-03-11 11:22:59 -08:00
|
|
|
private _hoveredSnapshot: { callLogId: number, phase: 'before' | 'after' | 'action' } | undefined;
|
2021-02-26 14:16:32 -08:00
|
|
|
private _snapshots = new Set<string>();
|
|
|
|
private _allMetadatas = new Map<number, CallMetadata>();
|
2021-01-25 14:49:26 -08:00
|
|
|
|
2021-02-11 17:46:54 -08:00
|
|
|
static getOrCreate(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<RecorderSupplement> {
|
2021-01-25 14:49:26 -08:00
|
|
|
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
|
|
|
|
if (!recorderPromise) {
|
2021-02-03 16:01:51 -08:00
|
|
|
const recorder = new RecorderSupplement(context, params);
|
2021-01-25 14:49:26 -08:00
|
|
|
recorderPromise = recorder.install().then(() => recorder);
|
|
|
|
(context as any)[symbol] = recorderPromise;
|
|
|
|
}
|
|
|
|
return recorderPromise;
|
|
|
|
}
|
2021-01-24 19:21:19 -08:00
|
|
|
|
2021-02-15 08:32:13 -08:00
|
|
|
static getNoCreate(context: BrowserContext): Promise<RecorderSupplement> | undefined {
|
|
|
|
return (context as any)[symbol] as Promise<RecorderSupplement> | undefined;
|
|
|
|
}
|
|
|
|
|
2021-02-03 16:01:51 -08:00
|
|
|
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
|
2021-01-24 19:21:19 -08:00
|
|
|
this._context = context;
|
2021-02-03 16:01:51 -08:00
|
|
|
this._params = params;
|
2021-02-05 14:24:27 -08:00
|
|
|
this._mode = params.startRecording ? 'recording' : 'none';
|
2021-02-19 09:33:24 -08:00
|
|
|
this._pauseOnNextStatement = !!params.pauseOnNextStatement;
|
2021-02-16 18:13:26 -08:00
|
|
|
const language = params.language || context._options.sdkLanguage;
|
2021-01-25 14:49:26 -08:00
|
|
|
|
2021-02-16 18:13:26 -08:00
|
|
|
const languages = new Set([
|
2021-03-03 14:32:09 -08:00
|
|
|
new JavaLanguageGenerator(),
|
2021-02-16 18:13:26 -08:00
|
|
|
new JavaScriptLanguageGenerator(),
|
|
|
|
new PythonLanguageGenerator(false),
|
|
|
|
new PythonLanguageGenerator(true),
|
|
|
|
new CSharpLanguageGenerator(),
|
|
|
|
]);
|
|
|
|
const primaryLanguage = [...languages].find(l => l.id === language)!;
|
|
|
|
if (!primaryLanguage)
|
2021-03-02 12:16:04 -08:00
|
|
|
throw new Error(`\n===============================\nUnsupported language: '${language}'\n===============================\n`);
|
2021-02-16 18:13:26 -08:00
|
|
|
|
|
|
|
languages.delete(primaryLanguage);
|
|
|
|
const orderedLanguages = [primaryLanguage, ...languages];
|
|
|
|
|
|
|
|
this._recorderSources = [];
|
|
|
|
const generator = new CodeGenerator(context._browser.options.name, !!params.startRecording, params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage);
|
|
|
|
let text = '';
|
|
|
|
generator.on('change', () => {
|
|
|
|
this._recorderSources = [];
|
|
|
|
for (const languageGenerator of orderedLanguages) {
|
|
|
|
const source: Source = {
|
|
|
|
file: languageGenerator.fileName,
|
|
|
|
text: generator.generateText(languageGenerator),
|
|
|
|
language: languageGenerator.highlighter,
|
|
|
|
highlight: []
|
|
|
|
};
|
|
|
|
source.revealLine = source.text.split('\n').length - 1;
|
|
|
|
this._recorderSources.push(source);
|
|
|
|
if (languageGenerator === orderedLanguages[0])
|
|
|
|
text = source.text;
|
|
|
|
}
|
2021-02-12 18:53:46 -08:00
|
|
|
this._pushAllSources();
|
2021-02-17 14:05:41 -08:00
|
|
|
this._recorderApp?.setFile(primaryLanguage.fileName);
|
2021-01-31 16:37:13 -08:00
|
|
|
});
|
2021-02-16 18:13:26 -08:00
|
|
|
if (params.outputFile) {
|
|
|
|
context.on(BrowserContext.Events.BeforeClose, () => {
|
|
|
|
fs.writeFileSync(params.outputFile!, text);
|
|
|
|
text = '';
|
|
|
|
});
|
|
|
|
process.on('exit', () => {
|
|
|
|
if (text)
|
|
|
|
fs.writeFileSync(params.outputFile!, text);
|
|
|
|
});
|
|
|
|
}
|
2021-01-24 19:21:19 -08:00
|
|
|
this._generator = generator;
|
2021-02-26 14:16:32 -08:00
|
|
|
this._snapshotter = new InMemorySnapshotter(context);
|
2021-01-24 19:21:19 -08:00
|
|
|
}
|
2020-12-28 14:50:12 -08:00
|
|
|
|
2021-01-24 19:21:19 -08:00
|
|
|
async install() {
|
2021-02-11 17:46:54 -08:00
|
|
|
const recorderApp = await RecorderApp.open(this._context);
|
2021-02-05 14:24:27 -08:00
|
|
|
this._recorderApp = recorderApp;
|
|
|
|
recorderApp.once('close', () => {
|
2021-03-01 12:20:04 -08:00
|
|
|
this._snapshotter.dispose().catch(() => {});
|
2021-02-05 14:24:27 -08:00
|
|
|
this._recorderApp = null;
|
|
|
|
});
|
|
|
|
recorderApp.on('event', (data: EventData) => {
|
|
|
|
if (data.event === 'setMode') {
|
2021-02-19 07:25:08 -08:00
|
|
|
this._setMode(data.params.mode);
|
2021-02-26 14:16:32 -08:00
|
|
|
this._refreshOverlay();
|
2021-02-19 07:25:08 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (data.event === 'selectorUpdated') {
|
|
|
|
this._highlightedSelector = data.params.selector;
|
2021-02-26 14:16:32 -08:00
|
|
|
this._refreshOverlay();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (data.event === 'callLogHovered') {
|
|
|
|
this._hoveredSnapshot = undefined;
|
2021-03-03 22:25:34 -08:00
|
|
|
if (this._isPaused() && data.params.callLogId)
|
2021-02-26 14:16:32 -08:00
|
|
|
this._hoveredSnapshot = data.params;
|
|
|
|
this._refreshOverlay();
|
2021-02-05 14:24:27 -08:00
|
|
|
return;
|
|
|
|
}
|
2021-02-12 10:11:30 -08:00
|
|
|
if (data.event === 'step') {
|
|
|
|
this._resume(true);
|
|
|
|
return;
|
|
|
|
}
|
2021-02-05 14:24:27 -08:00
|
|
|
if (data.event === 'resume') {
|
2021-02-12 10:11:30 -08:00
|
|
|
this._resume(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (data.event === 'pause') {
|
|
|
|
this._pauseOnNextStatement = true;
|
2021-02-05 14:24:27 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (data.event === 'clear') {
|
|
|
|
this._clearScript();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
recorderApp.setMode(this._mode),
|
2021-02-12 18:53:46 -08:00
|
|
|
recorderApp.setPaused(!!this._pausedCallsMetadata.size),
|
|
|
|
this._pushAllSources()
|
2021-02-05 14:24:27 -08:00
|
|
|
]);
|
|
|
|
|
|
|
|
this._context.on(BrowserContext.Events.Page, page => this._onPage(page));
|
2021-01-24 19:21:19 -08:00
|
|
|
for (const page of this._context.pages())
|
|
|
|
this._onPage(page);
|
2020-12-28 14:50:12 -08:00
|
|
|
|
2021-02-05 14:24:27 -08:00
|
|
|
this._context.once(BrowserContext.Events.Close, () => {
|
2021-01-24 19:21:19 -08:00
|
|
|
for (const timer of this._timers)
|
|
|
|
clearTimeout(timer);
|
|
|
|
this._timers.clear();
|
2021-02-05 14:24:27 -08:00
|
|
|
recorderApp.close().catch(() => {});
|
2021-01-24 19:21:19 -08:00
|
|
|
});
|
2020-12-28 14:50:12 -08:00
|
|
|
|
|
|
|
// Input actions that potentially lead to navigation are intercepted on the page and are
|
|
|
|
// performed by the Playwright.
|
2021-02-03 16:01:51 -08:00
|
|
|
await this._context.exposeBinding('_playwrightRecorderPerformAction', false,
|
2021-01-24 19:21:19 -08:00
|
|
|
(source: BindingSource, action: actions.Action) => this._performAction(source.frame, action));
|
2020-12-28 14:50:12 -08:00
|
|
|
|
|
|
|
// Other non-essential actions are simply being recorded.
|
2021-02-03 16:01:51 -08:00
|
|
|
await this._context.exposeBinding('_playwrightRecorderRecordAction', false,
|
2021-01-24 19:21:19 -08:00
|
|
|
(source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action));
|
2020-12-28 14:50:12 -08:00
|
|
|
|
|
|
|
// Commits last action so that no further signals are added to it.
|
2021-02-03 16:01:51 -08:00
|
|
|
await this._context.exposeBinding('_playwrightRecorderCommitAction', false,
|
2021-01-24 19:21:19 -08:00
|
|
|
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());
|
2020-12-28 14:50:12 -08:00
|
|
|
|
2021-02-12 10:11:30 -08:00
|
|
|
await this._context.exposeBinding('_playwrightRecorderState', false, source => {
|
2021-03-08 19:49:57 -08:00
|
|
|
let snapshotUrl: string | undefined;
|
2021-03-09 07:44:10 -08:00
|
|
|
let actionSelector = this._highlightedSelector;
|
2021-02-26 14:16:32 -08:00
|
|
|
let actionPoint: Point | undefined;
|
|
|
|
if (this._hoveredSnapshot) {
|
2021-03-08 19:49:57 -08:00
|
|
|
const metadata = this._allMetadatas.get(this._hoveredSnapshot.callLogId)!;
|
|
|
|
snapshotUrl = `${metadata.pageId}?name=${this._hoveredSnapshot.phase}@${this._hoveredSnapshot.callLogId}`;
|
2021-03-11 11:22:59 -08:00
|
|
|
actionPoint = this._hoveredSnapshot.phase === 'action' ? metadata?.point : undefined;
|
2021-02-26 14:16:32 -08:00
|
|
|
} else {
|
|
|
|
for (const [metadata, sdkObject] of this._currentCallsMetadata) {
|
|
|
|
if (source.page === sdkObject.attribution.page) {
|
|
|
|
actionPoint = metadata.point || actionPoint;
|
2021-03-09 07:44:10 -08:00
|
|
|
actionSelector = actionSelector || metadata.params.selector;
|
2021-02-26 14:16:32 -08:00
|
|
|
}
|
2021-02-12 18:53:46 -08:00
|
|
|
}
|
2021-02-12 10:11:30 -08:00
|
|
|
}
|
2021-02-26 14:16:32 -08:00
|
|
|
const uiState: UIState = {
|
|
|
|
mode: this._mode,
|
|
|
|
actionPoint,
|
|
|
|
actionSelector,
|
2021-03-08 19:49:57 -08:00
|
|
|
snapshotUrl,
|
2021-02-26 14:16:32 -08:00
|
|
|
};
|
2021-02-12 10:11:30 -08:00
|
|
|
return uiState;
|
2021-01-25 14:49:26 -08:00
|
|
|
});
|
|
|
|
|
2021-02-19 07:25:08 -08:00
|
|
|
await this._context.exposeBinding('_playwrightRecorderSetSelector', false, async (_, selector: string) => {
|
|
|
|
this._setMode('none');
|
|
|
|
await this._recorderApp?.setSelector(selector, true);
|
|
|
|
await this._recorderApp?.bringToFront();
|
|
|
|
});
|
|
|
|
|
2021-02-03 16:01:51 -08:00
|
|
|
await this._context.exposeBinding('_playwrightResume', false, () => {
|
2021-02-12 10:11:30 -08:00
|
|
|
this._resume(false).catch(() => {});
|
2021-01-25 14:49:26 -08:00
|
|
|
});
|
|
|
|
|
2021-03-01 12:20:04 -08:00
|
|
|
const snapshotBaseUrl = await this._snapshotter.initialize() + '/snapshot/';
|
2021-02-26 14:16:32 -08:00
|
|
|
await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest(), snapshotBaseUrl });
|
2021-01-25 14:49:26 -08:00
|
|
|
await this._context.extendInjectedScript(consoleApiSource.source);
|
2021-02-05 14:24:27 -08:00
|
|
|
(this._context as any).recorderAppForTest = recorderApp;
|
2021-01-25 14:49:26 -08:00
|
|
|
}
|
|
|
|
|
2021-02-12 18:53:46 -08:00
|
|
|
async pause(metadata: CallMetadata) {
|
|
|
|
const result = new Promise<void>(f => {
|
|
|
|
this._pausedCallsMetadata.set(metadata, f);
|
|
|
|
});
|
|
|
|
this._recorderApp!.setPaused(true);
|
2021-02-17 22:10:13 -08:00
|
|
|
metadata.pauseStartTime = monotonicTime();
|
2021-02-12 18:53:46 -08:00
|
|
|
this._updateUserSources();
|
|
|
|
this.updateCallLog([metadata]);
|
|
|
|
return result;
|
2020-12-28 14:50:12 -08:00
|
|
|
}
|
|
|
|
|
2021-02-26 14:16:32 -08:00
|
|
|
_isPaused(): boolean {
|
|
|
|
return !!this._pausedCallsMetadata.size;
|
|
|
|
}
|
|
|
|
|
2021-02-19 07:25:08 -08:00
|
|
|
private _setMode(mode: Mode) {
|
|
|
|
this._mode = mode;
|
|
|
|
this._recorderApp?.setMode(this._mode);
|
|
|
|
this._generator.setEnabled(this._mode === 'recording');
|
|
|
|
if (this._mode !== 'none')
|
|
|
|
this._context.pages()[0].bringToFront().catch(() => {});
|
|
|
|
}
|
|
|
|
|
2021-02-12 10:11:30 -08:00
|
|
|
private async _resume(step: boolean) {
|
|
|
|
this._pauseOnNextStatement = step;
|
2021-02-17 14:05:41 -08:00
|
|
|
this._recorderApp?.setPaused(false);
|
2021-02-12 18:53:46 -08:00
|
|
|
|
2021-02-17 22:10:13 -08:00
|
|
|
const endTime = monotonicTime();
|
|
|
|
for (const [metadata, callback] of this._pausedCallsMetadata) {
|
|
|
|
metadata.pauseEndTime = endTime;
|
2021-02-12 18:53:46 -08:00
|
|
|
callback();
|
2021-02-17 22:10:13 -08:00
|
|
|
}
|
2021-02-12 18:53:46 -08:00
|
|
|
this._pausedCallsMetadata.clear();
|
|
|
|
|
|
|
|
this._updateUserSources();
|
2021-02-12 22:06:45 -08:00
|
|
|
this.updateCallLog([...this._currentCallsMetadata.keys()]);
|
2021-02-05 14:24:27 -08:00
|
|
|
}
|
|
|
|
|
2021-02-26 14:16:32 -08:00
|
|
|
private _refreshOverlay() {
|
|
|
|
for (const page of this._context.pages())
|
2021-03-18 01:47:07 +08:00
|
|
|
page.mainFrame().evaluateExpression('window._playwrightRefreshOverlay()', false, undefined, 'main').catch(() => {});
|
2021-02-26 14:16:32 -08:00
|
|
|
}
|
|
|
|
|
2020-12-28 14:50:12 -08:00
|
|
|
private async _onPage(page: Page) {
|
|
|
|
// First page is called page, others are called popup1, popup2, etc.
|
2021-01-24 19:21:19 -08:00
|
|
|
const frame = page.mainFrame();
|
2020-12-28 14:50:12 -08:00
|
|
|
page.on('close', () => {
|
|
|
|
this._pageAliases.delete(page);
|
|
|
|
this._generator.addAction({
|
|
|
|
pageAlias,
|
2021-01-27 13:19:36 -08:00
|
|
|
...describeFrame(page.mainFrame()),
|
2020-12-28 14:50:12 -08:00
|
|
|
committed: true,
|
|
|
|
action: {
|
|
|
|
name: 'closePage',
|
|
|
|
signals: [],
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2021-01-24 19:21:19 -08:00
|
|
|
frame.on(Frame.Events.Navigation, () => this._onFrameNavigated(frame, page));
|
|
|
|
page.on(Page.Events.Download, () => this._onDownload(page));
|
|
|
|
page.on(Page.Events.Popup, popup => this._onPopup(page, popup));
|
|
|
|
page.on(Page.Events.Dialog, () => this._onDialog(page));
|
2020-12-28 14:50:12 -08:00
|
|
|
const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : '';
|
|
|
|
const pageAlias = 'page' + suffix;
|
|
|
|
this._pageAliases.set(page, pageAlias);
|
|
|
|
|
|
|
|
const isPopup = !!await page.opener();
|
|
|
|
// Could happen due to the await above.
|
|
|
|
if (page.isClosed())
|
|
|
|
return;
|
|
|
|
if (!isPopup) {
|
|
|
|
this._generator.addAction({
|
|
|
|
pageAlias,
|
2021-01-27 13:19:36 -08:00
|
|
|
...describeFrame(page.mainFrame()),
|
2020-12-28 14:50:12 -08:00
|
|
|
committed: true,
|
|
|
|
action: {
|
|
|
|
name: 'openPage',
|
2021-01-24 19:21:19 -08:00
|
|
|
url: page.mainFrame().url(),
|
2020-12-28 14:50:12 -08:00
|
|
|
signals: [],
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-31 16:37:13 -08:00
|
|
|
private _clearScript(): void {
|
|
|
|
this._generator.restart();
|
2021-02-03 16:01:51 -08:00
|
|
|
if (!!this._params.startRecording) {
|
2021-01-31 16:37:13 -08:00
|
|
|
for (const page of this._context.pages())
|
|
|
|
this._onFrameNavigated(page.mainFrame(), page);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-28 14:50:12 -08:00
|
|
|
private async _performAction(frame: Frame, action: actions.Action) {
|
2021-01-24 19:21:19 -08:00
|
|
|
const page = frame._page;
|
2020-12-28 14:50:12 -08:00
|
|
|
const actionInContext: ActionInContext = {
|
|
|
|
pageAlias: this._pageAliases.get(page)!,
|
2021-01-27 13:19:36 -08:00
|
|
|
...describeFrame(frame),
|
2020-12-28 14:50:12 -08:00
|
|
|
action
|
|
|
|
};
|
|
|
|
this._generator.willPerformAction(actionInContext);
|
2021-02-09 14:44:48 -08:00
|
|
|
const noCallMetadata = internalCallMetadata();
|
2021-01-27 15:57:28 -08:00
|
|
|
try {
|
|
|
|
const kActionTimeout = 5000;
|
|
|
|
if (action.name === 'click') {
|
|
|
|
const { options } = toClickOptions(action);
|
2021-02-09 14:44:48 -08:00
|
|
|
await frame.click(noCallMetadata, action.selector, { ...options, timeout: kActionTimeout });
|
2021-01-27 15:57:28 -08:00
|
|
|
}
|
|
|
|
if (action.name === 'press') {
|
|
|
|
const modifiers = toModifiers(action.modifiers);
|
|
|
|
const shortcut = [...modifiers, action.key].join('+');
|
2021-02-09 14:44:48 -08:00
|
|
|
await frame.press(noCallMetadata, action.selector, shortcut, { timeout: kActionTimeout });
|
2021-01-27 15:57:28 -08:00
|
|
|
}
|
|
|
|
if (action.name === 'check')
|
2021-02-09 14:44:48 -08:00
|
|
|
await frame.check(noCallMetadata, action.selector, { timeout: kActionTimeout });
|
2021-01-27 15:57:28 -08:00
|
|
|
if (action.name === 'uncheck')
|
2021-02-09 14:44:48 -08:00
|
|
|
await frame.uncheck(noCallMetadata, action.selector, { timeout: kActionTimeout });
|
2021-01-27 15:57:28 -08:00
|
|
|
if (action.name === 'select')
|
2021-02-09 14:44:48 -08:00
|
|
|
await frame.selectOption(noCallMetadata, action.selector, [], action.options.map(value => ({ value })), { timeout: kActionTimeout });
|
2021-01-27 15:57:28 -08:00
|
|
|
} catch (e) {
|
|
|
|
this._generator.performedActionFailed(actionInContext);
|
|
|
|
return;
|
2020-12-28 14:50:12 -08:00
|
|
|
}
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
actionInContext.committed = true;
|
|
|
|
this._timers.delete(timer);
|
|
|
|
}, 5000);
|
|
|
|
this._generator.didPerformAction(actionInContext);
|
|
|
|
this._timers.add(timer);
|
|
|
|
}
|
|
|
|
|
|
|
|
private async _recordAction(frame: Frame, action: actions.Action) {
|
|
|
|
this._generator.addAction({
|
2021-01-24 19:21:19 -08:00
|
|
|
pageAlias: this._pageAliases.get(frame._page)!,
|
2021-01-27 13:19:36 -08:00
|
|
|
...describeFrame(frame),
|
2020-12-28 14:50:12 -08:00
|
|
|
action
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private _onFrameNavigated(frame: Frame, page: Page) {
|
|
|
|
const pageAlias = this._pageAliases.get(page);
|
|
|
|
this._generator.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() });
|
|
|
|
}
|
|
|
|
|
|
|
|
private _onPopup(page: Page, popup: Page) {
|
|
|
|
const pageAlias = this._pageAliases.get(page)!;
|
|
|
|
const popupAlias = this._pageAliases.get(popup)!;
|
|
|
|
this._generator.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias });
|
|
|
|
}
|
2021-01-24 19:21:19 -08:00
|
|
|
private _onDownload(page: Page) {
|
2020-12-28 14:50:12 -08:00
|
|
|
const pageAlias = this._pageAliases.get(page)!;
|
|
|
|
this._generator.signal(pageAlias, page.mainFrame(), { name: 'download' });
|
|
|
|
}
|
|
|
|
|
2021-01-24 19:21:19 -08:00
|
|
|
private _onDialog(page: Page) {
|
2020-12-28 14:50:12 -08:00
|
|
|
const pageAlias = this._pageAliases.get(page)!;
|
|
|
|
this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) });
|
|
|
|
}
|
2021-02-12 10:11:30 -08:00
|
|
|
|
2021-03-11 11:22:59 -08:00
|
|
|
_captureSnapshot(sdkObject: SdkObject, metadata: CallMetadata, phase: 'before' | 'after' | 'action') {
|
2021-02-26 14:16:32 -08:00
|
|
|
if (sdkObject.attribution.page) {
|
2021-03-08 19:49:57 -08:00
|
|
|
const snapshotName = `${phase}@${metadata.id}`;
|
|
|
|
this._snapshots.add(snapshotName);
|
|
|
|
this._snapshotter.captureSnapshot(sdkObject.attribution.page, snapshotName);
|
2021-02-26 14:16:32 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-12 10:11:30 -08:00
|
|
|
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
2021-02-15 08:32:13 -08:00
|
|
|
if (this._mode === 'recording')
|
|
|
|
return;
|
2021-03-01 12:20:04 -08:00
|
|
|
this._captureSnapshot(sdkObject, metadata, 'before');
|
2021-02-12 22:06:45 -08:00
|
|
|
this._currentCallsMetadata.set(metadata, sdkObject);
|
2021-02-26 14:16:32 -08:00
|
|
|
this._allMetadatas.set(metadata.id, metadata);
|
2021-02-12 18:53:46 -08:00
|
|
|
this._updateUserSources();
|
|
|
|
this.updateCallLog([metadata]);
|
2021-02-19 09:33:24 -08:00
|
|
|
if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseOnStep(sdkObject, metadata)))
|
2021-02-12 18:53:46 -08:00
|
|
|
await this.pause(metadata);
|
2021-02-19 07:25:08 -08:00
|
|
|
if (metadata.params && metadata.params.selector) {
|
|
|
|
this._highlightedSelector = metadata.params.selector;
|
|
|
|
await this._recorderApp?.setSelector(this._highlightedSelector);
|
|
|
|
}
|
2021-02-12 10:11:30 -08:00
|
|
|
}
|
|
|
|
|
2021-02-26 14:16:32 -08:00
|
|
|
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
2021-02-15 08:32:13 -08:00
|
|
|
if (this._mode === 'recording')
|
|
|
|
return;
|
2021-03-11 11:22:59 -08:00
|
|
|
this._captureSnapshot(sdkObject, metadata, 'after');
|
2021-02-13 22:13:51 -08:00
|
|
|
if (!metadata.error)
|
|
|
|
this._currentCallsMetadata.delete(metadata);
|
2021-02-12 18:53:46 -08:00
|
|
|
this._pausedCallsMetadata.delete(metadata);
|
|
|
|
this._updateUserSources();
|
|
|
|
this.updateCallLog([metadata]);
|
2021-02-12 10:11:30 -08:00
|
|
|
}
|
|
|
|
|
2021-02-12 18:53:46 -08:00
|
|
|
private _updateUserSources() {
|
|
|
|
// Remove old decorations.
|
|
|
|
for (const source of this._userSources.values()) {
|
|
|
|
source.highlight = [];
|
|
|
|
source.revealLine = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply new decorations.
|
2021-02-17 14:05:41 -08:00
|
|
|
let fileToSelect = undefined;
|
2021-02-12 22:06:45 -08:00
|
|
|
for (const metadata of this._currentCallsMetadata.keys()) {
|
2021-02-12 18:53:46 -08:00
|
|
|
if (!metadata.stack || !metadata.stack[0])
|
|
|
|
continue;
|
|
|
|
const { file, line } = metadata.stack[0];
|
|
|
|
let source = this._userSources.get(file);
|
|
|
|
if (!source) {
|
|
|
|
source = { file, text: this._readSource(file), highlight: [], language: languageForFile(file) };
|
|
|
|
this._userSources.set(file, source);
|
|
|
|
}
|
|
|
|
if (line) {
|
|
|
|
const paused = this._pausedCallsMetadata.has(metadata);
|
2021-02-13 22:13:51 -08:00
|
|
|
source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
|
2021-02-17 14:05:41 -08:00
|
|
|
source.revealLine = line;
|
|
|
|
fileToSelect = source.file;
|
2021-02-12 18:53:46 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
this._pushAllSources();
|
2021-02-17 14:05:41 -08:00
|
|
|
if (fileToSelect)
|
|
|
|
this._recorderApp?.setFile(fileToSelect);
|
2021-02-12 18:53:46 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private _pushAllSources() {
|
2021-02-16 18:13:26 -08:00
|
|
|
this._recorderApp?.setSources([...this._recorderSources, ...this._userSources.values()]);
|
2021-02-12 18:53:46 -08:00
|
|
|
}
|
|
|
|
|
2021-02-26 14:16:32 -08:00
|
|
|
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
2021-02-15 08:32:13 -08:00
|
|
|
if (this._mode === 'recording')
|
|
|
|
return;
|
2021-03-11 11:22:59 -08:00
|
|
|
this._captureSnapshot(sdkObject, metadata, 'action');
|
2021-02-12 10:11:30 -08:00
|
|
|
if (this._pauseOnNextStatement)
|
2021-02-12 18:53:46 -08:00
|
|
|
await this.pause(metadata);
|
2021-02-12 10:11:30 -08:00
|
|
|
}
|
|
|
|
|
2021-03-11 11:22:59 -08:00
|
|
|
updateCallLog(metadatas: CallMetadata[]) {
|
2021-02-15 08:32:13 -08:00
|
|
|
if (this._mode === 'recording')
|
|
|
|
return;
|
2021-02-12 18:53:46 -08:00
|
|
|
const logs: CallLog[] = [];
|
|
|
|
for (const metadata of metadatas) {
|
|
|
|
if (!metadata.method)
|
|
|
|
continue;
|
2021-03-11 11:22:59 -08:00
|
|
|
let status: CallLogStatus = 'done';
|
2021-02-12 18:53:46 -08:00
|
|
|
if (this._currentCallsMetadata.has(metadata))
|
|
|
|
status = 'in-progress';
|
|
|
|
if (this._pausedCallsMetadata.has(metadata))
|
|
|
|
status = 'paused';
|
2021-03-11 11:22:59 -08:00
|
|
|
logs.push(metadataToCallLog(metadata, status, this._snapshots));
|
2021-02-12 10:11:30 -08:00
|
|
|
}
|
2021-02-12 18:53:46 -08:00
|
|
|
this._recorderApp?.updateCallLogs(logs);
|
2021-02-12 10:11:30 -08:00
|
|
|
}
|
|
|
|
|
2021-02-12 18:53:46 -08:00
|
|
|
private _readSource(fileName: string): string {
|
2021-02-12 10:11:30 -08:00
|
|
|
try {
|
2021-02-12 18:53:46 -08:00
|
|
|
return fs.readFileSync(fileName, 'utf-8');
|
2021-02-12 10:11:30 -08:00
|
|
|
} catch (e) {
|
2021-02-12 18:53:46 -08:00
|
|
|
return '// No source available';
|
2021-02-12 10:11:30 -08:00
|
|
|
}
|
|
|
|
}
|
2020-12-28 14:50:12 -08:00
|
|
|
}
|
2021-02-12 18:53:46 -08:00
|
|
|
|
|
|
|
function languageForFile(file: string) {
|
|
|
|
if (file.endsWith('.py'))
|
|
|
|
return 'python';
|
|
|
|
if (file.endsWith('.java'))
|
|
|
|
return 'java';
|
|
|
|
if (file.endsWith('.cs'))
|
|
|
|
return 'csharp';
|
|
|
|
return 'javascript';
|
2021-02-19 09:33:24 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
|
|
|
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
|
|
|
|
return false;
|
|
|
|
return metadata.method === 'pause';
|
|
|
|
}
|
|
|
|
|
|
|
|
function shouldPauseOnStep(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
|
|
|
return metadata.method === 'goto' || metadata.method === 'close';
|
|
|
|
}
|