Skip to content

Commit b454da9

Browse files
authored
fix(cli): more consistent chromePath handling (#284)
1 parent 5f0c02d commit b454da9

File tree

7 files changed

+153
-16
lines changed

7 files changed

+153
-16
lines changed

packages/cli/src/collect/collect.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
const path = require('path');
99
const yargsParser = require('yargs-parser');
10-
const ChromeLauncher = require('chrome-launcher').Launcher;
10+
const {determineChromePath} = require('../utils.js');
1111
const FallbackServer = require('./fallback-server.js');
1212
const LighthouseRunner = require('./lighthouse-runner.js');
1313
const PuppeteerManager = require('./puppeteer-manager.js');
@@ -41,10 +41,7 @@ function buildCommand(yargs) {
4141
},
4242
chromePath: {
4343
description: 'The path to the Chrome or Chromium executable to use for collection.',
44-
default:
45-
process.env.CHROME_PATH ||
46-
new PuppeteerManager(naiveOptions).getChromiumPath() ||
47-
ChromeLauncher.getInstallations()[0],
44+
default: determineChromePath(naiveOptions),
4845
},
4946
puppeteerScript: {
5047
description:

packages/cli/src/collect/puppeteer-manager.js

+9-6
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class PuppeteerManager {
2525
* setting of `collect.chromePath`.
2626
* @return {typeof import('puppeteer')|undefined}
2727
*/
28-
_requirePuppeteer() {
28+
static _requirePuppeteer() {
2929
try {
3030
// eslint-disable-next-line import/no-extraneous-dependencies
3131
return require('puppeteer');
@@ -43,7 +43,7 @@ class PuppeteerManager {
4343
if (this._browser) return this._browser;
4444

4545
// Delay require to only run after user requests puppeteer functionality.
46-
const puppeteer = this._requirePuppeteer();
46+
const puppeteer = PuppeteerManager._requirePuppeteer();
4747
if (!puppeteer) {
4848
throw new Error(`Unable to require 'puppeteer' for script, have you run 'npm i puppeteer'?`);
4949
}
@@ -65,13 +65,16 @@ class PuppeteerManager {
6565
return !!this._options.puppeteerScript;
6666
}
6767

68-
/** @return {string|undefined} */
69-
getChromiumPath() {
68+
/**
69+
* @param {{puppeteerScript?: string}} options
70+
* @return {string|undefined}
71+
*/
72+
static getChromiumPath(options) {
7073
// If we're not using puppeteer, return undefined.
71-
if (!this._options.puppeteerScript) return undefined;
74+
if (!options.puppeteerScript) return undefined;
7275

7376
// Otherwise, check to see if the expected puppeteer download exists.
74-
const puppeteer = this._requirePuppeteer();
77+
const puppeteer = PuppeteerManager._requirePuppeteer();
7578
const chromiumPath = puppeteer && puppeteer.executablePath();
7679
return chromiumPath && fs.existsSync(chromiumPath) ? chromiumPath : undefined;
7780
}

packages/cli/src/healthcheck/healthcheck.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
*/
66
'use strict';
77

8-
const fs = require('fs');
9-
const ChromeLauncher = require('chrome-launcher').Launcher;
8+
const {canAccessPath, determineChromePath} = require('../utils.js');
109
const ApiClient = require('@lhci/utils/src/api-client.js');
1110
const {loadAndParseRcFile, resolveRcFilePath} = require('@lhci/utils/src/lighthouserc.js');
1211
const {
@@ -65,8 +64,8 @@ const checks = [
6564
failureLabel: 'Chrome installation not found',
6665
shouldTest: () => true,
6766
test: opts => {
68-
if (opts.chromePath) return fs.existsSync(opts.chromePath);
69-
return ChromeLauncher.getInstallations().length > 0;
67+
if (opts.chromePath) return canAccessPath(opts.chromePath);
68+
return canAccessPath(determineChromePath(opts) || '');
7069
},
7170
},
7271
{

packages/cli/src/utils.js

+50-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
*/
66
'use strict';
77

8+
const fs = require('fs');
9+
const PuppeteerManager = require('./collect/puppeteer-manager.js');
10+
const ChromeLauncher = require('chrome-launcher').Launcher;
11+
812
/** @param {string} dependency */
913
function assertOptionalDependency(dependency) {
1014
try {
@@ -19,4 +23,49 @@ function assertOptionalDependency(dependency) {
1923
}
2024
}
2125

22-
module.exports = {assertOptionalDependency};
26+
/** @param {string} filePath @return {boolean} */
27+
function canAccessPath(filePath) {
28+
try {
29+
fs.accessSync(filePath);
30+
return true;
31+
} catch (_) {
32+
return false;
33+
}
34+
}
35+
36+
/**
37+
* ChromeLauncher has an annoying API that *throws* instead of returning an empty array.
38+
* Also enables testing of environments that don't have Chrome globally installed in tests.
39+
*
40+
* @return {string[]}
41+
*/
42+
function getChromeInstallationsSafe() {
43+
if (process.env.LHCITEST_IGNORE_CHROME_INSTALLATIONS) return [];
44+
45+
try {
46+
return ChromeLauncher.getInstallations();
47+
} catch (err) {
48+
return [];
49+
}
50+
}
51+
52+
/**
53+
* @param {Partial<LHCI.CollectCommand.Options>} options
54+
* @return {string|undefined}
55+
*/
56+
function determineChromePath(options) {
57+
return (
58+
options.chromePath ||
59+
process.env.CHROME_PATH ||
60+
PuppeteerManager.getChromiumPath(options) ||
61+
getChromeInstallationsSafe()[0] ||
62+
undefined
63+
);
64+
}
65+
66+
module.exports = {
67+
assertOptionalDependency,
68+
canAccessPath,
69+
determineChromePath,
70+
getChromeInstallationsSafe,
71+
};

packages/cli/test/collect-puppeteer.test.js

+19
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,23 @@ describe('Lighthouse CI collect CLI with puppeteer', () => {
4949
`);
5050
expect(status).toEqual(0);
5151
}, 180000);
52+
53+
it('should not fail on providing defaults without Chrome installations', () => {
54+
const {stdout, stderr, status} = runCLI(['collect', '--help'], {
55+
cwd: autorunDir,
56+
env: {LHCITEST_IGNORE_CHROME_INSTALLATIONS: '1'},
57+
});
58+
59+
// Make sure there is no default chromePath found
60+
const chromePathHelp = stdout.match(/--chromePath.*\n.*\n.*/);
61+
expect(chromePathHelp).toMatchInlineSnapshot(`
62+
Array [
63+
"--chromePath The path to the Chrome or Chromium executable to
64+
use for collection.
65+
--puppeteerScript The path to a script that manipulates the browser",
66+
]
67+
`);
68+
expect(stderr).toMatchInlineSnapshot(`""`);
69+
expect(status).toEqual(0);
70+
});
5271
});

packages/cli/test/healthcheck.test.js

+69
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77

88
/* eslint-env jest */
99

10+
const os = require('os');
11+
const fs = require('fs');
1012
const path = require('path');
13+
const rimraf = require('rimraf');
1114
const {runCLI} = require('./test-utils.js');
1215

1316
describe('Lighthouse CI healthcheck CLI', () => {
@@ -114,4 +117,70 @@ describe('Lighthouse CI healthcheck CLI', () => {
114117
expect(stderr).toMatchInlineSnapshot(`""`);
115118
});
116119
});
120+
121+
describe('chrome installation', () => {
122+
let fixtureDir;
123+
124+
beforeEach(() => {
125+
fixtureDir = path.join(os.tmpdir(), fs.mkdtempSync('lhcihealthcheck'));
126+
fs.mkdirSync(fixtureDir, {recursive: true});
127+
});
128+
129+
afterEach(() => {
130+
rimraf.sync(fixtureDir);
131+
});
132+
133+
it('should find a chrome installation installed on the system', () => {
134+
const {stdout, stderr, status} = runCLI(['healthcheck', '--fatal'], {
135+
cwd: fixtureDir,
136+
});
137+
138+
expect(stdout).toMatchInlineSnapshot(`
139+
"✅ .lighthouseci/ directory writable
140+
⚠️ Configuration file not found
141+
✅ Chrome installation found
142+
Healthcheck passed!
143+
"
144+
`);
145+
expect(stderr).toMatchInlineSnapshot(`""`);
146+
expect(status).toEqual(0);
147+
});
148+
149+
it('should not find a chrome installation when ignored', () => {
150+
const {stdout, stderr, status} = runCLI(['healthcheck', '--fatal'], {
151+
cwd: fixtureDir,
152+
env: {LHCITEST_IGNORE_CHROME_INSTALLATIONS: '1'},
153+
});
154+
155+
expect(stdout).toMatchInlineSnapshot(`
156+
"✅ .lighthouseci/ directory writable
157+
⚠️ Configuration file not found
158+
❌ Chrome installation not found
159+
Healthcheck failed!
160+
"
161+
`);
162+
expect(stderr).toMatchInlineSnapshot(`""`);
163+
expect(status).toEqual(1);
164+
});
165+
166+
it('should find a chrome installation when ignored but passed explicitly via config', () => {
167+
const ci = {collect: {chromePath: fixtureDir}};
168+
fs.writeFileSync(path.join(fixtureDir, '.lighthouserc.json'), JSON.stringify({ci}));
169+
170+
const {stdout, stderr, status} = runCLI(['healthcheck', '--fatal'], {
171+
cwd: fixtureDir,
172+
env: {LHCI_NO_LIGHTHOUSERC: undefined, LHCITEST_IGNORE_CHROME_INSTALLATIONS: '1'},
173+
});
174+
175+
expect(stdout).toMatchInlineSnapshot(`
176+
"✅ .lighthouseci/ directory writable
177+
✅ Configuration file found
178+
✅ Chrome installation found
179+
Healthcheck passed!
180+
"
181+
`);
182+
expect(stderr).toMatchInlineSnapshot(`""`);
183+
expect(status).toEqual(0);
184+
});
185+
});
117186
});

packages/cli/test/test-utils.js

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ function waitForCondition(fn, label) {
111111
function getCleanEnvironment(extraEnvVars) {
112112
const cleanEnv = {
113113
...process.env,
114+
CHROME_PATH: undefined,
114115
LHCI_GITHUB_TOKEN: undefined,
115116
LHCI_GITHUB_APP_TOKEN: undefined,
116117
LHCI_CANARY_SERVER_URL: undefined,

0 commit comments

Comments
 (0)