1 | #!/usr/bin/env node
|
2 |
|
3 |
|
4 |
|
5 | const fs = require('fs')
|
6 | const rimraf = require('rimraf')
|
7 | const path = require('path')
|
8 | const childProcess = require('child_process')
|
9 | const program = require('commander')
|
10 | const utils = require('../src/utils')
|
11 |
|
12 |
|
13 | const projectPkg = require(path.join(process.cwd(), 'package.json'))
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | const 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 |
|
29 | const parseBoolean = (val) => {
|
30 | return val === true || val === 'true'
|
31 | }
|
32 |
|
33 | const 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 |
|
59 |
|
60 |
|
61 |
|
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 |
|
142 | const 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 |
|
152 | program
|
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 |
|
201 | program
|
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 |
|
217 | program.parse(process.argv)
|
218 |
|
219 | if (!process.argv.slice(2).length) {
|
220 | program.help()
|
221 | }
|