UNPKG

8.62 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3/* eslint import/no-commonjs:0 no-useless-escape:0*/
4
5const fs = require('fs')
6const rimraf = require('rimraf')
7const path = require('path')
8const childProcess = require('child_process')
9const program = require('commander')
10const utils = require('../src/utils')
11
12// Do not hard-code - users must run this in their project directory.
13const projectPkg = require(path.join(process.cwd(), 'package.json'))
14
15/**
16 * Get a parameter value, falling back to environment variables, package.json
17 * or a default value.
18 */
19const getParam = (cmdValue, envKey, defaultVal) => {
20 const values = [
21 cmdValue,
22 process.env[envKey],
23 ((projectPkg.mobify || {}).testFrameworkConfig || {})[envKey],
24 defaultVal
25 ]
26 return values.find((x) => x !== undefined)
27}
28
29const parseBoolean = (val) => {
30 return val === true || val === 'true'
31}
32
33const runLighthouse = (
34 urls,
35 {
36 maxTTI,
37 minPWAScore,
38 minSEOScore,
39 minAccessibilityScore,
40 checkConsoleErrors,
41 mobifyPreview,
42 device,
43 outputDir
44 }
45) => {
46 outputDir = outputDir || path.resolve(process.cwd(), 'tests', 'lighthouse-reports')
47 const nodeModules = utils.findNodeModules(require.resolve('lighthouse'))
48 const lighthouse = path.resolve(nodeModules, '.bin', 'lighthouse')
49 if (fs.existsSync(outputDir)) {
50 rimraf.sync(outputDir)
51 }
52 fs.mkdirSync(outputDir)
53
54 const lighthouseConfig = path.resolve(path.dirname(__dirname), 'src', 'lighthouse-config.js')
55 const sharedChromeFlags = '--headless --allow-insecure-localhost --ignore-certificate-errors'
56
57 urls.forEach((url) => {
58 // If in preview mode, append the preview fragment to the URL, and update Chrome
59 // flags to ignore certificate errors, etc. Note that for preview mode we
60 // _need_ the user-agent "MobifyPreview" or else the v8 tag and loader.js will not treat
61 // it as a supported browser
62
63 url = mobifyPreview
64 ? `${url}#mobify-override\&mobify-path=true\&mobify-url=https://localhost:8443/loader.js\&mobify-global=true\&mobify-domain=\&mobify-all=true\&mobify=1\&mobify-debug=1&mobify-js=1`
65 : url
66 const opts = mobifyPreview
67 ? `--disable-device-emulation=true --chrome-flags='--user-agent="MobileMobifyPreview" ${sharedChromeFlags}'`
68 : `--chrome-flags='${sharedChromeFlags}'`
69 const cmd = `${lighthouse} "${url}" --config-path ${lighthouseConfig} --emulated-form-factor ${device} --quiet --output json --output html ${opts}`
70 console.log(cmd)
71 childProcess.execSync(cmd, {cwd: outputDir, stdio: [0, 1, 2]})
72 })
73
74 const jsonFiles = fs
75 .readdirSync(outputDir)
76 .filter((fileName) => fileName.match(/\.json$/))
77 .map((fileName) => path.resolve(outputDir, fileName))
78
79 const results = []
80
81 jsonFiles.forEach((fileName) => {
82 const report = JSON.parse(fs.readFileSync(fileName))
83
84 const pwaScore = 100 * report.categories.pwa.score
85 const pwaScoreBelow = pwaScore < minPWAScore
86
87 const seoScore = 100 * report.categories.seo.score
88 const seoScoreBelow = seoScore < minSEOScore
89
90 const accessibilityScore = 100 * report.categories.accessibility.score
91 const accessibilityScoreBelow = accessibilityScore < minAccessibilityScore
92
93 const tti = report.audits.interactive.rawValue
94 const ttiOver = tti > maxTTI
95
96 const url = report.requestedUrl
97
98 results.push({
99 error: ttiOver,
100 msg: `[${url}] Time to first interactive of ${tti} was ${
101 ttiOver ? 'above' : 'below'
102 } the maximum (${maxTTI})`
103 })
104 results.push({
105 error: pwaScoreBelow,
106 msg: `[${url}] Lighthouse PWA score of ${pwaScore} was ${
107 pwaScoreBelow ? 'below' : 'above or equal to'
108 } the minimum (${minPWAScore})`
109 })
110 results.push({
111 error: seoScoreBelow,
112 msg: `[${url}] Lighthouse SEO score of ${seoScore} was ${
113 seoScoreBelow ? 'below' : 'above or equal to'
114 } the minimum (${minSEOScore})`
115 })
116 results.push({
117 error: accessibilityScoreBelow,
118 msg: `[${url}] Lighthouse accessibility score of ${accessibilityScore} was ${
119 accessibilityScoreBelow ? 'below' : 'above or equal to'
120 } the minimum (${minAccessibilityScore})`
121 })
122
123 if (checkConsoleErrors) {
124 const errorsInConsole = report.audits['errors-in-console'].rawValue
125 const errorsLoggedInConsole = errorsInConsole != 0
126 results.push({
127 error: errorsLoggedInConsole,
128 msg: `[${url}] ${errorsInConsole} browser error(s) logged to the console`
129 })
130 }
131 })
132
133 const hasErrors = results.filter((result) => result.error).length > 0
134
135 results.forEach((result) => {
136 console.log(`${result.error ? 'FAIL:' : 'PASS:'} ${result.msg}`)
137 })
138 console.log(`Reports located in ${outputDir}`)
139 process.exit(hasErrors ? 1 : 0)
140}
141
142const runNightwatch = (nightwatchOpts, {config}) => {
143 config = config || path.resolve(path.dirname(__dirname), 'src', 'nightwatch-config.js')
144 nightwatchOpts = nightwatchOpts || ''
145 const nodeModules = utils.findNodeModules(require.resolve('nightwatch'))
146 const nightwatch = path.resolve(nodeModules, '.bin', 'nightwatch')
147 const cmd = `${nightwatch} --config ${config} ${nightwatchOpts}`
148 console.log(cmd)
149 childProcess.execSync(cmd, {stdio: [0, 1, 2]})
150}
151
152program
153 .command('lighthouse <urls...>')
154 .description(
155 `Runs lighthouse tests on a list of URLs.
156 eg. mobify-test-framework.js lighthouse https://www.merlinspotions.com/
157
158 Parameters can be set in several locations and they take precedence as follows:
159
160 command-line args -> environment variables -> "mobify.testFrameworkConfig"
161 section of package.json
162 `
163 )
164 .option(
165 '--maxTTI [milliseconds]',
166 'Maximum time to interactive before reporting a failure (default: 10000 ms)'
167 )
168 .option(
169 '--minPWAScore [percentage]',
170 'Minimum PWA score before reporting a failure (default: 90)'
171 )
172 .option(
173 '--minSEOScore [percentage]',
174 'Minimum SEO score before reporting a failure (default: 100)'
175 )
176 .option(
177 '--minAccessibilityScore [percentage]',
178 'Minimum accessibility score before reporting a failure (default: 100)'
179 )
180 .option(
181 '--checkConsoleErrors',
182 'Assert that browser errors are not logged to the console (default: false)'
183 )
184 .option('--mobifyPreview', 'Run tests using Mobify preview (default: false)')
185 .option('--device', "Form factor for tests (choices: 'mobile', 'desktop') (default: 'mobile')")
186 .option('--outputDir', `Output directory for reports (default: 'tests/lighthouse-reports')`)
187 .action((args, opts) => {
188 opts.maxTTI = parseFloat(getParam(opts.maxTTI, 'max_tti', '10000'))
189 opts.minPWAScore = parseFloat(getParam(opts.minPWAScore, 'min_pwa_score', '90'))
190 opts.minSEOScore = parseFloat(getParam(opts.minSEOScore, 'min_seo_score', '100'))
191 opts.minAccessibilityScore = parseFloat(
192 getParam(opts.minAccessibilityScore, 'min_accessibility_score', '100')
193 )
194 opts.checkConsoleErrors = parseBoolean(
195 getParam(opts.checkConsoleErrors, 'check_console_errors', 'false')
196 )
197 opts.device = getParam(opts.device, 'device', 'mobile')
198 return runLighthouse(args, opts)
199 })
200
201program
202 .command('nightwatch')
203 .description(
204 `Runs nightwatch end-to-end tests, using Mobify's recommended settings.
205 Use '--' to pass extra args directly to nightwatch, eg. mobify-test-framework.js nightwatch -- "--verbose --env chrome_incognito"
206
207 Environment variables:
208 NIGHTWATCH_SAUCE_USERNAME Saucelabs username
209 NIGHTWATCH_SAUCE_ACCESS_KEY Saucelabs password
210 NIGHTWATCH_SRC_FOLDERS Space-separated list of folders containing tests (default ['./tests/e2e'])
211 NIGHTWATCH_OUTPUT_FOLDER Output folder for test reports (default './tests/reports')
212 NIGHTWATCH_SCREENSHOTS_PATH Output folder for test screenshots (default './tests/screenshots')`
213 )
214 .option('--config [file]', `Path to a nightwatch config (default: Mobify's recommended config)`)
215 .action(runNightwatch)
216
217program.parse(process.argv)
218
219if (!process.argv.slice(2).length) {
220 program.help()
221}