ui5-test-runner 5.4.2 → 5.5.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/index.js CHANGED
@@ -12,7 +12,10 @@ const { preload } = require('./src/ui5')
12
12
  const { probe: probeBrowser } = require('./src/browsers')
13
13
  const { recreateDir, allocPromise } = require('./src/tools')
14
14
  const reserveConfigurationFactory = require('./src/reserve')
15
- const start = require('./src/start')
15
+ const { start } = require('./src/start')
16
+ const { executeIf } = require('./src/if')
17
+ const { batch } = require('./src/batch')
18
+ const { end } = require('./src/end')
16
19
 
17
20
  function send (message) {
18
21
  if (process.send) {
@@ -48,9 +51,33 @@ async function main () {
48
51
  output = getOutput(job)
49
52
  await recreateDir(job.reportDir)
50
53
  output.version()
54
+ if (job.batchMode) {
55
+ output.batchMode()
56
+ }
57
+ output.reportOnJobProgress()
51
58
  if (job.mode === 'capabilities') {
52
59
  return capabilities(job)
53
60
  }
61
+ if (job.if && !executeIf(job)) {
62
+ output.skipIf()
63
+ output.stop()
64
+ return
65
+ }
66
+
67
+ let startedCommand
68
+ if (job.startCommand) {
69
+ startedCommand = await start(job)
70
+ }
71
+
72
+ if (job.mode === 'batch') {
73
+ return await batch(job)
74
+ .finally(() => {
75
+ if (startedCommand) {
76
+ return startedCommand.stop()
77
+ }
78
+ })
79
+ }
80
+
54
81
  const configuration = await reserveConfigurationFactory(job)
55
82
  output.debug('reserve', 'configuration', configuration)
56
83
  const server = serve(configuration)
@@ -64,7 +91,6 @@ async function main () {
64
91
  job.port = port
65
92
  send({ msg: 'ready', port: job.port })
66
93
  output.serving(url)
67
- output.reportOnJobProgress()
68
94
  serverReady()
69
95
  })
70
96
  .on('error', error => {
@@ -73,11 +99,6 @@ async function main () {
73
99
  serverError()
74
100
  })
75
101
  await serverStarted
76
- let startedCommand
77
- if (job.start) {
78
- output.reportOnJobProgress()
79
- startedCommand = await start(job)
80
- }
81
102
  if (job.preload) {
82
103
  await preload(job)
83
104
  }
@@ -90,8 +111,8 @@ async function main () {
90
111
  if (job.watch) {
91
112
  delete job.start
92
113
  if (!job.watching) {
93
- output.watching(job.webapp)
94
- watch(job.webapp, { recursive: true }, async (eventType, filename) => {
114
+ output.watching(job.watchFolder)
115
+ watch(job.watchFolder, { recursive: true }, async (eventType, filename) => {
95
116
  output.changeDetected(eventType, filename)
96
117
  if (!job.start) {
97
118
  await recreateDir(job.reportDir)
@@ -106,12 +127,14 @@ async function main () {
106
127
  } else if (job.failed) {
107
128
  process.exitCode = -1
108
129
  }
130
+ if (job.endScript) {
131
+ await end(job)
132
+ }
109
133
  output.stop()
110
134
  await server.close()
111
135
  if (startedCommand) {
112
136
  await startedCommand.stop()
113
137
  }
114
- console.log('done ?')
115
138
  }
116
139
 
117
140
  main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "5.4.2",
3
+ "version": "5.5.0",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "test:coverall": "rimraf .nyc_output && jest --coverageDirectory .nyc_output --coverageReporters json && nyc --silent --no-clean npm run test:e2e && nyc merge .nyc_output .nyc_output/final/coverage.json && nyc report --temp-dir .nyc_output/final/ --report-dir coverage --branches 80 --functions 80 --lines 80 --statements 80",
18
18
  "test:unit": "jest",
19
19
  "test:unit:debug": "node --inspect node_modules/jest/bin/jest.js --runInBand --no-coverage",
20
- "test:e2e": "node test/e2e",
20
+ "test:e2e": "node . --batch test/e2e/.*\\.json --report-dir e2e --start \"node test/e2e/serve.js\" --start-wait-url http://localhost:8081 --start-wait-method HEAD --start-timeout 30s",
21
21
  "test:report": "node ./src/defaults/report.js ./test/report && reserve --config ./test/report/reserve.json",
22
22
  "test:text-report": "node ./src/defaults/text-report.js ./test/report",
23
23
  "build:doc": "node build/doc",
@@ -47,20 +47,20 @@
47
47
  "ps-tree": "^1.2.0",
48
48
  "punybind": "^1.2.1",
49
49
  "punyexpr": "^1.0.4",
50
- "reserve": "2.0.5"
50
+ "reserve": "2.1.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@openui5/types": "^1.132.1",
53
+ "@openui5/types": "^1.133.0",
54
54
  "@ui5/cli": "^4.0.13",
55
55
  "@ui5/middleware-code-coverage": "^2.0.1",
56
56
  "dotenv": "^16.4.7",
57
57
  "jest": "^29.7.0",
58
- "nock": "^13.5.6",
58
+ "nock": "^14.0.1",
59
59
  "nyc": "^17.1.0",
60
60
  "rimraf": "^6.0.1",
61
61
  "standard": "^17.1.2",
62
- "typescript": "^5.7.3",
63
- "ui5-tooling-transpile": "^3.5.3"
62
+ "typescript": "^5.8.2",
63
+ "ui5-tooling-transpile": "^3.7.1"
64
64
  },
65
65
  "optionalDependencies": {
66
66
  "fsevents": "^2.3.3"
@@ -77,7 +77,8 @@
77
77
  ],
78
78
  "globals": [
79
79
  "sap",
80
- "opaTest"
80
+ "opaTest",
81
+ "normalizePath"
81
82
  ]
82
83
  }
83
84
  }
package/src/batch.js ADDED
@@ -0,0 +1,196 @@
1
+ const { open, readdir, stat, unlink } = require('fs/promises')
2
+ const { extname, isAbsolute, join } = require('path')
3
+ const { allocPromise, filename } = require('./tools')
4
+ const { fork } = require('child_process')
5
+ const { interactive, getOutput, newProgress } = require('./output')
6
+ const { parallelize } = require('./parallelize')
7
+ const { $statusProgressCount } = require('./symbols')
8
+ const { $valueSources } = require('./symbols')
9
+ const { getCommand, toLongName } = require('./job')
10
+
11
+ const batchParameters = getCommand('.').options
12
+ .filter(option => option.description.includes('📡'))
13
+ .reduce((dictionary, option) => {
14
+ dictionary[option.long.substring(2)] = option
15
+ return dictionary
16
+ }, {})
17
+
18
+ const root = join(__dirname, '..')
19
+
20
+ const folder = (batchItems, job, folderPath) => {
21
+ getOutput(job).debug('batch', `adding folder: ${folderPath}`)
22
+ batchItems.push({
23
+ job,
24
+ path: folderPath,
25
+ id: filename(folderPath),
26
+ label: folderPath,
27
+ args: ['--cwd', folderPath]
28
+ })
29
+ }
30
+
31
+ const configurationFile = (batchItems, job, configurationFilePath) => {
32
+ getOutput(job).debug('batch', `adding configuration file: ${configurationFilePath}`)
33
+ try {
34
+ const {
35
+ batchId: id = filename(configurationFilePath),
36
+ batchLabel: label = configurationFilePath
37
+ } = require(configurationFilePath)
38
+ batchItems.push({
39
+ job,
40
+ path: configurationFilePath,
41
+ id,
42
+ label,
43
+ args: ['--config', configurationFilePath]
44
+ })
45
+ } catch (e) {
46
+ getOutput(job).batchFailed(configurationFilePath, 'invalid JSON configuration file')
47
+ }
48
+ }
49
+
50
+ const task = async ({ job, id, label, args }) => {
51
+ const output = getOutput(job)
52
+ const progress = newProgress(job)
53
+ const reportDir = join(job.reportDir, id)
54
+ progress.label = `${label} (${id})`
55
+ progress.count = 1
56
+ if (!interactive) {
57
+ output.log(`${label}...`)
58
+ }
59
+ const { promise, resolve, reject } = allocPromise()
60
+ const parameters = [
61
+ ...args,
62
+ '--batch-mode'
63
+ ]
64
+ if (job[$valueSources]) {
65
+ if (job[$valueSources].reportDir === 'cli') {
66
+ parameters.push('--report-dir', reportDir)
67
+ }
68
+ Object.keys(job[$valueSources])
69
+ .filter(name => job[$valueSources][name] === 'cli')
70
+ .forEach(name => {
71
+ const longName = toLongName(name)
72
+ const option = batchParameters[longName]
73
+ if (option) {
74
+ parameters.push(`--${longName}`)
75
+ if (!option.optional && !option.negate) {
76
+ if (option.variadic) {
77
+ parameters.push(...job[name].map(value => value.toString()))
78
+ } else {
79
+ parameters.push(job[name].toString())
80
+ }
81
+ }
82
+ }
83
+ })
84
+ }
85
+
86
+ const stdoutFilename = join(job.reportDir, `${id}.stdout.txt`)
87
+ const stdout = await open(stdoutFilename, 'w')
88
+ const stderrFilename = join(job.reportDir, `${id}.stderr.txt`)
89
+ const stderr = await open(stderrFilename, 'w')
90
+ const childProcess = fork(
91
+ join(root, 'index.js'),
92
+ parameters,
93
+ {
94
+ stdio: [0, stdout, stderr, 'ipc']
95
+ }
96
+ )
97
+ childProcess.on('message', data => {
98
+ if (data.type === 'progress') {
99
+ const { count, total } = data
100
+ progress.count = count
101
+ progress.total = total
102
+ }
103
+ })
104
+ childProcess.on('close', async code => {
105
+ await stdout.close()
106
+ await stderr.close()
107
+ if (code !== 0) {
108
+ reject(code)
109
+ } else {
110
+ await unlink(stdoutFilename)
111
+ await unlink(stderrFilename)
112
+ resolve()
113
+ }
114
+ })
115
+ return promise
116
+ .then(() => {
117
+ output.log('✔️ ', progress.label)
118
+ }, (reason) => {
119
+ ++job.errors
120
+ output.log('❌', progress.label, reason)
121
+ })
122
+ .finally(() => {
123
+ ++job[$statusProgressCount]
124
+ progress.done()
125
+ })
126
+ }
127
+
128
+ async function batch (job) {
129
+ /**
130
+ * job
131
+ * path: full path
132
+ * id: batchId | hash (using filename)
133
+ * label: batchLabel | configuration file path | path
134
+ * args: []
135
+ * --report-dir is always passed to aggregate reports under one root folder
136
+ */
137
+ const output = getOutput(job)
138
+ const batchItems = []
139
+ for (const batch of job.batch) {
140
+ output.debug('batch', `processing: ${batch}`)
141
+ // check if path
142
+ try {
143
+ let path = batch
144
+ if (!isAbsolute(path)) {
145
+ path = join(job.cwd, path)
146
+ }
147
+ const pathStat = await stat(path)
148
+ if (pathStat.isDirectory()) {
149
+ folder(batchItems, job, path)
150
+ } else if (pathStat.isFile() && extname(path) === '.json') {
151
+ configurationFile(batchItems, job, path)
152
+ } else {
153
+ output.batchFailed(batch, 'only folders and JSON configuration files are supported')
154
+ }
155
+ continue
156
+ } catch (e) {
157
+ // ignore
158
+ }
159
+ // Try using regular expression match
160
+ let re
161
+ try {
162
+ re = new RegExp(batch)
163
+ } catch (e) {
164
+ getOutput(job).batchFailed(batch, 'invalid regular expression')
165
+ continue
166
+ }
167
+ const scan = async (cwd) => {
168
+ const names = await readdir(cwd)
169
+ for (const name of names) {
170
+ const path = join(cwd, name)
171
+ const pathStat = await stat(path)
172
+ if (pathStat.isDirectory()) {
173
+ if (re.test(path) || re.test(path.replaceAll('\\', '/'))) {
174
+ folder(batchItems, job, path)
175
+ continue
176
+ }
177
+ await scan(path)
178
+ } else if (pathStat.isFile() && (re.test(path) || re.test(path.replaceAll('\\', '/')))) {
179
+ configurationFile(batchItems, job, path)
180
+ }
181
+ }
182
+ }
183
+ await scan(job.cwd)
184
+ }
185
+ if (batchItems.length) {
186
+ job.status = 'Running batch items...'
187
+ await parallelize(task, batchItems, job.parallel)
188
+ // TODO: end command ?
189
+ } else {
190
+ output.batchFailed(job.batch, 'no match')
191
+ }
192
+ output.stop()
193
+ return 0
194
+ }
195
+
196
+ module.exports = { batch, task }
@@ -8,7 +8,7 @@ const { performance } = require('perf_hooks')
8
8
  const { cleanDir, allocPromise, filename } = require('../tools')
9
9
  const { $statusProgressTotal, $statusProgressCount } = require('../symbols')
10
10
  const tests = require('./tests')
11
- const parallelize = require('../parallelize')
11
+ const { parallelize } = require('../parallelize')
12
12
 
13
13
  async function capabilities (job) {
14
14
  const output = getOutput(job)
@@ -30,16 +30,14 @@ async function capabilities (job) {
30
30
  }
31
31
 
32
32
  try {
33
- output.reportOnJobProgress()
34
- try {
35
- await probe(job)
36
- } catch (e) {
37
- output.error('Unable to probe')
38
- exit(-1)
39
- }
33
+ await probe(job)
34
+ } catch (e) {
35
+ output.error('Unable to probe')
36
+ exit(-1)
37
+ }
40
38
 
39
+ try {
41
40
  const listeners = []
42
-
43
41
  const configuration = await check({
44
42
  port: job.port,
45
43
  mappings: [
@@ -44,8 +44,8 @@ module.exports = [{
44
44
  }
45
45
  }, {
46
46
  label: 'Scripts (External QUnit)',
47
- for: capabilities => !!capabilities.scripts,
48
- url: 'https://ui5.sap.com/test-resources/sap/m/demokit/orderbrowser/webapp/test/unit/unitTests.qunit.html',
47
+ for: capabilities => !!capabilities.scripts && !capabilities.modules.includes('jsdom'),
48
+ url: 'https://ui5.sap.com/test-resources/sap/m/demokit/orderbrowser/webapp/test/Test.qunit.html?testsuite=test-resources/sap/ui/demo/orderbrowser/testsuite.qunit&test=unit/unitTests',
49
49
  scripts: ['post.js', 'qunit-hooks.js'],
50
50
  endpoint: qUnitEndpoints
51
51
  }, {
@@ -2,7 +2,7 @@ const { readFile, writeFile } = require('fs/promises')
2
2
  const { join } = require('path')
3
3
  const { Command, InvalidArgumentError } = require('commander')
4
4
  const { buildCsvWriter } = require('../csv-writer')
5
- const { any, boolean, integer } = require('../options')
5
+ const { any, arrayOf, boolean, integer, string } = require('../options')
6
6
 
7
7
  const noop = () => { }
8
8
 
@@ -51,7 +51,7 @@ module.exports = ({
51
51
  ]
52
52
  }
53
53
  if (option === 'language') {
54
- return [['-l, --language <lang...>', 'Language(s)', ['en-US']]]
54
+ return [['-l, --language <lang...>', 'Language(s) (see rfc5646)', arrayOf(string, true), ['en-US']]]
55
55
  }
56
56
  if (option === 'unsecure') {
57
57
  return [['-u, --unsecure', 'Disable security features', false]]
@@ -152,6 +152,7 @@ module.exports = ({
152
152
 
153
153
  options.chromeArgs = function () {
154
154
  const args = [
155
+ 'true', // Not sure why but this changes the behavior of the browser
155
156
  '--start-maximized',
156
157
  '--no-sandbox',
157
158
  '--disable-gpu',
@@ -74,7 +74,7 @@ async function main () {
74
74
  }
75
75
  }
76
76
  o('</testsuites>')
77
- await writeFile(join(reportDir, 'junit.xml'), output.join('\n'))
77
+ await writeFile(join(reportDir, process.env.JUNIT_XML_REPORT_FILENAME || 'junit.xml'), output.join('\n'))
78
78
  }
79
79
 
80
80
  main()
package/src/end.js ADDED
@@ -0,0 +1,58 @@
1
+ const { fork } = require('node:child_process')
2
+ const { join } = require('node:path')
3
+ const { getOutput } = require('./output')
4
+ const { allocPromise } = require('./tools')
5
+
6
+ async function end (job) {
7
+ const { endScript: end } = job
8
+ const output = getOutput(job)
9
+ const [script, ...args] = end.split(' ')
10
+
11
+ job.status = 'Executing end script'
12
+
13
+ output.debug('end', 'Starting script :', end)
14
+ const childProcess = fork(
15
+ script,
16
+ [...args, join(job.reportDir, 'job.js')],
17
+ {
18
+ cwd: job.cwd,
19
+ stdio: [0, 'pipe', 'pipe', 'ipc'],
20
+ windowsHide: true
21
+ }
22
+ )
23
+
24
+ const { promise: childProcessExit, resolve: childProcessExited } = allocPromise()
25
+ childProcess.on('close', childProcessExited)
26
+ output.monitor(childProcess)
27
+
28
+ job.status = 'Waiting for script to end'
29
+
30
+ if (job.endTimeout) {
31
+ let timedOut = false
32
+ const { promise: endTimeoutSignal, resolve: endTimeoutReached } = allocPromise()
33
+ const timeoutId = setTimeout(() => {
34
+ timedOut = true
35
+ endTimeoutReached()
36
+ }, job.endTimeout)
37
+
38
+ await Promise.race([
39
+ childProcessExit,
40
+ endTimeoutSignal
41
+ ])
42
+ clearTimeout(timeoutId)
43
+
44
+ if (timedOut) {
45
+ childProcess.kill()
46
+ throw new Error('Timeout while waiting for end script')
47
+ }
48
+ } else {
49
+ await childProcessExit
50
+ }
51
+
52
+ output.debug('end', 'Ended with exit code :', childProcess.exitCode)
53
+
54
+ // IMPORTANT : the end command CHANGES the exit code
55
+ process.exitCode = childProcess.exitCode
56
+ }
57
+
58
+ module.exports = { end }
package/src/if.js ADDED
@@ -0,0 +1,10 @@
1
+ const { punyexpr } = require('punyexpr')
2
+
3
+ function executeIf (job) {
4
+ return punyexpr(job.if)({
5
+ ...process.env,
6
+ NODE_MAJOR_VERSION: parseInt(parseInt(process.version.match(/v(\d+)\./)[1]))
7
+ })
8
+ }
9
+
10
+ module.exports = { executeIf }
package/src/job-mode.js CHANGED
@@ -17,6 +17,9 @@ function check (job, allowed, forbidden) {
17
17
  }
18
18
 
19
19
  function buildAndCheckMode (job) {
20
+ if (job.batch) {
21
+ return 'batch'
22
+ }
20
23
  if (job.capabilities) {
21
24
  check(job, [
22
25
  'capabilities',
@@ -33,8 +36,12 @@ function buildAndCheckMode (job) {
33
36
  'keepAlive',
34
37
  'alternateNpmPath',
35
38
  'outputInterval',
36
- 'screenshotTimeout'
37
- ], ['start'])
39
+ 'screenshotTimeout',
40
+ 'config',
41
+ 'batchMode',
42
+ 'batchId',
43
+ 'batchLabel'
44
+ ])
38
45
  return 'capabilities'
39
46
  }
40
47
  if (job.url && job.url.length) {
@@ -43,7 +50,6 @@ function buildAndCheckMode (job) {
43
50
  'libs',
44
51
  'mappings',
45
52
  'cache',
46
- 'watch',
47
53
  'testsuite'
48
54
  ])
49
55
  return 'url'
@@ -51,8 +57,7 @@ function buildAndCheckMode (job) {
51
57
  check(job, undefined, [
52
58
  'coverageProxy',
53
59
  'coverageProxyInclude',
54
- 'coverageProxyExclude',
55
- 'start'
60
+ 'coverageProxyExclude'
56
61
  ])
57
62
  return 'legacy'
58
63
  }
package/src/job.js CHANGED
@@ -93,68 +93,83 @@ function getCommand (cwd) {
93
93
  new Option('-c, --cwd <path>', '[💻🔗🧪] Set working directory')
94
94
  .default(cwd, 'current working directory')
95
95
  )
96
+ .option('--config <json>', '[💻🔗🧪] Configuration file (relative to cwd)', string, 'ui5-test-runner.json')
96
97
  .option('--port <port>', '[💻🔗🧪] Port to use (0 to use any free one)', integer, 0)
97
98
  .option('-r, --report-dir <path>', '[💻🔗🧪] Directory to output test reports (relative to cwd)', 'report')
98
- .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)
99
- .option('-f, --fail-fast [flag]', '[💻🔗🧪] Stop the execution after the first failing page', boolean, false)
100
- .option('-fo, --fail-opa-fast [flag]', '[💻🔗] Stop the OPA page execution after the first failing test', boolean, false)
99
+ .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)
100
+ .option('-f, --fail-fast [flag]', '[💻🔗🧪📡] Stop the execution after the first failing page', boolean, false)
101
+ .option('-fo, --fail-opa-fast [flag]', '[💻🔗📡] Stop the OPA page execution after the first failing test', boolean, false)
101
102
  .option('-k, --keep-alive [flag]', '[💻🔗🧪] Keep the server alive', boolean, false)
102
- .option('-l, --log-server [flag]', '[💻🔗🧪] Log inner server traces', boolean, false)
103
+ .option('-l, --log-server [flag]', '[💻🔗🧪📡] Log inner server traces', boolean, false)
103
104
  .option('-p, --parallel <count>', '[💻🔗🧪] Number of parallel tests executions', integer, 2)
104
- .option('-b, --browser <command>', '[💻🔗🧪] Browser instantiation command (relative to cwd or use $/ for provided ones)', '$/puppeteer.js')
105
- .option('--browser-args <argument...>', '[💻🔗🧪] Browser instantiation command parameters (use -- instead)')
106
- .option('--alternate-npm-path <path>', '[💻🔗] Alternate NPM path to look for packages (priority: local, alternate, global)')
107
- .option('--no-npm-install', '[💻🔗🧪] Prevent any NPM install (execution may fail if a dependency is missing)')
108
- .option('-bt, --browser-close-timeout <timeout>', '[💻🔗🧪] Maximum waiting time for browser close', timeout, 2000)
109
- .option('-br, --browser-retry <count>', '[💻🔗🧪] Browser instantiation retries : if the command fails unexpectedly, it is re-executed (0 means no retry)', 1)
110
- .option('-oi, --output-interval <interval>', '[💻🔗🧪] Interval for reporting progress on non interactive output (CI/CD) (0 means no output)', timeout, 30000)
111
- .option('--offline [flag]', '[💻🔗🧪] Limit network usage (implies --no-npm-install)', boolean, false)
105
+ .option('-b, --browser <command>', '[💻🔗🧪📡] Browser instantiation command (relative to cwd or use $/ for provided ones)', '$/puppeteer.js')
106
+ .option('--browser-args <argument...>', '[💻🔗🧪📡] Browser instantiation command parameters (use -- instead)')
107
+ .option('--alternate-npm-path <path>', '[💻🔗📡] Alternate NPM path to look for packages (priority: local, alternate, global)')
108
+ .option('--no-npm-install', '[💻🔗🧪📡] Prevent any NPM install (execution may fail if a dependency is missing)')
109
+ .option('-bt, --browser-close-timeout <timeout>', '[💻🔗🧪📡] Maximum waiting time for browser close', timeout, 2000)
110
+ .option('-br, --browser-retry <count>', '[💻🔗🧪📡] Browser instantiation retries : if the command fails unexpectedly, it is re-executed (0 means no retry)', 1)
111
+ .option('-oi, --output-interval <interval>', '[💻🔗🧪📡] Interval for reporting progress on non interactive output (CI/CD) (0 means no output)', timeout, 30000)
112
+ .option('--offline [flag]', '[💻🔗🧪📡] Limit network usage (implies --no-npm-install)', boolean, false)
112
113
 
113
114
  // Common to legacy and url
114
115
  .option('--webapp <path>', '[💻🔗] Base folder of the web application (relative to cwd)', 'webapp')
115
- .option('-pf, --page-filter <regexp>', '[💻🔗] Filter out pages not matching the regexp')
116
- .option('-pp, --page-params <params>', '[💻🔗] Add parameters to page URL')
117
- .option('--page-close-timeout <timeout>', '[💻🔗] Maximum waiting time for page close', timeout, 250)
118
- .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)
119
- .option('--screenshot [flag]', '[💻🔗] Take screenshots during the tests execution (if supported by the browser)', boolean, true)
120
- .option('--no-screenshot', '[💻🔗] Disable screenshots')
121
- .option('-st, --screenshot-timeout <timeout>', '[💻🔗] Maximum waiting time for browser screenshot', timeout, 5000)
122
- .option('-so, --split-opa [flag]', '[💻🔗] Split OPA tests using QUnit modules', boolean, false)
123
- .option('-rg, --report-generator <path...>', '[💻🔗] Report generator paths (relative to cwd or use $/ for provided ones)', ['$/report.js'])
124
- .option('--progress-page <path>', '[💻🔗] Progress page path (relative to cwd or use $/ for provided ones)', '$/report/default.html')
125
-
126
- .option('--coverage [flag]', '[💻🔗] Enable or disable code coverage', boolean)
127
- .option('--no-coverage', '[💻🔗] Disable code coverage')
128
- .option('-cs, --coverage-settings <path>', '[💻🔗] Path to a custom .nycrc.json file providing settings for instrumentation (relative to cwd or use $/ for provided ones)', '$/.nycrc.json')
116
+ .option('-pf, --page-filter <regexp>', '[💻🔗📡] Filter out pages not matching the regexp')
117
+ .option('-pp, --page-params <params>', '[💻🔗📡] Add parameters to page URL')
118
+ .option('--page-close-timeout <timeout>', '[💻🔗📡] Maximum waiting time for page close', timeout, 250)
119
+ .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)
120
+ .option('--screenshot [flag]', '[💻🔗📡] Take screenshots during the tests execution (if supported by the browser)', boolean, true)
121
+ .option('--no-screenshot', '[💻🔗📡] Disable screenshots during the tests execution (but not on failure, see --screenshot-on-failure)')
122
+ .option('--screenshot-on-failure <flag>', '[💻🔗📡] Take a screenshot when a test fails (even if --screenshot is false)', boolean, true)
123
+ .option('-st, --screenshot-timeout <timeout>', '[💻🔗📡] Maximum waiting time for browser screenshot', timeout, 5000)
124
+ .option('-so, --split-opa [flag]', '[💻🔗📡] Split OPA tests using QUnit modules', boolean, false)
125
+ .option('-rg, --report-generator <path...>', '[💻🔗📡] Report generator paths (relative to cwd or use $/ for provided ones)', ['$/report.js'])
126
+ .option('--progress-page <path>', '[💻🔗📡] Progress page path (relative to cwd or use $/ for provided ones)', '$/report/default.html')
127
+
128
+ .option('--coverage [flag]', '[💻🔗📡] Enable or disable code coverage', boolean)
129
+ .option('--no-coverage', '[💻🔗📡] Disable code coverage')
130
+ .option('-cs, --coverage-settings <path>', '[💻🔗📡] Path to a custom .nycrc.json file providing settings for instrumentation (relative to cwd or use $/ for provided ones)', '$/.nycrc.json')
129
131
  .option('-ctd, --coverage-temp-dir <path>', '[💻🔗] Directory to output raw coverage information to (relative to cwd)', '.nyc_output')
130
132
  .option('-crd, --coverage-report-dir <path>', '[💻🔗] Directory to store the coverage report files (relative to cwd)', 'coverage')
131
- .option('-cr, --coverage-reporters <reporter...>', '[💻🔗] List of nyc reporters to use (text is always used)', ['lcov', 'cobertura'])
132
- .option('-ccb, --coverage-check-branches <percent>', '[💻🔗] What % of branches must be covered', percent, 0)
133
- .option('-ccf, --coverage-check-functions <percent>', '[💻🔗] What % of functions must be covered', percent, 0)
134
- .option('-ccl, --coverage-check-lines <percent>', '[💻🔗] What % of lines must be covered', percent, 0)
135
- .option('-ccs, --coverage-check-statements <percent>', '[💻🔗] What % of statements must be covered', percent, 0)
136
- .option('-crs, --coverage-remote-scanner <path>', '[💻🔗] Scan for files when all coverage is requested', '$/scan-ui5.js')
133
+ .option('-cr, --coverage-reporters <reporter...>', '[💻🔗📡] List of nyc reporters to use (text is always used)', ['lcov', 'cobertura'])
134
+ .option('-ccb, --coverage-check-branches <percent>', '[💻🔗📡] What % of branches must be covered', percent, 0)
135
+ .option('-ccf, --coverage-check-functions <percent>', '[💻🔗📡] What % of functions must be covered', percent, 0)
136
+ .option('-ccl, --coverage-check-lines <percent>', '[💻🔗📡] What % of lines must be covered', percent, 0)
137
+ .option('-ccs, --coverage-check-statements <percent>', '[💻🔗📡] What % of statements must be covered', percent, 0)
138
+ .option('-crs, --coverage-remote-scanner <path>', '[💻🔗📡] Scan for files when all coverage is requested', '$/scan-ui5.js')
137
139
  .option('-s, --serve-only [flag]', '[💻🔗] Serve only', boolean, false)
138
140
 
141
+ .option('-w, --watch [flag]', '[💻🔗] Monitor the webapp folder (or the one specified with --watch-folder) and re-execute tests on change', boolean, false)
142
+ .option('--watch-folder <path>', '[💻🔗] Folder to monitor with watch (enables --watch if not specified)', string)
143
+
144
+ .option('--start <command>', '[💻🔗] Start command (might be an NPM script or a shell command)', string)
145
+ .option('--start-wait-url <command>', '[💻🔗] URL to wait for (🔗 defaulted to first url)', url)
146
+ .option('--start-wait-method <method>', '[💻🔗] HTTP method to check the waited URL', 'GET')
147
+ .option('--start-timeout <timeout>', '[💻🔗] Maximum waiting time for the start command (based on when the first URL becomes available)', timeout, 5000)
148
+
149
+ .option('--end <script>', '[💻🔗] End script (will receive path to `job.js`)', string)
150
+ .option('--end-timeout <timeout>', '[💻🔗] Maximum waiting time for the end script', timeout, 5000)
151
+
139
152
  // Specific to legacy (and might be used with url if pointing to local project)
140
- .option('--ui5 <url>', '[💻] UI5 url', url, 'https://ui5.sap.com')
141
- .option('--disable-ui5 [flag]', '[💻] Disable UI5 mapping (also disable libs)', boolean, false)
142
- .option('--libs <lib...>', '[💻] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
143
- .option('--mappings <mapping...>', '[💻] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
144
- .option('--cache <path>', '[💻] Cache UI5 resources locally in the given folder (empty to disable)')
145
- .option('--preload <library...>', '[💻] Preload UI5 libraries in the cache folder (only if --cache is used)', arrayOf(string))
153
+ .option('--ui5 <url>', '[💻📡] UI5 url', url, 'https://ui5.sap.com')
154
+ .option('--disable-ui5 [flag]', '[💻📡] Disable UI5 mapping (also disable libs)', boolean, false)
155
+ .option('--libs <lib...>', '[💻📡] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
156
+ .option('--mappings <mapping...>', '[💻📡] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
157
+ .option('--cache <path>', '[💻📡] Cache UI5 resources locally in the given folder (empty to disable)')
158
+ .option('--preload <library...>', '[💻📡] Preload UI5 libraries in the cache folder (only if --cache is used)', arrayOf(string))
146
159
  .option('--testsuite <path>', '[💻] Path of the testsuite file (relative to webapp, URL parameters are supported)', 'test/testsuite.qunit.html')
147
- .option('-w, --watch [flag]', '[💻] Monitor the webapp folder and re-execute tests on change', boolean, false)
148
-
149
- // Specific to url
150
- .option('--start <command>', '[🔗] Start command (might be an NPM script or a shell command)', string)
151
- .option('--start-timeout <timeout>', '[🔗] Maximum waiting time for the start command (based on when the first URL becomes available)', timeout, 5000)
152
160
 
153
161
  // Specific to coverage in url mode (experimental)
154
162
  .option('-cp, --coverage-proxy [flag]', `[🔗] ${EXPERIMENTAL_OPTION} use internal proxy to instrument remote files`, boolean, false)
155
163
  .option('-cpi, --coverage-proxy-include <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to instrument for coverage`, regex, '.*')
156
164
  .option('-cpe, --coverage-proxy-exclude <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to ignore for coverage`, regex, '/((test-)?resources|tests?)/')
157
165
 
166
+ // Batch mode related
167
+ .addOption(new Option('--batch-mode', 'Changes the way options are defaulted (in particular coverage temporary folders)', boolean).hideHelp())
168
+ .option('--batch <specification...>', 'Batch specification', arrayOf(string))
169
+ .option('--batch-id <id>', 'Batch id (used for naming report folder)', string)
170
+ .option('--batch-label <label>', 'Batch label (used while reporting on execution)', string)
171
+ .option('--if <condition>', 'Condition runner execution', string)
172
+
158
173
  .addOption(new Option('--debug-dev-mode', DEBUG_OPTION, boolean).hideHelp())
159
174
  .addOption(new Option('--debug-probe-only', DEBUG_OPTION, boolean).hideHelp())
160
175
  .addOption(new Option('--debug-keep-browser-open', DEBUG_OPTION, boolean).hideHelp())
@@ -211,6 +226,7 @@ function checkAccess ({ path, label, file /*, write */ }) {
211
226
 
212
227
  function finalize (job) {
213
228
  function toAbsolute (path, from = job.cwd) {
229
+ path = path.replace(/📂report\b/, job.reportDir)
214
230
  if (!isAbsolute(path)) {
215
231
  path = join(from, path)
216
232
  }
@@ -230,6 +246,7 @@ function finalize (job) {
230
246
  function updateToAbsolute (member, from = job.cwd) {
231
247
  job[member] = toAbsolute(job[member], from)
232
248
  }
249
+
233
250
  'browser,coverageSettings,coverageRemoteScanner,progressPage'
234
251
  .split(',')
235
252
  .forEach(setting => { job[setting] = checkDefault(job[setting]) })
@@ -282,6 +299,16 @@ function finalize (job) {
282
299
  })
283
300
  }
284
301
 
302
+ if (job.watchFolder) {
303
+ job.watch = true
304
+ job.watchFolder = updateToAbsolute(job.watchFolder)
305
+ } else if (job.watch) {
306
+ job.watchFolder = job.webapp
307
+ }
308
+ if (job.watchFolder) {
309
+ checkAccess({ path: job.watchFolder, label: 'Folder to watch' })
310
+ }
311
+
285
312
  const output = getOutput(job)
286
313
 
287
314
  if (job.coverage) {
@@ -336,6 +363,25 @@ function finalize (job) {
336
363
  configurable: false
337
364
  })
338
365
 
366
+ // Because start and end are already used
367
+ job.startCommand = job.start
368
+ delete job.start
369
+ job.endScript = job.end
370
+ delete job.end
371
+
372
+ if (job.startCommand) {
373
+ if (!job.startWaitUrl) {
374
+ job.startWaitUrl = job.url[0]
375
+ }
376
+ if (!job.startWaitUrl) {
377
+ throw new Error('Start command defined but no URL to wait for')
378
+ }
379
+ }
380
+
381
+ if (job.batchMode) {
382
+ job.outputInterval = 1000
383
+ }
384
+
339
385
  /* istanbul ignore next */
340
386
  if (process.env.DEBUG_ON_FAILED) {
341
387
  let failed
@@ -359,19 +405,34 @@ function finalize (job) {
359
405
  function fromCmdLine (cwd, args) {
360
406
  let job = parse(cwd, args)
361
407
 
362
- let defaultPath = join(job.cwd, 'ui5-test-runner.json')
363
- if (!isAbsolute(defaultPath)) {
364
- defaultPath = join(job.initialCwd, defaultPath)
408
+ let defaultPath
409
+ const isConfigSet = job[$valueSources].config === 'cli'
410
+ if (isAbsolute(job.config)) {
411
+ defaultPath = job.config
412
+ } else {
413
+ defaultPath = join(job.cwd, job.config)
414
+ if (!isAbsolute(defaultPath)) {
415
+ defaultPath = join(job.initialCwd, defaultPath)
416
+ }
365
417
  }
366
418
  let hasDefaultSettings = false
367
419
  try {
368
420
  checkAccess({ path: defaultPath, file: true })
369
421
  hasDefaultSettings = true
370
422
  } catch (e) {
423
+ if (isConfigSet) {
424
+ throw e
425
+ }
371
426
  // ignore
372
427
  }
373
428
  if (hasDefaultSettings) {
374
429
  const defaults = require(defaultPath)
430
+ if (defaults.cwd && !isAbsolute(defaults.cwd)) {
431
+ // make it relative to the configuration file
432
+ defaults.cwd = join(dirname(defaultPath), defaults.cwd)
433
+ } else if (isConfigSet) {
434
+ defaults.cwd = dirname(defaultPath)
435
+ }
375
436
  const { before, after, browser } = buildArgs(defaults)
376
437
  const sep = args.indexOf('--')
377
438
  if (sep === -1) {
@@ -397,5 +458,6 @@ function fromObject (cwd, parameters) {
397
458
  module.exports = {
398
459
  getCommand,
399
460
  fromCmdLine,
400
- fromObject
461
+ fromObject,
462
+ toLongName
401
463
  }
package/src/options.js CHANGED
@@ -70,10 +70,12 @@ module.exports = {
70
70
  return value
71
71
  },
72
72
 
73
- arrayOf (typeValidator) {
73
+ arrayOf (typeValidator, overrideDefault) {
74
+ let count = 0
74
75
  return function (value, previousValue) {
76
+ ++count
75
77
  let result
76
- if (previousValue === undefined) {
78
+ if (previousValue === undefined || (overrideDefault && count === 1)) {
77
79
  result = []
78
80
  } else {
79
81
  result = [...previousValue]
package/src/output.js CHANGED
@@ -427,6 +427,18 @@ function build (job) {
427
427
  browserIssue(job, { type: 'failed', url, code, dir })
428
428
  }),
429
429
 
430
+ skipIf: wrap(() => {
431
+ log(job, p80()`⚠️ [SKIPIF] Skipping execution (--if)`)
432
+ }),
433
+
434
+ batchFailed: wrap((batch, reason) => {
435
+ log(job, p80()`⚠️ [BATCHF] Failed to resolve batch ${batch}: ${reason}`)
436
+ }),
437
+
438
+ batchMode: wrap((batch, reason) => {
439
+ log(job, p80()`⚠️ [BATCHM] Batch mode item execution`)
440
+ }),
441
+
430
442
  startFailed: wrap((url, error) => {
431
443
  const p = p80()
432
444
  log(job, p`┌──────────${pad.x('─')}┐`)
@@ -492,6 +504,10 @@ function build (job) {
492
504
  log(job, p80()`⚠️ [SKPNYC] Skipping nyc instrumentation (--url)`)
493
505
  }),
494
506
 
507
+ coverageNotFound: wrap(() => {
508
+ log(job, p80()`⚠️ [COVMIS] Coverage missing`)
509
+ }),
510
+
495
511
  assumingOneOrigin: wrap(() => {
496
512
  log(job, p80()`⚠️ [COVORG] Considering only one origin`)
497
513
  }),
@@ -34,7 +34,7 @@ async function run (task) {
34
34
  task.stop = true
35
35
  reject(error)
36
36
  }
37
- let remaining = list.length - index - 1
37
+ let remaining = list.length - task.started
38
38
  while (task.active < (parallel + 1) && remaining) {
39
39
  --remaining
40
40
  ++task.active
@@ -43,7 +43,7 @@ async function run (task) {
43
43
  complete(task)
44
44
  }
45
45
 
46
- module.exports = function parallelize (method, list, parallel) {
46
+ function parallelize (method, list, parallel) {
47
47
  const { promise, resolve, reject } = allocPromise()
48
48
  const task = {
49
49
  method,
@@ -59,3 +59,5 @@ module.exports = function parallelize (method, list, parallel) {
59
59
  run(task)
60
60
  return promise
61
61
  }
62
+
63
+ module.exports = { parallelize }
@@ -102,6 +102,8 @@ async function done (job, urlWithHash, report) {
102
102
  if (report.__coverage__) {
103
103
  await collect(job, url, report.__coverage__)
104
104
  delete report.__coverage__
105
+ } else if (job.coverage) {
106
+ getOutput(job).coverageNotFound()
105
107
  }
106
108
  page.report = report
107
109
  stop(job, url)
@@ -177,7 +179,7 @@ module.exports = {
177
179
  }
178
180
  ++progress.count
179
181
  if (failed) {
180
- if (job.browserCapabilities.screenshot) {
182
+ if (job.browserCapabilities.screenshot && job.screenshotOnFailure) {
181
183
  try {
182
184
  const absoluteName = await screenshot(job, url, testId)
183
185
  test.screenshot = basename(absoluteName)
package/src/report.js CHANGED
@@ -56,6 +56,6 @@ module.exports = {
56
56
  job.failed = true
57
57
  }))
58
58
  await Promise.all(promises)
59
- job.status = 'Done'
59
+ job.status = 'Reports gemerated'
60
60
  }
61
61
  }
package/src/start.js CHANGED
@@ -6,29 +6,34 @@ const psTreeNodeCb = require('ps-tree')
6
6
  const { promisify } = require('util')
7
7
  const psTree = promisify(psTreeNodeCb)
8
8
 
9
- const $startedProcess = Symbol('startedProcess')
10
-
11
- module.exports = async function start (job) {
12
- let { start } = job
9
+ async function start (job) {
10
+ const { startWaitUrl: url, startWaitMethod: method } = job
11
+ let { startCommand: start } = job
13
12
  const output = getOutput(job)
14
- const [command] = start.split(' ')
13
+ const [command, ...parameters] = start.split(' ')
15
14
 
16
15
  job.status = 'Executing start command'
17
16
 
18
- // check if existing NPM script
19
- const packagePath = join(job.cwd, 'package.json')
20
- try {
21
- const packageStat = await stat(packagePath)
22
- if (packageStat.isFile()) {
23
- output.debug('start', 'Found package.json in cwd')
24
- const packageFile = JSON.parse(await readFile(packagePath, 'utf-8'))
25
- if (packageFile.scripts[command]) {
26
- output.debug('start', 'Found matching start script in package.json')
27
- start = `npm run ${start}`
17
+ // check if node
18
+ if (command === 'node') {
19
+ output.debug('start', `Replacing node with ${process.argv[0]}`)
20
+ start = [process.argv[0], ...parameters].join(' ')
21
+ } else {
22
+ // check if existing NPM script
23
+ const packagePath = join(job.cwd, 'package.json')
24
+ try {
25
+ const packageStat = await stat(packagePath)
26
+ if (packageStat.isFile()) {
27
+ output.debug('start', 'Found package.json in cwd')
28
+ const packageFile = JSON.parse(await readFile(packagePath, 'utf-8'))
29
+ if (packageFile.scripts[command]) {
30
+ output.debug('start', 'Found matching start script in package.json')
31
+ start = `npm run ${start}`
32
+ }
28
33
  }
34
+ } catch (e) {
35
+ output.debug('start', 'Missing or invalid package.json in cwd', e)
29
36
  }
30
- } catch (e) {
31
- output.debug('start', 'Missing or invalid package.json in cwd', e)
32
37
  }
33
38
 
34
39
  let childProcessExited = false
@@ -42,17 +47,14 @@ module.exports = async function start (job) {
42
47
  childProcessExited = true
43
48
  })
44
49
  output.monitor(childProcess)
45
- job[$startedProcess] = childProcess
46
50
 
47
51
  job.status = 'Waiting for URL to be reachable'
48
52
 
49
- const [url] = job.url
50
-
51
53
  const begin = Date.now()
52
54
  // eslint-disable-next-line no-unmodified-loop-condition
53
55
  while (!childProcessExited && Date.now() - begin <= job.startTimeout) {
54
56
  try {
55
- const response = await fetch(url)
57
+ const response = await fetch(url, { method })
56
58
  output.debug('start', url, response.status)
57
59
  if (response.status === 200) {
58
60
  break
@@ -89,3 +91,5 @@ module.exports = async function start (job) {
89
91
  stop
90
92
  }
91
93
  }
94
+
95
+ module.exports = { start }
package/src/tests.js CHANGED
@@ -11,7 +11,7 @@ const {
11
11
  $proxifiedUrls
12
12
  } = require('./symbols')
13
13
  const { UTRError } = require('./error')
14
- const parallelize = require('./parallelize')
14
+ const { parallelize } = require('./parallelize')
15
15
 
16
16
  function task (job, method) {
17
17
  return async (url, index, { length }) => {
@@ -38,6 +38,11 @@ function task (job, method) {
38
38
  }
39
39
 
40
40
  async function probeUrl (job, url) {
41
+ const parsedUrl = new URL(url)
42
+ if (parsedUrl.port === '0') {
43
+ parsedUrl.port = job.port
44
+ url = parsedUrl.toString()
45
+ }
41
46
  const output = getOutput(job)
42
47
  try {
43
48
  let scripts
@@ -122,6 +127,8 @@ async function process (job) {
122
127
  }
123
128
 
124
129
  await generate(job)
130
+
131
+ // TODO: #105 integrate end, find a way it can change job.failed
125
132
  }
126
133
 
127
134
  module.exports = {
package/src/ui5.js CHANGED
@@ -7,7 +7,7 @@ const { capture } = require('reserve')
7
7
  const { getOutput, newProgress } = require('./output')
8
8
  const { download } = require('./tools')
9
9
  const { $statusProgressCount, $statusProgressTotal } = require('./symbols')
10
- const parallelize = require('./parallelize')
10
+ const { parallelize } = require('./parallelize')
11
11
 
12
12
  const buildCacheBase = job => {
13
13
  const [, hostName] = /https?:\/\/([^/]*)/.exec(job.ui5)