ui5-test-runner 4.5.1 → 5.1.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
@@ -34,18 +34,15 @@ A self-sufficient test runner for UI5 applications enabling parallel execution o
34
34
 
35
35
  ## ⚠️ Breaking changes
36
36
 
37
- ### v4
38
- * Dropping support of Node.js 16
39
-
40
- ### v3
41
- * Dropping support of Node.js 14
42
-
43
- ### v2
44
-
45
- * Command line **parameters** as well as configuration file **syntax** have changed
46
- * Dependencies are installed **on demand**
47
- * Browser instantiation command evolved in an **incompatible way** (see [documentation](https://arnaudbuchholz.github.io/ui5-test-runner/browser.html)).
48
- * Output is different (report, traces)
37
+ | Version | Reason |
38
+ |-|-|
39
+ | **5**.0.0 | • Some coverage reports now includes **all** files, leading to a potential decrease of coverage |
40
+ | **4**.0.0 | • Drop support of Node.js 16 |
41
+ | **3**.0.0 | • Drop support of Node.js 14 |
42
+ | **2**.0.0 | • Command line **parameters** as well as configuration file **syntax** have changed |
43
+ || • Dependencies are installed **on demand** |
44
+ || • Browser instantiation command evolved in an **incompatible way** (see [documentation](https://arnaudbuchholz.github.io/ui5-test-runner/browser.html)) |
45
+ || Output is different (report, traces) |
49
46
 
50
47
  ## ✒ Contributors
51
48
 
@@ -0,0 +1,27 @@
1
+ {
2
+ "testTimeout": 15000,
3
+ "setupFilesAfterEnv": [
4
+ "./test/setup.js"
5
+ ],
6
+ "testPathIgnorePatterns": [
7
+ "/node_modules/",
8
+ "/capabilities/"
9
+ ],
10
+ "collectCoverage": true,
11
+ "collectCoverageFrom": [
12
+ "src/*.js"
13
+ ],
14
+ "coveragePathIgnorePatterns": [
15
+ "\\.spec\\.js",
16
+ "output\\.js",
17
+ "b\\capabilities\\b"
18
+ ],
19
+ "coverageThreshold": {
20
+ "global": {
21
+ "branches": 80,
22
+ "functions": 80,
23
+ "lines": 80,
24
+ "statements": 80
25
+ }
26
+ }
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "4.5.1",
3
+ "version": "5.1.0",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "test:report": "node ./src/defaults/report.js ./test/report && reserve --config ./test/report/reserve.json",
22
22
  "test:text-report": "node ./src/defaults/text-report.js ./test/report",
23
23
  "build:doc": "node build/doc",
24
- "clean": "npm uninstall -g ui5-test-runner puppeteer nyc selenium-webdriver playwright webdriverio"
24
+ "clean": "npm uninstall -g ui5-test-runner puppeteer nyc selenium-webdriver playwright webdriverio jsdom"
25
25
  },
26
26
  "repository": {
27
27
  "type": "git",
@@ -43,25 +43,24 @@
43
43
  },
44
44
  "homepage": "https://github.com/ArnaudBuchholz/ui5-test-runner#readme",
45
45
  "dependencies": {
46
- "commander": "^12.0.0",
47
- "mime": "^3.0.0",
46
+ "commander": "^12.1.0",
48
47
  "punybind": "^1.2.1",
49
48
  "punyexpr": "^1.0.4",
50
- "reserve": "^1.15.9"
49
+ "reserve": "2.0.1"
51
50
  },
52
51
  "devDependencies": {
53
- "@openui5/types": "^1.123.0",
54
- "@ui5/cli": "^3.9.2",
52
+ "@openui5/types": "^1.124.0",
53
+ "@ui5/cli": "^3.10.3",
55
54
  "@ui5/middleware-code-coverage": "^1.1.1",
56
55
  "dotenv": "^16.4.5",
57
56
  "jest": "^29.7.0",
58
57
  "nock": "^13.5.4",
59
58
  "nyc": "^15.1.0",
60
- "rimraf": "^5.0.5",
59
+ "rimraf": "^5.0.7",
61
60
  "standard": "^17.1.0",
62
61
  "start-server-and-test": "^2.0.3",
63
62
  "typescript": "^5.4.5",
64
- "ui5-tooling-transpile": "^3.3.7"
63
+ "ui5-tooling-transpile": "^3.4.2"
65
64
  },
66
65
  "optionalDependencies": {
67
66
  "fsevents": "^2.3.3"
@@ -77,32 +76,5 @@
77
76
  "sap",
78
77
  "opaTest"
79
78
  ]
80
- },
81
- "jest": {
82
- "testTimeout": 15000,
83
- "setupFilesAfterEnv": [
84
- "./test/setup.js"
85
- ],
86
- "testPathIgnorePatterns": [
87
- "/node_modules/",
88
- "/capabilities/"
89
- ],
90
- "collectCoverage": true,
91
- "collectCoverageFrom": [
92
- "src/*.js"
93
- ],
94
- "coveragePathIgnorePatterns": [
95
- "\\.spec\\.js",
96
- "output\\.js",
97
- "b\\capabilities\\b"
98
- ],
99
- "coverageThreshold": {
100
- "global": {
101
- "branches": 80,
102
- "functions": 80,
103
- "lines": 80,
104
- "statements": 80
105
- }
106
- }
107
79
  }
108
80
  }
package/src/browsers.js CHANGED
@@ -116,8 +116,9 @@ async function start (job, url, scripts = []) {
116
116
  }
117
117
  }
118
118
  if (resolvedScripts.length) {
119
- resolvedScripts.unshift(`window['ui5-test-runner/base-host'] = 'http://localhost:${job.port}'
120
- `)
119
+ resolvedScripts.unshift(`(function () {
120
+ window['ui5-test-runner/base-host'] = 'http://localhost:${job.port}'
121
+ }())`)
121
122
  }
122
123
  const progress = newProgress(job, url)
123
124
  const pageBrowser = {
@@ -62,17 +62,23 @@ async function capabilities (job) {
62
62
  const listener = listeners[listenerIndex]
63
63
  await listener({
64
64
  endpoint,
65
- body: JSON.parse(await body(request))
65
+ body: await body(request).json()
66
66
  })
67
67
  response.writeHead(200)
68
68
  response.end()
69
69
  }
70
70
  }, {
71
71
  match: '^/inject/(.*)',
72
- file: join(__dirname, '../inject/$1')
72
+ cwd: join(__dirname, '../inject'),
73
+ file: '$1',
74
+ static: !job.debugDevMode
73
75
  }, {
74
76
  match: '^/(.*)',
75
- file: join(__dirname, '$1')
77
+ cwd: __dirname,
78
+ file: '$1',
79
+ static: !job.debugDevMode
80
+ }, {
81
+ status: 404
76
82
  }]
77
83
  })
78
84
  const server = serve(configuration)
package/src/coverage.js CHANGED
@@ -1,11 +1,11 @@
1
1
  'use strict'
2
2
 
3
- const { join, dirname, isAbsolute } = require('path')
3
+ const { join, dirname, isAbsolute, relative, sep } = require('path')
4
4
  const { fork } = require('child_process')
5
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
- const { getOutput } = require('./output')
8
+ const { getOutput, newProgress } = require('./output')
9
9
  const { resolvePackage } = require('./npm')
10
10
  const { promisify } = require('util')
11
11
  const { UTRError } = require('./error')
@@ -64,7 +64,7 @@ async function instrument (job) {
64
64
  await cleanDir(job.coverageTempDir)
65
65
  await createDir(join(job.coverageTempDir, 'settings'))
66
66
  const settings = JSON.parse((await readFile(job.coverageSettings)).toString())
67
- settings.cwd = job.cwd
67
+ settings.cwd = job.webapp
68
68
  if (!settings.exclude) {
69
69
  settings.exclude = []
70
70
  }
@@ -75,6 +75,7 @@ async function instrument (job) {
75
75
  settings.exclude.push(join(job.reportDir, '**'))
76
76
  settings.exclude.push(join(job.coverageReportDir, '**'))
77
77
  await writeFile(job[$nycSettingsPath], JSON.stringify(settings))
78
+ job.nycSettings = settings
78
79
  if (job.mode === 'url') {
79
80
  if (!job[$remoteOnLegacy]) {
80
81
  job[$coverageRemote] = true
@@ -86,6 +87,92 @@ async function instrument (job) {
86
87
  await nyc(job, 'instrument', job.webapp, join(job.coverageTempDir, 'instrumented'), '--nycrc-path', job[$nycSettingsPath])
87
88
  }
88
89
 
90
+ function getUrlOrigin (job) {
91
+ const { origin } = new URL(job.url[0])
92
+ if (job.url.some(url => new URL(url).origin !== origin)) {
93
+ getOutput(job).assumingOneOrigin()
94
+ }
95
+ return origin
96
+ }
97
+
98
+ async function buildAllIndex (job) {
99
+ async function scanFs (path, onFolder, onFile) {
100
+ const items = await readdir(path)
101
+ await onFolder(items.length)
102
+ for (const item of items) {
103
+ const itemPath = join(path, item)
104
+ const itemStat = await stat(itemPath)
105
+ if (itemStat.isDirectory()) {
106
+ await scanFs(itemPath, onFolder, onFile)
107
+ } else {
108
+ await onFile(itemPath, (await readFile(itemPath)).toString())
109
+ }
110
+ }
111
+ }
112
+
113
+ const output = getOutput(job)
114
+ output.debug('coverage', 'Build index for all files...')
115
+ const progress = newProgress(job, 'Build index for all files', 1, 0)
116
+
117
+ try {
118
+ const index = []
119
+ let scan
120
+ let start
121
+ if (job.mode === 'legacy' || job[$remoteOnLegacy]) {
122
+ scan = scanFs
123
+ start = join(job.coverageTempDir, 'instrumented')
124
+ } else {
125
+ scan = require(job.coverageRemoteScanner)
126
+ start = getUrlOrigin(job)
127
+ }
128
+
129
+ await scan(
130
+ start,
131
+ count => {
132
+ progress.total += count
133
+ ++progress.count
134
+ },
135
+ async (file, source) => {
136
+ if (file.endsWith('.js') || file.endsWith('.ts')) {
137
+ output.debug('coverage', file)
138
+ try {
139
+ const coverageData = source
140
+ .match(/coverageData\s*=\s*({[^;]*});/)[1]
141
+ .replace(/([^"])(\w+):/g, (_, before, name) => `${before}"${name}":`)
142
+ const [, coveragePath] = coverageData.match(/"path"\s*:\s*"([^"]+)"/)
143
+ const UNDEFINED = '__undefined__'
144
+ const validatedCoverageData = JSON.stringify(
145
+ JSON.parse(coverageData.replace(/\bundefined\b/g, `"${UNDEFINED}"`)),
146
+ (key, value) => {
147
+ if (value === UNDEFINED) {
148
+ return undefined
149
+ }
150
+ return value
151
+ }
152
+ )
153
+ index.push(`"${coveragePath}": ${validatedCoverageData}`)
154
+ } catch (e) {
155
+ output.debug('coverage', `Error when extracting all coverage for ${file}`, e)
156
+ }
157
+ } else {
158
+ output.debug('coverage', `Ignore all coverage for ${file}`)
159
+ }
160
+ ++progress.count
161
+ }
162
+ )
163
+ if (index.length === 0) {
164
+ output.noInfoForAllCoverage()
165
+ } else {
166
+ await writeFile(join(job.coverageTempDir, 'all-index.json'), `{${index.join(',')}}`)
167
+ }
168
+ } catch (e) {
169
+ output.genericError(e)
170
+ output.noInfoForAllCoverage()
171
+ } finally {
172
+ progress.done()
173
+ }
174
+ }
175
+
89
176
  async function getReadableSource (job, pathOrUrl) {
90
177
  if (isAbsolute(pathOrUrl)) {
91
178
  try {
@@ -99,16 +186,64 @@ async function getReadableSource (job, pathOrUrl) {
99
186
  return filePath
100
187
  } catch (e) {}
101
188
  try {
102
- // Assuming all files are coming from the same server
103
- const { origin } = new URL(job.testPageUrls[0])
104
- const filePath = join(job.coverageTempDir, 'sources', pathOrUrl)
189
+ const origin = getUrlOrigin(job)
190
+ if (!job.coverageSourceDir) {
191
+ job.coverageSourceDir = join(job.coverageTempDir, 'sources')
192
+ }
193
+ const filePath = join(job.coverageSourceDir, pathOrUrl)
105
194
  await download(origin + pathOrUrl, filePath)
106
195
  return filePath
107
196
  } catch (e) {}
108
197
  }
109
198
 
199
+ async function checkAllSourcesAreAvailable (job, coverageFilename) {
200
+ const output = getOutput(job)
201
+ job.status = 'Checking remote source files'
202
+ output.debug('coverage', 'Checking remote source files...')
203
+ const coverageData = require(coverageFilename)
204
+ const filenames = Object.keys(coverageData)
205
+ let changes = false
206
+ let basePath
207
+ for (const filename of filenames) {
208
+ const fileData = coverageData[filename]
209
+ const filePath = await getReadableSource(job, fileData.path)
210
+ if (!filePath) {
211
+ // TODO this will compromise coverage report generation
212
+ continue
213
+ }
214
+ if (filePath && filePath !== fileData.path) {
215
+ fileData.path = filePath
216
+ changes = true
217
+ }
218
+ if (filename !== filePath) {
219
+ delete coverageData[filename]
220
+ coverageData[filePath] = fileData
221
+ changes = true
222
+ }
223
+ const fileFolder = dirname(filePath)
224
+ if (basePath === undefined) {
225
+ basePath = fileFolder
226
+ } else {
227
+ const diff = relative(basePath, fileFolder).split(sep)
228
+ while (diff.shift() === '..') {
229
+ basePath = dirname(basePath)
230
+ }
231
+ }
232
+ }
233
+ if (basePath !== job.nycSettings.cwd) {
234
+ job.nycSettings.cwd = basePath
235
+ await writeFile(job[$nycSettingsPath], JSON.stringify(job.nycSettings))
236
+ }
237
+ if (changes) {
238
+ await writeFile(coverageFilename, JSON.stringify(coverageData))
239
+ }
240
+ }
241
+
110
242
  async function generateCoverageReport (job) {
111
243
  job.status = 'Generating coverage report'
244
+ if (job.nycSettings.all) {
245
+ await buildAllIndex(job)
246
+ }
112
247
  const output = getOutput(job)
113
248
  output.debug('coverage', 'Generating coverage report...')
114
249
  await cleanDir(job.coverageReportDir)
@@ -116,23 +251,8 @@ async function generateCoverageReport (job) {
116
251
  await createDir(coverageMergedDir)
117
252
  const coverageFilename = join(coverageMergedDir, 'coverage.json')
118
253
  await nyc(job, 'merge', job.coverageTempDir, coverageFilename)
119
- if (job[$coverageRemote] && !job.coverageProxy) {
120
- job.status = 'Checking remote source files'
121
- output.debug('coverage', 'Checking remote source files...')
122
- const coverageData = require(coverageFilename)
123
- const filenames = Object.keys(coverageData)
124
- let changes = 0
125
- for (const filename of filenames) {
126
- const fileData = coverageData[filename]
127
- const filePath = await getReadableSource(job, fileData.path)
128
- if (filePath && filePath !== fileData.path) {
129
- fileData.path = filePath
130
- ++changes
131
- }
132
- }
133
- if (changes > 0) {
134
- await writeFile(coverageFilename, JSON.stringify(coverageData))
135
- }
254
+ if (job[$coverageRemote]) {
255
+ await checkAllSourcesAreAvailable(job, coverageFilename)
136
256
  }
137
257
  const reporters = job.coverageReporters.map(reporter => `--reporter=${reporter}`)
138
258
  if (!job.coverageReporters.includes('text')) {
@@ -177,8 +297,8 @@ module.exports = {
177
297
  const instrumentedBasePath = join(job.coverageTempDir, 'instrumented')
178
298
  const instrumentedMapping = {
179
299
  match: /(.*\.js)(\?.*)?$/,
180
- file: join(instrumentedBasePath, '$1'),
181
- 'ignore-if-not-found': true
300
+ cwd: instrumentedBasePath,
301
+ file: '$1'
182
302
  }
183
303
  if (job.mode === 'legacy' || job[$remoteOnLegacy]) {
184
304
  return [{
@@ -201,7 +321,7 @@ module.exports = {
201
321
  return [{
202
322
  match: /(.*\.js)(\?.*)?$/,
203
323
  custom: async (request, response, url) => {
204
- if (!url.match(job.coverageProxyInclude) || url.match(/\bresources\b/) || url.match(job.coverageProxyExclude)) {
324
+ if (!url.match(job.coverageProxyInclude) || url.match(job.coverageProxyExclude)) {
205
325
  getOutput(job).debug('coverage', 'coverage_proxy ignore', url)
206
326
  return
207
327
  }
@@ -210,21 +330,20 @@ module.exports = {
210
330
  await access(instrumentedSourcePath, constants.R_OK)
211
331
  return
212
332
  } catch (e) {}
213
- const instrumenting = sources[url]
214
- if (instrumenting) {
215
- await instrumenting
216
- return // ok
333
+ if (!sources[url]) {
334
+ sources[url] = (async () => {
335
+ const sourcePath = await getReadableSource(job, url)
336
+ getOutput(job).debug('coverage', 'coverage_proxy instrument', url, sourcePath)
337
+ if (sourcePath) {
338
+ const source = (await readFile(sourcePath)).toString()
339
+ const instrumentedSource = await instrument(source, sourcePath)
340
+ await createDir(dirname(instrumentedSourcePath))
341
+ await writeFile(instrumentedSourcePath, instrumentedSource)
342
+ delete sources[url]
343
+ }
344
+ })()
217
345
  }
218
- sources[url] = (async () => {
219
- const sourcePath = await getReadableSource(job, url)
220
- getOutput(job).debug('coverage', 'coverage_proxy instrument', url, sourcePath)
221
- if (sourcePath) {
222
- const source = (await readFile(sourcePath)).toString()
223
- const instrumentedSource = await instrument(source, sourcePath)
224
- await createDir(dirname(instrumentedSourcePath))
225
- await writeFile(instrumentedSourcePath, instrumentedSource)
226
- }
227
- })()
346
+ await sources[url]
228
347
  }
229
348
  },
230
349
  instrumentedMapping,
@@ -8,32 +8,64 @@ function fakeMatchMedia () {
8
8
  }
9
9
  }
10
10
 
11
- function wrapXHR (window, networkWriter) {
12
- const { XMLHttpRequest } = window
13
- const { open } = XMLHttpRequest.prototype
11
+ function wrapXHR ({ XMLHttpRequest }) {
12
+ const { open, send } = XMLHttpRequest.prototype
13
+ const $async = Symbol('async')
14
14
  XMLHttpRequest.prototype.open = function (...args) {
15
- const [method, url] = args
15
+ const [method, url, async] = args
16
16
  const log = () => {
17
17
  const { status } = this
18
- networkWriter.append({
18
+ console.log(JSON.stringify({
19
+ timestamp: new Date().toISOString(),
20
+ channel: 'network',
21
+ initiator: 'xhr',
19
22
  method,
20
23
  url,
24
+ async,
21
25
  status
22
- })
26
+ }))
23
27
  }
24
28
  this.addEventListener('load', log)
25
29
  this.addEventListener('error', log)
30
+ if (async === false) {
31
+ this[$async] = { method, url }
32
+ }
26
33
  return open.call(this, ...args)
27
34
  }
35
+ XMLHttpRequest.prototype.send = function (...args) {
36
+ if (this[$async]) {
37
+ const { method, url } = this[$async]
38
+ console.log(JSON.stringify({
39
+ timestamp: new Date().toISOString(),
40
+ channel: 'debug',
41
+ message: '>> XMLHttpRequest.prototype.send',
42
+ method,
43
+ url,
44
+ async: false
45
+ }))
46
+ }
47
+ const result = send.call(this, ...args)
48
+ if (this[$async]) {
49
+ const { method, url } = this[$async]
50
+ console.log(JSON.stringify({
51
+ timestamp: new Date().toISOString(),
52
+ channel: 'debug',
53
+ message: '<< XMLHttpRequest.prototype.send',
54
+ method,
55
+ url,
56
+ async: false
57
+ }))
58
+ }
59
+ return result
60
+ }
28
61
  }
29
62
 
30
- function adjustXPathResult (window) {
63
+ function adjustXPathResult ({ Document }) {
31
64
  /* https://ui5.sap.com/resources/sap/ui/model/odata/AnnotationParser-dbg.js
32
65
  getXPath: function() {
33
66
  xmlNodes.length = xmlNodes.snapshotLength;
34
67
  */
35
- const { Document } = window
36
- const evaluate = Document.prototype.evaluate
68
+ const { evaluate } = Document.prototype
37
69
  Document.prototype.evaluate = function () {
38
70
  const result = evaluate.apply(this, arguments)
39
71
  let length = result.length
@@ -48,10 +80,10 @@ function adjustXPathResult (window) {
48
80
  }
49
81
  }
50
82
 
51
- function fixMatchesDontThrow (window) {
52
- // https://github.com/jsdom/jsdom/issues/3057
53
- // Fix _nwsapiDontThrow which throws :-(
54
- const { document } = window
83
+ function fixMatchesDontThrow ({ document }) {
84
+ /* https://github.com/jsdom/jsdom/issues/3057
85
+ Fix _nwsapiDontThrow which throws :-(
86
+ */
55
87
  const [impl] = Object.getOwnPropertySymbols(document)
56
88
  const documentImpl = document[impl]
57
89
  let _nwsapiDontThrow
@@ -74,10 +106,45 @@ function fixMatchesDontThrow (window) {
74
106
  })
75
107
  }
76
108
 
77
- module.exports = ({
78
- window,
79
- networkWriter
80
- }) => {
109
+ function fixCaseSensitiveSelectors ({ Document }) {
110
+ /* https://github.com/SAP/openui5/blob/f41ed5504db1dc576dae7e7d403aaa02b918fef5/src/sap.ui.core/src/ui5loader-autoconfig.js#L75
111
+ oResult = check(globalThis.document.querySelector('SCRIPT[src][id=sap-ui-bootstrap]'), rResources);
112
+ jsdom uses case sensitive implementation of querySelector
113
+ */
114
+ const uppercaseTag = /\bSCRIPT\b/g
115
+ const { querySelector, querySelectorAll } = Document.prototype
116
+ Object.assign(Document.prototype, {
117
+ querySelector (selectors) {
118
+ const result = querySelector.call(this, selectors) || { length: 0 }
119
+ if (result.length === 0 && selectors.match(uppercaseTag)) {
120
+ console.log(JSON.stringify({
121
+ timestamp: new Date().toISOString(),
122
+ channel: 'debug',
123
+ message: 'overriding selectors upon empty result of document.querySelector',
124
+ selectors
125
+ }))
126
+ return querySelector.call(this, selectors.replace(uppercaseTag, tag => tag.toLowerCase()))
127
+ }
128
+ return result
129
+ },
130
+
131
+ querySelectorAll (selectors) {
132
+ const result = querySelectorAll.call(this, selectors) || { length: 0 }
133
+ if (result.length === 0 && selectors.match(uppercaseTag)) {
134
+ console.log(JSON.stringify({
135
+ timestamp: new Date().toISOString(),
136
+ channel: 'debug',
137
+ message: 'overriding selectors upon empty result of document.querySelectorAll',
138
+ selectors
139
+ }))
140
+ return querySelectorAll.call(this, selectors.replace(uppercaseTag, tag => tag.toLowerCase()))
141
+ }
142
+ return result
143
+ }
144
+ })
145
+ }
146
+
147
+ module.exports = window => {
81
148
  window.addEventListener('error', event => {
82
149
  const { message, filename, lineno, colno } = event
83
150
  window.console.error(`${filename}@${lineno}:${colno}: ${message}`)
@@ -89,7 +156,8 @@ module.exports = ({
89
156
  }
90
157
  window.matchMedia = window.matchMedia || fakeMatchMedia
91
158
 
92
- wrapXHR(window, networkWriter)
159
+ wrapXHR(window)
93
160
  adjustXPathResult(window)
94
161
  fixMatchesDontThrow(window)
162
+ fixCaseSensitiveSelectors(window)
95
163
  }
@@ -1,8 +1,4 @@
1
- module.exports = ({
2
- jsdom,
3
- networkWriter,
4
- consoleWriter
5
- }) => {
1
+ module.exports = jsdom => {
6
2
  const { ResourceLoader: JSDOMResourceLoader } = jsdom
7
3
 
8
4
  const { readFile } = require('fs/promises')
@@ -15,19 +11,24 @@ module.exports = ({
15
11
  const { response } = request
16
12
  let status
17
13
  if (response === undefined) {
18
- consoleWriter.append({
14
+ console.log(JSON.stringify({
15
+ timestamp: new Date().toISOString(),
16
+ channel: 'console',
19
17
  type: 'error',
20
18
  message: 'NETWORK ERROR : ' + (reason ? reason.toString() : 'unknown reason')
21
- })
19
+ }))
22
20
  status = 599
23
21
  } else {
24
22
  status = response.statusCode
25
23
  }
26
- networkWriter.append({
24
+ console.log(JSON.stringify({
25
+ timestamp: new Date().toISOString(),
26
+ channel: 'network',
27
+ initiator: 'resource-loader',
27
28
  method: 'GET',
28
29
  url,
29
30
  status
30
- })
31
+ }))
31
32
  }
32
33
  request.then(log, log)
33
34
  if (url.match(/sap\/ui\/test\/matchers\/Visible(-dbg)?.js/)) {
@@ -11,24 +11,42 @@ require('./browser')({
11
11
  capabilities: {
12
12
  modules: ['jsdom'],
13
13
  scripts: true,
14
- traces: ['console', 'network']
14
+ traces: ['multiplex']
15
15
  }
16
16
  },
17
17
 
18
18
  async run ({
19
19
  settings: { url, scripts, modules },
20
- options,
21
- consoleWriter,
22
- networkWriter
20
+ options
23
21
  }) {
24
22
  const jsdom = require(modules.jsdom)
25
23
  const { JSDOM, VirtualConsole } = jsdom
26
24
 
27
25
  const virtualConsole = new VirtualConsole()
28
- virtualConsole.on('error', (...args) => consoleWriter.append({ type: 'error', text: args.join(' ') }))
29
- virtualConsole.on('warn', (...args) => consoleWriter.append({ type: 'warning', text: args.join(' ') }))
30
- virtualConsole.on('info', (...args) => consoleWriter.append({ type: 'info', text: args.join(' ') }))
31
- virtualConsole.on('log', (...args) => consoleWriter.append({ type: 'log', text: args.join(' ') }))
26
+ virtualConsole.on('error', (...args) => console.log(JSON.stringify({
27
+ timestamp: new Date().toISOString(),
28
+ channel: 'console',
29
+ type: 'error',
30
+ message: args.join(' ')
31
+ })))
32
+ virtualConsole.on('warn', (...args) => console.log(JSON.stringify({
33
+ timestamp: new Date().toISOString(),
34
+ channel: 'console',
35
+ type: 'warning',
36
+ message: args.join(' ')
37
+ })))
38
+ virtualConsole.on('info', (...args) => console.log(JSON.stringify({
39
+ timestamp: new Date().toISOString(),
40
+ channel: 'console',
41
+ type: 'info',
42
+ message: args.join(' ')
43
+ })))
44
+ virtualConsole.on('log', (...args) => console.log(JSON.stringify({
45
+ timestamp: new Date().toISOString(),
46
+ channel: 'console',
47
+ type: 'log',
48
+ message: args.join(' ')
49
+ })))
32
50
 
33
51
  let mainWindow
34
52
 
@@ -41,7 +59,7 @@ require('./browser')({
41
59
  writable: false
42
60
  })
43
61
  }
44
- require('./jsdom/compatibility')({ window, networkWriter })
62
+ require('./jsdom/compatibility')(window)
45
63
  if (options.debug) {
46
64
  require('./jsdom/debug')(window)
47
65
  }
@@ -64,11 +82,7 @@ require('./browser')({
64
82
  runScripts: 'dangerously',
65
83
  pretendToBeVisual: true,
66
84
  virtualConsole,
67
- resources: require('./jsdom/resource-loader')({
68
- jsdom,
69
- networkWriter,
70
- consoleWriter
71
- }),
85
+ resources: require('./jsdom/resource-loader')(jsdom),
72
86
  beforeParse
73
87
  })
74
88
  }
@@ -39,7 +39,8 @@ async function main () {
39
39
  time = (new Date(test.end) - new Date(test.start)) / 1000
40
40
  }
41
41
  o(` <testcase
42
- name="${xmlEscape(test.name)}" ${
42
+ name="${xmlEscape(test.name)}"
43
+ classname="${xmlEscape(module.name)}" ${
43
44
  time === undefined
44
45
  ? ''
45
46
  : `
@@ -8,17 +8,22 @@ require('./browser')({
8
8
  name: 'puppeteer',
9
9
  options: [
10
10
  ['--visible [flag]', 'Show the browser', false],
11
+ ['--firefox [flag]', 'Use firefox instead of chrome', false],
12
+ ['--binary <binary>', 'Binary path'],
11
13
  ['-w, --viewport-width <width>', 'Viewport width', 1920],
12
14
  ['-h, --viewport-height <height>', 'Viewport height', 1080],
13
15
  ['-l, --language <lang...>', 'Language(s)', ['en-US']],
14
16
  ['-u, --unsecure', 'Disable security features', false],
15
17
  ['--basic-auth-username <username>', 'Username for basic authentication', ''],
16
18
  ['--basic-auth-password <password>', 'Password for basic authentication', '']
17
- ],
18
- capabilities: {
19
+ ]
20
+ },
21
+
22
+ async capabilities ({ settings, options }) {
23
+ return {
19
24
  modules: ['puppeteer'],
20
25
  screenshot: '.png',
21
- scripts: true,
26
+ scripts: !options.firefox,
22
27
  traces: ['console', 'network']
23
28
  }
24
29
  },
@@ -68,7 +73,14 @@ require('./browser')({
68
73
  )
69
74
  }
70
75
 
76
+ let product
77
+ if (options.firefox) {
78
+ product = 'firefox'
79
+ }
80
+
71
81
  browser = await puppeteer.launch({
82
+ product,
83
+ executablePath: options.binary,
72
84
  headless: options.visible ? false : 'new',
73
85
  defaultViewport: null,
74
86
  args
@@ -0,0 +1,20 @@
1
+ module.exports = async function scanUI5 (url, onFolder, onFile) {
2
+ if (url.match(/\/((?:test-)?resources\/.*)/)) {
3
+ return // ignore UI5 resources
4
+ }
5
+ const html = await (await fetch(url)).text()
6
+ const items = [...html.matchAll(/<a href="([^"]+)" class="icon/ig)]
7
+ .map(([_, item]) => item)
8
+ .filter(item => item.endsWith('/') || item.endsWith('.js') || item.endsWith('.ts'))
9
+ await onFolder(items.length)
10
+ for (const item of items) {
11
+ const itemUrl = new URL(item, url).toString()
12
+ if (item.endsWith('/')) {
13
+ await scanUI5(itemUrl, onFolder, onFile)
14
+ } else if (item.endsWith('.ts')) {
15
+ await onFile(itemUrl, await (await fetch(itemUrl.replace(/\.ts$/, '.js'))).text())
16
+ } else {
17
+ await onFile(itemUrl, await (await fetch(itemUrl + '?instrument=true')).text())
18
+ }
19
+ }
20
+ }
@@ -4,8 +4,7 @@ module.exports = async ({
4
4
  seleniumWebdriver,
5
5
  settings,
6
6
  options,
7
- loggingPreferences,
8
- $capabilities
7
+ loggingPreferences
9
8
  }) => {
10
9
  const { Browser, Builder } = seleniumWebdriver
11
10
  const edge = require(join(settings.modules['selenium-webdriver'], 'edge'))
@@ -4,15 +4,14 @@ module.exports = async ({
4
4
  seleniumWebdriver,
5
5
  settings,
6
6
  options,
7
- loggingPreferences,
8
- $capabilities
7
+ loggingPreferences
9
8
  }) => {
10
9
  const { Browser, Builder } = seleniumWebdriver
11
10
  const firefox = require(join(settings.modules['selenium-webdriver'], 'firefox'))
12
11
 
13
12
  const firefoxOptions = new firefox.Options()
14
13
  if (!options.visible) {
15
- firefoxOptions.headless = true
14
+ firefoxOptions.addArguments('-headless')
16
15
  }
17
16
  firefoxOptions.setLoggingPrefs(loggingPreferences)
18
17
  if (options.binary) {
@@ -7,7 +7,7 @@ const { writeFile } = require('fs/promises')
7
7
  let logging
8
8
  let driver
9
9
 
10
- function browser (value, defaultValue) {
10
+ function browser (value) {
11
11
  if (value === undefined) {
12
12
  return 'chrome'
13
13
  }
@@ -0,0 +1,89 @@
1
+ 'use strict'
2
+
3
+ const { InvalidArgumentError } = require('commander')
4
+
5
+ let browserio
6
+
7
+ function browser (value) {
8
+ if (value === undefined) {
9
+ return 'chrome'
10
+ }
11
+ if (!['chrome', 'firefox'].includes(value)) {
12
+ throw new InvalidArgumentError('Browser name')
13
+ }
14
+ return value
15
+ }
16
+
17
+ require('./browser')({
18
+ metadata: {
19
+ name: 'webdriverio',
20
+ options: [
21
+ ['--visible [flag]', 'Show the browser', false],
22
+ ['-b, --browser <name>', 'Browser driver', browser, 'chrome'],
23
+ ['--binary <binary>', 'Binary path']
24
+ ]
25
+ },
26
+
27
+ async capabilities ({ settings, options }) {
28
+ return {
29
+ modules: ['webdriverio'],
30
+ screenshot: '.png',
31
+ scripts: true,
32
+ traces: []
33
+ }
34
+ },
35
+
36
+ async screenshot ({ filename }) {
37
+ if (browserio) {
38
+ await browserio.saveScreenshot(filename)
39
+ return true
40
+ }
41
+ },
42
+
43
+ async beforeExit () {
44
+ if (browserio) {
45
+ await browserio.deleteSession()
46
+ }
47
+ },
48
+
49
+ async run ({
50
+ settings: { url, scripts, modules },
51
+ options,
52
+ consoleWriter,
53
+ networkWriter
54
+ }) {
55
+ const { remote } = require(modules.webdriverio)
56
+
57
+ const [browserOptions, args] = {
58
+ chrome: [
59
+ 'goog:chromeOptions',
60
+ options.visible ? [] : ['--headless=new', '--log-level=3', '--disable-gpu']
61
+ ],
62
+ firefox: [
63
+ 'moz:firefoxOptions',
64
+ options.visible ? [] : ['-headless']
65
+ ]
66
+ }[options.browser]
67
+
68
+ browserio = await remote({
69
+ capabilities: {
70
+ browserName: options.browser,
71
+ webSocketUrl: true,
72
+ [browserOptions]: {
73
+ args,
74
+ binary: options.binary
75
+ }
76
+ }
77
+ })
78
+
79
+ if (scripts && scripts.length) {
80
+ for (const script of scripts) {
81
+ await browserio.scriptAddPreloadScript({
82
+ functionDeclaration: `() => ${script}`
83
+ })
84
+ }
85
+ }
86
+
87
+ await browserio.url(url)
88
+ }
89
+ })
package/src/endpoints.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { join } = require('path')
3
+ const { join, dirname, basename } = require('path')
4
4
  const { body } = require('reserve')
5
5
  const { extractPageUrl } = require('./tools')
6
6
  const { Request, Response } = require('reserve')
@@ -18,7 +18,7 @@ const punybindBinPath = join(resolveDependencyPath('punybind'), 'dist/punybind.j
18
18
  module.exports = job => {
19
19
  async function endpointImpl (api, implementation, request) {
20
20
  const url = extractPageUrl(request.headers)
21
- const data = JSON.parse(await body(request))
21
+ const data = await body(request)
22
22
  try {
23
23
  await implementation.call(this, url, data)
24
24
  } catch (error) {
@@ -75,7 +75,9 @@ module.exports = job => {
75
75
  }, {
76
76
  // QUnit hooks
77
77
  match: '^/_/qunit-hooks.js',
78
- file: join(__dirname, './inject/qunit-hooks.js')
78
+ cwd: __dirname,
79
+ file: 'inject/qunit-hooks.js',
80
+ static: !job.debugDevMode
79
81
  }, {
80
82
  // Concatenate qunit.js source with hooks
81
83
  match: /\/thirdparty\/(qunit(?:-2)?(?:-dbg)?\.js)/,
@@ -123,23 +125,33 @@ module.exports = job => {
123
125
  }, {
124
126
  // UI to follow progress
125
127
  match: '^/_/progress.html',
126
- file: job.progressPage
128
+ cwd: dirname(job.progressPage),
129
+ file: basename(job.progressPage),
130
+ static: !job.debugDevMode
127
131
  }, {
128
132
  // Report 'main' substituted for progress
129
133
  match: '^/_/report/main.js',
130
- file: join(__dirname, 'defaults/report/progress.js')
134
+ cwd: __dirname,
135
+ file: 'defaults/report/progress.js',
136
+ static: !job.debugDevMode
131
137
  }, {
132
138
  // Other report resources
133
139
  match: '^/_/report/(.*)',
134
- file: join(__dirname, 'defaults/report/$1')
140
+ cwd: __dirname,
141
+ file: 'defaults/report/$1',
142
+ static: !job.debugDevMode
135
143
  }, {
136
144
  // punybind
137
145
  match: '^/_/punybind.js',
138
- file: punybindBinPath
146
+ cwd: dirname(punybindBinPath),
147
+ file: basename(punybindBinPath),
148
+ static: !job.debugDevMode
139
149
  }, {
140
150
  // punyexpr
141
151
  match: '^/_/punyexpr.js',
142
- file: punyexprBinPath
152
+ cwd: dirname(punyexprBinPath),
153
+ file: basename(punyexprBinPath),
154
+ static: !job.debugDevMode
143
155
  }, {
144
156
  // Endpoint to retry on progress
145
157
  method: 'INFO',
@@ -155,15 +167,21 @@ module.exports = job => {
155
167
  }, {
156
168
  // Endpoint to coverage files
157
169
  match: '^/_/coverage/(.*)',
158
- file: join(job.coverageReportDir, '$1')
170
+ cwd: job.coverageReportDir,
171
+ file: '$1',
172
+ static: false
159
173
  }, {
160
174
  // Endpoint to report
161
175
  match: '^/_/report.html',
162
- file: join(__dirname, 'report.html')
176
+ cwd: __dirname,
177
+ file: 'report.html',
178
+ static: !job.debugDevMode
163
179
  }, {
164
180
  // Endpoint to report files
165
181
  match: '^/_/(.*)',
166
- file: join(job.reportDir, '$1')
182
+ cwd: job.reportDir,
183
+ file: '$1',
184
+ static: false
167
185
  }]
168
186
  : []
169
187
  }
@@ -83,7 +83,10 @@
83
83
  })
84
84
  xhr.open('POST', base + '/_/' + url)
85
85
  xhr.setRequestHeader('x-page-url', top.location)
86
- xhr.send(stringify(data))
86
+ xhr.setRequestHeader('content-type', 'application/json')
87
+ const json = stringify(data)
88
+ xhr.setRequestHeader('content-length', json.length)
89
+ xhr.send(json)
87
90
  })
88
91
  }
89
92
  lastPost = lastPost
package/src/job-mode.js CHANGED
@@ -28,6 +28,7 @@ function buildAndCheckMode (job) {
28
28
  'reportDir',
29
29
  'pageTimeout',
30
30
  'browserCloseTimeout',
31
+ 'browserRetry',
31
32
  'failFast',
32
33
  'keepAlive',
33
34
  'alternateNpmPath',
package/src/job.js CHANGED
@@ -132,6 +132,7 @@ function getCommand (cwd) {
132
132
  .option('-ccf, --coverage-check-functions <percent>', '[💻🔗] What % of functions must be covered', percent, 0)
133
133
  .option('-ccl, --coverage-check-lines <percent>', '[💻🔗] What % of lines must be covered', percent, 0)
134
134
  .option('-ccs, --coverage-check-statements <percent>', '[💻🔗] What % of statements must be covered', percent, 0)
135
+ .option('-crs, --coverage-remote-scanner <path>', '[💻🔗] Scan for files when all coverage is requested', '$/scan-ui5.js')
135
136
  .option('-s, --serve-only [flag]', '[💻🔗] Serve only', boolean, false)
136
137
 
137
138
  // Specific to legacy (and might be used with url if pointing to local project)
@@ -146,9 +147,10 @@ function getCommand (cwd) {
146
147
 
147
148
  // Specific to coverage in url mode (experimental)
148
149
  .option('-cp, --coverage-proxy [flag]', `[🔗] ${EXPERIMENTAL_OPTION} use internal proxy to instrument remote files`, boolean, false)
149
- .option('-cpi, --coverage-proxy-include <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to instrument for coverage`, regex, regex('.*'))
150
- .option('-cpe, --coverage-proxy-exclude <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to ignore for coverage`, regex, regex('/((test-)?resources|tests?)/.*'))
150
+ .option('-cpi, --coverage-proxy-include <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to instrument for coverage`, regex, '.*')
151
+ .option('-cpe, --coverage-proxy-exclude <regexp>', `[🔗] ${EXPERIMENTAL_OPTION} urls to ignore for coverage`, regex, '/((test-)?resources|tests?)/')
151
152
 
153
+ .addOption(new Option('--debug-dev-mode', DEBUG_OPTION, boolean).hideHelp())
152
154
  .addOption(new Option('--debug-probe-only', DEBUG_OPTION, boolean).hideHelp())
153
155
  .addOption(new Option('--debug-keep-browser-open', DEBUG_OPTION, boolean).hideHelp())
154
156
  .addOption(new Option('--debug-memory', DEBUG_OPTION, boolean).hideHelp())
@@ -219,7 +221,7 @@ function finalize (job) {
219
221
  function updateToAbsolute (member, from = job.cwd) {
220
222
  job[member] = toAbsolute(job[member], from)
221
223
  }
222
- 'browser,coverageSettings,progressPage'
224
+ 'browser,coverageSettings,coverageRemoteScanner,progressPage'
223
225
  .split(',')
224
226
  .forEach(setting => { job[setting] = checkDefault(job[setting]) })
225
227
  updateToAbsolute('cwd', job.initialCwd)
@@ -299,6 +301,8 @@ function finalize (job) {
299
301
  overrideDirIfNotSet('coverageReportDir', settings['report-dir'])
300
302
  overrideDirIfNotSet('coverageTempDir', settings['temp-dir'])
301
303
  overrideIfNotSet('coverageReporters', settings.reporter)
304
+
305
+ checkAccess({ path: job.coverageRemoteScanner, label: 'coverage remote scanner', file: true })
302
306
  }
303
307
 
304
308
  if (job.mode === 'url') {
package/src/output.js CHANGED
@@ -260,6 +260,9 @@ function build (job) {
260
260
  version: () => {
261
261
  const { name, version } = require(join(__dirname, '../package.json'))
262
262
  log(job, p80()`${name}@${version}`)
263
+ if (job.debugDevMode) {
264
+ log(job, p80()`⚠️ Development mode ⚠️`)
265
+ }
263
266
  },
264
267
 
265
268
  serving: url => {
@@ -364,7 +367,7 @@ function build (job) {
364
367
  },
365
368
 
366
369
  packageNotLatest (name, latestVersion) {
367
- wrap(() => log(job, `/!\\ latest version of ${name} is ${latestVersion}`))()
370
+ wrap(() => log(job, `⚠️ [PKGVRS] latest version of ${name} is ${latestVersion}`))()
368
371
  },
369
372
 
370
373
  browserStart (url) {
@@ -472,7 +475,15 @@ function build (job) {
472
475
  }),
473
476
 
474
477
  instrumentationSkipped: wrap(() => {
475
- log(job, p80()`Skipping nyc instrumentation (--url)`)
478
+ log(job, p80()`⚠️ [SKPNYC] Skipping nyc instrumentation (--url)`)
479
+ }),
480
+
481
+ assumingOneOrigin: wrap(() => {
482
+ log(job, p80()`⚠️ [COVORG] Considering only one origin`)
483
+ }),
484
+
485
+ noInfoForAllCoverage: wrap(() => {
486
+ log(job, p80()`⚠️ [COVALL] Unable to process all coverage, report might be incomplete`)
476
487
  }),
477
488
 
478
489
  endpointError: wrap(({ api, url, data, error }) => {
@@ -548,7 +559,7 @@ function build (job) {
548
559
  }),
549
560
 
550
561
  unhandled: wrap(() => {
551
- warn(job, p80()`Some requests are not handled properly, check the unhandled.txt report for more info`)
562
+ warn(job, p80()`⚠️ [UNHAND] Some requests are not handled properly, check the unhandled.txt report for more info`)
552
563
  }),
553
564
 
554
565
  reportGeneratorFailed: wrap((generator, exitCode, buffers) => {
package/src/reserve.js CHANGED
@@ -1,4 +1,3 @@
1
- const { join } = require('path')
2
1
  const cors = require('./cors')
3
2
  const endpoints = require('./endpoints')
4
3
  const { mappings: coverage } = require('./coverage')
@@ -13,12 +12,13 @@ module.exports = async job => check({
13
12
  ...job.mappings ?? [],
14
13
  ...job.serveOnly ? [] : endpoints(job),
15
14
  ...ui5(job),
16
- ...await coverage(job), {
15
+ ...job.serveOnly ? [] : await coverage(job),
16
+ {
17
17
  // Project mapping
18
18
  match: /^\/(.*)/,
19
- file: join(job.webapp, '$1'),
20
- strict: true,
21
- 'ignore-if-not-found': true
19
+ cwd: job.webapp,
20
+ file: '$1',
21
+ static: !job.watch && !job.debugDevMode
22
22
  },
23
23
  ...job.serveOnly ? [{ status: 404 }] : unhandled(job)
24
24
  ]
package/src/ui5.js CHANGED
@@ -63,7 +63,7 @@ module.exports = {
63
63
 
64
64
  const cacheBase = buildCacheBase(job)
65
65
  const match = /\/((?:test-)?resources\/.*)/
66
- const ifCacheEnabled = (request, url, match) => job.cache ? match : false
66
+ const ifCacheEnabled = (request, url, match) => job.cache
67
67
  const uncachable = {}
68
68
  const cachingInProgress = {}
69
69
 
@@ -90,8 +90,9 @@ module.exports = {
90
90
  // UI5 from cache
91
91
  match,
92
92
  'if-match': ifCacheEnabled,
93
- file: join(cacheBase, '$1'),
94
- 'ignore-if-not-found': true
93
+ cwd: cacheBase,
94
+ file: '$1',
95
+ static: !job.debugDevMode
95
96
  }, {
96
97
  // UI5 caching
97
98
  method: 'GET',
@@ -130,8 +131,12 @@ module.exports = {
130
131
  job.libs.forEach(({ relative, source }) => {
131
132
  mappings.unshift({
132
133
  match: new RegExp(`\\/resources\\/${relative.replace(/\//g, '\\/')}(.*)`),
133
- file: join(source, '$1'),
134
- 'ignore-if-not-found': true
134
+ cwd: source,
135
+ file: '$1',
136
+ static: !job.watch && !job.debugDevMode
137
+ }, {
138
+ match: new RegExp(`\\/resources\\/${relative.replace(/\//g, '\\/')}(.*)`),
139
+ status: 404
135
140
  })
136
141
  })
137
142