ui5-test-runner 5.9.1 → 5.10.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.
@@ -7,7 +7,8 @@
7
7
  }
8
8
 
9
9
  const base = window['ui5-test-runner/base-host'] || ''
10
- const XHR = window.XMLHttpRequest
10
+ const probe = window['ui5-test-runner/probe'] || false
11
+ const batchSize = !probe && (window['ui5-test-runner/batch'] || 0)
11
12
 
12
13
  let lastPost = Promise.resolve()
13
14
 
@@ -64,9 +65,10 @@
64
65
  'circular:array': [].concat(value) // 'new' object
65
66
  }
66
67
  }
67
- return Object.assign({
68
- 'circular:id': id
69
- }, value)
68
+ return {
69
+ 'circular:id': id,
70
+ ...value
71
+ }
70
72
  }
71
73
  }
72
74
  return value
@@ -77,8 +79,26 @@
77
79
 
78
80
  const xPageUrl = top.location.toString()
79
81
 
80
- window[MODULE] = function post (url, data) {
81
- function request () {
82
+ const nativeFetch = window.fetch
83
+ let request
84
+ if (nativeFetch) {
85
+ request = async function (url, data) {
86
+ const response = await nativeFetch(base + '/_/' + url, {
87
+ method: 'POST',
88
+ headers: {
89
+ 'x-page-url': xPageUrl,
90
+ 'content-type': 'application/json'
91
+ },
92
+ body: stringify(data)
93
+ })
94
+ if (response.status !== 200) {
95
+ throw response.statusText
96
+ }
97
+ return response.text()
98
+ }
99
+ } else {
100
+ const XHR = window.XMLHttpRequest
101
+ request = function (url, data) {
82
102
  return new Promise(function (resolve, reject) {
83
103
  const xhr = new XHR()
84
104
  xhr.addEventListener('load', () => {
@@ -94,7 +114,10 @@
94
114
  xhr.send(json)
95
115
  })
96
116
  }
97
- lastPost = lastPost.then(request)
117
+ }
118
+
119
+ function post (url, data) {
120
+ lastPost = lastPost.then(() => request(url, data))
98
121
  if (!window.__unsafe__) {
99
122
  lastPost = lastPost
100
123
  .then(undefined, function (reason) {
@@ -103,4 +126,16 @@
103
126
  }
104
127
  return lastPost
105
128
  }
129
+
130
+ const aggregatedData = []
131
+
132
+ function batch (url, data) {
133
+ aggregatedData.push([url, data])
134
+ if (url === 'QUnit/done' || aggregatedData.length === batchSize) {
135
+ post('QUnit/batch', [...aggregatedData])
136
+ aggregatedData.length = 0
137
+ }
138
+ }
139
+
140
+ window[MODULE] = batchSize ? batch : post
106
141
  }())
@@ -36,6 +36,10 @@
36
36
  }
37
37
 
38
38
  function installQUnitHooks () {
39
+ if (window !== window.top || window !== window.parent) {
40
+ return // Do not install in iframe
41
+ }
42
+
39
43
  QUnit.begin(function (details) {
40
44
  details.isOpa = isOpa()
41
45
  return post('QUnit/begin', details)
@@ -47,21 +51,24 @@
47
51
 
48
52
  QUnit.log(function (log) {
49
53
  let ready = false
50
- post('QUnit/log', extend(log))
51
- .then(undefined, function () {
52
- console.error('Failed to POST to QUnit/log (no timestamp)', log)
53
- })
54
- .then(function () {
55
- ready = true
56
- })
57
- if (isOpa()) {
58
- window.sap.ui.test.Opa5.prototype.waitFor({
59
- timeout: 10,
60
- autoWait: false, // Ignore interactable constraint
61
- check: function () {
62
- return ready
63
- }
64
- })
54
+ const result = post('QUnit/log', extend(log))
55
+ if (result && result.then) {
56
+ result
57
+ .then(undefined, function () {
58
+ console.error('Failed to POST to QUnit/log (no timestamp)', log)
59
+ })
60
+ .then(function () {
61
+ ready = true
62
+ })
63
+ if (isOpa()) {
64
+ window.sap.ui.test.Opa5.prototype.waitFor({
65
+ timeout: 10,
66
+ autoWait: false, // Ignore interactable constraint
67
+ check: function () {
68
+ return ready
69
+ }
70
+ })
71
+ }
65
72
  }
66
73
  })
67
74
 
package/src/job.js CHANGED
@@ -114,6 +114,8 @@ function getCommand (cwd) {
114
114
  .option('--env <name=value...>', '[💻🔗🧪📡] Set environment variable', arrayOf(string))
115
115
  .option('--localhost <host>', `[💻🔗🧪📡] ${DANGEROUS_OPTION} Hostname for legacy URLs and callbacks`, string, 'localhost')
116
116
  .option('--ci [flag]', '[💻🔗🧪📡] CI mode (no interactive output)', boolean, false)
117
+ .option('--deep-probe [flag]', '[💻🔗🧪📡] Deep probe (recursive, slower)', boolean, false)
118
+ .option('--probe-parallel <count>', '[💻🔗🧪📡] Number of parallel probes (0 to use --parallel)', integer, 0)
117
119
 
118
120
  // Common to legacy and url
119
121
  .option('--webapp <path>', '[💻🔗] Base folder of the web application (relative to cwd)', 'webapp')
@@ -128,6 +130,8 @@ function getCommand (cwd) {
128
130
  .option('-so, --split-opa [flag]', '[💻🔗📡] Split OPA tests using QUnit modules', boolean, false)
129
131
  .option('-rg, --report-generator <path...>', '[💻🔗📡] Report generator paths (relative to cwd or use $/ for provided ones)', ['$/report.js'])
130
132
  .option('--progress-page <path>', '[💻🔗📡] Progress page path (relative to cwd or use $/ for provided ones)', '$/report/default.html')
133
+ .option('--jest [flag]', `[💻🔗📡] ${EXPERIMENTAL_OPTION} Simulate jest environment`)
134
+ .option('--qunit-batch-size <size>', `[💻🔗📡] ${EXPERIMENTAL_OPTION} QUnit hooks batch size (disables screenshots)`, integer, 0)
131
135
 
132
136
  .option('--coverage [flag]', '[💻🔗📡] Enable or disable code coverage', boolean)
133
137
  .option('--no-coverage', '[💻🔗📡] Disable code coverage')
@@ -145,10 +149,11 @@ function getCommand (cwd) {
145
149
  .option('-w, --watch [flag]', '[💻🔗] Monitor the webapp folder (or the one specified with --watch-folder) and re-execute tests on change', boolean, false)
146
150
  .option('--watch-folder <path>', '[💻🔗] Folder to monitor with watch (enables --watch if not specified)', string)
147
151
 
148
- .option('--start <command>', '[💻🔗] Start command (might be an NPM script or a shell command)', string)
152
+ .option('--start <command>', '[💻🔗] Start command (might be an NPM script or a shell command) ⚠️ the command is killed on tests completion', string)
149
153
  .option('--start-wait-url <command>', '[💻🔗] URL to wait for (🔗 defaulted to first url)', url)
150
154
  .option('--start-wait-method <method>', '[💻🔗] HTTP method to check the waited URL', 'GET')
151
- .option('--start-timeout <timeout>', '[💻🔗] Maximum waiting time for the start command (based on when the first URL becomes available)', timeout, 5000)
155
+
156
+ .option('--start-timeout <timeout>', '[💻🔗] Maximum waiting time for the start command (based on when the first URL becomes available, also used for termination)', timeout, 5000)
152
157
 
153
158
  .option('--end <script>', '[💻🔗] End script (will receive path to `job.js`)', string)
154
159
  .option('--end-timeout <timeout>', '[💻🔗] Maximum waiting time for the end script', timeout, 15000)
@@ -178,6 +183,7 @@ function getCommand (cwd) {
178
183
  .addOption(new Option('--debug-probe-only', DEBUG_OPTION, boolean).hideHelp())
179
184
  .addOption(new Option('--debug-keep-browser-open', DEBUG_OPTION, boolean).hideHelp())
180
185
  .addOption(new Option('--debug-memory', DEBUG_OPTION, boolean).hideHelp())
186
+ .addOption(new Option('--debug-handles', DEBUG_OPTION, boolean).hideHelp())
181
187
  .addOption(new Option('--debug-keep-report', DEBUG_OPTION, boolean).hideHelp())
182
188
  .addOption(new Option('--debug-capabilities-test <name>', DEBUG_OPTION).hideHelp())
183
189
  .addOption(new Option('--debug-capabilities-no-timeout', DEBUG_OPTION, boolean).hideHelp())
@@ -214,7 +220,7 @@ function checkAccess ({ path, label, file /*, write */ }) {
214
220
  // }
215
221
  accessSync(path, mode)
216
222
  } catch (error) {
217
- throw new Error(`Unable to access ${label}, check your settings`)
223
+ throw new Error(`Unable to access ${label || path}, check your settings`)
218
224
  }
219
225
  const stat = statSync(path)
220
226
  if (file) {
@@ -361,13 +367,18 @@ function finalize (job) {
361
367
 
362
368
  if (job.mode === 'url') {
363
369
  const port = job.port.toString()
364
- job[$remoteOnLegacy] = job.url.every(url => {
370
+ job[$remoteOnLegacy] = job.url && job.url.every(url => {
365
371
  // ignore host name since the machine might be exposed with any name
366
372
  const parsedUrl = new URL(url)
367
373
  return parsedUrl.port === port
368
374
  })
369
375
  }
370
376
 
377
+ if (job.qunitBatchSize) {
378
+ job.screenshot = false
379
+ job.screenshotOnFailure = false
380
+ }
381
+
371
382
  job[$status] = 'Starting'
372
383
  Object.defineProperty(job, 'status', {
373
384
  get () {
package/src/npm.js CHANGED
@@ -80,7 +80,9 @@ function getSafeJobAndOutput (nullableJob) {
80
80
  return { job: nullableJob, output: getOutput(nullableJob) }
81
81
  }
82
82
  return {
83
- job: {},
83
+ job: {
84
+ offline: true
85
+ },
84
86
  output: {
85
87
  debug: noop,
86
88
  resolvedPackage: noop,
package/src/output.js CHANGED
@@ -9,6 +9,8 @@ const {
9
9
  $statusProgressTotal
10
10
  } = require('./symbols')
11
11
  const { filename, noop, pad } = require('./tools')
12
+ const os = require('os')
13
+ const { describeHandle } = require('./handle')
12
14
 
13
15
  const $output = Symbol('output')
14
16
  const $outputStart = Symbol('output-start')
@@ -134,6 +136,22 @@ function progress (job, cleanFirst = true) {
134
136
  const fmt = size => `${(size / (1024 * 1024)).toFixed(2)}M`
135
137
  sequence.push(`MEM r:${fmt(rss)}, h:${fmt(heapUsed)}/${fmt(heapTotal)}, x:${fmt(external)}, a:${fmt(arrayBuffers)}\n`)
136
138
  }
139
+ if (job.debugHandles) {
140
+ ++output.lines
141
+ const activeHandles = process._getActiveHandles ? process._getActiveHandles() : []
142
+ sequence.push(`HANDLES ${activeHandles.length}\n`)
143
+ for (let index = 0; index < activeHandles.length; ++index) {
144
+ const handle = activeHandles[index]
145
+ const bullet = index === activeHandles.length - 1 ? '└' : '├'
146
+ ++output.lines
147
+ const handleDescription = describeHandle(handle).label
148
+ if (handleDescription.length > process.stdout.columns - 4) {
149
+ sequence.push(`${bullet}─ ${handleDescription.slice(0, process.stdout.columns - 4)}\n`)
150
+ } else {
151
+ sequence.push(`${bullet}─ ${handleDescription}\n`)
152
+ }
153
+ }
154
+ }
137
155
  if (job[$outputProgress]) {
138
156
  output.lines += job[$outputProgress].length
139
157
  job[$outputProgress].forEach(({ count, total, label }) => {
@@ -270,7 +288,24 @@ function build (job) {
270
288
 
271
289
  version: wrap(() => {
272
290
  const { name, version = 'dev' } = require(join(__dirname, '../package.json'))
273
- log(job, p80()`${name}@${version}`)
291
+ log(job, p80()` _ ____ _ _
292
+ _ _(_) ___| | |_ ___ ___| |_ _ __ _ _ _ __ _ __ ___ _ __
293
+ | | | | |___ \\ _____| __/ _ \\/ __| __|____| '__| | | | '_ \\| '_ \\ / _ \\ '__|
294
+ | |_| | |___) |_____| || __/\\__ \\ ||_____| | | |_| | | | | | | | __/ |
295
+ \\__,_|_|____/ \\__\\___||___/\\__| |_| \\__,_|_| |_|_| |_|\\___|_| `)
296
+ const now = new Date()
297
+ log(job, p80()`${name}@${version} / Node.js ${process.version} / ${now.toISOString()} (${now.getTimezoneOffset()})`)
298
+ const cpus = {}
299
+ for (const { model } of os.cpus()) {
300
+ if (cpus[model]) {
301
+ ++cpus[model]
302
+ } else {
303
+ cpus[model] = 1
304
+ }
305
+ }
306
+ for (const [model, count] of Object.entries(cpus)) {
307
+ log(job, p80()`${os.machine()} / ${count}x ${model}`)
308
+ }
274
309
  if (job.debugDevMode) {
275
310
  log(job, p80()`⚠️ Development mode ⚠️`)
276
311
  }
@@ -422,7 +457,7 @@ function build (job) {
422
457
  },
423
458
 
424
459
  packageNotLatest (name, latestVersion) {
425
- wrap(() => log(job, `⚠️ [PKGVRS] latest version of ${name} is ${latestVersion}`))()
460
+ wrap(() => log(job, `⚠️ [PKGVRS] Latest version of ${name} is ${latestVersion}`))()
426
461
  },
427
462
 
428
463
  emptyBrowserArg () {
@@ -430,7 +465,11 @@ function build (job) {
430
465
  },
431
466
 
432
467
  detectedLeakOfHandles () {
433
- wrap(() => log(job, '⚠️ [HDLEAK] leaking Node.js handle(s) detected. This may cause issues with the shutdown'))()
468
+ wrap(() => log(job, '⚠️ [HDLEAK] Leaking Node.js handle(s) detected. This may cause issues with the shutdown'))()
469
+ },
470
+
471
+ failedToTerminateStartCommand () {
472
+ wrap(() => log(job, '⚠️ [STRTCT] Failed to terminate start command. This may cause issues with the shutdown'))()
434
473
  },
435
474
 
436
475
  browserStart (url) {
@@ -476,6 +515,10 @@ function build (job) {
476
515
  browserIssue(job, { type: 'failed', url, code, dir })
477
516
  }),
478
517
 
518
+ browserChildProcessError: wrap((url, { code }) => {
519
+ log(job, p80()`⚠️ [BRWCPE] Child process error ${code}: ${pad.lt(url)}`)
520
+ }),
521
+
479
522
  skipIf: wrap(() => {
480
523
  log(job, p80()`⚠️ [SKIPIF] Skipping execution (--if)`)
481
524
  }),
@@ -640,6 +683,15 @@ function build (job) {
640
683
  } else {
641
684
  log(job, p`│ ${pad.w(error.toString())} │`)
642
685
  }
686
+ if (error.cause) {
687
+ log(job, p`├────────${pad.x('─')}──┤`)
688
+ log(job, p`│ Cause : ${pad.x(' ')} │`)
689
+ if (error.cause.stack) {
690
+ log(job, p`│ ${pad.w(error.cause.stack)} │`)
691
+ } else {
692
+ log(job, p`│ ${pad.w(error.cause.toString())} │`)
693
+ }
694
+ }
643
695
  log(job, p`└──────────${pad.x('─')}┘`)
644
696
  }),
645
697
 
package/src/reserve.js CHANGED
@@ -11,7 +11,7 @@ module.exports = async job => check({
11
11
  cors,
12
12
  ...job.mappings ?? [],
13
13
  ...job.serveOnly ? [] : endpoints(job),
14
- ...ui5(job),
14
+ ...await ui5(job),
15
15
  ...job.serveOnly ? [] : await coverage(job),
16
16
  {
17
17
  // Project mapping
package/src/start.js CHANGED
@@ -2,9 +2,7 @@ const { exec } = require('child_process')
2
2
  const { stat, readFile } = require('fs/promises')
3
3
  const { join } = require('path')
4
4
  const { getOutput } = require('./output')
5
- const psTreeNodeCb = require('ps-tree')
6
- const { promisify } = require('util')
7
- const psTree = promisify(psTreeNodeCb)
5
+ const pidtree = require('pidtree')
8
6
 
9
7
  async function start (job) {
10
8
  const { startWaitUrl: url, startWaitMethod: method } = job
@@ -40,23 +38,23 @@ async function start (job) {
40
38
  }
41
39
  }
42
40
 
43
- let childProcessExited = false
41
+ let startProcessExited = false
44
42
  output.debug('start', 'Starting command :', start)
45
- const childProcess = exec(start, {
43
+ const startProcess = exec(start, {
46
44
  cwd: job.cwd,
47
45
  windowsHide: true
48
46
  })
49
- childProcess.on('close', () => {
50
- output.debug('start', 'start command process exited')
51
- childProcessExited = true
47
+ startProcess.on('close', () => {
48
+ output.debug('start', 'Start command process exited')
49
+ startProcessExited = true
52
50
  })
53
- output.monitor(childProcess)
51
+ output.monitor(startProcess)
54
52
 
55
53
  job.status = 'Waiting for URL to be reachable'
56
54
 
57
55
  const begin = Date.now()
58
56
  // eslint-disable-next-line no-unmodified-loop-condition
59
- while (!childProcessExited && Date.now() - begin <= job.startTimeout) {
57
+ while (!startProcessExited && Date.now() - begin <= job.startTimeout) {
60
58
  try {
61
59
  const response = await fetch(url, { method })
62
60
  output.debug('start', url, response.status)
@@ -69,20 +67,60 @@ async function start (job) {
69
67
  }
70
68
  }
71
69
 
72
- if (childProcessExited) {
73
- throw new Error(`Start command failed with exit code ${childProcess.exitCode}`)
70
+ if (startProcessExited) {
71
+ throw new Error(`Start command failed with exit code ${startProcess.exitCode}`)
74
72
  }
75
73
 
76
74
  const stop = async () => {
77
- output.debug('start', 'Getting child processes...')
78
- const childProcesses = await psTree(childProcess.pid)
79
- for (const child of childProcesses) {
80
- output.debug('start', 'Terminating process', child.PID)
81
- try {
82
- process.kill(child.PID, 'SIGKILL')
83
- } catch (e) {
84
- output.debug('start', 'Failed to terminate process', child.PID, ':', e)
75
+ job.status = 'Terminating start command'
76
+ const begin = new Date()
77
+ // eslint-disable-next-line no-unmodified-loop-condition
78
+ while (!startProcessExited && Date.now() - begin <= job.startTimeout) {
79
+ output.debug('start', `Getting start command ${startProcess.pid} child processes...`)
80
+ const childProcesses = await pidtree(startProcess.pid, { advanced: true })
81
+ output.debug('start', 'Child processes', JSON.stringify(childProcesses))
82
+ if (childProcesses.length === 0) {
83
+ try {
84
+ output.debug('start', 'Terminating start command')
85
+ process.kill(startProcess.pid, 'SIGKILL')
86
+ } catch (e) {
87
+ output.debug('start', 'Failed to terminate start command', startProcess.pid, ':', e)
88
+ }
89
+ } else {
90
+ const depth = {}
91
+ let deepest = 1
92
+ let deepless = childProcesses.length
93
+ while (deepless > 0) {
94
+ for (const { ppid, pid } of childProcesses) {
95
+ if (ppid === startProcess.pid) {
96
+ depth[pid] = 1
97
+ --deepless
98
+ } else {
99
+ const parentDepth = depth[ppid]
100
+ if (parentDepth !== undefined) {
101
+ depth[pid] = parentDepth + 1
102
+ deepest = Math.max(deepest, parentDepth + 1)
103
+ --deepless
104
+ }
105
+ }
106
+ }
107
+ }
108
+ output.debug('start', 'Child processes', JSON.stringify(depth), 'terminating', deepest)
109
+ for (const { pid } of childProcesses) {
110
+ if (depth[pid] === deepest) {
111
+ output.debug('start', 'Terminating start child process', pid)
112
+ try {
113
+ process.kill(pid, 'SIGKILL')
114
+ } catch (e) {
115
+ output.debug('start', 'Failed to terminate start child process', pid, ':', e)
116
+ }
117
+ }
118
+ }
85
119
  }
120
+ await new Promise(resolve => setTimeout(resolve, 250))
121
+ }
122
+ if (!startProcessExited) {
123
+ output.failedToTerminateStartCommand()
86
124
  }
87
125
  }
88
126
 
package/src/tests.js CHANGED
@@ -48,9 +48,13 @@ async function probeUrl (job, url) {
48
48
  let scripts
49
49
  if (job.browserCapabilities.scripts) {
50
50
  scripts = [
51
+ '(function () { window[\'ui5-test-runner/probe\'] = true }())',
51
52
  'post.js',
52
53
  'qunit-redirect.js'
53
54
  ]
55
+ if (job.jest) {
56
+ scripts.push('jest2qunit.js')
57
+ }
54
58
  }
55
59
  await start(job, url, scripts)
56
60
  } catch (error) {
@@ -64,10 +68,19 @@ async function runTestPage (job, url) {
64
68
  try {
65
69
  let scripts
66
70
  if (job.browserCapabilities.scripts) {
67
- scripts = [
71
+ scripts = []
72
+ if (job.qunitBatchSize) {
73
+ scripts.push(
74
+ `(function () { window['ui5-test-runner/batch'] = ${job.qunitBatchSize} }())`
75
+ )
76
+ }
77
+ scripts.push(
68
78
  'post.js',
69
79
  'qunit-hooks.js'
70
- ]
80
+ )
81
+ if (job.jest) {
82
+ scripts.push('jest2qunit.js')
83
+ }
71
84
  if (job.coverage && !job.coverageProxy) {
72
85
  scripts.push(
73
86
  'opa-iframe-coverage.js',
@@ -102,13 +115,42 @@ async function process (job) {
102
115
  await save(job)
103
116
  job.testPageUrls = []
104
117
 
118
+ let probingRound = 0
119
+ const parallel = job.probeParallel || job.parallel
120
+ const confirmedTestPageUrls = []
105
121
  job.status = 'Probing urls'
106
- try {
107
- await parallelize(task(job, probeUrl), job.url, job.parallel)
108
- } catch (e) {
109
- output.genericError(e)
110
- job.failed = true
111
- }
122
+ do {
123
+ ++probingRound
124
+ if (probingRound >= 2) {
125
+ if (job.testPageUrls.length === 0) {
126
+ break
127
+ }
128
+ job.status = `Probing urls (${probingRound})`
129
+ job.url = job.testPageUrls.filter(url => !confirmedTestPageUrls.includes(url))
130
+ if (job.url.length) {
131
+ job.testPageUrls = []
132
+ } else {
133
+ job.testPageUrls = confirmedTestPageUrls
134
+ break
135
+ }
136
+ }
137
+ try {
138
+ await parallelize(task(job, probeUrl), job.url, parallel)
139
+ } catch (e) {
140
+ output.genericError(e)
141
+ job.failed = true
142
+ break
143
+ }
144
+ job.testPageUrls.forEach(url => {
145
+ if ((job.url.includes(url) && !confirmedTestPageUrls.includes(url)) ||
146
+ (url.includes('/resources/sap/ui/test/starter/Test.qunit.html?testsuite=') && url.includes('&test='))
147
+ ) {
148
+ confirmedTestPageUrls.push(url)
149
+ getOutput(job).debug('probe', 'confirmed:', url)
150
+ }
151
+ })
152
+ getOutput(job).debug('probe', 'from', job.url.length, 'to', job.testPageUrls.length, 'confirmed', confirmedTestPageUrls.length)
153
+ } while (job.deepProbe)
112
154
 
113
155
  /* istanbul ignore else */
114
156
  if (!job.debugProbeOnly && !job.failed) {