@eyeo/get-browser-binary 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gitlab-ci.yml CHANGED
@@ -27,42 +27,50 @@ test:basic:
27
27
  - npm run lint
28
28
  - npm test -- --grep "Utils"
29
29
 
30
- test:browsers:linux:
30
+ .linux:
31
31
  stage: test
32
32
  image: docker:24.0.5
33
33
  services:
34
34
  - docker:24.0.5-dind
35
35
  before_script:
36
36
  - docker build -f test/docker/Dockerfile -t browsers .
37
+
38
+ test:browsers:linux:
39
+ extends: .linux
37
40
  script:
38
- - docker run --shm-size=512m -t browsers
41
+ - docker run --shm-size=512m -t -e TEST_ARGS="--grep ^.*Browser((?!Version:.dev).)*\$" browsers
39
42
 
40
- test:browsers:windows:
43
+ test:browsers:linux:dev:
44
+ extends: .linux
45
+ script:
46
+ - docker run --shm-size=512m -t -e TEST_ARGS="--grep Version:.dev" browsers
47
+ allow_failure: true
48
+
49
+ .windows:
41
50
  stage: test
42
- variables:
43
- CI_PROJECT_ID_MAINSTREAM: 36688302
44
51
  before_script:
45
- - Invoke-WebRequest
46
- -Uri "${Env:CI_API_V4_URL}/projects/${Env:CI_PROJECT_ID_MAINSTREAM}/packages/generic/microsoft-edge/79.0.309/MicrosoftEdgeEnterpriseX64.msi"
47
- -Headers @{'JOB-TOKEN' = $Env:CI_JOB_TOKEN}
48
- -OutFile 'MicrosoftEdgeEnterpriseX64.msi'
49
- - Start-Process msiexec
50
- -ArgumentList "/i MicrosoftEdgeEnterpriseX64.msi /norestart /qn" -Wait
51
- - choco upgrade -y --no-progress nodejs --version 18.17.1
52
+ - choco install -y microsoft-edge
52
53
  - npm install
54
+ tags:
55
+ - saas-windows-medium-amd64
56
+ cache: {}
57
+
58
+ test:browsers:windows:
59
+ extends: .windows
53
60
  script:
54
61
  # Running Edge tests only on the preinstalled version
55
62
  # https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/29
56
63
  - npm test -- --grep "edge.*latest"
57
- - npm test -- --grep "chromium"
64
+ - npm test -- --grep "^.*chromium((?!Version:.dev).)*$"
58
65
  # Running only a subset of Firefox tests to avoid low OS resources error
59
66
  # https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/2
60
67
  - npm test -- --grep "firefox.*installs"
61
- tags:
62
- - shared-windows
63
- - windows
64
- - windows-1809
65
- cache: {}
68
+
69
+ test:browsers:windows:dev:
70
+ extends: .windows
71
+ script:
72
+ - npm test -- --grep "chromium.*Version:.dev"
73
+ allow_failure: true
66
74
 
67
75
  docs:
68
76
  stage: docs
package/README.md CHANGED
@@ -32,7 +32,7 @@ the right side.
32
32
 
33
33
  - Chromium >= 77 (Chromium ARM >= 92)
34
34
  - Firefox >= 68
35
- - Edge >= 95
35
+ - Edge >= 95 (Windows Edge >= 79)
36
36
 
37
37
  Note: Installing Edge is not supported on Windows. It is assumed to be installed
38
38
  because it is the default browser on that platform. On macOS, only the latest
@@ -43,6 +43,22 @@ Edge version is supported.
43
43
  Set the `VERBOSE` environment variable to `"true"` to get verbose logging on
44
44
  download requests.
45
45
 
46
+ ### Offline execution
47
+
48
+ It is possible to run browsers offline as long as they have been previously
49
+ installed. Example:
50
+
51
+ ```javascript
52
+ // Online
53
+ let {binary} = await BROWSERS[browser].installBrowser(version);
54
+ console.log(binary); // keep the browser binary location to use it offline
55
+ let driver = await BROWSERS[browser].getDriver(version); // let the driver binary download
56
+
57
+ // Offline
58
+ let customBrowserBinary = "<browser binary location>";
59
+ let driver = await BROWSERS[browser].getDriver(version, {customBrowserBinary});
60
+ ```
61
+
46
62
  ## Development
47
63
 
48
64
  ### Prerequisites
package/RELEASE_NOTES.md CHANGED
@@ -1,3 +1,39 @@
1
+ # Unreleased
2
+
3
+ # 0.17.0
4
+
5
+ - Stops calling `driver.close()` in `enableExtensionInIncognito()`
6
+ on non-Windows platforms (!114)
7
+ - Add instructions for offline execution (!110)
8
+ - Adds option to setup proxy through PAC file for Firefox (!115)
9
+
10
+ # 0.16.0
11
+
12
+ - Starts using selenium's automated driver management. That means additional
13
+ `chromedriver` and `msedgedriver` packages are no longer needed (#67)
14
+ - Loads the extension in Firefox by local path instead of being copied to a
15
+ temporary folder (!104)
16
+ - Checks if extensions can be loaded beforehand, by ensuring they contain a
17
+ `manifest.json` file. If not, a new `Extension manifest file not found` error
18
+ will be thrown (#71)
19
+ - Refactors headless runs by not using webdriver's deprecated headless() method
20
+ (#72)
21
+
22
+ ### Testing
23
+
24
+ - Fixes an issue with `npm test` which would fail when the `grep` option would
25
+ include parenthesis (#69)
26
+ - Adds the browser version information in the "runs" test (#74)
27
+
28
+ ### Notes for integrators
29
+
30
+ - Please make sure that your selenium-webdriver package version is at least
31
+ 4.15.0 (the one used in this release). Also make sure that you are not using the
32
+ `chromedriver` nor `msedgedriver` packages, otherwise the automated driver
33
+ management may not work as expected (#67)
34
+ - If you experience chromedriver issues, try deleting the `/browser-snapshots`
35
+ cache folder.
36
+
1
37
  # 0.15.0
2
38
 
3
39
  - Fixes the downloads of Chromium versions < 91 by replacing the remaining
package/index.js CHANGED
@@ -19,7 +19,7 @@ import {Chromium} from "./src/chromium.js";
19
19
  import {Firefox} from "./src/firefox.js";
20
20
  import {Edge} from "./src/edge.js";
21
21
 
22
- export {download, takeFullPageScreenshot, snapshotsBaseDir}
22
+ export {download, takeFullPageScreenshot, snapshotsBaseDir, getMajorVersion}
23
23
  from "./src/utils.js";
24
24
 
25
25
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eyeo/get-browser-binary",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Install browser binaries and matching webdrivers",
5
5
  "repository": {
6
6
  "type": "git",
package/src/browser.js CHANGED
@@ -84,17 +84,19 @@ export class Browser {
84
84
  /**
85
85
  * @typedef {Object} driverOptions
86
86
  * @property {boolean} [headless=true] Run the browser in headless mode,
87
- * or not. In Chromium >= 111, the
87
+ * or not. In Chromium >= 111 and Edge >= 114 the
88
88
  * {@link https://developer.chrome.com/articles/new-headless/ new headless mode}
89
89
  * is used.
90
- * @property {Array.<string>} [extensionPaths=[]] Loads extensions to the
91
- * browser.
90
+ * @property {Array.<string>} [extensionPaths=[]] Paths to folders containing
91
+ * unpacked extensions which will be loaded to the browser.
92
92
  * @property {boolean} [incognito=false] Runs the browser in incognito mode,
93
93
  * or not.
94
94
  * @property {boolean} [insecure=false] Forces the browser to accept insecure
95
95
  * certificates, or not.
96
96
  * @property {Array.<string>} [extraArgs=[]] Additional arguments to start
97
97
  * the browser with.
98
+ * @property {string} [proxy] Only for Firefox, path to network
99
+ * proxy autoconfig url (PAC file).
98
100
  * @property {string} [customBrowserBinary] Path to the browser binary to be
99
101
  * used, instead of the browser installed by installBrowser(). This option
100
102
  * overrides the version parameter in getDriver().
@@ -110,7 +112,8 @@ export class Browser {
110
112
  * browser install files to complete. When set to 0 there is no time limit.
111
113
  * @return {webdriver}
112
114
  * @throws {Error} Unsupported browser version, Unsupported platform, Browser
113
- * download failed, Driver download failed, Unable to start driver.
115
+ * download failed, Driver download failed, Unable to start driver, Manifest
116
+ * file not found.
114
117
  */
115
118
  static async getDriver(version, options = {}, downloadTimeout = 0) {
116
119
  // to be implemented by the subclass
package/src/chromium.js CHANGED
@@ -24,9 +24,8 @@ import chrome from "selenium-webdriver/chrome.js";
24
24
  import extractZip from "extract-zip";
25
25
 
26
26
  import {Browser} from "./browser.js";
27
- import {download, killDriverProcess, getMajorVersion, checkVersion,
28
- checkPlatform, errMsg, snapshotsBaseDir, platformArch}
29
- from "./utils.js";
27
+ import {download, getMajorVersion, checkVersion, checkPlatform, errMsg,
28
+ snapshotsBaseDir, platformArch, checkExtensionPaths} from "./utils.js";
30
29
 
31
30
  /**
32
31
  * Browser and webdriver functionality for Chromium.
@@ -116,8 +115,7 @@ export class Chromium extends Browser {
116
115
  // Linux example: "Chromium 112.0.5615.49 built on Debian 11.6"
117
116
  // Windows example: "114.0.5735.0"
118
117
  let versionNumber = installedVersion.split(" ")[1] || installedVersion;
119
- let base = await Chromium.#getBase(versionNumber);
120
- return {binary, versionNumber, base};
118
+ return {binary, versionNumber};
121
119
  }
122
120
 
123
121
  /**
@@ -134,14 +132,10 @@ export class Chromium extends Browser {
134
132
  static async installBrowser(version = "latest", downloadTimeout = 0) {
135
133
  const MIN_VERSION = process.arch == "arm64" ? 92 : 77;
136
134
 
137
- let binary;
138
- let versionNumber;
139
- let base;
140
-
141
135
  checkVersion(version, MIN_VERSION, Chromium.#CHANNELS);
142
- versionNumber = await Chromium.#getVersionForChannel(version);
136
+ let versionNumber = await Chromium.#getVersionForChannel(version);
143
137
 
144
- base = await Chromium.#getBase(versionNumber);
138
+ let base = await Chromium.#getBase(versionNumber);
145
139
  let startBase = base;
146
140
  let [platformDir, fileName] = {
147
141
  "win32-ia32": ["Win", "chrome-win.zip"],
@@ -154,6 +148,7 @@ export class Chromium extends Browser {
154
148
  let browserDir;
155
149
  let snapshotsDir = path.join(snapshotsBaseDir, "chromium");
156
150
 
151
+ let binary;
157
152
  while (true) {
158
153
  browserDir = path.join(snapshotsDir, `chromium-${platformArch}-${base}`);
159
154
  binary = Chromium.#getBinaryPath(browserDir);
@@ -192,54 +187,7 @@ export class Chromium extends Browser {
192
187
  }
193
188
  await extractZip(archive, {dir: browserDir});
194
189
 
195
- return {binary, versionNumber, base};
196
- }
197
-
198
- static async #installDriver(startBase) {
199
- let [dir, zip, driverBinary] = {
200
- "win32-ia32": ["Win", "chromedriver_win32.zip", "chromedriver.exe"],
201
- "win32-x64": ["Win_x64", "chromedriver_win32.zip", "chromedriver.exe"],
202
- "linux-x64": ["Linux_x64", "chromedriver_linux64.zip", "chromedriver"],
203
- "darwin-x64": ["Mac", "chromedriver_mac64.zip", "chromedriver"],
204
- "darwin-arm64": ["Mac_Arm", "chromedriver_mac64.zip", "chromedriver"]
205
- }[platformArch];
206
-
207
- let cacheDir = path.join(snapshotsBaseDir, "chromium", "cache",
208
- "chromedriver");
209
- let archive = path.join(cacheDir, `${startBase}-${zip}`);
210
- try {
211
- await fs.promises.access(archive);
212
- await extractZip(archive, {dir: cacheDir});
213
- }
214
- catch (e) { // zip file is either not cached or corrupted
215
- let base = startBase;
216
- while (true) {
217
- let url = `https://commondatastorage.googleapis.com/chromium-browser-snapshots/${dir}/${base}/${zip}`;
218
- try {
219
- await download(url, archive);
220
- break;
221
- }
222
- catch (err) {
223
- if (err.name == "HTTPError") {
224
- base--;
225
- archive = path.join(cacheDir, `${base}-${zip}`);
226
- if (base <= startBase - Chromium.#MAX_VERSION_DECREMENTS)
227
- throw new Error(`${errMsg.driverDownload}: Chromium base ${startBase}`);
228
- }
229
- else {
230
- throw new Error(`${errMsg.driverDownload}: ${url}\n${err}`);
231
- }
232
- }
233
- }
234
- await extractZip(archive, {dir: cacheDir});
235
- }
236
-
237
- await killDriverProcess("chromedriver");
238
- let driverPath = path.join(cacheDir, zip.split(".")[0], driverBinary);
239
- await fs.promises.rm(driverPath, {force: true});
240
- await extractZip(archive, {dir: cacheDir});
241
-
242
- return driverPath;
190
+ return {binary, versionNumber};
243
191
  }
244
192
 
245
193
  /** @see Browser.getDriver */
@@ -247,21 +195,24 @@ export class Chromium extends Browser {
247
195
  headless = true, extensionPaths = [], incognito = false, insecure = false,
248
196
  extraArgs = [], customBrowserBinary
249
197
  } = {}, downloadTimeout = 0) {
250
- let {binary, versionNumber, base} = customBrowserBinary ?
198
+ let {binary, versionNumber} = customBrowserBinary ?
251
199
  await Chromium.#getInstalledBrowserInfo(customBrowserBinary) :
252
200
  await Chromium.installBrowser(version, downloadTimeout);
253
- let driverPath = await Chromium.#installDriver(base);
254
- let serviceBuilder = new chrome.ServiceBuilder(driverPath);
201
+
255
202
  let options = new chrome.Options().addArguments("no-sandbox", ...extraArgs);
256
- if (extensionPaths.length > 0)
203
+ if (extensionPaths.length > 0) {
204
+ await checkExtensionPaths(extensionPaths);
257
205
  options.addArguments(`load-extension=${extensionPaths.join(",")}`);
206
+ }
258
207
  if (headless) {
259
- // New headless mode introduced in Chrome 111
260
- // https://developer.chrome.com/articles/new-headless/
261
- if (getMajorVersion(versionNumber) >= 111)
208
+ let majorVersion = getMajorVersion(versionNumber);
209
+ // https://www.selenium.dev/blog/2023/headless-is-going-away/
210
+ if (majorVersion >= 109)
262
211
  options.addArguments("headless=new");
212
+ else if (majorVersion >= 96)
213
+ options.addArguments("headless=chrome");
263
214
  else
264
- options.headless();
215
+ options.addArguments("headless");
265
216
  }
266
217
  if (insecure)
267
218
  options.addArguments("ignore-certificate-errors");
@@ -272,7 +223,6 @@ export class Chromium extends Browser {
272
223
  let builder = new webdriver.Builder();
273
224
  builder.forBrowser("chrome");
274
225
  builder.setChromeOptions(options);
275
- builder.setChromeService(serviceBuilder);
276
226
 
277
227
  return builder.build();
278
228
  }
@@ -315,8 +265,9 @@ export class Chromium extends Browser {
315
265
  setTimeout(() => resolve(enable()), 100);
316
266
  });
317
267
  }, extensionTitle, errMsg.extensionNotFound);
318
- if (version >= 115)
319
- // Closing the previously opened new window
268
+ if (version >= 115 && process.platform == "win32")
269
+ // Closing the previously opened new window. Needed on Windows to avoid a
270
+ // further `WebDriverError: disconnected` error
320
271
  await driver.close();
321
272
 
322
273
  await driver.switchTo().window(handle);
package/src/edge.js CHANGED
@@ -27,7 +27,7 @@ import extractZip from "extract-zip";
27
27
 
28
28
  import {Browser} from "./browser.js";
29
29
  import {download, killDriverProcess, wait, getMajorVersion, checkVersion,
30
- checkPlatform, errMsg, snapshotsBaseDir, platformArch}
30
+ checkPlatform, errMsg, snapshotsBaseDir, checkExtensionPaths}
31
31
  from "./utils.js";
32
32
 
33
33
  let {By} = webdriver;
@@ -93,7 +93,7 @@ export class Edge extends Browser {
93
93
  return `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`;
94
94
  case "linux":
95
95
  return channel == "stable" ?
96
- "microsoft-edge" : `microsoft-edge-${channel}`;
96
+ "/usr/bin/microsoft-edge" : `/usr/bin/microsoft-edge-${channel}`;
97
97
  case "darwin":
98
98
  return `${process.env.HOME}/Applications/${Edge.#darwinApp}.app/Contents/MacOS/${Edge.#darwinApp}`;
99
99
  default:
@@ -179,7 +179,7 @@ export class Edge extends Browser {
179
179
  return installedVersion.trim().replace(/.*\s/, "");
180
180
  }
181
181
 
182
- static async #installDriver(binary) {
182
+ static async #installOldWindowsDriver(binary) {
183
183
  async function extractEdgeZip(archive, cacheDir, driverPath) {
184
184
  await killDriverProcess("msedgedriver");
185
185
  await fs.promises.rm(driverPath, {force: true});
@@ -188,17 +188,12 @@ export class Edge extends Browser {
188
188
 
189
189
  let binaryPath = binary || Edge.#getBinaryPath();
190
190
  let versionNumber = await Edge.#getInstalledVersionNumber(binaryPath);
191
- let [zip, driverBinary] = {
192
- "win32-ia32": ["edgedriver_win32.zip", "msedgedriver.exe"],
193
- "win32-x64": ["edgedriver_win64.zip", "msedgedriver.exe"],
194
- "linux-x64": ["edgedriver_linux64.zip", "msedgedriver"],
195
- "darwin-x64": ["edgedriver_mac64.zip", "msedgedriver"],
196
- "darwin-arm64": ["edgedriver_mac64_m1.zip", "msedgedriver"]
197
- }[platformArch];
198
191
  let cacheDir = path.join(snapshotsBaseDir, "edge", "cache",
199
192
  `edgedriver-${versionNumber}`);
193
+ let zip = process.arch == "ia32" ?
194
+ "edgedriver_win32.zip" : "edgedriver_win64.zip";
200
195
  let archive = path.join(cacheDir, `${versionNumber}-${zip}`);
201
- let driverPath = path.join(cacheDir, driverBinary);
196
+ let driverPath = path.join(cacheDir, "msedgedriver.exe");
202
197
 
203
198
  try {
204
199
  await fs.promises.access(archive);
@@ -243,29 +238,36 @@ export class Edge extends Browser {
243
238
  await Edge.#getInstalledVersionNumber(binary);
244
239
  }
245
240
 
246
- let driverPath = await Edge.#installDriver(binary);
247
- let serviceBuilder = new edge.ServiceBuilder(driverPath);
248
-
249
241
  let options = new edge.Options().addArguments("no-sandbox", ...extraArgs);
250
242
  if (headless) {
251
243
  if (versionNumber && getMajorVersion(versionNumber) >= 114)
252
244
  options.addArguments("headless=new");
253
245
  else
254
- options.headless();
246
+ options.addArguments("headless");
255
247
  }
256
- if (extensionPaths.length > 0)
248
+ if (extensionPaths.length > 0) {
249
+ await checkExtensionPaths(extensionPaths);
257
250
  options.addArguments(`load-extension=${extensionPaths.join(",")}`);
251
+ }
258
252
  if (incognito)
259
253
  options.addArguments("inprivate");
260
254
  if (insecure)
261
255
  options.addArguments("ignore-certificate-errors");
262
256
  if (platform == "linux")
263
- options.setEdgeChromiumBinaryPath(`/usr/bin/${binary}`);
257
+ options.setEdgeChromiumBinaryPath(binary);
264
258
 
265
259
  let builder = new webdriver.Builder();
266
260
  builder.forBrowser("MicrosoftEdge");
267
261
  builder.setEdgeOptions(options);
268
- builder.setEdgeService(serviceBuilder);
262
+
263
+ if (getMajorVersion(versionNumber) < 95) {
264
+ // Selenium's automated driver download doesn't work on old Edge versions.
265
+ // Support to installing old drivers is only needed by Windows CI jobs.
266
+ // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/2#note_1189235804
267
+ let driverPath = await Edge.#installOldWindowsDriver(binary);
268
+ let serviceBuilder = new edge.ServiceBuilder(driverPath);
269
+ builder.setEdgeService(serviceBuilder);
270
+ }
269
271
 
270
272
  let driver;
271
273
  // On Windows CI, occasionally a SessionNotCreatedError is thrown, likely
package/src/firefox.js CHANGED
@@ -23,11 +23,12 @@ import fs from "fs";
23
23
  import got from "got";
24
24
  import webdriver from "selenium-webdriver";
25
25
  import firefox from "selenium-webdriver/firefox.js";
26
+ import {Command} from "selenium-webdriver/lib/command.js";
26
27
 
27
28
  import {Browser} from "./browser.js";
28
29
  import {download, extractTar, extractDmg, killDriverProcess, wait,
29
30
  getMajorVersion, checkVersion, checkPlatform, errMsg, snapshotsBaseDir,
30
- platformArch} from "./utils.js";
31
+ platformArch, checkExtensionPaths} from "./utils.js";
31
32
 
32
33
  let {until, By} = webdriver;
33
34
 
@@ -139,9 +140,14 @@ export class Firefox extends Browser {
139
140
  }
140
141
 
141
142
  /** @see Browser.getDriver */
143
+ /** In Firefox you can define url to proxy autoconfig file.
144
+ * Proxy Autoconfig file determine whether web requests should go through
145
+ * a proxy server or connect directly. This allow to use localhost mapped
146
+ * to any url (f.ex testpages.adblockplus.org)
147
+ */
142
148
  static async getDriver(version = "latest", {
143
149
  headless = true, extensionPaths = [], incognito = false, insecure = false,
144
- extraArgs = [], customBrowserBinary
150
+ extraArgs = [], customBrowserBinary, proxy
145
151
  } = {}, downloadTimeout = 0) {
146
152
  let binary;
147
153
  let versionNumber;
@@ -152,7 +158,7 @@ export class Firefox extends Browser {
152
158
 
153
159
  let options = new firefox.Options();
154
160
  if (headless)
155
- options.headless();
161
+ options.addArguments("--headless");
156
162
  if (incognito)
157
163
  options.addArguments("--private");
158
164
  if (insecure)
@@ -162,6 +168,10 @@ export class Firefox extends Browser {
162
168
  // Enabled by default on Firefox > 68
163
169
  if (versionNumber && getMajorVersion(versionNumber) == 68)
164
170
  options.setPreference("dom.promise_rejection_events.enabled", true);
171
+ if (proxy) {
172
+ options.setPreference("network.proxy.type", 2);
173
+ options.setPreference("network.proxy.autoconfig_url", proxy);
174
+ }
165
175
 
166
176
  options.setBinary(customBrowserBinary || binary);
167
177
 
@@ -184,8 +194,18 @@ export class Firefox extends Browser {
184
194
  }, 30000, `${errMsg.driverStart}: geckodriver`, 1000);
185
195
 
186
196
  for (let extensionPath of extensionPaths) {
187
- let temporary = true; // Parameter not documented on the webdriver docs
188
- await driver.installAddon(extensionPath, temporary);
197
+ await checkExtensionPaths([extensionPath]);
198
+
199
+ // This is based on Selenium's Firefox `installAddon` function. Rather
200
+ // than setting the "addon" parameter, which is the actual extension
201
+ // base64 encoded, we need to set the "path" which is the filepath to the
202
+ // extension. This allows downstream to test upgrade paths by updating the
203
+ // extension source code and reloading it.
204
+ // See https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/node/selenium-webdriver/firefox.js
205
+ await driver.execute(
206
+ new Command("install addon")
207
+ .setParameter("path", extensionPath)
208
+ .setParameter("temporary", true));
189
209
  }
190
210
  return driver;
191
211
  }
package/src/utils.js CHANGED
@@ -34,7 +34,8 @@ export const errMsg = {
34
34
  browserDownload: "Browser download failed",
35
35
  browserNotInstalled: "Browser is not installed",
36
36
  browserVersionCheck: "Checking the browser version failed",
37
- elemNotFound: "HTML element not found"
37
+ elemNotFound: "HTML element not found",
38
+ manifestNotFound: "Extension manifest file not found"
38
39
  };
39
40
  export const platformArch = `${process.platform}-${process.arch}`;
40
41
 
@@ -238,6 +239,12 @@ export async function takeFullPageScreenshot(driver, hideScrollbars = true) {
238
239
  return fullScreenshot;
239
240
  }
240
241
 
242
+ /**
243
+ * Returns the major version of a browser version number.
244
+ * @param {string} versionNumber Full browser version number.
245
+ * @return {number} Major version number.
246
+ * @throws {Error} Unsupported browser version.
247
+ */
241
248
  export function getMajorVersion(versionNumber) {
242
249
  let majorVersion = parseInt(versionNumber && versionNumber.split(".")[0], 10);
243
250
  if (isNaN(majorVersion))
@@ -258,3 +265,14 @@ export function checkPlatform() {
258
265
  if (!["win32", "linux", "darwin"].includes(process.platform))
259
266
  throw new Error(`${errMsg.unsupportedPlatform}: ${process.platform}`);
260
267
  }
268
+
269
+ export async function checkExtensionPaths(extensionPaths) {
270
+ for (let extensionPath of extensionPaths) {
271
+ try {
272
+ await fs.promises.access(path.join(extensionPath, "manifest.json"));
273
+ }
274
+ catch (err) {
275
+ throw new Error(`${errMsg.manifestNotFound}: ${extensionPath}`);
276
+ }
277
+ }
278
+ }
package/test/browsers.js CHANGED
@@ -25,14 +25,13 @@ import {BROWSERS, snapshotsBaseDir, takeFullPageScreenshot} from "../index.js";
25
25
  import {killDriverProcess} from "../src/utils.js";
26
26
  import {TEST_SERVER_URL} from "./test-server.js";
27
27
 
28
- import "geckodriver"; // Required to set the driver path on Windows
29
-
30
28
  const VERSIONS = {
31
29
  chromium: ["latest", "77.0.3865.0", "beta", "dev"],
32
30
  firefox: ["latest", "68.0", "beta"],
33
31
  edge: ["latest", "95.0.1020.40", "beta", "dev"]
34
32
  };
35
33
  const TEST_URL_BASIC = `${TEST_SERVER_URL}/basic.html`;
34
+ const PROXY_URL_BASIC = "http://testpages.adblockplus.org/basic.html";
36
35
  const TEST_URL_LONG = `${TEST_SERVER_URL}/long.html`;
37
36
 
38
37
  async function switchToHandle(driver, testFn) {
@@ -106,33 +105,22 @@ function getExtension(browser, version) {
106
105
  return {extensionPaths, manifest};
107
106
  }
108
107
 
109
- async function getCachedTimes(browser) {
110
- let cacheDir = path.join(snapshotsBaseDir, browser, "cache");
111
- let cacheFiles = await fs.promises.readdir(cacheDir);
112
-
113
- let browserCacheTime = null;
108
+ async function getInstallFileCTime(browser) {
114
109
  // Browsers installed at OS level are not tested
115
- if (browser != "edge") {
116
- let installTypes = [".zip", ".dmg", ".bz2"];
117
- let browserZip =
118
- cacheFiles.find(elem => installTypes.some(type => elem.includes(type)));
119
- let browserStat = await fs.promises.stat(path.join(cacheDir, browserZip));
120
- browserCacheTime = browserStat.ctimeMs;
121
- }
110
+ if (browser == "edge" && (process.platform == "win32" ||
111
+ process.platform == "darwin"))
112
+ return null;
122
113
 
123
- let driverCacheTime = null;
124
- // geckodriver is installed by npm, that's why Firefox is not tested here
125
- if (browser != "firefox") {
126
- let driverDir = cacheFiles.find(
127
- elem => !elem.endsWith(".zip") && !elem.endsWith(".pkg"));
128
- let driverFiles = await fs.promises.readdir(path.join(cacheDir, driverDir));
129
- let driverZip = driverFiles.find(elem => elem.endsWith(".zip"));
130
- let driverStat =
131
- await fs.promises.stat(path.join(cacheDir, driverDir, driverZip));
132
- driverCacheTime = driverStat.ctimeMs;
133
- }
114
+ const installTypes = [".zip", ".dmg", ".bz2", ".deb", ".exe"];
115
+
116
+ let cacheDir = path.join(snapshotsBaseDir, browser, "cache");
117
+ let cacheFiles = await fs.promises.readdir(cacheDir);
118
+ let browserZip =
119
+ cacheFiles.find(elem => installTypes.some(type => elem.includes(type)));
120
+ if (!browserZip)
121
+ throw new Error(`Files in ${cacheDir} don't belong to any known install file types: ${installTypes}`);
134
122
 
135
- return {browserCTime: browserCacheTime, driverCTime: driverCacheTime};
123
+ return (await fs.promises.stat(path.join(cacheDir, browserZip))).ctimeMs;
136
124
  }
137
125
 
138
126
  const browserNames = {
@@ -150,6 +138,19 @@ async function basicUrlTest(driver, browser) {
150
138
  expect(text).toEqual("Test server basic page");
151
139
  }
152
140
 
141
+ // Adding the browser version to the test title for logging purposes
142
+ function addVersionToTitle(ctx, version) {
143
+ ctx.test.title = `${ctx.test.title} [v${version}]`;
144
+ }
145
+
146
+ function isOldHeadlessMode(browser, version) {
147
+ // Chromium's old headless mode doesn't support loading extensions
148
+ return browser != "firefox" && (
149
+ !["latest", "beta", "dev"].includes(version) ||
150
+ // Edge on the Windows CI is v79, that is old headless
151
+ (browser == "edge" && process.platform == "win32"));
152
+ }
153
+
153
154
  for (let browser of Object.keys(BROWSERS)) {
154
155
  describe(`Browser: ${browser}`, () => {
155
156
  before(async() => {
@@ -162,7 +163,7 @@ for (let browser of Object.keys(BROWSERS)) {
162
163
  for (let version of VERSIONS[browser]) {
163
164
  describe(`Version: ${version}`, () => {
164
165
  let driver = null;
165
- let emptyCacheTimes = null;
166
+ let firstInstallFileCTime = null;
166
167
  let customBrowserBinary = null;
167
168
  let failedInstall = false;
168
169
 
@@ -177,6 +178,12 @@ for (let browser of Object.keys(BROWSERS)) {
177
178
  await killDriverProcess("chromedriver");
178
179
  else if (browser == "firefox")
179
180
  await killDriverProcess("geckodriver");
181
+
182
+ // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/77
183
+ if (process.platform == "win32" && browser == "chromium")
184
+ await killDriverProcess("chrome");
185
+ else if (process.platform == "win32" && browser == "edge")
186
+ await killDriverProcess("msedge");
180
187
  }
181
188
 
182
189
  beforeEach(function() {
@@ -208,31 +215,32 @@ for (let browser of Object.keys(BROWSERS)) {
208
215
  expect(installedVersion).toEqual(
209
216
  expect.stringContaining(normalize(versionNumber)));
210
217
 
211
- // Adding the version number to the test title for logging purposes
212
- this.test.title = `${this.test.title} [v${versionNumber}]`;
218
+ addVersionToTitle(this, versionNumber);
219
+
220
+ // Data used in further tests
213
221
  customBrowserBinary = binary;
222
+ firstInstallFileCTime = await getInstallFileCTime(browser);
214
223
  });
215
224
 
216
- it("runs", async() => {
225
+ it("runs", async function() {
217
226
  driver = await BROWSERS[browser].getDriver(version);
218
227
  await basicUrlTest(driver, browser);
219
228
 
220
- // When running all tests, this saves time on the cache test
221
- emptyCacheTimes = await getCachedTimes(browser);
229
+ let browserVersion =
230
+ (await driver.getCapabilities()).getBrowserVersion();
231
+ addVersionToTitle(this, browserVersion);
222
232
  });
223
233
 
224
- it("uses cached install files", async() => {
225
- if (emptyCacheTimes == null) { // Single test case run
226
- driver = await BROWSERS[browser].getDriver(version);
227
- emptyCacheTimes = await getCachedTimes(browser);
228
- await quitDriver();
229
- }
234
+ // This test depends on running the "installs" test
235
+ it("uses cached install files", async function() {
236
+ if (!firstInstallFileCTime)
237
+ this.skip();
230
238
 
231
239
  // assigning `driver` to allow the afterEach hook quit the driver
232
240
  driver = await BROWSERS[browser].getDriver(version);
233
- let existingCacheTimes = await getCachedTimes(browser);
241
+ let secondInstallFileCTime = await getInstallFileCTime(browser);
234
242
 
235
- expect(existingCacheTimes).toEqual(emptyCacheTimes);
243
+ expect(secondInstallFileCTime).toEqual(firstInstallFileCTime);
236
244
  });
237
245
 
238
246
  // This test depends on running the "installs" test
@@ -266,6 +274,22 @@ for (let browser of Object.keys(BROWSERS)) {
266
274
  expect(sizeSmall).toMeasureLessThan(sizeDefault);
267
275
  });
268
276
 
277
+ it("supports proxy", async function() {
278
+ if (browser != "firefox")
279
+ this.skip();
280
+
281
+ let headless = true;
282
+ let proxy = "http://localhost:3000/proxy-config.pac";
283
+ let extraArgs = [];
284
+ driver = await BROWSERS[browser].getDriver(
285
+ version, {headless, extraArgs, proxy});
286
+ await driver.navigate().to(PROXY_URL_BASIC);
287
+
288
+ let text = await driver.findElement(By.id("basic")).getText();
289
+ expect(text).toEqual("Test server basic page");
290
+ await quitDriver();
291
+ });
292
+
269
293
  it("takes a full page screenshot", async() => {
270
294
  driver = await BROWSERS[browser].getDriver(version);
271
295
  await driver.navigate().to(TEST_URL_LONG);
@@ -279,10 +303,13 @@ for (let browser of Object.keys(BROWSERS)) {
279
303
  expect(fullImg.bitmap.height).toBeGreaterThan(partImg.bitmap.height);
280
304
  });
281
305
 
306
+ // The only reason the geckodriver package is required in package.json
307
+ // is to install a specific version needed by Firefox 68.0, otherwise it
308
+ // fails to load extensions. When the oldest Firefox version is bumped,
309
+ // Selenium's automated driver download should be able to manage it.
310
+ // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/44
282
311
  it("loads an extension", async() => {
283
- // Chromium's old headless mode doesn't support loading extensions
284
- let headless = browser == "firefox" || (browser == "chromium" &&
285
- ["latest", "beta", "dev"].includes(version));
312
+ let headless = !isOldHeadlessMode(browser, version);
286
313
  let {extensionPaths} = getExtension(browser, version);
287
314
 
288
315
  driver = await BROWSERS[browser].getDriver(
@@ -302,6 +329,29 @@ for (let browser of Object.keys(BROWSERS)) {
302
329
  );
303
330
  await getHandle(driver, "/index.html");
304
331
  });
332
+
333
+ it("updates an extension", async() => {
334
+ let headless = !isOldHeadlessMode(browser, version);
335
+ let {extensionPaths, manifest} = getExtension(browser, version);
336
+
337
+ let tmpExtensionDir = path.join(snapshotsBaseDir, "extension");
338
+ await fs.promises.rm(tmpExtensionDir, {recursive: true, force: true});
339
+ await fs.promises.cp(extensionPaths[0], tmpExtensionDir,
340
+ {recursive: true});
341
+
342
+ driver = await BROWSERS[browser].getDriver(
343
+ version, {headless, extensionPaths: [tmpExtensionDir]});
344
+ await getHandle(driver, "/index.html");
345
+ let text = await driver.findElement(By.id("title")).getText();
346
+ expect(text).toEqual(`Browser test extension - ${manifest}`);
347
+
348
+ // The page is modified and reloaded to emulate an extension update
349
+ await fs.promises.cp(path.join(tmpExtensionDir, "update.html"),
350
+ path.join(tmpExtensionDir, "index.html"));
351
+ await driver.navigate().refresh();
352
+ text = await driver.findElement(By.id("title")).getText();
353
+ expect(text).toEqual(`Updated test extension - ${manifest}`);
354
+ });
305
355
  });
306
356
  }
307
357
 
@@ -314,5 +364,12 @@ for (let browser of Object.keys(BROWSERS)) {
314
364
  .rejects.toThrow(`Unsupported browser version: ${unsupported}`);
315
365
  }
316
366
  });
367
+
368
+ it("does not load an invalid extension", async() => {
369
+ let extensionPaths = [process.cwd()];
370
+
371
+ await expect(BROWSERS[browser].getDriver("latest", {extensionPaths}))
372
+ .rejects.toThrow(`Extension manifest file not found: ${extensionPaths[0]}`);
373
+ });
317
374
  });
318
375
  }
@@ -1 +1 @@
1
- <h1>Browser test extension - mv2</h1>
1
+ <h1 id="title">Browser test extension - mv2</h1>
@@ -0,0 +1 @@
1
+ <h1 id="title">Updated test extension - mv2</h1>
@@ -1 +1 @@
1
- <h1>Browser test extension - mv3</h1>
1
+ <h1 id="title">Browser test extension - mv3</h1>
@@ -0,0 +1 @@
1
+ <h1 id="title">Updated test extension - mv3</h1>
@@ -0,0 +1,7 @@
1
+
2
+ function FindProxyForURL(url, host) {
3
+ if (host === "testpages.adblockplus.org")
4
+ return "PROXY http://localhost:3000";
5
+
6
+ return "DIRECT";
7
+ }
package/test/runner.js CHANGED
@@ -53,10 +53,12 @@ export async function killTestServer() {
53
53
  }
54
54
 
55
55
  async function run() {
56
- let args = process.argv.join(" ").split("-- ")[1];
56
+ let args = process.argv.slice(2); // Keep npm args from "--" onwards
57
+ args = args.map(v => v.startsWith("-") ? v : `"${v}"`);
58
+
57
59
  await runTestServer();
58
60
  try {
59
- execSync(`npm run test-suite -- ${args}`, {stdio: "inherit"});
61
+ execSync(`npm run test-suite ${args.join(" ")}`, {stdio: "inherit"});
60
62
  }
61
63
  finally {
62
64
  await killTestServer();