chore: simplify code generation (#5466)

This commit is contained in:
Pavel Feldman 2021-02-16 18:13:26 -08:00 committed by GitHub
parent b6bd7c0d6a
commit 30e68f6d1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 673 additions and 417 deletions

View file

@ -14,9 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { EventEmitter } from 'events';
import type { BrowserContextOptions, LaunchOptions } from '../../../..'; import type { BrowserContextOptions, LaunchOptions } from '../../../..';
import { Frame } from '../../frames'; import { Frame } from '../../frames';
import { LanguageGenerator } from './language'; import { LanguageGenerator, LanguageGeneratorOptions } from './language';
import { Action, Signal } from './recorderActions'; import { Action, Signal } from './recorderActions';
import { describeFrame } from './utils'; import { describeFrame } from './utils';
@ -29,56 +30,55 @@ export type ActionInContext = {
committed?: boolean; committed?: boolean;
} }
export interface CodeGeneratorOutput { export class CodeGenerator extends EventEmitter {
printLn(text: string): void;
popLn(text: string): void;
}
export class CodeGenerator {
private _currentAction: ActionInContext | null = null; private _currentAction: ActionInContext | null = null;
private _lastAction: ActionInContext | null = null; private _lastAction: ActionInContext | null = null;
private _lastActionText: string | undefined; private _actions: ActionInContext[] = [];
private _languageGenerator: LanguageGenerator; private _enabled: boolean;
private _output: CodeGeneratorOutput; private _options: LanguageGeneratorOptions;
private _headerText = '';
private _footerText = '';
constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, output: CodeGeneratorOutput, languageGenerator: LanguageGenerator, deviceName: string | undefined, saveStorage: string | undefined) { constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName: string | undefined, saveStorage: string | undefined) {
this._output = output; super();
this._languageGenerator = languageGenerator;
launchOptions = { headless: false, ...launchOptions }; launchOptions = { headless: false, ...launchOptions };
if (generateHeaders) { this._enabled = generateHeaders;
this._headerText = this._languageGenerator.generateHeader(browserName, launchOptions, contextOptions, deviceName); this._options = { browserName, generateHeaders, launchOptions, contextOptions, deviceName, saveStorage };
this._footerText = '\n' + this._languageGenerator.generateFooter(saveStorage);
}
this.restart(); this.restart();
} }
restart() { restart() {
this._currentAction = null; this._currentAction = null;
this._lastAction = null; this._lastAction = null;
if (this._headerText) { this._actions = [];
this._output.printLn(this._headerText); }
this._output.printLn(this._footerText);
} setEnabled(enabled: boolean) {
this._enabled = enabled;
} }
addAction(action: ActionInContext) { addAction(action: ActionInContext) {
if (!this._enabled)
return;
this.willPerformAction(action); this.willPerformAction(action);
this.didPerformAction(action); this.didPerformAction(action);
} }
willPerformAction(action: ActionInContext) { willPerformAction(action: ActionInContext) {
if (!this._enabled)
return;
this._currentAction = action; this._currentAction = action;
} }
performedActionFailed(action: ActionInContext) { performedActionFailed(action: ActionInContext) {
if (!this._enabled)
return;
if (this._currentAction === action) if (this._currentAction === action)
this._currentAction = null; this._currentAction = null;
} }
didPerformAction(actionInContext: ActionInContext) { didPerformAction(actionInContext: ActionInContext) {
if (!this._enabled)
return;
const { action, pageAlias } = actionInContext; const { action, pageAlias } = actionInContext;
let eraseLastAction = false; let eraseLastAction = false;
if (this._lastAction && this._lastAction.pageAlias === pageAlias) { if (this._lastAction && this._lastAction.pageAlias === pageAlias) {
@ -94,41 +94,39 @@ export class CodeGenerator {
} }
if (lastAction && action.name === 'navigate' && lastAction.name === 'navigate') { if (lastAction && action.name === 'navigate' && lastAction.name === 'navigate') {
if (action.url === lastAction.url) { if (action.url === lastAction.url) {
// Already at a target URL.
this._currentAction = null; this._currentAction = null;
return; return;
} }
} }
for (const name of ['check', 'uncheck']) { for (const name of ['check', 'uncheck']) {
// Check and uncheck erase click.
if (lastAction && action.name === name && lastAction.name === 'click') { if (lastAction && action.name === name && lastAction.name === 'click') {
if ((action as any).selector === (lastAction as any).selector) if ((action as any).selector === (lastAction as any).selector)
eraseLastAction = true; eraseLastAction = true;
} }
} }
} }
this._printAction(actionInContext, eraseLastAction);
this._lastAction = actionInContext;
this._currentAction = null;
if (eraseLastAction)
this._actions.pop();
this._actions.push(actionInContext);
this.emit('change');
} }
commitLastAction() { commitLastAction() {
if (!this._enabled)
return;
const action = this._lastAction; const action = this._lastAction;
if (action) if (action)
action.committed = true; action.committed = true;
} }
_printAction(actionInContext: ActionInContext, eraseLastAction: boolean) {
if (this._footerText)
this._output.popLn(this._footerText);
if (eraseLastAction && this._lastActionText)
this._output.popLn(this._lastActionText);
const performingAction = !!this._currentAction;
this._currentAction = null;
this._lastAction = actionInContext;
this._lastActionText = this._languageGenerator.generateAction(actionInContext, performingAction);
this._output.printLn(this._lastActionText);
if (this._footerText)
this._output.printLn(this._footerText);
}
signal(pageAlias: string, frame: Frame, signal: Signal) { signal(pageAlias: string, frame: Frame, signal: Signal) {
if (!this._enabled)
return;
// Signal either arrives while action is being performed or shortly after. // Signal either arrives while action is being performed or shortly after.
if (this._currentAction) { if (this._currentAction) {
this._currentAction.action.signals.push(signal); this._currentAction.action.signals.push(signal);
@ -140,8 +138,9 @@ export class CodeGenerator {
return; return;
if (signal.name === 'download' && signals.length && signals[signals.length - 1].name === 'navigation') if (signal.name === 'download' && signals.length && signals[signals.length - 1].name === 'navigation')
signals.length = signals.length - 1; signals.length = signals.length - 1;
signal.isAsync = true;
this._lastAction.action.signals.push(signal); this._lastAction.action.signals.push(signal);
this._printAction(this._lastAction, true); this.emit('change');
return; return;
} }
@ -154,8 +153,19 @@ export class CodeGenerator {
name: 'navigate', name: 'navigate',
url: frame.url(), url: frame.url(),
signals: [], signals: [],
} },
}); });
} }
} }
generateText(languageGenerator: LanguageGenerator) {
const text = [];
if (this._options.generateHeaders)
text.push(languageGenerator.generateHeader(this._options));
for (const action of this._actions)
text.push(languageGenerator.generateAction(action));
if (this._options.generateHeaders)
text.push(languageGenerator.generateFooter(this._options.saveStorage));
return text.join('\n');
}
} }

View file

