From d3eff5038627dbd7f9dced1ac91ab8c593fdf3ab Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 3 Mar 2021 14:32:09 -0800 Subject: [PATCH] feat(java): implement codegen (#5692) --- src/server/supplements/recorder/java.ts | 211 ++++++++++++++++++ src/server/supplements/recorder/javascript.ts | 7 +- src/server/supplements/recorderSupplement.ts | 2 + .../highlightjs/highlightjs/index.js | 1 + .../highlightjs/highlightjs/languages/java.js | 176 +++++++++++++++ src/third_party/highlightjs/roll.sh | 2 +- test/cli/cli-codegen-1.spec.ts | 45 ++++ test/cli/cli-codegen-2.spec.ts | 52 +++++ test/cli/cli-codegen-csharp.spec.ts | 2 - test/cli/cli-codegen-java.spec.ts | 86 +++++++ 10 files changed, 578 insertions(+), 6 deletions(-) create mode 100644 src/server/supplements/recorder/java.ts create mode 100644 src/third_party/highlightjs/highlightjs/languages/java.js create mode 100644 test/cli/cli-codegen-java.spec.ts diff --git a/src/server/supplements/recorder/java.ts b/src/server/supplements/recorder/java.ts new file mode 100644 index 0000000000000..93c893577d5d2 --- /dev/null +++ b/src/server/supplements/recorder/java.ts @@ -0,0 +1,211 @@ +/** + * 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 type { BrowserContextOptions } from '../../../..'; +import { LanguageGenerator, LanguageGeneratorOptions, toSignalMap } from './language'; +import { ActionInContext } from './codeGenerator'; +import { Action, actionTitle } from './recorderActions'; +import { toModifiers } from './utils'; +import deviceDescriptors = require('../../deviceDescriptors'); +import { JavaScriptFormatter } from './javascript'; + +export class JavaLanguageGenerator implements LanguageGenerator { + id = 'java'; + fileName = ''; + highlighter = 'java'; + + generateAction(actionInContext: ActionInContext): string { + const { action, pageAlias } = actionInContext; + const formatter = new JavaScriptFormatter(6); + formatter.newLine(); + formatter.add('// ' + actionTitle(action)); + + if (action.name === 'openPage') { + formatter.add(`Page ${pageAlias} = context.newPage();`); + if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/') + formatter.add(`${pageAlias}.navigate("${action.url}");`); + return formatter.format(); + } + + const subject = actionInContext.isMainFrame ? pageAlias : + (actionInContext.frameName ? + `${pageAlias}.frame(${quote(actionInContext.frameName)})` : + `${pageAlias}.frameByUrl(${quote(actionInContext.frameUrl)})`); + + const signals = toSignalMap(action); + + if (signals.dialog) { + formatter.add(` ${pageAlias}.onceDialog(dialog -> { + System.out.println(String.format("Dialog message: %s", dialog.message())); + dialog.dismiss(); + });`); + } + + const actionCall = this._generateActionCall(action); + let code = `${subject}.${actionCall};`; + + if (signals.popup) { + code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> { + ${code} + });`; + } + + if (signals.download) { + code = `Download download = ${pageAlias}.waitForDownload(() -> { + ${code} + });`; + } + + if (signals.waitForNavigation) { + code = ` + // ${pageAlias}.waitForNavigation(new Page.WaitForNavigationOptions().withUrl(${quote(signals.waitForNavigation.url)}), () -> + ${pageAlias}.waitForNavigation(() -> { + ${code} + });`; + } + + formatter.add(code); + + if (signals.assertNavigation) + formatter.add(`// assert ${pageAlias}.url().equals(${quote(signals.assertNavigation.url)});`); + return formatter.format(); + } + + private _generateActionCall(action: Action): string { + switch (action.name) { + case 'openPage': + throw Error('Not reached'); + case 'closePage': + return 'close()'; + case 'click': { + let method = 'click'; + if (action.clickCount === 2) + method = 'dblclick'; + return `${method}(${quote(action.selector)})`; + } + case 'check': + return `check(${quote(action.selector)})`; + case 'uncheck': + return `uncheck(${quote(action.selector)})`; + case 'fill': + return `fill(${quote(action.selector)}, ${quote(action.text)})`; + case 'setInputFiles': + return `setInputFiles(${quote(action.selector)}, ${formatPath(action.files.length === 1 ? action.files[0] : action.files)})`; + case 'press': { + const modifiers = toModifiers(action.modifiers); + const shortcut = [...modifiers, action.key].join('+'); + return `press(${quote(action.selector)}, ${quote(shortcut)})`; + } + case 'navigate': + return `navigate(${quote(action.url)})`; + case 'select': + return `selectOption(${quote(action.selector)}, ${formatSelectOption(action.options.length > 1 ? action.options : action.options[0])})`; + } + } + + generateHeader(options: LanguageGeneratorOptions): string { + const formatter = new JavaScriptFormatter(); + formatter.add(` + import com.microsoft.playwright.*; + import com.microsoft.playwright.options.*; + + public class Example { + public static void main(String[] args) { + try (Playwright playwright = Playwright.create()) { + Browser browser = playwright.${options.browserName}().launch(${formatLaunchOptions(options.launchOptions)}); + BrowserContext context = browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`); + return formatter.format(); + } + + generateFooter(saveStorage: string | undefined): string { + const storageStateLine = saveStorage ? `\n context.storageState(new BrowserContext.StorageStateOptions().withPath(${quote(saveStorage)}));` : ''; + return `\n // ---------------------${storageStateLine} + } + } +}`; + } +} + +function formatPath(files: string | string[]): string { + if (Array.isArray(files)) { + if (files.length === 0) + return 'new Path[0]'; + return `new Path[] {${files.map(s => 'Paths.get(' + quote(s) + ')').join(', ')}}`; + } + return `Paths.get(${quote(files)})`; +} + +function formatSelectOption(options: string | string[]): string { + if (Array.isArray(options)) { + if (options.length === 0) + return 'new String[0]'; + return `new String[] {${options.map(s => quote(s)).join(', ')}}`; + } + return quote(options); +} + +function formatLaunchOptions(options: any): string { + const lines = []; + if (!Object.keys(options).length) + return ''; + lines.push('new BrowserType.LaunchOptions()'); + if (typeof options.headless === 'boolean') + lines.push(` .withHeadless(false)`); + return lines.join('\n'); +} + +function formatContextOptions(contextOptions: BrowserContextOptions, deviceName: string | undefined): string { + const lines = []; + if (!Object.keys(contextOptions).length && !deviceName) + return ''; + const device = deviceName ? deviceDescriptors[deviceName] : {}; + const options: BrowserContextOptions = { ...device, ...contextOptions }; + lines.push('new Browser.NewContextOptions()'); + if (options.colorScheme) + lines.push(` .withColorScheme(ColorScheme.${options.colorScheme.toUpperCase()})`); + if (options.geolocation) + lines.push(` .withGeolocation(${options.geolocation.latitude}, ${options.geolocation.longitude})`); + if (options.locale) + lines.push(` .withLocale("${options.locale}")`); + if (options.proxy) + lines.push(` .withProxy(new Proxy("${options.proxy.server}"))`); + if (options.timezoneId) + lines.push(` .withTimezoneId("${options.timezoneId}")`); + if (options.userAgent) + lines.push(` .withUserAgent("${options.userAgent}")`); + if (options.viewport) + lines.push(` .withViewportSize(${options.viewport.width}, ${options.viewport.height})`); + if (options.deviceScaleFactor) + lines.push(` .withDeviceScaleFactor(${options.deviceScaleFactor})`); + if (options.isMobile) + lines.push(` .withIsMobile(${options.isMobile})`); + if (options.hasTouch) + lines.push(` .withHasTouch(${options.hasTouch})`); + if (options.storageState) + lines.push(` .withStorageStatePath(Paths.get(${quote(options.storageState as string)}))`); + + return lines.join('\n'); +} + +function quote(text: string, char: string = '\"') { + if (char === '\'') + return char + text.replace(/[']/g, '\\\'') + char; + if (char === '"') + return char + text.replace(/["]/g, '\\"') + char; + if (char === '`') + return char + text.replace(/[`]/g, '\\`') + char; + throw new Error('Invalid escape char'); +} diff --git a/src/server/supplements/recorder/javascript.ts b/src/server/supplements/recorder/javascript.ts index 24fab75f58d3d..b7b605b6262b3 100644 --- a/src/server/supplements/recorder/javascript.ts +++ b/src/server/supplements/recorder/javascript.ts @@ -193,7 +193,7 @@ function formatContextOptions(options: BrowserContextOptions, deviceName: string return lines.join('\n'); } -class JavaScriptFormatter { +export class JavaScriptFormatter { private _baseIndent: string; private _baseOffset: string; private _lines: string[] = []; @@ -224,10 +224,11 @@ class JavaScriptFormatter { if (line.startsWith('}') || line.startsWith(']')) spaces = spaces.substring(this._baseIndent.length); - const extraSpaces = /^(for|while|if).*\(.*\)$/.test(previousLine) ? this._baseIndent : ''; + const extraSpaces = /^(for|while|if|try).*\(.*\)$/.test(previousLine) ? this._baseIndent : ''; previousLine = line; - line = spaces + extraSpaces + line; + const callCarryOver = line.startsWith('.with'); + line = spaces + extraSpaces + (callCarryOver ? this._baseIndent : '') + line; if (line.endsWith('{') || line.endsWith('[')) spaces += this._baseIndent; return this._baseOffset + line; diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index 5b296f68926f5..6c32e64d10ad8 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -22,6 +22,7 @@ import { describeFrame, toClickOptions, toModifiers } from './recorder/utils'; import { Page } from '../page'; import { Frame } from '../frames'; import { BrowserContext } from '../browserContext'; +import { JavaLanguageGenerator } from './recorder/java'; import { JavaScriptLanguageGenerator } from './recorder/javascript'; import { CSharpLanguageGenerator } from './recorder/csharp'; import { PythonLanguageGenerator } from './recorder/python'; @@ -81,6 +82,7 @@ export class RecorderSupplement { const language = params.language || context._options.sdkLanguage; const languages = new Set([ + new JavaLanguageGenerator(), new JavaScriptLanguageGenerator(), new PythonLanguageGenerator(false), new PythonLanguageGenerator(true), diff --git a/src/third_party/highlightjs/highlightjs/index.js b/src/third_party/highlightjs/highlightjs/index.js index af9e12dd38539..995333f42c81b 100644 --- a/src/third_party/highlightjs/highlightjs/index.js +++ b/src/third_party/highlightjs/highlightjs/index.js @@ -3,5 +3,6 @@ var hljs = require('./core'); hljs.registerLanguage('javascript', require('./languages/javascript')); hljs.registerLanguage('python', require('./languages/python')); hljs.registerLanguage('csharp', require('./languages/csharp')); +hljs.registerLanguage('java', require('./languages/java')); module.exports = hljs; \ No newline at end of file diff --git a/src/third_party/highlightjs/highlightjs/languages/java.js b/src/third_party/highlightjs/highlightjs/languages/java.js new file mode 100644 index 0000000000000..39170e511b510 --- /dev/null +++ b/src/third_party/highlightjs/highlightjs/languages/java.js @@ -0,0 +1,176 @@ +// https://docs.oracle.com/javase/specs/jls/se15/html/jls-3.html#jls-3.10 +var decimalDigits = '[0-9](_*[0-9])*'; +var frac = `\\.(${decimalDigits})`; +var hexDigits = '[0-9a-fA-F](_*[0-9a-fA-F])*'; +var NUMERIC = { + className: 'number', + variants: [ + // DecimalFloatingPointLiteral + // including ExponentPart + { begin: `(\\b(${decimalDigits})((${frac})|\\.)?|(${frac}))` + + `[eE][+-]?(${decimalDigits})[fFdD]?\\b` }, + // excluding ExponentPart + { begin: `\\b(${decimalDigits})((${frac})[fFdD]?\\b|\\.([fFdD]\\b)?)` }, + { begin: `(${frac})[fFdD]?\\b` }, + { begin: `\\b(${decimalDigits})[fFdD]\\b` }, + + // HexadecimalFloatingPointLiteral + { begin: `\\b0[xX]((${hexDigits})\\.?|(${hexDigits})?\\.(${hexDigits}))` + + `[pP][+-]?(${decimalDigits})[fFdD]?\\b` }, + + // DecimalIntegerLiteral + { begin: '\\b(0|[1-9](_*[0-9])*)[lL]?\\b' }, + + // HexIntegerLiteral + { begin: `\\b0[xX](${hexDigits})[lL]?\\b` }, + + // OctalIntegerLiteral + { begin: '\\b0(_*[0-7])*[lL]?\\b' }, + + // BinaryIntegerLiteral + { begin: '\\b0[bB][01](_*[01])*[lL]?\\b' }, + ], + relevance: 0 +}; + +/* +Language: Java +Author: Vsevolod Solovyov +Category: common, enterprise +Website: https://www.java.com/ +*/ + +function java(hljs) { + var JAVA_IDENT_RE = '[\u00C0-\u02B8a-zA-Z_$][\u00C0-\u02B8a-zA-Z_$0-9]*'; + var GENERIC_IDENT_RE = JAVA_IDENT_RE + '(<' + JAVA_IDENT_RE + '(\\s*,\\s*' + JAVA_IDENT_RE + ')*>)?'; + var KEYWORDS = 'false synchronized int abstract float private char boolean var static null if const ' + + 'for true while long strictfp finally protected import native final void ' + + 'enum else break transient catch instanceof byte super volatile case assert short ' + + 'package default double public try this switch continue throws protected public private ' + + 'module requires exports do'; + + var ANNOTATION = { + className: 'meta', + begin: '@' + JAVA_IDENT_RE, + contains: [ + { + begin: /\(/, + end: /\)/, + contains: ["self"] // allow nested () inside our annotation + }, + ] + }; + const NUMBER = NUMERIC; + + return { + name: 'Java', + aliases: ['jsp'], + keywords: KEYWORDS, + illegal: /<\/|#/, + contains: [ + hljs.COMMENT( + '/\\*\\*', + '\\*/', + { + relevance: 0, + contains: [ + { + // eat up @'s in emails to prevent them to be recognized as doctags + begin: /\w+@/, relevance: 0 + }, + { + className: 'doctag', + begin: '@[A-Za-z]+' + } + ] + } + ), + // relevance boost + { + begin: /import java\.[a-z]+\./, + keywords: "import", + relevance: 2 + }, + hljs.C_LINE_COMMENT_MODE, + hljs.C_BLOCK_COMMENT_MODE, + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + { + className: 'class', + beginKeywords: 'class interface enum', end: /[{;=]/, excludeEnd: true, + keywords: 'class interface enum', + illegal: /[:"\[\]]/, + contains: [ + { beginKeywords: 'extends implements' }, + hljs.UNDERSCORE_TITLE_MODE + ] + }, + { + // Expression keywords prevent 'keyword Name(...)' from being + // recognized as a function definition + beginKeywords: 'new throw return else', + relevance: 0 + }, + { + className: 'class', + begin: 'record\\s+' + hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', + returnBegin: true, + excludeEnd: true, + end: /[{;=]/, + keywords: KEYWORDS, + contains: [ + { beginKeywords: "record" }, + { + begin: hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', + returnBegin: true, + relevance: 0, + contains: [hljs.UNDERSCORE_TITLE_MODE] + }, + { + className: 'params', + begin: /\(/, end: /\)/, + keywords: KEYWORDS, + relevance: 0, + contains: [ + hljs.C_BLOCK_COMMENT_MODE + ] + }, + hljs.C_LINE_COMMENT_MODE, + hljs.C_BLOCK_COMMENT_MODE + ] + }, + { + className: 'function', + begin: '(' + GENERIC_IDENT_RE + '\\s+)+' + hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', returnBegin: true, end: /[{;=]/, + excludeEnd: true, + keywords: KEYWORDS, + contains: [ + { + begin: hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', returnBegin: true, + relevance: 0, + contains: [hljs.UNDERSCORE_TITLE_MODE] + }, + { + className: 'params', + begin: /\(/, end: /\)/, + keywords: KEYWORDS, + relevance: 0, + contains: [ + ANNOTATION, + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + NUMBER, + hljs.C_BLOCK_COMMENT_MODE + ] + }, + hljs.C_LINE_COMMENT_MODE, + hljs.C_BLOCK_COMMENT_MODE + ] + }, + NUMBER, + ANNOTATION + ] + }; +} + +module.exports = java; diff --git a/src/third_party/highlightjs/roll.sh b/src/third_party/highlightjs/roll.sh index 856e851bf0290..77190ac6f7b0f 100755 --- a/src/third_party/highlightjs/roll.sh +++ b/src/third_party/highlightjs/roll.sh @@ -5,7 +5,7 @@ set +x # Pick a stable release revision from here: # https://github.com/highlightjs/highlight.js/releases RELEASE_REVISION="af20048d5c601d6e30016d8171317bfdf8a6c242" -LANGUAGES="javascript python csharp" +LANGUAGES="javascript python csharp java" STYLES="tomorrow.css" trap "cd $(pwd -P)" EXIT diff --git a/test/cli/cli-codegen-1.spec.ts b/test/cli/cli-codegen-1.spec.ts index 72a0a6e0550d4..ec52c0d0eaf3e 100644 --- a/test/cli/cli-codegen-1.spec.ts +++ b/test/cli/cli-codegen-1.spec.ts @@ -47,6 +47,10 @@ describe('cli codegen', (suite, { browserName, headful, mode }) => { # Click text=Submit await page.click("text=Submit")`); + expect(sources.get('').text).toContain(` + // Click text=Submit + page.click("text=Submit");`); + expect(sources.get('').text).toContain(` // Click text=Submit await page.ClickAsync("text=Submit");`); @@ -113,6 +117,10 @@ await page.ClickAsync("text=Submit");`); # Click text=Submit await page.click("text=Submit")`); + expect(sources.get('').text).toContain(` + // Click text=Submit + page.click("text=Submit");`); + expect(sources.get('').text).toContain(` // Click text=Submit await page.ClickAsync("text=Submit");`); @@ -166,6 +174,9 @@ await page.ClickAsync("text=Submit");`); expect(sources.get('').text).toContain(` // Fill input[name="name"] await page.fill('input[name="name"]', 'John');`); + expect(sources.get('').text).toContain(` + // Fill input[name="name"] + page.fill("input[name=\\\"name\\\"]", "John");`); expect(sources.get('').text).toContain(` # Fill input[name="name"] @@ -216,6 +227,10 @@ await page.FillAsync(\"input[name=\\\"name\\\"]\", \"John\");`); // Press Enter with modifiers await page.press('input[name="name"]', 'Shift+Enter');`); + expect(sources.get('').text).toContain(` + // Press Enter with modifiers + page.press("input[name=\\\"name\\\"]", "Shift+Enter");`); + expect(sources.get('').text).toContain(` # Press Enter with modifiers page.press(\"input[name=\\\"name\\\"]\", \"Shift+Enter\")`); @@ -321,6 +336,10 @@ await page.PressAsync(\"input[name=\\\"name\\\"]\", \"Shift+Enter\");`); // Check input[name="accept"] await page.check('input[name="accept"]');`); + expect(sources.get('').text).toContain(` + // Check input[name="accept"] + page.check("input[name=\\\"accept\\\"]");`); + expect(sources.get('').text).toContain(` # Check input[name="accept"] page.check(\"input[name=\\\"accept\\\"]\")`); @@ -370,6 +389,10 @@ await page.CheckAsync(\"input[name=\\\"accept\\\"]\");`); // Uncheck input[name="accept"] await page.uncheck('input[name="accept"]');`); + expect(sources.get('').text).toContain(` + // Uncheck input[name="accept"] + page.uncheck("input[name=\\\"accept\\\"]");`); + expect(sources.get('').text).toContain(` # Uncheck input[name="accept"] page.uncheck(\"input[name=\\\"accept\\\"]\")`); @@ -401,6 +424,10 @@ await page.UncheckAsync(\"input[name=\\\"accept\\\"]\");`); // Select 2 await page.selectOption('select', '2');`); + expect(sources.get('').text).toContain(` + // Select 2 + page.selectOption("select", "2");`); + expect(sources.get('').text).toContain(` # Select 2 page.select_option(\"select\", \"2\")`); @@ -437,6 +464,12 @@ await page.SelectOptionAsync(\"select\", \"2\");`); page.click('text=link') ]);`); + expect(sources.get('').text).toContain(` + // Click text=link + Page page1 = page.waitForPopup(() -> { + page.click("text=link"); + });`); + expect(sources.get('').text).toContain(` # Click text=link with page.expect_popup() as popup_info: @@ -474,6 +507,11 @@ await Task.WhenAll( await page.click('text=link'); // assert.equal(page.url(), 'about:blank#foo');`); + expect(sources.get('').text).toContain(` + // Click text=link + page.click("text=link"); + // assert page.url().equals("about:blank#foo");`); + expect(sources.get('').text).toContain(` # Click text=link page.click(\"text=link\") @@ -512,6 +550,13 @@ await page.ClickAsync(\"text=link\"); page.click('text=link') ]);`); + expect(sources.get('').text).toContain(` + // Click text=link + // page.waitForNavigation(new Page.WaitForNavigationOptions().withUrl("about:blank#foo"), () -> + page.waitForNavigation(() -> { + page.click("text=link"); + });`); + expect(sources.get('').text).toContain(` # Click text=link # with page.expect_navigation(url=\"about:blank#foo\"): diff --git a/test/cli/cli-codegen-2.spec.ts b/test/cli/cli-codegen-2.spec.ts index daef698566b1f..c229c60339893 100644 --- a/test/cli/cli-codegen-2.spec.ts +++ b/test/cli/cli-codegen-2.spec.ts @@ -31,6 +31,10 @@ describe('cli codegen', (suite, { mode }) => { // Open new page const page = await context.newPage();`); + expect(sources.get('').text).toContain(` + // Open new page + Page page = context.newPage();`); + expect(sources.get('').text).toContain(` # Open new page page = context.new_page()`); @@ -53,6 +57,10 @@ var page = await context.NewPageAsync();`); // Open new page const page1 = await context.newPage();`); + expect(sources.get('').text).toContain(` + // Open new page + Page page1 = context.newPage();`); + expect(sources.get('').text).toContain(` # Open new page page1 = context.new_page()`); @@ -75,6 +83,9 @@ var page1 = await context.NewPageAsync();`); expect(sources.get('').text).toContain(` await page.close();`); + expect(sources.get('').text).toContain(` + page.close();`); + expect(sources.get('').text).toContain(` page.close()`); @@ -117,6 +128,10 @@ await page.CloseAsync();`); // Upload file-to-upload.txt await page.setInputFiles('input[type="file"]', 'file-to-upload.txt');`); + expect(sources.get('').text).toContain(` + // Upload file-to-upload.txt + page.setInputFiles("input[type=\\\"file\\\"]", Paths.get("file-to-upload.txt"));`); + expect(sources.get('').text).toContain(` # Upload file-to-upload.txt page.set_input_files(\"input[type=\\\"file\\\"]\", \"file-to-upload.txt\")`); @@ -150,6 +165,10 @@ await page.SetInputFilesAsync(\"input[type=\\\"file\\\"]\", \"file-to-upload.txt // Upload file-to-upload.txt, file-to-upload-2.txt await page.setInputFiles('input[type=\"file\"]', ['file-to-upload.txt', 'file-to-upload-2.txt']);`); + expect(sources.get('').text).toContain(` + // Upload file-to-upload.txt, file-to-upload-2.txt + page.setInputFiles("input[type=\\\"file\\\"]", new Path[] {Paths.get("file-to-upload.txt"), Paths.get("file-to-upload-2.txt")});`); + expect(sources.get('').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\"]`); @@ -182,6 +201,10 @@ await page.SetInputFilesAsync(\"input[type=\\\"file\\\"]\", new[] { \"file-to-up // Clear selected files await page.setInputFiles('input[type=\"file\"]', []);`); + expect(sources.get('').text).toContain(` + // Clear selected files + page.setInputFiles("input[type=\\\"file\\\"]", new Path[0]);`); + expect(sources.get('').text).toContain(` # Clear selected files page.set_input_files(\"input[type=\\\"file\\\"]\", []`); @@ -225,6 +248,12 @@ await page.SetInputFilesAsync(\"input[type=\\\"file\\\"]\", new[] { });`); page.click('text=Download') ]);`); + expect(sources.get('').text).toContain(` + // Click text=Download + Download download = page.waitForDownload(() -> { + page.click("text=Download"); + });`); + expect(sources.get('').text).toContain(` # Click text=Download with page.expect_download() as download_info: @@ -265,6 +294,14 @@ await Task.WhenAll( }); await page.click('text=click me');`); + expect(sources.get('').text).toContain(` + // Click text=click me + page.onceDialog(dialog -> { + System.out.println(String.format("Dialog message: %s", dialog.message())); + dialog.dismiss(); + }); + page.click("text=click me");`); + expect(sources.get('').text).toContain(` # Click text=click me page.once(\"dialog\", lambda dialog: dialog.dismiss()) @@ -359,6 +396,9 @@ await page.ClickAsync(\"text=click me\");`); expect(sources.get('').text).toContain(`await page1.fill('input', 'TextA');`); expect(sources.get('').text).toContain(`await page2.fill('input', 'TextB');`); + expect(sources.get('').text).toContain(`page1.fill("input", "TextA");`); + expect(sources.get('').text).toContain(`page2.fill("input", "TextB");`); + expect(sources.get('').text).toContain(`page1.fill(\"input\", \"TextA\")`); expect(sources.get('').text).toContain(`page2.fill(\"input\", \"TextB\")`); @@ -438,6 +478,10 @@ await page.ClickAsync(\"text=click me\");`); name: 'one' }).click('text=Hi, I\\'m frame');`); + expect(sources.get('').text).toContain(` + // Click text=Hi, I'm frame + page.frame("one").click("text=Hi, I'm frame");`); + expect(sources.get('').text).toContain(` # Click text=Hi, I'm frame page.frame(name=\"one\").click(\"text=Hi, I'm frame\")`); @@ -461,6 +505,10 @@ await page.GetFrame(name: \"one\").ClickAsync(\"text=Hi, I'm frame\");`); name: 'two' }).click('text=Hi, I\\'m frame');`); + expect(sources.get('').text).toContain(` + // Click text=Hi, I'm frame + page.frame("two").click("text=Hi, I'm frame");`); + expect(sources.get('').text).toContain(` # Click text=Hi, I'm frame page.frame(name=\"two\").click(\"text=Hi, I'm frame\")`); @@ -484,6 +532,10 @@ await page.GetFrame(name: \"two\").ClickAsync(\"text=Hi, I'm frame\");`); url: 'http://localhost:${server.PORT}/frames/frame.html' }).click('text=Hi, I\\'m frame');`); + expect(sources.get('').text).toContain(` + // Click text=Hi, I'm frame + page.frameByUrl("http://localhost:${server.PORT}/frames/frame.html").click("text=Hi, I'm frame");`); + expect(sources.get('').text).toContain(` # Click text=Hi, I'm frame page.frame(url=\"http://localhost:${server.PORT}/frames/frame.html\").click(\"text=Hi, I'm frame\")`); diff --git a/test/cli/cli-codegen-csharp.spec.ts b/test/cli/cli-codegen-csharp.spec.ts index 1ea6ece6d7d7a..1da87912977d7 100644 --- a/test/cli/cli-codegen-csharp.spec.ts +++ b/test/cli/cli-codegen-csharp.spec.ts @@ -43,7 +43,6 @@ it('should print the correct context options for custom settings', async ({ brow '--lang=es', '--proxy-server=http://myproxy:3128', '--timezone=Europe/Rome', - '--timeout=1000', '--user-agent=hardkodemium', '--viewport-size=1280,720', 'codegen', @@ -95,7 +94,6 @@ it('should print the correct context options when using a device and additional '--lang=es', '--proxy-server=http://myproxy:3128', '--timezone=Europe/Rome', - '--timeout=1000', '--user-agent=hardkodemium', '--viewport-size=1280,720', 'codegen', diff --git a/test/cli/cli-codegen-java.spec.ts b/test/cli/cli-codegen-java.spec.ts new file mode 100644 index 0000000000000..8e863e2b1fbef --- /dev/null +++ b/test/cli/cli-codegen-java.spec.ts @@ -0,0 +1,86 @@ +/** + * 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'; +import path from 'path'; +import { folio } from './cli.fixtures'; + +const { it, expect } = folio; + +const emptyHTML = new URL('file://' + path.join(__dirname, '..', 'assets', 'empty.html')).toString(); + +it('should print the correct imports and context options', async ({ runCLI, browserName }) => { + const cli = runCLI(['codegen', '--target=java', emptyHTML]); + const expectedResult = `import com.microsoft.playwright.*; +import com.microsoft.playwright.options.*; + +public class Example { + public static void main(String[] args) { + try (Playwright playwright = Playwright.create()) { + Browser browser = playwright.${browserName}().launch(new BrowserType.LaunchOptions() + .withHeadless(false)); + BrowserContext context = browser.newContext();`; + await cli.waitFor(expectedResult); + expect(cli.text()).toContain(expectedResult); +}); + +it('should print the correct context options for custom settings', async ({ runCLI, browserName }) => { + const cli = runCLI(['--color-scheme=light', 'codegen', '--target=java', emptyHTML]); + const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions() + .withColorScheme(ColorScheme.LIGHT));`; + await cli.waitFor(expectedResult); + expect(cli.text()).toContain(expectedResult); +}); + +it('should print the correct context options when using a device', async ({ runCLI }) => { + const cli = runCLI(['--device=Pixel 2', 'codegen', '--target=java', emptyHTML]); + const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions() + .withUserAgent("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36") + .withViewportSize(411, 731) + .withDeviceScaleFactor(2.625) + .withIsMobile(true) + .withHasTouch(true));`; + await cli.waitFor(expectedResult); + expect(cli.text()).toContain(expectedResult); +}); + +it('should print the correct context options when using a device and additional options', async ({ runCLI }) => { + const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', 'codegen', '--target=java', emptyHTML]); + const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions() + .withColorScheme(ColorScheme.LIGHT) + .withUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/604.1") + .withViewportSize(414, 896) + .withDeviceScaleFactor(2) + .withIsMobile(true) + .withHasTouch(true));`; + await cli.waitFor(expectedResult); + expect(cli.text()).toContain(expectedResult); +}); + +it('should print load/save storage_state', async ({ runCLI, browserName, testInfo }) => { + const loadFileName = testInfo.outputPath('load.json'); + const saveFileName = testInfo.outputPath('save.json'); + await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8'); + const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, 'codegen', '--target=java', emptyHTML]); + const expectedResult1 = `BrowserContext context = browser.newContext(new Browser.NewContextOptions() + .withStorageStatePath(Paths.get("${loadFileName}")));`; + await cli.waitFor(expectedResult1); + + const expectedResult2 = ` + // --------------------- + context.storageState(new BrowserContext.StorageStateOptions().withPath("${saveFileName}"))`; + await cli.waitFor(expectedResult2); +});