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