@ -14,16 +14,19 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions, LaunchOptions } from '../../../..'; import type { BrowserContextOptions } from '../../../..';
import { LanguageGenerator, sanitizeDeviceOptions } from './language'; import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language';
import { ActionInContext } from './codeGenerator'; import { ActionInContext } from './codeGenerator';
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions'; import { actionTitle, Action } from './recorderActions';
import { MouseClickOptions, toModifiers } from './utils'; import { MouseClickOptions, toModifiers } from './utils';
import deviceDescriptors = require('../../deviceDescriptors'); import deviceDescriptors = require('../../deviceDescriptors');
export class CSharpLanguageGenerator implements LanguageGenerator { export class CSharpLanguageGenerator implements LanguageGenerator {
id = 'csharp';
fileName = '<csharp>';
highlighter = 'csharp';
generateAction(actionInContext: ActionInContext, performingAction: boolean): string { generateAction(actionInContext: ActionInContext): string {
const { action, pageAlias } = actionInContext; const { action, pageAlias } = actionInContext;
const formatter = new CSharpFormatter(0); const formatter = new CSharpFormatter(0);
formatter.newLine(); formatter.newLine();
@ -41,63 +44,47 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
`${pageAlias}.GetFrame(name: ${quote(actionInContext.frameName)})` : `${pageAlias}.GetFrame(name: ${quote(actionInContext.frameName)})` :
`${pageAlias}.GetFrame(url: ${quote(actionInContext.frameUrl)})`); `${pageAlias}.GetFrame(url: ${quote(actionInContext.frameUrl)})`);
let navigationSignal: NavigationSignal | undefined; const signals = toSignalMap(action);
let popupSignal: PopupSignal | undefined;
let downloadSignal: DownloadSignal | undefined;
let dialogSignal: DialogSignal | undefined;
for (const signal of action.signals) {
if (signal.name === 'navigation')
navigationSignal = signal;
else if (signal.name === 'popup')
popupSignal = signal;
else if (signal.name === 'download')
downloadSignal = signal;
else if (signal.name === 'dialog')
dialogSignal = signal;
}
if (dialogSignal) { if (signals.dialog) {
formatter.add(` void ${pageAlias}_Dialog${dialogSignal.dialogAlias}_EventHandler(object sender, DialogEventArgs e) formatter.add(` void ${pageAlias}_Dialog${signals.dialog.dialogAlias}_EventHandler(object sender, DialogEventArgs e)
{ {
Console.WriteLine($"Dialog message: {e.Dialog.Message}"); Console.WriteLine($"Dialog message: {e.Dialog.Message}");
e.Dialog.DismissAsync(); e.Dialog.DismissAsync();
${pageAlias}.Dialog -= ${pageAlias}_Dialog${dialogSignal.dialogAlias}_EventHandler; ${pageAlias}.Dialog -= ${pageAlias}_Dialog${signals.dialog.dialogAlias}_EventHandler;
} }
${pageAlias}.Dialog += ${pageAlias}_Dialog${dialogSignal.dialogAlias}_EventHandler;`); ${pageAlias}.Dialog += ${pageAlias}_Dialog${signals.dialog.dialogAlias}_EventHandler;`);
} }
const waitForNavigation = navigationSignal && !performingAction; const emitTaskWhenAll = signals.waitForNavigation || signals.popup || signals.download;
const assertNavigation = navigationSignal && performingAction;
const emitTaskWhenAll = waitForNavigation || popupSignal || downloadSignal;
if (emitTaskWhenAll) { if (emitTaskWhenAll) {
if (popupSignal) if (signals.popup)
formatter.add(`var ${popupSignal.popupAlias}Task = ${pageAlias}.WaitForEventAsync(PageEvent.Popup)`); formatter.add(`var ${signals.popup.popupAlias}Task = ${pageAlias}.WaitForEventAsync(PageEvent.Popup)`);
else if (downloadSignal) else if (signals.download)
formatter.add(`var downloadTask = ${pageAlias}.WaitForEventAsync(PageEvent.Download);`); formatter.add(`var downloadTask = ${pageAlias}.WaitForEventAsync(PageEvent.Download);`);
formatter.add(`await Task.WhenAll(`); formatter.add(`await Task.WhenAll(`);
} }
// Popup signals. // Popup signals.
if (popupSignal) if (signals.popup)
formatter.add(`${popupSignal.popupAlias}Task,`); formatter.add(`${signals.popup.popupAlias}Task,`);
// Navigation signal. // Navigation signal.
if (waitForNavigation) if (signals.waitForNavigation)
formatter.add(`${pageAlias}.WaitForNavigationAsync(/*${quote(navigationSignal!.url)}*/),`); formatter.add(`${pageAlias}.WaitForNavigationAsync(/*${quote(signals.waitForNavigation.url)}*/),`);
// Download signals. // Download signals.
if (downloadSignal) if (signals.download)
formatter.add(`downloadTask,`); formatter.add(`downloadTask,`);
const prefix = (popupSignal || waitForNavigation || downloadSignal) ? '' : 'await '; const prefix = (signals.popup || signals.waitForNavigation || signals.download) ? '' : 'await ';
const actionCall = this._generateActionCall(action); const actionCall = this._generateActionCall(action);
const suffix = emitTaskWhenAll ? ');' : ';'; const suffix = emitTaskWhenAll ? ');' : ';';
formatter.add(`${prefix}${subject}.${actionCall}${suffix}`); formatter.add(`${prefix}${subject}.${actionCall}${suffix}`);
if (assertNavigation) if (signals.assertNavigation)
formatter.add(` // Assert.Equal(${quote(navigationSignal!.url)}, ${pageAlias}.Url);`); formatter.add(` // Assert.Equal(${quote(signals.assertNavigation.url)}, ${pageAlias}.Url);`);
return formatter.format(); return formatter.format();
} }
@ -142,19 +129,19 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
} }
} }
generateHeader(browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName?: string): string { generateHeader(options: LanguageGeneratorOptions): string {
const formatter = new CSharpFormatter(0); const formatter = new CSharpFormatter(0);
formatter.add(` formatter.add(`
await Playwright.InstallAsync(); await Playwright.InstallAsync();
using var playwright = await Playwright.CreateAsync(); using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.${toPascal(browserName)}.LaunchAsync(${formatArgs(launchOptions)}); await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatArgs(options.launchOptions)});
var context = await browser.NewContextAsync(${formatContextOptions(contextOptions, deviceName)});`); var context = await browser.NewContextAsync(${formatContextOptions(options.contextOptions, options.deviceName)});`);
return formatter.format(); return formatter.format();
} }
generateFooter(saveStorage: string | undefined): string { generateFooter(saveStorage: string | undefined): string {
const storageStateLine = saveStorage ? `\nawait context.StorageStateAsync(path: "${saveStorage}");` : ''; const storageStateLine = saveStorage ? `\nawait context.StorageStateAsync(path: "${saveStorage}");` : '';
return `// ---------------------${storageStateLine}`; return `\n// ---------------------${storageStateLine}`;
} }
} }

View file

@ -14,16 +14,19 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions, LaunchOptions } from '../../../..'; import type { BrowserContextOptions } from '../../../..';
import { LanguageGenerator, sanitizeDeviceOptions } from './language'; import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language';
import { ActionInContext } from './codeGenerator'; import { ActionInContext } from './codeGenerator';
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions'; import { Action, actionTitle } from './recorderActions';
import { MouseClickOptions, toModifiers } from './utils'; import { MouseClickOptions, toModifiers } from './utils';
import deviceDescriptors = require('../../deviceDescriptors'); import deviceDescriptors = require('../../deviceDescriptors');
export class JavaScriptLanguageGenerator implements LanguageGenerator { export class JavaScriptLanguageGenerator implements LanguageGenerator {
id = 'javascript';
fileName = '<javascript>';
highlighter = 'javascript';
generateAction(actionInContext: ActionInContext, performingAction: boolean): string { generateAction(actionInContext: ActionInContext): string {
const { action, pageAlias } = actionInContext; const { action, pageAlias } = actionInContext;
const formatter = new JavaScriptFormatter(2); const formatter = new JavaScriptFormatter(2);
formatter.newLine(); formatter.newLine();
@ -41,64 +44,48 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
`${pageAlias}.frame(${formatObject({ name: actionInContext.frameName })})` : `${pageAlias}.frame(${formatObject({ name: actionInContext.frameName })})` :
`${pageAlias}.frame(${formatObject({ url: actionInContext.frameUrl })})`); `${pageAlias}.frame(${formatObject({ url: actionInContext.frameUrl })})`);
let navigationSignal: NavigationSignal | undefined; const signals = toSignalMap(action);
let popupSignal: PopupSignal | undefined;
let downloadSignal: DownloadSignal | undefined;
let dialogSignal: DialogSignal | undefined;
for (const signal of action.signals) {
if (signal.name === 'navigation')
navigationSignal = signal;
else if (signal.name === 'popup')
popupSignal = signal;
else if (signal.name === 'download')
downloadSignal = signal;
else if (signal.name === 'dialog')
dialogSignal = signal;
}
if (dialogSignal) { if (signals.dialog) {
formatter.add(` ${pageAlias}.once('dialog', dialog => { formatter.add(` ${pageAlias}.once('dialog', dialog => {
console.log(\`Dialog message: $\{dialog.message()}\`); console.log(\`Dialog message: $\{dialog.message()}\`);
dialog.dismiss().catch(() => {}); dialog.dismiss().catch(() => {});
});`); });`);
} }
const waitForNavigation = navigationSignal && !performingAction; const emitPromiseAll = signals.waitForNavigation || signals.popup || signals.download;
const assertNavigation = navigationSignal && performingAction;
const emitPromiseAll = waitForNavigation || popupSignal || downloadSignal;
if (emitPromiseAll) { if (emitPromiseAll) {
// Generate either await Promise.all([]) or // Generate either await Promise.all([]) or
// const [popup1] = await Promise.all([]). // const [popup1] = await Promise.all([]).
let leftHandSide = ''; let leftHandSide = '';
if (popupSignal) if (signals.popup)
leftHandSide = `const [${popupSignal.popupAlias}] = `; leftHandSide = `const [${signals.popup.popupAlias}] = `;
else if (downloadSignal) else if (signals.download)
leftHandSide = `const [download] = `; leftHandSide = `const [download] = `;
formatter.add(`${leftHandSide}await Promise.all([`); formatter.add(`${leftHandSide}await Promise.all([`);
} }
// Popup signals. // Popup signals.
if (popupSignal) if (signals.popup)
formatter.add(`${pageAlias}.waitForEvent('popup'),`); formatter.add(`${pageAlias}.waitForEvent('popup'),`);
// Navigation signal. // Navigation signal.
if (waitForNavigation) if (signals.waitForNavigation)
formatter.add(`${pageAlias}.waitForNavigation(/*{ url: ${quote(navigationSignal!.url)} }*/),`); formatter.add(`${pageAlias}.waitForNavigation(/*{ url: ${quote(signals.waitForNavigation.url)} }*/),`);
// Download signals. // Download signals.
if (downloadSignal) if (signals.download)
formatter.add(`${pageAlias}.waitForEvent('download'),`); formatter.add(`${pageAlias}.waitForEvent('download'),`);
const prefix = (popupSignal || waitForNavigation || downloadSignal) ? '' : 'await '; const prefix = (signals.popup || signals.waitForNavigation || signals.download) ? '' : 'await ';
const actionCall = this._generateActionCall(action); const actionCall = this._generateActionCall(action);
const suffix = (waitForNavigation || emitPromiseAll) ? '' : ';'; const suffix = (signals.waitForNavigation || emitPromiseAll) ? '' : ';';
formatter.add(`${prefix}${subject}.${actionCall}${suffix}`); formatter.add(`${prefix}${subject}.${actionCall}${suffix}`);
if (emitPromiseAll) if (emitPromiseAll)
formatter.add(`]);`); formatter.add(`]);`);
else if (assertNavigation) else if (signals.assertNavigation)
formatter.add(` // assert.equal(${pageAlias}.url(), ${quote(navigationSignal!.url)});`); formatter.add(` // assert.equal(${pageAlias}.url(), ${quote(signals.assertNavigation.url)});`);
return formatter.format(); return formatter.format();
} }
@ -143,20 +130,20 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
} }
} }
generateHeader(browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName?: string): string { generateHeader(options: LanguageGeneratorOptions): string {
const formatter = new JavaScriptFormatter(); const formatter = new JavaScriptFormatter();
formatter.add(` formatter.add(`
const { ${browserName}${deviceName ? ', devices' : ''} } = require('playwright'); const { ${options.browserName}${options.deviceName ? ', devices' : ''} } = require('playwright');
(async () => { (async () => {
const browser = await ${browserName}.launch(${formatObjectOrVoid(launchOptions)}); const browser = await ${options.browserName}.launch(${formatObjectOrVoid(options.launchOptions)});
const context = await browser.newContext(${formatContextOptions(contextOptions, deviceName)});`); const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`);
return formatter.format(); return formatter.format();
} }
generateFooter(saveStorage: string | undefined): string { generateFooter(saveStorage: string | undefined): string {
const storageStateLine = saveStorage ? `\n await context.storageState({ path: '${saveStorage}' });` : ''; const storageStateLine = saveStorage ? `\n await context.storageState({ path: '${saveStorage}' });` : '';
return ` // ---------------------${storageStateLine} return `\n // ---------------------${storageStateLine}
await context.close(); await context.close();
await browser.close(); await browser.close();
})();`; })();`;

View file

@ -16,10 +16,23 @@
import type { BrowserContextOptions, LaunchOptions } from '../../../..'; import type { BrowserContextOptions, LaunchOptions } from '../../../..';
import { ActionInContext } from './codeGenerator'; import { ActionInContext } from './codeGenerator';
import { Action, DialogSignal, DownloadSignal, NavigationSignal, PopupSignal } from './recorderActions';
export type LanguageGeneratorOptions = {
browserName: string;
generateHeaders: boolean;
launchOptions: LaunchOptions;
contextOptions: BrowserContextOptions;
deviceName?: string;
saveStorage?: string;
};
export interface LanguageGenerator { export interface LanguageGenerator {
generateHeader(browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName?: string): string; id: string;
generateAction(actionInContext: ActionInContext, performingAction: boolean): string; fileName: string;
highlighter: string;
generateHeader(options: LanguageGeneratorOptions): string;
generateAction(actionInContext: ActionInContext): string;
generateFooter(saveStorage: string | undefined): string; generateFooter(saveStorage: string | undefined): string;
} }
@ -32,3 +45,30 @@ export function sanitizeDeviceOptions(device: any, options: BrowserContextOption
} }
return cleanedOptions; return cleanedOptions;
} }
export function toSignalMap(action: Action) {
let waitForNavigation: NavigationSignal | undefined;
let assertNavigation: NavigationSignal | undefined;
let popup: PopupSignal | undefined;
let download: DownloadSignal | undefined;
let dialog: DialogSignal | undefined;
for (const signal of action.signals) {
if (signal.name === 'navigation' && signal.isAsync)
waitForNavigation = signal;
else if (signal.name === 'navigation' && !signal.isAsync)
assertNavigation = signal;
else if (signal.name === 'popup')
popup = signal;
else if (signal.name === 'download')
download = signal;
else if (signal.name === 'dialog')
dialog = signal;
}
return {
waitForNavigation,
assertNavigation,
popup,
download,
dialog,
};
}

View file

@ -1,111 +0,0 @@
/**
* 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 fs from 'fs';
export interface RecorderOutput {
printLn(text: string): void;
popLn(text: string): void;
flush(): void;
}
export interface Writable {
write(data: string): void;
columns(): number;
}
export class OutputMultiplexer implements RecorderOutput {
private _outputs: RecorderOutput[]
private _enabled = true;
constructor(outputs: RecorderOutput[]) {
this._outputs = outputs;
}
setEnabled(enabled: boolean) {
this._enabled = enabled;
}
printLn(text: string) {
if (!this._enabled)
return;
for (const output of this._outputs)
output.printLn(text);
}
popLn(text: string) {
if (!this._enabled)
return;
for (const output of this._outputs)
output.popLn(text);
}
flush() {
if (!this._enabled)
return;
for (const output of this._outputs)
output.flush();
}
}
export class BufferedOutput implements RecorderOutput {
private _lines: string[] = [];
private _buffer: string | null = null;
private _onUpdate: ((text: string) => void);
constructor(onUpdate: (text: string) => void = () => {}) {
this._onUpdate = onUpdate;
}
printLn(text: string) {
this._buffer = null;
this._lines.push(...text.trimEnd().split('\n'));
this._onUpdate(this.buffer());
}
popLn(text: string) {
this._buffer = null;
this._lines.length -= text.trimEnd().split('\n').length;
}
buffer(): string {
if (this._buffer === null)
this._buffer = this._lines.join('\n');
return this._buffer;
}
clear() {
this._lines = [];
this._buffer = null;
this._onUpdate(this.buffer());
}
flush() {
}
}
export class FileOutput extends BufferedOutput implements RecorderOutput {
private _fileName: string;
constructor(fileName: string) {
super();
this._fileName = fileName;
process.on('exit', () => this.flush());
}
flush() {
fs.writeFileSync(this._fileName, this.buffer());
}
}

View file

@ -14,25 +14,31 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions, LaunchOptions } from '../../../..'; import type { BrowserContextOptions } from '../../../..';
import { LanguageGenerator, sanitizeDeviceOptions } from './language'; import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language';
import { ActionInContext } from './codeGenerator'; import { ActionInContext } from './codeGenerator';
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions'; import { actionTitle, Action } from './recorderActions';
import { MouseClickOptions, toModifiers } from './utils'; import { MouseClickOptions, toModifiers } from './utils';
import deviceDescriptors = require('../../deviceDescriptors'); import deviceDescriptors = require('../../deviceDescriptors');
export class PythonLanguageGenerator implements LanguageGenerator { export class PythonLanguageGenerator implements LanguageGenerator {
id = 'python';
fileName = '<python>';
highlighter = 'python';
private _awaitPrefix: '' | 'await '; private _awaitPrefix: '' | 'await ';
private _asyncPrefix: '' | 'async '; private _asyncPrefix: '' | 'async ';
private _isAsync: boolean; private _isAsync: boolean;
constructor(isAsync: boolean) { constructor(isAsync: boolean) {
this.id = isAsync ? 'python-async' : 'python';
this.fileName = isAsync ? '<async python>' : '<python>';
this._isAsync = isAsync; this._isAsync = isAsync;
this._awaitPrefix = isAsync ? 'await ' : ''; this._awaitPrefix = isAsync ? 'await ' : '';
this._asyncPrefix = isAsync ? 'async ' : ''; this._asyncPrefix = isAsync ? 'async ' : '';
} }
generateAction(actionInContext: ActionInContext, performingAction: boolean): string { generateAction(actionInContext: ActionInContext): string {
const { action, pageAlias } = actionInContext; const { action, pageAlias } = actionInContext;
const formatter = new PythonFormatter(4); const formatter = new PythonFormatter(4);
formatter.newLine(); formatter.newLine();
@ -50,47 +56,31 @@ export class PythonLanguageGenerator implements LanguageGenerator {
`${pageAlias}.frame(${formatOptions({ name: actionInContext.frameName }, false)})` : `${pageAlias}.frame(${formatOptions({ name: actionInContext.frameName }, false)})` :
`${pageAlias}.frame(${formatOptions({ url: actionInContext.frameUrl }, false)})`); `${pageAlias}.frame(${formatOptions({ url: actionInContext.frameUrl }, false)})`);
let navigationSignal: NavigationSignal | undefined; const signals = toSignalMap(action);
let popupSignal: PopupSignal | undefined;
let downloadSignal: DownloadSignal | undefined;
let dialogSignal: DialogSignal | undefined;
for (const signal of action.signals) {
if (signal.name === 'navigation')
navigationSignal = signal;
else if (signal.name === 'popup')
popupSignal = signal;
else if (signal.name === 'download')
downloadSignal = signal;
else if (signal.name === 'dialog')
dialogSignal = signal;
}
if (dialogSignal) if (signals.dialog)
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: asyncio.create_task(dialog.dismiss()))`); formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
const waitForNavigation = navigationSignal && !performingAction;
const assertNavigation = navigationSignal && performingAction;
const actionCall = this._generateActionCall(action); const actionCall = this._generateActionCall(action);
let code = `${this._awaitPrefix}${subject}.${actionCall}`; let code = `${this._awaitPrefix}${subject}.${actionCall}`;
if (popupSignal) { if (signals.popup) {
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as popup_info { code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as popup_info {
${code} ${code}
} }
${popupSignal.popupAlias} = popup_info.value`; ${signals.popup.popupAlias} = ${this._awaitPrefix}popup_info.value`;
} }
if (downloadSignal) { if (signals.download) {
code = `${this._asyncPrefix}with ${pageAlias}.expect_download() as download_info { code = `${this._asyncPrefix}with ${pageAlias}.expect_download() as download_info {
${code} ${code}
} }
download = download_info.value`; download = ${this._awaitPrefix}download_info.value`;
} }
if (waitForNavigation) { if (signals.waitForNavigation) {
code = ` code = `
# ${this._asyncPrefix}with ${pageAlias}.expect_navigation(url=${quote(navigationSignal!.url)}): # ${this._asyncPrefix}with ${pageAlias}.expect_navigation(url=${quote(signals.waitForNavigation.url)}):
${this._asyncPrefix}with ${pageAlias}.expect_navigation() { ${this._asyncPrefix}with ${pageAlias}.expect_navigation() {
${code} ${code}
}`; }`;
@ -98,8 +88,8 @@ export class PythonLanguageGenerator implements LanguageGenerator {
formatter.add(code); formatter.add(code);
if (assertNavigation) if (signals.assertNavigation)
formatter.add(` # assert ${pageAlias}.url == ${quote(navigationSignal!.url)}`); formatter.add(` # assert ${pageAlias}.url == ${quote(signals.assertNavigation.url)}`);
return formatter.format(); return formatter.format();
} }
@ -131,7 +121,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
case 'fill': case 'fill':
return `fill(${quote(action.selector)}, ${quote(action.text)})`; return `fill(${quote(action.selector)}, ${quote(action.text)})`;
case 'setInputFiles': case 'setInputFiles':
return `setInputFiles(${quote(action.selector)}, ${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`; return `set_input_files(${quote(action.selector)}, ${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
case 'press': { case 'press': {
const modifiers = toModifiers(action.modifiers); const modifiers = toModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); const shortcut = [...modifiers, action.key].join('+');
@ -140,11 +130,11 @@ export class PythonLanguageGenerator implements LanguageGenerator {
case 'navigate': case 'navigate':
return `goto(${quote(action.url)})`; return `goto(${quote(action.url)})`;
case 'select': case 'select':
return `selectOption(${quote(action.selector)}, ${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`; return `select_option(${quote(action.selector)}, ${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
} }
} }
generateHeader(browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName?: string): string { generateHeader(options: LanguageGeneratorOptions): string {
const formatter = new PythonFormatter(); const formatter = new PythonFormatter();
if (this._isAsync) { if (this._isAsync) {
formatter.add(` formatter.add(`
@ -152,15 +142,15 @@ import asyncio
from playwright.async_api import async_playwright from playwright.async_api import async_playwright
async def run(playwright) { async def run(playwright) {
browser = await playwright.${browserName}.launch(${formatOptions(launchOptions, false)}) browser = await playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
context = await browser.new_context(${formatContextOptions(contextOptions, deviceName)})`); context = await browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
} else { } else {
formatter.add(` formatter.add(`
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
def run(playwright) { def run(playwright) {
browser = playwright.${browserName}.launch(${formatOptions(launchOptions, false)}) browser = playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
context = browser.new_context(${formatContextOptions(contextOptions, deviceName)})`); context = browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
} }
return formatter.format(); return formatter.format();
} }
@ -168,7 +158,7 @@ def run(playwright) {
generateFooter(saveStorage: string | undefined): string { generateFooter(saveStorage: string | undefined): string {
if (this._isAsync) { if (this._isAsync) {
const storageStateLine = saveStorage ? `\n await context.storage_state(path="${saveStorage}")` : ''; const storageStateLine = saveStorage ? `\n await context.storage_state(path="${saveStorage}")` : '';
return ` # ---------------------${storageStateLine} return `\n # ---------------------${storageStateLine}
await context.close() await context.close()
await browser.close() await browser.close()
@ -178,7 +168,7 @@ async def main():
asyncio.run(main())`; asyncio.run(main())`;
} else { } else {
const storageStateLine = saveStorage ? `\n context.storage_state(path="${saveStorage}")` : ''; const storageStateLine = saveStorage ? `\n context.storage_state(path="${saveStorage}")` : '';
return ` # ---------------------${storageStateLine} return `\n # ---------------------${storageStateLine}
context.close() context.close()
browser.close() browser.close()

View file

@ -92,21 +92,25 @@ export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageActi
// Signals. // Signals.
export type NavigationSignal = { export type BaseSignal = {
isAsync?: boolean,
}
export type NavigationSignal = BaseSignal & {
name: 'navigation', name: 'navigation',
url: string, url: string,
}; };
export type PopupSignal = { export type PopupSignal = BaseSignal & {
name: 'popup', name: 'popup',
popupAlias: string, popupAlias: string,
}; };
export type DownloadSignal = { export type DownloadSignal = BaseSignal & {
name: 'download', name: 'download',
}; };
export type DialogSignal = { export type DialogSignal = BaseSignal & {
name: 'dialog', name: 'dialog',
dialogAlias: string, dialogAlias: string,
}; };

View file

@ -136,7 +136,7 @@ export class RecorderApp extends EventEmitter {
// Testing harness for runCLI mode. // Testing harness for runCLI mode.
{ {
if (process.env.PWCLI_EXIT_FOR_TEST) { if (process.env.PWCLI_EXIT_FOR_TEST && sources.length) {
process.stdout.write('\n-------------8<-------------\n'); process.stdout.write('\n-------------8<-------------\n');
process.stdout.write(sources[0].text); process.stdout.write(sources[0].text);
process.stdout.write('\n-------------8<-------------\n'); process.stdout.write('\n-------------8<-------------\n');

View file

@ -22,13 +22,11 @@ import { describeFrame, toClickOptions, toModifiers } from './recorder/utils';
import { Page } from '../page'; import { Page } from '../page';
import { Frame } from '../frames'; import { Frame } from '../frames';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { LanguageGenerator } from './recorder/language';
import { JavaScriptLanguageGenerator } from './recorder/javascript'; import { JavaScriptLanguageGenerator } from './recorder/javascript';
import { CSharpLanguageGenerator } from './recorder/csharp'; import { CSharpLanguageGenerator } from './recorder/csharp';
import { PythonLanguageGenerator } from './recorder/python'; import { PythonLanguageGenerator } from './recorder/python';
import * as recorderSource from '../../generated/recorderSource'; import * as recorderSource from '../../generated/recorderSource';
import * as consoleApiSource from '../../generated/consoleApiSource'; import * as consoleApiSource from '../../generated/consoleApiSource';
import { BufferedOutput, FileOutput, OutputMultiplexer, RecorderOutput } from './recorder/outputs';
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';
@ -47,14 +45,12 @@ export class RecorderSupplement {
private _timers = new Set<NodeJS.Timeout>(); private _timers = new Set<NodeJS.Timeout>();
private _context: BrowserContext; private _context: BrowserContext;
private _mode: Mode; private _mode: Mode;
private _output: OutputMultiplexer;
private _bufferedOutput: BufferedOutput;
private _recorderApp: RecorderApp | null = null; private _recorderApp: RecorderApp | null = null;
private _params: channels.BrowserContextRecorderSupplementEnableParams; private _params: channels.BrowserContextRecorderSupplementEnableParams;
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>(); private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
private _pausedCallsMetadata = new Map<CallMetadata, () => void>(); private _pausedCallsMetadata = new Map<CallMetadata, () => void>();
private _pauseOnNextStatement = true; private _pauseOnNextStatement = true;
private _recorderSource: Source; private _recorderSources: Source[];
private _userSources = new Map<string, 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> {
@ -75,32 +71,50 @@ export class RecorderSupplement {
this._context = context; this._context = context;
this._params = params; this._params = params;
this._mode = params.startRecording ? 'recording' : 'none'; this._mode = params.startRecording ? 'recording' : 'none';
let languageGenerator: LanguageGenerator; const language = params.language || context._options.sdkLanguage;
let language = params.language || context._options.sdkLanguage;
switch (language) {
case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break;
case 'csharp': languageGenerator = new CSharpLanguageGenerator(); break;
case 'python':
case 'python-async': languageGenerator = new PythonLanguageGenerator(params.language === 'python-async'); break;
default: throw new Error(`Invalid target: '${params.language}'`);
}
if (language === 'python-async')
language = 'python';
this._recorderSource = { file: '<recorder>', text: '', language, highlight: [] }; const languages = new Set([
this._bufferedOutput = new BufferedOutput(async text => { new JavaScriptLanguageGenerator(),
this._recorderSource.text = text; new PythonLanguageGenerator(false),
this._recorderSource.revealLine = text.split('\n').length - 1; new PythonLanguageGenerator(true),
new CSharpLanguageGenerator(),
]);
const primaryLanguage = [...languages].find(l => l.id === language)!;
if (!primaryLanguage)
throw new Error(`\n===============================\nInvalid target: '${params.language}'\n===============================\n`);
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;
}
this._pushAllSources(); this._pushAllSources();
}); });
const outputs: RecorderOutput[] = [ this._bufferedOutput ]; if (params.outputFile) {
if (params.outputFile) context.on(BrowserContext.Events.BeforeClose, () => {
outputs.push(new FileOutput(params.outputFile)); fs.writeFileSync(params.outputFile!, text);
this._output = new OutputMultiplexer(outputs); text = '';
this._output.setEnabled(!!params.startRecording); });
context.on(BrowserContext.Events.BeforeClose, () => this._output.flush()); process.on('exit', () => {
if (text)
const generator = new CodeGenerator(context._browser.options.name, !!params.startRecording, params.launchOptions || {}, params.contextOptions || {}, this._output, languageGenerator, params.device, params.saveStorage); fs.writeFileSync(params.outputFile!, text);
});
}
this._generator = generator; this._generator = generator;
} }
@ -114,7 +128,7 @@ export class RecorderSupplement {
if (data.event === 'setMode') { if (data.event === 'setMode') {
this._mode = data.params.mode; this._mode = data.params.mode;
recorderApp.setMode(this._mode); recorderApp.setMode(this._mode);
this._output.setEnabled(this._mode === 'recording'); this._generator.setEnabled(this._mode === 'recording');
if (this._mode !== 'none') if (this._mode !== 'none')
this._context.pages()[0].bringToFront().catch(() => {}); this._context.pages()[0].bringToFront().catch(() => {});
return; return;
@ -254,7 +268,6 @@ export class RecorderSupplement {
} }
private _clearScript(): void { private _clearScript(): void {
this._bufferedOutput.clear();
this._generator.restart(); this._generator.restart();
if (!!this._params.startRecording) { if (!!this._params.startRecording) {
for (const page of this._context.pages()) for (const page of this._context.pages())
@ -376,7 +389,7 @@ export class RecorderSupplement {
} }
private _pushAllSources() { private _pushAllSources() {
this._recorderApp?.setSources([this._recorderSource, ...this._userSources.values()]); this._recorderApp?.setSources([...this._recorderSources, ...this._userSources.values()]);
} }
async onBeforeInputAction(metadata: CallMetadata): Promise<void> { async onBeforeInputAction(metadata: CallMetadata): Promise<void> {

View file

@ -29,7 +29,7 @@ declare global {
playwrightSetSources: (sources: Source[]) => void; playwrightSetSources: (sources: Source[]) => void;
playwrightUpdateLogs: (callLogs: CallLog[]) => void; playwrightUpdateLogs: (callLogs: CallLog[]) => void;
dispatch(data: any): Promise<void>; dispatch(data: any): Promise<void>;
playwrightSourceEchoForTest: string; playwrightSourcesEchoForTest: Source[];
} }
} }
@ -38,20 +38,13 @@ export interface RecorderProps {
export const Recorder: React.FC<RecorderProps> = ({ export const Recorder: React.FC<RecorderProps> = ({
}) => { }) => {
const [source, setSource] = React.useState<Source>({ file: '', language: 'javascript', text: '', highlight: [] }); const [sources, setSources] = React.useState<Source[]>([]);
const [paused, setPaused] = React.useState(false); const [paused, setPaused] = React.useState(false);
const [log, setLog] = React.useState(new Map<number, CallLog>()); 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.playwrightSetSources = sources => { window.playwrightSetSources = setSources;
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;
window.playwrightUpdateLogs = callLogs => { window.playwrightUpdateLogs = callLogs => {
const newLog = new Map<number, CallLog>(log); const newLog = new Map<number, CallLog>(log);
@ -60,7 +53,20 @@ export const Recorder: React.FC<RecorderProps> = ({
setLog(newLog); setLog(newLog);
}; };
window.playwrightSourceEchoForTest = source.text; window.playwrightSourcesEchoForTest = sources;
const source = sources.find(source => {
let s = sources.find(s => s.revealLine);
if (!s)
s = sources.find(s => s.file === source.file);
if (!s)
s = sources[0];
return s;
}) || {
file: 'untitled',
text: '',
language: 'javascript',
highlight: []
};
const messagesEndRef = React.createRef<HTMLDivElement>(); const messagesEndRef = React.createRef<HTMLDivElement>();
React.useLayoutEffect(() => { React.useLayoutEffect(() => {

View file

@ -29,14 +29,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.hoverOverElement('button'); const selector = await recorder.hoverOverElement('button');
expect(selector).toBe('text=Submit'); expect(selector).toBe('text=Submit');
const [message] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console'), page.waitForEvent('console'),
recorder.waitForOutput('click'), recorder.waitForOutput('<javascript>', 'click'),
page.dispatchEvent('button', 'click', { detail: 1 }) page.dispatchEvent('button', 'click', { detail: 1 })
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Click text=Submit // Click text=Submit
await page.click('text=Submit');`); await page.click('text=Submit');`);
expect(sources.get('<python>').text).toContain(`
# Click text=Submit
page.click("text=Submit")`);
expect(sources.get('<async python>').text).toContain(`
# Click text=Submit
await page.click("text=Submit")`);
expect(sources.get('<csharp>').text).toContain(`
// Click text=Submit
await page.ClickAsync("text=Submit");`);
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
@ -57,12 +71,13 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.hoverOverElement('button'); const selector = await recorder.hoverOverElement('button');
expect(selector).toBe('text=Submit'); expect(selector).toBe('text=Submit');
const [message] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console'), page.waitForEvent('console'),
recorder.waitForOutput('click'), recorder.waitForOutput('<javascript>', 'click'),
page.dispatchEvent('button', 'click', { detail: 1 }) page.dispatchEvent('button', 'click', { detail: 1 })
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Click text=Submit // Click text=Submit
await page.click('text=Submit');`); await page.click('text=Submit');`);
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
@ -89,12 +104,12 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const divContents = await page.$eval(selector, div => div.outerHTML); const divContents = await page.$eval(selector, div => div.outerHTML);
expect(divContents).toBe(`<div onclick="console.log('click')"> Some long text here </div>`); expect(divContents).toBe(`<div onclick="console.log('click')"> Some long text here </div>`);
const [message] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console'), page.waitForEvent('console'),
recorder.waitForOutput('click'), recorder.waitForOutput('<javascript>', 'click'),
page.dispatchEvent('div', 'click', { detail: 1 }) page.dispatchEvent('div', 'click', { detail: 1 })
]); ]);
expect(recorder.output()).toContain(` expect(sources.get('<javascript>').text).toContain(`
// Click text=Some long text here // Click text=Some long text here
await page.click('text=Some long text here');`); await page.click('text=Some long text here');`);
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
@ -105,14 +120,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.focusElement('input'); const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="name"]'); expect(selector).toBe('input[name="name"]');
const [message] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console'), page.waitForEvent('console'),
recorder.waitForOutput('fill'), recorder.waitForOutput('<javascript>', 'fill'),
page.fill('input', 'John') page.fill('input', 'John')
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Fill input[name="name"] // Fill input[name="name"]
await page.fill('input[name="name"]', 'John');`); await page.fill('input[name="name"]', 'John');`);
expect(sources.get('<python>').text).toContain(`
# Fill input[name="name"]
page.fill(\"input[name=\\\"name\\\"]\", \"John\")`);
expect(sources.get('<async python>').text).toContain(`
# Fill input[name="name"]
await page.fill(\"input[name=\\\"name\\\"]\", \"John\")`);
expect(sources.get('<csharp>').text).toContain(`
// Fill input[name="name"]
await page.FillAsync(\"input[name=\\\"name\\\"]\", \"John\");`);
expect(message.text()).toBe('John'); expect(message.text()).toBe('John');
}); });
@ -121,12 +150,12 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.focusElement('textarea'); const selector = await recorder.focusElement('textarea');
expect(selector).toBe('textarea[name="name"]'); expect(selector).toBe('textarea[name="name"]');
const [message] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console'), page.waitForEvent('console'),
recorder.waitForOutput('fill'), recorder.waitForOutput('<javascript>', 'fill'),
page.fill('textarea', 'John') page.fill('textarea', 'John')
]); ]);
expect(recorder.output()).toContain(` expect(sources.get('<javascript>').text).toContain(`
// Fill textarea[name="name"] // Fill textarea[name="name"]
await page.fill('textarea[name="name"]', 'John');`); await page.fill('textarea[name="name"]', 'John');`);
expect(message.text()).toBe('John'); expect(message.text()).toBe('John');
@ -140,14 +169,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const messages: any[] = []; const messages: any[] = [];
page.on('console', message => messages.push(message)); page.on('console', message => messages.push(message));
await Promise.all([ const [, sources] = await Promise.all([
recorder.waitForActionPerformed(), recorder.waitForActionPerformed(),
recorder.waitForOutput('press'), recorder.waitForOutput('<javascript>', 'press'),
page.press('input', 'Shift+Enter') page.press('input', 'Shift+Enter')
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Press Enter with modifiers // Press Enter with modifiers
await page.press('input[name="name"]', 'Shift+Enter');`); await page.press('input[name="name"]', 'Shift+Enter');`);
expect(sources.get('<python>').text).toContain(`
# Press Enter with modifiers
page.press(\"input[name=\\\"name\\\"]\", \"Shift+Enter\")`);
expect(sources.get('<async python>').text).toContain(`
# Press Enter with modifiers
await page.press(\"input[name=\\\"name\\\"]\", \"Shift+Enter\")`);
expect(sources.get('<csharp>').text).toContain(`
// Press Enter with modifiers
await page.PressAsync(\"input[name=\\\"name\\\"]\", \"Shift+Enter\");`);
expect(messages[0].text()).toBe('press'); expect(messages[0].text()).toBe('press');
}); });
@ -158,24 +201,25 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
`); `);
await page.click('input[name="one"]'); await page.click('input[name="one"]');
await recorder.waitForOutput('click'); await recorder.waitForOutput('<javascript>', 'click');
await page.keyboard.type('foobar123'); await page.keyboard.type('foobar123');
await recorder.waitForOutput('foobar123'); await recorder.waitForOutput('<javascript>', 'foobar123');
await page.keyboard.press('Tab'); await page.keyboard.press('Tab');
await recorder.waitForOutput('Tab'); await recorder.waitForOutput('<javascript>', 'Tab');
await page.keyboard.type('barfoo321'); await page.keyboard.type('barfoo321');
await recorder.waitForOutput('barfoo321'); await recorder.waitForOutput('<javascript>', 'barfoo321');
expect(recorder.output()).toContain(` const text = recorder.sources().get('<javascript>').text;
expect(text).toContain(`
// Fill input[name="one"] // Fill input[name="one"]
await page.fill('input[name="one"]', 'foobar123');`); await page.fill('input[name="one"]', 'foobar123');`);
expect(recorder.output()).toContain(` expect(text).toContain(`
// Press Tab // Press Tab
await page.press('input[name="one"]', 'Tab');`); await page.press('input[name="one"]', 'Tab');`);
expect(recorder.output()).toContain(` expect(text).toContain(`
// Fill input[name="two"] // Fill input[name="two"]
await page.fill('input[name="two"]', 'barfoo321');`); await page.fill('input[name="two"]', 'barfoo321');`);
}); });
@ -190,12 +234,12 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
page.on('console', message => { page.on('console', message => {
messages.push(message); messages.push(message);
}); });
await Promise.all([ const [, sources] = await Promise.all([
recorder.waitForActionPerformed(), recorder.waitForActionPerformed(),
recorder.waitForOutput('press'), recorder.waitForOutput('<javascript>', 'press'),
page.press('input', 'ArrowDown') page.press('input', 'ArrowDown')
]); ]);
expect(recorder.output()).toContain(` expect(sources.get('<javascript>').text).toContain(`
// Press ArrowDown // Press ArrowDown
await page.press('input[name="name"]', 'ArrowDown');`); await page.press('input[name="name"]', 'ArrowDown');`);
expect(messages[0].text()).toBe('press:ArrowDown'); expect(messages[0].text()).toBe('press:ArrowDown');
@ -211,12 +255,12 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
page.on('console', message => { page.on('console', message => {
messages.push(message); messages.push(message);
}); });
await Promise.all([ const [, sources] = await Promise.all([
recorder.waitForActionPerformed(), recorder.waitForActionPerformed(),
recorder.waitForOutput('press'), recorder.waitForOutput('<javascript>', 'press'),
page.press('input', 'ArrowDown') page.press('input', 'ArrowDown')
]); ]);
expect(recorder.output()).toContain(` expect(sources.get('<javascript>').text).toContain(`
// Press ArrowDown // Press ArrowDown
await page.press('input[name="name"]', 'ArrowDown');`); await page.press('input[name="name"]', 'ArrowDown');`);
expect(messages.length).toBe(2); expect(messages.length).toBe(2);
@ -230,14 +274,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.focusElement('input'); const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="accept"]'); expect(selector).toBe('input[name="accept"]');
const [message] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console'), page.waitForEvent('console'),
recorder.waitForOutput('check'), recorder.waitForOutput('<javascript>', 'check'),
page.click('input') page.click('input')
]); ]);
await recorder.waitForOutput(`
expect(sources.get('<javascript>').text).toContain(`
// Check input[name="accept"] // Check input[name="accept"]
await page.check('input[name="accept"]');`); await page.check('input[name="accept"]');`);
expect(sources.get('<python>').text).toContain(`
# Check input[name="accept"]
page.check(\"input[name=\\\"accept\\\"]\")`);
expect(sources.get('<async python>').text).toContain(`
# Check input[name="accept"]
await page.check(\"input[name=\\\"accept\\\"]\")`);
expect(sources.get('<csharp>').text).toContain(`
// Check input[name="accept"]
await page.CheckAsync(\"input[name=\\\"accept\\\"]\");`);
expect(message.text()).toBe('true'); expect(message.text()).toBe('true');
}); });
@ -247,12 +305,13 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.focusElement('input'); const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="accept"]'); expect(selector).toBe('input[name="accept"]');
const [message] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console'), page.waitForEvent('console'),
recorder.waitForOutput('check'), recorder.waitForOutput('<javascript>', 'check'),
page.keyboard.press('Space') page.keyboard.press('Space')
]); ]);
await recorder.waitForOutput(`
expect(sources.get('<javascript>').text).toContain(`
// Check input[name="accept"] // Check input[name="accept"]
await page.check('input[name="accept"]');`); await page.check('input[name="accept"]');`);
expect(message.text()).toBe('true'); expect(message.text()).toBe('true');
@ -264,14 +323,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.focusElement('input'); const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="accept"]'); expect(selector).toBe('input[name="accept"]');
const [message] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console'), page.waitForEvent('console'),
recorder.waitForOutput('uncheck'), recorder.waitForOutput('<javascript>', 'uncheck'),
page.click('input') page.click('input')
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Uncheck input[name="accept"] // Uncheck input[name="accept"]
await page.uncheck('input[name="accept"]');`); await page.uncheck('input[name="accept"]');`);
expect(sources.get('<python>').text).toContain(`
# Uncheck input[name="accept"]
page.uncheck(\"input[name=\\\"accept\\\"]\")`);
expect(sources.get('<async python>').text).toContain(`
# Uncheck input[name="accept"]
await page.uncheck(\"input[name=\\\"accept\\\"]\")`);
expect(sources.get('<csharp>').text).toContain(`
// Uncheck input[name="accept"]
await page.UncheckAsync(\"input[name=\\\"accept\\\"]\");`);
expect(message.text()).toBe('false'); expect(message.text()).toBe('false');
}); });
@ -281,14 +354,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.hoverOverElement('select'); const selector = await recorder.hoverOverElement('select');
expect(selector).toBe('select'); expect(selector).toBe('select');
const [message] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console'), page.waitForEvent('console'),
recorder.waitForOutput('select'), recorder.waitForOutput('<javascript>', 'select'),
page.selectOption('select', '2') page.selectOption('select', '2')
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Select 2 // Select 2
await page.selectOption('select', '2');`); await page.selectOption('select', '2');`);
expect(sources.get('<python>').text).toContain(`
# Select 2
page.select_option(\"select\", \"2\")`);
expect(sources.get('<async python>').text).toContain(`
# Select 2
await page.select_option(\"select\", \"2\")`);
expect(sources.get('<csharp>').text).toContain(`
// Select 2
await page.SelectOptionAsync(\"select\", \"2\");`);
expect(message.text()).toBe('2'); expect(message.text()).toBe('2');
}); });
@ -300,17 +387,37 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.hoverOverElement('a'); const selector = await recorder.hoverOverElement('a');
expect(selector).toBe('text=link'); expect(selector).toBe('text=link');
const [popup] = await Promise.all([ const [popup, sources] = await Promise.all([
page.context().waitForEvent('page'), page.context().waitForEvent('page'),
recorder.waitForOutput('waitForEvent'), recorder.waitForOutput('<javascript>', 'waitForEvent'),
page.dispatchEvent('a', 'click', { detail: 1 }) page.dispatchEvent('a', 'click', { detail: 1 })
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Click text=link // Click text=link
const [page1] = await Promise.all([ const [page1] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup'),
page.click('text=link') page.click('text=link')
]);`); ]);`);
expect(sources.get('<python>').text).toContain(`
# Click text=link
with page.expect_popup() as popup_info:
page.click(\"text=link\")
page1 = popup_info.value`);
expect(sources.get('<async python>').text).toContain(`
# Click text=link
async with page.expect_popup() as popup_info:
await page.click(\"text=link\")
page1 = await popup_info.value`);
expect(sources.get('<csharp>').text).toContain(`
var page1Task = page.WaitForEventAsync(PageEvent.Popup)
await Task.WhenAll(
page1Task,
page.ClickAsync(\"text=link\"));`);
expect(popup.url()).toBe('about:blank'); expect(popup.url()).toBe('about:blank');
}); });
@ -319,15 +426,32 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.hoverOverElement('a'); const selector = await recorder.hoverOverElement('a');
expect(selector).toBe('text=link'); expect(selector).toBe('text=link');
await Promise.all([ const [, sources] = await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
recorder.waitForOutput('assert'), recorder.waitForOutput('<javascript>', 'assert'),
page.dispatchEvent('a', 'click', { detail: 1 }) page.dispatchEvent('a', 'click', { detail: 1 })
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Click text=link // Click text=link
await page.click('text=link'); await page.click('text=link');
// assert.equal(page.url(), 'about:blank#foo');`); // assert.equal(page.url(), 'about:blank#foo');`);
expect(sources.get('<python>').text).toContain(`
# Click text=link
page.click(\"text=link\")
# assert page.url == \"about:blank#foo\"`);
expect(sources.get('<async python>').text).toContain(`
# Click text=link
await page.click(\"text=link\")
# assert page.url == \"about:blank#foo\"`);
expect(sources.get('<csharp>').text).toContain(`
// Click text=link
await page.ClickAsync(\"text=link\");
// Assert.Equal(\"about:blank#foo\", page.Url);`);
expect(page.url()).toContain('about:blank#foo'); expect(page.url()).toContain('about:blank#foo');
}); });
@ -338,17 +462,37 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
const selector = await recorder.hoverOverElement('a'); const selector = await recorder.hoverOverElement('a');
expect(selector).toBe('text=link'); expect(selector).toBe('text=link');
await Promise.all([ const [, sources] = await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
recorder.waitForOutput('waitForNavigation'), recorder.waitForOutput('<javascript>', 'waitForNavigation'),
page.dispatchEvent('a', 'click', { detail: 1 }) page.dispatchEvent('a', 'click', { detail: 1 })
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Click text=link // Click text=link
await Promise.all([ await Promise.all([
page.waitForNavigation(/*{ url: 'about:blank#foo' }*/), page.waitForNavigation(/*{ url: 'about:blank#foo' }*/),
page.click('text=link') page.click('text=link')
]);`); ]);`);
expect(sources.get('<python>').text).toContain(`
# Click text=link
# with page.expect_navigation(url=\"about:blank#foo\"):
with page.expect_navigation():
page.click(\"text=link\")`);
expect(sources.get('<async python>').text).toContain(`
# Click text=link
# async with page.expect_navigation(url=\"about:blank#foo\"):
async with page.expect_navigation():
await page.click(\"text=link\")`);
expect(sources.get('<csharp>').text).toContain(`
// Click text=link
await Task.WhenAll(
page.WaitForNavigationAsync(/*\"about:blank#foo\"*/),
page.ClickAsync(\"text=link\"));`);
expect(page.url()).toContain('about:blank#foo'); expect(page.url()).toContain('about:blank#foo');
}); });
}); });

