From 59118b83f9c64ae57f098e7cf2ed44ca34c2c1c4 Mon Sep 17 00:00:00 2001 From: Pranav Jain Date: Mon, 12 Dec 2022 20:45:19 +0530 Subject: [PATCH] feat(android): add support for passing CR args & proxy when launching browser (#19212) Fixes https://github.com/microsoft/playwright/issues/19211 --- docs/src/api/class-androiddevice.md | 6 +++ .../playwright-core/src/protocol/validator.ts | 1 + .../src/server/android/android.ts | 37 +++++++++++++++---- packages/playwright-core/types/types.d.ts | 32 ++++++++++++++++ packages/protocol/src/channels.ts | 2 + packages/protocol/src/protocol.yml | 3 ++ tests/android/browser.spec.ts | 28 ++++++++++++++ 7 files changed, 102 insertions(+), 7 deletions(-) diff --git a/docs/src/api/class-androiddevice.md b/docs/src/api/class-androiddevice.md index 6ab8f95329..cff6c2e5de 100644 --- a/docs/src/api/class-androiddevice.md +++ b/docs/src/api/class-androiddevice.md @@ -143,6 +143,12 @@ Optional package name to launch instead of default Chrome for Android. ### option: AndroidDevice.launchBrowser.-inline- = %%-shared-context-params-list-v1.8-%% * since: v1.9 +### option: AndroidDevice.launchBrowser.proxy = %%-browser-option-proxy-%% +* since: v1.29 + +### option: AndroidDevice.launchBrowser.args = %%-browser-option-args-%% +* since: v1.29 + ## async method: AndroidDevice.longTap * since: v1.9 diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 53ececed59..85f6b5bcc8 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2388,6 +2388,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({ strictSelectors: tOptional(tBoolean), serviceWorkers: tOptional(tEnum(['allow', 'block'])), pkg: tOptional(tString), + args: tOptional(tArray(tString)), proxy: tOptional(tObject({ server: tString, bypass: tOptional(tString), diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 3b5182ae12..7819f2af1a 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -260,21 +260,44 @@ export class AndroidDevice extends SdkObject { this.emit(AndroidDevice.Events.Close); } - async launchBrowser(pkg: string = 'com.android.chrome', options: channels.BrowserNewContextParams): Promise { + async launchBrowser(pkg: string = 'com.android.chrome', options: channels.AndroidDeviceLaunchBrowserParams): Promise { debug('pw:android')('Force-stopping', pkg); await this._backend.runCommand(`shell:am force-stop ${pkg}`); const socketName = isUnderTest() ? 'webview_devtools_remote_playwright_test' : ('playwright-' + createGuid()); - const commandLine = [ + const commandLine = this._defaultArgs(options, socketName).join(' '); + debug('pw:android')('Starting', pkg, commandLine); + await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`); + await this._backend.runCommand(`shell:am start -a android.intent.action.VIEW -d about:blank ${pkg}`); + return await this._connectToBrowser(socketName, options); + } + + private _defaultArgs(options: channels.AndroidDeviceLaunchBrowserParams, socketName: string): string[] { + const chromeArguments = [ '_', '--disable-fre', '--no-default-browser-check', `--remote-debugging-socket-name=${socketName}`, ...chromiumSwitches, - ].join(' '); - debug('pw:android')('Starting', pkg, commandLine); - await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`); - await this._backend.runCommand(`shell:am start -a android.intent.action.VIEW -d about:blank ${pkg}`); - return await this._connectToBrowser(socketName, options); + ...this._innerDefaultArgs(options) + ]; + return chromeArguments; + } + + private _innerDefaultArgs(options: channels.AndroidDeviceLaunchBrowserParams): string[] { + const { args = [], proxy } = options; + const chromeArguments = []; + if (proxy) { + chromeArguments.push(`--proxy-server=${proxy.server}`); + const proxyBypassRules = []; + if (proxy.bypass) + proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t)); + if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>')) + proxyBypassRules.push('<-loopback>'); + if (proxyBypassRules.length > 0) + chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`); + } + chromeArguments.push(...args); + return chromeArguments; } async connectToWebView(socketName: string): Promise { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 47078c3c3c..053181936d 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -12945,6 +12945,12 @@ export interface AndroidDevice { */ acceptDownloads?: boolean; + /** + * Additional arguments to pass to the browser instance. The list of Chromium flags can be found + * [here](http://peter.sh/experiments/chromium-command-line-switches/). + */ + args?: Array; + /** * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route), @@ -13067,6 +13073,32 @@ export interface AndroidDevice { */ permissions?: Array; + /** + * Network proxy settings. + */ + proxy?: { + /** + * Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example `http://myproxy.com:3128` or + * `socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP proxy. + */ + server: string; + + /** + * Optional comma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`. + */ + bypass?: string; + + /** + * Optional username to use if HTTP proxy requires authentication. + */ + username?: string; + + /** + * Optional password to use if HTTP proxy requires authentication. + */ + password?: string; + }; + /** * Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file. * If not specified, the HAR is not recorded. Make sure to await diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 010da28b8c..70168edfb1 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -4330,6 +4330,7 @@ export type AndroidDeviceLaunchBrowserParams = { strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', pkg?: string, + args?: string[], proxy?: { server: string, bypass?: string, @@ -4384,6 +4385,7 @@ export type AndroidDeviceLaunchBrowserOptions = { strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', pkg?: string, + args?: string[], proxy?: { server: string, bypass?: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index bc29cb9674..acfcf8223f 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -3262,6 +3262,9 @@ AndroidDevice: parameters: $mixin: ContextOptions pkg: string? + args: + type: array? + items: string proxy: type: object? properties: diff --git a/tests/android/browser.spec.ts b/tests/android/browser.spec.ts index ee8aa104f8..9a189fe16a 100644 --- a/tests/android/browser.spec.ts +++ b/tests/android/browser.spec.ts @@ -32,6 +32,34 @@ test('androidDevice.launchBrowser', async function({ androidDevice }) { await context.close(); }); +test('androidDevice.launchBrowser should pass args with spaces', async ({ androidDevice }) => { + const context = await androidDevice.launchBrowser({ args: ['--user-agent=I am Foo'] }); + const page = await context.newPage(); + const userAgent = await page.evaluate(() => navigator.userAgent); + await context.close(); + expect(userAgent).toBe('I am Foo'); +}); + +test('androidDevice.launchBrowser should throw for bad proxy server value', async ({ androidDevice }) => { + const error = await androidDevice.launchBrowser({ + // @ts-expect-error server must be a string + proxy: { server: 123 } + }).catch(e => e); + expect(error.message).toContain('proxy.server: expected string, got number'); +}); + +test('androidDevice.launchBrowser should pass proxy config', async ({ androidDevice, server, mode }) => { + test.skip(mode === 'docker', 'proxy is not supported for remote connection'); + server.setRoute('/target.html', async (req, res) => { + res.end('Served by the proxy'); + }); + const context = await androidDevice.launchBrowser({ proxy: { server: `localhost:${server.PORT}` } }); + const page = await context.newPage(); + await page.goto('http://non-existent.com/target.html'); + expect(await page.title()).toBe('Served by the proxy'); + await context.close(); +}); + test('should create new page', async function({ androidDevice }) { const context = await androidDevice.launchBrowser(); const page = await context.newPage();