ui5-test-runner 5.11.2 → 5.13.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
@@ -58,14 +58,14 @@ async function main () {
58
58
  }
59
59
  output.reportOnJobProgress()
60
60
  checkLatest(job, name, version)
61
- if (job.mode === 'capabilities') {
62
- return capabilities(job)
63
- }
64
61
  if (job.if && !executeIf(job)) {
65
62
  output.skipIf()
66
63
  output.stop()
67
64
  return
68
65
  }
66
+ if (job.mode === 'capabilities') {
67
+ return capabilities(job)
68
+ }
69
69
 
70
70
  let startedCommand
71
71
  if (job.startCommand) {
@@ -74,10 +74,11 @@ async function main () {
74
74
 
75
75
  if (job.mode === 'batch') {
76
76
  return await batch(job)
77
- .finally(() => {
77
+ .finally(async () => {
78
78
  if (startedCommand) {
79
- return startedCommand.stop()
79
+ await startedCommand.stop()
80
80
  }
81
+ cleanHandles(job)
81
82
  })
82
83
  }
83
84
 
package/jest.config.json CHANGED
@@ -5,7 +5,8 @@
5
5
  ],
6
6
  "testPathIgnorePatterns": [
7
7
  "/node_modules/",
8
- "/capabilities/"
8
+ "/capabilities/",
9
+ "/e2e/"
9
10
  ],
10
11
  "collectCoverage": true,
11
12
  "collectCoverageFrom": [
@@ -16,6 +17,7 @@
16
17
  "\\.spec\\.js",
17
18
  "output\\.js",
18
19
  "handle\\.js",
20
+ "coverage\\.js",
19
21
  "b\\capabilities\\b"
20
22
  ],
21
23
  "coverageThreshold": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "5.11.2",
3
+ "version": "5.13.0",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -23,6 +23,8 @@
23
23
  "test:unit:debug": "node --inspect node_modules/jest/bin/jest.js --runInBand --no-coverage",
24
24
  "pretest:e2e": "npm install -g puppeteer selenium-webdriver playwright webdriverio jsdom",
25
25
  "test:e2e": "node . --batch \"test/e2e/[\\w_]*\\.json\" --report-dir e2e --start \"node test/e2e/serve.js\" --start-wait-url http://localhost:8081 --start-wait-method HEAD --start-timeout 30s",
26
+ "serve:e2e": "node test/e2e/serve.js",
27
+ "test:e2e:legacy-only": "node . --batch \"test/e2e/JS_LEGACY_[\\w_]*\\.json\" --report-dir e2e --start serve:e2e --start-wait-url http://localhost:8081 --start-wait-method HEAD --start-timeout 30s --debug-verbose start handle --ci",
26
28
  "test:report": "node ./src/defaults/report.js ./test/report && reserve --config ./test/report/reserve.json",
27
29
  "test:text-report": "node ./src/defaults/text-report.js ./test/report",
28
30
  "build:doc": "node build/doc",
@@ -51,17 +53,16 @@
51
53
  "homepage": "https://github.com/ArnaudBuchholz/ui5-test-runner#readme",
52
54
  "dependencies": {
53
55
  "commander": "^12.1.0",
54
- "pidtree": "^0.6.0",
55
56
  "punybind": "^1.2.1",
56
57
  "punyexpr": "1.2.0",
57
58
  "reserve": "2.3.4"
58
59
  },
59
60
  "devDependencies": {
60
- "@openui5/types": "^1.143.0",
61
- "@semantic-release/npm": "^13.1.2",
62
- "@ui5/cli": "^4.0.37",
61
+ "@openui5/types": "^1.143.1",
62
+ "@semantic-release/npm": "^13.1.3",
63
+ "@ui5/cli": "^4.0.38",
63
64
  "@ui5/middleware-code-coverage": "^2.0.2",
64
- "baseline-browser-mapping": "^2.8.32",
65
+ "baseline-browser-mapping": "^2.9.11",
65
66
  "dotenv": "^16.5.0",
66
67
  "jest": "^29.7.0",
67
68
  "nock": "^14.0.10",
@@ -71,7 +72,7 @@
71
72
  "semantic-release": "^25.0.2",
72
73
  "standard": "^17.1.2",
73
74
  "typescript": "^5.9.3",
74
- "ui5-tooling-transpile": "^3.9.2"
75
+ "ui5-tooling-transpile": "^3.10.0"
75
76
  },
76
77
  "optionalDependencies": {
77
78
  "fsevents": "^2.3.3"
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const { join, isAbsolute } = require('path')
4
+ const { writeFile } = require('fs').promises
5
+ const [,, reportDir] = process.argv
6
+ const verbose = process.argv.includes('--verbose')
7
+
8
+ const log = verbose ? console.log : () => {}
9
+
10
+ log('🏗 Building JSON report...')
11
+
12
+ async function main () {
13
+ const jobPath = isAbsolute(reportDir) ? reportDir : join(process.cwd(), reportDir)
14
+ log('📦 job path :', jobPath)
15
+ const rawJob = require(join(jobPath, 'job.js'))
16
+ const cleanJob = JSON.parse(JSON.stringify(rawJob, (key, value) => {
17
+ if (value && value instanceof RegExp) {
18
+ return value.toString()
19
+ }
20
+ return value
21
+ }))
22
+ const json = JSON.stringify(cleanJob, null, 2)
23
+ log('📦 json :', json.length)
24
+
25
+ await writeFile(join(reportDir, 'report.json'), json)
26
+ log('✅ generated.')
27
+ }
28
+
29
+ main()
30
+ .catch(reason => {
31
+ console.error(reason)
32
+ return -1
33
+ })
34
+ .then((code = 0) => {
35
+ process.exit(code)
36
+ })
package/src/job-mode.js CHANGED
@@ -41,16 +41,13 @@ function buildAndCheckMode (job) {
41
41
  'batchMode',
42
42
  'batchId',
43
43
  'batchLabel',
44
- 'ci'
44
+ 'ci',
45
+ 'if'
45
46
  ])
46
47
  return 'capabilities'
47
48
  }
48
49
  if (job.url && job.url.length) {
49
50
  check(job, undefined, [
50
- 'ui5',
51
- 'libs',
52
- 'mappings',
53
- 'cache',
54
51
  'testsuite'
55
52
  ])
56
53
  return 'url'
package/src/job.js CHANGED
@@ -159,7 +159,7 @@ function getCommand (cwd) {
159
159
  // Specific to legacy (and might be used with url if pointing to local project)
160
160
  .option('--ui5 <url>', '[💻📡] UI5 url', url, 'https://ui5.sap.com')
161
161
  .option('--disable-ui5 [flag]', '[💻📡] Disable UI5 mapping (also disable libs)', boolean, false)
162
- .option('--libs <lib...>', '[💻📡] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
162
+ .option('--libs <lib...>', '[💻📡] Library mapping (<relative>=<path> or <path>), use *=webapp/resources to map resources sub folder', arrayOf(lib))
163
163
  .option('--mappings <mapping...>', '[💻📡] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
164
164
  .option('--cache <path>', '[💻📡] Cache UI5 resources locally in the given folder (empty to disable)')
165
165
  .option('--preload <library...>', '[💻📡] Preload UI5 libraries in the cache folder (only if --cache is used)', arrayOf(string))
package/src/start.js CHANGED
@@ -1,58 +1,71 @@
1
- const { exec } = require('child_process')
2
- const { stat, readFile } = require('fs/promises')
1
+ const { spawn } = require('child_process')
2
+ const { readFile, access, constants } = require('fs/promises')
3
3
  const { join } = require('path')
4
4
  const { getOutput } = require('./output')
5
- const pidtree = require('pidtree')
5
+ const { platform } = require('os')
6
+ const { allocPromise } = require('./tools')
6
7
 
7
8
  async function start (job) {
8
9
  const { startWaitUrl: url, startWaitMethod: method } = job
9
- let { startCommand: start } = job
10
+ const { startCommand: start } = job
10
11
  const output = getOutput(job)
11
- const [command, ...parameters] = start.split(' ')
12
+ let [command, ...parameters] = start.split(' ')
12
13
 
13
14
  job.status = 'Executing start command'
14
15
 
15
- // check if node
16
- if (command === 'node') {
17
- let [node] = process.argv
18
- if (node.includes(' ')) {
19
- node = `"${node}"`
20
- }
21
- output.debug('start', `Replacing node with ${node}`)
22
- start = [node, ...parameters].join(' ')
23
- } else {
24
- // check if existing NPM script
16
+ // check if existing NPM script
17
+ if (command !== 'node' && parameters.length === 0) {
25
18
  const packagePath = join(job.cwd, 'package.json')
26
19
  try {
27
- const packageStat = await stat(packagePath)
28
- if (packageStat.isFile()) {
29
- output.debug('start', 'Found package.json in cwd')
30
- const packageFile = JSON.parse(await readFile(packagePath, 'utf-8'))
31
- if (packageFile.scripts[command]) {
32
- output.debug('start', 'Found matching start script in package.json')
33
- start = `npm run ${start}`
34
- }
20
+ await access(packagePath, constants.F_OK)
21
+ output.debug('start', 'Found package.json in cwd')
22
+ const packageFile = JSON.parse(await readFile(packagePath, 'utf-8'))
23
+ if (packageFile.scripts[command]) {
24
+ output.debug('start', 'Found matching script in package.json')
25
+ // grab npm-cli path
26
+ const { promise, resolve } = allocPromise()
27
+ const npmChildProcess = spawn('npm', {
28
+ shell: true,
29
+ encoding: 'utf8'
30
+ })
31
+ npmChildProcess.on('close', resolve)
32
+ const npmOutput = []
33
+ npmChildProcess.stdout.on('data', (data) => npmOutput.push(data.toString()))
34
+ await promise
35
+ const [, version, path] = /^npm@([^ ]+) (.*)$/gm.exec(npmOutput.join(''))
36
+ output.debug('start', `npm@${version} ${path}`)
37
+ parameters = [join(path, 'bin/npm-cli.js'), 'run', command]
38
+ command = 'node'
35
39
  }
36
40
  } catch (e) {
37
41
  output.debug('start', 'Missing or invalid package.json in cwd', e)
38
42
  }
39
43
  }
40
44
 
45
+ if (command === 'node') {
46
+ const [node] = process.argv
47
+ output.debug('start', `Replacing node with ${node}`)
48
+ command = node
49
+ }
50
+
41
51
  let startProcessExited = false
42
- output.debug('start', 'Starting command :', start)
43
- const startProcess = exec(start, {
52
+ output.debug('start', 'Spawning', [command, ...parameters])
53
+ const startProcess = spawn(command, parameters, {
44
54
  cwd: job.cwd,
45
- windowsHide: true
55
+ windowsHide: true,
56
+ detached: true
46
57
  })
47
58
  startProcess.on('close', () => {
48
59
  output.debug('start', 'Start command process exited')
49
60
  startProcessExited = true
50
61
  })
51
62
  output.monitor(startProcess)
63
+ output.debug('start', `Spawned process id ${startProcess.pid}`)
52
64
 
53
65
  job.status = 'Waiting for URL to be reachable'
54
66
 
55
67
  const begin = Date.now()
68
+ let lastError
56
69
  // eslint-disable-next-line no-unmodified-loop-condition
57
70
  while (!startProcessExited && Date.now() - begin <= job.startTimeout) {
58
71
  try {
@@ -62,7 +75,10 @@ async function start (job) {
62
75
  break
63
76
  }
64
77
  } catch (e) {
65
- output.debug('start', url, e)
78
+ if (e.toString() !== lastError) {
79
+ output.debug('start', url, e)
80
+ lastError = e.toString()
81
+ }
66
82
  await new Promise(resolve => setTimeout(resolve, 250))
67
83
  }
68
84
  }
@@ -73,61 +89,30 @@ async function start (job) {
73
89
 
74
90
  const stop = async () => {
75
91
  job.status = 'Terminating start command'
76
- const begin = new Date()
77
- // eslint-disable-next-line no-unmodified-loop-condition
78
- while (!startProcessExited && Date.now() - begin <= job.startTimeout) {
79
- output.debug('start', `Getting start command ${startProcess.pid} child processes...`)
80
- let childProcesses
92
+ if (platform() === 'win32') {
93
+ const killProcess = spawn('taskkill', ['/F', '/T', '/PID', startProcess.pid], {
94
+ windowsHide: true
95
+ })
96
+ const { promise, resolve } = allocPromise()
97
+ killProcess.on('close', resolve)
98
+ output.monitor(killProcess)
99
+ await promise
100
+ } else {
81
101
  try {
82
- childProcesses = await pidtree(startProcess.pid, { advanced: true })
83
- } catch (e) {
84
- output.genericError(e)
85
- break
86
- }
87
- output.debug('start', 'Child processes', JSON.stringify(childProcesses))
88
- if (childProcesses.length === 0) {
89
- try {
90
- output.debug('start', 'Terminating start command')
91
- process.kill(startProcess.pid, 'SIGKILL')
92
- } catch (e) {
93
- output.debug('start', 'Failed to terminate start command', startProcess.pid, ':', e)
94
- }
95
- } else {
96
- const depth = {}
97
- let deepest = 1
98
- let deepless = childProcesses.length
99
- while (deepless > 0) {
100
- for (const { ppid, pid } of childProcesses) {
101
- if (ppid === startProcess.pid) {
102
- depth[pid] = 1
103
- --deepless
104
- } else {
105
- const parentDepth = depth[ppid]
106
- if (parentDepth !== undefined) {
107
- depth[pid] = parentDepth + 1
108
- deepest = Math.max(deepest, parentDepth + 1)
109
- --deepless
110
- }
111
- }
112
- }
113
- }
114
- output.debug('start', 'Child processes', JSON.stringify(depth), 'terminating', deepest)
115
- for (const { pid } of childProcesses) {
116
- if (depth[pid] === deepest) {
117
- output.debug('start', 'Terminating start child process', pid)
118
- try {
119
- process.kill(pid, 'SIGKILL')
120
- } catch (e) {
121
- output.debug('start', 'Failed to terminate start child process', pid, ':', e)
122
- }
123
- }
124
- }
102
+ process.kill(-startProcess.pid)
103
+ } catch (error) {
104
+ output.genericError(error)
105
+ return
125
106
  }
107
+ }
108
+ const begin = Date.now()
109
+ // eslint-disable-next-line no-unmodified-loop-condition
110
+ while (!startProcessExited && Date.now() - begin <= job.startTimeout) {
126
111
  await new Promise(resolve => setTimeout(resolve, 250))
127
112
  }
128
- if (!startProcessExited) {
129
- output.failedToTerminateStartCommand()
130
- startProcess.kill()
113
+ if (startProcessExited) {
114
+ // Additional waiting time to release handles
115
+ await new Promise(resolve => setTimeout(resolve, 250))
131
116
  }
132
117
  }
133
118
 
package/src/ui5.js CHANGED
@@ -17,6 +17,7 @@ const buildCacheBase = job => {
17
17
  }
18
18
 
19
19
  const ui5mappings = async job => {
20
+ const output = getOutput(job)
20
21
  const cacheBase = buildCacheBase(job)
21
22
  const match = /\/((?:test-)?resources\/.*)/ // Captured value never starts with /
22
23
  const ifCacheEnabled = () => job.cache
@@ -35,12 +36,12 @@ const ui5mappings = async job => {
35
36
  const versionUrl = mappingUrl.replace('$1', 'resources/sap-ui-version.json')
36
37
  const versionResponse = await fetch(versionUrl)
37
38
  if (versionResponse.status !== 200) {
38
- getOutput(job).log('Unable to fetch UI5 version: ' + versionResponse.status + ' ' + versionResponse.statusText)
39
+ output.log('Unable to fetch UI5 version: ' + versionResponse.status + ' ' + versionResponse.statusText)
39
40
  throw new Error('Unable to fetch UI5 version')
40
41
  }
41
42
  const version = await versionResponse.json()
42
43
  const { version: coreVersion } = version.libraries.find(({ name }) => name === 'sap.ui.core')
43
- getOutput(job).log('UI5 version used by the local server: ' + coreVersion)
44
+ output.log('UI5 version used by the local server: ' + coreVersion)
44
45
  }
45
46
 
46
47
  const mappings = [{
@@ -86,7 +87,7 @@ const ui5mappings = async job => {
86
87
  file.end()
87
88
  uncachable[path] = true
88
89
  if (response.statusCode !== 404) {
89
- getOutput(job).failedToCacheUI5resource(path, response.statusCode)
90
+ output.failedToCacheUI5resource(path, response.statusCode)
90
91
  }
91
92
  return unlink(cachePath)
92
93
  })
@@ -107,12 +108,24 @@ const ui5mappings = async job => {
107
108
  }
108
109
  const relativeUrl = relative.replace(/\//g, '\\/')
109
110
  if (source.startsWith(job.webapp)) {
110
- const relativeAbsoluteUrl = '/' + relativePath(job.webapp, source).replace(/\\/g, '/')
111
- getOutput(job).debug('libs', `${relative} maps to webapp sub directory, use internal redirection to ${relativeAbsoluteUrl}`)
112
- mappings.unshift({
113
- match: new RegExp(`\\/resources\\/${relativeUrl}(.*)`),
114
- custom: (request, response, $1) => `${relativeAbsoluteUrl}${$1}`
115
- })
111
+ if (relative === '*') {
112
+ // Special handling to support webapp/resources folder (/!\ coverage won't be extracted for those files)
113
+ output.debug('libs', '* map to webapp sub directory (expected resources), use file access')
114
+ mappings.unshift({
115
+ match: /\/resources\/(.*)/,
116
+ cwd: source,
117
+ file: '$1',
118
+ static: !job.watch && !job.debugDevMode
119
+ })
120
+ } else {
121
+ // Use redirection to support local coverage instrumentation
122
+ const relativeAbsoluteUrl = '/' + relativePath(job.webapp, source).replace(/\\/g, '/')
123
+ output.debug('libs', `${relative} maps to webapp sub directory, use internal redirection to ${relativeAbsoluteUrl}`)
124
+ mappings.unshift({
125
+ match: new RegExp(`\\/resources\\/${relativeUrl}(.*)`),
126
+ custom: (request, response, $1) => `${relativeAbsoluteUrl}${$1}`
127
+ })
128
+ }
116
129
  } else {
117
130
  mappings.unshift({
118
131
  match: new RegExp(`\\/resources\\/${relativeUrl}(.*)`),
@@ -123,9 +136,9 @@ const ui5mappings = async job => {
123
136
  match: new RegExp(`\\/resources\\/${relativeUrl}(.*)`),
124
137
  custom: (request, response, $1) => {
125
138
  if ($1 === undefined) {
126
- getOutput(job).debug('libs', `Unable to map ${relative} : $1 is undefined`)
139
+ output.debug('libs', `Unable to map ${relative} : $1 is undefined`)
127
140
  } else {
128
- getOutput(job).debug('libs', `Unable to map ${relative}/${$1} to ${join(source, $1)}`)
141
+ output.debug('libs', `Unable to map ${relative}/${$1} to ${join(source, $1)}`)
129
142
  }
130
143
  return 404
131
144
  }