test(recorder): add recorder sanity tests (#2582)
This commit is contained in:
parent
9e7ea3ff7b
commit
59d0f8728d
|
|
@ -15,6 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Writable } from 'stream';
|
||||
import { helper } from './helper';
|
||||
import * as network from './network';
|
||||
import { Page, PageBinding } from './page';
|
||||
|
|
@ -89,6 +90,7 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
|
|||
readonly _downloads = new Set<Download>();
|
||||
readonly _browserBase: BrowserBase;
|
||||
readonly _logger: InnerLogger;
|
||||
private _debugController: DebugController | undefined;
|
||||
|
||||
constructor(browserBase: BrowserBase, options: BrowserContextOptions) {
|
||||
super();
|
||||
|
|
@ -99,8 +101,16 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
|
|||
}
|
||||
|
||||
async _initialize() {
|
||||
if (helper.isDebugMode())
|
||||
new DebugController(this);
|
||||
if (helper.isDebugMode() || helper.isRecordMode()) {
|
||||
this._debugController = new DebugController(this, {
|
||||
recorderOutput: helper.isRecordMode() ? process.stdout : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_initDebugModeForTest(options: { recorderOutput: Writable }): DebugController {
|
||||
this._debugController = new DebugController(this, options);
|
||||
return this._debugController;
|
||||
}
|
||||
|
||||
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { Playwright } from '../server/playwright';
|
|||
import { BrowserType, LaunchOptions } from '../server/browserType';
|
||||
import { DeviceDescriptors } from '../deviceDescriptors';
|
||||
import { BrowserContextOptions } from '../browserContext';
|
||||
import { setRecorderMode } from '../debug/debugController';
|
||||
import { helper } from '../helper';
|
||||
|
||||
const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']);
|
||||
|
|
@ -104,8 +103,7 @@ async function open(options: Options, url: string | undefined) {
|
|||
}
|
||||
|
||||
async function record(options: Options, url: string | undefined) {
|
||||
helper.setDebugMode();
|
||||
setRecorderMode();
|
||||
helper.setRecordMode(true);
|
||||
return await open(options, url);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,33 +14,30 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Writable } from 'stream';
|
||||
import { BrowserContextBase } from '../browserContext';
|
||||
import { Events } from '../events';
|
||||
import * as frames from '../frames';
|
||||
import { Page } from '../page';
|
||||
import { RecorderController } from './recorderController';
|
||||
|
||||
let isRecorderMode = false;
|
||||
|
||||
export function setRecorderMode(): void {
|
||||
isRecorderMode = true;
|
||||
}
|
||||
|
||||
export class DebugController {
|
||||
constructor(context: BrowserContextBase) {
|
||||
constructor(context: BrowserContextBase, options: { recorderOutput?: Writable | undefined }) {
|
||||
const installInFrame = async (frame: frames.Frame) => {
|
||||
try {
|
||||
const mainContext = await frame._mainContext();
|
||||
await mainContext.createDebugScript({ console: true, record: isRecorderMode });
|
||||
await mainContext.createDebugScript({ console: true, record: !!options.recorderOutput });
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
|
||||
if (options.recorderOutput)
|
||||
new RecorderController(context, options.recorderOutput);
|
||||
|
||||
context.on(Events.BrowserContext.Page, (page: Page) => {
|
||||
for (const frame of page.frames())
|
||||
installInFrame(frame);
|
||||
page.on(Events.Page.FrameNavigated, installInFrame);
|
||||
new RecorderController(page);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ export class Recorder {
|
|||
private async _onClick(event: MouseEvent) {
|
||||
if ((event.target as Element).nodeName === 'SELECT')
|
||||
return;
|
||||
if ((event.target as Element).nodeName === 'INPUT') {
|
||||
// Check/uncheck are handled in input.
|
||||
if (((event.target as HTMLInputElement).type || '').toLowerCase() === 'checkbox')
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform action consumes this event and asks Playwright to perform it.
|
||||
this._performAction(event, {
|
||||
|
|
@ -76,8 +81,7 @@ export class Recorder {
|
|||
}
|
||||
if ((event.target as Element).nodeName === 'SELECT') {
|
||||
const selectElement = event.target as HTMLSelectElement;
|
||||
// TODO: move this to this._performAction
|
||||
window.recordPlaywrightAction({
|
||||
this._performAction(event, {
|
||||
name: 'select',
|
||||
selector,
|
||||
options: [...selectElement.selectedOptions].map(option => option.value),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export type ActionName =
|
|||
|
||||
export type ActionBase = {
|
||||
signals: Signal[],
|
||||
frameUrl?: string,
|
||||
committed?: boolean,
|
||||
}
|
||||
|
||||
|
|
@ -78,30 +77,35 @@ export type NavigationSignal = {
|
|||
type: 'assert' | 'await',
|
||||
};
|
||||
|
||||
export type Signal = NavigationSignal;
|
||||
export type PopupSignal = {
|
||||
name: 'popup',
|
||||
popupAlias: string,
|
||||
};
|
||||
|
||||
export type Signal = NavigationSignal | PopupSignal;
|
||||
|
||||
export function actionTitle(action: Action): string {
|
||||
switch (action.name) {
|
||||
case 'check':
|
||||
return 'Check';
|
||||
return `Check ${action.selector}`;
|
||||
case 'uncheck':
|
||||
return 'Uncheck';
|
||||
return `Uncheck ${action.selector}`;
|
||||
case 'click': {
|
||||
if (action.clickCount === 1)
|
||||
return 'Click';
|
||||
return `Click ${action.selector}`;
|
||||
if (action.clickCount === 2)
|
||||
return 'Double click';
|
||||
return `Double click ${action.selector}`;
|
||||
if (action.clickCount === 3)
|
||||
return 'Triple click';
|
||||
return `Triple click ${action.selector}`;
|
||||
return `${action.clickCount}× click`;
|
||||
}
|
||||
case 'fill':
|
||||
return 'Fill';
|
||||
return `Fill ${action.selector}`;
|
||||
case 'navigate':
|
||||
return 'Go to';
|
||||
return `Go to ${action.url}`;
|
||||
case 'press':
|
||||
return 'Press';
|
||||
return `Press ${action.key}` + (action.modifiers ? ' with modifiers' : '');
|
||||
case 'select':
|
||||
return 'Select';
|
||||
return `Select ${action.selector}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,37 +14,46 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as actions from './recorderActions';
|
||||
import { Writable } from 'stream';
|
||||
import { BrowserContextBase } from '../browserContext';
|
||||
import * as dom from '../dom';
|
||||
import { Events } from '../events';
|
||||
import * as frames from '../frames';
|
||||
import { Page } from '../page';
|
||||
import { Events } from '../events';
|
||||
import * as actions from './recorderActions';
|
||||
import { TerminalOutput } from './terminalOutput';
|
||||
import * as dom from '../dom';
|
||||
|
||||
export class RecorderController {
|
||||
private _page: Page;
|
||||
private _output = new TerminalOutput();
|
||||
private _output: TerminalOutput;
|
||||
private _performingAction = false;
|
||||
private _pageAliases = new Map<Page, string>();
|
||||
private _lastPopupOrdinal = 0;
|
||||
|
||||
constructor(page: Page) {
|
||||
this._page = page;
|
||||
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.
|
||||
this._page.exposeBinding('performPlaywrightAction',
|
||||
(source, action: actions.Action) => this._performAction(source.frame, action));
|
||||
// Other non-essential actions are simply being recorded.
|
||||
this._page.exposeBinding('recordPlaywrightAction',
|
||||
(source, action: actions.Action) => this._recordAction(source.frame, action));
|
||||
// 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 => {});
|
||||
|
||||
this._page.on(Events.Page.FrameNavigated, (frame: frames.Frame) => this._onFrameNavigated(frame));
|
||||
// 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));
|
||||
});
|
||||
}
|
||||
|
||||
private async _performAction(frame: frames.Frame, action: actions.Action) {
|
||||
if (frame !== this._page.mainFrame())
|
||||
action.frameUrl = frame.url();
|
||||
this._performingAction = true;
|
||||
this._output.addAction(action);
|
||||
this._recordAction(frame, action);
|
||||
if (action.name === 'click') {
|
||||
const { options } = toClickOptions(action);
|
||||
await frame.click(action.selector, options);
|
||||
|
|
@ -58,36 +67,48 @@ export class RecorderController {
|
|||
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;
|
||||
setTimeout(() => action.committed = true, 2000);
|
||||
setTimeout(() => action.committed = true, 5000);
|
||||
}
|
||||
|
||||
private async _recordAction(frame: frames.Frame, action: actions.Action) {
|
||||
if (frame !== this._page.mainFrame())
|
||||
action.frameUrl = frame.url();
|
||||
this._output.addAction(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({ name: 'navigation', url: frame.url(), type: this._performingAction ? 'assert' : 'await' });
|
||||
} else {
|
||||
// If navigation happens out of the blue, we just log it.
|
||||
this._output.addAction({
|
||||
name: 'navigate',
|
||||
url: this._page.url(),
|
||||
signals: [],
|
||||
});
|
||||
this._output.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function toClickOptions(action: actions.ClickAction): { method: 'click' | 'dblclick', options: dom.ClickOptions } {
|
||||
let method: 'click' | 'dblclick' = 'click';
|
||||
if (action.clickCount === 2)
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Writable } from 'stream';
|
||||
import * as dom from '../dom';
|
||||
import { Formatter, formatColors } from '../utils/formatter';
|
||||
import { Action, NavigationSignal, actionTitle } from './recorderActions';
|
||||
import { Frame } from '../frames';
|
||||
import { formatColors, Formatter } from '../utils/formatter';
|
||||
import { Action, actionTitle, NavigationSignal, PopupSignal, Signal } from './recorderActions';
|
||||
import { toModifiers } from './recorderController';
|
||||
|
||||
const { cst, cmt, fnc, kwd, prp, str } = formatColors;
|
||||
|
|
@ -24,8 +26,10 @@ const { cst, cmt, fnc, kwd, prp, str } = formatColors;
|
|||
export class TerminalOutput {
|
||||
private _lastAction: Action | undefined;
|
||||
private _lastActionText: string | undefined;
|
||||
private _out: Writable;
|
||||
|
||||
constructor() {
|
||||
constructor(out: Writable) {
|
||||
this._out = out;
|
||||
const formatter = new Formatter();
|
||||
|
||||
formatter.add(`
|
||||
|
|
@ -36,11 +40,10 @@ export class TerminalOutput {
|
|||
${kwd('const')} ${cst('browser')} = ${kwd('await')} ${cst(`chromium`)}.${fnc('launch')}();
|
||||
${kwd('const')} ${cst('page')} = ${kwd('await')} ${cst('browser')}.${fnc('newPage')}();
|
||||
`);
|
||||
process.stdout.write(formatter.format());
|
||||
process.stdout.write(`\n})();`);
|
||||
this._out.write(formatter.format() + '\n`})();`\n');
|
||||
}
|
||||
|
||||
addAction(action: Action) {
|
||||
addAction(pageAlias: string, frame: Frame, action: Action) {
|
||||
// We augment last action based on the type.
|
||||
let eraseLastAction = false;
|
||||
if (this._lastAction && action.name === 'fill' && this._lastAction.name === 'fill') {
|
||||
|
|
@ -57,55 +60,85 @@ export class TerminalOutput {
|
|||
eraseLastAction = true;
|
||||
}
|
||||
}
|
||||
this._printAction(action, eraseLastAction);
|
||||
this._printAction(pageAlias, frame, action, eraseLastAction);
|
||||
}
|
||||
|
||||
_printAction(action: Action, eraseLastAction: boolean) {
|
||||
_printAction(pageAlias: string, frame: Frame, action: Action, eraseLastAction: boolean) {
|
||||
// We erase terminating `})();` at all times.
|
||||
let eraseLines = 1;
|
||||
if (eraseLastAction && this._lastActionText)
|
||||
eraseLines += this._lastActionText.split('\n').length;
|
||||
// And we erase the last action too if augmenting.
|
||||
for (let i = 0; i < eraseLines; ++i)
|
||||
process.stdout.write('\u001B[F\u001B[2K');
|
||||
this._out.write('\u001B[1A\u001B[2K');
|
||||
|
||||
this._lastAction = action;
|
||||
this._lastActionText = this._generateAction(action);
|
||||
console.log(this._lastActionText); // eslint-disable-line no-console
|
||||
console.log(`})();`); // eslint-disable-line no-console
|
||||
this._lastActionText = this._generateAction(pageAlias, frame, action);
|
||||
this._out.write(this._lastActionText + '\n})();\n');
|
||||
}
|
||||
|
||||
lastAction(): Action | undefined {
|
||||
return this._lastAction;
|
||||
}
|
||||
|
||||
signal(signal: NavigationSignal) {
|
||||
signal(pageAlias: string, frame: Frame, signal: Signal) {
|
||||
if (this._lastAction) {
|
||||
this._lastAction.signals.push(signal);
|
||||
this._printAction(this._lastAction, true);
|
||||
this._printAction(pageAlias, frame, this._lastAction, true);
|
||||
}
|
||||
}
|
||||
|
||||
private _generateAction(action: Action): string {
|
||||
private _generateAction(pageAlias: string, frame: Frame, action: Action): string {
|
||||
const formatter = new Formatter(2);
|
||||
formatter.newLine();
|
||||
formatter.add(cmt(actionTitle(action)));
|
||||
|
||||
const subject = frame === frame._page.mainFrame() ? cst(pageAlias) :
|
||||
`${cst(pageAlias)}.${fnc('frame')}(${formatObject({ url: frame.url() })})`;
|
||||
|
||||
let navigationSignal: NavigationSignal | undefined;
|
||||
if (action.name !== 'navigate' && action.signals && action.signals.length)
|
||||
navigationSignal = action.signals[action.signals.length - 1];
|
||||
let popupSignal: PopupSignal | undefined;
|
||||
for (const signal of action.signals) {
|
||||
if (signal.name === 'navigation')
|
||||
navigationSignal = signal;
|
||||
if (signal.name === 'popup')
|
||||
popupSignal = signal;
|
||||
}
|
||||
|
||||
const waitForNavigation = navigationSignal && navigationSignal.type === 'await';
|
||||
const assertNavigation = navigationSignal && navigationSignal.type === 'assert';
|
||||
if (waitForNavigation) {
|
||||
formatter.add(`${kwd('await')} ${cst('Promise')}.${fnc('all')}([
|
||||
${cst('page')}.${fnc('waitForNavigation')}({ ${prp('url')}: ${str(navigationSignal!.url)} }),`);
|
||||
|
||||
const emitPromiseAll = waitForNavigation || popupSignal;
|
||||
if (emitPromiseAll) {
|
||||
// Generate either await Promise.all([]) or
|
||||
// const [popup1] = await Promise.all([]).
|
||||
let leftHandSide = '';
|
||||
if (popupSignal)
|
||||
leftHandSide = `${kwd('const')} [${cst(popupSignal.popupAlias)}] = `;
|
||||
formatter.add(`${leftHandSide}${kwd('await')} ${cst('Promise')}.${fnc('all')}([`);
|
||||
}
|
||||
|
||||
const subject = action.frameUrl ?
|
||||
`${cst('page')}.${fnc('frame')}(${formatObject({ url: action.frameUrl })})` : cst('page');
|
||||
// Popup signals.
|
||||
if (popupSignal)
|
||||
formatter.add(`${cst(pageAlias)}.${fnc('waitForEvent')}(${str('popup')}),`);
|
||||
|
||||
// Navigation signal.
|
||||
if (waitForNavigation)
|
||||
formatter.add(`${cst(pageAlias)}.${fnc('waitForNavigation')}({ ${prp('url')}: ${str(navigationSignal!.url)} }),`);
|
||||
|
||||
const prefix = waitForNavigation ? '' : kwd('await') + ' ';
|
||||
const actionCall = this._generateActionCall(action);
|
||||
const suffix = waitForNavigation ? '' : ';';
|
||||
formatter.add(`${prefix}${subject}.${actionCall}${suffix}`);
|
||||
|
||||
if (emitPromiseAll)
|
||||
formatter.add(`]);`);
|
||||
else if (assertNavigation)
|
||||
formatter.add(` ${cst('assert')}.${fnc('equal')}(${cst(pageAlias)}.${fnc('url')}(), ${str(navigationSignal!.url)});`);
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
private _generateActionCall(action: Action): string {
|
||||
switch (action.name) {
|
||||
case 'click': {
|
||||
let method = 'click';
|
||||
|
|
@ -120,36 +153,24 @@ export class TerminalOutput {
|
|||
if (action.clickCount > 2)
|
||||
options.clickCount = action.clickCount;
|
||||
const optionsString = formatOptions(options);
|
||||
formatter.add(`${prefix}${subject}.${fnc(method)}(${str(action.selector)}${optionsString})${suffix}`);
|
||||
break;
|
||||
return `${fnc(method)}(${str(action.selector)}${optionsString})`;
|
||||
}
|
||||
case 'check':
|
||||
formatter.add(`${prefix}${subject}.${fnc('check')}(${str(action.selector)})${suffix}`);
|
||||
break;
|
||||
return `${fnc('check')}(${str(action.selector)})`;
|
||||
case 'uncheck':
|
||||
formatter.add(`${prefix}${subject}.${fnc('uncheck')}(${str(action.selector)})${suffix}`);
|
||||
break;
|
||||
return `${fnc('uncheck')}(${str(action.selector)})`;
|
||||
case 'fill':
|
||||
formatter.add(`${prefix}${subject}.${fnc('fill')}(${str(action.selector)}, ${str(action.text)})${suffix}`);
|
||||
break;
|
||||
return `${fnc('fill')}(${str(action.selector)}, ${str(action.text)})`;
|
||||
case 'press': {
|
||||
const modifiers = toModifiers(action.modifiers);
|
||||
const shortcut = [...modifiers, action.key].join('+');
|
||||
formatter.add(`${prefix}${subject}.${fnc('press')}(${str(action.selector)}, ${str(shortcut)})${suffix}`);
|
||||
break;
|
||||
return `${fnc('press')}(${str(action.selector)}, ${str(shortcut)})`;
|
||||
}
|
||||
case 'navigate':
|
||||
formatter.add(`${prefix}${subject}.${fnc('goto')}(${str(action.url)})${suffix}`);
|
||||
break;
|
||||
return `${fnc('goto')}(${str(action.url)})`;
|
||||
case 'select':
|
||||
formatter.add(`${prefix}${subject}.${fnc('select')}(${str(action.selector)}, ${formatObject(action.options.length > 1 ? action.options : action.options[0])})${suffix}`);
|
||||
break;
|
||||
return `${fnc('selectOption')}(${str(action.selector)}, ${formatObject(action.options.length > 1 ? action.options : action.options[0])})`;
|
||||
}
|
||||
if (waitForNavigation)
|
||||
formatter.add(`]);`);
|
||||
else if (assertNavigation)
|
||||
formatter.add(` ${cst('assert')}.${fnc('equal')}(${cst('page')}.${fnc('url')}(), ${str(navigationSignal!.url)});`);
|
||||
return formatter.format();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,9 +95,6 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
|||
}
|
||||
|
||||
createDebugScript(options: { record?: boolean, console?: boolean }): Promise<js.JSHandle<DebugScript> | undefined> {
|
||||
if (!helper.isDebugMode())
|
||||
return Promise.resolve(undefined);
|
||||
|
||||
if (!this._debugScriptPromise) {
|
||||
const source = `new (${debugScriptSource.source})()`;
|
||||
this._debugScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new js.JSHandle(this, 'object', objectId)).then(async debugScript => {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export type RegisteredListener = {
|
|||
export type Listener = (...args: any[]) => void;
|
||||
|
||||
let isInDebugMode = !!getFromENV('PWDEBUG');
|
||||
let isInRecordMode = false;
|
||||
|
||||
class Helper {
|
||||
static evaluationString(fun: Function | string, ...args: any[]): string {
|
||||
|
|
@ -306,8 +307,16 @@ class Helper {
|
|||
return isInDebugMode;
|
||||
}
|
||||
|
||||
static setDebugMode() {
|
||||
isInDebugMode = true;
|
||||
static setDebugMode(enabled: boolean) {
|
||||
isInDebugMode = enabled;
|
||||
}
|
||||
|
||||
static isRecordMode(): boolean {
|
||||
return isInRecordMode;
|
||||
}
|
||||
|
||||
static setRecordMode(enabled: boolean) {
|
||||
isInRecordMode = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
196
test/recorder.spec.js
Normal file
196
test/recorder.spec.js
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const { Writable } = require('stream');
|
||||
const {FFOX, CHROMIUM, WEBKIT} = require('./utils').testOptions(browserType);
|
||||
|
||||
const pattern = [
|
||||
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
|
||||
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
|
||||
].join('|')
|
||||
class WritableBuffer {
|
||||
constructor() {
|
||||
this.lines = [];
|
||||
}
|
||||
|
||||
write(chunk) {
|
||||
if (chunk === '\u001B[F\u001B[2K') {
|
||||
this.lines.pop();
|
||||
return;
|
||||
}
|
||||
this.lines.push(...chunk.split('\n'));
|
||||
if (this._callback && chunk.includes(this._text))
|
||||
this._callback();
|
||||
}
|
||||
|
||||
waitFor(text) {
|
||||
if (this.lines.join('\n').includes(text))
|
||||
return Promise.resolve();
|
||||
this._text = text;
|
||||
return new Promise(f => this._callback = f);
|
||||
}
|
||||
|
||||
data() {
|
||||
return this.lines.join('\n');
|
||||
}
|
||||
|
||||
text() {
|
||||
const pattern = [
|
||||
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
|
||||
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
|
||||
].join('|');
|
||||
return this.data().replace(new RegExp(pattern, 'g'), '');
|
||||
}
|
||||
}
|
||||
|
||||
describe('Recorder', function() {
|
||||
beforeEach(async state => {
|
||||
state.context = await state.browser.newContext();
|
||||
state.output = new WritableBuffer();
|
||||
const debugController = state.context._initDebugModeForTest({ recorderOutput: state.output });
|
||||
});
|
||||
|
||||
afterEach(async state => {
|
||||
await state.context.close();
|
||||
});
|
||||
|
||||
it('should click', async function({context, output, server}) {
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`<button onclick="console.log('click')">Submit</button>`);
|
||||
const [message] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
output.waitFor('click'),
|
||||
page.dispatchEvent('button', 'click', { detail: 1 })
|
||||
]);
|
||||
expect(output.text()).toContain(`
|
||||
// Click text="Submit"
|
||||
await page.click('text="Submit"');`);
|
||||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
||||
it('should fill', async function({context, output, server}) {
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`<input id="input" name="name" oninput="console.log(input.value)"></input>`);
|
||||
const [message] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
output.waitFor('fill'),
|
||||
page.fill('input', 'John')
|
||||
]);
|
||||
expect(output.text()).toContain(`
|
||||
// Fill input[name=name]
|
||||
await page.fill('input[name=name]', 'John');`);
|
||||
expect(message.text()).toBe('John');
|
||||
});
|
||||
|
||||
it('should press', async function({context, output, server}) {
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`<input name="name" onkeypress="console.log('press')"></input>`);
|
||||
const [message] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
output.waitFor('press'),
|
||||
page.press('input', 'Shift+Enter')
|
||||
]);
|
||||
expect(output.text()).toContain(`
|
||||
// Press Enter with modifiers
|
||||
await page.press('input[name=name]', 'Shift+Enter');`);
|
||||
expect(message.text()).toBe('press');
|
||||
});
|
||||
|
||||
it('should check', async function({context, output, server}) {
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`);
|
||||
const [message] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
output.waitFor('check'),
|
||||
page.dispatchEvent('input', 'click', { detail: 1 })
|
||||
]);
|
||||
await output.waitFor('check');
|
||||
expect(output.text()).toContain(`
|
||||
// Check input[name=accept]
|
||||
await page.check('input[name=accept]');`);
|
||||
expect(message.text()).toBe("true");
|
||||
});
|
||||
|
||||
it('should uncheck', async function({context, output, server}) {
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`<input id="checkbox" type="checkbox" checked name="accept" onchange="console.log(checkbox.checked)"></input>`);
|
||||
const [message] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
output.waitFor('uncheck'),
|
||||
page.dispatchEvent('input', 'click', { detail: 1 })
|
||||
]);
|
||||
expect(output.text()).toContain(`
|
||||
// Uncheck input[name=accept]
|
||||
await page.uncheck('input[name=accept]');`);
|
||||
expect(message.text()).toBe("false");
|
||||
});
|
||||
|
||||
it('should select', async function({context, output, server}) {
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent('<select id="age" onchange="console.log(age.selectedOptions[0].value)"><option value="1"><option value="2"></select>');
|
||||
const [message] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
output.waitFor('select'),
|
||||
page.selectOption('select', '2')
|
||||
]);
|
||||
expect(output.text()).toContain(`
|
||||
// Select select[id=age]
|
||||
await page.selectOption('select[id=age]', '2');`);
|
||||
expect(message.text()).toBe("2");
|
||||
});
|
||||
|
||||
it('should await popup', async function({context, output, server}) {
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent('<a target=_blank rel=noopener href="/popup/popup.html">link</a>');
|
||||
const [popup] = await Promise.all([
|
||||
context.waitForEvent('page'),
|
||||
output.waitFor('waitForEvent'),
|
||||
page.dispatchEvent('a', 'click', { detail: 1 })
|
||||
]);
|
||||
expect(output.text()).toContain(`
|
||||
// Click text="link"
|
||||
const [popup1] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
await page.click('text="link"');
|
||||
]);`);
|
||||
expect(popup.url()).toBe(`${server.PREFIX}/popup/popup.html`);
|
||||
});
|
||||
|
||||
it('should await navigation', async function({context, output, server}) {
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`<a onclick="setTimeout(() => window.location.href='${server.PREFIX}/popup/popup.html', 1000)">link</a>`);
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
output.waitFor('waitForNavigation'),
|
||||
page.dispatchEvent('a', 'click', { detail: 1 })
|
||||
]);
|
||||
expect(output.text()).toContain(`
|
||||
// Click text="link"
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ url: '${server.PREFIX}/popup/popup.html' }),
|
||||
page.click('text="link"')
|
||||
]);`);
|
||||
expect(page.url()).toContain('/popup/popup.html');
|
||||
});
|
||||
});
|
||||
|
|
@ -216,6 +216,7 @@ module.exports = {
|
|||
'./browsercontext.spec.js',
|
||||
'./ignorehttpserrors.spec.js',
|
||||
'./popup.spec.js',
|
||||
'./recorder.spec.js',
|
||||
],
|
||||
environments: [customEnvironment, 'browser'],
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue