@eyeo/get-browser-binary 0.6.0 → 0.7.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
@@ -55,10 +55,11 @@ test:browsers:windows:
55
55
  script:
56
56
  # Running only a subset of Firefox tests to avoid low OS resources error
57
57
  # https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/2
58
- - npm test -- --grep "firefox.*latest.*downloads"
58
+ - npm test -- --grep "firefox.*installs"
59
59
  # Running npm v8 on powershell has issues when the grep value contains the
60
60
  # pipe (|) literal. Storing that string as a verbatim string (single quotes)
61
61
  # and then sorrounding it with four double quotes does the trick.
62
+ # https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/29
62
63
  - $full_tests = '(chromium|edge.*latest)'
63
64
  - npm test -- --grep """"$full_tests""""
64
65
  tags:
package/.mocharc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "timeout": 50000
3
+ }
package/README.md CHANGED
@@ -1,19 +1,19 @@
1
1
  # get-browser-binary
2
2
 
3
- Download specific browser versions for Chromium, Firefox and Edge, and their
3
+ Install specific browser versions for Chromium, Firefox and Edge, and their
4
4
  matching [selenium webdriver](https://www.selenium.dev/selenium/docs/api/javascript/index.html).
5
5
 
6
6
  ## Getting started
7
7
 
8
- The sample below shows how to download the latest Chromium and run it using
8
+ The sample below shows how to install the latest Chromium and run it using
9
9
  selenium webdriver:
10
10
 
11
11
  ```javascript
12
12
  import {BROWSERS} from "@eyeo/get-browser-binary";
13
13
 
14
14
  (async function example() {
15
- let {binary} = await BROWSERS.chromium.downloadBinary("latest");
16
- console.log(`Chromium binary downloaded to ${binary}`);
15
+ let {binary} = await BROWSERS.chromium.installBrowser("latest");
16
+ console.log(`Chromium executable location: ${binary}`);
17
17
 
18
18
  let driver = await BROWSERS.chromium.getDriver("latest");
19
19
  await driver.navigate().to("https://example.com/");
@@ -34,7 +34,7 @@ the right side.
34
34
  - Firefox >= 60
35
35
  - Edge >= 95
36
36
 
37
- Note: Edge download is not supported on Windows. It is assumed to be installed
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.
39
39
 
40
40
  ## Development
@@ -52,7 +52,7 @@ npm install
52
52
 
53
53
  ### Folders to ignore / cache
54
54
 
55
- All browser and webdriver files will be downloaded to the `./browser-snapshots`
55
+ All browser and webdriver files will be extracted to the `./browser-snapshots`
56
56
  folder, which probably makes sense to be ignored (for instance, by adding it to
57
57
  `.gitignore`).
58
58
 
@@ -75,6 +75,13 @@ The `grep` option filters the tests to run with a regular expression. Example:
75
75
  npm test -- --grep "chromium.*latest"
76
76
  ```
77
77
 
78
+ The `timeout` option overrides the timeout defined by `.mocharc.json`.
79
+ Increasing the timeout may be useful on slow connection environments:
80
+
81
+ ```shell
82
+ npm test -- --timeout <ms>
83
+ ```
84
+
78
85
  ### Running tests on Docker
79
86
 
80
87
  Useful to reproduce the CI environment of the `test:browsers:linux` job:
@@ -84,10 +91,19 @@ docker build -f test/docker/Dockerfile -t browsers .
84
91
  docker run --shm-size=256m -it browsers
85
92
  ```
86
93
 
87
- The `grep` option can also be used on Docker via the `TEST_ARGS` parameter:
94
+ The `grep` and `timeout` options can also be used on Docker via the `TEST_ARGS`
95
+ parameter:
96
+
97
+ ```shell
98
+ docker run --shm-size=256m -e TEST_ARGS="--grep chromium.*latest --timeout 100000" -it browsers
99
+ ```
100
+
101
+ By default, tests delete the `./browser-snapshots` before each `Browser` suite
102
+ runs. To change that behavior you may set the `TEST_KEEP_SNAPSHOTS` environment
103
+ variable to `true`. Example:
88
104
 
89
105
  ```shell
90
- docker run --shm-size=256m -e TEST_ARGS="--grep chromium.*latest" -it browsers
106
+ TEST_KEEP_SNAPSHOTS=true npm test
91
107
  ```
92
108
 
93
109
  ## Building the documentation
package/RELEASE_NOTES.md CHANGED
@@ -1,3 +1,22 @@
1
+ # 0.7.0
2
+
3
+ - Implement takeFullPageScreenshot (#32)
4
+ - Unify error messages (#22)
5
+ - Document when a browser binary is downloaded or installed (#30)
6
+ - Use cached driver binary if it already exists (#20)
7
+ - Remove chromedriver dependency (#33)
8
+ - Fallback mechanism on minor browser versions (Edge) (#27)
9
+
10
+ ### Testing
11
+
12
+ - Use a manifest V3 extension on latest chromium-based browsers tests (#31)
13
+ - Allow test run to not delete browser snapshots (#28)
14
+ - Add a timeout option to run the tests (#24)
15
+
16
+ ### Notes for integrators
17
+
18
+ - The `downloadBinary` function has been renamed to `installBrowser` (!38).
19
+
1
20
  # 0.6.0
2
21
 
3
22
  - Removes Opera support (#26)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eyeo/get-browser-binary",
3
- "version": "0.6.0",
4
- "description": "Download browser binaries and matching webdrivers",
3
+ "version": "0.7.0",
4
+ "description": "Install browser binaries and matching webdrivers",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://gitlab.com/eyeo/developer-experience/get-browser-binary"
@@ -15,12 +15,12 @@
15
15
  "type": "module",
16
16
  "main": "index.js",
17
17
  "dependencies": {
18
- "chromedriver": "^90.0.1",
19
18
  "dmg": "^0.1.0",
20
19
  "extract-zip": "^2.0.1",
21
20
  "geckodriver": "^3.0.2",
22
21
  "got": "^11.8.2",
23
- "selenium-webdriver": "^4.6.1"
22
+ "jimp": "^0.16.2",
23
+ "selenium-webdriver": "^4.7.1"
24
24
  },
25
25
  "devDependencies": {
26
26
  "eslint": "^8.17.0",
package/src/browsers.js CHANGED
@@ -27,31 +27,47 @@ import firefox from "selenium-webdriver/firefox.js";
27
27
  import edge from "selenium-webdriver/edge.js";
28
28
  import command from "selenium-webdriver/lib/command.js";
29
29
  import extractZip from "extract-zip";
30
+ import Jimp from "jimp";
30
31
 
31
- import {download, extractTar, extractDmg, getBrowserVersion, killDriverProcess,
32
- wait} from "./utils.js";
32
+ import {download, extractTar, extractDmg, killDriverProcess, wait}
33
+ from "./utils.js";
33
34
 
34
35
  /**
35
- * Root folder where browser and webdriver binaries get downloaded.
36
+ * Root folder where browser and webdriver files get downloaded and extracted.
36
37
  * @type {string}
37
38
  */
38
39
  export let snapshotsBaseDir = path.join(process.cwd(), "browser-snapshots");
39
40
 
40
41
  let {until, By} = webdriver;
41
- let platform = `${process.platform}-${process.arch}`;
42
-
43
- function checkVersion(version, minVersion, channels) {
42
+ let {platform, arch} = process;
43
+ let platformArch = `${platform}-${arch}`;
44
+
45
+ const UNSUPPORTED_VERSION_ERROR = "Unsupported browser version";
46
+ const UNSUPPORTED_PLATFORM_ERROR = "Unsupported platform";
47
+ const DRIVER_DOWNLOAD_ERROR = "Driver download failed";
48
+ const DRIVER_START_ERROR = "Unable to start driver";
49
+ const EXTENSION_NOT_FOUND_ERROR = "Extension not found";
50
+ const BROWSER_DOWNLOAD_ERROR = "Browser download failed";
51
+ const BROWSER_NOT_INSTALLED_ERROR = "Browser is not installed";
52
+ const ELEMENT_NOT_FOUND_ERROR = "HTML element not found";
53
+
54
+ function checkVersion(version, minVersion, channels = []) {
44
55
  if (channels.includes(version))
45
56
  return;
46
57
 
47
58
  let mainVersion = parseInt(version && version.split(".")[0], 10);
48
59
  if (isNaN(mainVersion) || mainVersion < minVersion)
49
- throw new Error(`Unsupported browser version: ${version}`);
60
+ throw new Error(`${UNSUPPORTED_VERSION_ERROR}: ${version}`);
61
+ }
62
+
63
+ function checkPlatform() {
64
+ if (!["win32", "linux", "darwin"].includes(platform))
65
+ throw new Error(`${UNSUPPORTED_PLATFORM_ERROR}: ${platform}`);
50
66
  }
51
67
 
52
68
  /**
53
- * Base class for browser download functionality. Please see subclasses for
54
- * browser specific details.
69
+ * Base class for browser and webdriver functionality. Please see subclasses for
70
+ * browser specific details. All classes can be used statically.
55
71
  * @hideconstructor
56
72
  */
57
73
  class Browser {
@@ -63,13 +79,15 @@ class Browser {
63
79
  */
64
80
 
65
81
  /**
66
- * Downloads the browser binary file.
82
+ * Installs the browser. The installation process is detailed on the
83
+ * subclasses.
67
84
  * @param {string} version - Either full version number or channel/release.
68
85
  * Please find examples on the subclasses.
69
86
  * @return {BrowserBinary}
70
- * @throws {Error} Unsupported browser version.
87
+ * @throws {Error} Unsupported browser version, Unsupported platform, Browser
88
+ * download failed.
71
89
  */
72
- static async downloadBinary(version) {
90
+ static async installBrowser(version) {
73
91
  // to be implemented by the subclass
74
92
  }
75
93
 
@@ -80,7 +98,7 @@ class Browser {
80
98
  */
81
99
  static async getInstalledVersion(binary) {
82
100
  let stdout;
83
- if (process.platform == "win32") {
101
+ if (platform == "win32") {
84
102
  ({stdout} = await promisify(exec)(
85
103
  `(Get-ItemProperty ${binary}).VersionInfo.ProductVersion`,
86
104
  {shell: "powershell.exe"})
@@ -118,7 +136,8 @@ class Browser {
118
136
  * Please find examples on the subclasses.
119
137
  * @param {driverOptions?} options - Options to start the browser with.
120
138
  * @return {webdriver}
121
- * @throws {Error} Unsupported browser version.
139
+ * @throws {Error} Unsupported browser version, Unsupported platform, Browser
140
+ * download failed, Driver download failed, Unable to start driver.
122
141
  */
123
142
  static async getDriver(version, options = {}) {
124
143
  // to be implemented by the subclass
@@ -130,7 +149,8 @@ class Browser {
130
149
  * @param {webdriver} driver - The driver controlling the browser.
131
150
  * @param {string} extensionTitle - Title of the extebsion to be enabled.
132
151
  * @return {webdriver}
133
- * @throws {Error} Unsupported browser version.
152
+ * @throws {Error} Unsupported browser version, Extension not found, HTML
153
+ * element not found.
134
154
  */
135
155
  static async enableExtensionInIncognito(driver, extensionTitle) {
136
156
  // Allowing the extension in incognito mode can't happen programmatically:
@@ -138,22 +158,80 @@ class Browser {
138
158
  // https://bugzilla.mozilla.org/show_bug.cgi?id=1729315
139
159
  // That is done through the UI, to be implemented by the subclass
140
160
  }
161
+
162
+ /**
163
+ * @typedef {Object} Jimp
164
+ * @see https://github.com/oliver-moran/jimp/tree/master/packages/jimp
165
+ */
166
+
167
+ /**
168
+ * Takes a screenshot of the full page by scrolling from top to bottom.
169
+ * @param {webdriver} driver - The driver controlling the browser.
170
+ * @property {boolean} hideScrollbars=true - Hides any scrollbars before
171
+ * taking the screenshot, or not.
172
+ * @return {Jimp} A Jimp image object containing the screenshot.
173
+ * @example
174
+ * // Getting a base-64 encoded PNG from the returned screenshot
175
+ * let image = await Browser.takeFullPageScreenshot(driver);
176
+ * let encodedPNG = await image.getBase64Async("image/png");
177
+ */
178
+ static async takeFullPageScreenshot(driver, hideScrollbars = true) {
179
+ // On macOS scrollbars appear and disappear overlapping the content as
180
+ // scrolling occurs. Hiding the scrollbars helps getting reproducible
181
+ // screenshots.
182
+ if (hideScrollbars) {
183
+ await driver.executeScript(() => {
184
+ if (!document.head)
185
+ return;
186
+
187
+ let style = document.createElement("style");
188
+ style.textContent = "html { overflow-y: scroll; }";
189
+ document.head.appendChild(style);
190
+ if (document.documentElement.clientWidth == window.innerWidth)
191
+ style.textContent = "html::-webkit-scrollbar { display: none; }";
192
+ else
193
+ document.head.removeChild(style);
194
+ });
195
+ }
196
+
197
+ let fullScreenshot = new Jimp(0, 0);
198
+ while (true) {
199
+ let [width, height, offset] = await driver.executeScript((...args) => {
200
+ window.scrollTo(0, args[0]);
201
+ // Math.ceil rounds up potential decimal values on window.scrollY,
202
+ // ensuring the loop will not hang due to never reaching enough
203
+ // fullScreenshot's height.
204
+ return [document.documentElement.clientWidth,
205
+ document.documentElement.scrollHeight,
206
+ Math.ceil(window.scrollY)];
207
+ }, fullScreenshot.bitmap.height);
208
+ let data = await driver.takeScreenshot();
209
+ let partialScreenshot = await Jimp.read(Buffer.from(data, "base64"));
210
+ let combinedScreenshot =
211
+ new Jimp(width, offset + partialScreenshot.bitmap.height);
212
+ combinedScreenshot.composite(fullScreenshot, 0, 0);
213
+ combinedScreenshot.composite(partialScreenshot, 0, offset);
214
+ fullScreenshot = combinedScreenshot;
215
+
216
+ if (fullScreenshot.bitmap.height >= height)
217
+ break;
218
+ }
219
+ return fullScreenshot;
220
+ }
141
221
  }
142
222
 
143
223
  /**
144
- * Download functionality for Chromium. This class can be used statically.
224
+ * Browser and webdriver functionality for Chromium.
145
225
  * @hideconstructor
146
226
  * @extends Browser
147
227
  */
148
228
  class Chromium extends Browser {
149
- static #DRIVER = "chromedriver";
150
-
151
- static async #getBranchBasePosition(version) {
152
- let data = await got(`https://omahaproxy.appspot.com/deps.json?version=${version}`).json();
153
- return data.chromium_base_position;
154
- }
229
+ static #CHANNELS = ["latest", "beta", "dev"];
155
230
 
