ui5-test-runner 5.3.0 โ†’ 5.3.1

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/src/job.js CHANGED
@@ -1,392 +1,393 @@
1
- 'use strict'
2
-
3
- const { Command, Option, InvalidArgumentError } = require('commander')
4
- const { statSync, accessSync, constants } = require('fs')
5
- const { dirname, join, isAbsolute } = require('path')
6
- const { name, description, version } = require(join(__dirname, '../package.json'))
7
- const { getOutput } = require('./output')
8
- const { $valueSources, $remoteOnLegacy } = require('./symbols')
9
- const { buildAndCheckMode } = require('./job-mode')
10
- const { boolean, integer, timeout, url, arrayOf, regex, percent, string } = require('./options')
11
-
12
- const $status = Symbol('status')
13
-
14
- function toLongName (name) {
15
- return name.replace(/([A-Z])([a-z]+)/g, (match, firstLetter, reminder) => `-${firstLetter.toLowerCase()}${reminder}`)
16
- }
17
-
18
- function buildArgs (parameters) {
19
- const before = []
20
- const after = []
21
- let browser = []
22
- Object.keys(parameters).forEach(name => {
23
- if (name === '--') {
24
- return
25
- }
26
- const value = parameters[name]
27
- let args
28
- if (name.startsWith('!')) {
29
- args = after
30
- name = name.substring(1)
31
- } else {
32
- args = before
33
- }
34
- args.push(`--${toLongName(name)}`)
35
- if (value !== null) {
36
- if (Array.isArray(value)) {
37
- args.push(...value)
38
- } else {
39
- args.push(value)
40
- }
41
- }
42
- })
43
- if (parameters['--']) {
44
- browser = parameters['--']
45
- }
46
- const stringify = args => args.map(value => value.toString())
47
- return {
48
- before: stringify(before),
49
- after: stringify(after),
50
- browser: stringify(browser)
51
- }
52
- }
53
-
54
- function lib (value) {
55
- if (value.includes('=')) {
56
- const [relative, source] = value.split('=')
57
- return { relative, source }
58
- } else {
59
- return { relative: '', source: value }
60
- }
61
- }
62
-
63
- function mapping (value) {
64
- try {
65
- const [, match, handler, mapping] = /([^=]*)=(file|url)\((.*)\)/.exec(value)
66
- return {
67
- match,
68
- [handler]: mapping
69
- }
70
- } catch (e) {
71
- throw new InvalidArgumentError('Invalid mapping')
72
- }
73
- }
74
-
75
- function getCommand (cwd) {
76
- const command = new Command()
77
- command.exitOverride()
78
-
79
- const DEBUG_OPTION = '(๐Ÿž for debugging purpose)'
80
- const EXPERIMENTAL_OPTION = '[โš ๏ธ experimental]'
81
-
82
- command
83
- .name(name)
84
- .description(description)
85
- .version(version)
86
-
87
- .option('--capabilities', '๐Ÿงช Capabilities tester for browser')
88
- .option('-u, --url <url...>', '๐Ÿ”— URL of the testsuite / page to test', arrayOf(url))
89
-
90
- // Common to all modes
91
- .addOption(
92
- new Option('-c, --cwd <path>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Set working directory')
93
- .default(cwd, 'current working directory')
94
- )
95
- .option('--port <port>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Port to use (0 to use any free one)', integer, 0)
96
- .option('-r, --report-dir <path>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Directory to output test reports (relative to cwd)', 'report')
97
- .option('-pt, --page-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Limit the page execution time, fails the page if it takes longer than the timeout (0 means no timeout)', timeout, 0)
98
- .option('-f, --fail-fast [flag]', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Stop the execution after the first failing page', boolean, false)
99
- .option('-fo, --fail-opa-fast [flag]', '[๐Ÿ’ป๐Ÿ”—] Stop the OPA page execution after the first failing test', boolean, false)
100
- .option('-k, --keep-alive [flag]', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Keep the server alive', boolean, false)
101
- .option('-l, --log-server [flag]', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Log inner server traces', boolean, false)
102
- .option('-p, --parallel <count>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Number of parallel tests executions', integer, 2)
103
- .option('-b, --browser <command>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Browser instantiation command (relative to cwd or use $/ for provided ones)', '$/puppeteer.js')
104
- .option('--browser-args <argument...>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Browser instantiation command parameters (use -- instead)')
105
- .option('--alternate-npm-path <path>', '[๐Ÿ’ป๐Ÿ”—] Alternate NPM path to look for packages (priority: local, alternate, global)')
106
- .option('--no-npm-install', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Prevent any NPM install (execution may fail if a dependency is missing)')
107
- .option('-bt, --browser-close-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Maximum waiting time for browser close', timeout, 2000)
108
- .option('-br, --browser-retry <count>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Browser instantiation retries : if the command fails unexpectedly, it is re-executed (0 means no retry)', 1)
109
- .option('-oi, --output-interval <interval>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Interval for reporting progress on non interactive output (CI/CD) (0 means no output)', timeout, 30000)
110
- .option('--offline', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Limit network usage (implies --no-npm-install)', boolean, false)
111
-
112
- // Common to legacy and url
113
- .option('--webapp <path>', '[๐Ÿ’ป๐Ÿ”—] Base folder of the web application (relative to cwd)', 'webapp')
114
- .option('-pf, --page-filter <regexp>', '[๐Ÿ’ป๐Ÿ”—] Filter out pages not matching the regexp')
115
- .option('-pp, --page-params <params>', '[๐Ÿ’ป๐Ÿ”—] Add parameters to page URL')
116
- .option('--page-close-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—] Maximum waiting time for page close', timeout, 250)
117
- .option('-t, --global-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—] Limit the pages execution time, fail the page if it takes longer than the timeout (0 means no timeout)', timeout, 0)
118
- .option('--screenshot [flag]', '[๐Ÿ’ป๐Ÿ”—] Take screenshots during the tests execution (if supported by the browser)', boolean, true)
119
- .option('--no-screenshot', '[๐Ÿ’ป๐Ÿ”—] Disable screenshots')
120
- .option('-st, --screenshot-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—] Maximum waiting time for browser screenshot', timeout, 5000)
121
- .option('-so, --split-opa', '[๐Ÿ’ป๐Ÿ”—] Split OPA tests using QUnit modules', boolean, false)
122
- .option('-rg, --report-generator <path...>', '[๐Ÿ’ป๐Ÿ”—] Report generator paths (relative to cwd or use $/ for provided ones)', ['$/report.js'])
123
- .option('--progress-page <path>', '[๐Ÿ’ป๐Ÿ”—] Progress page path (relative to cwd or use $/ for provided ones)', '$/report/default.html')
124
-
125
- .option('--coverage [flag]', '[๐Ÿ’ป๐Ÿ”—] Enable or disable code coverage', boolean)
126
- .option('--no-coverage', '[๐Ÿ’ป๐Ÿ”—] Disable code coverage')
127
- .option('-cs, --coverage-settings <path>', '[๐Ÿ’ป๐Ÿ”—] Path to a custom nyc.json file providing settings for instrumentation (relative to cwd or use $/ for provided ones)', '$/nyc.json')
128
- .option('-ctd, --coverage-temp-dir <path>', '[๐Ÿ’ป๐Ÿ”—] Directory to output raw coverage information to (relative to cwd)', '.nyc_output')
129
- .option('-crd, --coverage-report-dir <path>', '[๐Ÿ’ป๐Ÿ”—] Directory to store the coverage report files (relative to cwd)', 'coverage')
130
- .option('-cr, --coverage-reporters <reporter...>', '[๐Ÿ’ป๐Ÿ”—] List of nyc reporters to use (text is always used)', ['lcov', 'cobertura'])
131
- .option('-ccb, --coverage-check-branches <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of branches must be covered', percent, 0)
132
- .option('-ccf, --coverage-check-functions <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of functions must be covered', percent, 0)
133
- .option('-ccl, --coverage-check-lines <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of lines must be covered', percent, 0)
134
- .option('-ccs, --coverage-check-statements <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of statements must be covered', percent, 0)
135
- .option('-crs, --coverage-remote-scanner <path>', '[๐Ÿ’ป๐Ÿ”—] Scan for files when all coverage is requested', '$/scan-ui5.js')
136
- .option('-s, --serve-only [flag]', '[๐Ÿ’ป๐Ÿ”—] Serve only', boolean, false)
137
-
138
- // Specific to legacy (and might be used with url if pointing to local project)
139
- .option('--ui5 <url>', '[๐Ÿ’ป] UI5 url', url, 'https://ui5.sap.com')
140
- .option('--disable-ui5', '[๐Ÿ’ป] Disable UI5 mapping (also disable libs)', boolean, false)
141
- .option('--libs <lib...>', '[๐Ÿ’ป] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
142
- .option('--mappings <mapping...>', '[๐Ÿ’ป] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
143
- .option('--cache <path>', '[๐Ÿ’ป] Cache UI5 resources locally in the given folder (empty to disable)')
144
- .option('--preload <library...>', '[๐Ÿ’ป] Preload UI5 libraries in the cache folder (only if --cache is used)', arrayOf(string))
145
- .option('--testsuite <path>', '[๐Ÿ’ป] Path of the testsuite file (relative to webapp, URL parameters are supported)', 'test/testsuite.qunit.html')
146
- .option('-w, --watch [flag]', '[๐Ÿ’ป] Monitor the webapp folder and re-execute tests on change', boolean, false)
147
-
148
- // Specific to coverage in url mode (experimental)
149
- .option('-cp, --coverage-proxy [flag]', `[๐Ÿ”—] ${EXPERIMENTAL_OPTION} use internal proxy to instrument remote files`, boolean, false)
150
- .option('-cpi, --coverage-proxy-include <regexp>', `[๐Ÿ”—] ${EXPERIMENTAL_OPTION} urls to instrument for coverage`, regex, '.*')
151
- .option('-cpe, --coverage-proxy-exclude <regexp>', `[๐Ÿ”—] ${EXPERIMENTAL_OPTION} urls to ignore for coverage`, regex, '/((test-)?resources|tests?)/')
152
-
153
- .addOption(new Option('--debug-dev-mode', DEBUG_OPTION, boolean).hideHelp())
154
- .addOption(new Option('--debug-probe-only', DEBUG_OPTION, boolean).hideHelp())
155
- .addOption(new Option('--debug-keep-browser-open', DEBUG_OPTION, boolean).hideHelp())
156
- .addOption(new Option('--debug-memory', DEBUG_OPTION, boolean).hideHelp())
157
- .addOption(new Option('--debug-keep-report', DEBUG_OPTION, boolean).hideHelp())
158
- .addOption(new Option('--debug-capabilities-test <name>', DEBUG_OPTION).hideHelp())
159
- .addOption(new Option('--debug-capabilities-no-timeout', DEBUG_OPTION, boolean).hideHelp())
160
- .addOption(new Option('--debug-coverage-no-custom-fs', DEBUG_OPTION, boolean).hideHelp())
161
- .addOption(new Option('--debug-verbose <module...>', DEBUG_OPTION, arrayOf(string), []).hideHelp())
162
-
163
- return command
164
- }
165
-
166
- function parse (cwd, args) {
167
- const command = getCommand(cwd)
168
-
169
- command.parse(args, { from: 'user' })
170
- const options = command.opts()
171
-
172
- return Object.assign({
173
- initialCwd: cwd,
174
- browserArgs: command.args,
175
- [$valueSources]: Object.keys(options).reduce((valueSources, name) => {
176
- if (name !== 'browserArgs') {
177
- valueSources[name] = command.getOptionValueSource(name)
178
- }
179
- return valueSources
180
- }, {})
181
- }, options)
182
- }
183
-
184
- function checkAccess ({ path, label, file /*, write */ }) {
185
- try {
186
- const mode = constants.R_OK
187
- // if (write) {
188
- // mode |= constants.W_OK
189
- // }
190
- accessSync(path, mode)
191
- } catch (error) {
192
- throw new Error(`Unable to access ${label}, check your settings`)
193
- }
194
- const stat = statSync(path)
195
- if (file) {
196
- if (!stat.isFile()) {
197
- throw new Error(`Unable to access ${label}, file expected`)
198
- }
199
- } else {
200
- if (!stat.isDirectory()) {
201
- throw new Error(`Unable to access ${label}, folder expected`)
202
- }
203
- }
204
- }
205
-
206
- function finalize (job) {
207
- function toAbsolute (path, from = job.cwd) {
208
- if (!isAbsolute(path)) {
209
- return join(from, path)
210
- }
211
- return path
212
- }
213
-
214
- function checkDefault (path) {
215
- if (path.startsWith('$/')) {
216
- return join(__dirname, './defaults', path.replace('$/', ''))
217
- }
218
- return path
219
- }
220
-
221
- function updateToAbsolute (member, from = job.cwd) {
222
- job[member] = toAbsolute(job[member], from)
223
- }
224
- 'browser,coverageSettings,coverageRemoteScanner,progressPage'
225
- .split(',')
226
- .forEach(setting => { job[setting] = checkDefault(job[setting]) })
227
- updateToAbsolute('cwd', job.initialCwd)
228
- 'webapp,browser,reportDir,coverageSettings,coverageTempDir,coverageReportDir'
229
- .split(',')
230
- .forEach(setting => updateToAbsolute(setting))
231
- if (job.cache) {
232
- updateToAbsolute('cache')
233
- if (job.preload && job.offline) {
234
- throw new Error('--preload cannot be used with --offline')
235
- }
236
- } else if (job.preload) {
237
- throw new Error('--preload cannot be used without --cache')
238
- }
239
- if (job.alternateNpmPath) {
240
- checkAccess({ path: job.alternateNpmPath, label: 'Alternate NPM path' })
241
- }
242
- job.mode = buildAndCheckMode(job)
243
- if (job.mode === 'legacy') {
244
- checkAccess({ path: job.webapp, label: 'webapp folder' })
245
-
246
- const [, testsuiteFile] = job.testsuite.match(/([^?]*)(\?.*)?$/)
247
- const testsuitePath = toAbsolute(testsuiteFile, job.webapp)
248
- checkAccess({ path: testsuitePath, label: 'testsuite', file: true })
249
- } else if (job.mode === 'url') {
250
- if (job[$valueSources].coverage !== 'cli') {
251
- job.coverage = false
252
- }
253
- }
254
- checkAccess({ path: job.browser, label: 'browser command', file: true })
255
- job.reportGenerator = job.reportGenerator.map(setting => {
256
- const path = toAbsolute(checkDefault(setting), job.cwd)
257
- checkAccess({ path, label: 'report generator', file: true })
258
- return path
259
- })
260
-
261
- if (!job.libs) {
262
- job.libs = []
263
- } else {
264
- job.libs.forEach(libMapping => {
265
- libMapping.source = toAbsolute(libMapping.source)
266
- let description
267
- if (libMapping.relative) {
268
- description = `lib mapping of ${libMapping.relative}`
269
- } else {
270
- description = 'generic lib mapping'
271
- }
272
- checkAccess({ path: libMapping.source, label: `${description} (${libMapping.source})` })
273
- })
274
- }
275
-
276
- const output = getOutput(job)
277
-
278
- if (job.coverage) {
279
- function overrideIfNotSet (option, valueFromSettings) {
280
- if (valueFromSettings && job[$valueSources][option] !== 'cli') {
281
- output.debug('coverage', `${option} extracted from nyc settings : ${valueFromSettings}`)
282
- job[option] = valueFromSettings
283
- }
284
- }
285
-
286
- function overrideDirIfNotSet (option, valueFromSettings) {
287
- if (valueFromSettings && !isAbsolute(valueFromSettings)) {
288
- valueFromSettings = join(dirname(job.coverageSettings), valueFromSettings)
289
- }
290
- overrideIfNotSet(option, valueFromSettings)
291
- }
292
-
293
- checkAccess({ path: job.coverageSettings, file: true, label: 'coverage settings' })
294
-
295
- let settings
296
- try {
297
- settings = require(job.coverageSettings)
298
- } catch (e) {
299
- throw new Error(`Unable to read ${job.coverageSettings} as JSON`)
300
- }
301
- overrideDirIfNotSet('coverageReportDir', settings['report-dir'])
302
- overrideDirIfNotSet('coverageTempDir', settings['temp-dir'])
303
- overrideIfNotSet('coverageReporters', settings.reporter)
304
-
305
- checkAccess({ path: job.coverageRemoteScanner, label: 'coverage remote scanner', file: true })
306
- }
307
-
308
- if (job.mode === 'url') {
309
- const port = job.port.toString()
310
- job[$remoteOnLegacy] = job.url.every(url => {
311
- // ignore host name since the machine might be exposed with any name
312
- const parsedUrl = new URL(url)
313
- return parsedUrl.port === port
314
- })
315
- }
316
-
317
- job[$status] = 'Starting'
318
- Object.defineProperty(job, 'status', {
319
- get () {
320
- return job[$status]
321
- },
322
- set (value) {
323
- job[$status] = value
324
- output.status(value)
325
- },
326
- enumerable: false,
327
- configurable: false
328
- })
329
-
330
- /* istanbul ignore next */
331
- if (process.env.DEBUG_ON_FAILED) {
332
- let failed
333
- Object.defineProperty(job, 'failed', {
334
- get () {
335
- return failed
336
- },
337
- set (value) {
338
- if (value) {
339
- // eslint-disable-next-line no-debugger
340
- debugger
341
- }
342
- failed = value
343
- },
344
- enumerable: true,
345
- configurable: false
346
- })
347
- }
348
- }
349
-
350
- function fromCmdLine (cwd, args) {
351
- let job = parse(cwd, args)
352
-
353
- let defaultPath = join(job.cwd, 'ui5-test-runner.json')
354
- if (!isAbsolute(defaultPath)) {
355
- defaultPath = join(job.initialCwd, defaultPath)
356
- }
357
- let hasDefaultSettings = false
358
- try {
359
- checkAccess({ path: defaultPath, file: true })
360
- hasDefaultSettings = true
361
- } catch (e) {
362
- // ignore
363
- }
364
- if (hasDefaultSettings) {
365
- const defaults = require(defaultPath)
366
- const { before, after, browser } = buildArgs(defaults)
367
- const sep = args.indexOf('--')
368
- if (sep === -1) {
369
- args = [...before, ...args, ...after, '--', ...browser]
370
- } else {
371
- args = [...before, ...args.slice(0, sep), ...after, '--', ...browser, ...args.slice(sep + 1)]
372
- }
373
- job = parse(cwd, args)
374
- }
375
-
376
- finalize(job)
377
- return job
378
- }
379
-
380
- function fromObject (cwd, parameters) {
381
- const { before, browser } = buildArgs(parameters)
382
- if (browser.length) {
383
- return fromCmdLine(cwd, [...before, '--', ...browser])
384
- }
385
- return fromCmdLine(cwd, [...before])
386
- }
387
-
388
- module.exports = {
389
- getCommand,
390
- fromCmdLine,
391
- fromObject
392
- }
1
+ 'use strict'
2
+
3
+ const { Command, Option, InvalidArgumentError } = require('commander')
4
+ const { statSync, accessSync, constants } = require('fs')
5
+ const { dirname, join, isAbsolute } = require('path')
6
+ const { name, description, version } = require(join(__dirname, '../package.json'))
7
+ const { getOutput } = require('./output')
8
+ const { $valueSources, $remoteOnLegacy } = require('./symbols')
9
+ const { buildAndCheckMode } = require('./job-mode')
10
+ const { boolean, integer, timeout, url, arrayOf, regex, percent, string } = require('./options')
11
+
12
+ const $status = Symbol('status')
13
+
14
+ function toLongName (name) {
15
+ return name.replace(/([A-Z])([a-z]+)/g, (match, firstLetter, reminder) => `-${firstLetter.toLowerCase()}${reminder}`)
16
+ }
17
+
18
+ function buildArgs (parameters) {
19
+ const before = []
20
+ const after = []
21
+ let browser = []
22
+ Object.keys(parameters).forEach(name => {
23
+ if (name === '--') {
24
+ return
25
+ }
26
+ const value = parameters[name]
27
+ let args
28
+ if (name.startsWith('!')) {
29
+ args = after
30
+ name = name.substring(1)
31
+ } else {
32
+ args = before
33
+ }
34
+ args.push(`--${toLongName(name)}`)
35
+ if (value !== null) {
36
+ if (Array.isArray(value)) {
37
+ args.push(...value)
38
+ } else {
39
+ args.push(value)
40
+ }
41
+ }
42
+ })
43
+ if (parameters['--']) {
44
+ browser = parameters['--']
45
+ }
46
+ const stringify = args => args.map(value => value.toString())
47
+ return {
48
+ before: stringify(before),
49
+ after: stringify(after),
50
+ browser: stringify(browser)
51
+ }
52
+ }
53
+
54
+ function lib (value) {
55
+ if (value.includes('=')) {
56
+ const [relative, source] = value.split('=')
57
+ return { relative, source }
58
+ } else {
59
+ return { relative: '', source: value }
60
+ }
61
+ }
62
+
63
+ function mapping (value) {
64
+ try {
65
+ const [, match, handler, mapping] = /([^=]*)=(file|url)\((.*)\)/.exec(value)
66
+ return {
67
+ match,
68
+ [handler]: mapping
69
+ }
70
+ } catch (e) {
71
+ throw new InvalidArgumentError('Invalid mapping')
72
+ }
73
+ }
74
+
75
+ function getCommand (cwd) {
76
+ const command = new Command()
77
+ command.exitOverride()
78
+
79
+ const DEBUG_OPTION = '(๐Ÿž for debugging purpose)'
80
+ const EXPERIMENTAL_OPTION = '[โš ๏ธ experimental]'
81
+
82
+ command
83
+ .name(name)
84
+ .description(description)
85
+ .version(version)
86
+
87
+ .option('--capabilities', '๐Ÿงช Capabilities tester for browser')
88
+ .option('-u, --url <url...>', '๐Ÿ”— URL of the testsuite / page to test', arrayOf(url))
89
+
90
+ // Common to all modes
91
+ .addOption(
92
+ new Option('-c, --cwd <path>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Set working directory')
93
+ .default(cwd, 'current working directory')
94
+ )
95
+ .option('--port <port>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Port to use (0 to use any free one)', integer, 0)
96
+ .option('-r, --report-dir <path>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Directory to output test reports (relative to cwd)', 'report')
97
+ .option('-pt, --page-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Limit the page execution time, fails the page if it takes longer than the timeout (0 means no timeout)', timeout, 0)
98
+ .option('-f, --fail-fast [flag]', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Stop the execution after the first failing page', boolean, false)
99
+ .option('-fo, --fail-opa-fast [flag]', '[๐Ÿ’ป๐Ÿ”—] Stop the OPA page execution after the first failing test', boolean, false)
100
+ .option('-k, --keep-alive [flag]', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Keep the server alive', boolean, false)
101
+ .option('-l, --log-server [flag]', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Log inner server traces', boolean, false)
102
+ .option('-p, --parallel <count>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Number of parallel tests executions', integer, 2)
103
+ .option('-b, --browser <command>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Browser instantiation command (relative to cwd or use $/ for provided ones)', '$/puppeteer.js')
104
+ .option('--browser-args <argument...>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Browser instantiation command parameters (use -- instead)')
105
+ .option('--alternate-npm-path <path>', '[๐Ÿ’ป๐Ÿ”—] Alternate NPM path to look for packages (priority: local, alternate, global)')
106
+ .option('--no-npm-install', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Prevent any NPM install (execution may fail if a dependency is missing)')
107
+ .option('-bt, --browser-close-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Maximum waiting time for browser close', timeout, 2000)
108
+ .option('-br, --browser-retry <count>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Browser instantiation retries : if the command fails unexpectedly, it is re-executed (0 means no retry)', 1)
109
+ .option('-oi, --output-interval <interval>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Interval for reporting progress on non interactive output (CI/CD) (0 means no output)', timeout, 30000)
110
+ .option('--offline', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Limit network usage (implies --no-npm-install)', boolean, false)
111
+
112
+ // Common to legacy and url
113
+ .option('--webapp <path>', '[๐Ÿ’ป๐Ÿ”—] Base folder of the web application (relative to cwd)', 'webapp')
114
+ .option('-pf, --page-filter <regexp>', '[๐Ÿ’ป๐Ÿ”—] Filter out pages not matching the regexp')
115
+ .option('-pp, --page-params <params>', '[๐Ÿ’ป๐Ÿ”—] Add parameters to page URL')
116
+ .option('--page-close-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—] Maximum waiting time for page close', timeout, 250)
117
+ .option('-t, --global-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—] Limit the pages execution time, fail the page if it takes longer than the timeout (0 means no timeout)', timeout, 0)
118
+ .option('--screenshot [flag]', '[๐Ÿ’ป๐Ÿ”—] Take screenshots during the tests execution (if supported by the browser)', boolean, true)
119
+ .option('--no-screenshot', '[๐Ÿ’ป๐Ÿ”—] Disable screenshots')
120
+ .option('-st, --screenshot-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—] Maximum waiting time for browser screenshot', timeout, 5000)
121
+ .option('-so, --split-opa', '[๐Ÿ’ป๐Ÿ”—] Split OPA tests using QUnit modules', boolean, false)
122
+ .option('-rg, --report-generator <path...>', '[๐Ÿ’ป๐Ÿ”—] Report generator paths (relative to cwd or use $/ for provided ones)', ['$/report.js'])
123
+ .option('--progress-page <path>', '[๐Ÿ’ป๐Ÿ”—] Progress page path (relative to cwd or use $/ for provided ones)', '$/report/default.html')
124
+
125
+ .option('--coverage [flag]', '[๐Ÿ’ป๐Ÿ”—] Enable or disable code coverage', boolean)
126
+ .option('--no-coverage', '[๐Ÿ’ป๐Ÿ”—] Disable code coverage')
127
+ .option('-cs, --coverage-settings <path>', '[๐Ÿ’ป๐Ÿ”—] Path to a custom nyc.json file providing settings for instrumentation (relative to cwd or use $/ for provided ones)', '$/nyc.json')
128
+ .option('-ctd, --coverage-temp-dir <path>', '[๐Ÿ’ป๐Ÿ”—] Directory to output raw coverage information to (relative to cwd)', '.nyc_output')
129
+ .option('-crd, --coverage-report-dir <path>', '[๐Ÿ’ป๐Ÿ”—] Directory to store the coverage report files (relative to cwd)', 'coverage')
130
+ .option('-cr, --coverage-reporters <reporter...>', '[๐Ÿ’ป๐Ÿ”—] List of nyc reporters to use (text is always used)', ['lcov', 'cobertura'])
131
+ .option('-ccb, --coverage-check-branches <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of branches must be covered', percent, 0)
132
+ .option('-ccf, --coverage-check-functions <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of functions must be covered', percent, 0)
133
+ .option('-ccl, --coverage-check-lines <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of lines must be covered', percent, 0)
134
+ .option('-ccs, --coverage-check-statements <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of statements must be covered', percent, 0)
135
+ .option('-crs, --coverage-remote-scanner <path>', '[๐Ÿ’ป๐Ÿ”—] Scan for files when all coverage is requested', '$/scan-ui5.js')
136
+ .option('-s, --serve-only [flag]', '[๐Ÿ’ป๐Ÿ”—] Serve only', boolean, false)
137
+
138
+ // Specific to legacy (and might be used with url if pointing to local project)
139
+ .option('--ui5 <url>', '[๐Ÿ’ป] UI5 url', url, 'https://ui5.sap.com')
140
+ .option('--disable-ui5', '[๐Ÿ’ป] Disable UI5 mapping (also disable libs)', boolean, false)
141
+ .option('--libs <lib...>', '[๐Ÿ’ป] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
142
+ .option('--mappings <mapping...>', '[๐Ÿ’ป] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
143
+ .option('--cache <path>', '[๐Ÿ’ป] Cache UI5 resources locally in the given folder (empty to disable)')
144
+ .option('--preload <library...>', '[๐Ÿ’ป] Preload UI5 libraries in the cache folder (only if --cache is used)', arrayOf(string))
145
+ .option('--testsuite <path>', '[๐Ÿ’ป] Path of the testsuite file (relative to webapp, URL parameters are supported)', 'test/testsuite.qunit.html')
146
+ .option('-w, --watch [flag]', '[๐Ÿ’ป] Monitor the webapp folder and re-execute tests on change', boolean, false)
147
+
148
+ // Specific to coverage in url mode (experimental)
149
+ .option('-cp, --coverage-proxy [flag]', `[๐Ÿ”—] ${EXPERIMENTAL_OPTION} use internal proxy to instrument remote files`, boolean, false)
150
+ .option('-cpi, --coverage-proxy-include <regexp>', `[๐Ÿ”—] ${EXPERIMENTAL_OPTION} urls to instrument for coverage`, regex, '.*')
151
+ .option('-cpe, --coverage-proxy-exclude <regexp>', `[๐Ÿ”—] ${EXPERIMENTAL_OPTION} urls to ignore for coverage`, regex, '/((test-)?resources|tests?)/')
152
+
153
+ .addOption(new Option('--debug-dev-mode', DEBUG_OPTION, boolean).hideHelp())
154
+ .addOption(new Option('--debug-probe-only', DEBUG_OPTION, boolean).hideHelp())
155
+ .addOption(new Option('--debug-keep-browser-open', DEBUG_OPTION, boolean).hideHelp())
156
+ .addOption(new Option('--debug-memory', DEBUG_OPTION, boolean).hideHelp())
157
+ .addOption(new Option('--debug-keep-report', DEBUG_OPTION, boolean).hideHelp())
158
+ .addOption(new Option('--debug-capabilities-test <name>', DEBUG_OPTION).hideHelp())
159
+ .addOption(new Option('--debug-capabilities-no-timeout', DEBUG_OPTION, boolean).hideHelp())
160
+ .addOption(new Option('--debug-capabilities-no-script', DEBUG_OPTION, boolean).hideHelp())
161
+ .addOption(new Option('--debug-coverage-no-custom-fs', DEBUG_OPTION, boolean).hideHelp())
162
+ .addOption(new Option('--debug-verbose <module...>', DEBUG_OPTION, arrayOf(string), []).hideHelp())
163
+
164
+ return command
165
+ }
166
+
167
+ function parse (cwd, args) {
168
+ const command = getCommand(cwd)
169
+
170
+ command.parse(args, { from: 'user' })
171
+ const options = command.opts()
172
+
173
+ return Object.assign({
174
+ initialCwd: cwd,
175
+ browserArgs: command.args,
176
+ [$valueSources]: Object.keys(options).reduce((valueSources, name) => {
177
+ if (name !== 'browserArgs') {
178
+ valueSources[name] = command.getOptionValueSource(name)
179
+ }
180
+ return valueSources
181
+ }, {})
182
+ }, options)
183
+ }
184
+
185
+ function checkAccess ({ path, label, file /*, write */ }) {
186
+ try {
187
+ const mode = constants.R_OK
188
+ // if (write) {
189
+ // mode |= constants.W_OK
190
+ // }
191
+ accessSync(path, mode)
192
+ } catch (error) {
193
+ throw new Error(`Unable to access ${label}, check your settings`)
194
+ }
195
+ const stat = statSync(path)
196
+ if (file) {
197
+ if (!stat.isFile()) {
198
+ throw new Error(`Unable to access ${label}, file expected`)
199
+ }
200
+ } else {
201
+ if (!stat.isDirectory()) {
202
+ throw new Error(`Unable to access ${label}, folder expected`)
203
+ }
204
+ }
205
+ }
206
+
207
+ function finalize (job) {
208
+ function toAbsolute (path, from = job.cwd) {
209
+ if (!isAbsolute(path)) {
210
+ return join(from, path)
211
+ }
212
+ return path
213
+ }
214
+
215
+ function checkDefault (path) {
216
+ if (path.startsWith('$/')) {
217
+ return join(__dirname, './defaults', path.replace('$/', ''))
218
+ }
219
+ return path
220
+ }
221
+
222
+ function updateToAbsolute (member, from = job.cwd) {
223
+ job[member] = toAbsolute(job[member], from)
224
+ }
225
+ 'browser,coverageSettings,coverageRemoteScanner,progressPage'
226
+ .split(',')
227
+ .forEach(setting => { job[setting] = checkDefault(job[setting]) })
228
+ updateToAbsolute('cwd', job.initialCwd)
229
+ 'webapp,browser,reportDir,coverageSettings,coverageTempDir,coverageReportDir'
230
+ .split(',')
231
+ .forEach(setting => updateToAbsolute(setting))
232
+ if (job.cache) {
233
+ updateToAbsolute('cache')
234
+ if (job.preload && job.offline) {
235
+ throw new Error('--preload cannot be used with --offline')
236
+ }
237
+ } else if (job.preload) {
238
+ throw new Error('--preload cannot be used without --cache')
239
+ }
240
+ if (job.alternateNpmPath) {
241
+ checkAccess({ path: job.alternateNpmPath, label: 'Alternate NPM path' })
242
+ }
243
+ job.mode = buildAndCheckMode(job)
244
+ if (job.mode === 'legacy') {
245
+ checkAccess({ path: job.webapp, label: 'webapp folder' })
246
+
247
+ const [, testsuiteFile] = job.testsuite.match(/([^?]*)(\?.*)?$/)
248
+ const testsuitePath = toAbsolute(testsuiteFile, job.webapp)
249
+ checkAccess({ path: testsuitePath, label: 'testsuite', file: true })
250
+ } else if (job.mode === 'url') {
251
+ if (job[$valueSources].coverage !== 'cli') {
252
+ job.coverage = false
253
+ }
254
+ }
255
+ checkAccess({ path: job.browser, label: 'browser command', file: true })
256
+ job.reportGenerator = job.reportGenerator.map(setting => {
257
+ const path = toAbsolute(checkDefault(setting), job.cwd)
258
+ checkAccess({ path, label: 'report generator', file: true })
259
+ return path
260
+ })
261
+
262
+ if (!job.libs) {
263
+ job.libs = []
264
+ } else {
265
+ job.libs.forEach(libMapping => {
266
+ libMapping.source = toAbsolute(libMapping.source)
267
+ let description
268
+ if (libMapping.relative) {
269
+ description = `lib mapping of ${libMapping.relative}`
270
+ } else {
271
+ description = 'generic lib mapping'
272
+ }
273
+ checkAccess({ path: libMapping.source, label: `${description} (${libMapping.source})` })
274
+ })
275
+ }
276
+
277
+ const output = getOutput(job)
278
+
279
+ if (job.coverage) {
280
+ function overrideIfNotSet (option, valueFromSettings) {
281
+ if (valueFromSettings && job[$valueSources][option] !== 'cli') {
282
+ output.debug('coverage', `${option} extracted from nyc settings : ${valueFromSettings}`)
283
+ job[option] = valueFromSettings
284
+ }
285
+ }
286
+
287
+ function overrideDirIfNotSet (option, valueFromSettings) {
288
+ if (valueFromSettings && !isAbsolute(valueFromSettings)) {
289
+ valueFromSettings = join(dirname(job.coverageSettings), valueFromSettings)
290
+ }
291
+ overrideIfNotSet(option, valueFromSettings)
292
+ }
293
+
294
+ checkAccess({ path: job.coverageSettings, file: true, label: 'coverage settings' })
295
+
296
+ let settings
297
+ try {
298
+ settings = require(job.coverageSettings)
299
+ } catch (e) {
300
+ throw new Error(`Unable to read ${job.coverageSettings} as JSON`)
301
+ }
302
+ overrideDirIfNotSet('coverageReportDir', settings['report-dir'])
303
+ overrideDirIfNotSet('coverageTempDir', settings['temp-dir'])
304
+ overrideIfNotSet('coverageReporters', settings.reporter)
305
+
306
+ checkAccess({ path: job.coverageRemoteScanner, label: 'coverage remote scanner', file: true })
307
+ }
308
+
309
+ if (job.mode === 'url') {
310
+ const port = job.port.toString()
311
+ job[$remoteOnLegacy] = job.url.every(url => {
312
+ // ignore host name since the machine might be exposed with any name
313
+ const parsedUrl = new URL(url)
314
+ return parsedUrl.port === port
315
+ })
316
+ }
317
+
318
+ job[$status] = 'Starting'
319
+ Object.defineProperty(job, 'status', {
320
+ get () {
321
+ return job[$status]
322
+ },
323
+ set (value) {
324
+ job[$status] = value
325
+ output.status(value)
326
+ },
327
+ enumerable: false,
328
+ configurable: false
329
+ })
330
+
331
+ /* istanbul ignore next */
332
+ if (process.env.DEBUG_ON_FAILED) {
333
+ let failed
334
+ Object.defineProperty(job, 'failed', {
335
+ get () {
336
+ return failed
337
+ },
338
+ set (value) {
339
+ if (value) {
340
+ // eslint-disable-next-line no-debugger
341
+ debugger
342
+ }
343
+ failed = value
344
+ },
345
+ enumerable: true,
346
+ configurable: false
347
+ })
348
+ }
349
+ }
350
+
351
+ function fromCmdLine (cwd, args) {
352
+ let job = parse(cwd, args)
353
+
354
+ let defaultPath = join(job.cwd, 'ui5-test-runner.json')
355
+ if (!isAbsolute(defaultPath)) {
356
+ defaultPath = join(job.initialCwd, defaultPath)
357
+ }
358
+ let hasDefaultSettings = false
359
+ try {
360
+ checkAccess({ path: defaultPath, file: true })
361
+ hasDefaultSettings = true
362
+ } catch (e) {
363
+ // ignore
364
+ }
365
+ if (hasDefaultSettings) {
366
+ const defaults = require(defaultPath)
367
+ const { before, after, browser } = buildArgs(defaults)
368
+ const sep = args.indexOf('--')
369
+ if (sep === -1) {
370
+ args = [...before, ...args, ...after, '--', ...browser]
371
+ } else {
372
+ args = [...before, ...args.slice(0, sep), ...after, '--', ...browser, ...args.slice(sep + 1)]
373
+ }
374
+ job = parse(cwd, args)
375
+ }
376
+
377
+ finalize(job)
378
+ return job
379
+ }
380
+
381
+ function fromObject (cwd, parameters) {
382
+ const { before, browser } = buildArgs(parameters)
383
+ if (browser.length) {
384
+ return fromCmdLine(cwd, [...before, '--', ...browser])
385
+ }
386
+ return fromCmdLine(cwd, [...before])
387
+ }
388
+
389
+ module.exports = {
390
+ getCommand,
391
+ fromCmdLine,
392
+ fromObject
393
+ }