ui5-test-runner 5.3.7 → 5.4.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/README.md CHANGED
@@ -48,3 +48,4 @@ A self-sufficient test runner for UI5 applications enabling parallel execution o
48
48
 
49
49
  * [Marian Zeis](https://github.com/marianfoo): Documentation page revamp [PR #54](https://github.com/ArnaudBuchholz/ui5-test-runner/pull/54)
50
50
  * [Raj Singh](https://github.com/rajxsingh): Basic HTTP Authentication in Puppeteer [PR #71](https://github.com/ArnaudBuchholz/ui5-test-runner/pull/71)
51
+ * [Andreas Kunz](https://github.com/akudev): Improved documentation for TypeScript testing and reviewed `nyc` settings handling [PR #110](https://github.com/ArnaudBuchholz/ui5-test-runner/pull/110)
package/index.js CHANGED
@@ -3,13 +3,16 @@
3
3
  'use strict'
4
4
 
5
5
  const { serve } = require('reserve')
6
+ const { watch } = require('fs')
7
+ const { capabilities } = require('./src/capabilities')
8
+ const { execute } = require('./src/tests')
6
9
  const { fromCmdLine } = require('./src/job')
7
10
  const { getOutput } = require('./src/output')
11
+ const { preload } = require('./src/ui5')
12
+ const { probe: probeBrowser } = require('./src/browsers')
13
+ const { recreateDir, allocPromise } = require('./src/tools')
8
14
  const reserveConfigurationFactory = require('./src/reserve')
9
- const { execute } = require('./src/tests')
10
- const { capabilities } = require('./src/capabilities')
11
- const { watch } = require('fs')
12
- const { recreateDir } = require('./src/tools')
15
+ const start = require('./src/start')
13
16
 
14
17
  function send (message) {
15
18
  if (process.send) {
@@ -20,7 +23,7 @@ function send (message) {
20
23
  }
21
24
  }
22
25
 
23
- async function notifyAndExecuteTests (job, output) {
26
+ async function notifyAndExecuteTests (job) {
24
27
  send({ msg: 'begin' })
25
28
  try {
26
29
  await execute(job)
@@ -44,8 +47,8 @@ async function main () {
44
47
  job = fromCmdLine(process.cwd(), process.argv.slice(2))
45
48
  output = getOutput(job)
46
49
  await recreateDir(job.reportDir)
50
+ output.version()
47
51
  if (job.mode === 'capabilities') {
48
- output.version()
49
52
  return capabilities(job)
50
53
  }
51
54
  const configuration = await reserveConfigurationFactory(job)
@@ -54,43 +57,61 @@ async function main () {
54
57
  if (job.logServer) {
55
58
  server.on('redirected', output.redirected)
56
59
  }
60
+
61
+ const { promise: serverStarted, resolve: serverReady, reject: serverError } = allocPromise()
57
62
  server
58
63
  .on('ready', async ({ url, port }) => {
59
64
  job.port = port
60
65
  send({ msg: 'ready', port: job.port })
61
66
  output.serving(url)
62
67
  output.reportOnJobProgress()
63
- if (job.serveOnly) {
64
- job.status = 'Serving'
65
- return
66
- }
67
- await notifyAndExecuteTests(job)
68
- if (job.watch) {
69
- delete job.start
70
- if (!job.watching) {
71
- output.watching(job.webapp)
72
- watch(job.webapp, { recursive: true }, (eventType, filename) => {
73
- output.changeDetected(eventType, filename)
74
- if (!job.start) {
75
- notifyAndExecuteTests(job)
76
- }
77
- })
78
- job.watching = true
79
- }
80
- } else if (job.keepAlive) {
81
- job.status = 'Serving'
82
- } else if (job.failed) {
83
- output.stop()
84
- process.exit(-1)
85
- } else {
86
- output.stop()
87
- process.exit(0)
88
- }
68
+ serverReady()
89
69
  })
90
70
  .on('error', error => {
91
71
  output.serverError(error)
92
72
  send({ msg: 'error', error })
73
+ serverError()
93
74
  })
75
+ await serverStarted
76
+ let startedCommand
77
+ if (job.start) {
78
+ output.reportOnJobProgress()
79
+ startedCommand = await start(job)
80
+ }
81
+ if (job.preload) {
82
+ await preload(job)
83
+ }
84
+ if (job.serveOnly) {
85
+ job.status = 'Serving'
86
+ return
87
+ }
88
+ await probeBrowser(job)
89
+ await notifyAndExecuteTests(job)
90
+ if (job.watch) {
91
+ delete job.start
92
+ if (!job.watching) {
93
+ output.watching(job.webapp)
94
+ watch(job.webapp, { recursive: true }, async (eventType, filename) => {
95
+ output.changeDetected(eventType, filename)
96
+ if (!job.start) {
97
+ await recreateDir(job.reportDir)
98
+ notifyAndExecuteTests(job)
99
+ }
100
+ })
101
+ job.watching = true
102
+ }
103
+ } else if (job.keepAlive) {
104
+ job.status = 'Serving'
105
+ return
106
+ } else if (job.failed) {
107
+ process.exitCode = -1
108
+ }
109
+ output.stop()
110
+ await server.close()
111
+ if (startedCommand) {
112
+ await startedCommand.stop()
113
+ }
114
+ console.log('done ?')
94
115
  }
95
116
 
96
117
  main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "5.3.7",
3
+ "version": "5.4.0",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -44,23 +44,23 @@
44
44
  "homepage": "https://github.com/ArnaudBuchholz/ui5-test-runner#readme",
45
45
  "dependencies": {
46
46
  "commander": "^12.1.0",
47
+ "ps-tree": "^1.2.0",
47
48
  "punybind": "^1.2.1",
48
49
  "punyexpr": "^1.0.4",
49
50
  "reserve": "2.0.5"
50
51
  },
51
52
  "devDependencies": {
52
- "@openui5/types": "^1.130.1",
53
- "@ui5/cli": "^4.0.11",
53
+ "@openui5/types": "^1.132.0",
54
+ "@ui5/cli": "^4.0.12",
54
55
  "@ui5/middleware-code-coverage": "^2.0.1",
55
- "dotenv": "^16.4.5",
56
+ "dotenv": "^16.4.7",
56
57
  "jest": "^29.7.0",
57
58
  "nock": "^13.5.6",
58
59
  "nyc": "^17.1.0",
59
60
  "rimraf": "^6.0.1",
60
61
  "standard": "^17.1.2",
61
- "start-server-and-test": "^2.0.8",
62
- "typescript": "^5.7.2",
63
- "ui5-tooling-transpile": "^3.5.1"
62
+ "typescript": "^5.7.3",
63
+ "ui5-tooling-transpile": "^3.5.3"
64
64
  },
65
65
  "optionalDependencies": {
66
66
  "fsevents": "^2.3.3"
package/src/coverage.js CHANGED
@@ -60,7 +60,7 @@ const customFileSystem = {
60
60
 
61
61
  async function instrument (job) {
62
62
  await setupNyc(job)
63
- job[$nycSettingsPath] = join(job.coverageTempDir, 'settings/nyc.json')
63
+ job[$nycSettingsPath] = join(job.coverageTempDir, 'settings/.nycrc.json')
64
64
  await cleanDir(job.coverageTempDir)
65
65
  await createDir(join(job.coverageTempDir, 'settings'))
66
66
  const settings = JSON.parse((await readFile(job.coverageSettings)).toString())
@@ -40,11 +40,11 @@
40
40
  details.isOpa = isOpa()
41
41
  return post('QUnit/begin', details)
42
42
  })
43
-
43
+
44
44
  QUnit.testStart(function (details) {
45
45
  return post('QUnit/testStart', extend(details))
46
46
  })
47
-
47
+
48
48
  QUnit.log(function (log) {
49
49
  let ready = false
50
50
  post('QUnit/log', extend(log))
@@ -64,11 +64,11 @@
64
64
  })
65
65
  }
66
66
  })
67
-
67
+
68
68
  QUnit.testDone(function (report) {
69
69
  return post('QUnit/testDone', report)
70
70
  })
71
-
71
+
72
72
  QUnit.done(function (report) {
73
73
  if (window.__coverage__) {
74
74
  report.__coverage__ = window.__coverage__
@@ -78,7 +78,7 @@
78
78
  }
79
79
 
80
80
  if (typeof window.QUnit !== 'undefined' && QUnit.begin) {
81
- installQUnitHooks();
81
+ installQUnitHooks()
82
82
  } else {
83
83
  let QUnit
84
84
  let install = true
@@ -87,7 +87,7 @@
87
87
  get: function () {
88
88
  return QUnit
89
89
  },
90
-
90
+
91
91
  set: function (value) {
92
92
  QUnit = value
93
93
  if (QUnit && QUnit.begin && install) {
@@ -97,5 +97,4 @@
97
97
  }
98
98
  })
99
99
  }
100
-
101
100
  }())
package/src/job-mode.js CHANGED
@@ -33,7 +33,7 @@ function buildAndCheckMode (job) {
33
33
  'keepAlive',
34
34
  'alternateNpmPath',
35
35
  'outputInterval'
36
- ])
36
+ ], ['start'])
37
37
  return 'capabilities'
38
38
  }
39
39
  if (job.url && job.url.length) {
@@ -50,7 +50,8 @@ function buildAndCheckMode (job) {
50
50
  check(job, undefined, [
51
51
  'coverageProxy',
52
52
  'coverageProxyInclude',
53
- 'coverageProxyExclude'
53
+ 'coverageProxyExclude',
54
+ 'start'
54
55
  ])
55
56
  return 'legacy'
56
57
  }
package/src/job.js CHANGED
@@ -103,11 +103,11 @@ function getCommand (cwd) {
103
103
  .option('-b, --browser <command>', '[💻🔗🧪] Browser instantiation command (relative to cwd or use $/ for provided ones)', '$/puppeteer.js')
104
104
  .option('--browser-args <argument...>', '[💻🔗🧪] Browser instantiation command parameters (use -- instead)')
105
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)')
106
+ .option('--no-npm-install [flag]', '[💻🔗🧪] Prevent any NPM install (execution may fail if a dependency is missing)', boolean)
107
107
  .option('-bt, --browser-close-timeout <timeout>', '[💻🔗🧪] Maximum waiting time for browser close', timeout, 2000)
108
108
  .option('-br, --browser-retry <count>', '[💻🔗🧪] Browser instantiation retries : if the command fails unexpectedly, it is re-executed (0 means no retry)', 1)
109
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)
110
+ .option('--offline [flag]', '[💻🔗🧪] Limit network usage (implies --no-npm-install)', boolean, false)
111
111
 
112
112
  // Common to legacy and url
113
113
  .option('--webapp <path>', '[💻🔗] Base folder of the web application (relative to cwd)', 'webapp')
@@ -116,15 +116,15 @@ function getCommand (cwd) {
116
116
  .option('--page-close-timeout <timeout>', '[💻🔗] Maximum waiting time for page close', timeout, 250)
117
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
118
  .option('--screenshot [flag]', '[💻🔗] Take screenshots during the tests execution (if supported by the browser)', boolean, true)
119
- .option('--no-screenshot', '[💻🔗] Disable screenshots')
119
+ .option('--no-screenshot [flag]', '[💻🔗] Disable screenshots', boolean)
120
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)
121
+ .option('-so, --split-opa [flag]', '[💻🔗] Split OPA tests using QUnit modules', boolean, false)
122
122
  .option('-rg, --report-generator <path...>', '[💻🔗] Report generator paths (relative to cwd or use $/ for provided ones)', ['$/report.js'])
123
123
  .option('--progress-page <path>', '[💻🔗] Progress page path (relative to cwd or use $/ for provided ones)', '$/report/default.html')
124
124
 
125
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')
126
+ .option('--no-coverage [flag]', '[💻🔗] Disable code coverage', boolean)
127
+ .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')
128
128
  .option('-ctd, --coverage-temp-dir <path>', '[💻🔗] Directory to output raw coverage information to (relative to cwd)', '.nyc_output')
129
129
  .option('-crd, --coverage-report-dir <path>', '[💻🔗] Directory to store the coverage report files (relative to cwd)', 'coverage')
130
130
  .option('-cr, --coverage-reporters <reporter...>', '[💻🔗] List of nyc reporters to use (text is always used)', ['lcov', 'cobertura'])
@@ -137,7 +137,7 @@ function getCommand (cwd) {
137
137
 
138
138
  // Specific to legacy (and might be used with url if pointing to local project)
139
139
  .option('--ui5 <url>', '[💻] UI5 url', url, 'https://ui5.sap.com')
140
- .option('--disable-ui5', '[💻] Disable UI5 mapping (also disable libs)', boolean, false)
140
+ .option('--disable-ui5 [flag]', '[💻] Disable UI5 mapping (also disable libs)', boolean, false)
141
141
  .option('--libs <lib...>', '[💻] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
142
142
  .option('--mappings <mapping...>', '[💻] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
143
143
  .option('--cache <path>', '[💻] Cache UI5 resources locally in the given folder (empty to disable)')
@@ -145,6 +145,10 @@ function getCommand (cwd) {
145
145
  .option('--testsuite <path>', '[💻] Path of the testsuite file (relative to webapp, URL parameters are supported)', 'test/testsuite.qunit.html')
146
146
  .option('-w, --watch [flag]', '[💻] Monitor the webapp folder and re-execute tests on change', boolean, false)
147
147
 
148
+ // Specific to url
149
+ .option('--start <command>', '[🔗] Start command (might be an NPM script or a shell command)', string)
150
+ .option('--start-timeout <timeout>', '[🔗] Maximum waiting time for the start command (based on when the first URL becomes available)', timeout, 5000)
151
+
148
152
  // Specific to coverage in url mode (experimental)
149
153
  .option('-cp, --coverage-proxy [flag]', `[🔗] ${EXPERIMENTAL_OPTION} use internal proxy to instrument remote files`, boolean, false)
150
154
  .option('-cpi, --coverage-proxy-include <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to instrument for coverage`, regex, '.*')
package/src/output.js CHANGED
@@ -171,16 +171,23 @@ function output (job, ...args) {
171
171
  writeFileSync(
172
172
  join(job.reportDir, 'output.txt'),
173
173
  args.map(arg => {
174
- if (typeof arg === 'object') {
175
- return JSON.stringify(arg, undefined, 2)
176
- }
177
174
  if (arg === undefined) {
178
175
  return 'undefined'
179
176
  }
180
177
  if (arg === null) {
181
178
  return 'null'
182
179
  }
183
- return arg.toString()
180
+ if (arg instanceof Error) {
181
+ let error = `${arg.name} ${arg.message}`
182
+ if (arg.cause) {
183
+ error += `, cause : ${arg.cause.name} ${arg.cause.message}`
184
+ }
185
+ return error
186
+ }
187
+ if (typeof arg !== 'string') {
188
+ return JSON.stringify(arg, undefined, 2)
189
+ }
190
+ return arg
184
191
  }).join(' ') + '\n',
185
192
  {
186
193
  encoding: 'utf-8',
@@ -324,6 +331,9 @@ function build (job) {
324
331
  }),
325
332
 
326
333
  reportOnJobProgress () {
334
+ if (this.reportIntervalId) {
335
+ return
336
+ }
327
337
  if (interactive) {
328
338
  this.reportIntervalId = setInterval(progress.bind(null, job), 250)
329
339
  } else if (job.outputInterval && !inJest) {
package/src/start.js ADDED
@@ -0,0 +1,91 @@
1
+ const { exec } = require('child_process')
2
+ const { stat, readFile } = require('fs/promises')
3
+ const { join } = require('path')
4
+ const { getOutput } = require('./output')
5
+ const psTreeNodeCb = require('ps-tree')
6
+ const { promisify } = require('util')
7
+ const psTree = promisify(psTreeNodeCb)
8
+
9
+ const $startedProcess = Symbol('startedProcess')
10
+
11
+ module.exports = async function start (job) {
12
+ let { start } = job
13
+ const output = getOutput(job)
14
+ const [command] = start.split(' ')
15
+
16
+ job.status = 'Executing start command'
17
+
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}`
28
+ }
29
+ }
30
+ } catch (e) {
31
+ output.debug('start', 'Missing or invalid package.json in cwd', e)
32
+ }
33
+
34
+ let childProcessExited = false
35
+ output.debug('start', 'Starting command :', start)
36
+ const childProcess = exec(start, {
37
+ cwd: job.cwd,
38
+ windowsHide: true
39
+ })
40
+ childProcess.on('close', () => {
41
+ output.debug('start', 'start command process exited')
42
+ childProcessExited = true
43
+ })
44
+ output.monitor(childProcess)
45
+ job[$startedProcess] = childProcess
46
+
47
+ job.status = 'Waiting for URL to be reachable'
48
+
49
+ const [url] = job.url
50
+
51
+ const begin = Date.now()
52
+ // eslint-disable-next-line no-unmodified-loop-condition
53
+ while (!childProcessExited && Date.now() - begin <= job.startTimeout) {
54
+ try {
55
+ const response = await fetch(url)
56
+ output.debug('start', url, response.status)
57
+ if (response.status === 200) {
58
+ break
59
+ }
60
+ } catch (e) {
61
+ output.debug('start', url, e)
62
+ await new Promise(resolve => setTimeout(resolve, 250))
63
+ }
64
+ }
65
+
66
+ if (childProcessExited) {
67
+ throw new Error(`Start command failed with exit code ${childProcess.exitCode}`)
68
+ }
69
+
70
+ const stop = async () => {
71
+ output.debug('start', 'Getting child processes...')
72
+ const childProcesses = await psTree(childProcess.pid)
73
+ for (const child of childProcesses) {
74
+ output.debug('start', 'Terminating process', child.PID)
75
+ try {
76
+ process.kill(child.PID, 'SIGKILL')
77
+ } catch (e) {
78
+ output.debug('start', 'Failed to terminate process', child.PID, ':', e)
79
+ }
80
+ }
81
+ }
82
+
83
+ if (Date.now() - begin > job.startTimeout) {
84
+ await stop()
85
+ throw new Error(`Timeout while waiting for ${url}`)
86
+ }
87
+
88
+ return {
89
+ stop
90
+ }
91
+ }
package/src/tests.js CHANGED
@@ -1,8 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const { probe: probeBrowser, start } = require('./browsers')
3
+ const { start } = require('./browsers')
4
4
  const { instrument } = require('./coverage')
5
- const { recreateDir } = require('./tools')
6
5
  const { globallyTimedOut } = require('./timeout')
7
6
  const { save, generate } = require('./report')
8
7
  const { getOutput } = require('./output')
@@ -12,7 +11,6 @@ const {
12
11
  $proxifiedUrls
13
12
  } = require('./symbols')
14
13
  const { UTRError } = require('./error')
15
- const { preload } = require('./ui5')
16
14
  const parallelize = require('./parallelize')
17
15
 
18
16
  function task (job, method) {
@@ -128,12 +126,6 @@ async function process (job) {
128
126
 
129
127
  module.exports = {
130
128
  async execute (job) {
131
- await recreateDir(job.reportDir)
132
- getOutput(job).version()
133
- if (job.preload) {
134
- await preload(job)
135
- }
136
- await probeBrowser(job)
137
129
  if (job.mode !== 'url') {
138
130
  job.url = [`http://localhost:${job.port}/${job.testsuite}`]
139
131
  } else if (!job.browserCapabilities.scripts) {
File without changes