chore: simplify code generation (#5466)
This commit is contained in:
parent
b6bd7c0d6a
commit
30e68f6d1f
|
|
@ -14,9 +14,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
import { Frame } from '../../frames';
|
||||
import { LanguageGenerator } from './language';
|
||||
import { LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import { Action, Signal } from './recorderActions';
|
||||
import { describeFrame } from './utils';
|
||||
|
||||
|
|
@ -29,56 +30,55 @@ export type ActionInContext = {
|
|||
committed?: boolean;
|
||||
}
|
||||
|
||||
export interface CodeGeneratorOutput {
|
||||
printLn(text: string): void;
|
||||
popLn(text: string): void;
|
||||
}
|
||||
|
||||
export class CodeGenerator {
|
||||
export class CodeGenerator extends EventEmitter {
|
||||
private _currentAction: ActionInContext | null = null;
|
||||
private _lastAction: ActionInContext | null = null;
|
||||
private _lastActionText: string | undefined;
|
||||
private _languageGenerator: LanguageGenerator;
|
||||
private _output: CodeGeneratorOutput;
|
||||
private _headerText = '';
|
||||
private _footerText = '';
|
||||
private _actions: ActionInContext[] = [];
|
||||
private _enabled: boolean;
|
||||
private _options: LanguageGeneratorOptions;
|
||||
|
||||
constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, output: CodeGeneratorOutput, languageGenerator: LanguageGenerator, deviceName: string | undefined, saveStorage: string | undefined) {
|
||||
this._output = output;
|
||||
this._languageGenerator = languageGenerator;
|
||||
constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName: string | undefined, saveStorage: string | undefined) {
|
||||
super();
|
||||
|
||||
launchOptions = { headless: false, ...launchOptions };
|
||||
if (generateHeaders) {
|
||||
this._headerText = this._languageGenerator.generateHeader(browserName, launchOptions, contextOptions, deviceName);
|
||||
this._footerText = '\n' + this._languageGenerator.generateFooter(saveStorage);
|
||||
}
|
||||
this._enabled = generateHeaders;
|
||||
this._options = { browserName, generateHeaders, launchOptions, contextOptions, deviceName, saveStorage };
|
||||
this.restart();
|
||||
}
|
||||
|
||||
restart() {
|
||||
this._currentAction = null;
|
||||
this._lastAction = null;
|
||||
if (this._headerText) {
|
||||
this._output.printLn(this._headerText);
|
||||
this._output.printLn(this._footerText);
|
||||
}
|
||||
this._actions = [];
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean) {
|
||||
this._enabled = enabled;
|
||||
}
|
||||
|
||||
addAction(action: ActionInContext) {
|
||||
if (!this._enabled)
|
||||
return;
|
||||
this.willPerformAction(action);
|
||||
this.didPerformAction(action);
|
||||
}
|
||||
|
||||
willPerformAction(action: ActionInContext) {
|
||||
if (!this._enabled)
|
||||
return;
|
||||
this._currentAction = action;
|
||||
}
|
||||
|
||||
performedActionFailed(action: ActionInContext) {
|
||||
if (!this._enabled)
|
||||
return;
|
||||
if (this._currentAction === action)
|
||||
this._currentAction = null;
|
||||
}
|
||||
|
||||
didPerformAction(actionInContext: ActionInContext) {
|
||||
if (!this._enabled)
|
||||
return;
|
||||
const { action, pageAlias } = actionInContext;
|
||||
let eraseLastAction = false;
|
||||
if (this._lastAction && this._lastAction.pageAlias === pageAlias) {
|
||||
|
|
@ -94,41 +94,39 @@ export class CodeGenerator {
|
|||
}
|
||||
if (lastAction && action.name === 'navigate' && lastAction.name === 'navigate') {
|
||||
if (action.url === lastAction.url) {
|
||||
// Already at a target URL.
|
||||
this._currentAction = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const name of ['check', 'uncheck']) {
|
||||
// Check and uncheck erase click.
|
||||
if (lastAction && action.name === name && lastAction.name === 'click') {
|
||||
if ((action as any).selector === (lastAction as any).selector)
|
||||
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() {
|
||||
if (!this._enabled)
|
||||
return;
|
||||
const action = this._lastAction;
|
||||
if (action)
|
||||
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) {
|
||||
if (!this._enabled)
|
||||
return;
|
||||
// Signal either arrives while action is being performed or shortly after.
|
||||
if (this._currentAction) {
|
||||
this._currentAction.action.signals.push(signal);
|
||||
|
|
@ -140,8 +138,9 @@ export class CodeGenerator {
|
|||
return;
|
||||
if (signal.name === 'download' && signals.length && signals[signals.length - 1].name === 'navigation')
|
||||
signals.length = signals.length - 1;
|
||||
signal.isAsync = true;
|
||||
this._lastAction.action.signals.push(signal);
|
||||
this._printAction(this._lastAction, true);
|
||||
this.emit('change');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -154,8 +153,19 @@ export class CodeGenerator {
|
|||
name: 'navigate',
|
||||
url: frame.url(),
|
||||
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.
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
import { LanguageGenerator, sanitizeDeviceOptions } from './language';
|
||||
import type { BrowserContextOptions } from '../../../..';
|
||||
import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language';
|
||||
import { ActionInContext } from './codeGenerator';
|
||||
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions';
|
||||
import { actionTitle, Action } from './recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from './utils';
|
||||
import deviceDescriptors = require('../../deviceDescriptors');
|
||||
|
||||
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 formatter = new CSharpFormatter(0);
|
||||
formatter.newLine();
|
||||
|
|
@ -41,63 +44,47 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
`${pageAlias}.GetFrame(name: ${quote(actionInContext.frameName)})` :
|
||||
`${pageAlias}.GetFrame(url: ${quote(actionInContext.frameUrl)})`);
|
||||
|
||||
let navigationSignal: NavigationSignal | undefined;
|
||||
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;
|
||||
}
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
if (dialogSignal) {
|
||||
formatter.add(` void ${pageAlias}_Dialog${dialogSignal.dialogAlias}_EventHandler(object sender, DialogEventArgs e)
|
||||
if (signals.dialog) {
|
||||
formatter.add(` void ${pageAlias}_Dialog${signals.dialog.dialogAlias}_EventHandler(object sender, DialogEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"Dialog message: {e.Dialog.Message}");
|
||||
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 assertNavigation = navigationSignal && performingAction;
|
||||
|
||||
const emitTaskWhenAll = waitForNavigation || popupSignal || downloadSignal;
|
||||
const emitTaskWhenAll = signals.waitForNavigation || signals.popup || signals.download;
|
||||
if (emitTaskWhenAll) {
|
||||
if (popupSignal)
|
||||
formatter.add(`var ${popupSignal.popupAlias}Task = ${pageAlias}.WaitForEventAsync(PageEvent.Popup)`);
|
||||
else if (downloadSignal)
|
||||
if (signals.popup)
|
||||
formatter.add(`var ${signals.popup.popupAlias}Task = ${pageAlias}.WaitForEventAsync(PageEvent.Popup)`);
|
||||
else if (signals.download)
|
||||
formatter.add(`var downloadTask = ${pageAlias}.WaitForEventAsync(PageEvent.Download);`);
|
||||
|
||||
formatter.add(`await Task.WhenAll(`);
|
||||
}
|
||||
|
||||
// Popup signals.
|
||||
if (popupSignal)
|
||||
formatter.add(`${popupSignal.popupAlias}Task,`);
|
||||
if (signals.popup)
|
||||
formatter.add(`${signals.popup.popupAlias}Task,`);
|
||||
|
||||
// Navigation signal.
|
||||
if (waitForNavigation)
|
||||
formatter.add(`${pageAlias}.WaitForNavigationAsync(/*${quote(navigationSignal!.url)}*/),`);
|
||||
if (signals.waitForNavigation)
|
||||
formatter.add(`${pageAlias}.WaitForNavigationAsync(/*${quote(signals.waitForNavigation.url)}*/),`);
|
||||
|
||||
// Download signals.
|
||||
if (downloadSignal)
|
||||
if (signals.download)
|
||||
formatter.add(`downloadTask,`);
|
||||
|
||||
const prefix = (popupSignal || waitForNavigation || downloadSignal) ? '' : 'await ';
|
||||
const prefix = (signals.popup || signals.waitForNavigation || signals.download) ? '' : 'await ';
|
||||
const actionCall = this._generateActionCall(action);
|
||||
const suffix = emitTaskWhenAll ? ');' : ';';
|
||||
formatter.add(`${prefix}${subject}.${actionCall}${suffix}`);
|
||||
|
||||
if (assertNavigation)
|
||||
formatter.add(` // Assert.Equal(${quote(navigationSignal!.url)}, ${pageAlias}.Url);`);
|
||||
if (signals.assertNavigation)
|
||||
formatter.add(` // Assert.Equal(${quote(signals.assertNavigation.url)}, ${pageAlias}.Url);`);
|
||||
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);
|
||||
formatter.add(`
|
||||
await Playwright.InstallAsync();
|
||||
using var playwright = await Playwright.CreateAsync();
|
||||
await using var browser = await playwright.${toPascal(browserName)}.LaunchAsync(${formatArgs(launchOptions)});
|
||||
var context = await browser.NewContextAsync(${formatContextOptions(contextOptions, deviceName)});`);
|
||||
await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatArgs(options.launchOptions)});
|
||||
var context = await browser.NewContextAsync(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
generateFooter(saveStorage: string | undefined): string {
|
||||
const storageStateLine = saveStorage ? `\nawait context.StorageStateAsync(path: "${saveStorage}");` : '';
|
||||
return `// ---------------------${storageStateLine}`;
|
||||
return `\n// ---------------------${storageStateLine}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,16 +14,19 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
import { LanguageGenerator, sanitizeDeviceOptions } from './language';
|
||||
import type { BrowserContextOptions } from '../../../..';
|
||||
import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language';
|
||||
import { ActionInContext } from './codeGenerator';
|
||||
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions';
|
||||
import { Action, actionTitle } from './recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from './utils';
|
||||
import deviceDescriptors = require('../../deviceDescriptors');
|
||||
|
||||
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 formatter = new JavaScriptFormatter(2);
|
||||
formatter.newLine();
|
||||
|
|
@ -41,64 +44,48 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
`${pageAlias}.frame(${formatObject({ name: actionInContext.frameName })})` :
|
||||
`${pageAlias}.frame(${formatObject({ url: actionInContext.frameUrl })})`);
|
||||
|
||||
let navigationSignal: NavigationSignal | undefined;
|
||||
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;
|
||||
}
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
if (dialogSignal) {
|
||||
if (signals.dialog) {
|
||||
formatter.add(` ${pageAlias}.once('dialog', dialog => {
|
||||
console.log(\`Dialog message: $\{dialog.message()}\`);
|
||||
dialog.dismiss().catch(() => {});
|
||||
});`);
|
||||
}
|
||||
|
||||
const waitForNavigation = navigationSignal && !performingAction;
|
||||
const assertNavigation = navigationSignal && performingAction;
|
||||
|
||||
const emitPromiseAll = waitForNavigation || popupSignal || downloadSignal;
|
||||
const emitPromiseAll = signals.waitForNavigation || signals.popup || signals.download;
|
||||
if (emitPromiseAll) {
|
||||
// Generate either await Promise.all([]) or
|
||||
// const [popup1] = await Promise.all([]).
|
||||
let leftHandSide = '';
|
||||
if (popupSignal)
|
||||
leftHandSide = `const [${popupSignal.popupAlias}] = `;
|
||||
else if (downloadSignal)
|
||||
if (signals.popup)
|
||||
leftHandSide = `const [${signals.popup.popupAlias}] = `;
|
||||
else if (signals.download)
|
||||
leftHandSide = `const [download] = `;
|
||||
formatter.add(`${leftHandSide}await Promise.all([`);
|
||||
}
|
||||
|
||||
// Popup signals.
|
||||
if (popupSignal)
|
||||
if (signals.popup)
|
||||
formatter.add(`${pageAlias}.waitForEvent('popup'),`);
|
||||
|
||||
// Navigation signal.
|
||||
if (waitForNavigation)
|
||||
formatter.add(`${pageAlias}.waitForNavigation(/*{ url: ${quote(navigationSignal!.url)} }*/),`);
|
||||
if (signals.waitForNavigation)
|
||||
formatter.add(`${pageAlias}.waitForNavigation(/*{ url: ${quote(signals.waitForNavigation.url)} }*/),`);
|
||||
|
||||
// Download signals.
|
||||
if (downloadSignal)
|
||||
if (signals.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 suffix = (waitForNavigation || emitPromiseAll) ? '' : ';';
|
||||
const suffix = (signals.waitForNavigation || emitPromiseAll) ? '' : ';';
|
||||
formatter.add(`${prefix}${subject}.${actionCall}${suffix}`);
|
||||
|
||||
if (emitPromiseAll)
|
||||
formatter.add(`]);`);
|
||||
else if (assertNavigation)
|
||||
formatter.add(` // assert.equal(${pageAlias}.url(), ${quote(navigationSignal!.url)});`);
|
||||
else if (signals.assertNavigation)
|
||||
formatter.add(` // assert.equal(${pageAlias}.url(), ${quote(signals.assertNavigation.url)});`);
|
||||
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();
|
||||
formatter.add(`
|
||||
const { ${browserName}${deviceName ? ', devices' : ''} } = require('playwright');
|
||||
const { ${options.browserName}${options.deviceName ? ', devices' : ''} } = require('playwright');
|
||||
|
||||
(async () => {
|
||||
const browser = await ${browserName}.launch(${formatObjectOrVoid(launchOptions)});
|
||||
const context = await browser.newContext(${formatContextOptions(contextOptions, deviceName)});`);
|
||||
const browser = await ${options.browserName}.launch(${formatObjectOrVoid(options.launchOptions)});
|
||||
const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
generateFooter(saveStorage: string | undefined): string {
|
||||
const storageStateLine = saveStorage ? `\n await context.storageState({ path: '${saveStorage}' });` : '';
|
||||
return ` // ---------------------${storageStateLine}
|
||||
return `\n // ---------------------${storageStateLine}
|
||||
await context.close();
|
||||
await browser.close();
|
||||
})();`;
|
||||
|
|
|
|||
|
|
@ -16,10 +16,23 @@
|
|||
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
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 {
|
||||
generateHeader(browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName?: string): string;
|
||||
generateAction(actionInContext: ActionInContext, performingAction: boolean): string;
|
||||
id: string;
|
||||
fileName: string;
|
||||
highlighter: string;
|
||||
generateHeader(options: LanguageGeneratorOptions): string;
|
||||
generateAction(actionInContext: ActionInContext): string;
|
||||
generateFooter(saveStorage: string | undefined): string;
|
||||
}
|
||||
|
||||
|
|
@ -32,3 +45,30 @@ export function sanitizeDeviceOptions(device: any, options: BrowserContextOption
|
|||
}
|
||||
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.
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
import { LanguageGenerator, sanitizeDeviceOptions } from './language';
|
||||
import type { BrowserContextOptions } from '../../../..';
|
||||
import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language';
|
||||
import { ActionInContext } from './codeGenerator';
|
||||
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions';
|
||||
import { actionTitle, Action } from './recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from './utils';
|
||||
import deviceDescriptors = require('../../deviceDescriptors');
|
||||
|
||||
export class PythonLanguageGenerator implements LanguageGenerator {
|
||||
id = 'python';
|
||||
fileName = '<python>';
|
||||
highlighter = 'python';
|
||||
|
||||
private _awaitPrefix: '' | 'await ';
|
||||
private _asyncPrefix: '' | 'async ';
|
||||
private _isAsync: boolean;
|
||||
|
||||
constructor(isAsync: boolean) {
|
||||
this.id = isAsync ? 'python-async' : 'python';
|
||||
this.fileName = isAsync ? '<async python>' : '<python>';
|
||||
this._isAsync = isAsync;
|
||||
this._awaitPrefix = isAsync ? 'await ' : '';
|
||||
this._asyncPrefix = isAsync ? 'async ' : '';
|
||||
}
|
||||
|
||||
generateAction(actionInContext: ActionInContext, performingAction: boolean): string {
|
||||
generateAction(actionInContext: ActionInContext): string {
|
||||
const { action, pageAlias } = actionInContext;
|
||||
const formatter = new PythonFormatter(4);
|
||||
formatter.newLine();
|
||||
|
|
@ -50,47 +56,31 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
`${pageAlias}.frame(${formatOptions({ name: actionInContext.frameName }, false)})` :
|
||||
`${pageAlias}.frame(${formatOptions({ url: actionInContext.frameUrl }, false)})`);
|
||||
|
||||
let navigationSignal: NavigationSignal | undefined;
|
||||
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;
|
||||
}
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
if (dialogSignal)
|
||||
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: asyncio.create_task(dialog.dismiss()))`);
|
||||
|
||||
const waitForNavigation = navigationSignal && !performingAction;
|
||||
const assertNavigation = navigationSignal && performingAction;
|
||||
if (signals.dialog)
|
||||
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
|
||||
|
||||
const actionCall = this._generateActionCall(action);
|
||||
let code = `${this._awaitPrefix}${subject}.${actionCall}`;
|
||||
|
||||
if (popupSignal) {
|
||||
if (signals.popup) {
|
||||
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as popup_info {
|
||||
${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}
|
||||
}
|
||||
download = download_info.value`;
|
||||
download = ${this._awaitPrefix}download_info.value`;
|
||||
}
|
||||
|
||||
if (waitForNavigation) {
|
||||
if (signals.waitForNavigation) {
|
||||
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() {
|
||||
${code}
|
||||
}`;
|
||||
|
|
@ -98,8 +88,8 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
|
||||
formatter.add(code);
|
||||
|
||||
if (assertNavigation)
|
||||
formatter.add(` # assert ${pageAlias}.url == ${quote(navigationSignal!.url)}`);
|
||||
if (signals.assertNavigation)
|
||||
formatter.add(` # assert ${pageAlias}.url == ${quote(signals.assertNavigation.url)}`);
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +121,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
case 'fill':
|
||||
return `fill(${quote(action.selector)}, ${quote(action.text)})`;
|
||||
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': {
|
||||
const modifiers = toModifiers(action.modifiers);
|
||||
const shortcut = [...modifiers, action.key].join('+');
|
||||
|
|
@ -140,11 +130,11 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
case 'navigate':
|
||||
return `goto(${quote(action.url)})`;
|
||||
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();
|
||||
if (this._isAsync) {
|
||||
formatter.add(`
|
||||
|
|
@ -152,15 +142,15 @@ import asyncio
|
|||
from playwright.async_api import async_playwright
|
||||
|
||||
async def run(playwright) {
|
||||
browser = await playwright.${browserName}.launch(${formatOptions(launchOptions, false)})
|
||||
context = await browser.new_context(${formatContextOptions(contextOptions, deviceName)})`);
|
||||
browser = await playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
|
||||
context = await browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
|
||||
} else {
|
||||
formatter.add(`
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
def run(playwright) {
|
||||
browser = playwright.${browserName}.launch(${formatOptions(launchOptions, false)})
|
||||
context = browser.new_context(${formatContextOptions(contextOptions, deviceName)})`);
|
||||
browser = playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
|
||||
context = browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
|
@ -168,7 +158,7 @@ def run(playwright) {
|
|||
generateFooter(saveStorage: string | undefined): string {
|
||||
if (this._isAsync) {
|
||||
const storageStateLine = saveStorage ? `\n await context.storage_state(path="${saveStorage}")` : '';
|
||||
return ` # ---------------------${storageStateLine}
|
||||
return `\n # ---------------------${storageStateLine}
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
|
|
@ -178,7 +168,7 @@ async def main():
|
|||
asyncio.run(main())`;
|
||||
} else {
|
||||
const storageStateLine = saveStorage ? `\n context.storage_state(path="${saveStorage}")` : '';
|
||||
return ` # ---------------------${storageStateLine}
|
||||
return `\n # ---------------------${storageStateLine}
|
||||
context.close()
|
||||
browser.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -92,21 +92,25 @@ export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageActi
|
|||
|
||||
// Signals.
|
||||
|
||||
export type NavigationSignal = {
|
||||
export type BaseSignal = {
|
||||
isAsync?: boolean,
|
||||
}
|
||||
|
||||
export type NavigationSignal = BaseSignal & {
|
||||
name: 'navigation',
|
||||
url: string,
|
||||
};
|
||||
|
||||
export type PopupSignal = {
|
||||
export type PopupSignal = BaseSignal & {
|
||||
name: 'popup',
|
||||
popupAlias: string,
|
||||
};
|
||||
|
||||
export type DownloadSignal = {
|
||||
export type DownloadSignal = BaseSignal & {
|
||||
name: 'download',
|
||||
};
|
||||
|
||||
export type DialogSignal = {
|
||||
export type DialogSignal = BaseSignal & {
|
||||
name: 'dialog',
|
||||
dialogAlias: string,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export class RecorderApp extends EventEmitter {
|
|||
|
||||
// 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(sources[0].text);
|
||||
process.stdout.write('\n-------------8<-------------\n');
|
||||
|
|
|
|||
|
|
@ -22,13 +22,11 @@ import { describeFrame, toClickOptions, toModifiers } from './recorder/utils';
|
|||
import { Page } from '../page';
|
||||
import { Frame } from '../frames';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { LanguageGenerator } from './recorder/language';
|
||||
import { JavaScriptLanguageGenerator } from './recorder/javascript';
|
||||
import { CSharpLanguageGenerator } from './recorder/csharp';
|
||||
import { PythonLanguageGenerator } from './recorder/python';
|
||||
import * as recorderSource from '../../generated/recorderSource';
|
||||
import * as consoleApiSource from '../../generated/consoleApiSource';
|
||||
import { BufferedOutput, FileOutput, OutputMultiplexer, RecorderOutput } from './recorder/outputs';
|
||||
import { RecorderApp } from './recorder/recorderApp';
|
||||
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
|
||||
import { Point } from '../../common/types';
|
||||
|
|
@ -47,14 +45,12 @@ export class RecorderSupplement {
|
|||
private _timers = new Set<NodeJS.Timeout>();
|
||||
private _context: BrowserContext;
|
||||
private _mode: Mode;
|
||||
private _output: OutputMultiplexer;
|
||||
private _bufferedOutput: BufferedOutput;
|
||||
private _recorderApp: RecorderApp | null = null;
|
||||
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
||||
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
||||
private _pausedCallsMetadata = new Map<CallMetadata, () => void>();
|
||||
private _pauseOnNextStatement = true;
|
||||
private _recorderSource: Source;
|
||||
private _recorderSources: Source[];
|
||||
private _userSources = new Map<string, Source>();
|
||||
|
||||
static getOrCreate(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<RecorderSupplement> {
|
||||
|
|
@ -75,32 +71,50 @@ export class RecorderSupplement {
|
|||
this._context = context;
|
||||
this._params = params;
|
||||
this._mode = params.startRecording ? 'recording' : 'none';
|
||||
let languageGenerator: LanguageGenerator;
|
||||
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';
|
||||
const language = params.language || context._options.sdkLanguage;
|
||||
|
||||
this._recorderSource = { file: '<recorder>', text: '', language, highlight: [] };
|
||||
this._bufferedOutput = new BufferedOutput(async text => {
|
||||
this._recorderSource.text = text;
|
||||
this._recorderSource.revealLine = text.split('\n').length - 1;
|
||||
const languages = new Set([
|
||||
new JavaScriptLanguageGenerator(),
|
||||
new PythonLanguageGenerator(false),
|
||||
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();
|
||||
});
|
||||
const outputs: RecorderOutput[] = [ this._bufferedOutput ];
|
||||
if (params.outputFile)
|
||||
outputs.push(new FileOutput(params.outputFile));
|
||||
this._output = new OutputMultiplexer(outputs);
|
||||
this._output.setEnabled(!!params.startRecording);
|
||||
context.on(BrowserContext.Events.BeforeClose, () => this._output.flush());
|
||||
|
||||
const generator = new CodeGenerator(context._browser.options.name, !!params.startRecording, params.launchOptions || {}, params.contextOptions || {}, this._output, languageGenerator, params.device, params.saveStorage);
|
||||
if (params.outputFile) {
|
||||
context.on(BrowserContext.Events.BeforeClose, () => {
|
||||
fs.writeFileSync(params.outputFile!, text);
|
||||
text = '';
|
||||
});
|
||||
process.on('exit', () => {
|
||||
if (text)
|
||||
fs.writeFileSync(params.outputFile!, text);
|
||||
});
|
||||
}
|
||||
this._generator = generator;
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +128,7 @@ export class RecorderSupplement {
|
|||
if (data.event === 'setMode') {
|
||||
this._mode = data.params.mode;
|
||||
recorderApp.setMode(this._mode);
|
||||
this._output.setEnabled(this._mode === 'recording');
|
||||
this._generator.setEnabled(this._mode === 'recording');
|
||||
if (this._mode !== 'none')
|
||||
this._context.pages()[0].bringToFront().catch(() => {});
|
||||
return;
|
||||
|
|
@ -254,7 +268,6 @@ export class RecorderSupplement {
|
|||
}
|
||||
|
||||
private _clearScript(): void {
|
||||
this._bufferedOutput.clear();
|
||||
this._generator.restart();
|
||||
if (!!this._params.startRecording) {
|
||||
for (const page of this._context.pages())
|
||||
|
|
@ -376,7 +389,7 @@ export class RecorderSupplement {
|
|||
}
|
||||
|
||||
private _pushAllSources() {
|
||||
this._recorderApp?.setSources([this._recorderSource, ...this._userSources.values()]);
|
||||
this._recorderApp?.setSources([...this._recorderSources, ...this._userSources.values()]);
|
||||
}
|
||||
|
||||
async onBeforeInputAction(metadata: CallMetadata): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ declare global {
|
|||
playwrightSetSources: (sources: Source[]) => void;
|
||||
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
|
||||
dispatch(data: any): Promise<void>;
|
||||
playwrightSourceEchoForTest: string;
|
||||
playwrightSourcesEchoForTest: Source[];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,20 +38,13 @@ export interface 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 [log, setLog] = React.useState(new Map<number, CallLog>());
|
||||
const [mode, setMode] = React.useState<Mode>('none');
|
||||
|
||||
window.playwrightSetMode = setMode;
|
||||
window.playwrightSetSources = sources => {
|
||||
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.playwrightSetSources = setSources;
|
||||
window.playwrightSetPaused = setPaused;
|
||||
window.playwrightUpdateLogs = callLogs => {
|
||||
const newLog = new Map<number, CallLog>(log);
|
||||
|
|
@ -60,7 +53,20 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
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>();
|
||||
React.useLayoutEffect(() => {
|
||||
|
|
|
|||
|
|
@ -29,14 +29,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
const selector = await recorder.hoverOverElement('button');
|
||||
expect(selector).toBe('text=Submit');
|
||||
|
||||
const [message] = await Promise.all([
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
recorder.waitForOutput('click'),
|
||||
recorder.waitForOutput('<javascript>', 'click'),
|
||||
page.dispatchEvent('button', 'click', { detail: 1 })
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// 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');
|
||||
});
|
||||
|
||||
|
|
@ -57,12 +71,13 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
const selector = await recorder.hoverOverElement('button');
|
||||
expect(selector).toBe('text=Submit');
|
||||
|
||||
const [message] = await Promise.all([
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
recorder.waitForOutput('click'),
|
||||
recorder.waitForOutput('<javascript>', 'click'),
|
||||
page.dispatchEvent('button', 'click', { detail: 1 })
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Click text=Submit
|
||||
await page.click('text=Submit');`);
|
||||
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);
|
||||
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'),
|
||||
recorder.waitForOutput('click'),
|
||||
recorder.waitForOutput('<javascript>', 'click'),
|
||||
page.dispatchEvent('div', 'click', { detail: 1 })
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Click text=Some long text here
|
||||
await page.click('text=Some long text here');`);
|
||||
expect(message.text()).toBe('click');
|
||||
|
|
@ -105,14 +120,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
const selector = await recorder.focusElement('input');
|
||||
expect(selector).toBe('input[name="name"]');
|
||||
|
||||
const [message] = await Promise.all([
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
recorder.waitForOutput('fill'),
|
||||
recorder.waitForOutput('<javascript>', 'fill'),
|
||||
page.fill('input', 'John')
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Fill input[name="name"]
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
@ -121,12 +150,12 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
const selector = await recorder.focusElement('textarea');
|
||||
expect(selector).toBe('textarea[name="name"]');
|
||||
|
||||
const [message] = await Promise.all([
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
recorder.waitForOutput('fill'),
|
||||
recorder.waitForOutput('<javascript>', 'fill'),
|
||||
page.fill('textarea', 'John')
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Fill textarea[name="name"]
|
||||
await page.fill('textarea[name="name"]', 'John');`);
|
||||
expect(message.text()).toBe('John');
|
||||
|
|
@ -140,14 +169,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
|
||||
const messages: any[] = [];
|
||||
page.on('console', message => messages.push(message));
|
||||
await Promise.all([
|
||||
const [, sources] = await Promise.all([
|
||||
recorder.waitForActionPerformed(),
|
||||
recorder.waitForOutput('press'),
|
||||
recorder.waitForOutput('<javascript>', 'press'),
|
||||
page.press('input', 'Shift+Enter')
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Press Enter with modifiers
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
@ -158,24 +201,25 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
`);
|
||||
|
||||
await page.click('input[name="one"]');
|
||||
await recorder.waitForOutput('click');
|
||||
await recorder.waitForOutput('<javascript>', 'click');
|
||||
await page.keyboard.type('foobar123');
|
||||
await recorder.waitForOutput('foobar123');
|
||||
await recorder.waitForOutput('<javascript>', 'foobar123');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await recorder.waitForOutput('Tab');
|
||||
await recorder.waitForOutput('<javascript>', 'Tab');
|
||||
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"]
|
||||
await page.fill('input[name="one"]', 'foobar123');`);
|
||||
|
||||
expect(recorder.output()).toContain(`
|
||||
expect(text).toContain(`
|
||||
// Press Tab
|
||||
await page.press('input[name="one"]', 'Tab');`);
|
||||
|
||||
expect(recorder.output()).toContain(`
|
||||
expect(text).toContain(`
|
||||
// Fill input[name="two"]
|
||||
await page.fill('input[name="two"]', 'barfoo321');`);
|
||||
});
|
||||
|
|
@ -190,12 +234,12 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
page.on('console', message => {
|
||||
messages.push(message);
|
||||
});
|
||||
await Promise.all([
|
||||
const [, sources] = await Promise.all([
|
||||
recorder.waitForActionPerformed(),
|
||||
recorder.waitForOutput('press'),
|
||||
recorder.waitForOutput('<javascript>', 'press'),
|
||||
page.press('input', 'ArrowDown')
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Press ArrowDown
|
||||
await page.press('input[name="name"]', 'ArrowDown');`);
|
||||
expect(messages[0].text()).toBe('press:ArrowDown');
|
||||
|
|
@ -211,12 +255,12 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
page.on('console', message => {
|
||||
messages.push(message);
|
||||
});
|
||||
await Promise.all([
|
||||
const [, sources] = await Promise.all([
|
||||
recorder.waitForActionPerformed(),
|
||||
recorder.waitForOutput('press'),
|
||||
recorder.waitForOutput('<javascript>', 'press'),
|
||||
page.press('input', 'ArrowDown')
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Press ArrowDown
|
||||
await page.press('input[name="name"]', 'ArrowDown');`);
|
||||
expect(messages.length).toBe(2);
|
||||
|
|
@ -230,14 +274,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
const selector = await recorder.focusElement('input');
|
||||
expect(selector).toBe('input[name="accept"]');
|
||||
|
||||
const [message] = await Promise.all([
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
recorder.waitForOutput('check'),
|
||||
recorder.waitForOutput('<javascript>', 'check'),
|
||||
page.click('input')
|
||||
]);
|
||||
await recorder.waitForOutput(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// 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');
|
||||
});
|
||||
|
||||
|
|
@ -247,12 +305,13 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
const selector = await recorder.focusElement('input');
|
||||
expect(selector).toBe('input[name="accept"]');
|
||||
|
||||
const [message] = await Promise.all([
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
recorder.waitForOutput('check'),
|
||||
recorder.waitForOutput('<javascript>', 'check'),
|
||||
page.keyboard.press('Space')
|
||||
]);
|
||||
await recorder.waitForOutput(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Check input[name="accept"]
|
||||
await page.check('input[name="accept"]');`);
|
||||
expect(message.text()).toBe('true');
|
||||
|
|
@ -264,14 +323,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
const selector = await recorder.focusElement('input');
|
||||
expect(selector).toBe('input[name="accept"]');
|
||||
|
||||
const [message] = await Promise.all([
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
recorder.waitForOutput('uncheck'),
|
||||
recorder.waitForOutput('<javascript>', 'uncheck'),
|
||||
page.click('input')
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// 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');
|
||||
});
|
||||
|
||||
|
|
@ -281,14 +354,28 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
const selector = await recorder.hoverOverElement('select');
|
||||
expect(selector).toBe('select');
|
||||
|
||||
const [message] = await Promise.all([
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console'),
|
||||
recorder.waitForOutput('select'),
|
||||
recorder.waitForOutput('<javascript>', 'select'),
|
||||
page.selectOption('select', '2')
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// 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');
|
||||
});
|
||||
|
||||
|
|
@ -300,17 +387,37 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
const selector = await recorder.hoverOverElement('a');
|
||||
expect(selector).toBe('text=link');
|
||||
|
||||
const [popup] = await Promise.all([
|
||||
const [popup, sources] = await Promise.all([
|
||||
page.context().waitForEvent('page'),
|
||||
recorder.waitForOutput('waitForEvent'),
|
||||
recorder.waitForOutput('<javascript>', 'waitForEvent'),
|
||||
page.dispatchEvent('a', 'click', { detail: 1 })
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Click text=link
|
||||
const [page1] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
@ -319,15 +426,32 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
|
||||
const selector = await recorder.hoverOverElement('a');
|
||||
expect(selector).toBe('text=link');
|
||||
await Promise.all([
|
||||
const [, sources] = await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
recorder.waitForOutput('assert'),
|
||||
recorder.waitForOutput('<javascript>', 'assert'),
|
||||
page.dispatchEvent('a', 'click', { detail: 1 })
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Click text=link
|
||||
await page.click('text=link');
|
||||
// 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');
|
||||
});
|
||||
|
||||
|
|
@ -338,17 +462,37 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => {
|
|||
const selector = await recorder.hoverOverElement('a');
|
||||
expect(selector).toBe('text=link');
|
||||
|
||||
await Promise.all([
|
||||
const [, sources] = await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
recorder.waitForOutput('waitForNavigation'),
|
||||
recorder.waitForOutput('<javascript>', 'waitForNavigation'),
|
||||
page.dispatchEvent('a', 'click', { detail: 1 })
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Click text=link
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'about:blank#foo' }*/),
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,21 +25,64 @@ describe('cli codegen', (suite, { mode }) => {
|
|||
}, () => {
|
||||
it('should contain open page', async ({ recorder }) => {
|
||||
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 }) => {
|
||||
await recorder.setContentAndWait(``);
|
||||
await context.newPage();
|
||||
await recorder.waitForOutput('page1');
|
||||
expect(recorder.output()).toContain('const page1 = await context.newPage();');
|
||||
const sources = await recorder.waitForOutput('<javascript>', 'page1');
|
||||
|
||||
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 }) => {
|
||||
await recorder.setContentAndWait(``);
|
||||
await context.newPage();
|
||||
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 }) => {
|
||||
|
|
@ -51,7 +94,7 @@ describe('cli codegen', (suite, { mode }) => {
|
|||
const selector = await recorder.hoverOverElement('html');
|
||||
expect(selector).toBe('html');
|
||||
await recorder.page.close();
|
||||
await recorder.waitForOutput('page.close();');
|
||||
await recorder.waitForOutput('<javascript>', 'page.close();');
|
||||
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.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
|
||||
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 }) => {
|
||||
|
|
@ -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.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
|
||||
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 }) => {
|
||||
|
|
@ -104,9 +176,24 @@ describe('cli codegen', (suite, { mode }) => {
|
|||
await page.setInputFiles('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
|
||||
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 }) => {
|
||||
|
|
@ -129,12 +216,33 @@ describe('cli codegen', (suite, { mode }) => {
|
|||
page.waitForEvent('download'),
|
||||
page.click('text=Download')
|
||||
]);
|
||||
await recorder.waitForOutput(`
|
||||
const sources = await recorder.waitForOutput('<javascript>', 'waitForEvent');
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Click text=Download
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('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 }) => {
|
||||
|
|
@ -146,13 +254,38 @@ describe('cli codegen', (suite, { mode }) => {
|
|||
await dialog.dismiss();
|
||||
});
|
||||
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
|
||||
page.once('dialog', dialog => {
|
||||
console.log(\`Dialog message: $\{dialog.message()}\`);
|
||||
console.log(\`Dialog message: \${dialog.message()}\`);
|
||||
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 }) => {
|
||||
|
|
@ -169,7 +302,7 @@ describe('cli codegen', (suite, { mode }) => {
|
|||
</script>`, httpServer.PREFIX);
|
||||
for (let i = 1; i < 3; ++i) {
|
||||
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');
|
||||
|
||||
await page.click('a', { modifiers: [ platform === 'darwin' ? 'Meta' : 'Control'] });
|
||||
await recorder.waitForOutput('page1');
|
||||
const sources = await recorder.waitForOutput('<javascript>', 'page1');
|
||||
|
||||
if (browserName === 'chromium') {
|
||||
expect(recorder.output()).toContain(`
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Open new page
|
||||
const page1 = await context.newPage();
|
||||
page1.goto('about:blank?foo');`);
|
||||
} else if (browserName === 'firefox') {
|
||||
expect(recorder.output()).toContain(`
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Click text=link
|
||||
const [page1] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
|
|
@ -216,13 +350,23 @@ describe('cli codegen', (suite, { mode }) => {
|
|||
await recorder.setPageContentAndWait(popup2, '<input id=name>');
|
||||
|
||||
await popup1.type('input', 'TextA');
|
||||
await recorder.waitForOutput('TextA');
|
||||
await recorder.waitForOutput('<javascript>', 'TextA');
|
||||
|
||||
await popup2.type('input', 'TextB');
|
||||
await recorder.waitForOutput('TextB');
|
||||
await recorder.waitForOutput('<javascript>', 'TextB');
|
||||
|
||||
expect(recorder.output()).toContain(`await page1.fill('input', 'TextA');`);
|
||||
expect(recorder.output()).toContain(`await page2.fill('input', 'TextB');`);
|
||||
const sources = recorder.sources();
|
||||
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 }) => {
|
||||
|
|
@ -239,7 +383,7 @@ describe('cli codegen', (suite, { mode }) => {
|
|||
page.on('console', message => messages.push(message.text()));
|
||||
await Promise.all([
|
||||
page.click('button'),
|
||||
recorder.waitForOutput('page.click')
|
||||
recorder.waitForOutput('<javascript>', 'page.click')
|
||||
]);
|
||||
expect(messages).toEqual(['mousedown', 'mouseup', 'click']);
|
||||
});
|
||||
|
|
@ -283,35 +427,74 @@ describe('cli codegen', (suite, { mode }) => {
|
|||
const frameTwo = page.frame({ name: 'two' });
|
||||
const otherFrame = page.frames().find(f => f !== page.mainFrame() && !f.name());
|
||||
|
||||
await Promise.all([
|
||||
recorder.waitForOutput('one'),
|
||||
let [sources] = await Promise.all([
|
||||
recorder.waitForOutput('<javascript>', 'one'),
|
||||
frameOne.click('div'),
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
await page.frame({
|
||||
name: 'one'
|
||||
}).click('text=Hi, I\\'m frame');`);
|
||||
|
||||
await Promise.all([
|
||||
recorder.waitForOutput('two'),
|
||||
expect(sources.get('<python>').text).toContain(`
|
||||
# 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'),
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
await page.frame({
|
||||
name: 'two'
|
||||
}).click('text=Hi, I\\'m frame');`);
|
||||
|
||||
await Promise.all([
|
||||
recorder.waitForOutput('url: \''),
|
||||
expect(sources.get('<python>').text).toContain(`
|
||||
# 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'),
|
||||
]);
|
||||
expect(recorder.output()).toContain(`
|
||||
|
||||
expect(sources.get('<javascript>').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
await page.frame({
|
||||
url: '${otherFrame.url()}'
|
||||
url: 'http://localhost:${server.PORT}/frames/frame.html'
|
||||
}).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 }) => {
|
||||
|
|
@ -329,6 +512,6 @@ describe('cli codegen', (suite, { mode }) => {
|
|||
await page.evaluate('pushState()');
|
||||
|
||||
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 type { BrowserType, Browser, Page } from '../..';
|
||||
export { config } from 'folio';
|
||||
import type { Source } from '../../src/server/supplements/recorder/recorderTypes';
|
||||
|
||||
type WorkerFixtures = {
|
||||
browserType: BrowserType<Browser>;
|
||||
|
|
@ -66,7 +67,7 @@ class Recorder {
|
|||
_actionReporterInstalled: boolean
|
||||
_actionPerformedCallback: Function
|
||||
recorderPage: Page;
|
||||
private _text: string = '';
|
||||
private _sources = new Map<string, Source>();
|
||||
|
||||
constructor(page: Page, recorderPage: Page) {
|
||||
this.page = page;
|
||||
|
|
@ -97,24 +98,26 @@ class Recorder {
|
|||
]);
|
||||
}
|
||||
|
||||
async waitForOutput(text: string): Promise<void> {
|
||||
this._text = await this.recorderPage.evaluate((text: string) => {
|
||||
async waitForOutput(file: string, text: string): Promise<Map<string, Source>> {
|
||||
const sources: Source[] = await this.recorderPage.evaluate((params: { text: string, file: string }) => {
|
||||
const w = window as any;
|
||||
return new Promise(f => {
|
||||
const poll = () => {
|
||||
if (w.playwrightSourceEchoForTest && w.playwrightSourceEchoForTest.includes(text)) {
|
||||
f(w.playwrightSourceEchoForTest);
|
||||
return;
|
||||
}
|
||||
const source = (w.playwrightSourcesEchoForTest || []).find((s: Source) => s.file === params.file);
|
||||
if (source && source.text.includes(params.text))
|
||||
f(w.playwrightSourcesEchoForTest);
|
||||
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 {
|
||||
return this._text;
|
||||
sources(): Map<string, Source> {
|
||||
return this._sources;
|
||||
}
|
||||
|
||||
async waitForHighlight(action: () => Promise<void>): Promise<string> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue