Skip to content

Commit 30e68f6

Browse files
authored
chore: simplify code generation (#5466)
1 parent b6bd7c0 commit 30e68f6

File tree

13 files changed

+673
-417
lines changed

13 files changed

+673
-417
lines changed

src/server/supplements/recorder/codeGenerator.ts

Lines changed: 50 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { EventEmitter } from 'events';
1718
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
1819
import { Frame } from '../../frames';
19-
import { LanguageGenerator } from './language';
20+
import { LanguageGenerator, LanguageGeneratorOptions } from './language';
2021
import { Action, Signal } from './recorderActions';
2122
import { describeFrame } from './utils';
2223

@@ -29,56 +30,55 @@ export type ActionInContext = {
2930
committed?: boolean;
3031
}
3132

32-
export interface CodeGeneratorOutput {
33-
printLn(text: string): void;
34-
popLn(text: string): void;
35-
}
36-
37-
export class CodeGenerator {
33+
export class CodeGenerator extends EventEmitter {
3834
private _currentAction: ActionInContext | null = null;
3935
private _lastAction: ActionInContext | null = null;
40-
private _lastActionText: string | undefined;
41-
private _languageGenerator: LanguageGenerator;
42-
private _output: CodeGeneratorOutput;
43-
private _headerText = '';
44-
private _footerText = '';
36+
private _actions: ActionInContext[] = [];
37+
private _enabled: boolean;
38+
private _options: LanguageGeneratorOptions;
4539

46-
constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, output: CodeGeneratorOutput, languageGenerator: LanguageGenerator, deviceName: string | undefined, saveStorage: string | undefined) {
47-
this._output = output;
48-
this._languageGenerator = languageGenerator;
40+
constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName: string | undefined, saveStorage: string | undefined) {
41+
super();
4942

5043
launchOptions = { headless: false, ...launchOptions };
51-
if (generateHeaders) {
52-
this._headerText = this._languageGenerator.generateHeader(browserName, launchOptions, contextOptions, deviceName);
53-
this._footerText = '\n' + this._languageGenerator.generateFooter(saveStorage);
54-
}
44+
this._enabled = generateHeaders;
45+
this._options = { browserName, generateHeaders, launchOptions, contextOptions, deviceName, saveStorage };
5546
this.restart();
5647
}
5748

5849
restart() {
5950
this._currentAction = null;
6051
this._lastAction = null;
61-
if (this._headerText) {
62-
this._output.printLn(this._headerText);
63-
this._output.printLn(this._footerText);
64-
}
52+
this._actions = [];
53+
}
54+
55+
setEnabled(enabled: boolean) {
56+
this._enabled = enabled;
6557
}
6658

6759
addAction(action: ActionInContext) {
60+
if (!this._enabled)
61+
return;
6862
this.willPerformAction(action);
6963
this.didPerformAction(action);
7064
}
7165

7266
willPerformAction(action: ActionInContext) {
67+
if (!this._enabled)
68+
return;
7369
this._currentAction = action;
7470
}
7571

7672
performedActionFailed(action: ActionInContext) {
73+
if (!this._enabled)
74+
return;
7775
if (this._currentAction === action)
7876
this._currentAction = null;
7977
}
8078

8179
didPerformAction(actionInContext: ActionInContext) {
80+
if (!this._enabled)
81+
return;
8282
const { action, pageAlias } = actionInContext;
8383
let eraseLastAction = false;
8484
if (this._lastAction && this._lastAction.pageAlias === pageAlias) {
@@ -94,41 +94,39 @@ export class CodeGenerator {
9494
}
9595
if (lastAction && action.name === 'navigate' && lastAction.name === 'navigate') {
9696
if (action.url === lastAction.url) {
97+
// Already at a target URL.
9798
this._currentAction = null;
9899
return;
99100
}
100101
}
101102
for (const name of ['check', 'uncheck']) {
103+
// Check and uncheck erase click.
102104
if (lastAction && action.name === name && lastAction.name === 'click') {
103105
if ((action as any).selector === (lastAction as any).selector)
104106
eraseLastAction = true;
105107
}
106108
}
107109
}
108-
this._printAction(actionInContext, eraseLastAction);
110+
111+
this._lastAction = actionInContext;
112+
this._currentAction = null;
113+
if (eraseLastAction)
114+
this._actions.pop();
115+
this._actions.push(actionInContext);
116+
this.emit('change');
109117
}
110118

111119
commitLastAction() {
120+
if (!this._enabled)
121+
return;
112122
const action = this._lastAction;
113123
if (action)
114124
action.committed = true;
115125
}
116126

117-
_printAction(actionInContext: ActionInContext, eraseLastAction: boolean) {
118-
if (this._footerText)
119-
this._output.popLn(this._footerText);
120-
if (eraseLastAction && this._lastActionText)
121-
this._output.popLn(this._lastActionText);
122-
const performingAction = !!this._currentAction;
123-
this._currentAction = null;
124-
this._lastAction = actionInContext;
125-
this._lastActionText = this._languageGenerator.generateAction(actionInContext, performingAction);
126-
this._output.printLn(this._lastActionText);
127-
if (this._footerText)
128-
this._output.printLn(this._footerText);
129-
}
130-
131127
signal(pageAlias: string, frame: Frame, signal: Signal) {
128+
if (!this._enabled)
129+
return;
132130
// Signal either arrives while action is being performed or shortly after.
133131
if (this._currentAction) {
134132
this._currentAction.action.signals.push(signal);
@@ -140,8 +138,9 @@ export class CodeGenerator {
140138
return;
141139
if (signal.name === 'download' && signals.length && signals[signals.length - 1].name === 'navigation')
142140
signals.length = signals.length - 1;
141+
signal.isAsync = true;
143142
this._lastAction.action.signals.push(signal);
144-
this._printAction(this._lastAction, true);
143+
this.emit('change');
145144
return;
146145
}
147146

@@ -154,8 +153,19 @@ export class CodeGenerator {
154153
name: 'navigate',
155154
url: frame.url(),
156155
signals: [],
157-
}
156+
},
158157
});
159158
}
160159
}
160+
161+
generateText(languageGenerator: LanguageGenerator) {
162+
const text = [];
163+
if (this._options.generateHeaders)
164+
text.push(languageGenerator.generateHeader(this._options));
165+
for (const action of this._actions)
166+
text.push(languageGenerator.generateAction(action));
167+
if (this._options.generateHeaders)
168+
text.push(languageGenerator.generateFooter(this._options.saveStorage));
169+
return text.join('\n');
170+
}
161171
}