View file

@ -25,21 +25,64 @@ describe('cli codegen', (suite, { mode }) => {
}, () => { }, () => {
it('should contain open page', async ({ recorder }) => { it('should contain open page', async ({ recorder }) => {
await recorder.setContentAndWait(``); await recorder.setContentAndWait(``);
await recorder.waitForOutput(`const page = await context.newPage();`); const sources = await recorder.waitForOutput('<javascript>', `page.goto`);
expect(sources.get('<javascript>').text).toContain(`
// Open new page
const page = await context.newPage();`);
expect(sources.get('<python>').text).toContain(`
# Open new page
page = context.new_page()`);
expect(sources.get('<async python>').text).toContain(`
# Open new page
page = await context.new_page()`);
expect(sources.get('<csharp>').text).toContain(`
// Open new page
var page = await context.NewPageAsync();`);
}); });
it('should contain second page', async ({ context, recorder }) => { it('should contain second page', async ({ context, recorder }) => {
await recorder.setContentAndWait(``); await recorder.setContentAndWait(``);
await context.newPage(); await context.newPage();
await recorder.waitForOutput('page1'); const sources = await recorder.waitForOutput('<javascript>', 'page1');
expect(recorder.output()).toContain('const page1 = await context.newPage();');
expect(sources.get('<javascript>').text).toContain(`
// Open new page
const page1 = await context.newPage();`);
expect(sources.get('<python>').text).toContain(`
# Open new page
page1 = context.new_page()`);
expect(sources.get('<async python>').text).toContain(`
# Open new page
page1 = await context.new_page()`);
expect(sources.get('<csharp>').text).toContain(`
// Open new page
var page1 = await context.NewPageAsync();`);
}); });
it('should contain close page', async ({ context, recorder }) => { it('should contain close page', async ({ context, recorder }) => {
await recorder.setContentAndWait(``); await recorder.setContentAndWait(``);
await context.newPage(); await context.newPage();
await recorder.page.close(); await recorder.page.close();
await recorder.waitForOutput('page.close();'); const sources = await recorder.waitForOutput('<javascript>', 'page.close();');
expect(sources.get('<javascript>').text).toContain(`
await page.close();`);
expect(sources.get('<python>').text).toContain(`
page.close()`);
expect(sources.get('<async python>').text).toContain(`
await page.close()`);
expect(sources.get('<csharp>').text).toContain(`
await page.CloseAsync();`);
}); });
it('should not lead to an error if html gets clicked', async ({ context, recorder }) => { it('should not lead to an error if html gets clicked', async ({ context, recorder }) => {
@ -51,7 +94,7 @@ describe('cli codegen', (suite, { mode }) => {
const selector = await recorder.hoverOverElement('html'); const selector = await recorder.hoverOverElement('html');
expect(selector).toBe('html'); expect(selector).toBe('html');
await recorder.page.close(); await recorder.page.close();
await recorder.waitForOutput('page.close();'); await recorder.waitForOutput('<javascript>', 'page.close();');
expect(errors.length).toBe(0); expect(errors.length).toBe(0);
}); });
@ -68,9 +111,24 @@ describe('cli codegen', (suite, { mode }) => {
await page.setInputFiles('input[type=file]', 'test/assets/file-to-upload.txt'); await page.setInputFiles('input[type=file]', 'test/assets/file-to-upload.txt');
await page.click('input[type=file]'); await page.click('input[type=file]');
await recorder.waitForOutput(` const sources = await recorder.waitForOutput('<javascript>', 'setInputFiles');
expect(sources.get('<javascript>').text).toContain(`
// Upload file-to-upload.txt // Upload file-to-upload.txt
await page.setInputFiles('input[type="file"]', 'file-to-upload.txt');`); await page.setInputFiles('input[type="file"]', 'file-to-upload.txt');`);
expect(sources.get('<python>').text).toContain(`
# Upload file-to-upload.txt
page.set_input_files(\"input[type=\\\"file\\\"]\", \"file-to-upload.txt\")`);
expect(sources.get('<async python>').text).toContain(`
# Upload file-to-upload.txt
await page.set_input_files(\"input[type=\\\"file\\\"]\", \"file-to-upload.txt\")`);
expect(sources.get('<csharp>').text).toContain(`
// Upload file-to-upload.txt
await page.SetInputFilesAsync(\"input[type=\\\"file\\\"]\", \"file-to-upload.txt\");`);
}); });
it('should upload multiple files', (test, { browserName }) => { it('should upload multiple files', (test, { browserName }) => {
@ -86,9 +144,23 @@ describe('cli codegen', (suite, { mode }) => {
await page.setInputFiles('input[type=file]', ['test/assets/file-to-upload.txt', 'test/assets/file-to-upload-2.txt']); await page.setInputFiles('input[type=file]', ['test/assets/file-to-upload.txt', 'test/assets/file-to-upload-2.txt']);
await page.click('input[type=file]'); await page.click('input[type=file]');
await recorder.waitForOutput(` const sources = await recorder.waitForOutput('<javascript>', 'setInputFiles');
expect(sources.get('<javascript>').text).toContain(`
// Upload file-to-upload.txt, file-to-upload-2.txt // Upload file-to-upload.txt, file-to-upload-2.txt
await page.setInputFiles('input[type="file"]', ['file-to-upload.txt', 'file-to-upload-2.txt']);`); await page.setInputFiles('input[type=\"file\"]', ['file-to-upload.txt', 'file-to-upload-2.txt']);`);
expect(sources.get('<python>').text).toContain(`
# Upload file-to-upload.txt, file-to-upload-2.txt
page.set_input_files(\"input[type=\\\"file\\\"]\", [\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`);
expect(sources.get('<async python>').text).toContain(`
# Upload file-to-upload.txt, file-to-upload-2.txt
await page.set_input_files(\"input[type=\\\"file\\\"]\", [\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`);
expect(sources.get('<csharp>').text).toContain(`
// Upload file-to-upload.txt, file-to-upload-2.txt
await page.SetInputFilesAsync(\"input[type=\\\"file\\\"]\", new[] { \"file-to-upload.txt\", \"file-to-upload-2.txt\" });`);
}); });
it('should clear files', (test, { browserName }) => { it('should clear files', (test, { browserName }) => {
@ -104,9 +176,24 @@ describe('cli codegen', (suite, { mode }) => {
await page.setInputFiles('input[type=file]', []); await page.setInputFiles('input[type=file]', []);
await page.click('input[type=file]'); await page.click('input[type=file]');
await recorder.waitForOutput(` const sources = await recorder.waitForOutput('<javascript>', 'setInputFiles');
expect(sources.get('<javascript>').text).toContain(`
// Clear selected files // Clear selected files
await page.setInputFiles('input[type="file"]', []);`); await page.setInputFiles('input[type=\"file\"]', []);`);
expect(sources.get('<python>').text).toContain(`
# Clear selected files
page.set_input_files(\"input[type=\\\"file\\\"]\", []`);
expect(sources.get('<async python>').text).toContain(`
# Clear selected files
await page.set_input_files(\"input[type=\\\"file\\\"]\", []`);
expect(sources.get('<csharp>').text).toContain(`
// Clear selected files
await page.SetInputFilesAsync(\"input[type=\\\"file\\\"]\", new[] { });`);
}); });
it('should download files', async ({ page, recorder, httpServer }) => { it('should download files', async ({ page, recorder, httpServer }) => {
@ -129,12 +216,33 @@ describe('cli codegen', (suite, { mode }) => {
page.waitForEvent('download'), page.waitForEvent('download'),
page.click('text=Download') page.click('text=Download')
]); ]);
await recorder.waitForOutput(` const sources = await recorder.waitForOutput('<javascript>', 'waitForEvent');
expect(sources.get('<javascript>').text).toContain(`
// Click text=Download // Click text=Download
const [download] = await Promise.all([ const [download] = await Promise.all([
page.waitForEvent('download'), page.waitForEvent('download'),
page.click('text=Download') page.click('text=Download')
]);`); ]);`);
expect(sources.get('<python>').text).toContain(`
# Click text=Download
with page.expect_download() as download_info:
page.click(\"text=Download\")
download = download_info.value`);
expect(sources.get('<async python>').text).toContain(`
# Click text=Download
async with page.expect_download() as download_info:
await page.click(\"text=Download\")
download = await download_info.value`);
expect(sources.get('<csharp>').text).toContain(`
// Click text=Download
var downloadTask = page.WaitForEventAsync(PageEvent.Download);
await Task.WhenAll(
downloadTask,
page.ClickAsync(\"text=Download\"));`);
}); });
it('should handle dialogs', async ({ page, recorder }) => { it('should handle dialogs', async ({ page, recorder }) => {
@ -146,13 +254,38 @@ describe('cli codegen', (suite, { mode }) => {
await dialog.dismiss(); await dialog.dismiss();
}); });
await page.click('text=click me'); await page.click('text=click me');
await recorder.waitForOutput(`
const sources = await recorder.waitForOutput('<javascript>', 'once');
expect(sources.get('<javascript>').text).toContain(`
// Click text=click me // Click text=click me
page.once('dialog', dialog => { page.once('dialog', dialog => {
console.log(\`Dialog message: $\{dialog.message()}\`); console.log(\`Dialog message: \${dialog.message()}\`);
dialog.dismiss().catch(() => {}); dialog.dismiss().catch(() => {});
}); });
await page.click('text=click me')`); await page.click('text=click me');`);
expect(sources.get('<python>').text).toContain(`
# Click text=click me
page.once(\"dialog\", lambda dialog: dialog.dismiss())
page.click(\"text=click me\")`);
expect(sources.get('<async python>').text).toContain(`
# Click text=click me
page.once(\"dialog\", lambda dialog: dialog.dismiss())
await page.click(\"text=click me\")`);
expect(sources.get('<csharp>').text).toContain(`
// Click text=click me
void page_Dialog1_EventHandler(object sender, DialogEventArgs e)
{
Console.WriteLine($\"Dialog message: {e.Dialog.Message}\");
e.Dialog.DismissAsync();
page.Dialog -= page_Dialog1_EventHandler;
}
page.Dialog += page_Dialog1_EventHandler;
await page.ClickAsync(\"text=click me\");`);
}); });
it('should handle history.postData', async ({ page, recorder, httpServer }) => { it('should handle history.postData', async ({ page, recorder, httpServer }) => {
@ -169,7 +302,7 @@ describe('cli codegen', (suite, { mode }) => {
</script>`, httpServer.PREFIX); </script>`, httpServer.PREFIX);
for (let i = 1; i < 3; ++i) { for (let i = 1; i < 3; ++i) {
await page.evaluate('pushState()'); await page.evaluate('pushState()');
await recorder.waitForOutput(`await page.goto('${httpServer.PREFIX}/#seqNum=${i}');`); await recorder.waitForOutput('<javascript>', `await page.goto('${httpServer.PREFIX}/#seqNum=${i}');`);
} }
}); });
@ -182,14 +315,15 @@ describe('cli codegen', (suite, { mode }) => {
expect(selector).toBe('text=link'); expect(selector).toBe('text=link');
await page.click('a', { modifiers: [ platform === 'darwin' ? 'Meta' : 'Control'] }); await page.click('a', { modifiers: [ platform === 'darwin' ? 'Meta' : 'Control'] });
await recorder.waitForOutput('page1'); const sources = await recorder.waitForOutput('<javascript>', 'page1');
if (browserName === 'chromium') { if (browserName === 'chromium') {
expect(recorder.output()).toContain(` expect(sources.get('<javascript>').text).toContain(`
// Open new page // Open new page
const page1 = await context.newPage(); const page1 = await context.newPage();
page1.goto('about:blank?foo');`); page1.goto('about:blank?foo');`);
} else if (browserName === 'firefox') { } else if (browserName === 'firefox') {
expect(recorder.output()).toContain(` expect(sources.get('<javascript>').text).toContain(`
// Click text=link // Click text=link
const [page1] = await Promise.all([ const [page1] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup'),
@ -216,13 +350,23 @@ describe('cli codegen', (suite, { mode }) => {
await recorder.setPageContentAndWait(popup2, '<input id=name>'); await recorder.setPageContentAndWait(popup2, '<input id=name>');
await popup1.type('input', 'TextA'); await popup1.type('input', 'TextA');
await recorder.waitForOutput('TextA'); await recorder.waitForOutput('<javascript>', 'TextA');
await popup2.type('input', 'TextB'); await popup2.type('input', 'TextB');
await recorder.waitForOutput('TextB'); await recorder.waitForOutput('<javascript>', 'TextB');
expect(recorder.output()).toContain(`await page1.fill('input', 'TextA');`); const sources = recorder.sources();
expect(recorder.output()).toContain(`await page2.fill('input', 'TextB');`); expect(sources.get('<javascript>').text).toContain(`await page1.fill('input', 'TextA');`);
expect(sources.get('<javascript>').text).toContain(`await page2.fill('input', 'TextB');`);
expect(sources.get('<python>').text).toContain(`page1.fill(\"input\", \"TextA\")`);
expect(sources.get('<python>').text).toContain(`page2.fill(\"input\", \"TextB\")`);
expect(sources.get('<async python>').text).toContain(`await page1.fill(\"input\", \"TextA\")`);
expect(sources.get('<async python>').text).toContain(`await page2.fill(\"input\", \"TextB\")`);
expect(sources.get('<csharp>').text).toContain(`await page1.FillAsync(\"input\", \"TextA\");`);
expect(sources.get('<csharp>').text).toContain(`await page2.FillAsync(\"input\", \"TextB\");`);
}); });
it('click should emit events in order', async ({ page, recorder }) => { it('click should emit events in order', async ({ page, recorder }) => {
@ -239,7 +383,7 @@ describe('cli codegen', (suite, { mode }) => {
page.on('console', message => messages.push(message.text())); page.on('console', message => messages.push(message.text()));
await Promise.all([ await Promise.all([
page.click('button'), page.click('button'),
recorder.waitForOutput('page.click') recorder.waitForOutput('<javascript>', 'page.click')
]); ]);
expect(messages).toEqual(['mousedown', 'mouseup', 'click']); expect(messages).toEqual(['mousedown', 'mouseup', 'click']);
}); });
@ -283,35 +427,74 @@ describe('cli codegen', (suite, { mode }) => {
const frameTwo = page.frame({ name: 'two' }); const frameTwo = page.frame({ name: 'two' });
const otherFrame = page.frames().find(f => f !== page.mainFrame() && !f.name()); const otherFrame = page.frames().find(f => f !== page.mainFrame() && !f.name());
await Promise.all([ let [sources] = await Promise.all([
recorder.waitForOutput('one'), recorder.waitForOutput('<javascript>', 'one'),
frameOne.click('div'), frameOne.click('div'),
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Click text=Hi, I'm frame // Click text=Hi, I'm frame
await page.frame({ await page.frame({
name: 'one' name: 'one'
}).click('text=Hi, I\\'m frame');`); }).click('text=Hi, I\\'m frame');`);
await Promise.all([ expect(sources.get('<python>').text).toContain(`
recorder.waitForOutput('two'), # Click text=Hi, I'm frame
page.frame(name=\"one\").click(\"text=Hi, I'm frame\")`);
expect(sources.get('<async python>').text).toContain(`
# Click text=Hi, I'm frame
await page.frame(name=\"one\").click(\"text=Hi, I'm frame\")`);
expect(sources.get('<csharp>').text).toContain(`
// Click text=Hi, I'm frame
await page.GetFrame(name: \"one\").ClickAsync(\"text=Hi, I'm frame\");`);
[sources] = await Promise.all([
recorder.waitForOutput('<javascript>', 'two'),
frameTwo.click('div'), frameTwo.click('div'),
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Click text=Hi, I'm frame // Click text=Hi, I'm frame
await page.frame({ await page.frame({
name: 'two' name: 'two'
}).click('text=Hi, I\\'m frame');`); }).click('text=Hi, I\\'m frame');`);
await Promise.all([ expect(sources.get('<python>').text).toContain(`
recorder.waitForOutput('url: \''), # Click text=Hi, I'm frame
page.frame(name=\"two\").click(\"text=Hi, I'm frame\")`);
expect(sources.get('<async python>').text).toContain(`
# Click text=Hi, I'm frame
await page.frame(name=\"two\").click(\"text=Hi, I'm frame\")`);
expect(sources.get('<csharp>').text).toContain(`
// Click text=Hi, I'm frame
await page.GetFrame(name: \"two\").ClickAsync(\"text=Hi, I'm frame\");`);
[sources] = await Promise.all([
recorder.waitForOutput('<javascript>', 'url: \''),
otherFrame.click('div'), otherFrame.click('div'),
]); ]);
expect(recorder.output()).toContain(`
expect(sources.get('<javascript>').text).toContain(`
// Click text=Hi, I'm frame // Click text=Hi, I'm frame
await page.frame({ await page.frame({
url: '${otherFrame.url()}' url: 'http://localhost:${server.PORT}/frames/frame.html'
}).click('text=Hi, I\\'m frame');`); }).click('text=Hi, I\\'m frame');`);
expect(sources.get('<python>').text).toContain(`
# Click text=Hi, I'm frame
page.frame(url=\"http://localhost:${server.PORT}/frames/frame.html\").click(\"text=Hi, I'm frame\")`);
expect(sources.get('<async python>').text).toContain(`
# Click text=Hi, I'm frame
await page.frame(url=\"http://localhost:${server.PORT}/frames/frame.html\").click(\"text=Hi, I'm frame\")`);
expect(sources.get('<csharp>').text).toContain(`
// Click text=Hi, I'm frame
await page.GetFrame(url: \"http://localhost:${server.PORT}/frames/frame.html\").ClickAsync(\"text=Hi, I'm frame\");`);
}); });
it('should record navigations after identical pushState', async ({ page, recorder, httpServer }) => { it('should record navigations after identical pushState', async ({ page, recorder, httpServer }) => {
@ -329,6 +512,6 @@ describe('cli codegen', (suite, { mode }) => {
await page.evaluate('pushState()'); await page.evaluate('pushState()');
await page.goto(httpServer.PREFIX + '/page2.html'); await page.goto(httpServer.PREFIX + '/page2.html');
await recorder.waitForOutput(`await page.goto('${httpServer.PREFIX}/page2.html');`); await recorder.waitForOutput('<javascript>', `await page.goto('${httpServer.PREFIX}/page2.html');`);
}); });
}); });

View file

@ -20,6 +20,7 @@ import { ChildProcess, spawn } from 'child_process';
import { folio as baseFolio } from '../recorder.fixtures'; import { folio as baseFolio } from '../recorder.fixtures';
import type { BrowserType, Browser, Page } from '../..'; import type { BrowserType, Browser, Page } from '../..';
export { config } from 'folio'; export { config } from 'folio';
import type { Source } from '../../src/server/supplements/recorder/recorderTypes';
type WorkerFixtures = { type WorkerFixtures = {
browserType: BrowserType<Browser>; browserType: BrowserType<Browser>;
@ -66,7 +67,7 @@ class Recorder {
_actionReporterInstalled: boolean _actionReporterInstalled: boolean
_actionPerformedCallback: Function _actionPerformedCallback: Function
recorderPage: Page; recorderPage: Page;
private _text: string = ''; private _sources = new Map<string, Source>();
constructor(page: Page, recorderPage: Page) { constructor(page: Page, recorderPage: Page) {
this.page = page; this.page = page;
@ -97,24 +98,26 @@ class Recorder {
]); ]);
} }
async waitForOutput(text: string): Promise<void> { async waitForOutput(file: string, text: string): Promise<Map<string, Source>> {
this._text = await this.recorderPage.evaluate((text: string) => { const sources: Source[] = await this.recorderPage.evaluate((params: { text: string, file: string }) => {
const w = window as any; const w = window as any;
return new Promise(f => { return new Promise(f => {
const poll = () => { const poll = () => {
if (w.playwrightSourceEchoForTest && w.playwrightSourceEchoForTest.includes(text)) { const source = (w.playwrightSourcesEchoForTest || []).find((s: Source) => s.file === params.file);
f(w.playwrightSourceEchoForTest); if (source && source.text.includes(params.text))
return; f(w.playwrightSourcesEchoForTest);
}
setTimeout(poll, 300); setTimeout(poll, 300);
}; };
setTimeout(poll); poll();
}); });
}, text); }, { text, file });
for (const source of sources)
this._sources.set(source.file, source);
return this._sources;
} }
output(): string { sources(): Map<string, Source> {
return this._text; return this._sources;
} }
async waitForHighlight(action: () => Promise<void>): Promise<string> { async waitForHighlight(action: () => Promise<void>): Promise<string> {