ui5-test-runner 3.3.4 → 4.0.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 +9 -1
- package/package.json +38 -9
- package/src/browsers.js +9 -1
- package/src/capabilities/index.js +6 -6
- package/src/capabilities/tests/screenshot/index.html +13 -3
- package/src/capabilities/tests/screenshot/index.js +11 -5
- package/src/capabilities/tests/traces/index.js +1 -1
- package/src/coverage.js +25 -15
- package/src/coverage.spec.js +3 -1
- package/src/defaults/playwright.js +3 -1
- package/src/defaults/puppeteer.js +10 -20
- package/src/error.js +2 -0
- package/src/job.js +18 -5
- package/src/job.spec.js +6 -0
- package/src/options.js +4 -0
- package/src/output.js +10 -0
- package/src/qunit-hooks.js +44 -9
- package/src/qunit-hooks.spec.js +22 -3
- package/src/report.js +4 -1
- package/src/simulate.spec.js +25 -0
- package/src/symbols.js +2 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ A self-sufficient test runner for UI5 applications enabling parallel execution o
|
|
|
20
20
|
|
|
21
21
|
## 💿 How to install
|
|
22
22
|
|
|
23
|
-
* Works with [Node.js](https://nodejs.org/en/download/) >=
|
|
23
|
+
* Works with [Node.js](https://nodejs.org/en/download/) >= 18
|
|
24
24
|
* Local installation
|
|
25
25
|
* `npm install --save-dev ui5-test-runner`
|
|
26
26
|
* Trigger either with `npx ui5-test-runner` or through an npm script invoking `ui5-test-runner`
|
|
@@ -49,6 +49,9 @@ A self-sufficient test runner for UI5 applications enabling parallel execution o
|
|
|
49
49
|
|
|
50
50
|
## ⚠️ Breaking changes
|
|
51
51
|
|
|
52
|
+
### v4
|
|
53
|
+
* Dropping support of Node.js 16
|
|
54
|
+
|
|
52
55
|
### v3
|
|
53
56
|
* Dropping support of Node.js 14
|
|
54
57
|
|
|
@@ -58,3 +61,8 @@ A self-sufficient test runner for UI5 applications enabling parallel execution o
|
|
|
58
61
|
* Dependencies are installed **on demand**
|
|
59
62
|
* Browser instantiation command evolved in an **incompatible way** (see [documentation](https://arnaudbuchholz.github.io/ui5-test-runner/browser.html)).
|
|
60
63
|
* Output is different (report, traces)
|
|
64
|
+
|
|
65
|
+
## ✒ Contributors
|
|
66
|
+
|
|
67
|
+
* [Marian Zeis](https://github.com/marianfoo): Documentation page revamp [PR #54](https://github.com/ArnaudBuchholz/ui5-test-runner/pull/54)
|
|
68
|
+
* [Raj Singh](https://github.com/rajxsingh): Basic HTTP Authentication in Puppeteer [PR #71](https://github.com/ArnaudBuchholz/ui5-test-runner/pull/71)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ui5-test-runner",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Standalone test runner for UI5",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -12,11 +12,16 @@
|
|
|
12
12
|
"ui5-test-runner": "./index.js"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=18"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"lint": "standard --fix",
|
|
19
|
-
"test": "npm run test:unit && npm run test:
|
|
19
|
+
"test": "npm run test:unit && npm run test:browsers && npm run test:samples",
|
|
20
|
+
"test:browsers": "npm run test:integration:jsdom && npm run test:integration:puppeteer && npm run test:integration:selenium-webdriver-chrome && npm run test:integration:playwright",
|
|
21
|
+
"test:samples": "npm run test:samples:js && npm run test:samples:ts && npm run test:auth-sample",
|
|
22
|
+
"test:samples:js": "npm run test:sample:js:legacy && npm run test:sample:js:coverage:legacy && npm run test:sample:js:legacy-remote && npm run test:sample:js:coverage:legacy-remote && npm run test:sample:js:remote && npm run test:sample:js:coverage:remote",
|
|
23
|
+
"test:samples:ts": "npm run test:sample:ts:remote && npm run test:sample:ts:coverage:remote",
|
|
24
|
+
"test:auth-sample": "npm run test:auth-sample:remote",
|
|
20
25
|
"test:coverall": "rimraf .nyc_output && jest --coverageDirectory .nyc_output --coverageReporters json && nyc --silent --no-clean npm run test:integration:jsdom && nyc --silent --no-clean npm run test:integration:puppeteer && nyc --silent --no-clean npm run test:integration:selenium-webdriver-chrome && nyc --silent --no-clean npm run test:integration:playwright && 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",
|
|
21
26
|
"test:unit": "jest",
|
|
22
27
|
"test:unit:debug": "jest --runInBand",
|
|
@@ -26,7 +31,21 @@
|
|
|
26
31
|
"test:integration:playwright": "node . --capabilities --browser $/playwright.js",
|
|
27
32
|
"test:report": "node ./src/defaults/report.js ./test/report && reserve --config ./test/report/reserve.json",
|
|
28
33
|
"test:text-report": "node ./src/defaults/text-report.js ./test/report",
|
|
29
|
-
"
|
|
34
|
+
"test:sample:js:legacy": "node . --cwd ./test/sample.js",
|
|
35
|
+
"test:sample:js:coverage:legacy": "node . --cwd ./test/sample.js --coverage --coverage-settings nyc.json --coverage-check-statements 67",
|
|
36
|
+
"test:sample:js:legacy-remote": "node . --port 8081 --cwd ./test/sample.js --url http://localhost:8081/test/testsuite.qunit.html",
|
|
37
|
+
"test:sample:js:coverage:legacy-remote": "node . --port 8081 --cwd ./test/sample.js --url http://localhost:8081/test/testsuite.qunit.html --coverage --coverage-settings nyc.json --coverage-check-statements 67",
|
|
38
|
+
"test:sample:js:remote": "start-server-and-test 'npm run serve:sample:js' http://localhost:8080 'node . --url http://localhost:8080/test/testsuite.qunit.html'",
|
|
39
|
+
"test:sample:js:coverage:remote": "start-server-and-test 'npm run serve:sample:js' http://localhost:8080 'node . --url http://localhost:8080/test/testsuite.qunit.html --coverage --coverage-check-statements 67'",
|
|
40
|
+
"serve:sample:js": "ui5 serve --config ./test/sample.js/ui5.yaml",
|
|
41
|
+
"test:sample:ts:remote": "start-server-and-test 'npm run serve:sample:ts' http://localhost:8080 'node . --url http://localhost:8080/test/testsuite.qunit.html'",
|
|
42
|
+
"serve:sample:ts": "cd ./test/sample.ts && node ui5.cjs serve",
|
|
43
|
+
"test:sample:ts:coverage:remote": "start-server-and-test 'npm run serve:sample:ts:coverage' http://localhost:8080 'node . --url http://localhost:8080/test/testsuite.qunit.html --coverage --coverage-check-statements 67'",
|
|
44
|
+
"serve:sample:ts:coverage": "cd ./test/sample.ts && node ui5.cjs serve --config ui5-coverage.yaml",
|
|
45
|
+
"test:auth-sample:remote": "start-server-and-test 'npm run serve:auth-sample' http://localhost:8080 'node . --url http://localhost:8080/test/testsuite.qunit.html --browser $/puppeteer.js --browser-args --basic-auth-username testUsername --browser-args --basic-auth-password testPassword'",
|
|
46
|
+
"serve:auth-sample": "cd ./test/auth_sample.js && reserve --config ./reserve.json",
|
|
47
|
+
"build:doc": "node build/doc",
|
|
48
|
+
"clean": "npm uninstall -g ui5-test-runner puppeteer nyc selenium-webdriver playwright webdriverio"
|
|
30
49
|
},
|
|
31
50
|
"repository": {
|
|
32
51
|
"type": "git",
|
|
@@ -48,17 +67,23 @@
|
|
|
48
67
|
},
|
|
49
68
|
"homepage": "https://github.com/ArnaudBuchholz/ui5-test-runner#readme",
|
|
50
69
|
"dependencies": {
|
|
51
|
-
"commander": "^
|
|
70
|
+
"commander": "^12.0.0",
|
|
52
71
|
"mime": "^3.0.0",
|
|
53
72
|
"punybind": "^1.2.1",
|
|
54
73
|
"punyexpr": "^1.0.4",
|
|
55
|
-
"reserve": "^1.15.
|
|
74
|
+
"reserve": "^1.15.6"
|
|
56
75
|
},
|
|
57
76
|
"devDependencies": {
|
|
77
|
+
"@openui5/types": "^1.120.6",
|
|
78
|
+
"@ui5/cli": "^3.9.0",
|
|
79
|
+
"@ui5/middleware-code-coverage": "^1.1.1",
|
|
58
80
|
"jest": "^29.7.0",
|
|
59
|
-
"nock": "^13.
|
|
81
|
+
"nock": "^13.5.1",
|
|
60
82
|
"nyc": "^15.1.0",
|
|
61
|
-
"standard": "^17.1.0"
|
|
83
|
+
"standard": "^17.1.0",
|
|
84
|
+
"start-server-and-test": "^2.0.3",
|
|
85
|
+
"typescript": "^5.3.3",
|
|
86
|
+
"ui5-tooling-transpile": "^3.3.3"
|
|
62
87
|
},
|
|
63
88
|
"optionalDependencies": {
|
|
64
89
|
"fsevents": "^2.3.3"
|
|
@@ -69,6 +94,10 @@
|
|
|
69
94
|
"qunit",
|
|
70
95
|
"node",
|
|
71
96
|
"jest"
|
|
97
|
+
],
|
|
98
|
+
"globals": [
|
|
99
|
+
"sap",
|
|
100
|
+
"opaTest"
|
|
72
101
|
]
|
|
73
102
|
},
|
|
74
103
|
"jest": {
|
|
@@ -98,4 +127,4 @@
|
|
|
98
127
|
}
|
|
99
128
|
}
|
|
100
129
|
}
|
|
101
|
-
}
|
|
130
|
+
}
|
package/src/browsers.js
CHANGED
|
@@ -214,13 +214,16 @@ async function screenshot (job, url, filename) {
|
|
|
214
214
|
if (!job.browserCapabilities.screenshot) {
|
|
215
215
|
throw UTRError.BROWSER_SCREENSHOT_NOT_SUPPORTED()
|
|
216
216
|
}
|
|
217
|
+
const output = getOutput(job)
|
|
218
|
+
const id = ++lastScreenshotId
|
|
217
219
|
try {
|
|
218
220
|
const { childProcess, reportDir } = job[$browsers][url]
|
|
219
221
|
const absoluteFilename = join(reportDir, filename + job.browserCapabilities.screenshot)
|
|
220
222
|
if (childProcess.connected) {
|
|
221
|
-
|
|
223
|
+
output.debug('screenshot', id, url, absoluteFilename)
|
|
222
224
|
const { promise, resolve, reject } = allocPromise()
|
|
223
225
|
screenshots[id] = resolve
|
|
226
|
+
output.debug('screenshot', id, 'sending command')
|
|
224
227
|
childProcess.send({
|
|
225
228
|
id,
|
|
226
229
|
command: 'screenshot',
|
|
@@ -229,15 +232,20 @@ async function screenshot (job, url, filename) {
|
|
|
229
232
|
const timeoutId = setTimeout(() => {
|
|
230
233
|
reject(UTRError.BROWSER_SCREENSHOT_TIMEOUT())
|
|
231
234
|
}, job.screenshotTimeout)
|
|
235
|
+
output.debug('screenshot', id, 'command sent, waiting for answer')
|
|
232
236
|
await promise
|
|
237
|
+
output.debug('screenshot', id, 'answer received')
|
|
233
238
|
clearTimeout(timeoutId)
|
|
234
239
|
const result = await stat(absoluteFilename)
|
|
240
|
+
output.debug('screenshot', id, 'file size :', result.size)
|
|
235
241
|
if (!result.isFile() || result.size === 0) {
|
|
236
242
|
throw new Error('File expected')
|
|
237
243
|
}
|
|
244
|
+
output.debug('screenshot', id, 'done')
|
|
238
245
|
return absoluteFilename
|
|
239
246
|
}
|
|
240
247
|
} catch (e) {
|
|
248
|
+
output.debug('screenshot', id, e.message)
|
|
241
249
|
if (e.code === UTRError.BROWSER_SCREENSHOT_TIMEOUT_CODE) {
|
|
242
250
|
throw e
|
|
243
251
|
}
|
|
@@ -4,7 +4,6 @@ const { check, serve, body } = require('reserve')
|
|
|
4
4
|
const { probe, start, stop } = require('../browsers')
|
|
5
5
|
const { join } = require('path')
|
|
6
6
|
const { getOutput } = require('../output')
|
|
7
|
-
const EventEmitter = require('events')
|
|
8
7
|
const { performance } = require('perf_hooks')
|
|
9
8
|
const { cleanDir, allocPromise, filename } = require('../tools')
|
|
10
9
|
const { $browsers } = require('../symbols')
|
|
@@ -60,13 +59,16 @@ async function capabilities (job) {
|
|
|
60
59
|
const { referer, 'x-page-url': xPageUrl } = request.headers
|
|
61
60
|
const listenerIndex = (xPageUrl || referer).match(/\blistener=(\d+)/)[1]
|
|
62
61
|
const listener = listeners[listenerIndex]
|
|
63
|
-
listener
|
|
62
|
+
await listener({
|
|
64
63
|
endpoint,
|
|
65
64
|
body: JSON.parse(await body(request))
|
|
66
65
|
})
|
|
67
66
|
response.writeHead(200)
|
|
68
67
|
response.end()
|
|
69
68
|
}
|
|
69
|
+
}, {
|
|
70
|
+
match: '^/inject/(.*)',
|
|
71
|
+
file: join(__dirname, '../inject/$1')
|
|
70
72
|
}, {
|
|
71
73
|
match: '^/(.*)',
|
|
72
74
|
file: join(__dirname, '$1')
|
|
@@ -105,8 +107,6 @@ async function capabilities (job) {
|
|
|
105
107
|
const { label, url, scripts, endpoint = () => { } } = filteredTests.shift()
|
|
106
108
|
|
|
107
109
|
const listenerIndex = listeners.length
|
|
108
|
-
const listener = new EventEmitter()
|
|
109
|
-
listeners.push(listener)
|
|
110
110
|
let pageUrl
|
|
111
111
|
if (url.startsWith('http')) {
|
|
112
112
|
pageUrl = url
|
|
@@ -151,7 +151,7 @@ async function capabilities (job) {
|
|
|
151
151
|
const context = {
|
|
152
152
|
job
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
listeners[listenerIndex] = async data => {
|
|
155
155
|
try {
|
|
156
156
|
if (await endpoint.call(context, data, pageUrl) !== false) {
|
|
157
157
|
done()
|
|
@@ -159,7 +159,7 @@ async function capabilities (job) {
|
|
|
159
159
|
} catch (e) {
|
|
160
160
|
done(e)
|
|
161
161
|
}
|
|
162
|
-
}
|
|
162
|
+
}
|
|
163
163
|
|
|
164
164
|
start(job, pageUrl, scripts)
|
|
165
165
|
.catch(reason => done(reason))
|
|
@@ -3,11 +3,21 @@
|
|
|
3
3
|
<body>
|
|
4
4
|
<H1>screenshot</H1>
|
|
5
5
|
<p>Checks if the browser supports screenshot</p>
|
|
6
|
+
<input type="text" id="first" value="first">
|
|
7
|
+
<input type="text" id="second" value="second">
|
|
6
8
|
<span style="font-size: 32rem;">😉</span>
|
|
9
|
+
<script src="/inject/post.js"></script>
|
|
7
10
|
<script>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
document.getElementById('first').focus()
|
|
12
|
+
const post = window['ui5-test-runner/post']
|
|
13
|
+
post('/_/log', { step:'screenshot' })
|
|
14
|
+
.then(() => {
|
|
15
|
+
const current = document.activeElement
|
|
16
|
+
return post('/_/log', {
|
|
17
|
+
step: 'focus',
|
|
18
|
+
current: current ? current.id : ''
|
|
19
|
+
})
|
|
20
|
+
})
|
|
11
21
|
</script>
|
|
12
22
|
</body>
|
|
13
23
|
</html>
|
|
@@ -8,11 +8,17 @@ module.exports = {
|
|
|
8
8
|
label: 'Screenshot',
|
|
9
9
|
for: capabilities => !!capabilities.screenshot,
|
|
10
10
|
url: 'screenshot/index.html',
|
|
11
|
-
endpoint: async function (
|
|
11
|
+
endpoint: async function ({ body: { step, current } }, url) {
|
|
12
12
|
const { job } = this
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
if (step === 'screenshot') {
|
|
14
|
+
const fileName = await screenshot(job, url, 'screenshot')
|
|
15
|
+
const fileInfo = await stat(fileName)
|
|
16
|
+
assert.ok(fileInfo.isFile(), 'The file was generated')
|
|
17
|
+
assert.ok(fileInfo.size > 1024, 'The file contains something')
|
|
18
|
+
return false
|
|
19
|
+
} else {
|
|
20
|
+
assert.strictEqual(current, 'first')
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
17
23
|
}
|
|
18
24
|
}
|
package/src/coverage.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
const { join, dirname, isAbsolute } = require('path')
|
|
4
4
|
const { fork } = require('child_process')
|
|
5
|
-
const { cleanDir, createDir, filename, download } = require('./tools')
|
|
5
|
+
const { cleanDir, createDir, filename, download, allocPromise } = require('./tools')
|
|
6
6
|
const { readdir, readFile, stat, writeFile, access, constants } = require('fs').promises
|
|
7
7
|
const { Readable } = require('stream')
|
|
8
8
|
const { getOutput } = require('./output')
|
|
9
9
|
const { resolvePackage } = require('./npm')
|
|
10
10
|
const { promisify } = require('util')
|
|
11
|
+
const { UTRError } = require('./error')
|
|
12
|
+
const { $remoteOnLegacy } = require('./symbols')
|
|
11
13
|
|
|
12
14
|
const $nycSettingsPath = Symbol('nycSettingsPath')
|
|
13
15
|
const $coverageFileIndex = Symbol('coverageFileIndex')
|
|
@@ -28,9 +30,13 @@ async function nyc (job, ...args) {
|
|
|
28
30
|
output.nyc(...args)
|
|
29
31
|
const childProcess = fork(nycScript, args, { stdio: 'pipe' })
|
|
30
32
|
output.monitor(childProcess)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const { promise, resolve, reject } = allocPromise()
|
|
34
|
+
childProcess.on('close', async code => {
|
|
35
|
+
if (code !== 0) {
|
|
36
|
+
reject(UTRError.NYC_FAILED(`Return code ${code}`))
|
|
37
|
+
}
|
|
38
|
+
resolve()
|
|
39
|
+
})
|
|
34
40
|
return promise
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -70,15 +76,9 @@ async function instrument (job) {
|
|
|
70
76
|
settings.exclude.push(join(job.coverageReportDir, '**'))
|
|
71
77
|
await writeFile(job[$nycSettingsPath], JSON.stringify(settings))
|
|
72
78
|
if (job.mode === 'url') {
|
|
73
|
-
|
|
74
|
-
const useLocal = job.url.some(url => {
|
|
75
|
-
// ignore host name since the machine might be exposed with any name
|
|
76
|
-
const parsedUrl = new URL(url)
|
|
77
|
-
return parsedUrl.port === port
|
|
78
|
-
})
|
|
79
|
-
if (!useLocal) {
|
|
80
|
-
getOutput(job).instrumentationSkipped()
|
|
79
|
+
if (!job[$remoteOnLegacy]) {
|
|
81
80
|
job[$coverageRemote] = true
|
|
81
|
+
getOutput(job).instrumentationSkipped()
|
|
82
82
|
return
|
|
83
83
|
}
|
|
84
84
|
}
|
|
@@ -125,6 +125,9 @@ async function generateCoverageReport (job) {
|
|
|
125
125
|
}
|
|
126
126
|
const checks = []
|
|
127
127
|
if (job.coverageCheckBranches || job.coverageCheckFunctions || job.coverageCheckLines || job.coverageCheckStatements) {
|
|
128
|
+
if (!job.coverageReporters.includes('lcov')) {
|
|
129
|
+
reporters.push('--reporter=lcov')
|
|
130
|
+
}
|
|
128
131
|
checks.push(
|
|
129
132
|
`--branches=${job.coverageCheckBranches}`,
|
|
130
133
|
`--functions=${job.coverageCheckFunctions}`,
|
|
@@ -134,6 +137,13 @@ async function generateCoverageReport (job) {
|
|
|
134
137
|
)
|
|
135
138
|
}
|
|
136
139
|
await nyc(job, 'report', ...reporters, ...checks, '--temp-dir', coverageMergedDir, '--report-dir', job.coverageReportDir, '--nycrc-path', job[$nycSettingsPath])
|
|
140
|
+
if (checks.length) {
|
|
141
|
+
// The checks are not triggered if the coverage is empty
|
|
142
|
+
const lcov = await stat(join(job.coverageReportDir, 'lcov.info'))
|
|
143
|
+
if (lcov.size === 0) {
|
|
144
|
+
throw UTRError.NYC_FAILED('No coverage information extracted')
|
|
145
|
+
}
|
|
146
|
+
}
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
module.exports = {
|
|
@@ -146,18 +156,18 @@ module.exports = {
|
|
|
146
156
|
}
|
|
147
157
|
await writeFile(coverageFileName, JSON.stringify(coverageData))
|
|
148
158
|
},
|
|
149
|
-
generateCoverageReport: job => job.coverage
|
|
159
|
+
generateCoverageReport: job => job.coverage ? generateCoverageReport(job) : Promise.resolve(),
|
|
150
160
|
mappings: async job => {
|
|
151
161
|
if (!job.coverage) {
|
|
152
162
|
return []
|
|
153
163
|
}
|
|
154
164
|
const instrumentedBasePath = join(job.coverageTempDir, 'instrumented')
|
|
155
165
|
const instrumentedMapping = {
|
|
156
|
-
match:
|
|
166
|
+
match: /(.*\.js)(\?.*)?$/,
|
|
157
167
|
file: join(instrumentedBasePath, '$1'),
|
|
158
168
|
'ignore-if-not-found': true
|
|
159
169
|
}
|
|
160
|
-
if (job.mode === 'legacy') {
|
|
170
|
+
if (job.mode === 'legacy' || job[$remoteOnLegacy]) {
|
|
161
171
|
return [{
|
|
162
172
|
...instrumentedMapping,
|
|
163
173
|
'custom-file-system': job.debugCoverageNoCustomFs ? undefined : customFileSystem
|
package/src/coverage.spec.js
CHANGED
|
@@ -4,6 +4,7 @@ const { instrument, generateCoverageReport, mappings } = require('./coverage')
|
|
|
4
4
|
const { stat } = require('fs/promises')
|
|
5
5
|
const { cleanDir, createDir } = require('./tools')
|
|
6
6
|
const { getOutput } = require('./output')
|
|
7
|
+
const { $remoteOnLegacy } = require('./symbols')
|
|
7
8
|
|
|
8
9
|
describe('src/coverage', () => {
|
|
9
10
|
const cwd = join(__dirname, '../test/project')
|
|
@@ -108,7 +109,8 @@ describe('src/coverage', () => {
|
|
|
108
109
|
Object.assign(job, {
|
|
109
110
|
mode: 'url',
|
|
110
111
|
port: 8080,
|
|
111
|
-
url: ['http://localhost:8080/whatever/test.html']
|
|
112
|
+
url: ['http://localhost:8080/whatever/test.html'],
|
|
113
|
+
[$remoteOnLegacy]: true // added on job finalization
|
|
112
114
|
})
|
|
113
115
|
await instrument(job)
|
|
114
116
|
expect(instrumentationSkipped).not.toHaveBeenCalled()
|
|
@@ -88,7 +88,9 @@ require('./browser')({
|
|
|
88
88
|
networkWriter
|
|
89
89
|
}) {
|
|
90
90
|
const browsers = require(modules.playwright)
|
|
91
|
-
browser = await browsers[options.browser].launch(
|
|
91
|
+
browser = await browsers[options.browser].launch({
|
|
92
|
+
headless: !options.visible
|
|
93
|
+
})
|
|
92
94
|
|
|
93
95
|
let recordVideo
|
|
94
96
|
if (options.video) {
|
|
@@ -11,27 +11,13 @@ require('./browser')({
|
|
|
11
11
|
['-w, --viewport-width <width>', 'Viewport width', 1920],
|
|
12
12
|
['-h, --viewport-height <height>', 'Viewport height', 1080],
|
|
13
13
|
['-l, --language <lang...>', 'Language(s)', ['en-US']],
|
|
14
|
-
['-u, --unsecure', 'Disable security features', false]
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// screenshot: '.png',
|
|
20
|
-
// scripts: true,
|
|
21
|
-
// traces: ['console', 'network']
|
|
22
|
-
// }
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
// TODO remove when Node16 is no more supported
|
|
26
|
-
async capabilities () {
|
|
27
|
-
const version = process.version.match(/^v(\d+\.\d+)/)[1]
|
|
28
|
-
let screenshot
|
|
29
|
-
if (!version.startsWith('16')) {
|
|
30
|
-
screenshot = '.png'
|
|
31
|
-
}
|
|
32
|
-
return {
|
|
14
|
+
['-u, --unsecure', 'Disable security features', false],
|
|
15
|
+
['--basic-auth-username <username>', 'Username for basic authentication', ''],
|
|
16
|
+
['--basic-auth-password <password>', 'Password for basic authentication', '']
|
|
17
|
+
],
|
|
18
|
+
capabilities: {
|
|
33
19
|
modules: ['puppeteer'],
|
|
34
|
-
screenshot,
|
|
20
|
+
screenshot: '.png',
|
|
35
21
|
scripts: true,
|
|
36
22
|
traces: ['console', 'network']
|
|
37
23
|
}
|
|
@@ -94,6 +80,10 @@ require('./browser')({
|
|
|
94
80
|
await page.setBypassCSP(true)
|
|
95
81
|
}
|
|
96
82
|
|
|
83
|
+
if (options.basicAuthUsername || options.basicAuthPassword) {
|
|
84
|
+
await page.authenticate({ username: options.basicAuthUsername, password: options.basicAuthPassword })
|
|
85
|
+
}
|
|
86
|
+
|
|
97
87
|
page
|
|
98
88
|
.on('console', message => consoleWriter.append({
|
|
99
89
|
type: message.type(),
|
package/src/error.js
CHANGED
package/src/job.js
CHANGED
|
@@ -5,9 +5,9 @@ const { statSync, accessSync, constants } = require('fs')
|
|
|
5
5
|
const { dirname, join, isAbsolute } = require('path')
|
|
6
6
|
const { name, description, version } = require(join(__dirname, '../package.json'))
|
|
7
7
|
const { getOutput } = require('./output')
|
|
8
|
-
const { $valueSources } = require('./symbols')
|
|
8
|
+
const { $valueSources, $remoteOnLegacy } = require('./symbols')
|
|
9
9
|
const { buildAndCheckMode } = require('./job-mode')
|
|
10
|
-
const { boolean, integer, timeout, url, arrayOf, regex, percent } = require('./options')
|
|
10
|
+
const { boolean, integer, timeout, url, arrayOf, regex, percent, string } = require('./options')
|
|
11
11
|
|
|
12
12
|
const $status = Symbol('status')
|
|
13
13
|
|
|
@@ -107,6 +107,7 @@ function getCommand (cwd) {
|
|
|
107
107
|
.option('-br, --browser-retry <count>', '[💻🔗🧪] Browser instantiation retries : if the command fails unexpectedly, it is re-executed (0 means no retry)', 1)
|
|
108
108
|
|
|
109
109
|
// Common to legacy and url
|
|
110
|
+
.option('-qs, --qunit-strict', '[💻🔗] Strict mode on qunit execution (fails if no modules declared)', boolean, false)
|
|
110
111
|
.option('-pf, --page-filter <regexp>', '[💻🔗] Filter out pages not matching the regexp')
|
|
111
112
|
.option('-pp, --page-params <params>', '[💻🔗] Add parameters to page URL')
|
|
112
113
|
.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)
|
|
@@ -114,7 +115,7 @@ function getCommand (cwd) {
|
|
|
114
115
|
.option('--no-screenshot', '[💻🔗] Disable screenshots')
|
|
115
116
|
.option('-st, --screenshot-timeout <timeout>', '[💻🔗] Maximum waiting time for browser screenshot', timeout, 5000)
|
|
116
117
|
.option('-rg, --report-generator <path...>', '[💻🔗] Report generator paths (relative to cwd or use $/ for provided ones)', ['$/report.js'])
|
|
117
|
-
.option('
|
|
118
|
+
.option('--progress-page <path>', '[💻🔗] progress page path (relative to cwd or use $/ for provided ones)', '$/report/default.html')
|
|
118
119
|
|
|
119
120
|
.option('--coverage [flag]', '[💻🔗] Enable or disable code coverage', boolean)
|
|
120
121
|
.option('--no-coverage', '[💻🔗] Disable code coverage')
|
|
@@ -135,7 +136,7 @@ function getCommand (cwd) {
|
|
|
135
136
|
.option('--mappings <mapping...>', '[💻] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
|
|
136
137
|
.option('--cache <path>', '[💻] Cache UI5 resources locally in the given folder (empty to disable)')
|
|
137
138
|
.option('--webapp <path>', '[💻] Base folder of the web application (relative to cwd)', 'webapp')
|
|
138
|
-
.option('--testsuite <path>', '[💻] Path of the testsuite file (relative to webapp)', 'test/testsuite.qunit.html')
|
|
139
|
+
.option('--testsuite <path>', '[💻] Path of the testsuite file (relative to webapp, URL parameters are supported)', 'test/testsuite.qunit.html')
|
|
139
140
|
.option('-w, --watch [flag]', '[💻] Monitor the webapp folder and re-execute tests on change', boolean, false)
|
|
140
141
|
|
|
141
142
|
// Specific to coverage in url mode (experimental)
|
|
@@ -151,6 +152,7 @@ function getCommand (cwd) {
|
|
|
151
152
|
.addOption(new Option('--debug-capabilities-no-timeout', DEBUG_OPTION, boolean).hideHelp())
|
|
152
153
|
.addOption(new Option('--debug-coverage', DEBUG_OPTION, boolean).hideHelp())
|
|
153
154
|
.addOption(new Option('--debug-coverage-no-custom-fs', DEBUG_OPTION, boolean).hideHelp())
|
|
155
|
+
.addOption(new Option('--debug-verbose <module...>', DEBUG_OPTION, arrayOf(string), []).hideHelp())
|
|
154
156
|
|
|
155
157
|
return command
|
|
156
158
|
}
|
|
@@ -226,7 +228,9 @@ function finalize (job) {
|
|
|
226
228
|
job.mode = buildAndCheckMode(job)
|
|
227
229
|
if (job.mode === 'legacy') {
|
|
228
230
|
checkAccess({ path: job.webapp, label: 'webapp folder' })
|
|
229
|
-
|
|
231
|
+
|
|
232
|
+
const [, testsuiteFile] = job.testsuite.match(/([^?]*)(\?.*)?$/)
|
|
233
|
+
const testsuitePath = toAbsolute(testsuiteFile, job.webapp)
|
|
230
234
|
checkAccess({ path: testsuitePath, label: 'testsuite', file: true })
|
|
231
235
|
} else if (job.mode === 'url') {
|
|
232
236
|
if (job[$valueSources].coverage !== 'cli') {
|
|
@@ -287,6 +291,15 @@ function finalize (job) {
|
|
|
287
291
|
overrideIfNotSet('coverageReporters', settings.reporter)
|
|
288
292
|
}
|
|
289
293
|
|
|
294
|
+
if (job.mode === 'url') {
|
|
295
|
+
const port = job.port.toString()
|
|
296
|
+
job[$remoteOnLegacy] = job.url.every(url => {
|
|
297
|
+
// ignore host name since the machine might be exposed with any name
|
|
298
|
+
const parsedUrl = new URL(url)
|
|
299
|
+
return parsedUrl.port === port
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
290
303
|
job[$status] = 'Starting'
|
|
291
304
|
Object.defineProperty(job, 'status', {
|
|
292
305
|
get () {
|
package/src/job.spec.js
CHANGED
|
@@ -211,6 +211,12 @@ describe('job', () => {
|
|
|
211
211
|
})).toThrow()
|
|
212
212
|
})
|
|
213
213
|
|
|
214
|
+
it('supports parameters for testsuite (stripping ?)', () => {
|
|
215
|
+
expect(() => buildJob({
|
|
216
|
+
testsuite: 'test/testsuite.qunit.html?a=b'
|
|
217
|
+
})).not.toThrow()
|
|
218
|
+
})
|
|
219
|
+
|
|
214
220
|
it('fails on a missing file (points to a folder)', () => {
|
|
215
221
|
expect(() => buildJob({
|
|
216
222
|
testsuite: 'lib'
|
package/src/options.js
CHANGED
package/src/output.js
CHANGED
|
@@ -225,6 +225,12 @@ function build (job) {
|
|
|
225
225
|
log(job, p80()`Server running at ${pad.lt(url)}`)
|
|
226
226
|
},
|
|
227
227
|
|
|
228
|
+
debug: wrap((module, ...args) => {
|
|
229
|
+
if (job.debugVerbose && job.debugVerbose.includes(module)) {
|
|
230
|
+
console.log(`🐞${module}`, ...args)
|
|
231
|
+
}
|
|
232
|
+
}),
|
|
233
|
+
|
|
228
234
|
redirected: wrap(({ method, url, statusCode, timeSpent }) => {
|
|
229
235
|
if (url.startsWith('/_/progress')) {
|
|
230
236
|
return // avoids pollution
|
|
@@ -412,6 +418,10 @@ function build (job) {
|
|
|
412
418
|
log(job, p80()`Skipping nyc instrumentation (--url)`)
|
|
413
419
|
}),
|
|
414
420
|
|
|
421
|
+
qunitEarlyStart: wrap(url => {
|
|
422
|
+
log(job, p80()`QUnit start without tests in ${pad.lt(url)}`)
|
|
423
|
+
}),
|
|
424
|
+
|
|
415
425
|
endpointError: wrap(({ api, url, data, error }) => {
|
|
416
426
|
const p = p80()
|
|
417
427
|
log(job, p`┌──────────${pad.x('─')}┐`)
|
package/src/qunit-hooks.js
CHANGED
|
@@ -13,27 +13,40 @@ function error (job, url, details = '') {
|
|
|
13
13
|
throw UTRError.QUNIT_ERROR(details)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function invalidTestId (job, url, testId) {
|
|
17
|
+
error(job, url, `No QUnit unit test found with id ${testId}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
function get (job, urlWithHash, testId) {
|
|
17
21
|
const url = stripUrlHash(urlWithHash)
|
|
18
22
|
const page = job.qunitPages && job.qunitPages[url]
|
|
19
23
|
if (!page) {
|
|
20
24
|
error(job, url, `No QUnit page found for ${urlWithHash}`)
|
|
21
25
|
}
|
|
26
|
+
let testModule
|
|
22
27
|
let test
|
|
23
28
|
if (testId !== undefined) {
|
|
24
29
|
page.modules.every(module => {
|
|
25
30
|
test = module.tests.find(test => test.testId === testId)
|
|
26
|
-
|
|
31
|
+
if (test === undefined) {
|
|
32
|
+
return true
|
|
33
|
+
} else {
|
|
34
|
+
testModule = module
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
27
37
|
})
|
|
28
|
-
if (!test) {
|
|
29
|
-
|
|
38
|
+
if (!test && job.qunitStrict) {
|
|
39
|
+
invalidTestId(job, url, testId)
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
|
-
return { url, page, test }
|
|
42
|
+
return { url, page, testModule, test }
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
async function done (job, urlWithHash, report) {
|
|
36
46
|
const { url, page } = get(job, urlWithHash)
|
|
47
|
+
if (page.earlyStart && page.count === 0) {
|
|
48
|
+
return // wait
|
|
49
|
+
}
|
|
37
50
|
if (job.browserCapabilities.screenshot) {
|
|
38
51
|
try {
|
|
39
52
|
await screenshot(job, url, 'done')
|
|
@@ -41,11 +54,11 @@ async function done (job, urlWithHash, report) {
|
|
|
41
54
|
getOutput(job).genericError(error, url)
|
|
42
55
|
}
|
|
43
56
|
}
|
|
57
|
+
page.end = new Date()
|
|
44
58
|
if (report.__coverage__) {
|
|
45
|
-
collect(job, url, report.__coverage__)
|
|
59
|
+
await collect(job, url, report.__coverage__)
|
|
46
60
|
delete report.__coverage__
|
|
47
61
|
}
|
|
48
|
-
page.end = new Date()
|
|
49
62
|
page.report = report
|
|
50
63
|
stop(job, url)
|
|
51
64
|
}
|
|
@@ -55,8 +68,12 @@ module.exports = {
|
|
|
55
68
|
|
|
56
69
|
async begin (job, urlWithHash, { isOpa, totalTests, modules }) {
|
|
57
70
|
const url = stripUrlHash(urlWithHash)
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
const earlyStart = !totalTests || !modules
|
|
72
|
+
if (earlyStart) {
|
|
73
|
+
getOutput(job).qunitEarlyStart(url)
|
|
74
|
+
if (job.qunitStrict) {
|
|
75
|
+
error(job, url, 'Invalid begin hook details')
|
|
76
|
+
}
|
|
60
77
|
}
|
|
61
78
|
if (!job.qunitPages) {
|
|
62
79
|
job.qunitPages = {}
|
|
@@ -70,16 +87,31 @@ module.exports = {
|
|
|
70
87
|
count: totalTests,
|
|
71
88
|
modules
|
|
72
89
|
}
|
|
90
|
+
if (earlyStart) {
|
|
91
|
+
qunitPage.earlyStart = true
|
|
92
|
+
}
|
|
73
93
|
job.qunitPages[url] = qunitPage
|
|
74
94
|
},
|
|
75
95
|
|
|
76
96
|
async testStart (job, urlWithHash, { module, name, testId }) {
|
|
77
|
-
|
|
97
|
+
let { page, testModule, test } = get(job, urlWithHash, testId)
|
|
98
|
+
if (!testModule) {
|
|
99
|
+
testModule = { name: module, tests: [] }
|
|
100
|
+
page.modules.push(testModule)
|
|
101
|
+
}
|
|
102
|
+
if (!test) {
|
|
103
|
+
test = { name, testId }
|
|
104
|
+
testModule.tests.push(test)
|
|
105
|
+
++page.count
|
|
106
|
+
}
|
|
78
107
|
test.start = new Date()
|
|
79
108
|
},
|
|
80
109
|
|
|
81
110
|
async log (job, urlWithHash, { module, name, testId, ...log }) {
|
|
82
111
|
const { url, page, test } = get(job, urlWithHash, testId)
|
|
112
|
+
if (!test) {
|
|
113
|
+
invalidTestId(job, url, testId)
|
|
114
|
+
}
|
|
83
115
|
if (!test.logs) {
|
|
84
116
|
test.logs = []
|
|
85
117
|
}
|
|
@@ -97,6 +129,9 @@ module.exports = {
|
|
|
97
129
|
async testDone (job, urlWithHash, { name, module, testId, assertions, ...report }) {
|
|
98
130
|
const { failed } = report
|
|
99
131
|
const { url, page, test } = get(job, urlWithHash, testId)
|
|
132
|
+
if (!test) {
|
|
133
|
+
invalidTestId(job, url, testId)
|
|
134
|
+
}
|
|
100
135
|
if (failed) {
|
|
101
136
|
if (job.browserCapabilities.screenshot) {
|
|
102
137
|
try {
|
package/src/qunit-hooks.spec.js
CHANGED
|
@@ -10,10 +10,12 @@ jest.mock('./coverage.js', () => ({
|
|
|
10
10
|
const { collect } = require('./coverage')
|
|
11
11
|
|
|
12
12
|
const mockGenericError = jest.fn()
|
|
13
|
+
const mockQunitEarlyStart = jest.fn()
|
|
13
14
|
|
|
14
15
|
jest.mock('./output.js', () => ({
|
|
15
16
|
getOutput: () => ({
|
|
16
|
-
genericError: mockGenericError
|
|
17
|
+
genericError: mockGenericError,
|
|
18
|
+
qunitEarlyStart: mockQunitEarlyStart
|
|
17
19
|
})
|
|
18
20
|
}))
|
|
19
21
|
|
|
@@ -174,7 +176,11 @@ describe('src/qunit-hooks', () => {
|
|
|
174
176
|
})
|
|
175
177
|
})
|
|
176
178
|
|
|
177
|
-
describe('validation', () => {
|
|
179
|
+
describe('validation (--qunit-strict)', () => {
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
job.qunitStrict = true
|
|
182
|
+
})
|
|
183
|
+
|
|
178
184
|
afterEach(() => {
|
|
179
185
|
expect(stop).toHaveBeenCalledWith(job, url)
|
|
180
186
|
expect(job.failed).toStrictEqual(true)
|
|
@@ -185,6 +191,7 @@ describe('src/qunit-hooks', () => {
|
|
|
185
191
|
isOpa: false,
|
|
186
192
|
modules: getModules()
|
|
187
193
|
})).rejects.toThrow(UTRError.QUNIT_ERROR('Invalid begin hook details'))
|
|
194
|
+
expect(mockQunitEarlyStart).toHaveBeenCalled()
|
|
188
195
|
})
|
|
189
196
|
|
|
190
197
|
it('requires modules', async () => {
|
|
@@ -192,6 +199,7 @@ describe('src/qunit-hooks', () => {
|
|
|
192
199
|
isOpa: false,
|
|
193
200
|
totalTests: 1
|
|
194
201
|
})).rejects.toThrow(UTRError.QUNIT_ERROR('Invalid begin hook details'))
|
|
202
|
+
expect(mockQunitEarlyStart).toHaveBeenCalled()
|
|
195
203
|
})
|
|
196
204
|
})
|
|
197
205
|
})
|
|
@@ -594,7 +602,18 @@ describe('src/qunit-hooks', () => {
|
|
|
594
602
|
expect(job.failed).toStrictEqual(true)
|
|
595
603
|
})
|
|
596
604
|
|
|
597
|
-
it('fails on invalid test id', async () => {
|
|
605
|
+
it('fails on invalid test id (--qunit-strict)', async () => {
|
|
606
|
+
job.qunitStrict = true
|
|
607
|
+
await expect(testDone(job, url, {
|
|
608
|
+
...getTestDoneFor1a(),
|
|
609
|
+
testId: '1c'
|
|
610
|
+
}))
|
|
611
|
+
.rejects.toThrow(UTRError.QUNIT_ERROR('No QUnit unit test found with id 1c'))
|
|
612
|
+
expect(stop).toHaveBeenCalledWith(job, url)
|
|
613
|
+
expect(job.failed).toStrictEqual(true)
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it('fails on invalid test id (--no-qunit-strict)', async () => {
|
|
598
617
|
await expect(testDone(job, url, {
|
|
599
618
|
...getTestDoneFor1a(),
|
|
600
619
|
testId: '1c'
|
package/src/report.js
CHANGED
|
@@ -51,7 +51,10 @@ module.exports = {
|
|
|
51
51
|
})
|
|
52
52
|
return promise
|
|
53
53
|
})
|
|
54
|
-
promises.push(generateCoverageReport(job)
|
|
54
|
+
promises.push(generateCoverageReport(job).catch(e => {
|
|
55
|
+
output.genericError(e)
|
|
56
|
+
job.failed = true
|
|
57
|
+
}))
|
|
55
58
|
await Promise.all(promises)
|
|
56
59
|
job.status = 'Done'
|
|
57
60
|
}
|
package/src/simulate.spec.js
CHANGED
|
@@ -213,6 +213,31 @@ describe('simulate', () => {
|
|
|
213
213
|
})
|
|
214
214
|
})
|
|
215
215
|
|
|
216
|
+
describe('simple test execution (--no-qunit-strict)', () => {
|
|
217
|
+
beforeAll(async () => {
|
|
218
|
+
await setup('simple-early')
|
|
219
|
+
pages = {
|
|
220
|
+
'testsuite.qunit.html': async referer => {
|
|
221
|
+
await post('/_/addTestPages', referer, [
|
|
222
|
+
referer.replace('testsuite.qunit.html', 'page1.html')
|
|
223
|
+
])
|
|
224
|
+
},
|
|
225
|
+
'page1.html': async referer => {
|
|
226
|
+
await post('/_/QUnit/begin', referer, { totalTests: 0, modules: [] })
|
|
227
|
+
await post('/_/QUnit/done', referer, { failed: 0 })
|
|
228
|
+
await post('/_/QUnit/testStart', referer, { module: 'module', name: 'test', testId: '1' })
|
|
229
|
+
await post('/_/QUnit/testDone', referer, { testId: '1', failed: 0, passed: 1 })
|
|
230
|
+
await post('/_/QUnit/done', referer, { failed: 0 })
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
await safeExecute()
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('succeeded', () => {
|
|
237
|
+
expect(job.failed).toStrictEqual(false)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
216
241
|
describe('error', () => {
|
|
217
242
|
describe('one test fail', () => {
|
|
218
243
|
beforeAll(async () => {
|
package/src/symbols.js
CHANGED
|
@@ -4,5 +4,6 @@ module.exports = {
|
|
|
4
4
|
$probeUrlsCompleted: Symbol('probeUrlsCompleted'),
|
|
5
5
|
$testPagesStarted: Symbol('testPagesStarted'),
|
|
6
6
|
$testPagesCompleted: Symbol('testPagesCompleted'),
|
|
7
|
-
$valueSources: Symbol('valueSources')
|
|
7
|
+
$valueSources: Symbol('valueSources'),
|
|
8
|
+
$remoteOnLegacy: Symbol('remoteOnLegacy')
|
|
8
9
|
}
|