Skip to content

Commit 097f7c3

Browse files
committed
feat(java): implement codegen (#5692)
1 parent ab3b8a1 commit 097f7c3

File tree

10 files changed

+578
-6
lines changed

10 files changed

+578
-6
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { BrowserContextOptions } from '../../../..';
18+
import { LanguageGenerator, LanguageGeneratorOptions, toSignalMap } from './language';
19+
import { ActionInContext } from './codeGenerator';
20+
import { Action, actionTitle } from './recorderActions';
21+
import { toModifiers } from './utils';
22+
import deviceDescriptors = require('../../deviceDescriptors');
23+
import { JavaScriptFormatter } from './javascript';
24+
25+
export class JavaLanguageGenerator implements LanguageGenerator {
26+
id = 'java';
27+
fileName = '<java>';
28+
highlighter = 'java';
29+
30+
generateAction(actionInContext: ActionInContext): string {
31+
const { action, pageAlias } = actionInContext;
32+
const formatter = new JavaScriptFormatter(6);
33+
formatter.newLine();
34+
formatter.add('// ' + actionTitle(action));
35+
36+
if (action.name === 'openPage') {
37+
formatter.add(`Page ${pageAlias} = context.newPage();`);
38+
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
39+
formatter.add(`${pageAlias}.navigate("${action.url}");`);
40+
return formatter.format();
41+
}
42+
43+
const subject = actionInContext.isMainFrame ? pageAlias :
44+
(actionInContext.frameName ?
45+
`${pageAlias}.frame(${quote(actionInContext.frameName)})` :
46+
`${pageAlias}.frameByUrl(${quote(actionInContext.frameUrl)})`);
47+
48+
const signals = toSignalMap(action);
49+
50+
if (signals.dialog) {
51+
formatter.add(` ${pageAlias}.onceDialog(dialog -> {
52+
System.out.println(String.format("Dialog message: %s", dialog.message()));
53+
dialog.dismiss();
54+
});`);
55+
}
56+
57+
const actionCall = this._generateActionCall(action);
58+
let code = `${subject}.${actionCall};`;
59+
60+
if (signals.popup) {
61+
code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> {
62+
${code}
63+
});`;
64+
}
65+
66+
if (signals.download) {
67+
code = `Download download = ${pageAlias}.waitForDownload(() -> {
68+
${code}
69+
});`;
70+
}
71+
72+
if (signals.waitForNavigation) {
73+
code = `
74+
// ${pageAlias}.waitForNavigation(new Page.WaitForNavigationOptions().withUrl(${quote(signals.waitForNavigation.url)}), () ->
75+
${pageAlias}.waitForNavigation(() -> {
76+
${code}
77+
});`;
78+
}
79+
80+
formatter.add(code);
81+
82+
if (signals.assertNavigation)
83+
formatter.add(`// assert ${pageAlias}.url().equals(${quote(signals.assertNavigation.url)});`);
84+
return formatter.format();
85+
}
86+
87+
private _generateActionCall(action: Action): string {
88+
switch (action.name) {
89+
case 'openPage':
90+
throw Error('Not reached');
91+
case 'closePage':
92+
return 'close()';
93+
case 'click': {
94+
let method = 'click';
95+
if (action.clickCount === 2)
96+
method = 'dblclick';
97+
return `${method}(${quote(action.selector)})`;
98+
}
99+
case 'check':
100+
return `check(${quote(action.selector)})`;
101+
case 'uncheck':
102+
return `uncheck(${quote(action.selector)})`;
103+
case 'fill':
104+
return `fill(${quote(action.selector)}, ${quote(action.text)})`;
105+
case 'setInputFiles':
106+
return `setInputFiles(${quote(action.selector)}, ${formatPath(action.files.length === 1 ? action.files[0] : action.files)})`;
107+
case 'press': {
108+
const modifiers = toModifiers(action.modifiers);
109+
const shortcut = [...modifiers, action.key].join('+');
110+
return `press(${quote(action.selector)}, ${quote(shortcut)})`;
111+
}
112+
case 'navigate':
113+
return `navigate(${quote(action.url)})`;
114+
case 'select':
115+
return `selectOption(${quote(action.selector)}, ${formatSelectOption(action.options.length > 1 ? action.options : action.options[0])})`;
116+
}
117+
}
118+
119+
generateHeader(options: LanguageGeneratorOptions): string {
120+
const formatter = new JavaScriptFormatter();
121+
formatter.add(`
122+
import com.microsoft.playwright.*;
123+
import com.microsoft.playwright.options.*;
124+
125+
public class Example {
126+
public static void main(String[] args) {
127+
try (Playwright playwright = Playwright.create()) {
128+
Browser browser = playwright.${options.browserName}().launch(${formatLaunchOptions(options.launchOptions)});
129+
BrowserContext context = browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`);
130+
return formatter.format();
131+
}
132+
133+
generateFooter(saveStorage: string | undefined): string {
134+
const storageStateLine = saveStorage ? `\n context.storageState(new BrowserContext.StorageStateOptions().withPath(${quote(saveStorage)}));` : '';
135+
return `\n // ---------------------${storageStateLine}
136+
}
137+
}
138+
}`;
139+
}
140+
}
141+
142+
function formatPath(files: string | string[]): string {
143+
if (Array.isArray(files)) {
144+
if (files.length === 0)
145+
return 'new Path[0]';
146+
return `new Path[] {${files.map(s => 'Paths.get(' + quote(s) + ')').join(', ')}}`;
147+
}
148+
return `Paths.get(${quote(files)})`;
149+
}
150+
151+
function formatSelectOption(options: string | string[]): string {
152+
if (Array.isArray(options)) {
153+
if (options.length === 0)
154+
return 'new String[0]';
155+
return `new String[] {${options.map(s => quote(s)).join(', ')}}`;
156+
}
157+
return quote(options);
158+
}
159+
160+
function formatLaunchOptions(options: any): string {
161+
const lines = [];
162+
if (!Object.keys(options).length)
163+
return '';
164+
lines.push('new BrowserType.LaunchOptions()');
165+
if (typeof options.headless === 'boolean')
166+
lines.push(` .withHeadless(false)`);
167+
return lines.join('\n');
168+
}
169+
170+
function formatContextOptions(contextOptions: BrowserContextOptions, deviceName: string | undefined): string {
171+
const lines = [];
172+
if (!Object.keys(contextOptions).length && !deviceName)
173+
return '';
174+
const device = deviceName ? deviceDescriptors[deviceName] : {};
175+
const options: BrowserContextOptions = { ...device, ...contextOptions };
176+
lines.push('new Browser.NewContextOptions()');
177+
if (options.colorScheme)
178+
lines.push(` .withColorScheme(ColorScheme.${options.colorScheme.toUpperCase()})`);
179+
if (options.geolocation)
180+
lines.push(` .withGeolocation(${options.geolocation.latitude}, ${options.geolocation.longitude})`);
181+
if (options.locale)
182+
lines.push(` .withLocale("${options.locale}")`);
183+
if (options.proxy)
184+
lines.push(` .withProxy(new Proxy("${options.proxy.server}"))`);
185+
if (options.timezoneId)
186+
lines.push(` .withTimezoneId("${options.timezoneId}")`);
187+
if (options.userAgent)
188+
lines.push(` .withUserAgent("${options.userAgent}")`);
189+
if (options.viewport)
190+
lines.push(` .withViewportSize(${options.viewport.width}, ${options.viewport.height})`);
191+
if (options.deviceScaleFactor)
192+
lines.push(` .withDeviceScaleFactor(${options.deviceScaleFactor})`);
193+
if (options.isMobile)
194+
lines.push(` .withIsMobile(${options.isMobile})`);
195+
if (options.hasTouch)
196+
lines.push(` .withHasTouch(${options.hasTouch})`);
197+
if (options.storageState)
198+
lines.push(` .withStorageStatePath(Paths.get(${quote(options.storageState as string)}))`);
199+
200+
return lines.join('\n');
201+
}
202+
203+
function quote(text: string, char: string = '\"') {
204+
if (char === '\'')
205+
return char + text.replace(/[']/g, '\\\'') + char;
206+
if (char === '"')
207+
return char + text.replace(/["]/g, '\\"') + char;
208+
if (char === '`')
209+
return char + text.replace(/[`]/g, '\\`') + char;
210+
throw new Error('Invalid escape char');
211+
}

src/server/supplements/recorder/javascript.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ function formatContextOptions(options: BrowserContextOptions, deviceName: string
193193
return lines.join('\n');
194194
}
195195

196-
class JavaScriptFormatter {
196+
export class JavaScriptFormatter {
197197
private _baseIndent: string;
198198
private _baseOffset: string;
199199
private _lines: string[] = [];
@@ -224,10 +224,11 @@ class JavaScriptFormatter {
224224
if (line.startsWith('}') || line.startsWith(']'))
225225
spaces = spaces.substring(this._baseIndent.length);
226226

227-
const extraSpaces = /^(for|while|if).*\(.*\)$/.test(previousLine) ? this._baseIndent : '';
227+
const extraSpaces = /^(for|while|if|try).*\(.*\)$/.test(previousLine) ? this._baseIndent : '';
228228
previousLine = line;
229229

230-
line = spaces + extraSpaces + line;
230+
const callCarryOver = line.startsWith('.with');
231+
line = spaces + extraSpaces + (callCarryOver ? this._baseIndent : '') + line;
231232
if (line.endsWith('{') || line.endsWith('['))
232233
spaces += this._baseIndent;
233234
return this._baseOffset + line;

src/server/supplements/recorderSupplement.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { describeFrame, toClickOptions, toModifiers } from './recorder/utils';
2222
import { Page } from '../page';
2323
import { Frame } from '../frames';
2424
import { BrowserContext } from '../browserContext';
25+
import { JavaLanguageGenerator } from './recorder/java';
2526
import { JavaScriptLanguageGenerator } from './recorder/javascript';
2627
import { CSharpLanguageGenerator } from './recorder/csharp';
2728
import { PythonLanguageGenerator } from './recorder/python';
@@ -76,6 +77,7 @@ export class RecorderSupplement {
7677
const language = params.language || context._options.sdkLanguage;
7778

7879
const languages = new Set([
80+
new JavaLanguageGenerator(),
7981
new JavaScriptLanguageGenerator(),
8082
new PythonLanguageGenerator(false),
8183
new PythonLanguageGenerator(true),

src/third_party/highlightjs/highlightjs/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ var hljs = require('./core');
33
hljs.registerLanguage('javascript', require('./languages/javascript'));
44
hljs.registerLanguage('python', require('./languages/python'));
55
hljs.registerLanguage('csharp', require('./languages/csharp'));
6+
hljs.registerLanguage('java', require('./languages/java'));
67

78
module.exports = hljs;

0 commit comments

Comments
 (0)