ui5-test-runner 2.0.4 → 3.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/) >= 14
23
+ * Works with [Node.js](https://nodejs.org/en/download/) >= 16
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`
@@ -47,7 +47,10 @@ A self-sufficient test runner for UI5 applications enabling parallel execution o
47
47
  ## ⚖️ License
48
48
  [![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)
49
49
 
50
- ## ⚠️ Breaking change
50
+ ## ⚠️ Breaking changes
51
+
52
+ ### v3
53
+ * Dropping support of Node.js 14
51
54
 
52
55
  ### v2
53
56
 
package/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "2.0.4",
3
+ "version": "3.0.0",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -12,7 +12,7 @@
12
12
  "ui5-test-runner": "./index.js"
13
13
  },
14
14
  "engines": {
15
- "node": ">=14.0.0"
15
+ "node": ">=16"
16
16
  },
17
17
  "scripts": {
18
18
  "lint": "standard --fix",
@@ -22,7 +22,7 @@
22
22
  "test:integration:puppeteer": "node . --capabilities --browser $/puppeteer.js",
23
23
  "test:integration:selenium-webdriver-chrome": "node . --capabilities --browser $/selenium-webdriver.js -- --browser chrome",
24
24
  "test:integration:jsdom": "node . --capabilities --browser $/jsdom.js",
25
- "test:report": "reserve --config ./test/report/reserve.json",
25
+ "test:report": "node ./src/defaults/report.js ./test/report && reserve --config ./test/report/reserve.json",
26
26
  "build:doc": "node build/doc"
27
27
  },
28
28
  "repository": {
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "homepage": "https://github.com/ArnaudBuchholz/ui5-test-runner#readme",
47
47
  "dependencies": {
48
- "commander": "^10.0.0",
48
+ "commander": "^10.0.1",
49
49
  "mime": "^3.0.0",
50
50
  "punybind": "^1.2.1",
51
51
  "punyexpr": "^1.0.4",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "jest": "^29.5.0",
56
- "nock": "^13.3.0",
56
+ "nock": "^13.3.1",
57
57
  "nyc": "^15.1.0",
58
58
  "standard": "^17.0.0"
59
59
  },
@@ -142,7 +142,7 @@ describe('src/browser', () => {
142
142
  })
143
143
  await probe(job)
144
144
  expect(job.browserCapabilities.modules).toStrictEqual(['reserve', 'dependentModule'])
145
- expect(job.browserModules.reserve).toStrictEqual('reserve')
145
+ expect(job.browserModules.reserve).toStrictEqual(join(__dirname, '../node_modules/reserve'))
146
146
  expect(job.browserModules.dependentModule).toStrictEqual(join(npmGlobal, 'dependentModule'))
147
147
  })
148
148
 
@@ -78,6 +78,7 @@
78
78
  <pre {{else}}>&#10060; {{ log.message }}</pre>
79
79
  <img {{if}}="log.screenshot" loading="lazy" class="log" src="{{ qunitTest.pageId }}/{{ log.screenshot }}" alt="Copy folder '{{ qunitTest.pageId }}' from the job report">
80
80
  </div>
81
+ <img {{if}}="qunitTest.screenshot" loading="lazy" class="log" src="{{ qunitTest.pageId }}/{{ qunitTest.screenshot }}" alt="Copy folder '{{ qunitTest.pageId }}' from the job report">
81
82
  </div>
82
83
  <div style="display: {{ disconnected ? 'block' : 'none' }};">&#10060; Disconnected</div>
83
84
  </body>
@@ -1,7 +1,7 @@
1
1
  /* global report, job */
2
2
  report.ready.then(update => {
3
- const hashChange = () => {
4
- const [, pageId, testId] = location.hash.match(/#?([^-]*)(?:-(.*))?/)
3
+ const hashChange = hash => {
4
+ const [, pageId, testId] = (hash || '').match(/#?([^-]*)(?:-(.*))?/)
5
5
  let [qunitPage, qunitTest] = [null, null]
6
6
  if (pageId) {
7
7
  const url = Object.keys(job.qunitPages).find(pageUrl => job.qunitPages[pageUrl].id === pageId)
@@ -39,6 +39,19 @@ report.ready.then(update => {
39
39
  })
40
40
  }
41
41
 
42
- window.addEventListener('hashchange', hashChange)
43
- hashChange()
42
+ window.addEventListener('hashchange', () => {
43
+ hashChange(location.hash)
44
+ })
45
+ if (window.location.href === 'about:srcdoc') {
46
+ window.addEventListener('click', (event) => {
47
+ const { href } = event.target
48
+ if (href) {
49
+ const lastHash = href.lastIndexOf('#')
50
+ hashChange(href.substring(lastHash))
51
+ }
52
+ event.preventDefault()
53
+ return false
54
+ })
55
+ }
56
+ hashChange(location.hash)
44
57
  })
@@ -12,7 +12,8 @@ async function readDefault (name) {
12
12
  }
13
13
 
14
14
  async function readDependency (name) {
15
- return (await readFile(resolveDependencyPath(name))).toString()
15
+ const path = join(resolveDependencyPath(name), `dist/${name}.js`)
16
+ return (await readFile(path)).toString()
16
17
  }
17
18
 
18
19
  function minifyJs (src) {
package/src/endpoints.js CHANGED
@@ -12,8 +12,8 @@ const { readFile } = require('fs/promises')
12
12
  const { TextEncoder } = require('util')
13
13
  const { resolveDependencyPath } = require('./npm.js')
14
14
 
15
- const punyexprBinPath = resolveDependencyPath('punyexpr')
16
- const punybindBinPath = resolveDependencyPath('punybind')
15
+ const punyexprBinPath = join(resolveDependencyPath('punyexpr'), 'dist/punyexpr.js')
16
+ const punybindBinPath = join(resolveDependencyPath('punybind'), 'dist/punybind.js')
17
17
 
18
18
  module.exports = job => {
19
19
  async function endpointImpl (api, implementation, request) {
package/src/job.js CHANGED
@@ -95,6 +95,7 @@ function getCommand (cwd) {
95
95
  .option('-r, --report-dir <path>', '[💻🔗🧪] Directory to output test reports (relative to cwd)', 'report')
96
96
  .option('-pt, --page-timeout <timeout>', '[💻🔗🧪] Limit the page execution time, fails the page if it takes longer than the timeout (0 means no timeout)', timeout, 0)
97
97
  .option('-f, --fail-fast [flag]', '[💻🔗🧪] Stop the execution after the first failing page', boolean, false)
98
+ .option('-fo, --fail-opa-fast [flag]', '[💻🔗] Stop the OPA page execution after the first failing test', boolean, false)
98
99
  .option('-k, --keep-alive [flag]', '[💻🔗🧪] Keep the server alive', boolean, false)
99
100
  .option('-l, --log-server [flag]', '[💻🔗🧪] Log inner server traces', boolean, false)
100
101
  .option('-p, --parallel <count>', '[💻🔗🧪] Number of parallel tests executions', 2)
package/src/npm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const { exec } = require('child_process')
2
- const { join } = require('path')
2
+ const { sep, join } = require('path')
3
3
  const { stat } = require('fs/promises')
4
4
  const { UTRError } = require('./error')
5
5
  const { getOutput } = require('./output')
@@ -31,18 +31,24 @@ async function folderExists (path) {
31
31
  let localRoot
32
32
  let globalRoot
33
33
 
34
+ function resolveDependencyPath (name) {
35
+ require(name)
36
+ const pattern = `${sep}node_modules${sep}${name}${sep}`
37
+ const path = Object.keys(require.cache).filter(path => path.includes(pattern))[0]
38
+ if (path) {
39
+ const pos = path.indexOf(pattern)
40
+ return path.substring(0, pos + pattern.length - 1)
41
+ }
42
+ }
43
+
34
44
  module.exports = {
35
- resolveDependencyPath (name) {
36
- require(name)
37
- return Object.keys(require.cache).filter(path => path.endsWith(`${name}.js`))[0]
38
- },
45
+ resolveDependencyPath,
39
46
 
40
47
  async resolvePackage (job, name) {
41
48
  let modulePath
42
49
  let justInstalled = false
43
50
  try {
44
- require(name)
45
- modulePath = name
51
+ modulePath = resolveDependencyPath(name)
46
52
  } catch (e) {
47
53
  }
48
54
  if (!modulePath) {
package/src/npm.spec.js CHANGED
@@ -40,7 +40,7 @@ describe('src/npm', () => {
40
40
 
41
41
  it('detects already installed local package', async () => {
42
42
  const path = await resolvePackage(job, 'reserve')
43
- expect(path).toStrictEqual('reserve')
43
+ expect(path).toStrictEqual(join(__dirname, '../node_modules/reserve'))
44
44
  expect(output.resolvedPackage).toHaveBeenCalledTimes(1)
45
45
  expect(output.packageNotLatest).not.toHaveBeenCalled()
46
46
  })
package/src/output.js CHANGED
@@ -185,6 +185,13 @@ function browserIssue (job, { type, url, code, dir }) {
185
185
  log(job, p`└──────────${pad.x('─')}┘`)
186
186
  }
187
187
 
188
+ const formatTime = duration => {
189
+ duration = Math.ceil(duration / 1000)
190
+ const seconds = duration % 60
191
+ const minutes = (duration - seconds) / 60
192
+ return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0')
193
+ }
194
+
188
195
  function build (job) {
189
196
  let wrap
190
197
  if (interactive) {
@@ -199,6 +206,8 @@ function build (job) {
199
206
  } else {
200
207
  wrap = method => method
201
208
  }
209
+ const outputStart = Date.now()
210
+ const getElapsed = () => formatTime(Date.now() - outputStart)
202
211
 
203
212
  return {
204
213
  lastTick: 0,
@@ -236,9 +245,10 @@ function build (job) {
236
245
  } else {
237
246
  method = log
238
247
  }
248
+ const text = `${getElapsed()} │ ${status}`
239
249
  method(job, '')
240
- method(job, status)
241
- method(job, ''.padStart(status.length, '─'))
250
+ method(job, text)
251
+ method(job, '──────┴'.padEnd(text.length, '─'))
242
252
  },
243
253
 
244
254
  watching: wrap(path => {
@@ -295,18 +305,25 @@ function build (job) {
295
305
  },
296
306
 
297
307
  browserStart (url) {
308
+ const text = p80()`${getElapsed()} >> ${pad.lt(url)}`
298
309
  if (interactive) {
299
- output(job, '>>', url)
310
+ output(job, text)
300
311
  } else {
301
- wrap(() => log(job, p80()`>> ${pad.lt(url)}`))()
312
+ wrap(() => log(job, text))()
302
313
  }
303
314
  },
304
315
 
305
316
  browserStopped (url) {
317
+ let duration = ''
318
+ const page = job.qunitPages && job.qunitPages[url]
319
+ if (page) {
320
+ duration = ' (' + formatTime(page.end - page.start) + ')'
321
+ }
322
+ const text = p80()`${getElapsed()} << ${pad.lt(url + duration)}`
306
323
  if (interactive) {
307
- output(job, '<<', url)
324
+ output(job, text)
308
325
  } else {
309
- wrap(() => log(job, p80()`<< ${pad.lt(url)}`))()
326
+ wrap(() => log(job, text))()
310
327
  }
311
328
  },
312
329
 
@@ -436,10 +453,6 @@ function build (job) {
436
453
  log(job, p80()`!! FAILFAST ${pad.lt(url)}`)
437
454
  }),
438
455
 
439
- timeSpent: wrap((start, end = new Date()) => {
440
- log(job, p80()`Time spent: ${end - start}ms`)
441
- }),
442
-
443
456
  noTestPageFound: wrap(() => {
444
457
  err(job, p80()`No test page found (or all filtered out)`)
445
458
  }),
@@ -32,6 +32,24 @@ function get (job, urlWithHash, testId) {
32
32
  return { url, page, test }
33
33
  }
34
34
 
35
+ async function done (job, urlWithHash, report) {
36
+ const { url, page } = get(job, urlWithHash)
37
+ if (job.browserCapabilities.screenshot) {
38
+ try {
39
+ await screenshot(job, url, 'done')
40
+ } catch (error) {
41
+ getOutput(job).genericError(error, url)
42
+ }
43
+ }
44
+ if (report.__coverage__) {
45
+ collect(job, url, report.__coverage__)
46
+ delete report.__coverage__
47
+ }
48
+ page.end = new Date()
49
+ page.report = report
50
+ stop(job, url)
51
+ }
52
+
35
53
  module.exports = {
36
54
  get,
37
55
 
@@ -82,7 +100,8 @@ module.exports = {
82
100
  if (failed) {
83
101
  if (job.browserCapabilities.screenshot) {
84
102
  try {
85
- await screenshot(job, url, testId)
103
+ const absoluteName = await screenshot(job, url, testId)
104
+ test.screenshot = basename(absoluteName)
86
105
  } catch (error) {
87
106
  getOutput(job).genericError(error, url)
88
107
  }
@@ -94,23 +113,23 @@ module.exports = {
94
113
  }
95
114
  test.end = new Date()
96
115
  test.report = report
116
+ if (job.failOpaFast && failed) {
117
+ // skip remaining tests
118
+ page.modules.forEach(module => {
119
+ module.tests.forEach(test => {
120
+ if (!test.report) {
121
+ test.skip = true
122
+ }
123
+ })
124
+ })
125
+ await done(job, urlWithHash, {
126
+ failed: page.failed,
127
+ passed: page.passed,
128
+ total: page.count,
129
+ runtime: 0
130
+ })
131
+ }
97
132
  },
98
133
 
99
- async done (job, urlWithHash, report) {
100
- const { url, page } = get(job, urlWithHash)
101
- if (job.browserCapabilities.screenshot) {
102
- try {
103
- await screenshot(job, url, 'done')
104
- } catch (error) {
105
- getOutput(job).genericError(error, url)
106
- }
107
- }
108
- if (report.__coverage__) {
109
- collect(job, url, report.__coverage__)
110
- delete report.__coverage__
111
- }
112
- page.end = new Date()
113
- page.report = report
114
- stop(job, url)
115
- }
134
+ done
116
135
  }
@@ -33,6 +33,7 @@ describe('src/qunit-hooks', () => {
33
33
 
34
34
  beforeEach(() => {
35
35
  screenshot.mockReset()
36
+ screenshot.mockImplementation((job, url, testId) => Promise.resolve(`whatever/${testId}.png`))
36
37
  stop.mockClear()
37
38
  mockGenericError.mockClear()
38
39
  job = {
@@ -549,6 +550,8 @@ describe('src/qunit-hooks', () => {
549
550
  total: 1
550
551
  })
551
552
  expect(screenshot).toHaveBeenCalledWith(job, url, '1a')
553
+ const { test } = get(job, url, '1a')
554
+ expect(test.screenshot).toStrictEqual('1a.png')
552
555
  })
553
556
 
554
557
  it('takes a screenshot (hash changing)', async () => {
@@ -600,6 +603,44 @@ describe('src/qunit-hooks', () => {
600
603
  expect(stop).toHaveBeenCalledWith(job, url)
601
604
  expect(job.failed).toStrictEqual(true)
602
605
  })
606
+
607
+ describe.only('fail OPA fast behavior', () => {
608
+ beforeEach(async () => {
609
+ job.failOpaFast = true
610
+ await testDone(job, url, {
611
+ ...getTestDoneFor1a(),
612
+ passed: 0,
613
+ failed: 1,
614
+ total: 1
615
+ })
616
+ })
617
+
618
+ it('fails the test immediately', () => {
619
+ const { test } = get(job, url, '1a')
620
+ expect(test).toMatchObject({
621
+ report: {
622
+ passed: 0,
623
+ failed: 1,
624
+ total: 1
625
+ }
626
+ })
627
+ })
628
+
629
+ it('flags the remaining tests as skipped', () => {
630
+ const { page } = get(job, url)
631
+ page.modules.forEach(module => {
632
+ module.tests.forEach(test => {
633
+ if (test.testId !== '1a') {
634
+ expect(test.skip).toStrictEqual(true)
635
+ }
636
+ })
637
+ })
638
+ })
639
+
640
+ it('stops the page immediately', () => {
641
+ expect(stop).toHaveBeenCalledWith(job, url)
642
+ })
643
+ })
603
644
  })
604
645
 
605
646
  describe('done', () => {
package/src/report.js CHANGED
@@ -45,7 +45,6 @@ module.exports = {
45
45
  })
46
46
  promises.push(generateCoverageReport(job))
47
47
  await Promise.all(promises)
48
- output.timeSpent(job.start)
49
48
  job.status = 'Done'
50
49
  }
51
50
  }
package/src/tools.js CHANGED
@@ -1,17 +1,8 @@
1
1
  'use strict'
2
2
 
3
- const fsPromises = require('fs').promises
4
- const { mkdir, stat } = fsPromises
3
+ const { mkdir, rm, stat } = require('fs').promises
5
4
  const { createHash } = require('crypto')
6
5
 
7
- let rm
8
- /* istanbul ignore next */ // Hard to test both in the same run
9
- if (process.version > 'v14.14') {
10
- rm = fsPromises.rm
11
- } else {
12
- rm = fsPromises.rmdir
13
- }
14
-
15
6
  const recursive = { recursive: true }
16
7
 
17
8
  const stripUrlHash = url => url.split('#')[0]