diff --git a/packages/playwright-core/src/cli/cli.ts b/packages/playwright-core/src/cli/cli.ts index 53fe63e627..aaf8bb27e7 100755 --- a/packages/playwright-core/src/cli/cli.ts +++ b/packages/playwright-core/src/cli/cli.ts @@ -68,7 +68,7 @@ Examples: commandWithOpenOptions('codegen [url]', 'open page and generate code for user actions', [ ['-o, --output ', 'saves the generated script to a file'], - ['--target ', `language to generate, one of javascript, test, python, python-async, csharp`, language()], + ['--target ', `language to generate, one of javascript, test, python, python-async, pytest, csharp, java`, language()], ]).action(function(url, options) { codegen(options, url, options.target, options.output).catch(logErrorAndExit); }).addHelpText('afterAll', ` diff --git a/packages/playwright-core/src/server/recorder/csharp.ts b/packages/playwright-core/src/server/recorder/csharp.ts index a5ba8268ed..ecc62868c0 100644 --- a/packages/playwright-core/src/server/recorder/csharp.ts +++ b/packages/playwright-core/src/server/recorder/csharp.ts @@ -161,14 +161,14 @@ export class CSharpLanguageGenerator implements LanguageGenerator { function formatObject(value: any, indent = ' ', name = ''): string { if (typeof value === 'string') { - if (['permissions', 'colorScheme', 'modifiers', 'button'].includes(name)) + if (['permissions', 'colorScheme', 'modifiers', 'button', 'recordHarContent', 'recordHarMode', 'serviceWorkers'].includes(name)) return `${getClassName(name)}.${toPascal(value)}`; return quote(value); } if (Array.isArray(value)) return `new[] { ${value.map(o => formatObject(o, indent, name)).join(', ')} }`; if (typeof value === 'object') { - const keys = Object.keys(value); + const keys = Object.keys(value).filter(key => value[key] !== undefined).sort(); if (!keys.length) return name ? `new ${getClassName(name)}` : ''; const tokens: string[] = []; @@ -193,6 +193,9 @@ function getClassName(value: string): string { case 'permissions': return 'ContextPermission'; case 'modifiers': return 'KeyboardModifier'; case 'button': return 'MouseButton'; + case 'recordHarMode': return 'HarMode'; + case 'recordHarContent': return 'HarContentPolicy'; + case 'serviceWorkers': return 'ServiceWorkerPolicy'; default: return toPascal(value); } } @@ -209,19 +212,32 @@ function toPascal(value: string): string { return value[0].toUpperCase() + value.slice(1); } +function convertContextOptions(options: BrowserContextOptions): any { + const result: any = { ...options }; + if (options.recordHar) { + result['recordHarPath'] = options.recordHar.path; + result['recordHarContent'] = options.recordHar.content; + result['recordHarMode'] = options.recordHar.mode; + result['recordHarOmitContent'] = options.recordHar.omitContent; + result['recordHarUrlFilter'] = options.recordHar.urlFilter; + delete result.recordHar; + } + return result; +} + function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string { const device = deviceName && deviceDescriptors[deviceName]; if (!device) { if (!Object.entries(options).length) return ''; - return formatObject(options, ' ', 'BrowserNewContextOptions'); + return formatObject(convertContextOptions(options), ' ', 'BrowserNewContextOptions'); } options = sanitizeDeviceOptions(device, options); if (!Object.entries(options).length) return `playwright.Devices[${quote(deviceName!)}]`; - return formatObject(options, ' ', `BrowserNewContextOptions(playwright.Devices[${quote(deviceName!)}])`); + return formatObject(convertContextOptions(options), ' ', `BrowserNewContextOptions(playwright.Devices[${quote(deviceName!)}])`); } class CSharpFormatter { diff --git a/packages/playwright-core/src/server/recorder/java.ts b/packages/playwright-core/src/server/recorder/java.ts index 14f29f7204..af08e8af96 100644 --- a/packages/playwright-core/src/server/recorder/java.ts +++ b/packages/playwright-core/src/server/recorder/java.ts @@ -175,13 +175,13 @@ function formatSelectOption(options: string | string[]): string { function formatLaunchOptions(options: any): string { const lines = []; - if (!Object.keys(options).length) + if (!Object.keys(options).filter(key => options[key] !== undefined).length) return ''; lines.push('new BrowserType.LaunchOptions()'); - if (typeof options.headless === 'boolean') - lines.push(` .setHeadless(false)`); if (options.channel) lines.push(` .setChannel(${quote(options.channel)})`); + if (typeof options.headless === 'boolean') + lines.push(` .setHeadless(false)`); return lines.join('\n'); } @@ -210,6 +210,18 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName: lines.push(` .setLocale(${quote(options.locale)})`); if (options.proxy) lines.push(` .setProxy(new Proxy(${quote(options.proxy.server)}))`); + if (options.recordHar?.content) + lines.push(` .setRecordHarContent(HarContentPolicy.${options.recordHar?.content.toUpperCase()})`); + if (options.recordHar?.mode) + lines.push(` .setRecordHarMode(HarMode.${options.recordHar?.mode.toUpperCase()})`); + if (options.recordHar?.omitContent) + lines.push(` .setRecordHarOmitContent(true)`); + if (options.recordHar?.path) + lines.push(` .setRecordHarPath(Paths.get(${quote(options.recordHar.path)}))`); + if (options.recordHar?.urlFilter) + lines.push(` .setRecordHarUrlFilter(${quote(options.recordHar.urlFilter as string)})`); + if (options.serviceWorkers) + lines.push(` .setServiceWorkers(ServiceWorkerPolicy.${options.serviceWorkers.toUpperCase()})`); if (options.storageState) lines.push(` .setStorageStatePath(Paths.get(${quote(options.storageState as string)}))`); if (options.timezoneId) diff --git a/packages/playwright-core/src/server/recorder/javascript.ts b/packages/playwright-core/src/server/recorder/javascript.ts index 2ccbd77a65..3182c7cdf8 100644 --- a/packages/playwright-core/src/server/recorder/javascript.ts +++ b/packages/playwright-core/src/server/recorder/javascript.ts @@ -222,7 +222,7 @@ function formatObject(value: any, indent = ' '): string { if (Array.isArray(value)) return `[${value.map(o => formatObject(o)).join(', ')}]`; if (typeof value === 'object') { - const keys = Object.keys(value); + const keys = Object.keys(value).filter(key => value[key] !== undefined).sort(); if (!keys.length) return '{}'; const tokens: string[] = []; diff --git a/packages/playwright-core/src/server/recorder/python.ts b/packages/playwright-core/src/server/recorder/python.ts index 695e52cd80..21fcced3cc 100644 --- a/packages/playwright-core/src/server/recorder/python.ts +++ b/packages/playwright-core/src/server/recorder/python.ts @@ -240,7 +240,7 @@ function toSnakeCase(name: string): string { } function formatOptions(value: any, hasArguments: boolean, asDict?: boolean): string { - const keys = Object.keys(value); + const keys = Object.keys(value).filter(key => value[key] !== undefined).sort(); if (!keys.length) return ''; return (hasArguments ? ', ' : '') + keys.map(key => { @@ -250,11 +250,24 @@ function formatOptions(value: any, hasArguments: boolean, asDict?: boolean): str }).join(', '); } +function convertContextOptions(options: BrowserContextOptions): any { + const result: any = { ...options }; + if (options.recordHar) { + result['record_har_path'] = options.recordHar.path; + result['record_har_content'] = options.recordHar.content; + result['record_har_mode'] = options.recordHar.mode; + result['record_har_omit_content'] = options.recordHar.omitContent; + result['record_har_url_filter'] = options.recordHar.urlFilter; + delete result.recordHar; + } + return result; +} + function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined, asDict?: boolean): string { const device = deviceName && deviceDescriptors[deviceName]; if (!device) - return formatOptions(options, false, asDict); - return `**playwright.devices[${quote(deviceName!)}]` + formatOptions(sanitizeDeviceOptions(device, options), true, asDict); + return formatOptions(convertContextOptions(options), false, asDict); + return `**playwright.devices[${quote(deviceName!)}]` + formatOptions(convertContextOptions(sanitizeDeviceOptions(device, options)), true, asDict); } class PythonFormatter { diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index 30e3f1bf50..f9d99c0aaf 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -544,14 +544,6 @@ test.describe('cli codegen', () => { expect(fs.existsSync(traceFileName)).toBeTruthy(); }); - test('should --save-har', async ({ runCLI }, testInfo) => { - const harFileName = testInfo.outputPath('har.har'); - const cli = runCLI([`--save-har=${harFileName}`]); - await cli.exited; - const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8')); - expect(json.log.creator.name).toBe('Playwright'); - }); - test('should fill tricky characters', async ({ page, openRecorder }) => { const recorder = await openRecorder(); diff --git a/tests/library/inspector/cli-codegen-csharp.spec.ts b/tests/library/inspector/cli-codegen-csharp.spec.ts index f78b35ad82..3c22373a1a 100644 --- a/tests/library/inspector/cli-codegen-csharp.spec.ts +++ b/tests/library/inspector/cli-codegen-csharp.spec.ts @@ -20,7 +20,7 @@ import { test, expect } from './inspectorTest'; const emptyHTML = new URL('file://' + path.join(__dirname, '..', '..', 'assets', 'empty.html')).toString(); const launchOptions = (channel: string) => { - return channel ? `Headless = false,\n Channel = "${channel}",` : `Headless = false,`; + return channel ? `Channel = "${channel}",\n Headless = false,` : `Headless = false,`; }; function capitalize(browserName: string): string { @@ -70,21 +70,21 @@ test('should print the correct context options for custom settings', async ({ br }); var context = await browser.NewContextAsync(new BrowserNewContextOptions { - ViewportSize = new ViewportSize - { - Width = 1280, - Height = 720, - }, + ColorScheme = ColorScheme.Dark, Geolocation = new Geolocation { Latitude = 37.819722m, Longitude = -122.478611m, }, - Permissions = new[] { ContextPermission.Geolocation }, - UserAgent = "hardkodemium", Locale = "es", - ColorScheme = ColorScheme.Dark, + Permissions = new[] { ContextPermission.Geolocation }, TimezoneId = "Europe/Rome", + UserAgent = "hardkodemium", + ViewportSize = new ViewportSize + { + Height = 720, + Width = 1280, + }, });`; await cli.waitFor(expectedResult); expect(cli.text()).toContain(expectedResult); @@ -131,21 +131,21 @@ test('should print the correct context options when using a device and additiona }); var context = await browser.NewContextAsync(new BrowserNewContextOptions(playwright.Devices["iPhone 11"]) { - UserAgent = "hardkodemium", - ViewportSize = new ViewportSize - { - Width = 1280, - Height = 720, - }, + ColorScheme = ColorScheme.Dark, Geolocation = new Geolocation { Latitude = 37.819722m, Longitude = -122.478611m, }, - Permissions = new[] { ContextPermission.Geolocation }, Locale = "es", - ColorScheme = ColorScheme.Dark, + Permissions = new[] { ContextPermission.Geolocation }, TimezoneId = "Europe/Rome", + UserAgent = "hardkodemium", + ViewportSize = new ViewportSize + { + Height = 720, + Width = 1280, + }, });`; await cli.waitFor(expectedResult); @@ -176,3 +176,20 @@ test('should print load/save storageState', async ({ browserName, channel, runCL `; await cli.waitFor(expectedResult2); }); + +test('should work with --save-har', async ({ runCLI }, testInfo) => { + const harFileName = testInfo.outputPath('har.har'); + const cli = runCLI(['--target=csharp', `--save-har=${harFileName}`]); + const expectedResult = ` + var context = await browser.NewContextAsync(new BrowserNewContextOptions + { + RecordHarMode = HarMode.Minimal, + RecordHarPath = ${JSON.stringify(harFileName)}, + ServiceWorkers = ServiceWorkerPolicy.Block, + });`; + await cli.waitFor(expectedResult).catch(e => e); + expect(cli.text()).toContain(expectedResult); + await cli.exited; + const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8')); + expect(json.log.creator.name).toBe('Playwright'); +}); diff --git a/tests/library/inspector/cli-codegen-java.spec.ts b/tests/library/inspector/cli-codegen-java.spec.ts index e41a6746b8..d374979839 100644 --- a/tests/library/inspector/cli-codegen-java.spec.ts +++ b/tests/library/inspector/cli-codegen-java.spec.ts @@ -20,7 +20,7 @@ import { test, expect } from './inspectorTest'; const emptyHTML = new URL('file://' + path.join(__dirname, '..', '..', 'assets', 'empty.html')).toString(); const launchOptions = (channel: string) => { - return channel ? `.setHeadless(false)\n .setChannel("${channel}")` : '.setHeadless(false)'; + return channel ? `.setChannel("${channel}")\n .setHeadless(false)` : '.setHeadless(false)'; }; test('should print the correct imports and context options', async ({ runCLI, channel, browserName }) => { @@ -83,10 +83,24 @@ test('should print load/save storage_state', async ({ runCLI, browserName }, tes await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8'); const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=java', emptyHTML]); const expectedResult1 = `BrowserContext context = browser.newContext(new Browser.NewContextOptions() - .setStorageStatePath(Paths.get("${loadFileName.replace(/\\/g, '\\\\')}")));`; + .setStorageStatePath(Paths.get(${JSON.stringify(loadFileName)})));`; await cli.waitFor(expectedResult1); const expectedResult2 = ` context.storageState(new BrowserContext.StorageStateOptions().setPath("${saveFileName.replace(/\\/g, '\\\\')}"))`; await cli.waitFor(expectedResult2); }); + +test('should work with --save-har', async ({ runCLI }, testInfo) => { + const harFileName = testInfo.outputPath('har.har'); + const cli = runCLI(['--target=java', `--save-har=${harFileName}`]); + const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions() + .setRecordHarMode(HarMode.MINIMAL) + .setRecordHarPath(Paths.get(${JSON.stringify(harFileName)})) + .setServiceWorkers(ServiceWorkerPolicy.BLOCK));`; + await cli.waitFor(expectedResult).catch(e => e); + expect(cli.text()).toContain(expectedResult); + await cli.exited; + const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8')); + expect(json.log.creator.name).toBe('Playwright'); +}); diff --git a/tests/library/inspector/cli-codegen-javascript.spec.ts b/tests/library/inspector/cli-codegen-javascript.spec.ts index fe202ccb2c..4f76fdd7f3 100644 --- a/tests/library/inspector/cli-codegen-javascript.spec.ts +++ b/tests/library/inspector/cli-codegen-javascript.spec.ts @@ -21,7 +21,7 @@ import { test, expect } from './inspectorTest'; const emptyHTML = new URL('file://' + path.join(__dirname, '..', '..', 'assets', 'empty.html')).toString(); const launchOptions = (channel: string) => { - return channel ? `headless: false,\n channel: '${channel}'` : 'headless: false'; + return channel ? `channel: '${channel}',\n headless: false` : 'headless: false'; }; test('should print the correct imports and context options', async ({ browserName, channel, runCLI }) => { diff --git a/tests/library/inspector/cli-codegen-python-async.spec.ts b/tests/library/inspector/cli-codegen-python-async.spec.ts index 631e160a7b..7df2935672 100644 --- a/tests/library/inspector/cli-codegen-python-async.spec.ts +++ b/tests/library/inspector/cli-codegen-python-async.spec.ts @@ -20,7 +20,7 @@ import { test, expect } from './inspectorTest'; const emptyHTML = new URL('file://' + path.join(__dirname, '..', '..', 'assets', 'empty.html')).toString(); const launchOptions = (channel: string) => { - return channel ? `headless=False, channel="${channel}"` : 'headless=False'; + return channel ? `channel="${channel}", headless=False` : 'headless=False'; }; test('should print the correct imports and context options', async ({ browserName, channel, runCLI }) => { @@ -151,3 +151,14 @@ asyncio.run(main()) `; await cli.waitFor(expectedResult2); }); + +test('should work with --save-har', async ({ runCLI }, testInfo) => { + const harFileName = testInfo.outputPath('har.har'); + const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`]); + const expectedResult = `context = await browser.new_context(record_har_mode="minimal", record_har_path=${JSON.stringify(harFileName)}, service_workers="block")`; + await cli.waitFor(expectedResult).catch(e => e); + expect(cli.text()).toContain(expectedResult); + await cli.exited; + const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8')); + expect(json.log.creator.name).toBe('Playwright'); +}); diff --git a/tests/library/inspector/cli-codegen-python.spec.ts b/tests/library/inspector/cli-codegen-python.spec.ts index 40b81476dd..b0b5e19dc2 100644 --- a/tests/library/inspector/cli-codegen-python.spec.ts +++ b/tests/library/inspector/cli-codegen-python.spec.ts @@ -20,7 +20,7 @@ import { test, expect } from './inspectorTest'; const emptyHTML = new URL('file://' + path.join(__dirname, '..', '..', 'assets', 'empty.html')).toString(); const launchOptions = (channel: string) => { - return channel ? `headless=False, channel="${channel}"` : 'headless=False'; + return channel ? `channel="${channel}", headless=False` : 'headless=False'; }; test('should print the correct imports and context options', async ({ runCLI, channel, browserName }) => { diff --git a/tests/library/inspector/cli-codegen-test.spec.ts b/tests/library/inspector/cli-codegen-test.spec.ts index 25604566f0..7f32c22183 100644 --- a/tests/library/inspector/cli-codegen-test.spec.ts +++ b/tests/library/inspector/cli-codegen-test.spec.ts @@ -90,3 +90,18 @@ test('test', async ({ page }) => {`; await cli.waitFor(expectedResult); }); + +test('should work with --save-har', async ({ runCLI }, testInfo) => { + const harFileName = testInfo.outputPath('har.har'); + const cli = runCLI(['--target=test', `--save-har=${harFileName}`]); + const expectedResult = ` + recordHar: { + mode: 'minimal', + path: '${harFileName.replace(/\\/g, '\\\\')}' + }`; + await cli.waitFor(expectedResult).catch(e => e); + expect(cli.text()).toContain(expectedResult); + await cli.exited; + const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8')); + expect(json.log.creator.name).toBe('Playwright'); +});