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