156
231
  static async #getVersionForChannel(channel) {
232
+ if (!Chromium.#CHANNELS.includes(channel))
233
+ return channel;
234
+
157
235
  if (channel == "latest")
158
236
  channel = "stable";
159
237
 
@@ -163,7 +241,7 @@ class Chromium extends Browser {
163
241
  "linux-x64": "linux",
164
242
  "darwin-x64": "mac",
165
243
  "dawrin-arm64": "mac_arm64"
166
- }[platform];
244
+ }[platformArch];
167
245
  let data = await got(`https://omahaproxy.appspot.com/all.json?os=${os}`).json();
168
246
  let release = data[0].versions.find(ver => ver.channel == channel);
169
247
  let {current_version: version} = release;
@@ -177,55 +255,67 @@ class Chromium extends Browser {
177
255
  }
178
256
 
179
257
  static #getBinaryPath(dir) {
180
- switch (process.platform) {
181
- case "win32":
182
- return path.join(dir, "chrome-win", "chrome.exe");
183
- case "linux":
184
- return path.join(dir, "chrome-linux", "chrome");
185
- case "darwin":
186
- return path.join(dir, "chrome-mac", "Chromium.app", "Contents", "MacOS",
187
- "Chromium");
188
- default:
189
- throw new Error(`Unexpected platform: ${process.platform}`);
190
- }
258
+ checkPlatform();
259
+ return {
260
+ win32: path.join(dir, "chrome-win", "chrome.exe"),
261
+ linux: path.join(dir, "chrome-linux", "chrome"),
262
+ darwin: path.join(dir, "chrome-mac", "Chromium.app", "Contents", "MacOS",
263
+ "Chromium")
264
+ }[platform];
191
265
  }
