ui5-test-runner 4.2.0 → 4.3.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/package.json +5 -5
- package/src/add-test-pages.js +2 -0
- package/src/browsers.js +11 -4
- package/src/job.js +4 -0
- package/src/output.js +78 -58
- package/src/qunit-hooks.js +45 -23
- package/src/reserve.js +1 -1
- package/src/symbols.js +3 -1
- package/src/tests.js +12 -1
- package/src/tools.js +18 -6
- package/src/ui5.js +141 -73
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ui5-test-runner",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "Standalone test runner for UI5",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -66,19 +66,19 @@
|
|
|
66
66
|
"mime": "^3.0.0",
|
|
67
67
|
"punybind": "^1.2.1",
|
|
68
68
|
"punyexpr": "^1.0.4",
|
|
69
|
-
"reserve": "^1.15.
|
|
69
|
+
"reserve": "^1.15.8"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@openui5/types": "^1.121.0",
|
|
73
73
|
"@ui5/cli": "^3.9.1",
|
|
74
74
|
"@ui5/middleware-code-coverage": "^1.1.1",
|
|
75
75
|
"jest": "^29.7.0",
|
|
76
|
-
"nock": "^13.5.
|
|
76
|
+
"nock": "^13.5.4",
|
|
77
77
|
"nyc": "^15.1.0",
|
|
78
78
|
"standard": "^17.1.0",
|
|
79
79
|
"start-server-and-test": "^2.0.3",
|
|
80
|
-
"typescript": "^5.
|
|
81
|
-
"ui5-tooling-transpile": "^3.3.
|
|
80
|
+
"typescript": "^5.4.2",
|
|
81
|
+
"ui5-tooling-transpile": "^3.3.7"
|
|
82
82
|
},
|
|
83
83
|
"optionalDependencies": {
|
|
84
84
|
"fsevents": "^2.3.3"
|
package/src/add-test-pages.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { stop } = require('./browsers')
|
|
4
4
|
const { URL } = require('url')
|
|
5
|
+
const { getOutput } = require('./output')
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
async addTestPages (job, url, pages) {
|
|
9
|
+
getOutput(job).debug('probe', `addTestPages from ${url}`, pages)
|
|
8
10
|
let testPageUrls
|
|
9
11
|
pages = pages.map(relativeUrl => {
|
|
10
12
|
const absoluteUrl = new URL(relativeUrl, url)
|
package/src/browsers.js
CHANGED
|
@@ -5,7 +5,7 @@ const { join } = require('path')
|
|
|
5
5
|
const { writeFile, readFile, open, stat, unlink } = require('fs/promises')
|
|
6
6
|
const { recreateDir, filename, allocPromise } = require('./tools')
|
|
7
7
|
const { getPageTimeout, pageTimedOut } = require('./timeout')
|
|
8
|
-
const { getOutput } = require('./output')
|
|
8
|
+
const { getOutput, newProgress } = require('./output')
|
|
9
9
|
const { resolvePackage } = require('./npm')
|
|
10
10
|
const { UTRError } = require('./error')
|
|
11
11
|
const { $browsers } = require('./symbols')
|
|
@@ -119,11 +119,14 @@ async function start (job, url, scripts = []) {
|
|
|
119
119
|
resolvedScripts.unshift(`window['ui5-test-runner/base-host'] = 'http://localhost:${job.port}'
|
|
120
120
|
`)
|
|
121
121
|
}
|
|
122
|
+
const progress = newProgress(job)
|
|
123
|
+
progress.label = url
|
|
122
124
|
const pageBrowser = {
|
|
123
125
|
url,
|
|
124
126
|
reportDir,
|
|
125
127
|
scripts: resolvedScripts,
|
|
126
|
-
retry: 0
|
|
128
|
+
retry: 0,
|
|
129
|
+
progress
|
|
127
130
|
}
|
|
128
131
|
const { promise, resolve, reject } = allocPromise()
|
|
129
132
|
pageBrowser.done = value => {
|
|
@@ -136,8 +139,12 @@ async function start (job, url, scripts = []) {
|
|
|
136
139
|
}
|
|
137
140
|
job[$browsers][url] = pageBrowser
|
|
138
141
|
await run(job, pageBrowser)
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
try {
|
|
143
|
+
await promise
|
|
144
|
+
} finally {
|
|
145
|
+
progress.done()
|
|
146
|
+
output.browserStopped(url)
|
|
147
|
+
}
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
async function run (job, pageBrowser) {
|
package/src/job.js
CHANGED
|
@@ -112,6 +112,7 @@ function getCommand (cwd) {
|
|
|
112
112
|
.option('--webapp <path>', '[💻🔗] Base folder of the web application (relative to cwd)', 'webapp')
|
|
113
113
|
.option('-pf, --page-filter <regexp>', '[💻🔗] Filter out pages not matching the regexp')
|
|
114
114
|
.option('-pp, --page-params <params>', '[💻🔗] Add parameters to page URL')
|
|
115
|
+
.option('--page-close-timeout <timeout>', '[💻🔗] Maximum waiting time for page close', timeout, 250)
|
|
115
116
|
.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)
|
|
116
117
|
.option('--screenshot [flag]', '[💻🔗] Take screenshots during the tests execution (if supported by the browser)', boolean, true)
|
|
117
118
|
.option('--no-screenshot', '[💻🔗] Disable screenshots')
|
|
@@ -137,6 +138,7 @@ function getCommand (cwd) {
|
|
|
137
138
|
.option('--libs <lib...>', '[💻] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
|
|
138
139
|
.option('--mappings <mapping...>', '[💻] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
|
|
139
140
|
.option('--cache <path>', '[💻] Cache UI5 resources locally in the given folder (empty to disable)')
|
|
141
|
+
.option('--preload <library...>', '[💻] Preload UI5 libraries in the cache folder (only if --cache is used)', arrayOf(string))
|
|
140
142
|
.option('--testsuite <path>', '[💻] Path of the testsuite file (relative to webapp, URL parameters are supported)', 'test/testsuite.qunit.html')
|
|
141
143
|
.option('-w, --watch [flag]', '[💻] Monitor the webapp folder and re-execute tests on change', boolean, false)
|
|
142
144
|
|
|
@@ -224,6 +226,8 @@ function finalize (job) {
|
|
|
224
226
|
.forEach(setting => updateToAbsolute(setting))
|
|
225
227
|
if (job.cache) {
|
|
226
228
|
updateToAbsolute('cache')
|
|
229
|
+
} else if (job.preload) {
|
|
230
|
+
throw new Error('--preload cannot be used without --cache')
|
|
227
231
|
}
|
|
228
232
|
if (job.alternateNpmPath) {
|
|
229
233
|
checkAccess({ path: job.alternateNpmPath, label: 'Alternate NPM path' })
|
package/src/output.js
CHANGED
|
@@ -5,9 +5,8 @@ const { join } = require('path')
|
|
|
5
5
|
const { memoryUsage } = require('process')
|
|
6
6
|
const {
|
|
7
7
|
$browsers,
|
|
8
|
-
$
|
|
9
|
-
$
|
|
10
|
-
$testPagesCompleted
|
|
8
|
+
$statusProgressCount,
|
|
9
|
+
$statusProgressTotal
|
|
11
10
|
} = require('./symbols')
|
|
12
11
|
const { filename, noop, pad } = require('./tools')
|
|
13
12
|
|
|
@@ -15,6 +14,7 @@ const inJest = typeof jest !== 'undefined'
|
|
|
15
14
|
const interactive = process.stdout.columns !== undefined && !inJest
|
|
16
15
|
const $output = Symbol('output')
|
|
17
16
|
const $outputStart = Symbol('output-start')
|
|
17
|
+
const $outputProgress = Symbol('output-progress')
|
|
18
18
|
|
|
19
19
|
if (!interactive) {
|
|
20
20
|
const UTF8_BOM_CODE = '\ufeff'
|
|
@@ -41,66 +41,90 @@ const formatTime = duration => {
|
|
|
41
41
|
|
|
42
42
|
const getElapsed = job => formatTime(Date.now() - job[$outputStart])
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
function clean (job) {
|
|
44
|
+
function * buildCleanSequence (job) {
|
|
47
45
|
const { lines } = job[$output]
|
|
48
46
|
if (!lines) {
|
|
49
47
|
return
|
|
50
48
|
}
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
yield '\x1b[?12l'
|
|
50
|
+
yield `\x1b[${lines.toString()}F`
|
|
53
51
|
for (let line = 0; line < lines; ++line) {
|
|
54
52
|
if (line > 1) {
|
|
55
|
-
|
|
53
|
+
yield '\x1b[1E'
|
|
56
54
|
}
|
|
57
|
-
|
|
55
|
+
yield ''.padEnd(process.stdout.columns, ' ')
|
|
58
56
|
}
|
|
59
57
|
if (lines > 1) {
|
|
60
|
-
|
|
58
|
+
yield `\x1b[${(lines - 1).toString()}F`
|
|
61
59
|
} else {
|
|
62
|
-
|
|
60
|
+
yield '\x1b[1G'
|
|
63
61
|
}
|
|
64
62
|
}
|
|
65
63
|
|
|
64
|
+
function clean (job) {
|
|
65
|
+
process.stdout.write([...buildCleanSequence(job)].join(''))
|
|
66
|
+
}
|
|
67
|
+
|
|
66
68
|
const BAR_WIDTH = 10
|
|
67
69
|
|
|
68
|
-
function bar (ratio, msg) {
|
|
69
|
-
|
|
70
|
+
function * bar (ratio, msg) {
|
|
71
|
+
yield '['
|
|
70
72
|
if (typeof ratio === 'string') {
|
|
71
73
|
if (ratio.length > BAR_WIDTH) {
|
|
72
|
-
|
|
74
|
+
yield ratio.substring(0, BAR_WIDTH - 3)
|
|
75
|
+
yield '...'
|
|
73
76
|
} else {
|
|
74
77
|
const padded = ratio.padStart(BAR_WIDTH - Math.floor((BAR_WIDTH - ratio.length) / 2), '-').padEnd(BAR_WIDTH, '-')
|
|
75
|
-
|
|
78
|
+
yield padded
|
|
76
79
|
}
|
|
77
|
-
|
|
80
|
+
yield '] '
|
|
78
81
|
} else {
|
|
79
82
|
const filled = Math.floor(BAR_WIDTH * ratio)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
yield ''.padEnd(filled, '\u2588')
|
|
84
|
+
yield ''.padEnd(BAR_WIDTH - filled, '\u2591')
|
|
85
|
+
yield '] '
|
|
86
|
+
yield Math.floor(100 * ratio).toString().padStart(3, ' ').toString()
|
|
87
|
+
yield '%'
|
|
83
88
|
}
|
|
84
|
-
|
|
89
|
+
yield ' '
|
|
85
90
|
const spaceLeft = process.stdout.columns - BAR_WIDTH - 14
|
|
86
91
|
if (msg.length > spaceLeft) {
|
|
87
|
-
|
|
92
|
+
yield '...'
|
|
93
|
+
yield msg.substring(msg.length - spaceLeft - 3)
|
|
88
94
|
} else {
|
|
89
|
-
|
|
95
|
+
yield msg
|
|
90
96
|
}
|
|
91
|
-
|
|
97
|
+
yield '\n'
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
const TICKS = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f']
|
|
95
101
|
|
|
102
|
+
class Progress {
|
|
103
|
+
#job = undefined
|
|
104
|
+
|
|
105
|
+
constructor (job) {
|
|
106
|
+
this.#job = job
|
|
107
|
+
if (!job[$outputProgress]) {
|
|
108
|
+
job[$outputProgress] = []
|
|
109
|
+
}
|
|
110
|
+
job[$outputProgress].push(this)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
done () {
|
|
114
|
+
const pos = this.#job[$outputProgress].indexOf(this)
|
|
115
|
+
this.#job[$outputProgress].splice(pos, 1)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
function progress (job, cleanFirst = true) {
|
|
120
|
+
const sequence = []
|
|
97
121
|
if (interactive) {
|
|
98
122
|
if (cleanFirst) {
|
|
99
|
-
|
|
123
|
+
sequence.push(...buildCleanSequence(job))
|
|
100
124
|
}
|
|
101
125
|
} else {
|
|
102
126
|
if (job[$browsers]) {
|
|
103
|
-
|
|
127
|
+
sequence.push(`${getElapsed(job)} │ Progress\n──────┴──────────\n`)
|
|
104
128
|
} else {
|
|
105
129
|
return
|
|
106
130
|
}
|
|
@@ -112,41 +136,28 @@ function progress (job, cleanFirst = true) {
|
|
|
112
136
|
++output.lines
|
|
113
137
|
const { rss, heapTotal, heapUsed, external, arrayBuffers } = memoryUsage()
|
|
114
138
|
const fmt = size => `${(size / (1024 * 1024)).toFixed(2)}M`
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
if (job[$probeUrlsStarted]) {
|
|
118
|
-
const total = job.url.length + job.testPageUrls.length
|
|
119
|
-
if (job[$testPagesCompleted] !== total) {
|
|
120
|
-
progressRatio = (job[$probeUrlsCompleted] + (job[$testPagesCompleted] || 0)) / total
|
|
121
|
-
}
|
|
139
|
+
sequence.push(`MEM r:${fmt(rss)}, h:${fmt(heapUsed)}/${fmt(heapTotal)}, x:${fmt(external)}, a:${fmt(arrayBuffers)}\n`)
|
|
122
140
|
}
|
|
123
|
-
if (job[$
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (page) {
|
|
131
|
-
const { count, passed, failed } = page
|
|
132
|
-
if (count) {
|
|
133
|
-
const progress = passed + failed
|
|
134
|
-
bar(progress / count, pageUrl)
|
|
135
|
-
starting = false
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (starting) {
|
|
140
|
-
bar('starting', pageUrl)
|
|
141
|
+
if (job[$outputProgress]) {
|
|
142
|
+
output.lines += job[$outputProgress].length
|
|
143
|
+
job[$outputProgress].forEach(({ count, total, label }) => {
|
|
144
|
+
if (total !== undefined) {
|
|
145
|
+
sequence.push(...bar((count || 0) / (total || 1), label))
|
|
146
|
+
} else {
|
|
147
|
+
sequence.push(...bar('starting', label))
|
|
141
148
|
}
|
|
142
149
|
})
|
|
143
150
|
}
|
|
151
|
+
if (job[$statusProgressTotal]) {
|
|
152
|
+
progressRatio = (job[$statusProgressCount] || 0) / job[$statusProgressTotal]
|
|
153
|
+
}
|
|
144
154
|
const status = `${TICKS[++output.lastTick % TICKS.length]} ${job.status}`
|
|
145
155
|
if (progressRatio !== undefined) {
|
|
146
|
-
bar(progressRatio, status)
|
|
156
|
+
sequence.push(...bar(progressRatio, status))
|
|
147
157
|
} else {
|
|
148
|
-
|
|
158
|
+
sequence.push(status, '\n')
|
|
149
159
|
}
|
|
160
|
+
process.stdout.write(sequence.join(''))
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
function output (job, ...args) {
|
|
@@ -250,12 +261,15 @@ function build (job) {
|
|
|
250
261
|
log(job, p80()`Server running at ${pad.lt(url)}`)
|
|
251
262
|
},
|
|
252
263
|
|
|
253
|
-
debug:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
debug: (moduleSpecifier, ...args) => {
|
|
265
|
+
const [mainModule] = moduleSpecifier.split('/')
|
|
266
|
+
if (job.debugVerbose && (job.debugVerbose.includes(moduleSpecifier) || job.debugVerbose.includes(mainModule))) {
|
|
267
|
+
wrap(() => {
|
|
268
|
+
console.log(`🐞${moduleSpecifier}`, ...args)
|
|
269
|
+
output(job, `🐞${moduleSpecifier}`, ...args)
|
|
270
|
+
})()
|
|
257
271
|
}
|
|
258
|
-
}
|
|
272
|
+
},
|
|
259
273
|
|
|
260
274
|
redirected: wrap(({ method, url, statusCode, timeSpent }) => {
|
|
261
275
|
if (url.startsWith('/_/progress')) {
|
|
@@ -281,6 +295,8 @@ function build (job) {
|
|
|
281
295
|
method(job, '')
|
|
282
296
|
method(job, text)
|
|
283
297
|
method(job, '──────┴'.padEnd(text.length, '─'))
|
|
298
|
+
delete job[$statusProgressCount]
|
|
299
|
+
delete job[$statusProgressTotal]
|
|
284
300
|
},
|
|
285
301
|
|
|
286
302
|
watching: wrap(path => {
|
|
@@ -552,5 +568,9 @@ module.exports = {
|
|
|
552
568
|
job[$output] = build(job)
|
|
553
569
|
}
|
|
554
570
|
return job[$output]
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
newProgress (job) {
|
|
574
|
+
return new Progress(job)
|
|
555
575
|
}
|
|
556
576
|
}
|
package/src/qunit-hooks.js
CHANGED
|
@@ -5,7 +5,10 @@ const { collect } = require('./coverage')
|
|
|
5
5
|
const { UTRError } = require('./error')
|
|
6
6
|
const { getOutput } = require('./output')
|
|
7
7
|
const { basename } = require('path')
|
|
8
|
-
const { filename, stripUrlHash } = require('./tools')
|
|
8
|
+
const { filename, stripUrlHash, allocPromise } = require('./tools')
|
|
9
|
+
const { $browsers } = require('./symbols')
|
|
10
|
+
const $doneResolve = Symbol('doneResolve')
|
|
11
|
+
const $doneTimeout = Symbol('doneTimeout')
|
|
9
12
|
|
|
10
13
|
function error (job, url, details = '') {
|
|
11
14
|
stop(job, url)
|
|
@@ -40,11 +43,12 @@ function merge (targetModules, modules) {
|
|
|
40
43
|
function get (job, urlWithHash, { testId, modules, isOpa } = {}) {
|
|
41
44
|
const url = stripUrlHash(urlWithHash)
|
|
42
45
|
const page = job.qunitPages && job.qunitPages[url]
|
|
46
|
+
const progress = (job[$browsers] && job[$browsers][url] && job[$browsers][url].progress) || { total: 0, count: 0 }
|
|
43
47
|
if (!page) {
|
|
44
48
|
error(job, url, `No QUnit page found for ${urlWithHash}`)
|
|
45
49
|
}
|
|
46
50
|
if (modules && modules.length) {
|
|
47
|
-
page.count = merge(page.modules, modules)
|
|
51
|
+
progress.total = page.count = merge(page.modules, modules)
|
|
48
52
|
}
|
|
49
53
|
if (!page.isOpa && isOpa) {
|
|
50
54
|
page.isOpa = true
|
|
@@ -65,7 +69,7 @@ function get (job, urlWithHash, { testId, modules, isOpa } = {}) {
|
|
|
65
69
|
invalidTestId(job, url, testId)
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
|
-
return { url, page, testModule, test }
|
|
72
|
+
return { url, page, testModule, test, progress }
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
async function done (job, urlWithHash, report) {
|
|
@@ -73,27 +77,33 @@ async function done (job, urlWithHash, report) {
|
|
|
73
77
|
if (page.count === 0) {
|
|
74
78
|
return // wait
|
|
75
79
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
const { promise, resolve } = allocPromise()
|
|
81
|
+
page[$doneResolve] = resolve
|
|
82
|
+
page[$doneTimeout] = setTimeout(async () => {
|
|
83
|
+
if (job.browserCapabilities.screenshot) {
|
|
84
|
+
try {
|
|
85
|
+
await screenshot(job, url, 'done')
|
|
86
|
+
} catch (error) {
|
|
87
|
+
getOutput(job).genericError(error, url)
|
|
88
|
+
}
|
|
81
89
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
page.end = new Date()
|
|
91
|
+
if (report.__coverage__) {
|
|
92
|
+
await collect(job, url, report.__coverage__)
|
|
93
|
+
delete report.__coverage__
|
|
94
|
+
}
|
|
95
|
+
page.report = report
|
|
96
|
+
stop(job, url)
|
|
97
|
+
resolve()
|
|
98
|
+
}, job.pageCloseTimeout)
|
|
99
|
+
return promise
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
module.exports = {
|
|
93
103
|
get,
|
|
94
104
|
|
|
95
105
|
async begin (job, urlWithHash, details) {
|
|
96
|
-
getOutput(job).debug('qunit', 'begin', urlWithHash, details)
|
|
106
|
+
getOutput(job).debug('qunit/begin', 'begin', urlWithHash, details)
|
|
97
107
|
const { isOpa, totalTests, modules } = details
|
|
98
108
|
const url = stripUrlHash(urlWithHash)
|
|
99
109
|
if (!job.qunitPages) {
|
|
@@ -109,16 +119,24 @@ module.exports = {
|
|
|
109
119
|
modules
|
|
110
120
|
}
|
|
111
121
|
job.qunitPages[url] = qunitPage
|
|
122
|
+
const { progress } = get(job, url)
|
|
123
|
+
progress.count = 0
|
|
124
|
+
progress.total = totalTests
|
|
112
125
|
},
|
|
113
126
|
|
|
114
127
|
async testStart (job, urlWithHash, details) {
|
|
115
|
-
getOutput(job).debug('qunit', 'testStart', urlWithHash, details)
|
|
116
|
-
const { test } = get(job, urlWithHash, details)
|
|
128
|
+
getOutput(job).debug('qunit/testStart', 'testStart', urlWithHash, details)
|
|
129
|
+
const { page, test } = get(job, urlWithHash, details)
|
|
130
|
+
const { [$doneTimeout]: doneTimeout, [$doneResolve]: doneResolve } = page
|
|
131
|
+
if (doneTimeout) {
|
|
132
|
+
clearTimeout(doneTimeout)
|
|
133
|
+
doneResolve()
|
|
134
|
+
}
|
|
117
135
|
test.start = new Date()
|
|
118
136
|
},
|
|
119
137
|
|
|
120
138
|
async log (job, urlWithHash, details) {
|
|
121
|
-
getOutput(job).debug('qunit', 'log', urlWithHash, details)
|
|
139
|
+
getOutput(job).debug('qunit/log', 'log', urlWithHash, details)
|
|
122
140
|
const { url, page, test } = get(job, urlWithHash, details)
|
|
123
141
|
const { isOpa, modules, module, name, testId, ...log } = details
|
|
124
142
|
if (!test) {
|
|
@@ -139,13 +157,14 @@ module.exports = {
|
|
|
139
157
|
},
|
|
140
158
|
|
|
141
159
|
async testDone (job, urlWithHash, details) {
|
|
142
|
-
getOutput(job).debug('qunit', 'testDone', urlWithHash, details)
|
|
160
|
+
getOutput(job).debug('qunit/testDone', 'testDone', urlWithHash, details)
|
|
143
161
|
const { name, module, testId, assertions, ...report } = details
|
|
144
162
|
const { failed } = report
|
|
145
|
-
const { url, page, test } = get(job, urlWithHash, { testId })
|
|
163
|
+
const { url, page, test, progress } = get(job, urlWithHash, { testId })
|
|
146
164
|
if (!test) {
|
|
147
165
|
invalidTestId(job, url, testId)
|
|
148
166
|
}
|
|
167
|
+
++progress.count
|
|
149
168
|
if (failed) {
|
|
150
169
|
if (job.browserCapabilities.screenshot) {
|
|
151
170
|
try {
|
|
@@ -180,5 +199,8 @@ module.exports = {
|
|
|
180
199
|
}
|
|
181
200
|
},
|
|
182
201
|
|
|
183
|
-
done
|
|
202
|
+
async done (job, urlWithHash, report) {
|
|
203
|
+
getOutput(job).debug('qunit/done', 'done', urlWithHash, report)
|
|
204
|
+
await done(job, urlWithHash, report)
|
|
205
|
+
}
|
|
184
206
|
}
|
package/src/reserve.js
CHANGED
|
@@ -2,7 +2,7 @@ const { join } = require('path')
|
|
|
2
2
|
const cors = require('./cors')
|
|
3
3
|
const endpoints = require('./endpoints')
|
|
4
4
|
const { mappings: coverage } = require('./coverage')
|
|
5
|
-
const ui5 = require('./ui5')
|
|
5
|
+
const { mappings: ui5 } = require('./ui5')
|
|
6
6
|
const { check } = require('reserve')
|
|
7
7
|
const unhandled = require('./unhandled')
|
|
8
8
|
|
package/src/symbols.js
CHANGED
|
@@ -6,5 +6,7 @@ module.exports = {
|
|
|
6
6
|
$testPagesCompleted: Symbol('testPagesCompleted'),
|
|
7
7
|
$valueSources: Symbol('valueSources'),
|
|
8
8
|
$remoteOnLegacy: Symbol('remoteOnLegacy'),
|
|
9
|
-
$proxifiedUrls: Symbol('proxifiedUrls')
|
|
9
|
+
$proxifiedUrls: Symbol('proxifiedUrls'),
|
|
10
|
+
$statusProgressCount: Symbol('statusProgressCount'),
|
|
11
|
+
$statusProgressTotal: Symbol('statusProgressTotal')
|
|
10
12
|
}
|
package/src/tests.js
CHANGED
|
@@ -10,10 +10,13 @@ const {
|
|
|
10
10
|
$probeUrlsStarted,
|
|
11
11
|
$probeUrlsCompleted,
|
|
12
12
|
$testPagesStarted,
|
|
13
|
-
$testPagesCompleted
|
|
13
|
+
$testPagesCompleted,
|
|
14
|
+
$statusProgressTotal,
|
|
15
|
+
$statusProgressCount
|
|
14
16
|
} = require('./symbols')
|
|
15
17
|
const { UTRError } = require('./error')
|
|
16
18
|
const { $proxifiedUrls } = require('./symbols')
|
|
19
|
+
const { preload } = require('./ui5')
|
|
17
20
|
|
|
18
21
|
async function run (task, job) {
|
|
19
22
|
const {
|
|
@@ -25,6 +28,10 @@ async function run (task, job) {
|
|
|
25
28
|
const output = getOutput(job)
|
|
26
29
|
const urls = job[urlsMember]
|
|
27
30
|
const { length } = urls
|
|
31
|
+
if (job[$statusProgressTotal] === undefined) {
|
|
32
|
+
job[$statusProgressTotal] = length
|
|
33
|
+
job[$statusProgressCount] = 0
|
|
34
|
+
}
|
|
28
35
|
if (job[completedMember] === length) {
|
|
29
36
|
return
|
|
30
37
|
}
|
|
@@ -47,6 +54,7 @@ async function run (task, job) {
|
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
++job[completedMember]
|
|
57
|
+
++job[$statusProgressCount]
|
|
50
58
|
return run(task, job)
|
|
51
59
|
}
|
|
52
60
|
|
|
@@ -158,6 +166,9 @@ module.exports = {
|
|
|
158
166
|
async execute (job) {
|
|
159
167
|
await recreateDir(job.reportDir)
|
|
160
168
|
getOutput(job).version()
|
|
169
|
+
if (job.preload) {
|
|
170
|
+
await preload(job)
|
|
171
|
+
}
|
|
161
172
|
await probe(job)
|
|
162
173
|
if (job.mode !== 'url') {
|
|
163
174
|
job.url = [`http://localhost:${job.port}/${job.testsuite}`]
|
package/src/tools.js
CHANGED
|
@@ -127,6 +127,7 @@ function allocPromise () {
|
|
|
127
127
|
|
|
128
128
|
async function download (url, filename) {
|
|
129
129
|
const { hostname, port, origin } = new URL(url)
|
|
130
|
+
const error = reason => new Error(`Error downloading ${url} to ${filename}, ${reason}`)
|
|
130
131
|
const options = {
|
|
131
132
|
hostname,
|
|
132
133
|
port,
|
|
@@ -139,16 +140,27 @@ async function download (url, filename) {
|
|
|
139
140
|
const { promise, resolve, reject } = allocPromise()
|
|
140
141
|
const request = protocol.request(options, async response => {
|
|
141
142
|
if (response.statusCode !== 200) {
|
|
142
|
-
reject(response.statusCode)
|
|
143
|
+
reject(error(`server responded with ${response.statusCode}`))
|
|
143
144
|
output.end()
|
|
144
|
-
|
|
145
|
+
try {
|
|
146
|
+
await unlink(filename)
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// ignore
|
|
149
|
+
}
|
|
145
150
|
return
|
|
146
151
|
}
|
|
147
|
-
response.on('error',
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
response.on('error', reason => {
|
|
153
|
+
reject(error(`response failed : ${reason.toString()}`))
|
|
154
|
+
})
|
|
155
|
+
response
|
|
156
|
+
.pipe(output)
|
|
157
|
+
.on('finish', () => {
|
|
158
|
+
resolve(filename)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
request.on('error', reason => {
|
|
162
|
+
reject(error(`request failed : ${reason.toString()}`))
|
|
150
163
|
})
|
|
151
|
-
request.on('error', reject)
|
|
152
164
|
request.end()
|
|
153
165
|
return promise
|
|
154
166
|
}
|
package/src/ui5.js
CHANGED
|
@@ -2,90 +2,158 @@
|
|
|
2
2
|
|
|
3
3
|
const { dirname, join } = require('path')
|
|
4
4
|
const { createWriteStream } = require('fs')
|
|
5
|
-
const { mkdir, unlink } = require('fs').promises
|
|
5
|
+
const { mkdir, unlink, stat } = require('fs').promises
|
|
6
6
|
const { capture } = require('reserve')
|
|
7
|
-
const { getOutput } = require('./output')
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (job.disableUi5) {
|
|
11
|
-
return []
|
|
12
|
-
}
|
|
7
|
+
const { getOutput, newProgress } = require('./output')
|
|
8
|
+
const { download, allocPromise } = require('./tools')
|
|
9
|
+
const { $statusProgressCount, $statusProgressTotal } = require('./symbols')
|
|
13
10
|
|
|
11
|
+
const buildCacheBase = job => {
|
|
14
12
|
const [, hostName] = /https?:\/\/([^/]*)/.exec(job.ui5)
|
|
15
13
|
const [, version] = /(\d+\.\d+\.\d+)?$/.exec(job.ui5)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const ifCacheEnabled = (request, url, match) => job.cache ? match : false
|
|
19
|
-
const uncachable = {}
|
|
20
|
-
const cachingInProgress = {}
|
|
14
|
+
return join(job.cache || '', hostName.replace(':', '_'), version || '')
|
|
15
|
+
}
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const cachingPromise = cachingInProgress[path]
|
|
36
|
-
/* istanbul ignore next */ // Hard to reproduce
|
|
37
|
-
if (cachingPromise) {
|
|
38
|
-
await cachingPromise
|
|
17
|
+
module.exports = {
|
|
18
|
+
preload: async job => {
|
|
19
|
+
const cacheBase = buildCacheBase(job)
|
|
20
|
+
|
|
21
|
+
const get = async (path, expectedSize) => {
|
|
22
|
+
const filePath = join(cacheBase, 'resources/' + path)
|
|
23
|
+
try {
|
|
24
|
+
const info = await stat(filePath)
|
|
25
|
+
if (expectedSize !== undefined && info.isFile() && info.size === expectedSize) {
|
|
26
|
+
return filePath
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// ignore
|
|
39
30
|
}
|
|
31
|
+
return download((new URL('resources/' + path, job.ui5)).toString(), filePath)
|
|
40
32
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
33
|
+
|
|
34
|
+
const lib = async name => {
|
|
35
|
+
progress.label = name
|
|
36
|
+
progress.count = 0
|
|
37
|
+
const { promise, resolve/*, reject */ } = allocPromise()
|
|
38
|
+
const libPath = name.replace(/\./g, '/') + '/'
|
|
39
|
+
const { resources } = require(await get(libPath + 'resources.json'))
|
|
40
|
+
progress.total = resources.length
|
|
41
|
+
let index = 0
|
|
42
|
+
let active = 0
|
|
43
|
+
|
|
44
|
+
const task = async () => {
|
|
45
|
+
++active
|
|
46
|
+
const { name, size } = resources[index++]
|
|
47
|
+
await get(libPath + name, size)
|
|
48
|
+
++progress.count
|
|
49
|
+
if (index < resources.length) {
|
|
50
|
+
task()
|
|
51
|
+
}
|
|
52
|
+
if (--active === 0) {
|
|
53
|
+
resolve()
|
|
54
|
+
}
|
|
59
55
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
getOutput(job).failedToCacheUI5resource(path, response.statusCode)
|
|
67
|
-
}
|
|
68
|
-
return unlink(cachePath)
|
|
69
|
-
})
|
|
70
|
-
.then(() => {
|
|
71
|
-
delete cachingInProgress[path]
|
|
72
|
-
})
|
|
56
|
+
|
|
57
|
+
for (let parallel = 0; parallel < 8; ++parallel) {
|
|
58
|
+
task()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return promise
|
|
73
62
|
}
|
|
74
|
-
}, {
|
|
75
|
-
// UI5 from url
|
|
76
|
-
method: ['GET', 'HEAD'],
|
|
77
|
-
match,
|
|
78
|
-
url: `${job.ui5}/$1`,
|
|
79
|
-
'ignore-unverifiable-certificate': true
|
|
80
|
-
}]
|
|
81
63
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
64
|
+
job.status = 'Preloading UI5'
|
|
65
|
+
job[$statusProgressCount] = 0
|
|
66
|
+
job[$statusProgressTotal] = job.preload.length + 1
|
|
67
|
+
await get('sap-ui-version.json')
|
|
68
|
+
await get('sap-ui-core.js')
|
|
69
|
+
const progress = newProgress(job)
|
|
70
|
+
await lib('sap.ui.core')
|
|
71
|
+
for (const name of job.preload) {
|
|
72
|
+
++job[$statusProgressCount]
|
|
73
|
+
await lib(name)
|
|
74
|
+
}
|
|
75
|
+
progress.done()
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
mappings: job => {
|
|
79
|
+
if (job.disableUi5) {
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cacheBase = buildCacheBase(job)
|
|
84
|
+
const match = /\/((?:test-)?resources\/.*)/
|
|
85
|
+
const ifCacheEnabled = (request, url, match) => job.cache ? match : false
|
|
86
|
+
const uncachable = {}
|
|
87
|
+
const cachingInProgress = {}
|
|
88
|
+
|
|
89
|
+
const mappings = [{
|
|
90
|
+
/* Prevent caching issues :
|
|
91
|
+
* - Caching was not possible (99% URL does not exist)
|
|
92
|
+
* - Caching is in progress (must wait for the end of the writing stream)
|
|
93
|
+
*/
|
|
94
|
+
match,
|
|
95
|
+
'if-match': ifCacheEnabled,
|
|
96
|
+
custom: async (request, response, path) => {
|
|
97
|
+
if (uncachable[path]) {
|
|
98
|
+
response.writeHead(404)
|
|
99
|
+
response.end()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
const cachingPromise = cachingInProgress[path]
|
|
103
|
+
/* istanbul ignore next */ // Hard to reproduce
|
|
104
|
+
if (cachingPromise) {
|
|
105
|
+
await cachingPromise
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}, {
|
|
109
|
+
// UI5 from cache
|
|
110
|
+
match,
|
|
111
|
+
'if-match': ifCacheEnabled,
|
|
112
|
+
file: join(cacheBase, '$1'),
|
|
86
113
|
'ignore-if-not-found': true
|
|
114
|
+
}, {
|
|
115
|
+
// UI5 caching
|
|
116
|
+
method: 'GET',
|
|
117
|
+
match,
|
|
118
|
+
'if-match': ifCacheEnabled,
|
|
119
|
+
custom: async (request, response, path) => {
|
|
120
|
+
const filePath = /([^?#]+)/.exec(unescape(path))[1] // filter URL parameters & hash (assuming resources are static)
|
|
121
|
+
const cachePath = join(cacheBase, filePath)
|
|
122
|
+
const cacheFolder = dirname(cachePath)
|
|
123
|
+
await mkdir(cacheFolder, { recursive: true })
|
|
124
|
+
if (cachingInProgress[path]) {
|
|
125
|
+
return request.url // loop back to use cached result
|
|
126
|
+
}
|
|
127
|
+
const file = createWriteStream(cachePath)
|
|
128
|
+
cachingInProgress[path] = capture(response, file)
|
|
129
|
+
.catch(reason => {
|
|
130
|
+
file.end()
|
|
131
|
+
uncachable[path] = true
|
|
132
|
+
if (response.statusCode !== 404) {
|
|
133
|
+
getOutput(job).failedToCacheUI5resource(path, response.statusCode)
|
|
134
|
+
}
|
|
135
|
+
return unlink(cachePath)
|
|
136
|
+
})
|
|
137
|
+
.then(() => {
|
|
138
|
+
delete cachingInProgress[path]
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}, {
|
|
142
|
+
// UI5 from url
|
|
143
|
+
method: ['GET', 'HEAD'],
|
|
144
|
+
match,
|
|
145
|
+
url: `${job.ui5}/$1`,
|
|
146
|
+
'ignore-unverifiable-certificate': true
|
|
147
|
+
}]
|
|
148
|
+
|
|
149
|
+
job.libs.forEach(({ relative, source }) => {
|
|
150
|
+
mappings.unshift({
|
|
151
|
+
match: new RegExp(`\\/resources\\/${relative.replace(/\//g, '\\/')}(.*)`),
|
|
152
|
+
file: join(source, '$1'),
|
|
153
|
+
'ignore-if-not-found': true
|
|
154
|
+
})
|
|
87
155
|
})
|
|
88
|
-
})
|
|
89
156
|
|
|
90
|
-
|
|
157
|
+
return mappings
|
|
158
|
+
}
|
|
91
159
|
}
|