mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
150 lines
5.7 KiB
TypeScript
150 lines
5.7 KiB
TypeScript
/**
|
|
* 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 { Writable } from 'stream';
|
|
import { BrowserContextBase } from '../browserContext';
|
|
import * as types from '../types';
|
|
import { Events } from '../events';
|
|
import * as frames from '../frames';
|
|
import { Page } from '../page';
|
|
import * as actions from './recorderActions';
|
|
import { TerminalOutput } from './terminalOutput';
|
|
|
|
export class RecorderController {
|
|
private _output: TerminalOutput;
|
|
private _performingAction = false;
|
|
private _pageAliases = new Map<Page, string>();
|
|
private _lastPopupOrdinal = 0;
|
|
private _timers = new Set<NodeJS.Timeout>();
|
|
|
|
constructor(context: BrowserContextBase, output: Writable) {
|
|
this._output = new TerminalOutput(output || process.stdout);
|
|
context.on(Events.BrowserContext.Page, (page: Page) => {
|
|
// First page is called page, others are called popup1, popup2, etc.
|
|
const pageName = this._pageAliases.size ? 'popup' + ++this._lastPopupOrdinal : 'page';
|
|
this._pageAliases.set(page, pageName);
|
|
page.on(Events.Page.Close, () => this._pageAliases.delete(page));
|
|
|
|
// Input actions that potentially lead to navigation are intercepted on the page and are
|
|
// performed by the Playwright.
|
|
page.exposeBinding('performPlaywrightAction',
|
|
(source, action: actions.Action) => this._performAction(source.frame, action)).catch(e => {});
|
|
|
|
// Other non-essential actions are simply being recorded.
|
|
page.exposeBinding('recordPlaywrightAction',
|
|
(source, action: actions.Action) => this._recordAction(source.frame, action)).catch(e => {});
|
|
|
|
page.on(Events.Page.FrameNavigated, (frame: frames.Frame) => this._onFrameNavigated(frame));
|
|
page.on(Events.Page.Popup, (popup: Page) => this._onPopup(page, popup));
|
|
});
|
|
|
|
context.once(Events.BrowserContext.Close, () => {
|
|
for (const timer of this._timers)
|
|
clearTimeout(timer);
|
|
this._timers.clear();
|
|
});
|
|
}
|
|
|
|
private async _performAction(frame: frames.Frame, action: actions.Action) {
|
|
this._performingAction = true;
|
|
this._recordAction(frame, action);
|
|
if (action.name === 'click') {
|
|
const { options } = toClickOptions(action);
|
|
await frame.click(action.selector, options);
|
|
}
|
|
if (action.name === 'press') {
|
|
const modifiers = toModifiers(action.modifiers);
|
|
const shortcut = [...modifiers, action.key].join('+');
|
|
await frame.press(action.selector, shortcut);
|
|
}
|
|
if (action.name === 'check')
|
|
await frame.check(action.selector);
|
|
if (action.name === 'uncheck')
|
|
await frame.uncheck(action.selector);
|
|
if (action.name === 'select')
|
|
await frame.selectOption(action.selector, action.options);
|
|
this._performingAction = false;
|
|
const timer = setTimeout(() => {
|
|
action.committed = true;
|
|
this._timers.delete(timer);
|
|
}, 5000);
|
|
this._timers.add(timer);
|
|
}
|
|
|
|
private async _recordAction(frame: frames.Frame, action: actions.Action) {
|
|
this._output.addAction(this._pageAliases.get(frame._page)!, frame, action);
|
|
}
|
|
|
|
private _onFrameNavigated(frame: frames.Frame) {
|
|
if (frame.parentFrame())
|
|
return;
|
|
const pageAlias = this._pageAliases.get(frame._page);
|
|
const action = this._output.lastAction();
|
|
// We only augment actions that have not been committed.
|
|
if (action && !action.committed && action.name !== 'navigate') {
|
|
// If we hit a navigation while action is executed, we assert it. Otherwise, we await it.
|
|
this._output.signal(pageAlias!, frame, { name: 'navigation', url: frame.url(), type: this._performingAction ? 'assert' : 'await' });
|
|
} else if (!action || action.committed) {
|
|
// If navigation happens out of the blue, we just log it.
|
|
this._output.addAction(
|
|
pageAlias!, frame, {
|
|
name: 'navigate',
|
|
url: frame.url(),
|
|
signals: [],
|
|
});
|
|
}
|
|
}
|
|
|
|
private _onPopup(page: Page, popup: Page) {
|
|
const pageAlias = this._pageAliases.get(page)!;
|
|
const popupAlias = this._pageAliases.get(popup)!;
|
|
const action = this._output.lastAction();
|
|
// We only augment actions that have not been committed.
|
|
if (action && !action.committed) {
|
|
// If we hit a navigation while action is executed, we assert it. Otherwise, we await it.
|
|
this._output.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias });
|
|
}
|
|
}
|
|
}
|
|
|
|
export function toClickOptions(action: actions.ClickAction): { method: 'click' | 'dblclick', options: types.MouseClickOptions } {
|
|
let method: 'click' | 'dblclick' = 'click';
|
|
if (action.clickCount === 2)
|
|
method = 'dblclick';
|
|
const modifiers = toModifiers(action.modifiers);
|
|
const options: types.MouseClickOptions = {};
|
|
if (action.button !== 'left')
|
|
options.button = action.button;
|
|
if (modifiers.length)
|
|
options.modifiers = modifiers;
|
|
if (action.clickCount > 2)
|
|
options.clickCount = action.clickCount;
|
|
return { method, options };
|
|
}
|
|
|
|
export function toModifiers(modifiers: number): ('Alt' | 'Control' | 'Meta' | 'Shift')[] {
|
|
const result: ('Alt' | 'Control' | 'Meta' | 'Shift')[] = [];
|
|
if (modifiers & 1)
|
|
result.push('Alt');
|
|
if (modifiers & 2)
|
|
result.push('Control');
|
|
if (modifiers & 4)
|
|
result.push('Meta');
|
|
if (modifiers & 8)
|
|
result.push('Shift');
|
|
return result;
|
|
}
|