192
266
 
193
- static async #downloadChromium(chromiumRevision) {
194
- const MAX_VERSION_DECREMENTS = 50;
267
+ /**
268
+ * Installs the browser. The Chromium executable gets extracted in the
269
+ * {@link snapshotsBaseDir} folder, ready to go.
270
+ * @param {string} version - Either "latest", "beta", "dev" or a full version
271
+ * number (i.e. "77.0.3865.0"). Defaults to "latest".
272
+ * @return {BrowserBinary}
273
+ * @throws {Error} Unsupported browser version, Unsupported platform, Browser
274
+ * download failed.
275
+ */
276
+ static async installBrowser(version = "latest") {
277
+ const MIN_VERSION = 75;
278
+ const MAX_VERSION_DECREMENTS = 80;
279
+
280
+ checkVersion(version, MIN_VERSION, Chromium.#CHANNELS);
281
+ let versionNumber = await Chromium.#getVersionForChannel(version);
195
282
 
196
- let revision = parseInt(chromiumRevision, 10);
197
- let startingRevision = revision;
283
+ let {chromium_base_position: chromiumBase} =
284
+ await got(`https://omahaproxy.appspot.com/deps.json?version=${versionNumber}`).json();
285
+ let base = parseInt(chromiumBase, 10);
286
+ let startBase = base;
198
287
  let [platformDir, fileName] = {
199
288
  "win32-ia32": ["Win", "chrome-win.zip"],
200
289
  "win32-x64": ["Win_x64", "chrome-win.zip"],
201
290
  "linux-x64": ["Linux_x64", "chrome-linux.zip"],
202
291
  "darwin-x64": ["Mac", "chrome-mac.zip"],
203
292
  "dawrin-arm64": ["Mac_Arm", "chrome-mac.zip"]
204
- }[platform];
205
- let archive = null;
206
- let browserDir = null;
293
+ }[platformArch];
294
+ let archive;
295
+ let browserDir;
207
296
  let snapshotsDir = path.join(snapshotsBaseDir, "chromium");
297
+ let binary;
208
298
 
209
299
  while (true) {
210
- browserDir = path.join(snapshotsDir, `chromium-${platform}-${revision}`);
300
+ browserDir = path.join(snapshotsDir, `chromium-${platformArch}-${base}`);
301
+ binary = Chromium.#getBinaryPath(browserDir);
211
302
 
212
303
  try {
213
304
  await fs.promises.access(browserDir);
214
- return {binary: Chromium.#getBinaryPath(browserDir), revision};
305
+ return {binary, versionNumber, base};
215
306
  }
216
307
  catch (e) {}
217
308
 
218
309
  await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
219
310
 
220
- archive = path.join(snapshotsDir, "cache", `${revision}-${fileName}`);
221
-
311
+ archive = path.join(snapshotsDir, "cache", `${base}-${fileName}`);
222
312
  try {
223
313
  try {
224
314
  await fs.promises.access(archive);
225
315
  }
226
316
  catch (e) {
227
317
  await download(
228
- `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${platformDir}%2F${revision}%2F${fileName}?alt=media`,
318
+ `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${platformDir}%2F${base}%2F${fileName}?alt=media`,
229
319
  archive);
230
320
  }
231
321
  break;
@@ -233,58 +323,53 @@ class Chromium extends Browser {
233
323
  catch (e) {
234
324
  // Chromium advises decrementing the branch_base_position when no
235
325
  // matching build was found. See https://www.chromium.org/getting-involved/download-chromium
236
- revision--;
237
- if (revision <= startingRevision - MAX_VERSION_DECREMENTS)
238
- throw new Error(`No Chromium package found for ${startingRevision}`);
326
+ base--;
327
+ if (base <= startBase - MAX_VERSION_DECREMENTS)
328
+ throw new Error(`${BROWSER_DOWNLOAD_ERROR}: Chromium base ${startBase}`);
239
329
  }
240
330
  }
241
-
242
331
  await extractZip(archive, {dir: browserDir});
243
- return {binary: Chromium.#getBinaryPath(browserDir), revision};
244
- }
245
332
 
246
- /**
247
- * Downloads the browser binary file.
248
- * @param {string} version - Either "latest", "beta", "dev" or a full version
249
- * number (i.e. "77.0.3865.0"). Defaults to "latest".
250
- * @return {BrowserBinary}
251
- */
252
- static async downloadBinary(version = "latest") {
253
- const MIN_VERSION = 75;
254
- const CHANNELS = ["latest", "beta", "dev"];
255
-
256
- checkVersion(version, MIN_VERSION, CHANNELS);
257
-
258
- let versionNumber = CHANNELS.includes(version) ?
259
- await Chromium.#getVersionForChannel(version) : version;
260
-
261
- let base = await Chromium.#getBranchBasePosition(versionNumber);
262
-
263
- let {binary, revision} = await Chromium.#downloadChromium(base);
264
- return {binary, versionNumber, revision};
333
+ return {binary, versionNumber, base};
265
334
  }
266
335
 
267
- static async #installDriver(revision, version) {
268
- let [dir, zip, driver] = {
336
+ static async #installDriver(base, versionNumber) {
337
+ let [dir, zip, driverBinary] = {
269
338
  "win32-ia32": ["Win", "chromedriver_win32.zip", "chromedriver.exe"],
270
339
  "win32-x64": ["Win_x64", "chromedriver_win32.zip", "chromedriver.exe"],
271
340
  "linux-x64": ["Linux_x64", "chromedriver_linux64.zip", "chromedriver"],
272
341
  "darwin-x64": ["Mac", "chromedriver_mac64.zip", "chromedriver"],
273
342
  "darwin-arm64": ["Mac_Arm", "chromedriver_mac64.zip", "chromedriver"]
274
- }[platform];
343
+ }[platformArch];
275
344
 
276
- let cacheDir = path.join(snapshotsBaseDir, "chromium", "cache", version);
277
- let destinationDir = path.join(process.cwd(), "node_modules",
278
- Chromium.#DRIVER, "lib", Chromium.#DRIVER);
279
- let archive = path.join(cacheDir, `${revision}-${zip}`);
345
+ let cacheDir = path.join(snapshotsBaseDir, "chromium", "cache",
346
+ versionNumber);
347
+ let archive = path.join(cacheDir, `${base}-${zip}`);
280
348
 
281
- await download(`https://commondatastorage.googleapis.com/chromium-browser-snapshots/${dir}/${revision}/${zip}`,
282
- archive);
349
+ try {
350
+ await fs.promises.access(archive);
351
+ await extractZip(archive, {dir: cacheDir});
352
+ }
353
+ catch (e) { // zip file is either not cached or corrupted
354
+ let url = `https://commondatastorage.googleapis.com/chromium-browser-snapshots/${dir}/${base}/${zip}`;
355
+ try {
356
+ await download(url, archive);
357
+ }
358
+ catch (err) {
359
+ throw new Error(`${DRIVER_DOWNLOAD_ERROR}: ${url}\n${err}`);
360
+ }
361
+ await extractZip(archive, {dir: cacheDir});
362
+ }
363
+
364
+ await killDriverProcess("chromedriver");
365
+ let driverPath = path.join(cacheDir, zip.split(".")[0], driverBinary);
366
+ try {
367
+ await fs.promises.rm(driverPath, {recursive: true});
368
+ }
369
+ catch (e) {} // file does not exist
283
370
  await extractZip(archive, {dir: cacheDir});
284
- await killDriverProcess(Chromium.#DRIVER);
285
- await fs.promises.mkdir(destinationDir, {recursive: true});
286
- await fs.promises.copyFile(path.join(cacheDir, zip.split(".")[0], driver),
287
- path.join(destinationDir, driver));
371
+
372
+ return driverPath;
288
373
  }
289
374
 
290
375
  /** @see Browser.getDriver */
@@ -292,10 +377,9 @@ class Chromium extends Browser {
292
377
  headless = true, extensionPaths = [], incognito = false, insecure = false,
293
378
  extraArgs = []
294
379
  } = {}) {
295
- let {binary, revision, versionNumber} =
296
- await Chromium.downloadBinary(version);
297
- await Chromium.#installDriver(revision, versionNumber);
298
-
380
+ let {binary, versionNumber, base} = await Chromium.installBrowser(version);
381
+ let driverPath = await Chromium.#installDriver(base, versionNumber);
382
+ let serviceBuilder = new chrome.ServiceBuilder(driverPath);
299
383
  let options = new chrome.Options().addArguments("no-sandbox", ...extraArgs);
300
384
  if (extensionPaths.length > 0)
301
385
  options.addArguments(`load-extension=${extensionPaths.join(",")}`);
@@ -310,6 +394,7 @@ class Chromium extends Browser {
310
394
  let builder = new webdriver.Builder();
311
395
  builder.forBrowser("chrome");
312
396
  builder.setChromeOptions(options);
397
+ builder.setChromeService(serviceBuilder);
313
398
 
314
399
  return builder.build();
315
400
  }
@@ -334,106 +419,106 @@ class Chromium extends Browser {
334
419
  continue;
335
420
 
336
421
  extensionDetails = shadowRoot.getElementById("detailsButton");
422
+ break;
337
423
  }
338
-
339
424
  if (!extensionDetails)
340
- reject("Extension was not found");
425
+ reject(`${args[1]}: ${args[0]}`);
341
426
 
342
427
  extensionDetails.click();
343
428
  setTimeout(() => resolve(enable()), 100);
344
429
  });
345
- }, extensionTitle);
430
+ }, extensionTitle, EXTENSION_NOT_FOUND_ERROR);
346
431
  }
347
432
  }
348
433
 
349
434
  /**
350
- * Download functionality for Firefox. This class can be used statically.
435
+ * Browser and webdriver functionality for Firefox.
351
436
  * @hideconstructor
352
437
  * @extends Browser
353
438
  */
354
439
  class Firefox extends Browser {
355
- static async #getVersionForChannel(branch) {
440
+ static #CHANNELS = ["latest", "beta"];
441
+
442
+ static async #getVersionForChannel(channel) {
443
+ if (!Firefox.#CHANNELS.includes(channel))
444
+ return channel;
445
+
356
446
  let data = await got("https://product-details.mozilla.org/1.0/firefox_versions.json").json();
357
- return branch == "beta" ?
447
+ return channel == "beta" ?
358
448
  data.LATEST_FIREFOX_DEVEL_VERSION : data.LATEST_FIREFOX_VERSION;
359
449
  }
360
450
 
361
451
  static #getBinaryPath(dir) {
362
- switch (process.platform) {
363
- case "win32":
364
- return path.join(dir, "core", "firefox.exe");
365
- case "linux":
366
- return path.join(dir, "firefox", "firefox");
367
- case "darwin":
368
- return path.join(dir, "Firefox.app", "Contents", "MacOS", "firefox");
369
- default:
370
- throw new Error(`Unexpected platform: ${process.platform}`);
371
- }
452
+ checkPlatform();
453
+ return {
454
+ win32: path.join(dir, "core", "firefox.exe"),
455
+ linux: path.join(dir, "firefox", "firefox"),
456
+ darwin: path.join(dir, "Firefox.app", "Contents", "MacOS", "firefox")
457
+ }[platform];
372
458
  }