src/server/supplements/recorder/csharp.ts

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@
1414
* limitations under the License.
1515
*/
1616

17-
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
18-
import { LanguageGenerator, sanitizeDeviceOptions } from './language';
17+
import type { BrowserContextOptions } from '../../../..';
18+
import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language';
1919
import { ActionInContext } from './codeGenerator';
20-
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions';
20+
import { actionTitle, Action } from './recorderActions';
2121
import { MouseClickOptions, toModifiers } from './utils';
2222
import deviceDescriptors = require('../../deviceDescriptors');
2323

2424
export class CSharpLanguageGenerator implements LanguageGenerator {
25+
id = 'csharp';
26+
fileName = '<csharp>';
27+
highlighter = 'csharp';
2528

26-
generateAction(actionInContext: ActionInContext, performingAction: boolean): string {
29+
generateAction(actionInContext: ActionInContext): string {
2730
const { action, pageAlias } = actionInContext;
2831
const formatter = new CSharpFormatter(0);
2932
formatter.newLine();
@@ -41,63 +44,47 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
4144
`${pageAlias}.GetFrame(name: ${quote(actionInContext.frameName)})` :
4245
`${pageAlias}.GetFrame(url: ${quote(actionInContext.frameUrl)})`);
4346

44-
let navigationSignal: NavigationSignal | undefined;
45-
let popupSignal: PopupSignal | undefined;
46-
let downloadSignal: DownloadSignal | undefined;
47-
let dialogSignal: DialogSignal | undefined;
48-
for (const signal of action.signals) {
49-
if (signal.name === 'navigation')
50-
navigationSignal = signal;
51-
else if (signal.name === 'popup')
52-
popupSignal = signal;
53-
else if (signal.name === 'download')
54-
downloadSignal = signal;
55-
else if (signal.name === 'dialog')
56-
dialogSignal = signal;
57-
}
47+
const signals = toSignalMap(action);
5848

59-
if (dialogSignal) {
60-
formatter.add(` void ${pageAlias}_Dialog${dialogSignal.dialogAlias}_EventHandler(object sender, DialogEventArgs e)
49+
if (signals.dialog) {
50+
formatter.add(` void ${pageAlias}_Dialog${signals.dialog.dialogAlias}_EventHandler(object sender, DialogEventArgs e)
6151
{
6252
Console.WriteLine($"Dialog message: {e.Dialog.Message}");
6353
e.Dialog.DismissAsync();
64-
${pageAlias}.Dialog -= ${pageAlias}_Dialog${dialogSignal.dialogAlias}_EventHandler;
54+
${pageAlias}.Dialog -= ${pageAlias}_Dialog${signals.dialog.dialogAlias}_EventHandler;
6555
}
66-
${pageAlias}.Dialog += ${pageAlias}_Dialog${dialogSignal.dialogAlias}_EventHandler;`);
56+
${pageAlias}.Dialog += ${pageAlias}_Dialog${signals.dialog.dialogAlias}_EventHandler;`);
6757
}
6858

69-
const waitForNavigation = navigationSignal && !performingAction;
70-
const assertNavigation = navigationSignal && performingAction;
71-
72-
const emitTaskWhenAll = waitForNavigation || popupSignal || downloadSignal;
59+
const emitTaskWhenAll = signals.waitForNavigation || signals.popup || signals.download;
7360
if (emitTaskWhenAll) {
74-
if (popupSignal)
75-
formatter.add(`var ${popupSignal.popupAlias}Task = ${pageAlias}.WaitForEventAsync(PageEvent.Popup)`);
76-
else if (downloadSignal)
61+
if (signals.popup)
62+
formatter.add(`var ${signals.popup.popupAlias}Task = ${pageAlias}.WaitForEventAsync(PageEvent.Popup)`);
63+
else if (signals.download)
7764
formatter.add(`var downloadTask = ${pageAlias}.WaitForEventAsync(PageEvent.Download);`);
7865

7966
formatter.add(`await Task.WhenAll(`);
8067
}
8168

8269
// Popup signals.
83-
if (popupSignal)
84-
formatter.add(`${popupSignal.popupAlias}Task,`);
70+
if (signals.popup)
71+
formatter.add(`${signals.popup.popupAlias}Task,`);
8572

8673
// Navigation signal.
87-
if (waitForNavigation)
88-
formatter.add(`${pageAlias}.WaitForNavigationAsync(/*${quote(navigationSignal!.url)}*/),`);
74+
if (signals.waitForNavigation)
75+
formatter.add(`${pageAlias}.WaitForNavigationAsync(/*${quote(signals.waitForNavigation.url)}*/),`);
8976

9077
// Download signals.
91-
if (downloadSignal)
78+
if (signals.download)
9279
formatter.add(`downloadTask,`);
9380

94-
const prefix = (popupSignal || waitForNavigation || downloadSignal) ? '' : 'await ';
81+
const prefix = (signals.popup || signals.waitForNavigation || signals.download) ? '' : 'await ';
9582
const actionCall = this._generateActionCall(action);
9683
const suffix = emitTaskWhenAll ? ');' : ';';
9784
formatter.add(`${prefix}${subject}.${actionCall}${suffix}`);
9885

99-
if (assertNavigation)
100-
formatter.add(` // Assert.Equal(${quote(navigationSignal!.url)}, ${pageAlias}.Url);`);
86+
if (signals.assertNavigation)
87+
formatter.add(` // Assert.Equal(${quote(signals.assertNavigation.url)}, ${pageAlias}.Url);`);
10188
return formatter.format();
10289
}
10390

@@ -142,19 +129,19 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
142129
}
143130
}
144131

145-
generateHeader(browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName?: string): string {
132+
generateHeader(options: LanguageGeneratorOptions): string {
146133
const formatter = new CSharpFormatter(0);
147134
formatter.add(`
148135
await Playwright.InstallAsync();
149136
using var playwright = await Playwright.CreateAsync();
150-
await using var browser = await playwright.${toPascal(browserName)}.LaunchAsync(${formatArgs(launchOptions)});
151-
var context = await browser.NewContextAsync(${formatContextOptions(contextOptions, deviceName)});`);
137+
await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatArgs(options.launchOptions)});
138+
var context = await browser.NewContextAsync(${formatContextOptions(options.contextOptions, options.deviceName)});`);
152139
return formatter.format();
153140
}
154141

155142
generateFooter(saveStorage: string | undefined): string {
156143
const storageStateLine = saveStorage ? `\nawait context.StorageStateAsync(path: "${saveStorage}");` : '';
157-
return `// ---------------------${storageStateLine}`;
144+
return `\n// ---------------------${storageStateLine}`;
158145
}
159146
}
160147

0 commit comments

Comments
 (0)