chore: simplify code generation (#5466)
This commit is contained in:
parent
b6bd7c0d6a
commit
30e68f6d1f
|
|
@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
})();`;
|
})();`;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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');`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue