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 CHANGED
@@ -9,15 +9,15 @@
9
9
  [![MIT License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
10
  [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner.svg?type=shield)](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 crashes. 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.
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
- * Concept is detailed in the article [REserve - Testing UI5](https://arnaud-buchholz.medium.com/reserve-testing-ui5-85187d5eb7f1)
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
- * You may follow the progress of the test executions using http://localhost:8080/_/progress.html
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 `./my.namespace.feature.project.lib/src/my/namespace/feature/lib/library.js`)
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 *(but the tests are not executed)*.
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
  [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_large)
@@ -16,20 +16,35 @@ if (reportDir) {
16
16
  }
17
17
 
18
18
  process.on('message', async message => {
19
- if (message.command === 'stop') {
20
- if (reportDir && page) {
21
- await page.screenshot({ path: join(reportDir, 'screenshot.png') })
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
- await browser.close()
24
- process.exit(0)
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
- console.error('ERROR', error)
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
- console.log(`Server running at ${url}`)
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
- console.log('Watching changes on', job.webapp)
51
+ output.watching(job.webapp)
50
52
  watch(job.webapp, { recursive: true }, (eventType, filename) => {
51
- console.log(eventType, filename)
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
- console.error('ERROR', error)
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.0.7",
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
- "colors": "^1.4.0",
43
- "mime": "^2.5.2",
42
+ "mime": "^3.0.0",
44
43
  "nyc": "^15.1.0",
45
- "puppeteer": "^10.2.0",
46
- "reserve": "^1.11.7"
44
+ "puppeteer": "^11.0.0",
45
+ "reserve": "^1.12.1"
47
46
  },
48
47
  "devDependencies": {
49
- "jest": "^27.1.0",
50
- "nock": "^13.1.3",
51
- "standard": "^16.0.3"
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
- console.log('>>', relativeUrl)
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
- console.log('<<', relativeUrl)
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
- console.log('>> RETRY', pageBrowser.retry, relativeUrl)
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: 'inherit' })
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
- console.log('!! TIMEOUT', relativeUrl)
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
- console.log('!! BROWSER CLOSED', relativeUrl)
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
- console.log('nyc', ...args)
13
- const childProcess = fork(nycScript, args, {
14
- stdio: 'inherit'
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
- const url = extractUrl(request.headers)
16
- const data = JSON.parse(await body(request))
17
- if (job.parallel === -1) {
18
- console.log(url, data)
19
- }
20
- try {
21
- await implementation.call(this, url, data)
22
- } catch (e) {
23
- console.error(`Exception when processing ${url}`)
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
- job.testPages[url] = {
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: endpoint((url, report) => {
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.tests.push(report)
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
- promise = writeFile(coverageFileName, JSON.stringify(report.__coverage__))
167
+ await writeFile(coverageFileName, JSON.stringify(report.__coverage__))
127
168
  delete report.__coverage__
128
169
  }
129
170
  page.report = report
130
171
  }
131
- promise.then(() => stop(job, url))
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' && Array.isArray(value)) {
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': json.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
- const xhr = new XMLHttpRequest()
7
- xhr.open('POST', '/_/' + url)
8
- xhr.send(JSON.stringify(data))
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
- post('QUnit/begin', details)
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
- console.warn('Invalid browserRetry value, defaulting to 1')
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
- console.error(`Unexpected value for option ${name} : ${error.message}`)
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.forEach(child => {
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
- json('pages.json')
32
- .then(pages => Object.keys(pages).forEach(page => {
33
- const path = pages[page]
34
- const details = document.body.appendChild(el('details'))
35
- const summary = details.appendChild(el('summary'))
36
- const ok = '\u2714\ufe0f'
37
- const ko = '\u274c'
38
- summary.appendChild(el('b', {}, page))
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
- json(`${path}.json`)
41
- .then(report => {
42
- if (!report.report || report.failed) {
43
- summary.appendChild(tx(ko))
44
- } else {
45
- summary.appendChild(tx(ok))
46
- }
47
- const table = details.appendChild(el('table', { width: '100%' }))
48
- const row = (module, name = ' ') => {
49
- const tr = table.appendChild(el('tr'))
50
- tr.innerHTML = `<td width="15%">${module}</td><td width="85%">${name}</td>`
51
- return tr
52
- }
53
- let lastModule
54
- report.tests.forEach(test => {
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
- module = ' '
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
- row(`Timed out ${ko}`)
122
+ stringifiedValue = JSON.stringify(value)
78
123
  }
79
- details.appendChild(el('a', { href: `${path}/screenshot.png`, target: '_blank' }, 'screenshot'))
80
- details.appendChild(el('span', {}, ' '))
81
- details.appendChild(el('a', { href: `${path}/console.txt`, target: '_blank' }, 'console'))
82
- }, () => {
83
- summary.appendChild(tx(ko))
84
- details.appendChild(el('p', {}, 'No report found ', el('i', {}, '(fatal error or timeout)')))
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
@@ -19,6 +19,6 @@ module.exports = job => check({
19
19
  strict: true,
20
20
  'ignore-if-not-found': true
21
21
  },
22
- ...unhandled
22
+ ...unhandled(job)
23
23
  ]
24
24
  })
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, '/test/testsuite.qunit.html')
29
+ await start(job, '/' + job.testsuite)
17
30
  if (job.testPageUrls.length === 0) {
18
- console.log('No test page found')
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
- console.log('!! TIMEOUT', url)
58
+ output.globalTimeout(url)
46
59
  } else if (job.failFast && job.failed) {
47
- console.log('!! FAILFAST', url)
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
- console.table(pages)
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
- console.log(`Time spent: ${new Date() - job.start}ms`)
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
- console.error(`Unable to cache '${path}' (status ${response.statusCode})`)
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
- custom: ({ headers, method, url }, response) => {
7
- if (method === 'GET' && url.match(/favicon\.ico$|-preload\.js$|-dbg(\.[^.]+)*\.js$|i18n_\w+\.properties$/)) {
8
- return 404 // expected
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
- if (method === 'GET') {
11
- console.warn(`?? ${extractUrl(headers)} 404 ${method} ${url}`)
12
- return 404
13
- }
14
- console.error(`!! ${extractUrl(headers)} 500 ${method} ${url}`)
15
- return 500
16
- }
17
- }]
31
+ }]
32
+ }