From cbc486079f0989bdaf75eab9802f2b60593142d2 Mon Sep 17 00:00:00 2001 From: sipayrt Date: Mon, 18 Mar 2024 14:19:55 +0300 Subject: [PATCH] feat: add ability to enable new Chrome headless mode --- README.md | 2 +- src/browser/new-browser.js | 14 +++-- src/config/browser-options.js | 17 +++++- src/config/types.ts | 2 +- test/src/browser/new-browser.js | 36 ++++++++--- test/src/config/browser-options.js | 97 ++++++++++++++++++++++-------- 6 files changed, 124 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index d944a1e15..04885344a 100644 --- a/README.md +++ b/README.md @@ -1234,7 +1234,7 @@ Cloud service access key or secret key. Default value is `null`. Ability to choose different datacenters for run in cloud service. Default value is `null`. #### headless -Ability to run headless browser in cloud service. Default value is `null`. +Ability to run headless browser in cloud service. Default value is `null`. Can be set as a Boolean (the default value of the browser will be used). For Chrome browsers starting from version 112 also can be specified as a string with "new"|"old" values - this will enable the new headless mode (see [Chrome's blog post](https://developer.chrome.com/docs/chromium/new-headless)). #### isolation Ability to execute tests in isolated clean-state environment ([incognito browser context](https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-createBrowserContext)). It means that `testsPerSession` can be set to `Infinity` in order to speed up tests execution and save browser resources. Currently works only in chrome@93 and higher. Default value is `null`, but `true` for chrome@93 and higher. diff --git a/src/browser/new-browser.js b/src/browser/new-browser.js index aba217595..be02b5482 100644 --- a/src/browser/new-browser.js +++ b/src/browser/new-browser.js @@ -16,19 +16,23 @@ const DEFAULT_PORT = 4444; const headlessBrowserOptions = { chrome: { capabilityName: "goog:chromeOptions", - args: ["headless", "disable-gpu"], + getArgs: headlessMode => { + const headlessValue = _.isBoolean(headlessMode) ? "headless" : `headless=${headlessMode}`; + + return [headlessValue, "disable-gpu"]; + }, }, firefox: { capabilityName: "moz:firefoxOptions", - args: ["-headless"], + getArgs: () => ["-headless"], }, msedge: { capabilityName: "ms:edgeOptions", - args: ["--headless"], + getArgs: () => ["--headless"], }, edge: { capabilityName: "ms:edgeOptions", - args: ["--headless"], + getArgs: () => ["--headless"], }, }; @@ -138,7 +142,7 @@ module.exports = class NewBrowser extends Browser { const browserCapabilities = capabilities[capabilitySettings.capabilityName] ?? {}; capabilities[capabilitySettings.capabilityName] = { ...browserCapabilities, - args: [...(browserCapabilities.args ?? []), ...capabilitySettings.args], + args: [...(browserCapabilities.args ?? []), ...capabilitySettings.getArgs(headless)], }; return capabilities; } diff --git a/src/config/browser-options.js b/src/config/browser-options.js index ceec97242..9d215f944 100644 --- a/src/config/browser-options.js +++ b/src/config/browser-options.js @@ -313,7 +313,22 @@ function buildBrowserOptions(defaultFactory, extra) { user: options.optionalString("user"), key: options.optionalString("key"), region: options.optionalString("region"), - headless: options.optionalBoolean("headless"), + headless: option({ + defaultValue: defaultFactory("headless"), + validate: value => { + if (_.isNull(value) || _.isBoolean(value)) { + return; + } + + if (typeof value !== "string") { + throw new Error('"headless" option should be boolean or string with "new" or "old" values'); + } + + if (value !== "old" && value !== "new") { + throw new Error(`"headless" option should be "new" or "old", but got "${value}"`); + } + }, + }), isolation: option({ defaultValue: defaultFactory("isolation"), diff --git a/src/config/types.ts b/src/config/types.ts index 98cf39c12..8d939e70e 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -118,7 +118,7 @@ export interface CommonConfig { headers: Record | null; system: SystemConfig; - headless: boolean | null; + headless: "old" | "new" | boolean | null; isolation: boolean; openAndWaitOpts: { diff --git a/test/src/browser/new-browser.js b/test/src/browser/new-browser.js index 5f0237b16..353911bb1 100644 --- a/test/src/browser/new-browser.js +++ b/test/src/browser/new-browser.js @@ -60,17 +60,33 @@ describe("NewBrowser", () => { }); describe("headless setting", () => { - it("should generate browser specific settings - chrome", async () => { - await mkBrowser_({ - headless: true, - desiredCapabilities: { browserName: "chrome" }, - }).init(); + describe("chrome", () => { + it("should generate browser specific settings", async () => { + await mkBrowser_({ + headless: true, + desiredCapabilities: { browserName: "chrome" }, + }).init(); + + assert.calledWithMatch(webdriverio.remote, { + capabilities: { + browserName: "chrome", + "goog:chromeOptions": { args: ["headless", "disable-gpu"] }, + }, + }); + }); - assert.calledWithMatch(webdriverio.remote, { - capabilities: { - browserName: "chrome", - "goog:chromeOptions": { args: ["headless", "disable-gpu"] }, - }, + it("should add passed value to args if string was passed", async () => { + await mkBrowser_({ + headless: "new", + desiredCapabilities: { browserName: "chrome" }, + }).init(); + + assert.calledWithMatch(webdriverio.remote, { + capabilities: { + browserName: "chrome", + "goog:chromeOptions": { args: ["headless=new", "disable-gpu"] }, + }, + }); }); }); diff --git a/test/src/config/browser-options.js b/test/src/config/browser-options.js index 46d4f0792..a96fe32c5 100644 --- a/test/src/config/browser-options.js +++ b/test/src/config/browser-options.js @@ -1736,40 +1736,85 @@ describe("config browser-options", () => { }); }); - ["strictSSL", "headless"].forEach(option => { - describe(option, () => { - it(`should throw error if ${option} is not a null or boolean`, () => { - const readConfig = _.set({}, "browsers.b1", mkBrowser_({ [option]: "string" })); + describe("strictSSL", () => { + it(`should throw error if option is not a null or boolean`, () => { + const readConfig = _.set({}, "browsers.b1", mkBrowser_({ strictSSL: "string" })); - Config.read.returns(readConfig); + Config.read.returns(readConfig); - assert.throws(() => createConfig(), Error, `"${option}" must be a boolean`); - }); + assert.throws(() => createConfig(), Error, `"strictSSL" must be a boolean`); + }); - it("should set a default value if it is not set in config", () => { - const readConfig = _.set({}, "browsers.b1", mkBrowser_()); - Config.read.returns(readConfig); + it("should set a default value if it is not set in config", () => { + const readConfig = _.set({}, "browsers.b1", mkBrowser_()); + Config.read.returns(readConfig); - const config = createConfig(); + const config = createConfig(); - assert.equal(config[option], defaults[option]); - }); + assert.equal(config.strictSSL, defaults.strictSSL); + }); - it(`should override ${option} option`, () => { - const readConfig = { - [option]: false, - browsers: { - b1: mkBrowser_(), - b2: mkBrowser_({ [option]: true }), - }, - }; - Config.read.returns(readConfig); + it(`should override "strictSSL" option`, () => { + const readConfig = { + strictSSL: false, + browsers: { + b1: mkBrowser_(), + b2: mkBrowser_({ strictSSL: true }), + }, + }; + Config.read.returns(readConfig); - const config = createConfig(); + const config = createConfig(); - assert.isFalse(config.browsers.b1[option]); - assert.isTrue(config.browsers.b2[option]); - }); + assert.isFalse(config.browsers.b1.strictSSL); + assert.isTrue(config.browsers.b2.strictSSL); + }); + }); + + describe("headless", () => { + it("should throw error if option is not a null, boolean or string", () => { + const readConfig = _.set({}, "browsers.b1", mkBrowser_({ headless: { a: 1 } })); + + Config.read.returns(readConfig); + + assert.throws( + () => createConfig(), + Error, + '"headless" option should be boolean or string with "new" or "old" values', + ); + }); + + it("should throw error if option is string with invalid values", () => { + const readConfig = _.set({}, "browsers.b1", mkBrowser_({ headless: "some" })); + + Config.read.returns(readConfig); + + assert.throws(() => createConfig(), Error, '"headless" option should be "new" or "old", but got "some"'); + }); + + it("should set a default value if it is not set in config", () => { + const readConfig = _.set({}, "browsers.b1", mkBrowser_()); + Config.read.returns(readConfig); + + const config = createConfig(); + + assert.equal(config.headless, defaults.headless); + }); + + it("should override 'headless' option", () => { + const readConfig = { + headless: false, + browsers: { + b1: mkBrowser_(), + b2: mkBrowser_({ headless: true }), + }, + }; + Config.read.returns(readConfig); + + const config = createConfig(); + + assert.isFalse(config.browsers.b1.headless); + assert.isTrue(config.browsers.b2.headless); }); }); });