ui5-test-runner 3.2.0 → 3.3.1

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.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -17,6 +17,7 @@
17
17
  "scripts": {
18
18
  "lint": "standard --fix",
19
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",
20
+ "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",
20
21
  "test:unit": "jest",
21
22
  "test:unit:debug": "jest --runInBand",
22
23
  "test:integration:puppeteer": "node . --capabilities --browser $/puppeteer.js",
@@ -24,6 +25,7 @@
24
25
  "test:integration:jsdom": "node . --capabilities --browser $/jsdom.js",
25
26
  "test:integration:playwright": "node . --capabilities --browser $/playwright.js",
26
27
  "test:report": "node ./src/defaults/report.js ./test/report && reserve --config ./test/report/reserve.json",
28
+ "test:text-report": "node ./src/defaults/text-report.js ./test/report",
27
29
  "build:doc": "node build/doc"
28
30
  },
29
31
  "repository": {
@@ -46,20 +48,20 @@
46
48
  },
47
49
  "homepage": "https://github.com/ArnaudBuchholz/ui5-test-runner#readme",
48
50
  "dependencies": {
49
- "commander": "^10.0.1",
51
+ "commander": "^11.0.0",
50
52
  "mime": "^3.0.0",
51
53
  "punybind": "^1.2.1",
52
54
  "punyexpr": "^1.0.4",
53
- "reserve": "^1.15.2"
55
+ "reserve": "^1.15.3"
54
56
  },
55
57
  "devDependencies": {
56
- "jest": "^29.5.0",
57
- "nock": "^13.3.1",
58
+ "jest": "^29.7.0",
59
+ "nock": "^13.3.3",
58
60
  "nyc": "^15.1.0",
59
61
  "standard": "^17.1.0"
60
62
  },
61
63
  "optionalDependencies": {
62
- "fsevents": "^2.3.2"
64
+ "fsevents": "^2.3.3"
63
65
  },
