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 +6 -5
- package/jest.config.json +3 -1
- package/package.json +8 -7
- package/src/defaults/json-report.js +36 -0
- package/src/job-mode.js +2 -5
- package/src/job.js +1 -1
- package/src/start.js +63 -78
- package/src/ui5.js +24 -11
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
|
-
|
|
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.
|
|
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.
|
|
61
|
-
"@semantic-release/npm": "^13.1.
|
|
62
|
-
"@ui5/cli": "^4.0.
|
|
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.
|
|
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.
|
|
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 {
|
|
2
|
-
const {
|
|
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
|
|
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
|
-
|
|
10
|
+
const { startCommand: start } = job
|
|
10
11
|
const output = getOutput(job)
|
|
11
|
-
|
|
12
|
+
let [command, ...parameters] = start.split(' ')
|
|
12
13
|
|
|
13
14
|
job.status = 'Executing start command'
|
|
14
15
|
|
|
15
|
-
// check if
|
|
16
|
-
if (command
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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', '
|
|
43
|
-
const startProcess =
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
} catch (
|
|
84
|
-
output.genericError(
|
|
85
|
-
|
|
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 (
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
139
|
+
output.debug('libs', `Unable to map ${relative} : $1 is undefined`)
|
|
127
140
|
} else {
|
|
128
|
-
|
|
141
|
+
output.debug('libs', `Unable to map ${relative}/${$1} to ${join(source, $1)}`)
|
|
129
142
|
}
|
|
130
143
|
return 404
|
|
131
144
|
}
|