ui5-test-runner 3.3.4 → 4.0.0

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