64
66
  "standard": {
65
67
  "env": [
package/src/coverage.js CHANGED
@@ -1,18 +1,28 @@
1
1
  'use strict'
2
2
 
3
- const { join } = require('path')
3
+ const { join, dirname } = require('path')
4
4
  const { fork } = require('child_process')
5
- const { cleanDir, createDir, filename } = require('./tools')
6
- const { readdir, readFile, stat, writeFile } = require('fs').promises
5
+ const { cleanDir, createDir, filename, download } = require('./tools')
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
+ const { promisify } = require('util')
10
11
 
11
12
  const $nycSettingsPath = Symbol('nycSettingsPath')
12
13
  const $coverageFileIndex = Symbol('coverageFileIndex')
14
+ const $coverageRemote = Symbol('coverageRemote')
13
15
 
16
+ let nycInstallationPath
14
17
  let nycScript
15
18
 
19
+ async function setupNyc (job) {
20
+ if (!nycInstallationPath) {
21
+ nycInstallationPath = resolvePackage(job, 'nyc')
22
+ }
23
+ nycScript = join(await nycInstallationPath, 'bin/nyc.js')
24
+ }
25
+
16
26
  async function nyc (job, ...args) {
17
27
  const output = getOutput(job)
18
28
  output.nyc(...args)
@@ -43,10 +53,7 @@ const customFileSystem = {
43
53
  }
44
54
 
45
55
  async function instrument (job) {
46
- if (!nycScript) {
47
- const nyc = await resolvePackage(job, 'nyc')
48
- nycScript = join(nyc, 'bin/nyc.js')
49
- }
56
+ await setupNyc(job)
50
57
  job[$nycSettingsPath] = join(job.coverageTempDir, 'settings/nyc.json')
51
58
  await cleanDir(job.coverageTempDir)
52
59
  await createDir(join(job.coverageTempDir, 'settings'))
@@ -71,6 +78,7 @@ async function instrument (job) {
71
78
  })
72
79
  if (!useLocal) {
73
80
  getOutput(job).instrumentationSkipped()
81
+ job[$coverageRemote] = true
74
82
  return
75
83
  }
76
84
  }
@@ -81,9 +89,28 @@ async function instrument (job) {
81
89
  async function generateCoverageReport (job) {
82
90
  job.status = 'Generating coverage report'
83
91
  await cleanDir(job.coverageReportDir)
84
- await nyc(job, 'merge', job.coverageTempDir, join(job.coverageTempDir, 'coverage.json'))
92
+ const coverageMergedDir = join(job.coverageTempDir, 'merged')
93
+ await createDir(coverageMergedDir)
94
+ const coverageFilename = join(coverageMergedDir, 'coverage.json')
95
+ await nyc(job, 'merge', job.coverageTempDir, coverageFilename)
96
+ if (job[$coverageRemote] && !job.coverageProxy) {
97
+ job.status = 'Collecting remote source files'
98
+ // Assuming all files are coming from the same server
99
+ const { origin } = new URL(job.testPageUrls[0])
100
+ const sourcesBasePath = join(job.coverageTempDir, 'sources')
101
+ const coverageData = require(coverageFilename)
102
+ const filenames = Object.keys(coverageData)
103
+ for (const filename of filenames) {
104
+ const fileData = coverageData[filename]
105
+ const { path } = fileData
106
+ const filePath = join(sourcesBasePath, path)
107
+ fileData.path = filePath
108
+ await download(origin + path, filePath)
109
+ }
110
+ await writeFile(coverageFilename, JSON.stringify(coverageData))
111
+ }
85
112
  const reporters = job.coverageReporters.map(reporter => `--reporter=${reporter}`)
86
- await nyc(job, 'report', ...reporters, '--temp-dir', job.coverageTempDir, '--report-dir', job.coverageReportDir, '--nycrc-path', job[$nycSettingsPath])
113
+ await nyc(job, 'report', ...reporters, '--temp-dir', coverageMergedDir, '--report-dir', job.coverageReportDir, '--nycrc-path', job[$nycSettingsPath])
87
114
  }
88
115
 
89
116
  module.exports = {
@@ -97,12 +124,67 @@ module.exports = {
97
124
  await writeFile(coverageFileName, JSON.stringify(coverageData))
98
125
  },
99
126
  generateCoverageReport: job => job.coverage && generateCoverageReport(job),
100
- mappings: job => job.coverage
101
- ? [{
102
- match: /^\/(.*\.js)$/,
103
- file: join(job.coverageTempDir, 'instrumented', '$1'),
104
- 'ignore-if-not-found': true,
127
+ mappings: async job => {
128
+ if (!job.coverage) {
129
+ return []
130
+ }
131
+ const instrumentedBasePath = join(job.coverageTempDir, 'instrumented')
132
+ const instrumentedMapping = {
133
+ match: /^\/(.*\.js)$/,
134
+ file: join(instrumentedBasePath, '$1'),
135
+ 'ignore-if-not-found': true
136
+ }
137
+ if (job.mode === 'legacy') {
138
+ return [{
139
+ ...instrumentedMapping,
105
140
  'custom-file-system': job.debugCoverageNoCustomFs ? undefined : customFileSystem
106
141
  }]
107
- : []
142
+ }
143
+ if (job.mode === 'url' && job.coverageProxy) {
144
+ await setupNyc(job)
145
+ const { origin } = new URL(job.url[0])
146
+ const sourcesBasePath = join(job.coverageTempDir, 'sources')
147
+ const { createInstrumenter } = require(join(await nycInstallationPath, 'node_modules/istanbul-lib-instrument'))
148
+ const instrumenter = createInstrumenter({
149
+ produceSourceMap: true,
150
+ coverageGlobalScope: 'window.top',
151
+ coverageGlobalScopeFunc: false
152
+ })
153
+ const instrument = promisify(instrumenter.instrument.bind(instrumenter))
154
+ const sources = {}
155
+ return [{
156
+ match: /(.*\.js)(\?.*)?$/,
157
+ custom: async (request, response, url) => {
158
+ if (!url.match(job.coverageProxyInclude) || url.match(job.coverageProxyExclude)) {
159
+ return // Ignore
160
+ }
161
+ const sourcePath = join(sourcesBasePath, url)
162
+ try {
163
+ await access(sourcePath, constants.R_OK)
164
+ } catch (e) {
165
+ try {
166
+ if (sources[url]) {
167
+ await sources[url]
168
+ } else {
169
+ sources[url] = await download(origin + url, sourcePath)
170
+ }
171
+ } catch (statusCode) {
172
+ return statusCode
173
+ }
174
+ }
175
+ const source = (await readFile(sourcePath)).toString()
176
+ const instrumentedSource = await instrument(source, sourcePath)
177
+ const instrumentedSourcePath = join(instrumentedBasePath, url)
178
+ await createDir(dirname(instrumentedSourcePath))
179
+ await writeFile(instrumentedSourcePath, instrumentedSource)
180
+ }
181
+ },
182
+ instrumentedMapping,
183
+ {
184
+ match: /(.*)$/,
185
+ url: `${origin}$1`
186
+ }]
187
+ }
188
+ return []
189
+ }
108
190
  }
@@ -40,7 +40,7 @@ describe('src/coverage', () => {
40
40
  })
41
41
 
42
42
  it('does not create a mapping', async () => {
43
- const coverageMappings = mappings(job)
43
+ const coverageMappings = await mappings(job)
44
44
  expect(coverageMappings.length).toStrictEqual(0)
45
45
  })
46
46
  })
@@ -73,7 +73,7 @@ describe('src/coverage', () => {
73
73
  })
74
74
 
75
75
  it('creates a mapping', async () => {
76
- const coverageMappings = mappings(job)
76
+ const coverageMappings = await mappings(job)
77
77
  expect(coverageMappings.length).toStrictEqual(1)
78
78
  })
79
79
 
@@ -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)
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { InvalidArgumentError } = require('commander')
4
4
  const { join } = require('path')
5
+ const { exec } = require('child_process')
5
6
 
6
7
  let browser
7
8
  let context
@@ -29,13 +30,33 @@ require('./browser')({
29
30
  ['-u, --unsecure', 'Disable security features', false],
30
31
  ['-v, --video', 'Record video', false],
31
32
  ['-n, --har', 'Record network activity with har file', false]
32
- ],
33
- capabilities: {
33
+ ]
34
+ },
35
+
36
+ async capabilities ({ settings, options }) {
37
+ const capabilities = {
34
38
  modules: ['playwright'],
35
39
  screenshot: '.png',
36
40
  scripts: true,
37
41
  traces: ['console', 'network']
38
42
  }
43
+ if (!settings.modules) {
44
+ return {
45
+ ...capabilities,
46
+ 'probe-with-modules': true
47
+ }
48
+ }
49
+ return await new Promise((resolve, reject) => {
50
+ exec('npx playwright install', (err, stdout, stderr) => {
51
+ console.log(stdout)
52
+ console.error(stderr)
53
+ if (err) {
54
+ reject(new Error('Unable to finalize playwright installation'))
55
+ } else {
56
+ resolve(capabilities)
57
+ }
58
+ })
59
+ })
39
60
  },
40
61
 
41
62
  async screenshot ({ filename }) {
@@ -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
  }
@@ -15,6 +15,7 @@
15
15
  <table style="visibility: {{ testPageUrls.length > 0 ? 'visible' : 'hidden' }};">
16
16
  <tr>
17
17
  <th>&nbsp;</th>
18
+ <th class="count">Type</th>
18
19
  <th class="status" style="width: 10rem;"><div><span>Status</span></div></th>
19
20
  <th class="count">Tests</th>
20
21
  <th class="count">Passed</th>
@@ -26,6 +27,11 @@
26
27
  <span {{if}}="qunitPages === undefined || !qunitPages[url]">{{ url }}</span>
27
28
  <a {{else}} href="#{{ qunitPages[url].id }}">{{ url }}</a>
28
29
  </td>
30
+ <td>
31
+ <span {{if}}="!qunitPages[url]">-</span>
32
+ <span {{elseif}}="qunitPages[url].isOpa" title="OPA test">&#129404;</span>
33
+ <span {{else}} title="Unit test">&#129514;</span>
34
+ </td>
29
35
  <td>
30
36
  <span {{if}}="!qunitPages[url]">-</span>
31
37
  <span {{elseif}}="qunitPages[url].failed">&#10060;</span>
@@ -0,0 +1,93 @@
1
+ 'use strict'
2
+
3
+ const { join, isAbsolute } = require('path')
4
+ const [, , reportDir] = process.argv
5
+ const { pad } = require('../tools')
6
+
7
+ const p = pad(process.stdout.columns || 80)
8
+ const log = console.log.bind(console)
9
+
10
+ function collectErrors (page) {
11
+ const errors = []
12
+ page.modules.forEach(module => {
13
+ module.tests.forEach(test => {
14
+ if (test.report.failed) {
15
+ errors.push({ module: module.name, ...test })
16
+ }
17
+ })
18
+ })
19
+ return errors
20
+ }
21
+
22
+ async function main () {
23
+ let jobPath
24
+ if (isAbsolute(reportDir)) {
25
+ jobPath = join(reportDir, 'job.js')
26
+ } else {
27
+ jobPath = join(process.cwd(), reportDir, 'job.js')
28
+ }
29
+ const job = require(jobPath)
30
+ const failedUrls = []
31
+ log(p`┌─${pad.x('─')}───────────────────┐`)
32
+ function render (url) {
33
+ const page = job.qunitPages && job.qunitPages[url]
34
+ if (!page || !page.report) {
35
+ log(p`│${pad.lt(url)} 🧨 │`)
36
+ failedUrls.push(url)
37
+ } else {
38
+ const type = page.isOpa ? '🥼' : '🧪'
39
+ const status = page.report.failed > 0 ? '🐞' : ' '
40
+ if (page.report.failed > 0) {
41
+ failedUrls.push(url)
42
+ }
43
+ log(p`│${pad.lt(url)} ${type} ${status} ${page.passed.toString().padStart(3, ' ')}/${page.count.toString().padEnd(3, ' ')}│`)
44
+ }
45
+ }
46
+ job.testPageUrls.forEach(render)
47
+ Object.keys(job.qunitPages || []).forEach(url => {
48
+ if (!job.testPageUrls.includes(url)) {
49
+ render(url)
50
+ }
51
+ })
52
+ log(p`└─${pad.x('─')}────────────────────┘`)
53
+ failedUrls.forEach(url => {
54
+ log()
55
+ log(p`[${pad.lt(url)}]`)
56
+ const page = job.qunitPages && job.qunitPages[url]
57
+ if (!page) {
58
+ log(p`Unable to run the page (check the execution log)`)
59
+ } else {
60
+ const pageIndex = job.testPageUrls.indexOf(url)
61
+ const pageHash = job.testPageHashes[pageIndex]
62
+ if (pageHash) {
63
+ log(p`Page execution folder name : ${pageHash}`)
64
+ }
65
+ let errors = collectErrors(page)
66
+ const { length } = errors
67
+ if (page.isOpa) {
68
+ // Focus on the first error only
69
+ errors = errors.slice(0, 1)
70
+ }
71
+ errors.forEach((test, index) => {
72
+ if (index > 0) {
73
+ log()
74
+ }
75
+ log(`${test.module} ▶ ${test.name}`)
76
+ test.logs.filter(({ result }) => !result).forEach(({ message }) => log(message))
77
+ })
78
+ if (page.isOpa && length > 1) {
79
+ log()
80
+ log(p`(${length} errors occurred but it is recommended to focus on the first OPA error)`)
81
+ }
82
+ }
83
+ })
84
+ }
85
+
86
+ main()
87
+ .catch(reason => {
88
+ console.error(reason)
89
+ return -1
90
+ })
91
+ .then((code = 0) => {
92
+ process.exit(code)
93
+ })
@@ -0,0 +1,27 @@
1
+ (function () {
2
+ 'use strict'
3
+
4
+ // inspired from ui5/resources/sap/ui/qunit/qunit-coverage-istanbul-dbg.js
5
+
6
+ function appendUrlParameter (url) {
7
+ const urlObject = new URL(url, document.baseURI)
8
+ urlObject.searchParams.set('instrument', 'true')
9
+ return urlObject.toString()
10
+ }
11
+
12
+ const nativeSetAttribute = HTMLScriptElement.prototype.setAttribute
13
+ HTMLScriptElement.prototype.setAttribute = function (name, value) {
14
+ if (name === 'data-sap-ui-module') {
15
+ this.src = appendUrlParameter(this.src)
16
+ }
17
+ nativeSetAttribute.apply(this, arguments)
18
+ }
19
+
20
+ const nativeXhrOpen = XMLHttpRequest.prototype.open
21
+ XMLHttpRequest.prototype.open = function (method, url) {
22
+ if (window.sap && window.sap.ui && window.sap.ui.loader && url && url.endsWith('.js')) {
23
+ arguments[1] = appendUrlParameter(url)
24
+ }
25
+ nativeXhrOpen.apply(this, arguments)
26
+ }
27
+ }())
package/src/job-mode.js CHANGED
@@ -34,9 +34,22 @@ function buildAndCheckMode (job) {
34
34
  return 'capabilities'
35
35
  }
36
36
  if (job.url && job.url.length) {
37
- check(job, undefined, ['testsuite'])
37
+ check(job, undefined, [
38
+ 'ui5',
39
+ 'libs',
40
+ 'mappings',
41
+ 'cache',
42
+ 'webapp',
43
+ 'watch',
44
+ 'testsuite'
45
+ ])
38
46
  return 'url'
39
47
  }
48
+ check(job, undefined, [
49
+ 'coverageProxy',
50
+ 'coverageProxyInclude',
51
+ 'coverageProxyExclude'
52
+ ])
40
53
  return 'legacy'
41
54
  }
42
55
 
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 } = require('./options')
10
+ const { boolean, integer, timeout, url, arrayOf, regex } = require('./options')
11
11
 
