ui5-test-runner 3.3.0 → 3.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
6
  "files": [
package/src/browsers.js CHANGED
@@ -4,7 +4,7 @@ const { fork } = require('child_process')
4
4
  const { join } = require('path')
5
5
  const { writeFile, readFile, open, stat, unlink } = require('fs/promises')
6
6
  const { recreateDir, filename, allocPromise } = require('./tools')
7
- const { getPageTimeout } = require('./timeout')
7
+ const { getPageTimeout, pageTimedOut } = require('./timeout')
8
8
  const { getOutput } = require('./output')
9
9
  const { resolvePackage } = require('./npm')
10
10
  const { UTRError } = require('./error')
@@ -174,6 +174,7 @@ async function run (job, pageBrowser) {
174
174
  if (timeout) {
175
175
  pageBrowser.timeoutId = setTimeout(() => {
176
176
  output.browserTimeout(url, dir)
177
+ pageTimedOut(job, url)
177
178
  stop(job, url)
178
179
  }, timeout)
179
180
  }
package/src/coverage.js CHANGED
@@ -110,7 +110,20 @@ async function generateCoverageReport (job) {
110
110
  await writeFile(coverageFilename, JSON.stringify(coverageData))
111
111
  }
112
112
  const reporters = job.coverageReporters.map(reporter => `--reporter=${reporter}`)
113
- await nyc(job, 'report', ...reporters, '--temp-dir', coverageMergedDir, '--report-dir', job.coverageReportDir, '--nycrc-path', job[$nycSettingsPath])
113
+ if (!job.coverageReporters.includes('text')) {
114
+ reporters.push('--reporter=text')
115
+ }
116
+ const checks = []
117
+ if (job.coverageCheckBranches || job.coverageCheckFunctions || job.coverageCheckLines || job.coverageCheckStatements) {
118
+ checks.push(
119
+ `--branches=${job.coverageCheckBranches}`,
120
+ `--functions=${job.coverageCheckFunctions}`,
121
+ `--lines=${job.coverageCheckLines}`,
122
+ `--statements=${job.coverageCheckStatements}`,
123
+ '--check-coverage'
124
+ )
125
+ }
126
+ await nyc(job, 'report', ...reporters, ...checks, '--temp-dir', coverageMergedDir, '--report-dir', job.coverageReportDir, '--nycrc-path', job[$nycSettingsPath])
114
127
  }
115
128
 
116
129
  module.exports = {
@@ -151,6 +164,7 @@ module.exports = {
151
164
  coverageGlobalScopeFunc: false
152
165
  })
153
166
  const instrument = promisify(instrumenter.instrument.bind(instrumenter))
167
+ const sources = {}
154
168
  return [{
155
169
  match: /(.*\.js)(\?.*)?$/,
156
170
  custom: async (request, response, url) => {
@@ -161,8 +175,15 @@ module.exports = {
161
175
  try {
162
176
  await access(sourcePath, constants.R_OK)
163
177
  } catch (e) {
164
- console.log('download', url)
165
- await download(origin + url, sourcePath)
178
+ try {
179
+ if (sources[url]) {
180
+ await sources[url]
181
+ } else {
182
+ sources[url] = await download(origin + url, sourcePath)
183
+ }
184
+ } catch (statusCode) {
185
+ return statusCode
186
+ }
166
187
  }
167
188
  const source = (await readFile(sourcePath)).toString()
168
189
  const instrumentedSource = await instrument(source, sourcePath)
@@ -39,6 +39,8 @@ async function main () {
39
39
  >`)
40
40
  if (test.skip) {
41
41
  o(' <skipped></skipped>')
42
+ } else if (!test.report) {
43
+ o(' <skipped>(no report found)</skipped>')
42
44
  } else if (test.report.failed) {
43
45
  test.logs
44
46
  .filter(({ result }) => !result)
@@ -12,10 +12,26 @@ require('./browser')({
12
12
  ['-h, --viewport-height <height>', 'Viewport height', 1080],
13
13
  ['-l, --language <lang...>', 'Language(s)', ['en-US']],
14
14
  ['-u, --unsecure', 'Disable security features', false]
15
- ],
16
- capabilities: {
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 {
17
33
  modules: ['puppeteer'],
18
- screenshot: '.png',
34
+ screenshot,
19
35
  scripts: true,
20
36
  traces: ['console', 'network']
21
37
  }
@@ -12,6 +12,9 @@
12
12
  <h1>{{ status || 'Test report' }}</h1>
13
13
  <div {{if}}="end === undefined" class="elapsed">In progress since {{ elapsed(start) }}</div>
14
14
  <div {{else}} class="elapsed">Duration : {{ elapsed(start, end) }} <a href="#" id="download">&#128230;</a></div>
15
+ <div {{if}}="timedOut">
16
+ &#9202;&#65039; Timed out
17
+ </div>
15
18
  <table style="visibility: {{ testPageUrls.length > 0 ? 'visible' : 'hidden' }};">
16
19
  <tr>
17
20
  <th>&nbsp;</th>
@@ -30,7 +33,7 @@
30
33
  <td>
31
34
  <span {{if}}="!qunitPages[url]">-</span>
32
35
  <span {{elseif}}="qunitPages[url].isOpa" title="OPA test">&#129404;</span>
33
- <span {{else}} title="Unit test">&#129514;</span>
36
+ <span {{elseif}}="qunitPages[url].start" title="Unit test">&#129514;</span>
34
37
  </td>
35
38
  <td>
36
39
  <span {{if}}="!qunitPages[url]">-</span>
@@ -56,6 +59,9 @@
56
59
  <a href="#">&#9204; back to report</a>
57
60
  <div {{if}}="qunitPage.end === undefined" class="elapsed">In progress since {{ elapsed(qunitPage.start) }}</div>
58
61
  <div {{else}} class="elapsed">Duration : {{ elapsed(qunitPage.start, qunitPage.end) }}</div>
62
+ <div {{if}}="timedOut">
63
+ &#9202;&#65039; Timed out
64
+ </div>
59
65
  <div {{for}}="module of qunitPage.modules">
60
66
  <h2>{{ module.name }}</h2>
61
67
  <div {{for}}="test of module.tests">
@@ -1,19 +1,29 @@
1
1
  'use strict'
2
2
 
3
3
  const { join, isAbsolute } = require('path')
4
- const [, , reportDir] = process.argv
4
+ const [, , reportDir, expectedWidth] = process.argv
5
5
  const { pad } = require('../tools')
6
6
 
7
- const p = pad(process.stdout.columns || 80)
7
+ const p = pad(process.stdout.columns || parseInt(expectedWidth || '80', 10))
8
8
  const log = console.log.bind(console)
9
9
 
10
10
  function collectErrors (page) {
11
11
  const errors = []
12
12
  page.modules.forEach(module => {
13
- module.tests.forEach(test => {
14
- if (test.report.failed) {
13
+ module.tests.every(test => {
14
+ if (!test.report) {
15
+ if (!test.skip) {
16
+ test.logs ??= []
17
+ test.logs.push({
18
+ message: '(no report found)'
19
+ })
20
+ errors.push({ module: module.name, ...test })
21
+ return false
22
+ }
23
+ } else if (test.report.failed) {
15
24
  errors.push({ module: module.name, ...test })
16
25
  }
26
+ return true
17
27
  })
18
28
  })
19
29
  return errors
@@ -28,8 +38,11 @@ async function main () {
28
38
  }
29
39
  const job = require(jobPath)
30
40
  const failedUrls = []
31
- log(p`┌─${pad.x('─')}───────────────────┐`)
41
+ let rendered = 0
32
42
  function render (url) {
43
+ if (++rendered === 1) {
44
+ log(p`┌─${pad.x('─')}───────────────────┐`)
45
+ }
33
46
  const page = job.qunitPages && job.qunitPages[url]
34
47
  if (!page || !page.report) {
35
48
  log(p`│${pad.lt(url)} 🧨 │`)
@@ -49,7 +62,9 @@ async function main () {
49
62
  render(url)
50
63
  }
51
64
  })
52
- log(p`└─${pad.x('─')}────────────────────┘`)
65
+ if (rendered > 0) {
66
+ log(p`└─${pad.x('─')}────────────────────┘`)
67
+ }
53
68
  failedUrls.forEach(url => {
54
69
  log()
55
70
  log(p`[${pad.lt(url)}]`)
package/src/job.js CHANGED
@@ -7,7 +7,7 @@ const { name, description, version } = require(join(__dirname, '../package.json'
7
7
  const { getOutput } = require('./output')
8
8
  const { $valueSources } = require('./symbols')
9
9
  const { buildAndCheckMode } = require('./job-mode')
10
- const { boolean, integer, timeout, url, arrayOf, regex } = require('./options')
10
+ const { boolean, integer, timeout, url, arrayOf, regex, percent } = require('./options')
11
11
 
12
12
  const $status = Symbol('status')
13
13
 
@@ -106,7 +106,7 @@ function getCommand (cwd) {
106
106
  .option('-bt, --browser-close-timeout <timeout>', '[💻🔗🧪] Maximum waiting time for browser close', timeout, 2000)
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
- // Common to legacy and testing
109
+ // Common to legacy and url
110
110
  .option('-pf, --page-filter <regexp>', '[💻🔗] Filter out pages not matching the regexp')
111
111
  .option('-pp, --page-params <params>', '[💻🔗] Add parameters to page URL')
112
112
  .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)
@@ -119,13 +119,18 @@ function getCommand (cwd) {
119
119
  .option('--coverage [flag]', '[💻🔗] Enable or disable code coverage', boolean)
120
120
  .option('--no-coverage', '[💻🔗] Disable code coverage')
121
121
  .option('-cs, --coverage-settings <path>', '[💻🔗] Path to a custom nyc.json file providing settings for instrumentation (relative to cwd or use $/ for provided ones)', '$/nyc.json')
122
- .option('-ct, --coverage-temp-dir <path>', '[💻🔗] Directory to output raw coverage information to (relative to cwd)', '.nyc_output')
123
- .option('-cr, --coverage-report-dir <path>', '[💻🔗] Directory to store the coverage report files (relative to cwd)', 'coverage')
124
- .option('-cr, --coverage-reporters <reporter...>', '[💻🔗] List of nyc reporters to use', ['lcov', 'cobertura'])
122
+ .option('-ctd, --coverage-temp-dir <path>', '[💻🔗] Directory to output raw coverage information to (relative to cwd)', '.nyc_output')
123
+ .option('-crd, --coverage-report-dir <path>', '[💻🔗] Directory to store the coverage report files (relative to cwd)', 'coverage')
124
+ .option('-cr, --coverage-reporters <reporter...>', '[💻🔗] List of nyc reporters to use (text is always used)', ['lcov', 'cobertura'])
125
+ .option('-ccb, --coverage-check-branches <percent>', '[💻🔗] What % of branches must be covered', percent, 0)
126
+ .option('-ccf, --coverage-check-functions <percent>', '[💻🔗] What % of functions must be covered', percent, 0)
127
+ .option('-ccl, --coverage-check-lines <percent>', '[💻🔗] What % of lines must be covered', percent, 0)
128
+ .option('-ccs, --coverage-check-statements <percent>', '[💻🔗] What % of statements must be covered', percent, 0)
125
129
  .option('-s, --serve-only [flag]', '[💻🔗] Serve only', boolean, false)
126
130
 
127
- // Specific to legacy
131
+ // Specific to legacy (and might be used with url if pointing to local project)
128
132
  .option('--ui5 <url>', '[💻] UI5 url', url, 'https://ui5.sap.com')
133
+ .option('--no-ui5', '[💻] Disable UI5 mapping (also disable libs)')
129
134
  .option('--libs <lib...>', '[💻] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
130
135
  .option('--mappings <mapping...>', '[💻] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
131
136
  .option('--cache <path>', '[💻] Cache UI5 resources locally in the given folder (empty to disable)')
@@ -135,8 +140,8 @@ function getCommand (cwd) {
135
140
 
136
141
  // Specific to coverage in url mode (experimental)
137
142
  .option('-cp, --coverage-proxy [flag]', `[🔗] ${EXPERIMENTAL_OPTION} use internal proxy to instrument remote files`, boolean, false)
138
- .option('-cpi, --coverage-proxy-include <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to instrument for coverage`, regex, regex('webapp/.*'))
139
- .option('-cpe, --coverage-proxy-exclude <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to ignore for coverage`, regex, regex('(test-)?resources/.*'))
143
+ .option('-cpi, --coverage-proxy-include <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to instrument for coverage`, regex, regex('.*'))
144
+ .option('-cpe, --coverage-proxy-exclude <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to ignore for coverage`, regex, regex('/((test-)?resources|tests?)/.*'))
140
145
 
141
146
  .addOption(new Option('--debug-probe-only', DEBUG_OPTION, boolean).hideHelp())
142
147
  .addOption(new Option('--debug-keep-browser-open', DEBUG_OPTION, boolean).hideHelp())
package/src/options.js CHANGED
@@ -77,5 +77,13 @@ module.exports = {
77
77
  result.push(typeValidator(value))
78
78
  return result
79
79
  }
80
+ },
81
+
82
+ percent (value) {
83
+ const int = integer(value)
84
+ if (int > 100) {
85
+ throw new InvalidArgumentError('Invalid percent')
86
+ }
87
+ return int
80
88
  }
81
89
  }
@@ -1,4 +1,4 @@
1
- const { any, boolean, integer, timeout, url, arrayOf } = require('./options')
1
+ const { any, boolean, integer, timeout, url, arrayOf, percent } = require('./options')
2
2
  const { InvalidArgumentError } = require('commander')
3
3
 
4
4
  function checkType ({ method, validValues, invalidValues }) {
@@ -115,6 +115,22 @@ describe('src/options', () => {
115
115
  ]
116
116
  })
117
117
 
118
+ checkType({
119
+ method: percent,
120
+ validValues: {
121
+ 0: 0,
122
+ 1: 1,
123
+ 10: 10,
124
+ 100: 100
125
+ },
126
+ invalidValues: [
127
+ '',
128
+ '-1',
129
+ 'abc',
130
+ '101'
131
+ ]
132
+ })
133
+
118
134
  describe('arrayOf', () => {
119
135
  it('builds a type validator that aggregates validated values in an array', () => {
120
136
  const validator = arrayOf(integer)
package/src/report.js CHANGED
@@ -22,7 +22,7 @@ async function save (job) {
22
22
 
23
23
  function generateTextReport (job) {
24
24
  const { promise, resolve } = allocPromise()
25
- const childProcess = fork(join(__dirname, 'defaults/text-report.js'), [job.reportDir], { stdio: 'pipe' })
25
+ const childProcess = fork(join(__dirname, 'defaults/text-report.js'), [job.reportDir, process.stdout.columns || ''], { stdio: 'pipe' })
26
26
  getOutput(job).monitor(childProcess, true)
27
27
  childProcess.on('close', resolve)
28
28
  return promise
@@ -301,7 +301,7 @@ describe('simulate', () => {
301
301
 
302
302
  describe('global timeout', () => {
303
303
  beforeAll(async () => {
304
- await setup('timeout', {
304
+ await setup('global-timeout', {
305
305
  parallel: 1,
306
306
  globalTimeout: 10000
307
307
  })
@@ -325,6 +325,62 @@ describe('simulate', () => {
325
325
 
326
326
  it('failed', () => {
327
327
  expect(job.failed).toStrictEqual(true)
328
+ expect(job.timedOut).toStrictEqual(true)
329
+ })
330
+ })
331
+
332
+ describe('page timeout', () => {
333
+ beforeAll(async () => {
334
+ await setup('page-timeout', {
335
+ parallel: 1,
336
+ pageTimeout: 250
337
+ })
338
+ pages = {
339
+ 'testsuite.qunit.html': async referer => {
340
+ await post('/_/addTestPages', referer, [
341
+ '/page1.html'
342
+ ])
343
+ },
344
+ 'page1.html': async headers => {
345
+ await post('/_/QUnit/begin', headers, {
346
+ totalTests: 3,
347
+ modules: [{
348
+ name: '0',
349
+ tests: [{
350
+ name: '1',
351
+ testId: '1'
352
+ }, {
353
+ name: '2',
354
+ testId: '2'
355
+ }, {
356
+ name: '3',
357
+ testId: '3'
358
+ }]
359
+ }]
360
+ })
361
+ await post('/_/QUnit/testStart', headers, { module: '0', name: '1', testId: '1' })
362
+ await post('/_/QUnit/testDone', headers, { testId: '1', failed: 0, passed: 1 })
363
+ await post('/_/QUnit/testStart', headers, { module: '0', name: '2', testId: '2' })
364
+ }
365
+ }
366
+ await safeExecute()
367
+ })
368
+
369
+ it('failed', () => {
370
+ expect(job.failed).toStrictEqual(true)
371
+ expect(job.timedOut).toStrictEqual(true)
372
+ const page = job.qunitPages['http://localhost:0/page1.html']
373
+ expect(page.failed).toStrictEqual(2)
374
+ expect(page.end).not.toBeUndefined()
375
+ expect(page.timedOut).toStrictEqual(true)
376
+ const { tests } = page.modules[0]
377
+ expect(tests[1].report.failed).toStrictEqual(1)
378
+ expect(tests[1].report.passed).toStrictEqual(0)
379
+ expect(tests[1].end).not.toBeUndefined()
380
+ expect(tests[2].report.failed).toStrictEqual(1)
381
+ expect(tests[2].report.passed).toStrictEqual(0)
382
+ expect(tests[2].start).toBeUndefined()
383
+ expect(tests[2].end).toBeUndefined()
328
384
  })
329
385
  })
330
386
 
package/src/tests.js CHANGED
@@ -35,6 +35,7 @@ async function run (task, job) {
35
35
  if (globallyTimedOut(job)) {
36
36
  output.globalTimeout(url)
37
37
  job.failed = true
38
+ job.timedOut = true
38
39
  } else if (job.failFast && job.failed) {
39
40
  output.failFast(url)
40
41
  } else {
package/src/timeout.js CHANGED
@@ -16,5 +16,38 @@ module.exports = {
16
16
  return new Date() - job.start > job.globalTimeout
17
17
  }
18
18
  return false
19
+ },
20
+
21
+ pageTimedOut (job, url) {
22
+ const page = job.qunitPages && job.qunitPages[url]
23
+ if (page) {
24
+ const now = new Date()
25
+ page.end = now
26
+ page.timedOut = true
27
+ page.modules.forEach(module => {
28
+ module.tests.forEach(test => {
29
+ if (!test.report) {
30
+ ++page.failed
31
+ if (test.start && !test.end) {
32
+ test.end = now
33
+ }
34
+ test.logs ??= []
35
+ test.logs.push({
36
+ result: false,
37
+ message: 'Page timed out'
38
+ })
39
+ test.report = {
40
+ skipped: false,
41
+ todo: false,
42
+ failed: 1,
43
+ passed: 0,
44
+ total: 1
45
+ }
46
+ }
47
+ })
48
+ })
49
+ job.failed = true
50
+ job.timedOut = true
51
+ }
19
52
  }
20
53
  }
package/src/tools.js CHANGED
@@ -6,6 +6,7 @@ const { createHash } = require('crypto')
6
6
  const { createWriteStream } = require('fs')
7
7
  const http = require('http')
8
8
  const https = require('https')
9
+ const { unlink } = require('fs/promises')
9
10
 
10
11
  const recursive = { recursive: true }
11
12
 
@@ -136,7 +137,13 @@ async function download (url, filename) {
136
137
  await mkdir(dirname(filename), recursive)
137
138
  const output = createWriteStream(filename)
138
139
  const { promise, resolve, reject } = allocPromise()
139
- const request = protocol.request(options, response => {
140
+ const request = protocol.request(options, async response => {
141
+ if (response.statusCode !== 200) {
142
+ reject(response.statusCode)
143
+ output.end()
144
+ await unlink(filename)
145
+ return
146
+ }
140
147
  response.on('error', reject)
141
148
  response.on('end', resolve)
142
149
  response.pipe(output)
package/src/ui5.js CHANGED
@@ -7,7 +7,7 @@ const { capture } = require('reserve')
7
7
  const { getOutput } = require('./output')
8
8
 
9
9
  module.exports = job => {
10
- if (job.mode === 'url') {
10
+ if (job.noUi5) {
11
11
  return []
12
12
  }
13
13
 
package/src/unhandled.js CHANGED
@@ -23,7 +23,7 @@ module.exports = job => {
23
23
  getOutput(job).unhandled()
24
24
  outputUnhandled = false
25
25
  }
26
- writeFile(unhandled, `${extractPageUrl(headers)} ${status} ${method} ${url}\n`, {
26
+ writeFile(unhandled, `${extractPageUrl(headers) || headers.referer} ${status} ${method} ${url}\n`, {
27
27
  flag: 'a'
28
28
  }, noop)
29
29
  return status