373
459
 
374
460
  static #extractFirefoxArchive(archive, dir) {
375
- switch (process.platform) {
461
+ switch (platform) {
376
462
  case "win32":
377
- // Procedure inspired from mozinstall:
378
- // https://hg.mozilla.org/mozilla-central/file/tip/testing/mozbase/mozinstall/mozinstall/mozinstall.py
379
463
  return promisify(exec)(`"${archive}" /extractdir=${dir}`);
380
464
  case "linux":
381
465
  return extractTar(archive, dir);
382
466
  case "darwin":
383
467
  return extractDmg(archive, dir);
384
468
  default:
385
- throw new Error(`Unexpected platform: ${process.platform}`);
469
+ checkPlatform();
386
470
  }
387
471
  }
388
472
 
389
- static async #downloadFirefox(version) {
473
+ /**
474
+ * Installs the browser. The Firefox executable gets extracted in the
475
+ * {@link snapshotsBaseDir} folder, ready to go.
476
+ * @param {string} version - Either "latest", "beta" or a full version
477
+ * number (i.e. "68.0"). Defaults to "latest".
478
+ * @return {BrowserBinary}
479
+ * @throws {Error} Unsupported browser version, Unsupported platform, Browser
480
+ * download failed.
481
+ */
482
+ static async installBrowser(version = "latest") {
483
+ const MIN_VERSION = 60;
484
+
485
+ checkVersion(version, MIN_VERSION, Firefox.#CHANNELS);
486
+ let versionNumber = await Firefox.#getVersionForChannel(version);
487
+
390
488
  let [buildPlatform, fileName] = {
391
- "win32-ia32": ["win32", `Firefox Setup ${version}.exe`],
392
- "win32-x64": ["win64", `Firefox Setup ${version}.exe`],
393
- "linux-x64": ["linux-x86_64", `firefox-${version}.tar.bz2`],
394
- "darwin-x64": ["mac", `Firefox ${version}.dmg`],
395
- "darwin-arm64": ["mac", `Firefox ${version}.dmg`]
396
- }[platform];
489
+ "win32-ia32": ["win32", `Firefox Setup ${versionNumber}.exe`],
490
+ "win32-x64": ["win64", `Firefox Setup ${versionNumber}.exe`],
491
+ "linux-x64": ["linux-x86_64", `firefox-${versionNumber}.tar.bz2`],
492
+ "darwin-x64": ["mac", `Firefox ${versionNumber}.dmg`],
493
+ "darwin-arm64": ["mac", `Firefox ${versionNumber}.dmg`]
494
+ }[platformArch];
397
495
 
398
496
  let snapshotsDir = path.join(snapshotsBaseDir, "firefox");
399
- let browserDir = path.join(snapshotsDir, `firefox-${platform}-${version}`);
400
- let archive = path.join(snapshotsDir, "cache", fileName);
401
-
497
+ let browserDir = path.join(snapshotsDir,
498
+ `firefox-${platformArch}-${versionNumber}`);
499
+ let binary = Firefox.#getBinaryPath(browserDir);
402
500
  try {
403
501
  await fs.promises.access(browserDir);
404
- return Firefox.#getBinaryPath(browserDir);
502
+ return {binary, versionNumber};
405
503
  }
406
504
  catch (e) {}
407
505
 
506
+ let archive = path.join(snapshotsDir, "cache", fileName);
408
507
  await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
409
508
  try {
410
509
  await fs.promises.access(archive);
411
510
  }
412
511
  catch (e) {
413
- let url = `https://archive.mozilla.org/pub/firefox/releases/${version}/${buildPlatform}/en-US/${fileName}`;
414
- await download(url, archive);
512
+ let url = `https://archive.mozilla.org/pub/firefox/releases/${versionNumber}/${buildPlatform}/en-US/${fileName}`;
513
+ try {
514
+ await download(url, archive);
515
+ }
516
+ catch (err) {
517
+ throw new Error(`${BROWSER_DOWNLOAD_ERROR}: ${url}\n${err}`);
518
+ }
415
519
  }
416
-
417
520
  await Firefox.#extractFirefoxArchive(archive, browserDir);
418
- return Firefox.#getBinaryPath(browserDir);
419
- }
420
-
421
- /**
422
- * Downloads the browser binary file
423
- * @param {string} version - Either "latest", "beta" or a full version
424
- * number (i.e. "68.0"). Defaults to "latest".
425
- * @return {BrowserBinary}
426
- */
427
- static async downloadBinary(version = "latest") {
428
- const MIN_VERSION = 60;
429
- const CHANNELS = ["latest", "beta"];
430
-
431
- checkVersion(version, MIN_VERSION, CHANNELS);
432
-
433
- let versionNumber = CHANNELS.includes(version) ?
434
- await Firefox.#getVersionForChannel(version) : version;
435
521
 
436
- let binary = await Firefox.#downloadFirefox(versionNumber);
437
522
  return {binary, versionNumber};
438
523
  }
439
524
 
@@ -442,7 +527,7 @@ class Firefox extends Browser {
442
527
  headless = true, extensionPaths = [], incognito = false, insecure = false,
443
528
  extraArgs = []
444
529
  } = {}) {
445
- let {binary} = await Firefox.downloadBinary(version);
530
+ let {binary} = await Firefox.installBrowser(version);
446
531
 
447
532
  let options = new firefox.Options();
448
533
  if (headless)
@@ -471,7 +556,7 @@ class Firefox extends Browser {
471
556
  throw err;
472
557
  await killDriverProcess("geckodriver");
473
558
  }
474
- }, 30000, "geckodriver didn't start, likely due to low OS resources", 1000);
559
+ }, 30000, `${DRIVER_START_ERROR}: geckodriver`, 1000);
475
560
 