12
12
  const $status = Symbol('status')
13
13
 
@@ -76,7 +76,8 @@ function getCommand (cwd) {
76
76
  const command = new Command()
77
77
  command.exitOverride()
78
78
 
79
- const DEBUG_OPTION = '(For debugging purpose)'
79
+ const DEBUG_OPTION = '(🐞 for debugging purpose)'
80
+ const EXPERIMENTAL_OPTION = '[⚠️ experimental]'
80
81
 
81
82
  command
82
83
  .name(name)
@@ -105,7 +106,7 @@ function getCommand (cwd) {
105
106
  .option('-bt, --browser-close-timeout <timeout>', '[💻🔗🧪] Maximum waiting time for browser close', timeout, 2000)
106
107
  .option('-br, --browser-retry <count>', '[💻🔗🧪] Browser instantiation retries : if the command fails unexpectedly, it is re-executed (0 means no retry)', 1)
107
108
 
108
- // Common to legacy and testing
109
+ // Common to legacy and url
109
110
  .option('-pf, --page-filter <regexp>', '[💻🔗] Filter out pages not matching the regexp')
110
111
  .option('-pp, --page-params <params>', '[💻🔗] Add parameters to page URL')
111
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)
@@ -115,21 +116,28 @@ function getCommand (cwd) {
115
116
  .option('-rg, --report-generator <path...>', '[💻🔗] Report generator paths (relative to cwd or use $/ for provided ones)', ['$/report.js'])
116
117
  .option('-pp, --progress-page <path>', '[💻🔗] progress page path (relative to cwd or use $/ for provided ones)', '$/report/default.html')
117
118
 
118
- // Specific to legacy
119
+ .option('--coverage [flag]', '[💻🔗] Enable or disable code coverage', boolean)
120
+ .option('--no-coverage', '[💻🔗] Disable code coverage')
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'])
125
+ .option('-s, --serve-only [flag]', '[💻🔗] Serve only', boolean, false)
126
+
127
+ // Specific to legacy (and might be used with url if pointing to local project)
119
128
  .option('--ui5 <url>', '[💻] UI5 url', url, 'https://ui5.sap.com')
129
+ .option('--no-ui5', '[💻] Disable UI5 mapping (also disable libs)')
120
130
  .option('--libs <lib...>', '[💻] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
121
131
  .option('--mappings <mapping...>', '[💻] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
122
132
  .option('--cache <path>', '[💻] Cache UI5 resources locally in the given folder (empty to disable)')
123
133
  .option('--webapp <path>', '[💻] Base folder of the web application (relative to cwd)', 'webapp')
124
134
  .option('--testsuite <path>', '[💻] Path of the testsuite file (relative to webapp)', 'test/testsuite.qunit.html')
125
- .option('-s, --serve-only [flag]', '[💻] Serve only', boolean, false)
126
135
  .option('-w, --watch [flag]', '[💻] Monitor the webapp folder and re-execute tests on change', boolean, false)
127
- .option('--coverage [flag]', '[💻] Enable or disable code coverage', boolean)
128
- .option('--no-coverage', '[💻] Disable code coverage')
129
- .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')
130
- .option('-ct, --coverage-temp-dir <path>', '[💻] Directory to output raw coverage information to (relative to cwd)', '.nyc_output')
131
- .option('-cr, --coverage-report-dir <path>', '[💻] Directory to store the coverage report files (relative to cwd)', 'coverage')
132
- .option('-cr, --coverage-reporters <reporter...>', '[💻] List of nyc reporters to use', ['lcov', 'cobertura'])
136
+
137
+ // Specific to coverage in url mode (experimental)
138
+ .option('-cp, --coverage-proxy [flag]', `[🔗] ${EXPERIMENTAL_OPTION} use internal proxy to instrument remote files`, boolean, false)
139
+ .option('-cpi, --coverage-proxy-include <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to instrument for coverage`, regex, regex('.*'))
140
+ .option('-cpe, --coverage-proxy-exclude <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to ignore for coverage`, regex, regex('/((test-)?resources|tests?)/.*'))
133
141
 
134
142
  .addOption(new Option('--debug-probe-only', DEBUG_OPTION, boolean).hideHelp())
135
143
  .addOption(new Option('--debug-keep-browser-open', DEBUG_OPTION, boolean).hideHelp())
package/src/job.spec.js CHANGED
@@ -72,9 +72,8 @@ describe('job', () => {
72
72
  expect(job.screenshot).toStrictEqual(false)
73
73
  })
74
74
 
75
- it('url disables webapp checking and coverage', () => {
75
+ it('url disables coverage by default', () => {
76
76
  const job = buildJob({
77
- webapp: 'not_a_folder',
78
77
  url: 'http://localhost:8080'
79
78
  })
80
79
  expect(job.url).toStrictEqual(['http://localhost:8080'])
@@ -83,7 +82,6 @@ describe('job', () => {
83
82
 
84
83
  it('url still allows coverage', () => {
85
84
  const job = buildJob({
86
- webapp: 'not_a_folder',
87
85
  url: 'http://localhost:8080',
88
86
  coverage: true
89
87
  })
@@ -255,6 +253,13 @@ describe('job', () => {
255
253
  })).toThrow()
256
254
  })
257
255
  })
256
+
257
+ it('url forbids the use of webapp', () => {
258
+ expect(() => buildJob({
259
+ webapp: 'not_a_folder',
260
+ url: 'http://localhost:8080'
261
+ })).toThrow(UTRError.MODE_INCOMPATIBLE_OPTION('webapp'))
262
+ })
258
263
  })
259
264
 
260
265
  describe('Using ui5-test-runner.json', () => {
package/src/options.js CHANGED
@@ -31,6 +31,14 @@ module.exports = {
31
31
  throw new InvalidArgumentError('Invalid boolean')
32
32
  },
33
33
 
34
+ regex (value) {
35
+ try {
36
+ return new RegExp(value)
37
+ } catch (e) {
38
+ throw new InvalidArgumentError('Invalid regex')
39
+ }
40
+ },
41
+
34
42
  integer,
35
43
 
36
44
  timeout (value) {
package/src/output.js CHANGED
@@ -488,44 +488,6 @@ function build (job) {
488
488
  warn(job, p80()`Some requests are not handled properly, check the unhandled.txt report for more info`)
489
489
  }),
490
490
 
491
- results: wrap(() => {
492
- const p = p80()
493
- log(job, p`┌──────────${pad.x('─')}┐`)
494
- log(job, p`│ RESULTS ${pad.x(' ')} │`)
495
- log(job, p`├─────┬─${pad.x('─')}──┤`)
496
- const messages = []
497
- function result (url) {
498
- const page = job.qunitPages && job.qunitPages[url]
499
- let message
500
- if (!page || !page.report) {
501
- message = 'Unable to run the page'
502
- } else if (page.report.failed > 1) {
503
- message = `${page.report.failed} tests failed`
504
- } else if (page.report.failed === 1) {
505
- message = '1 test failed'
506
- }
507
- if (message) {
508
- log(job, p`│ ${(messages.length + 1).toString().padStart(3, ' ')} │ ${pad.lt(url)} │`)
509
- messages.push(message)
510
- } else {
511
- log(job, p`│ OK │ ${pad.lt(url)} │`)
512
- }
513
- }
514
- job.testPageUrls.forEach(result)
515
- Object.keys(job.qunitPages || []).forEach(url => {
516
- if (!job.testPageUrls.includes(url)) {
517
- result(url)
518
- }
519
- })
520
- log(job, p`└─────┴───${pad.x('─')}┘`)
521
- messages.forEach((message, index) => {
522
- log(job, p`${(index + 1).toString().padStart(3, ' ')}: ${message}`)
523
- })
524
- if (!messages.length) {
525
- log(job, p`Success !`)
526
- }
527
- }),
528
-
529
491
  reportGeneratorFailed: wrap((generator, exitCode, buffers) => {
530
492
  const p = p80()
531
493
  log(job, p`┌──────────${pad.x('─')}┐`)
package/src/report.js CHANGED
@@ -20,6 +20,14 @@ async function save (job) {
20
20
  await serialize(job, 'job', job)
21
21
  }
22
22
 
23
+ function generateTextReport (job) {
24
+ const { promise, resolve } = allocPromise()
25
+ const childProcess = fork(join(__dirname, 'defaults/text-report.js'), [job.reportDir], { stdio: 'pipe' })
26
+ getOutput(job).monitor(childProcess, true)
27
+ childProcess.on('close', resolve)
28
+ return promise
29
+ }
30
+
23
31
  module.exports = {
24
32
  save,
25
33
 
@@ -28,9 +36,9 @@ module.exports = {
28
36
  job.end = new Date()
29
37
  job.failed = !!job.failed
30
38
  job.status = 'Generating reports'
31
- output.results()
32
39
  job.testPageHashes = job.testPageUrls.map(url => filename(url))
33
40
  await save(job)
41
+ await generateTextReport(job)
34
42
  const promises = job.reportGenerator.map(generator => {
35
43
  const { promise, resolve } = allocPromise()
36
44
  const childProcess = fork(generator, [job.reportDir], { stdio: 'pipe' })
package/src/reserve.js CHANGED
@@ -6,14 +6,14 @@ const ui5 = require('./ui5')
6
6
  const { check } = require('reserve')
7
7
  const unhandled = require('./unhandled')
8
8
 
9
- module.exports = job => check({
9
+ module.exports = async job => check({
10
10
  port: job.port,
11
11
  mappings: [
12
12
  cors,
13
13
  ...job.mappings ?? [],
14
14
  ...job.serveOnly ? [] : endpoints(job),
15
15
  ...ui5(job),
16
- ...coverage(job), {
16
+ ...await coverage(job), {
17
17
  // Project mapping
18
18
  match: /^\/(.*)/,
19
19
  file: join(job.webapp, '$1'),
package/src/tests.js CHANGED
@@ -73,11 +73,24 @@ async function runTestPage (job, url) {
73
73
  scripts = [
74
74
  'post.js',
75
75
  'qunit-intercept.js',
76
- 'qunit-hooks.js',
77
- 'opa-iframe-coverage.js'
76
+ 'qunit-hooks.js'
78
77
  ]
78
+ if (job.coverage && !job.coverageProxy) {
79
+ scripts.push(
80
+ 'opa-iframe-coverage.js',
81
+ 'ui5-coverage.js' // TODO detect if middleware exists before injecting this
82
+ )
83
+ }
84
+ }
85
+ if (job.coverageProxy) {
86
+ const { origin } = new URL(url)
87
+ const proxifiedUrl = url.replace(origin, `http://localhost:${job.port}`)
88
+ await start(job, proxifiedUrl, scripts)
89
+ job.qunitPages[url] = job.qunitPages[proxifiedUrl]
90
+ delete job.qunitPages[proxifiedUrl]
91
+ } else {
92
+ await start(job, url, scripts)
79
93
  }
80
- await start(job, url, scripts)
81
94
  } catch (error) {
82
95
  output.startFailed(url, error)
83
96
  throw error
package/src/tools.js CHANGED
@@ -1,7 +1,12 @@
1
1
  'use strict'
2
2
 
3
+ const { dirname } = require('path')
3
4
  const { mkdir, rm, stat } = require('fs').promises
4
5
  const { createHash } = require('crypto')
6
+ const { createWriteStream } = require('fs')
7
+ const http = require('http')
8
+ const https = require('https')
9
+ const { unlink } = require('fs/promises')
5
10
 
6
11
  const recursive = { recursive: true }
7
12
 
@@ -110,6 +115,44 @@ pad.x = (text) => ({ [$op]: $x, text })
110
115
  pad.lt = (text, padding = ' ') => ({ [$op]: $lt, text, padding })
111
116
  pad.w = (text) => ({ [$op]: $w, text })
112
117
 
118
+ function allocPromise () {
119
+ let resolve
120
+ let reject
121
+ const promise = new Promise((_resolve, _reject) => {
122
+ resolve = _resolve
123
+ reject = _reject
124
+ })
125
+ return { promise, resolve, reject }
126
+ }
127
+
128
+ async function download (url, filename) {
129
+ const { hostname, port, origin } = new URL(url)
130
+ const options = {
131
+ hostname,
132
+ port,
133
+ path: url.substring(origin.length),
134
+ method: 'GET'
135
+ }
136
+ const protocol = url.startsWith('https:') ? https : http
137
+ await mkdir(dirname(filename), recursive)
138
+ const output = createWriteStream(filename)
139
+ const { promise, resolve, reject } = allocPromise()
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
+ }
147
+ response.on('error', reject)
148
+ response.on('end', resolve)
149
+ response.pipe(output)
150
+ })
151
+ request.on('error', reject)
152
+ request.end()
153
+ return promise
154
+ }
155
+
113
156
  module.exports = {
114
157
  stripUrlHash,
115
158
  filename,
@@ -117,15 +160,8 @@ module.exports = {
117
160
  createDir: dir => mkdir(dir, recursive),
118
161
  recreateDir: dir => cleanDir(dir).then(() => mkdir(dir, recursive)),
119
162
  extractPageUrl: headers => headers['x-page-url'],
120
- allocPromise () {
121
- let resolve
122
- let reject
123
- const promise = new Promise((_resolve, _reject) => {
124
- resolve = _resolve
125
- reject = _reject
126
- })
127
- return { promise, resolve, reject }
128
- },
163
+ allocPromise,
129
164
  noop () {},
130
- pad
165
+ pad,
166
+ download
131
167
  }
package/src/ui5.js CHANGED
@@ -7,6 +7,10 @@ const { capture } = require('reserve')
7
7
  const { getOutput } = require('./output')
8
8
 
9
9
  module.exports = job => {
10
+ if (job.noUi5) {
11
+ return []
12
+ }
13
+
10
14
  const [, hostName] = /https?:\/\/([^/]*)/.exec(job.ui5)
11
15
  const [, version] = /(\d+\.\d+\.\d+)?$/.exec(job.ui5)
12
16
  const cacheBase = join(job.cache || '', hostName.replace(':', '_'), version || '')
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