Skip to content

Commit

Permalink
chore: simplify code generation (#5466)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Feb 17, 2021
1 parent b6bd7c0 commit 30e68f6
Show file tree
Hide file tree
Showing 13 changed files with 673 additions and 417 deletions.
90 changes: 50 additions & 40 deletions src/server/supplements/recorder/codeGenerator.ts
Expand Up @@ -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';

Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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;
}

Expand All @@ -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');
}
}
69 changes: 28 additions & 41 deletions src/server/supplements/recorder/csharp.ts
Expand Up @@ -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();
Expand All @@ -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();
}

Expand Down Expand Up @@ -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}`;
}
}

Expand Down

0 comments on commit 30e68f6

Please sign in to comment.