476
561
  for (let extensionPath of extensionPaths) {
477
562
  await driver.execute(
@@ -485,10 +570,9 @@ class Firefox extends Browser {
485
570
 
486
571
  /** @see Browser.enableExtensionInIncognito */
487
572
  static async enableExtensionInIncognito(driver, extensionTitle) {
488
- let version = await getBrowserVersion(driver);
489
- if (version < 87)
490
- // The UI workaround assumes web elements only present on Firefox >= 87
491
- throw new Error(`Only supported on Firefox >= 87. Current version: ${version}`);
573
+ let version = (await driver.getCapabilities()).getBrowserVersion();
574
+ // The UI workaround assumes web elements only present on Firefox >= 87
575
+ checkVersion(version, 87);
492
576
 
493
577
  await driver.navigate().to("about:addons");
494
578
  await driver.wait(until.elementLocated(By.name("extension")), 1000).click();
@@ -501,33 +585,50 @@ class Firefox extends Browser {
501
585
  await elem.click();
502
586
  return await driver.findElement(By.name("private-browsing")).click();
503
587
  }
504
- throw new Error(`Extension "${extensionTitle}" not found`);
588
+ throw new Error(`${EXTENSION_NOT_FOUND_ERROR}: ${extensionTitle}`);
505
589
  }
506
590
  }
507
591
 
508
592
  /**
509
- * Download functionality for Edge. This class can be used statically.
593
+ * Browser and webdriver functionality for Edge.
510
594
  * @hideconstructor
511
595
  * @extends Browser
512
596
  */
513
597
  class Edge extends Browser {
514
- static #DRIVER = "msedgedriver";
598
+ static #CHANNELS = ["latest", "beta", "dev"];
515
599
 
516
600
  static async #getVersionForChannel(version) {
517
- if (!["latest", "beta", "dev"].includes(version))
518
- return {versionNumber: version, channel: "stable"};
601
+ let channel = "stable";
602
+ if (Edge.#CHANNELS.includes(version) && version != "latest")
603
+ channel = version;
519
604
 
520
- let channel = version == "latest" ? "stable" : version;
521
605
  let {body} = await got(`https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/`);
522
- let regex = /href="microsoft-edge-(stable|beta|dev)_(.*?)-1_/gm;
523
- let matches;
524
- let versionNumbers = [];
525
- while ((matches = regex.exec(body)) !== null)
526
- versionNumbers.push(matches[2]);
527
-
528
- let compareVersions = (v1, v2) =>
529
- parseInt(v1.split(".")[0], 10) < parseInt(v2.split(".")[0], 10) ? 1 : -1;
530
- let versionNumber = versionNumbers.sort(compareVersions)[0];
606
+ let versionNumber;
607
+ if (Edge.#CHANNELS.includes(version)) {
608
+ let regex = /href="microsoft-edge-(stable|beta|dev)_(.*?)-1_/gm;
609
+ let matches;
610
+ let versionNumbers = [];
611
+ while ((matches = regex.exec(body)) !== null)
612
+ versionNumbers.push(matches[2]);
613
+
614
+ let compareVersions = (v1, v2) =>
615
+ parseInt(v1.split(".")[0], 10) < parseInt(v2.split(".")[0], 10) ?
616
+ 1 : -1;
617
+ versionNumber = versionNumbers.sort(compareVersions)[0];
618
+ }
619
+ else {
620
+ let split = version.split(".");
621
+ let minorVersion = split.length == 4 ? parseInt(split.pop(), 10) : -1;
622
+ let majorVersion = split.join(".");
623
+ let found;
624
+ while (!found && minorVersion >= 0) {
625
+ versionNumber = `${majorVersion}.${minorVersion}`;
626
+ found = body.includes(versionNumber);
627
+ minorVersion--;
628
+ }
629
+ if (!found)
630
+ throw new Error(`${UNSUPPORTED_VERSION_ERROR}: ${version}`);
631
+ }
531
632
 
532
633
  return {versionNumber, channel};
533
634
  }
@@ -539,7 +640,7 @@ class Edge extends Browser {
539
640
  }
540
641
 
541
642
  static #getBinaryPath(channel = "stable") {
542
- switch (process.platform) {
643
+ switch (platform) {
543
644
  case "win32":
544
645
  let programFiles = process.env["ProgramFiles(x86)"] ?
545
646
  "${Env:ProgramFiles(x86)}" : "${Env:ProgramFiles}";
@@ -551,26 +652,28 @@ class Edge extends Browser {
551
652
  let appName = Edge.#getDarwinAppName(channel);
552
653
  return `${process.env.HOME}/Applications/${appName}.app/Contents/MacOS/${appName}`;
553
654
  default:
554
- throw new Error(`Unexpected platform: ${process.platform}`);
655
+ checkPlatform();
555
656
  }
556
657
  }
557
658
 
558
659
  /**
559
- * Downloads the browser binary file.
660
+ * Installs the browser. On Linux, Edge is installed as a system package,
661
+ * which requires root permissions. On MacOS, Edge is installed as a user
662
+ * app (not as a system app). Installing Edge on Windows is not supported.
560
663
  * @param {string} version - Either "latest", "beta", "dev" or a full version
561
- * number (i.e. "95.0.1020.53"). Defaults to "latest". This is only
562
- * available on Linux.
664
+ * number (i.e. "95.0.1020.40"). Defaults to "latest".
563
665
  * @return {BrowserBinary}
666
+ * @throws {Error} Unsupported browser version, Unsupported platform, Browser
667
+ * download failed.
564
668
  */
565
- static async downloadBinary(version = "latest") {
566
- if (process.platform == "win32")
669
+ static async installBrowser(version = "latest") {
670
+ if (platform == "win32")
567
671
  // Edge is mandatory on Windows, can't be uninstalled or downgraded
568
672
  // https://support.microsoft.com/en-us/microsoft-edge/why-can-t-i-uninstall-microsoft-edge-ee150b3b-7d7a-9984-6d83-eb36683d526d
569
- throw new Error("Edge download is not supported in Windows");
673
+ throw new Error(`${UNSUPPORTED_PLATFORM_ERROR}: ${platform}`);
570
674
 
571
675
  const MIN_VERSION = 95;
572
- const CHANNELS = ["latest", "beta", "dev"];
573
- checkVersion(version, MIN_VERSION, CHANNELS);
676
+ checkVersion(version, MIN_VERSION, Edge.#CHANNELS);
574
677
  let {versionNumber, channel} = await Edge.#getVersionForChannel(version);
575
678
 
576
679
  let darwinName = {
@@ -581,14 +684,14 @@ class Edge extends Browser {
581
684
  let filename = {
582
685
  linux: `microsoft-edge-${channel}_${versionNumber}-1_amd64.deb`,
583
686
  darwin: `${darwinName}-${versionNumber}.pkg`
584
- }[process.platform];
687
+ }[platform];
585
688
  let darwinArch = process.arch == "arm64" ?
586
689
  "03adf619-38c6-4249-95ff-4a01c0ffc962" :
587
690
  "C1297A47-86C4-4C1F-97FA-950631F94777";
588
- let downloadUrl = {
691
+ let url = {
589
692
  linux: `https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/${filename}`,
590
693
  darwin: `https://officecdnmac.microsoft.com/pr/${darwinArch}/MacAutoupdate/${filename}`
591
- }[process.platform];
694
+ }[platform];
592
695
 
593
696
  let snapshotsDir = path.join(snapshotsBaseDir, "edge");
594
697
  let archive = path.join(snapshotsDir, "cache", filename);
@@ -600,16 +703,16 @@ class Edge extends Browser {
600
703
  catch (e) {}
601
704
 
602
705
  try {
603
- await download(downloadUrl, archive);
706
+ await download(url, archive);
604
707
  }
605
708
  catch (err) {
606
- throw new Error(`Edge download failed at ${downloadUrl}\n${err}`);
709
+ throw new Error(`${BROWSER_DOWNLOAD_ERROR}: ${url}\n${err}`);
607
710
  }
608
711
 
609
- if (process.platform == "linux") {
712
+ if (platform == "linux") {
610
713
  await promisify(exec)(`dpkg -i ${archive}`);
611
714
  }
612
- else if (process.platform == "darwin") {
715
+ else if (platform == "darwin") {
613
716
  let appName = Edge.#getDarwinAppName(channel);
614
717
  try {
615
718
  await fs.promises.rm(`${process.env.HOME}/Applications/${appName}.app`, {recursive: true});
@@ -629,47 +732,55 @@ class Edge extends Browser {
629
732
  }
630
733
 
631
734
  static async #installDriver() {
735
+ async function extractEdgeZip(archive, cacheDir, driverPath) {
736
+ await killDriverProcess("msedgedriver");
737
+ try {
738
+ await fs.promises.rm(driverPath, {recursive: true});
739
+ }
740
+ catch (e) {} // file does not exist
741
+ await extractZip(archive, {dir: cacheDir});
742
+ }
743
+
632
744
  let binary = Edge.#getBinaryPath();
633
745
  let versionNumber = await Edge.#getInstalledVersionNumber(binary);
634
746
  if (!versionNumber)
635
- throw new Error("Edge is not installed");
747
+ throw new Error(`${BROWSER_NOT_INSTALLED_ERROR}: Edge`);
636
748
 
637
- let [zip, driver] = {
749
+ let [zip, driverBinary] = {
638
750
  "win32-ia32": ["edgedriver_win32.zip", "msedgedriver.exe"],
639
751
  "win32-x64": ["edgedriver_win64.zip", "msedgedriver.exe"],
640
752
  "linux-x64": ["edgedriver_linux64.zip", "msedgedriver"],
641
753
  "darwin-x64": ["edgedriver_mac64.zip", "msedgedriver"],
642
754
  "darwin-arm64": ["edgedriver_arm64.zip", "msedgedriver"]
643
- }[platform];
755
+ }[platformArch];
644
756
  let cacheDir = path.join(snapshotsBaseDir, "edge", "cache",
645
757
  `edgedriver-${versionNumber}`);
646
758
  let archive = path.join(cacheDir, `${versionNumber}-${zip}`);
759
+ let driverPath = path.join(cacheDir, driverBinary);
647
760
 
648
- let vSplit = versionNumber.split(".");
649
- let lastNum = parseInt(vSplit[3], 10);
650
- while (lastNum >= 0) {
651
- try {
652
- let attempt = `${vSplit[0]}.${vSplit[1]}.${vSplit[2]}.${lastNum}`;
653
- await download(`https://msedgedriver.azureedge.net/${attempt}/${zip}`,
654
- archive);
655
- break;
656
- }
657
- catch (e) {
658
- lastNum--;
659
- }
660
- }
661
-
662
- if (lastNum < 0)
663
- throw new Error(`msedgedriver was not found for Edge ${versionNumber}`);
664
-
665
- await killDriverProcess(Edge.#DRIVER);
666
- let driverPath = path.join(cacheDir, driver);
667
761
  try {
668
- await fs.promises.rm(driverPath, {recursive: true});
762
+ await fs.promises.access(archive);
763
+ await extractEdgeZip(archive, cacheDir, driverPath);
669
764
  }
670
- catch (e) {} // file does not exist
671
- await extractZip(archive, {dir: cacheDir});
765
+ catch (e) { // zip file is either not cached or corrupted
766
+ let vSplit = versionNumber.split(".");
767
+ let lastNum = parseInt(vSplit[3], 10);
768
+ while (lastNum >= 0) {
769
+ try {
770
+ let attempt = `${vSplit[0]}.${vSplit[1]}.${vSplit[2]}.${lastNum}`;
771
+ await download(`https://msedgedriver.azureedge.net/${attempt}/${zip}`,
772
+ archive);
773
+ break;
774
+ }
775
+ catch (e2) {
776
+ lastNum--;
777
+ }
778
+ }
779
+ if (lastNum < 0)
780
+ throw new Error(`${DRIVER_DOWNLOAD_ERROR}: Edge ${versionNumber}`);
672
781
 
782
+ await extractEdgeZip(archive, cacheDir, driverPath);
783
+ }
673
784
  return driverPath;
674
785
  }
675
786
 
@@ -678,8 +789,8 @@ class Edge extends Browser {
678
789
  headless = true, extensionPaths = [], incognito = false, insecure = false,
679
790
  extraArgs = []
680
791
  } = {}) {
681
- if (process.platform == "linux" || process.platform == "darwin")
682
- await Edge.downloadBinary(version);
792
+ if (platform == "linux" || platform == "darwin")
793
+ await Edge.installBrowser(version);
683
794
 
684
795
  let driverPath = await Edge.#installDriver();
685
796
  let serviceBuilder = new edge.ServiceBuilder(driverPath);
@@ -718,17 +829,19 @@ class Edge extends Browser {
718
829
  await button.click();
719
830
  return await driver.findElement(By.id("itemAllowIncognito")).click();
720
831
  }
721
- throw new Error("Details button not found");
832
+ throw new Error(`${ELEMENT_NOT_FOUND_ERROR}: Details button`);
722
833
  }
723
- throw new Error(`Extension "${extensionTitle}" not found`);
834
+ throw new Error(`${EXTENSION_NOT_FOUND_ERROR}: ${extensionTitle}`);
724
835
  }
725
836
  }
726
837
 
727
838
  /**
728
839
  * @type {Object}
729
- * @property {Chromium} chromium - Download functionality for Chromium.
730
- * @property {Firefox} firefox - Download functionality for Firefox.
731
- * @property {Edge} edge - Download functionality for Edge.
840
+ * @property {Chromium} chromium - Browser and webdriver functionality for
841
+ * Chromium.
842
+ * @property {Firefox} firefox - Browser and webdriver functionality for
843
+ * Firefox.
844
+ * @property {Edge} edge - Browser and webdriver functionality for Edge.
732
845
  */
733
846
  export const BROWSERS = {
734
847
  chromium: Chromium,
package/src/utils.js CHANGED
@@ -77,11 +77,6 @@ export async function extractDmg(archive, dir) {
77
77
  }
78
78
  }
79
79
 
80
- export async function getBrowserVersion(driver) {
81
- let version = (await driver.getCapabilities()).getBrowserVersion();
82
- return version ? parseInt(version.split(".")[0], 10) : null;
83
- }
84
-
85
80
  // Useful to unlock the driver file before replacing it or executing it
86
81
  export async function killDriverProcess(driverName) {
87
82
  let cmd = `kill $(pgrep ${driverName})`;
package/test/browsers.js CHANGED
@@ -21,10 +21,9 @@ import path from "path";
21
21
 
22
22
  import {BROWSERS, snapshotsBaseDir} from "../index.js";
23
23
  import {killDriverProcess} from "../src/utils.js";
24
+ import Jimp from "jimp";
24
25
 
25
- // Required to set the driver path on Windows
26
- import "chromedriver";
27
- import "geckodriver";
26
+ import "geckodriver"; // Required to set the driver path on Windows
28
27
 
29
28
  const VERSIONS = {
30
29
  chromium: ["latest", "75.0.3770.0", "beta", "dev"],
@@ -32,7 +31,7 @@ const VERSIONS = {
32
31
  edge: ["latest", "95.0.1020.40", "beta", "dev"]
33
32
  };
34
33
  const TEST_URL = "https://gitlab.com/eyeo/developer-experience/get-browser-binary";
35
- let extensionPaths = [path.resolve(process.cwd(), "test", "extension")];
34
+ const TEST_URL_LONG_PAGE = "https://abptestpages.org/";
36
35
 
37
36
  async function switchToHandle(driver, testFn) {
38
37
  for (let handle of await driver.getAllWindowHandles()) {
@@ -44,7 +43,6 @@ async function switchToHandle(driver, testFn) {
44
43
  catch (e) {
45
44
  continue;
46
45
  }
47
-
48
46
  if (testFn(url))
49
47
  return handle;
50
48
  }
@@ -90,11 +88,28 @@ expect.extend({
90
88
  }
91
89
  });
92
90
 
93
- for (let browser of Object.keys(BROWSERS)) {
94
- describe(`Browser: ${browser}`, function() {
95
- this.timeout(150000);
91
+ function getExtension(browser, version) {
92
+ let manifest = "mv2";
93
+ // The latest Edge installed on the Gitlab Windows Shared Runners is Edge 79,
94
+ // which does not support manifest v3. More info:
95
+ // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/29
96
+ let windowsSharedRunners =
97
+ browser == "edge" && process.platform == "win32" && process.env.CI_JOB_ID;
98
+ if (["chromium", "edge"].includes(browser) &&
99
+ ["latest", "beta", "dev"].includes(version) && !windowsSharedRunners)
100
+ manifest = "mv3";
101
+
102
+ let extensionPaths =
103
+ [path.resolve(process.cwd(), "test", "extension", manifest)];
104
+ return {extensionPaths, manifest};
105
+ }
96
106
 
107
+ for (let browser of Object.keys(BROWSERS)) {
108
+ describe(`Browser: ${browser}`, () => {
97
109
  before(async() => {
110
+ if (process.env.TEST_KEEP_SNAPSHOTS == "true")
111
+ return;
112
+
98
113
  try {
99
114
  await fs.promises.rm(snapshotsBaseDir, {recursive: true});
100
115
  }
@@ -120,12 +135,12 @@ for (let browser of Object.keys(BROWSERS)) {
120
135
 
121
136
  afterEach(quitDriver);
122
137
 
123
- it("downloads", async function() {
138
+ it("installs", async function() {
124
139
  if (browser == "edge" && process.platform == "win32")
125
140
  this.skip();
126
141
 
127
142
  let {binary, versionNumber} =
128
- await BROWSERS[browser].downloadBinary(version);
143
+ await BROWSERS[browser].installBrowser(version);
129
144
  let browserName = browser == "edge" ? /(edge|Edge)/ : browser;
130
145
  expect(binary).toEqual(expect.stringMatching(browserName));
131
146
 
@@ -167,8 +182,22 @@ for (let browser of Object.keys(BROWSERS)) {
167
182
  expect(sizeDevToolsOpen).toMeasureLessThan(sizeDevToolsClosed);
168
183
  });
169
184
 
185
+ it("takes a full page screenshot", async() => {
186
+ driver = await BROWSERS[browser].getDriver(version);
187
+ await driver.navigate().to(TEST_URL_LONG_PAGE);
188
+
189
+ let fullImg = await BROWSERS[browser].takeFullPageScreenshot(driver);
190
+ // Taking a regular webdriver screenshot, which should be shorter
191
+ let data = await driver.takeScreenshot();
192
+ let partImg = await Jimp.read(Buffer.from(data, "base64"));
193
+
194
+ expect(fullImg.bitmap.width).toBeGreaterThan(0);
195
+ expect(fullImg.bitmap.height).toBeGreaterThan(partImg.bitmap.height);
196
+ });
197
+
170
198
  it("loads an extension", async() => {
171
199
  let headless = browser == "firefox";
200
+ let {extensionPaths} = getExtension(browser, version);
172
201
 
173
202
  driver = await BROWSERS[browser].getDriver(
174
203
  version, {headless, extensionPaths});
@@ -179,22 +208,23 @@ for (let browser of Object.keys(BROWSERS)) {
179
208
  if (browser == "firefox" && version == "60.0")
180
209
  this.skip();
181
210
 
211
+ let {extensionPaths, manifest} = getExtension(browser, version);
182
212
  driver = await BROWSERS[browser].getDriver(
183
213
  version, {headless: false, extensionPaths, incognito: true});
184
214
  await BROWSERS[browser].enableExtensionInIncognito(
185
- driver, "Browser download test extension"
215
+ driver, `Browser test extension - ${manifest}`
186
216
  );
187
217
  await getHandle(driver, "/index.html");
188
218
  });
189
219
  });
190
220
  }
191
221
 
192
- it("does not download unsupported versions", async function() {
222
+ it("does not install unsupported versions", async function() {
193
223
  if (browser == "edge" && process.platform == "win32")
194
224
  this.skip();
195
225
 
196
226
  for (let unsupported of ["0.0", "invalid"]) {
197
- await expect(BROWSERS[browser].downloadBinary(unsupported))
227
+ await expect(BROWSERS[browser].installBrowser(unsupported))
198
228
  .rejects.toThrow(`Unsupported browser version: ${unsupported}`);
199
229
  }
200
230
  });
@@ -0,0 +1 @@
1
+ <h1>Browser test extension - mv2</h1>
@@ -1,7 +1,6 @@
1
1
  {
2
- "name": "Browser download test extension",
2
+ "name": "Browser test extension - mv2",
3
3
  "version": "0.1",
4
- "description": "Browser download test extension",
5
4
  "manifest_version": 2,
6
5
  "background": {
7
6
  "scripts": ["background.js"]
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ chrome.tabs.create({url: "index.html"});
@@ -0,0 +1 @@
1
+ <h1>Browser test extension - mv3</h1>
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "Browser test extension - mv3",
3
+ "version": "0.1",
4
+ "manifest_version": 3,
5
+ "background": {
6
+ "service_worker": "background.js"
7
+ }
8
+ }
@@ -1 +0,0 @@
1
- <h1>Browser download test extension</h1>