ui5-test-runner 1.0.7 → 1.1.3
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 +26 -7
- package/defaults/chromium.js +20 -5
- package/index.js +8 -7
- package/package.json +13 -8
- package/src/browsers.js +49 -8
- package/src/coverage.js +13 -4
- package/src/endpoints.js +66 -22
- package/src/inject/qunit-hooks.js +55 -6
- package/src/job.js +20 -2
- package/src/output.js +199 -0
- package/src/report.html +165 -52
- package/src/reserve.js +1 -1
- package/src/tests.js +21 -7
- package/src/ui5.js +2 -1
- package/src/unhandled.js +27 -12
package/README.md
CHANGED
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_shield)
|
|
11
11
|
|
|
12
|
-
A test runner for UI5 applications enabling parallel execution of tests.
|
|
12
|
+
A self-sufficient test runner for UI5 applications enabling parallel execution of tests.
|
|
13
13
|
|
|
14
|
-
> To put it in a nutshell, some applications have so many tests that when you run them in a browser, it ends up **crashing**. The main reason is **memory consumption** : the browser process goes up to 2 GB and it
|
|
14
|
+
> To put it in a nutshell, some applications have so many tests that when you run them in a browser, it ends up **crashing**. The main reason is **memory consumption** : the browser process goes up to 2 GB and it blows up. JavaScript is using garbage collecting but it needs time to operate and the stress caused by executing the tests does not let enough bandwidth for the browser to free up the memory.
|
|
15
15
|
|
|
16
16
|
> This tool is designed and built as a **substitute** of the [UI5 karma runner](https://github.com/SAP/karma-ui5). It executes all the tests in **parallel** thanks to several browser instances *(which also **reduces the total execution time**)*.
|
|
17
17
|
|
|
18
18
|
## Documentation
|
|
19
19
|
|
|
20
|
-
*
|
|
20
|
+
* Initial concept is detailed in the article [REserve - Testing UI5](https://arnaud-buchholz.medium.com/reserve-testing-ui5-85187d5eb7f1)
|
|
21
21
|
* Tool was presented during UI5Con'21 : [A different approach to UI5 tests execution](https://youtu.be/EBp0bdIqu4s)
|
|
22
22
|
|
|
23
23
|
## How to install
|
|
@@ -29,8 +29,11 @@ A test runner for UI5 applications enabling parallel execution of tests.
|
|
|
29
29
|
|
|
30
30
|
* Clone the project [training-ui5con18-opa](https://github.com/ArnaudBuchholz/training-ui5con18-opa) and run `npm install`
|
|
31
31
|
* Inside the project, use `npm run karma` to test with the karma runner
|
|
32
|
-
* Inside the project, use `ui5-test-runner -port:8080 -ui5:https://ui5.sap.com/1.87.0/ -cache:.ui5`
|
|
33
|
-
*
|
|
32
|
+
* Inside the project, use `ui5-test-runner -port:8080 -ui5:https://ui5.sap.com/1.87.0/ -cache:.ui5 -keepAlive`
|
|
33
|
+
* Follow the progress of the test executions using http://localhost:8080/_/progress.html
|
|
34
|
+
* When the tests are completed, inspect the results by opening :
|
|
35
|
+
- http://localhost:8080/_/report.html
|
|
36
|
+
- http://localhost:8080/_/coverage/lcov-report/index.html
|
|
34
37
|
|
|
35
38
|
## How to use
|
|
36
39
|
|
|
@@ -69,7 +72,7 @@ The list of options is detailed below but to explain the command :
|
|
|
69
72
|
|
|
70
73
|
* `-cache:.ui5` : caches UI5 resources to boost loading of pages. It stores resources in a project folder named `.ui5` *(you may use an absolute path if preferred)*.
|
|
71
74
|
|
|
72
|
-
* `-libs:my/namespace/feature/lib/=../my.namespace.feature.project.lib/src/my/namespace/feature/lib/` : maps the library path (access to URL `/resources/my/namespace/feature/lib/library.js` will be mapped to the file path
|
|
75
|
+
* `-libs:my/namespace/feature/lib/=../my.namespace.feature.project.lib/src/my/namespace/feature/lib/` : maps the library path (access to URL `/resources/my/namespace/feature/lib/library.js` will be mapped to the file path `../my.namespace.feature.project.lib/src/my/namespace/feature/lib/library.js`)
|
|
73
76
|
|
|
74
77
|
You may also use :
|
|
75
78
|
* `-ui5:https://ui5.sap.com/1.92.1/` : uses a specific version of UI5
|
|
@@ -78,7 +81,7 @@ You may also use :
|
|
|
78
81
|
|
|
79
82
|
* `"-args:__URL__ __REPORT__ --visible"` : changes the browser spawning command line to make the browser windows **visible**
|
|
80
83
|
|
|
81
|
-
* `-parallel:3` : increases *(changes)* the number of parallel execution *(by default it uses 2)*. You may even use `0` to only serve the application *(
|
|
84
|
+
* `-parallel:3` : increases *(changes)* the number of parallel execution *(by default it uses 2)*. You may even use `0` to only serve the application *(the tests are not executed)*.
|
|
82
85
|
|
|
83
86
|
* `-keepAlive` : the server remains active after executing the tests
|
|
84
87
|
|
|
@@ -126,6 +129,7 @@ You may also use :
|
|
|
126
129
|
| libs | | Folder(s) containing dependent libraries *(relative to `cwd`)*.<br/>Might be used multiple times, two syntaxes are supported :<ul><li>`-libs:path` adds `path` to the list of libraries, mapped directly under `/resources/`</li><li>`-libs:rel/=path` adds the `path` to the list of libraries, mapped under `/resources/rel/`</li></ul> |
|
|
127
130
|
| cache | `''` | Cache UI5 resources locally in the given folder *(empty to disable)* |
|
|
128
131
|
| webapp | `'webapp'` | base folder of the web application *(relative to `cwd`)* |
|
|
132
|
+
| testsuite | `'test/testsuite.qunit.html'` | path / URL to the testsuite file *(relative to `webapp`)* |
|
|
129
133
|
| pageFilter | `''` | [regexp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) to select which pages to execute |
|
|
130
134
|
| pageParams | `''` | Parameters added to each page URL.<br/>For instance : `'sap-ui-theme=sap_belize&sap-ui-debug=true'` |
|
|
131
135
|
| pageTimeout | `0` | Limit the page execution time (ms), fails the page if it takes longer than the timeout (`0` to disable the timeout) |
|
|
@@ -137,6 +141,7 @@ You may also use :
|
|
|
137
141
|
| browser | *String, see description* | Browser instantiation command, it should point to a node.js script *(absolute or relative to `cwd`)*.<br/>By default, a script will instantiate chromium through puppetteer |
|
|
138
142
|
| browserRetry | `1` | Browser instantiation retries : if the command **fails** unexpectedly, it is re-executed *(`0` means no retry)*.<br/>The page **fails** if **all attempts** fail |
|
|
139
143
|
| args | `'__URL__ __REPORT__'` | Browser instantiation arguments :<ul><li>`'__URL__'` is replaced with the URL to open</li><li>`'__REPORT__'` is replaced with a folder path that is associated with the current URL *(can be used to store additional traces such as console logs or screenshots)*</li><li>`'__RETRY__'` is replaced with the retry count *(0 for the first execution, can be used to put additional traces or change strategy)*</i>*</li></ul> |
|
|
144
|
+
| noScreenshot | `false` | No screenshot is taken during the tests execution (faster if the browser command supports screenshot) |
|
|
140
145
|
| -- | | Parameters given right after `--` are directly added to the browser instantiation arguments *(see below)* |
|
|
141
146
|
| parallel | `2` | Number of parallel tests executions (`0` to ignore tests and keep alive) |
|
|
142
147
|
| tstReportDir | `'report'` | Directory to output test reports *(relative to `cwd`)* |
|
|
@@ -187,10 +192,24 @@ For instance :
|
|
|
187
192
|
|
|
188
193
|
> Structure of the `libs` parameter
|
|
189
194
|
|
|
195
|
+
## Tips & tricks
|
|
196
|
+
|
|
197
|
+
* The runner takes a screenshot for **every** OPA assertion (`Opa5.assert.ok`)
|
|
198
|
+
* To benefit from **parallelization**, split the OPA test pages per journey.<br> An example pattern :
|
|
199
|
+
- **Declare** the list of journeys in a json file : [`AllJourneys.json`](https://github.com/ArnaudBuchholz/training-ui5con18-opa/blob/master/webapp/test/integration/AllJourneys.json)
|
|
200
|
+
- **Enumerate** `AllJourneys.json` in [`testsuite.qunit.html`](https://github.com/ArnaudBuchholz/training-ui5con18-opa/blob/master/webapp/test/testsuite.qunit.html#L17) to declare as many pages as journeys
|
|
201
|
+
- **Modify** [`AllJourneys.js`](https://github.com/ArnaudBuchholz/training-ui5con18-opa/blob/master/webapp/test/integration/AllJourneys.js#L26) to support a `journey=` parameter and list all declared journeys
|
|
202
|
+
|
|
190
203
|
## Building a custom browser instantiation command
|
|
191
204
|
|
|
192
205
|
* You may follow the pattern being used by [`chromium.js`](https://github.com/ArnaudBuchholz/ui5-test-runner/blob/main/defaults/chromium.js)
|
|
193
206
|
* It is **mandatory** to ensure that the child process explicitly exits at some point *(see this [thread](https://github.com/nodejs/node-v0.x-archive/issues/2605) explaining the fork behavior with Node.js)*
|
|
207
|
+
* The child process will receive messages that must be handled appropriately :
|
|
208
|
+
- `message.command === 'stop'` : the browser must be closed and the command line must exit
|
|
209
|
+
- `message.command === 'capabilities'` : a message must be sent back to indicate if the following features are supported *(boolean)*
|
|
210
|
+
- `screenshot` : the browser can take screenshots (in the `__REPORT__` folder, name is provided when needed)
|
|
211
|
+
- `consoleLog` : the browser serializes the console traces (in the `__REPORT__` folder with the name `console.txt`)
|
|
212
|
+
- `message.command === 'screenshot'` : should generate a screenshot (the message contains a `filename` member). To indicate that the screenshot is done, the command line must send back the same message (`process.send(message)`).
|
|
194
213
|
|
|
195
214
|
## License
|
|
196
215
|
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_large)
|
package/defaults/chromium.js
CHANGED
|
@@ -16,20 +16,35 @@ if (reportDir) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
process.on('message', async message => {
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
await
|
|
19
|
+
try {
|
|
20
|
+
if (message.command === 'stop') {
|
|
21
|
+
await browser.close()
|
|
22
|
+
process.exit(0)
|
|
23
|
+
} else if (message.command === 'screenshot') {
|
|
24
|
+
if (reportDir && page) {
|
|
25
|
+
await page.screenshot({ path: join(reportDir, message.filename) })
|
|
26
|
+
process.send(message)
|
|
27
|
+
}
|
|
28
|
+
} else if (message.command === 'capabilities') {
|
|
29
|
+
process.send({
|
|
30
|
+
command: 'capabilities',
|
|
31
|
+
screenshot: true,
|
|
32
|
+
consoleLog: true
|
|
33
|
+
})
|
|
22
34
|
}
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(e)
|
|
37
|
+
process.exit(-2)
|
|
25
38
|
}
|
|
26
39
|
})
|
|
27
40
|
|
|
28
41
|
async function main () {
|
|
29
42
|
browser = await puppeteer.launch({
|
|
30
43
|
headless,
|
|
44
|
+
defaultViewport: null,
|
|
31
45
|
args: [
|
|
32
46
|
url,
|
|
47
|
+
'--start-maximized',
|
|
33
48
|
'--no-sandbox',
|
|
34
49
|
'--disable-gpu',
|
|
35
50
|
'--disable-extensions'
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict'
|
|
4
4
|
|
|
5
|
+
const output = require('./src/output')
|
|
5
6
|
const { log, serve } = require('reserve')
|
|
6
7
|
const jobFactory = require('./src/job')
|
|
7
8
|
const reserveConfigurationFactory = require('./src/reserve')
|
|
@@ -23,7 +24,7 @@ async function notifyAndExecuteTests (job) {
|
|
|
23
24
|
await executeTests(job)
|
|
24
25
|
send({ msg: 'end', status: job.failed || 0 })
|
|
25
26
|
} catch (error) {
|
|
26
|
-
|
|
27
|
+
output.genericError(error)
|
|
27
28
|
send({ msg: 'error', error })
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -40,15 +41,16 @@ async function main () {
|
|
|
40
41
|
job.port = port
|
|
41
42
|
send({ msg: 'ready', port: job.port })
|
|
42
43
|
if (!job.logServer) {
|
|
43
|
-
|
|
44
|
+
output.serving(url)
|
|
44
45
|
}
|
|
46
|
+
output.report(job)
|
|
45
47
|
await notifyAndExecuteTests(job)
|
|
46
48
|
if (job.watch) {
|
|
47
49
|
delete job.start
|
|
48
50
|
if (!job.watching) {
|
|
49
|
-
|
|
51
|
+
output.watching(job.webapp)
|
|
50
52
|
watch(job.webapp, { recursive: true }, (eventType, filename) => {
|
|
51
|
-
|
|
53
|
+
output.changeDetected(eventType, filename)
|
|
52
54
|
if (!job.start) {
|
|
53
55
|
notifyAndExecuteTests(job)
|
|
54
56
|
}
|
|
@@ -57,15 +59,14 @@ async function main () {
|
|
|
57
59
|
}
|
|
58
60
|
} else if (job.keepAlive) {
|
|
59
61
|
job.status = 'Serving'
|
|
60
|
-
console.log('Keeping alive.')
|
|
61
62
|
} else {
|
|
62
63
|
process.exit(job.failed || 0)
|
|
63
64
|
}
|
|
64
65
|
})
|
|
65
66
|
.on('error', error => {
|
|
66
|
-
|
|
67
|
+
output.genericError(error)
|
|
67
68
|
send({ msg: 'error', error })
|
|
68
69
|
})
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
main()
|
|
72
|
+
main().catch(reason => output.genericError(reason))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ui5-test-runner",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Standalone test runner for UI5",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -39,16 +39,15 @@
|
|
|
39
39
|
},
|
|
40
40
|
"homepage": "https://github.com/ArnaudBuchholz/ui5-test-runner#readme",
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"
|
|
43
|
-
"mime": "^2.5.2",
|
|
42
|
+
"mime": "^3.0.0",
|
|
44
43
|
"nyc": "^15.1.0",
|
|
45
|
-
"puppeteer": "^
|
|
46
|
-
"reserve": "^1.
|
|
44
|
+
"puppeteer": "^11.0.0",
|
|
45
|
+
"reserve": "^1.12.1"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"jest": "^27.1
|
|
50
|
-
"nock": "^13.1
|
|
51
|
-
"standard": "^16.0.
|
|
48
|
+
"jest": "^27.3.1",
|
|
49
|
+
"nock": "^13.2.1",
|
|
50
|
+
"standard": "^16.0.4"
|
|
52
51
|
},
|
|
53
52
|
"standard": {
|
|
54
53
|
"env": [
|
|
@@ -59,10 +58,16 @@
|
|
|
59
58
|
]
|
|
60
59
|
},
|
|
61
60
|
"jest": {
|
|
61
|
+
"setupFilesAfterEnv": [
|
|
62
|
+
"./__mocks__/setup.js"
|
|
63
|
+
],
|
|
62
64
|
"collectCoverage": true,
|
|
63
65
|
"collectCoverageFrom": [
|
|
64
66
|
"src/*.js"
|
|
65
67
|
],
|
|
68
|
+
"coveragePathIgnorePatterns": [
|
|
69
|
+
"output\\.js"
|
|
70
|
+
],
|
|
66
71
|
"coverageThreshold": {
|
|
67
72
|
"global": {
|
|
68
73
|
"branches": 80,
|
package/src/browsers.js
CHANGED
|
@@ -4,12 +4,16 @@ const { fork } = require('child_process')
|
|
|
4
4
|
const { join } = require('path')
|
|
5
5
|
const { recreateDir, filename } = require('./tools')
|
|
6
6
|
const { getPageTimeout } = require('./timeout')
|
|
7
|
+
const output = require('./output')
|
|
8
|
+
|
|
9
|
+
let lastScreenshotId = 0
|
|
10
|
+
const screenshots = {}
|
|
7
11
|
|
|
8
12
|
function start (job, relativeUrl) {
|
|
9
13
|
if (!job.browsers) {
|
|
10
14
|
job.browsers = {}
|
|
11
15
|
}
|
|
12
|
-
|
|
16
|
+
output.browserStart(relativeUrl)
|
|
13
17
|
const reportDir = join(job.tstReportDir, filename(relativeUrl))
|
|
14
18
|
const args = job.args.split(' ')
|
|
15
19
|
.map(arg => arg
|
|
@@ -28,35 +32,72 @@ function start (job, relativeUrl) {
|
|
|
28
32
|
job.browsers[relativeUrl] = pageBrowser
|
|
29
33
|
run(job, pageBrowser)
|
|
30
34
|
return promise.then(() => {
|
|
31
|
-
|
|
35
|
+
output.browserStopped(relativeUrl)
|
|
32
36
|
})
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
async function run (job, pageBrowser) {
|
|
36
40
|
const { relativeUrl } = pageBrowser
|
|
37
41
|
if (pageBrowser.retry) {
|
|
38
|
-
|
|
42
|
+
output.browserRetry(relativeUrl, pageBrowser.retry)
|
|
39
43
|
}
|
|
40
44
|
await recreateDir(pageBrowser.reportDir)
|
|
41
45
|
delete pageBrowser.stopped
|
|
42
|
-
const childProcess = fork(job.browser, pageBrowser.args.map(arg => arg.replace('__RETRY__', pageBrowser.retry)), { stdio: '
|
|
46
|
+
const childProcess = fork(job.browser, pageBrowser.args.map(arg => arg.replace('__RETRY__', pageBrowser.retry)), { stdio: 'pipe' })
|
|
47
|
+
output.monitor(childProcess)
|
|
43
48
|
pageBrowser.childProcess = childProcess
|
|
44
49
|
const timeout = getPageTimeout(job)
|
|
45
50
|
if (timeout) {
|
|
46
51
|
pageBrowser.timeoutId = setTimeout(() => {
|
|
47
|
-
|
|
52
|
+
output.browserTimeout(relativeUrl)
|
|
48
53
|
stop(job, relativeUrl)
|
|
49
54
|
}, timeout)
|
|
50
55
|
}
|
|
56
|
+
childProcess.on('message', message => {
|
|
57
|
+
if (message.command === 'screenshot') {
|
|
58
|
+
const { id } = message
|
|
59
|
+
screenshots[id]()
|
|
60
|
+
delete screenshots[id]
|
|
61
|
+
} else /* istanbul ignore else */ if (message.command === 'capabilities') {
|
|
62
|
+
job.browserCapabilities = { ...message }
|
|
63
|
+
delete job.browserCapabilities.command
|
|
64
|
+
output.browserCapabilities(job.browserCapabilities)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
51
67
|
childProcess.on('close', () => {
|
|
52
68
|
if (!pageBrowser.stopped) {
|
|
53
|
-
|
|
69
|
+
output.browserClosed(relativeUrl)
|
|
54
70
|
stop(job, relativeUrl, true)
|
|
55
71
|
}
|
|
56
72
|
})
|
|
73
|
+
if (!job.browserCapabilities) {
|
|
74
|
+
childProcess.send({ command: 'capabilities' })
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function screenshot (job, relativeUrl, filename) {
|
|
79
|
+
if (job.noScreenshot || !job.browserCapabilities || !job.browserCapabilities.screenshot) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const pageBrowser = job.browsers[relativeUrl]
|
|
83
|
+
if (pageBrowser) {
|
|
84
|
+
const { childProcess } = pageBrowser
|
|
85
|
+
if (childProcess.connected) {
|
|
86
|
+
const id = ++lastScreenshotId
|
|
87
|
+
const promise = new Promise(resolve => {
|
|
88
|
+
screenshots[id] = resolve
|
|
89
|
+
})
|
|
90
|
+
childProcess.send({
|
|
91
|
+
id,
|
|
92
|
+
command: 'screenshot',
|
|
93
|
+
filename
|
|
94
|
+
})
|
|
95
|
+
await promise
|
|
96
|
+
}
|
|
97
|
+
}
|
|
57
98
|
}
|
|
58
99
|
|
|
59
|
-
function stop (job, relativeUrl, retry = false) {
|
|
100
|
+
async function stop (job, relativeUrl, retry = false) {
|
|
60
101
|
const pageBrowser = job.browsers[relativeUrl]
|
|
61
102
|
if (pageBrowser) {
|
|
62
103
|
pageBrowser.stopped = true
|
|
@@ -76,4 +117,4 @@ function stop (job, relativeUrl, retry = false) {
|
|
|
76
117
|
}
|
|
77
118
|
}
|
|
78
119
|
|
|
79
|
-
module.exports = { start, stop }
|
|
120
|
+
module.exports = { start, screenshot, stop }
|
package/src/coverage.js
CHANGED
|
@@ -5,14 +5,14 @@ const { fork } = require('child_process')
|
|
|
5
5
|
const { cleanDir, createDir } = require('./tools')
|
|
6
6
|
const { readdir, readFile, stat, writeFile } = require('fs').promises
|
|
7
7
|
const { Readable } = require('stream')
|
|
8
|
+
const output = require('./output')
|
|
8
9
|
|
|
9
10
|
const nycScript = require.resolve('nyc/bin/nyc.js')
|
|
10
11
|
|
|
11
12
|
function nyc (...args) {
|
|
12
|
-
|
|
13
|
-
const childProcess = fork(nycScript, args, {
|
|
14
|
-
|
|
15
|
-
})
|
|
13
|
+
output.nyc(...args)
|
|
14
|
+
const childProcess = fork(nycScript, args, { stdio: 'pipe' })
|
|
15
|
+
output.monitor(childProcess)
|
|
16
16
|
let done
|
|
17
17
|
const promise = new Promise(resolve => { done = resolve })
|
|
18
18
|
childProcess.on('close', done)
|
|
@@ -44,6 +44,15 @@ async function instrument (job) {
|
|
|
44
44
|
await createDir(join(job.covTempDir, 'settings'))
|
|
45
45
|
const settings = JSON.parse((await readFile(job.covSettings)).toString())
|
|
46
46
|
settings.cwd = job.cwd
|
|
47
|
+
if (!settings.exclude) {
|
|
48
|
+
settings.exclude = []
|
|
49
|
+
}
|
|
50
|
+
settings.exclude.push(join(job.covTempDir, '**'))
|
|
51
|
+
if (job.cache) {
|
|
52
|
+
settings.exclude.push(join(job.cache, '**'))
|
|
53
|
+
}
|
|
54
|
+
settings.exclude.push(join(job.tstReportDir, '**'))
|
|
55
|
+
settings.exclude.push(join(job.covReportDir, '**'))
|
|
47
56
|
await writeFile(job.nycSettingsPath, JSON.stringify(settings))
|
|
48
57
|
await nyc('instrument', job.webapp, join(job.covTempDir, 'instrumented'), '--nycrc-path', job.nycSettingsPath)
|
|
49
58
|
}
|
package/src/endpoints.js
CHANGED
|
@@ -2,29 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
const { join } = require('path')
|
|
4
4
|
const { body } = require('reserve')
|
|
5
|
-
const { stop } = require('./browsers')
|
|
5
|
+
const { screenshot, stop } = require('./browsers')
|
|
6
6
|
const { writeFile } = require('fs').promises
|
|
7
7
|
const { extractUrl, filename } = require('./tools')
|
|
8
8
|
const { Request, Response } = require('reserve')
|
|
9
|
+
const output = require('./output')
|
|
9
10
|
|
|
10
11
|
module.exports = job => {
|
|
12
|
+
async function endpointImpl (implementation, request) {
|
|
13
|
+
const url = extractUrl(request.headers)
|
|
14
|
+
const data = JSON.parse(await body(request))
|
|
15
|
+
if (job.parallel === -1) {
|
|
16
|
+
output.endpoint(url, data)
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await implementation.call(this, url, data)
|
|
20
|
+
} catch (e) {
|
|
21
|
+
output.endpointError(url, data, e)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function synchronousEndpoint (implementation) {
|
|
26
|
+
return async function (request, response) {
|
|
27
|
+
await endpointImpl(implementation, request)
|
|
28
|
+
response.writeHead(200)
|
|
29
|
+
response.end()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
function endpoint (implementation) {
|
|
12
34
|
return async function (request, response) {
|
|
13
35
|
response.writeHead(200)
|
|
14
36
|
response.end()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
console.error(data)
|
|
25
|
-
console.error(e)
|
|
37
|
+
await endpointImpl(implementation, request)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getPageTest (page, testId) {
|
|
42
|
+
const { tests, order } = page
|
|
43
|
+
if (!tests[testId]) {
|
|
44
|
+
tests[testId] = {
|
|
45
|
+
timestamps: []
|
|
26
46
|
}
|
|
47
|
+
order.push(testId)
|
|
27
48
|
}
|
|
49
|
+
return tests[testId]
|
|
28
50
|
}
|
|
29
51
|
|
|
30
52
|
return job.parallel
|
|
@@ -94,41 +116,60 @@ module.exports = job => {
|
|
|
94
116
|
// Endpoint to receive QUnit.begin
|
|
95
117
|
match: '^/_/QUnit/begin',
|
|
96
118
|
custom: endpoint((url, details) => {
|
|
97
|
-
|
|
119
|
+
const page = {
|
|
120
|
+
isOpa: details.isOpa,
|
|
98
121
|
total: details.totalTests,
|
|
99
122
|
failed: 0,
|
|
100
123
|
passed: 0,
|
|
101
|
-
tests:
|
|
124
|
+
tests: {},
|
|
125
|
+
order: []
|
|
126
|
+
}
|
|
127
|
+
details.modules.forEach(module => {
|
|
128
|
+
module.tests.forEach(test => getPageTest(page, test.testId))
|
|
129
|
+
})
|
|
130
|
+
job.testPages[url] = page
|
|
131
|
+
})
|
|
132
|
+
}, {
|
|
133
|
+
// Endpoint to receive QUnit.log
|
|
134
|
+
match: '^/_/QUnit/log',
|
|
135
|
+
custom: synchronousEndpoint(async (url, report) => {
|
|
136
|
+
const page = job.testPages[url]
|
|
137
|
+
if (page.isOpa) {
|
|
138
|
+
const { testId, runtime } = report
|
|
139
|
+
getPageTest(page, testId).timestamps.push(runtime)
|
|
140
|
+
await screenshot(job, url, `${testId}-${runtime}.png`)
|
|
102
141
|
}
|
|
103
142
|
})
|
|
104
143
|
}, {
|
|
105
144
|
// Endpoint to receive QUnit.testDone
|
|
106
145
|
match: '^/_/QUnit/testDone',
|
|
107
|
-
custom:
|
|
146
|
+
custom: synchronousEndpoint(async (url, report) => {
|
|
108
147
|
const page = job.testPages[url]
|
|
148
|
+
const { testId } = report
|
|
109
149
|
if (report.failed) {
|
|
150
|
+
await screenshot(job, url, `${testId}.png`)
|
|
110
151
|
job.failed = true
|
|
111
152
|
++page.failed
|
|
112
153
|
} else {
|
|
113
154
|
++page.passed
|
|
114
155
|
}
|
|
115
|
-
page.
|
|
156
|
+
getPageTest(page, testId).report = report
|
|
116
157
|
})
|
|
117
158
|
}, {
|
|
118
159
|
// Endpoint to receive QUnit.done
|
|
119
160
|
match: '^/_/QUnit/done',
|
|
120
|
-
custom: endpoint((url, report) => {
|
|
121
|
-
let promise = Promise.resolve()
|
|
161
|
+
custom: endpoint(async (url, report) => {
|
|
122
162
|
const page = job.testPages[url]
|
|
123
163
|
if (page) {
|
|
164
|
+
await screenshot(job, url, 'screenshot.png')
|
|
124
165
|
if (report.__coverage__) {
|
|
125
166
|
const coverageFileName = join(job.covTempDir, `${filename(url)}.json`)
|
|
126
|
-
|
|
167
|
+
await writeFile(coverageFileName, JSON.stringify(report.__coverage__))
|
|
127
168
|
delete report.__coverage__
|
|
128
169
|
}
|
|
129
170
|
page.report = report
|
|
130
171
|
}
|
|
131
|
-
|
|
172
|
+
stop(job, url)
|
|
132
173
|
})
|
|
133
174
|
}, {
|
|
134
175
|
// UI to follow progress
|
|
@@ -139,14 +180,17 @@ module.exports = job => {
|
|
|
139
180
|
match: '^/_/progress',
|
|
140
181
|
custom: async (request, response) => {
|
|
141
182
|
const json = JSON.stringify(job, (key, value) => {
|
|
142
|
-
if (key === 'tests' &&
|
|
183
|
+
if (((key === 'tests' || key === 'browsers') && typeof value === 'object') ||
|
|
184
|
+
(key === 'order' && Array.isArray(value))
|
|
185
|
+
) {
|
|
143
186
|
return undefined // Filter out
|
|
144
187
|
}
|
|
145
188
|
return value
|
|
146
189
|
})
|
|
190
|
+
const length = (new TextEncoder().encode(json)).length
|
|
147
191
|
response.writeHead(200, {
|
|
148
192
|
'Content-Type': 'application/json',
|
|
149
|
-
'Content-Length':
|
|
193
|
+
'Content-Length': length
|
|
150
194
|
})
|
|
151
195
|
response.end(json)
|
|
152
196
|
}
|
|
@@ -2,24 +2,73 @@
|
|
|
2
2
|
(function () {
|
|
3
3
|
'use strict'
|
|
4
4
|
|
|
5
|
+
if (window['ui5-test-runner/qunit-hooks']) {
|
|
6
|
+
return // already installed
|
|
7
|
+
}
|
|
8
|
+
window['ui5-test-runner/qunit-hooks'] = true
|
|
9
|
+
|
|
5
10
|
function post (url, data) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
return new Promise(function (resolve, reject) {
|
|
12
|
+
const xhr = new XMLHttpRequest()
|
|
13
|
+
xhr.open('POST', '/_/' + url)
|
|
14
|
+
xhr.send(JSON.stringify(data))
|
|
15
|
+
xhr.onreadystatechange = function () {
|
|
16
|
+
if (xhr.readyState === 4) {
|
|
17
|
+
if (xhr.status === 200) {
|
|
18
|
+
resolve(xhr.responseText)
|
|
19
|
+
} else {
|
|
20
|
+
reject(xhr.statusText)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isOpa () {
|
|
28
|
+
try {
|
|
29
|
+
return !!window.sap.ui.test.Opa5
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
9
33
|
}
|
|
10
34
|
|
|
11
35
|
QUnit.begin(function (details) {
|
|
12
|
-
|
|
36
|
+
details.isOpa = isOpa()
|
|
37
|
+
return post('QUnit/begin', details)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
QUnit.log(function (report) {
|
|
41
|
+
let ready = false
|
|
42
|
+
const log = {
|
|
43
|
+
testId: report.testId,
|
|
44
|
+
runtime: report.runtime
|
|
45
|
+
}
|
|
46
|
+
post('QUnit/log', log)
|
|
47
|
+
.then(undefined, function () {
|
|
48
|
+
console.error('Failed to POST to QUnit/log (no timestamp)', log)
|
|
49
|
+
})
|
|
50
|
+
.then(function () {
|
|
51
|
+
ready = true
|
|
52
|
+
})
|
|
53
|
+
if (isOpa()) {
|
|
54
|
+
window.sap.ui.test.Opa5.prototype.waitFor({
|
|
55
|
+
timeout: 10,
|
|
56
|
+
autoWait: false, // Ignore interactable constraint
|
|
57
|
+
check: function () {
|
|
58
|
+
return ready
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
13
62
|
})
|
|
14
63
|
|
|
15
64
|
QUnit.testDone(function (report) {
|
|
16
|
-
post('QUnit/testDone', report)
|
|
65
|
+
return post('QUnit/testDone', report)
|
|
17
66
|
})
|
|
18
67
|
|
|
19
68
|
QUnit.done(function (report) {
|
|
20
69
|
if (window.__coverage__) {
|
|
21
70
|
report.__coverage__ = window.__coverage__
|
|
22
71
|
}
|
|
23
|
-
post('QUnit/done', report)
|
|
72
|
+
return post('QUnit/done', report)
|
|
24
73
|
})
|
|
25
74
|
}())
|
package/src/job.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { accessSync } = require('fs')
|
|
3
4
|
const { join, isAbsolute } = require('path')
|
|
5
|
+
const output = require('./output')
|
|
4
6
|
|
|
5
7
|
function allocate (cwd) {
|
|
6
8
|
return {
|
|
@@ -11,6 +13,7 @@ function allocate (cwd) {
|
|
|
11
13
|
libs: [],
|
|
12
14
|
cache: '',
|
|
13
15
|
webapp: 'webapp',
|
|
16
|
+
testsuite: 'test/testsuite.qunit.html',
|
|
14
17
|
pageFilter: '',
|
|
15
18
|
pageParams: '',
|
|
16
19
|
pageTimeout: 0,
|
|
@@ -22,6 +25,7 @@ function allocate (cwd) {
|
|
|
22
25
|
|
|
23
26
|
browser: join(__dirname, '../defaults/chromium.js'),
|
|
24
27
|
browserRetry: 1,
|
|
28
|
+
noScreenshot: false,
|
|
25
29
|
args: '__URL__ __REPORT__',
|
|
26
30
|
|
|
27
31
|
parallel: 2,
|
|
@@ -35,6 +39,14 @@ function allocate (cwd) {
|
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
function checkAccess (path, label) {
|
|
43
|
+
try {
|
|
44
|
+
accessSync(path)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(`Unable to access ${label}, check your settings`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
function finalize (job) {
|
|
39
51
|
Object.keys(job)
|
|
40
52
|
.filter(name => name.startsWith('!'))
|
|
@@ -55,9 +67,15 @@ function finalize (job) {
|
|
|
55
67
|
'webapp,browser,tstReportDir,covSettings,covTempDir,covReportDir'
|
|
56
68
|
.split(',')
|
|
57
69
|
.forEach(setting => updateToAbsolute(setting))
|
|
70
|
+
checkAccess(job.webapp, 'webapp folder')
|
|
71
|
+
checkAccess(job.browser, 'browser command')
|
|
72
|
+
|
|
73
|
+
const testsuitePath = toAbsolute(job.testsuite, job.webapp)
|
|
74
|
+
checkAccess(testsuitePath, 'testsuite')
|
|
58
75
|
|
|
59
76
|
job.libs.forEach(libMapping => {
|
|
60
77
|
libMapping.source = toAbsolute(libMapping.source)
|
|
78
|
+
checkAccess(libMapping.source, `lib mapping of ${libMapping.relative}`)
|
|
61
79
|
})
|
|
62
80
|
|
|
63
81
|
if (job.parallel <= 0) {
|
|
@@ -65,7 +83,7 @@ function finalize (job) {
|
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
if (job.browserRetry < 0) {
|
|
68
|
-
|
|
86
|
+
output.unexpectedOptionValue('browserRetry', 'defaulting to 1')
|
|
69
87
|
job.browserRetry = 1
|
|
70
88
|
}
|
|
71
89
|
}
|
|
@@ -97,7 +115,7 @@ function parseJobParam (job, arg) {
|
|
|
97
115
|
try {
|
|
98
116
|
job[name] = valueParser(value, job[name])
|
|
99
117
|
} catch (error) {
|
|
100
|
-
|
|
118
|
+
output.unexpectedOptionValue(name, error.message)
|
|
101
119
|
}
|
|
102
120
|
}
|
|
103
121
|
}
|
package/src/output.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const nativeConsole = console
|
|
4
|
+
const mockConsole = {}
|
|
5
|
+
const interactive = process.stdout.columns !== undefined
|
|
6
|
+
let lastTick = 0
|
|
7
|
+
const ticks = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f']
|
|
8
|
+
let job
|
|
9
|
+
let lines = 1
|
|
10
|
+
|
|
11
|
+
function write (...parts) {
|
|
12
|
+
parts.forEach(part => process.stdout.write(part))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function clean () {
|
|
16
|
+
write(`\x1b[${lines.toString()}F`)
|
|
17
|
+
for (let line = 0; line < lines; ++line) {
|
|
18
|
+
if (line > 1) {
|
|
19
|
+
write('\x1b[1E')
|
|
20
|
+
}
|
|
21
|
+
write(''.padEnd(process.stdout.columns, ' '))
|
|
22
|
+
}
|
|
23
|
+
if (lines > 1) {
|
|
24
|
+
write(`\x1b[${(lines - 1).toString()}F`)
|
|
25
|
+
} else {
|
|
26
|
+
write('\x1b[1G')
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const width = 10
|
|
31
|
+
|
|
32
|
+
function bar (ratio, msg) {
|
|
33
|
+
write('[')
|
|
34
|
+
if (typeof ratio === 'string') {
|
|
35
|
+
if (ratio.length > width) {
|
|
36
|
+
write(ratio.substring(0, width - 3), '...')
|
|
37
|
+
} else {
|
|
38
|
+
const padded = ratio.padStart(width - Math.floor((width - ratio.length) / 2), '-').padEnd(width, '-')
|
|
39
|
+
write(padded)
|
|
40
|
+
}
|
|
41
|
+
write('] ')
|
|
42
|
+
} else {
|
|
43
|
+
const filled = Math.floor(width * ratio)
|
|
44
|
+
write(''.padEnd(filled, '\u2588'), ''.padEnd(width - filled, '\u2591'))
|
|
45
|
+
const percent = Math.floor(100 * ratio).toString().padStart(3, ' ')
|
|
46
|
+
write('] ', percent, '%')
|
|
47
|
+
}
|
|
48
|
+
write(' ', msg, '\n')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function progress (cleanFirst = true) {
|
|
52
|
+
if (cleanFirst) {
|
|
53
|
+
clean()
|
|
54
|
+
}
|
|
55
|
+
lines = 1
|
|
56
|
+
let progressRatio
|
|
57
|
+
if (job.testPageUrls && job.testPages && job.parallel > 0) {
|
|
58
|
+
const total = job.testPageUrls.length
|
|
59
|
+
const done = Object.keys(job.testPages)
|
|
60
|
+
.filter(pageUrl => !!job.testPages[pageUrl].report)
|
|
61
|
+
.length
|
|
62
|
+
if (done < total) {
|
|
63
|
+
progressRatio = done / total
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (job.browsers) {
|
|
67
|
+
const runningPages = Object.keys(job.browsers)
|
|
68
|
+
lines += runningPages.length
|
|
69
|
+
runningPages.forEach(pageUrl => {
|
|
70
|
+
let starting = true
|
|
71
|
+
if (job.testPages) {
|
|
72
|
+
const page = job.testPages[pageUrl]
|
|
73
|
+
if (page) {
|
|
74
|
+
const { total, passed, failed } = page
|
|
75
|
+
if (total) {
|
|
76
|
+
const progress = passed + failed
|
|
77
|
+
bar(progress / total, pageUrl)
|
|
78
|
+
starting = false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (starting) {
|
|
83
|
+
bar('starting', pageUrl)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
const status = `${ticks[++lastTick % ticks.length]} ${job.status}`
|
|
88
|
+
if (progressRatio !== undefined) {
|
|
89
|
+
bar(progressRatio, status)
|
|
90
|
+
} else {
|
|
91
|
+
write(status, '\n')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function wrap (write) {
|
|
96
|
+
if (job) {
|
|
97
|
+
clean()
|
|
98
|
+
}
|
|
99
|
+
write()
|
|
100
|
+
if (job) {
|
|
101
|
+
progress(false)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Object.getOwnPropertyNames(console).forEach(name => {
|
|
106
|
+
mockConsole[name] = function (...args) {
|
|
107
|
+
wrap(() => nativeConsole[name](...args))
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (interactive) {
|
|
112
|
+
global.console = mockConsole
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
serving (url) {
|
|
117
|
+
console.log(`Server running at ${url}`)
|
|
118
|
+
},
|
|
119
|
+
watching (path) {
|
|
120
|
+
console.log('Watching changes on', path)
|
|
121
|
+
},
|
|
122
|
+
changeDetected (eventType, filename) {
|
|
123
|
+
console.log(eventType, filename)
|
|
124
|
+
},
|
|
125
|
+
report (newJob) {
|
|
126
|
+
job = newJob
|
|
127
|
+
if (interactive) {
|
|
128
|
+
process.stdout.write('\n')
|
|
129
|
+
setInterval(progress, 250)
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
browserStart (url) {
|
|
133
|
+
if (!interactive) {
|
|
134
|
+
console.log('>>', url)
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
browserCapabilities (capabilities) {
|
|
138
|
+
console.log('Browser capabilities :', capabilities)
|
|
139
|
+
},
|
|
140
|
+
browserStopped (url) {
|
|
141
|
+
if (!interactive) {
|
|
142
|
+
console.log('<<', url)
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
browserClosed (url) {
|
|
146
|
+
console.log('!! BROWSER CLOSED', url)
|
|
147
|
+
},
|
|
148
|
+
browserRetry (url, retry) {
|
|
149
|
+
console.log('>> RETRY', retry, url)
|
|
150
|
+
},
|
|
151
|
+
browserTimeout (url) {
|
|
152
|
+
console.log('!! TIMEOUT', url)
|
|
153
|
+
},
|
|
154
|
+
monitor (childProcess) {
|
|
155
|
+
['stdout', 'stderr'].forEach(channel => {
|
|
156
|
+
childProcess[channel].on('data', chunk => {
|
|
157
|
+
wrap(() => process[channel].write(chunk))
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
},
|
|
161
|
+
nyc (...args) {
|
|
162
|
+
console.log('nyc', ...args)
|
|
163
|
+
},
|
|
164
|
+
endpoint (url, data) {
|
|
165
|
+
console.log(url, data)
|
|
166
|
+
},
|
|
167
|
+
endpointError (url, data, error) {
|
|
168
|
+
console.error(`Exception when processing ${url}`)
|
|
169
|
+
console.error(data)
|
|
170
|
+
console.error(error)
|
|
171
|
+
},
|
|
172
|
+
globalTimeout (url) {
|
|
173
|
+
console.log('!! TIMEOUT', url)
|
|
174
|
+
},
|
|
175
|
+
failFast (url) {
|
|
176
|
+
console.log('!! FAILFAST', url)
|
|
177
|
+
},
|
|
178
|
+
timeSpent (start, end = new Date()) {
|
|
179
|
+
console.log(`Time spent: ${end - start}ms`)
|
|
180
|
+
},
|
|
181
|
+
unexpectedOptionValue (optionName, message) {
|
|
182
|
+
console.error(`Unexpected value for option ${optionName} : ${message}`)
|
|
183
|
+
},
|
|
184
|
+
noTestPageFound () {
|
|
185
|
+
console.error('No test page found (or all filtered out)')
|
|
186
|
+
},
|
|
187
|
+
failedToCacheUI5resource (path, statusCode) {
|
|
188
|
+
console.error(`Unable to cache '${path}' (status ${statusCode})`)
|
|
189
|
+
},
|
|
190
|
+
genericError (error) {
|
|
191
|
+
console.error('An unexpected error occurred :', error.message || error)
|
|
192
|
+
},
|
|
193
|
+
unhandled () {
|
|
194
|
+
console.warn('Some requests are not handled properly, check the unhandled.txt report for more info')
|
|
195
|
+
},
|
|
196
|
+
results (pages) {
|
|
197
|
+
console.table(pages)
|
|
198
|
+
}
|
|
199
|
+
}
|
package/src/report.html
CHANGED
|
@@ -1,9 +1,67 @@
|
|
|
1
1
|
<html>
|
|
2
2
|
<head>
|
|
3
3
|
<title>ui5-test-runner report</title>
|
|
4
|
+
<style>
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.testId {
|
|
11
|
+
padding-right: .5rem;
|
|
12
|
+
font-size: x-small;
|
|
13
|
+
color: gray;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.runtime {
|
|
17
|
+
padding-left: .5rem;
|
|
18
|
+
padding-right: .5rem;
|
|
19
|
+
font-size: x-small;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.runtime::after {
|
|
23
|
+
content: "ms"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.timestamp {
|
|
27
|
+
font-size: x-small;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.timestamp::before {
|
|
31
|
+
content: "@"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.timestamp::after {
|
|
35
|
+
content: "ms"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
img.log {
|
|
39
|
+
width: 50%;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
table.job {
|
|
43
|
+
font-size: small;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
table.job.object {
|
|
47
|
+
font-size: x-small;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
table.job th {
|
|
51
|
+
text-align: right;
|
|
52
|
+
padding-right: .5rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pre {
|
|
56
|
+
margin-top: 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
</style>
|
|
4
60
|
</head>
|
|
5
61
|
<body>
|
|
6
62
|
<script>
|
|
63
|
+
const ok = '\u2714\ufe0f'
|
|
64
|
+
const ko = '\u274c'
|
|
7
65
|
|
|
8
66
|
function tx (content) {
|
|
9
67
|
return document.createTextNode(content)
|
|
@@ -14,13 +72,15 @@ function el (name, attributes = {}, ...children) {
|
|
|
14
72
|
Object.keys(attributes).forEach(attribute => {
|
|
15
73
|
element.setAttribute(attribute, attributes[attribute])
|
|
16
74
|
})
|
|
17
|
-
children
|
|
75
|
+
children
|
|
76
|
+
.filter(child => !!child)
|
|
77
|
+
.forEach(child => {
|
|
18
78
|
if (typeof child !== 'object') {
|
|
19
79
|
element.appendChild(tx(child.toString()))
|
|
20
80
|
} else {
|
|
21
81
|
element.appendChild(child)
|
|
22
82
|
}
|
|
23
|
-
|
|
83
|
+
})
|
|
24
84
|
return element
|
|
25
85
|
}
|
|
26
86
|
|
|
@@ -28,62 +88,115 @@ async function json (url) {
|
|
|
28
88
|
return (await fetch(url)).json()
|
|
29
89
|
}
|
|
30
90
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
91
|
+
async function generate () {
|
|
92
|
+
const job = await json('job.json')
|
|
93
|
+
let screenshot = false
|
|
94
|
+
let consoleLog = false
|
|
95
|
+
if (job.browserCapabilities) {
|
|
96
|
+
screenshot = job.browserCapabilities.screenshot
|
|
97
|
+
consoleLog = job.browserCapabilities.consoleLog
|
|
98
|
+
}
|
|
99
|
+
if (job.noScreenshot) {
|
|
100
|
+
screenshot = false
|
|
101
|
+
}
|
|
39
102
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let { module, name, failed } = test
|
|
56
|
-
if (module !== lastModule) {
|
|
57
|
-
lastModule = module
|
|
103
|
+
const jobDetails = document.body.appendChild(el('details'))
|
|
104
|
+
const jobSummary = jobDetails.appendChild(el('summary'))
|
|
105
|
+
jobSummary.appendChild(el('span', {}, 'job details'))
|
|
106
|
+
const jobParams = jobDetails.appendChild(el('table', { class: 'job' }))
|
|
107
|
+
|
|
108
|
+
function dump (obj, level) {
|
|
109
|
+
Object.keys(obj).forEach(property => {
|
|
110
|
+
const value = obj[property]
|
|
111
|
+
const tr = level.appendChild(el('tr'))
|
|
112
|
+
tr.appendChild(el('th', {}, property))
|
|
113
|
+
if (typeof value !== 'object') {
|
|
114
|
+
let stringifiedValue
|
|
115
|
+
if (typeof value === 'string') {
|
|
116
|
+
if (value.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)) {
|
|
117
|
+
stringifiedValue = new Date(value).toString()
|
|
58
118
|
} else {
|
|
59
|
-
|
|
119
|
+
stringifiedValue = value
|
|
60
120
|
}
|
|
61
|
-
if (failed) {
|
|
62
|
-
const tr = row(module)
|
|
63
|
-
const td = tr.querySelectorAll('td')[1]
|
|
64
|
-
const assertions = td.appendChild(el('details'))
|
|
65
|
-
assertions.appendChild(el('summary', {}, name + ko))
|
|
66
|
-
test.assertions.forEach(assertion => {
|
|
67
|
-
const bullet = assertion.result && ok || ko
|
|
68
|
-
assertions.appendChild(el('div', {}, el('pre', {}, bullet + ' ' + assertion.message)))
|
|
69
|
-
})
|
|
70
|
-
} else {
|
|
71
|
-
row(module, name)
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
if (report.report) {
|
|
75
|
-
row(`${report.report.runtime} ms`)
|
|
76
121
|
} else {
|
|
77
|
-
|
|
122
|
+
stringifiedValue = JSON.stringify(value)
|
|
78
123
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
124
|
+
tr.appendChild(el('td', {}, stringifiedValue))
|
|
125
|
+
} else {
|
|
126
|
+
const subLevel = tr.appendChild(el('td')).appendChild(el('table', { class: 'job object' }))
|
|
127
|
+
dump(value, subLevel)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
dump(job, jobParams)
|
|
132
|
+
|
|
133
|
+
const pages = await json('pages.json')
|
|
134
|
+
Object.keys(pages).forEach(async page => {
|
|
135
|
+
const pagePath = pages[page]
|
|
136
|
+
const pageDetails = document.body.appendChild(el('details'))
|
|
137
|
+
const pageSummary = pageDetails.appendChild(el('summary'))
|
|
138
|
+
pageSummary.appendChild(el('b', {}, page))
|
|
139
|
+
|
|
140
|
+
const pageReport = await json(`${pagePath}.json`)
|
|
141
|
+
const { isOpa, report, failed } = pageReport
|
|
142
|
+
if (!report || failed) {
|
|
143
|
+
pageSummary.appendChild(tx(ko))
|
|
144
|
+
} else {
|
|
145
|
+
pageSummary.appendChild(tx(ok))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const pageDetailsTable = pageDetails.appendChild(el('table', { width: '100%' }))
|
|
149
|
+
const addPageDetailsRow = module => {
|
|
150
|
+
const tr = pageDetailsTable.appendChild(el('tr'))
|
|
151
|
+
tr.innerHTML = `<td width="15%" valign="top">${module}</td><td width="85%"> </td>`
|
|
152
|
+
return tr
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let lastModule
|
|
156
|
+
pageReport.order.forEach(testId => {
|
|
157
|
+
const { timestamps, report } = pageReport.tests[testId]
|
|
158
|
+
const { module, name, failed, assertions, runtime } = report
|
|
159
|
+
|
|
160
|
+
let displayModule = module
|
|
161
|
+
if (module !== lastModule) {
|
|
162
|
+
lastModule = module
|
|
163
|
+
} else {
|
|
164
|
+
displayModule = ' '
|
|
165
|
+
}
|
|
166
|
+
const pageAssertions = addPageDetailsRow(displayModule).querySelectorAll('td')[1].appendChild(el('details'))
|
|
167
|
+
pageAssertions.appendChild(el('summary', {}, name, el('span', { class: 'runtime' }, runtime), el('span', { class: 'testId' }, testId), failed ? ko : ''))
|
|
168
|
+
const validTimestamps = assertions.length === timestamps.length
|
|
169
|
+
assertions.forEach((assertion, index) => {
|
|
170
|
+
const bullet = assertion.result && ok || ko
|
|
171
|
+
const timestamp = timestamps[index]
|
|
172
|
+
let preview
|
|
173
|
+
if (isOpa && screenshot && validTimestamps) {
|
|
174
|
+
preview = el('img', { loading: 'lazy', src: `${pagePath}/${testId}-${timestamp}.png`, class: 'log' })
|
|
175
|
+
}
|
|
176
|
+
pageAssertions.appendChild(el('div', {}, el('div', { class: 'timestamp' }, timestamp), el('pre', {}, bullet + ' ' + assertion.message), preview))
|
|
85
177
|
})
|
|
86
|
-
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
if (report) {
|
|
181
|
+
addPageDetailsRow(`${report.runtime} ms`)
|
|
182
|
+
} else {
|
|
183
|
+
addPageDetailsRow(`Timed out ${ko}`)
|
|
184
|
+
}
|
|
185
|
+
if (screenshot) {
|
|
186
|
+
pageDetails.appendChild(el('a', { href: `${pagePath}/screenshot.png`, target: '_blank' }, 'screenshot'))
|
|
187
|
+
pageDetails.appendChild(el('span', {}, ' '))
|
|
188
|
+
}
|
|
189
|
+
if (consoleLog) {
|
|
190
|
+
pageDetails.appendChild(el('a', { href: `${pagePath}/console.txt`, target: '_blank' }, 'console'))
|
|
191
|
+
}
|
|
192
|
+
}, () => {
|
|
193
|
+
pageSummary.appendChild(tx(ko))
|
|
194
|
+
pageDetails.appendChild(el('p', {}, 'No report found ', el('i', {}, '(fatal error or timeout)')))
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
generate()
|
|
199
|
+
|
|
87
200
|
</script>
|
|
88
201
|
</body>
|
|
89
202
|
</html>
|
package/src/reserve.js
CHANGED
package/src/tests.js
CHANGED
|
@@ -6,16 +6,29 @@ const { filename, recreateDir } = require('./tools')
|
|
|
6
6
|
const { join } = require('path')
|
|
7
7
|
const { copyFile, writeFile } = require('fs').promises
|
|
8
8
|
const { globallyTimedOut } = require('./timeout')
|
|
9
|
+
const output = require('./output')
|
|
10
|
+
|
|
11
|
+
async function saveJob (job) {
|
|
12
|
+
await writeFile(join(job.tstReportDir, 'job.json'), JSON.stringify({
|
|
13
|
+
...job,
|
|
14
|
+
// Following members are useless because already serialized or not relevant
|
|
15
|
+
status: undefined,
|
|
16
|
+
testPageUrls: undefined,
|
|
17
|
+
browsers: undefined,
|
|
18
|
+
testPages: undefined
|
|
19
|
+
}))
|
|
20
|
+
}
|
|
9
21
|
|
|
10
22
|
async function extractTestPages (job) {
|
|
11
23
|
job.start = new Date()
|
|
12
24
|
await instrument(job)
|
|
13
|
-
job.status = 'Extracting test pages'
|
|
14
25
|
await recreateDir(job.tstReportDir)
|
|
26
|
+
await saveJob(job)
|
|
27
|
+
job.status = 'Extracting test pages'
|
|
15
28
|
job.testPageUrls = []
|
|
16
|
-
await start(job, '/
|
|
29
|
+
await start(job, '/' + job.testsuite)
|
|
17
30
|
if (job.testPageUrls.length === 0) {
|
|
18
|
-
|
|
31
|
+
output.noTestPageFound()
|
|
19
32
|
job.failed = true
|
|
20
33
|
return Promise.resolve()
|
|
21
34
|
}
|
|
@@ -42,9 +55,9 @@ async function runTestPage (job) {
|
|
|
42
55
|
const index = job.testPagesStarted++
|
|
43
56
|
const url = job.testPageUrls[index]
|
|
44
57
|
if (globallyTimedOut(job)) {
|
|
45
|
-
|
|
58
|
+
output.globalTimeout(url)
|
|
46
59
|
} else if (job.failFast && job.failed) {
|
|
47
|
-
|
|
60
|
+
output.failFast(url)
|
|
48
61
|
} else {
|
|
49
62
|
await start(job, url)
|
|
50
63
|
const page = job.testPages[url]
|
|
@@ -78,10 +91,11 @@ async function generateReport (job) {
|
|
|
78
91
|
job.failed += 1
|
|
79
92
|
}
|
|
80
93
|
}
|
|
81
|
-
|
|
94
|
+
output.results(pages)
|
|
95
|
+
await saveJob(job)
|
|
82
96
|
await copyFile(join(__dirname, 'report.html'), join(job.tstReportDir, 'report.html'))
|
|
83
97
|
await generateCoverageReport(job)
|
|
84
|
-
|
|
98
|
+
output.timeSpent(job.start)
|
|
85
99
|
job.status = 'Done'
|
|
86
100
|
}
|
|
87
101
|
|
package/src/ui5.js
CHANGED
|
@@ -4,6 +4,7 @@ const { dirname, join } = require('path')
|
|
|
4
4
|
const { createWriteStream } = require('fs')
|
|
5
5
|
const { mkdir, unlink } = require('fs').promises
|
|
6
6
|
const { capture } = require('reserve')
|
|
7
|
+
const output = require('./output')
|
|
7
8
|
|
|
8
9
|
module.exports = job => {
|
|
9
10
|
const [, hostName] = /https?:\/\/([^/]*)/.exec(job.ui5)
|
|
@@ -58,7 +59,7 @@ module.exports = job => {
|
|
|
58
59
|
file.end()
|
|
59
60
|
uncachable[path] = true
|
|
60
61
|
if (response.statusCode !== 404) {
|
|
61
|
-
|
|
62
|
+
output.failedToCacheUI5resource(path, response.statusCode)
|
|
62
63
|
}
|
|
63
64
|
return unlink(cachePath)
|
|
64
65
|
})
|
package/src/unhandled.js
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { extractUrl } = require('./tools')
|
|
4
|
+
const { join } = require('path')
|
|
5
|
+
const { writeFile } = require('fs')
|
|
6
|
+
const output = require('./output')
|
|
4
7
|
|
|
5
|
-
module.exports =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
module.exports = job => {
|
|
9
|
+
const unhandled = join(job.tstReportDir, 'unhandled.txt')
|
|
10
|
+
let outputUnhandled = true
|
|
11
|
+
return [{
|
|
12
|
+
custom: ({ headers, method, url }) => {
|
|
13
|
+
if (method === 'GET' && url.match(/favicon\.ico$|-preload\.js$|-dbg(\.[^.]+)*\.js$|i18n_\w+\.properties$/)) {
|
|
14
|
+
return 404 // expected
|
|
15
|
+
}
|
|
16
|
+
let status
|
|
17
|
+
if (method === 'GET') {
|
|
18
|
+
status = 404
|
|
19
|
+
} else {
|
|
20
|
+
status = 500
|
|
21
|
+
}
|
|
22
|
+
if (outputUnhandled) {
|
|
23
|
+
output.unhandled()
|
|
24
|
+
outputUnhandled = false
|
|
25
|
+
}
|
|
26
|
+
writeFile(unhandled, `${extractUrl(headers)} ${status} ${method} ${url}\n`, {
|
|
27
|
+
flag: 'a'
|
|
28
|
+
}, () => {})
|
|
29
|
+
return status
|
|
9
30
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return 404
|
|
13
|
-
}
|
|
14
|
-
console.error(`!! ${extractUrl(headers)} 500 ${method} ${url}`)
|
|
15
|
-
return 500
|
|
16
|
-
}
|
|
17
|
-
}]
|
|
31
|
+
}]
|
|
32
|
+
}
|