feat(android): add support for passing CR args & proxy when launching browser (#19212)
Fixes https://github.com/microsoft/playwright/issues/19211
This commit is contained in:
parent
a2172e1799
commit
59118b83f9
|
|
@ -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-%%
|
### option: AndroidDevice.launchBrowser.-inline- = %%-shared-context-params-list-v1.8-%%
|
||||||
* since: v1.9
|
* 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
|
## async method: AndroidDevice.longTap
|
||||||
* since: v1.9
|
* since: v1.9
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2388,6 +2388,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
|
||||||
strictSelectors: tOptional(tBoolean),
|
strictSelectors: tOptional(tBoolean),
|
||||||
serviceWorkers: tOptional(tEnum(['allow', 'block'])),
|
serviceWorkers: tOptional(tEnum(['allow', 'block'])),
|
||||||
pkg: tOptional(tString),
|
pkg: tOptional(tString),
|
||||||
|
args: tOptional(tArray(tString)),
|
||||||
proxy: tOptional(tObject({
|
proxy: tOptional(tObject({
|
||||||
server: tString,
|
server: tString,
|
||||||
bypass: tOptional(tString),
|
bypass: tOptional(tString),
|
||||||
|
|
|
||||||
|
|
@ -260,21 +260,44 @@ export class AndroidDevice extends SdkObject {
|
||||||
this.emit(AndroidDevice.Events.Close);
|
this.emit(AndroidDevice.Events.Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchBrowser(pkg: string = 'com.android.chrome', options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
async launchBrowser(pkg: string = 'com.android.chrome', options: channels.AndroidDeviceLaunchBrowserParams): Promise<BrowserContext> {
|
||||||
debug('pw:android')('Force-stopping', pkg);
|
debug('pw:android')('Force-stopping', pkg);
|
||||||
await this._backend.runCommand(`shell:am force-stop ${pkg}`);
|
await this._backend.runCommand(`shell:am force-stop ${pkg}`);
|
||||||
const socketName = isUnderTest() ? 'webview_devtools_remote_playwright_test' : ('playwright-' + createGuid());
|
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',
|
'--disable-fre',
|
||||||
'--no-default-browser-check',
|
'--no-default-browser-check',
|
||||||
`--remote-debugging-socket-name=${socketName}`,
|
`--remote-debugging-socket-name=${socketName}`,
|
||||||
...chromiumSwitches,
|
...chromiumSwitches,
|
||||||
].join(' ');
|
...this._innerDefaultArgs(options)
|
||||||
debug('pw:android')('Starting', pkg, commandLine);
|
];
|
||||||
await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
|
return chromeArguments;
|
||||||
await this._backend.runCommand(`shell:am start -a android.intent.action.VIEW -d about:blank ${pkg}`);
|
}
|
||||||
return await this._connectToBrowser(socketName, options);
|
|
||||||
|
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<BrowserContext> {
|
async connectToWebView(socketName: string): Promise<BrowserContext> {
|
||||||
|
|
|
||||||
32
packages/playwright-core/types/types.d.ts
vendored
32
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -12945,6 +12945,12 @@ export interface AndroidDevice {
|
||||||
*/
|
*/
|
||||||
acceptDownloads?: boolean;
|
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<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto),
|
* 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),
|
* [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),
|
||||||
|
|
@ -13067,6 +13073,32 @@ export interface AndroidDevice {
|
||||||
*/
|
*/
|
||||||
permissions?: Array<string>;
|
permissions?: Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* 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
|
* If not specified, the HAR is not recorded. Make sure to await
|
||||||
|
|
|
||||||
|
|
@ -4330,6 +4330,7 @@ export type AndroidDeviceLaunchBrowserParams = {
|
||||||
strictSelectors?: boolean,
|
strictSelectors?: boolean,
|
||||||
serviceWorkers?: 'allow' | 'block',
|
serviceWorkers?: 'allow' | 'block',
|
||||||
pkg?: string,
|
pkg?: string,
|
||||||
|
args?: string[],
|
||||||
proxy?: {
|
proxy?: {
|
||||||
server: string,
|
server: string,
|
||||||
bypass?: string,
|
bypass?: string,
|
||||||
|
|
@ -4384,6 +4385,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
||||||
strictSelectors?: boolean,
|
strictSelectors?: boolean,
|
||||||
serviceWorkers?: 'allow' | 'block',
|
serviceWorkers?: 'allow' | 'block',
|
||||||
pkg?: string,
|
pkg?: string,
|
||||||
|
args?: string[],
|
||||||
proxy?: {
|
proxy?: {
|
||||||
server: string,
|
server: string,
|
||||||
bypass?: string,
|
bypass?: string,
|
||||||
|
|
|
||||||
|
|
@ -3262,6 +3262,9 @@ AndroidDevice:
|
||||||
parameters:
|
parameters:
|
||||||
$mixin: ContextOptions
|
$mixin: ContextOptions
|
||||||
pkg: string?
|
pkg: string?
|
||||||
|
args:
|
||||||
|
type: array?
|
||||||
|
items: string
|
||||||
proxy:
|
proxy:
|
||||||
type: object?
|
type: object?
|
||||||
properties:
|
properties:
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,34 @@ test('androidDevice.launchBrowser', async function({ androidDevice }) {
|
||||||
await context.close();
|
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('<html><title>Served by the proxy</title></html>');
|
||||||
|
});
|
||||||
|
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 }) {
|
test('should create new page', async function({ androidDevice }) {
|
||||||
const context = await androidDevice.launchBrowser();
|
const context = await androidDevice.launchBrowser();
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue