ui5-test-runner 5.7.1 → 5.7.3

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/index.js CHANGED
@@ -16,6 +16,8 @@ const { start } = require('./src/start')
16
16
  const { executeIf } = require('./src/if')
17
17
  const { batch } = require('./src/batch')
18
18
  const { end } = require('./src/end')
19
+ const { checkLatest } = require('./src/npm')
20
+ const { cleanHandles } = require('./src/clean')
19
21
 
20
22
  function send (message) {
21
23
  if (process.send) {
@@ -50,11 +52,12 @@ async function main () {
50
52
  job = fromCmdLine(process.cwd(), process.argv.slice(2))
51
53
  output = getOutput(job)
52
54
  await recreateDir(job.reportDir)
53
- output.version()
55
+ const { name, version } = output.version()
54
56
  if (job.batchMode) {
55
57
  output.batchMode()
56
58
  }
57
59
  output.reportOnJobProgress()
60
+ checkLatest(job, name, version)
58
61
  if (job.mode === 'capabilities') {
59
62
  return capabilities(job)
60
63
  }
@@ -82,7 +85,10 @@ async function main () {
82
85
  output.debug('reserve', 'configuration', configuration)
83
86
  const server = serve(configuration)
84
87
  if (job.logServer) {
85
- server.on('redirected', output.redirected)
88
+ server
89
+ .on('incoming', output.logServerIncoming)
90
+ .on('redirected', output.logServerRedirected)
91
+ .on('closed', output.logServerClosed)
86
92
  }
87
93
 
88
94
  const { promise: serverStarted, resolve: serverReady, reject: serverError } = allocPromise()
@@ -145,10 +151,16 @@ async function main () {
145
151
  await end(job)
146
152
  }
147
153
  output.stop()
148
- await server.close()
154
+ await server.close({ close: true })
155
+ await new Promise(resolve => setTimeout(resolve, 100)) // wait for server handles to be released
156
+ if (job.logServer) {
157
+ output.logServerSummary()
158
+ }
149
159
  if (startedCommand) {
150
160
  await startedCommand.stop()
151
161
  }
162
+ output.debug('main', 'terminated')
163
+ cleanHandles(job)
152
164
  }
153
165
 
154
166
  main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "5.7.1",
3
+ "version": "5.7.3",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -22,7 +22,8 @@
22
22
  "test:report": "node ./src/defaults/report.js ./test/report && reserve --config ./test/report/reserve.json",
23
23
  "test:text-report": "node ./src/defaults/text-report.js ./test/report",
24
24
  "build:doc": "node build/doc",
25
- "clean": "npm uninstall -g ui5-test-runner puppeteer nyc selenium-webdriver playwright webdriverio jsdom"
25
+ "clean": "npm uninstall -g ui5-test-runner puppeteer nyc selenium-webdriver playwright webdriverio jsdom",
26
+ "update": "ncu -i --format group"
26
27
  },
27
28
  "repository": {
28
29
  "type": "git",
@@ -47,21 +48,22 @@
47
48
  "commander": "^12.1.0",
48
49
  "ps-tree": "^1.2.0",
49
50
  "punybind": "^1.2.1",
50
- "punyexpr": "1.0.4",
51
- "reserve": "2.2.0"
51
+ "punyexpr": "1.1.1",
52
+ "reserve": "2.3.1"
52
53
  },
53
54
  "devDependencies": {
54
- "@openui5/types": "^1.135.0",
55
- "@ui5/cli": "^4.0.14",
55
+ "@openui5/types": "^1.136.0",
56
+ "@ui5/cli": "^4.0.18",
56
57
  "@ui5/middleware-code-coverage": "^2.0.1",
57
58
  "dotenv": "^16.5.0",
58
59
  "jest": "^29.7.0",
59
- "nock": "^14.0.3",
60
+ "nock": "^14.0.5",
61
+ "npm-check-updates": "^18.0.1",
60
62
  "nyc": "^17.1.0",
61
63
  "rimraf": "^6.0.1",
62
64
  "standard": "^17.1.2",
63
65
  "typescript": "^5.8.3",
64
- "ui5-tooling-transpile": "^3.7.5"
66
+ "ui5-tooling-transpile": "^3.8.0"
65
67
  },
66
68
  "optionalDependencies": {
67
69
  "fsevents": "^2.3.3"
@@ -81,8 +81,14 @@ async function capabilities (job) {
81
81
  })
82
82
  const server = serve(configuration)
83
83
  if (job.logServer) {
84
- server.on('redirected', output.redirected)
84
+ server
85
+ .on('incoming', output.logServerIncoming)
86
+ .on('redirected', output.logServerRedirected)
87
+ .on('closed', output.logServerClosed)
85
88
  }
89
+ server.on('error', (error) => {
90
+ output.error('REserve error:', error)
91
+ })
86
92
  await new Promise(resolve => server
87
93
  .on('ready', ({ port }) => {
88
94
  job.port = port
@@ -7,14 +7,12 @@ module.exports = [{
7
7
  url: 'basic/index.html'
8
8
  }, {
9
9
  label: 'Loads a UI5 example',
10
- for: capabilities => !capabilities.modules.includes('jsdom'), // does not work anymore on JSDOM
11
10
  url: 'basic/ui5.html',
12
11
  endpoint: ({ body }) => {
13
12
  assert.strictEqual(body['sap.m.Button'], true)
14
13
  }
15
14
  }, {
16
15
  label: 'Loads a UI5 inside an iframe',
17
- for: capabilities => !capabilities.modules.includes('jsdom'), // does not work anymore on JSDOM
18
16
  url: 'basic/iframe.html',
19
17
  endpoint: ({ body }) => {
20
18
  assert.strictEqual(body['sap.m.Button'], true)
@@ -14,7 +14,7 @@
14
14
  sap.ui.getCore().attachInit(function() {
15
15
  const xhr = new XMLHttpRequest()
16
16
  xhr.open('POST', '/_/log')
17
- if (parent !== window) {
17
+ if (parent !== window && parent.document.location.toString() !== 'about:blank') {
18
18
  xhr.setRequestHeader('x-page-url', parent.document.location.toString())
19
19
  }
20
20
  xhr.send('{"sap.m.Button":' + !!sap.m.Button + '}')
package/src/clean.js ADDED
@@ -0,0 +1,29 @@
1
+ const { getOutput } = require('./output')
2
+
3
+ module.exports = {
4
+ cleanHandles (job) {
5
+ const output = getOutput(job)
6
+ const activeHandles = process._getActiveHandles ? process._getActiveHandles() : []
7
+ let displayWarning = true
8
+ for (const handle of activeHandles) {
9
+ const className = handle && handle.constructor && handle.constructor.name
10
+ output.debug('handle', 'active handle', className)
11
+ if (className === 'TLSSocket') {
12
+ if (displayWarning) {
13
+ displayWarning = false
14
+ output.detectedLeakOfHandles()
15
+ }
16
+ let info
17
+ if (handle._httpMessage) {
18
+ const { path, method, host, protocol } = handle._httpMessage
19
+ info = `${method} ${protocol}//${host}${path}`
20
+ } else {
21
+ const { localAddress, localPort, remoteAddress, remotePort } = handle
22
+ info = `from ${localAddress}:${localPort} to ${remoteAddress}:${remotePort}`
23
+ }
24
+ output.debug('handle', 'TLS socket', info)
25
+ handle.destroy()
26
+ }
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,123 @@
1
+ 'use strict'
2
+
3
+ const log = (attributes) => {
4
+ console.log(JSON.stringify(attributes, (key, value) => {
5
+ if (key === 'headers' && typeof value === 'object') {
6
+ const literal = {}
7
+ value.forEach((headerValue, headerName) => {
8
+ literal[headerName] = headerValue
9
+ })
10
+ return literal
11
+ }
12
+ return value
13
+ }, 2))
14
+ }
15
+
16
+ require('./browser')({
17
+ metadata: {
18
+ name: 'happy-dom',
19
+ options: [
20
+ ['--debug [flag]', 'Enable more traces', false]
21
+ ],
22
+ capabilities: {
23
+ modules: ['happy-dom'],
24
+ scripts: true,
25
+ traces: ['multiplex']
26
+ }
27
+ },
28
+
29
+ async run ({
30
+ settings: { url, scripts, modules }
31
+ // options
32
+ }) {
33
+ const happyDom = require(modules['happy-dom'])
34
+ const { Browser } = happyDom
35
+ const browser = new Browser({
36
+ settings: {
37
+ fetch: {
38
+ interceptor: {
39
+ beforeAsyncRequest: async ({ request: { method, url, headers } }) => {
40
+ log({
41
+ timestamp: new Date().toISOString(),
42
+ channel: 'network',
43
+ type: 'request',
44
+ async: true,
45
+ request: { method, url, headers }
46
+ })
47
+ },
48
+ beforeSyncRequest: ({ request: { method, url, headers } }) => {
49
+ log({
50
+ timestamp: new Date().toISOString(),
51
+ channel: 'network',
52
+ type: 'request',
53
+ async: false,
54
+ request: { method, url, headers }
55
+ })
56
+ },
57
+ afterAsyncResponse: async ({ request: { method, url, headers }, response, window }) => {
58
+ const body = await response.text()
59
+ log({
60
+ timestamp: new Date().toISOString(),
61
+ channel: 'network',
62
+ type: 'response',
63
+ async: true,
64
+ request: { method, url, headers },
65
+ response: {
66
+ ...response,
67
+ body
68
+ }
69
+ })
70
+ return new window.Response(body, {
71
+ status: response.status,
72
+ statusText: response.statusText,
73
+ headers: response.headers
74
+ })
75
+ },
76
+ afterSyncResponse: ({ request: { method, url, headers }, response }) => {
77
+ log({
78
+ timestamp: new Date().toISOString(),
79
+ channel: 'network',
80
+ type: 'response',
81
+ async: false,
82
+ request: { method, url, headers },
83
+ response: {
84
+ ...response,
85
+ body: response.body.toString('utf8')
86
+ }
87
+ })
88
+ }
89
+ }
90
+ }
91
+ },
92
+ console: {
93
+ error: (...args) => log({
94
+ timestamp: new Date().toISOString(),
95
+ channel: 'console',
96
+ type: 'error',
97
+ message: args.join(' ')
98
+ }),
99
+ warn: (...args) => log({
100
+ timestamp: new Date().toISOString(),
101
+ channel: 'console',
102
+ type: 'warning',
103
+ message: args.join(' ')
104
+ }),
105
+ info: (...args) => log({
106
+ timestamp: new Date().toISOString(),
107
+ channel: 'console',
108
+ type: 'info',
109
+ message: args.join(' ')
110
+ }),
111
+ log: (...args) => log({
112
+ timestamp: new Date().toISOString(),
113
+ channel: 'console',
114
+ type: 'log',
115
+ message: args.join(' ')
116
+ })
117
+ }
118
+ })
119
+ const page = browser.newPage()
120
+ scripts.forEach(script => page.mainFrame.window.eval(script))
121
+ page.goto(url)
122
+ }
123
+ })
@@ -115,8 +115,8 @@ function fixCaseSensitiveSelectors ({ Document }) {
115
115
  const { querySelector, querySelectorAll } = Document.prototype
116
116
  Object.assign(Document.prototype, {
117
117
  querySelector (selectors) {
118
- const result = querySelector.call(this, selectors) || { length: 0 }
119
- if (result.length === 0 && selectors.match(uppercaseTag)) {
118
+ const result = querySelector.call(this, selectors)
119
+ if (result === null && selectors.match(uppercaseTag)) {
120
120
  console.log(JSON.stringify({
121
121
  timestamp: new Date().toISOString(),
122
122
  channel: 'debug',
@@ -23,6 +23,12 @@ require('./browser')({
23
23
  const { JSDOM, VirtualConsole } = jsdom
24
24
 
25
25
  const virtualConsole = new VirtualConsole()
26
+ virtualConsole.on('jsdomError', (...args) => console.log(JSON.stringify({
27
+ timestamp: new Date().toISOString(),
28
+ channel: 'console',
29
+ type: 'jsdomError',
30
+ message: args.join(' ')
31
+ })))
26
32
  virtualConsole.on('error', (...args) => console.log(JSON.stringify({
27
33
  timestamp: new Date().toISOString(),
28
34
  channel: 'console',
@@ -2,7 +2,9 @@
2
2
  <head>
3
3
  <title>ui5-test-runner</title>
4
4
  <link rel="stylesheet" href="/_/report/styles.css">
5
+ <script>window.module = window.module || {}</script>
5
6
  <script src="/_/punyexpr.js"></script>
7
+ <script>window.punyexpr = module.exports.punyexpr</script>
6
8
  <script src="/_/punybind.js"></script>
7
9
  <script src="/_/report/common.js"></script>
8
10
  <script src="/_/report/main.js"></script>
package/src/job.js CHANGED
@@ -150,7 +150,7 @@ function getCommand (cwd) {
150
150
  .option('--start-timeout <timeout>', '[💻🔗] Maximum waiting time for the start command (based on when the first URL becomes available)', timeout, 5000)
151
151
 
152
152
  .option('--end <script>', '[💻🔗] End script (will receive path to `job.js`)', string)
153
- .option('--end-timeout <timeout>', '[💻🔗] Maximum waiting time for the end script', timeout, 5000)
153
+ .option('--end-timeout <timeout>', '[💻🔗] Maximum waiting time for the end script', timeout, 15000)
154
154
 
155
155
  // Specific to legacy (and might be used with url if pointing to local project)
156
156
  .option('--ui5 <url>', '[💻📡] UI5 url', url, 'https://ui5.sap.com')
package/src/npm.js CHANGED
@@ -73,11 +73,41 @@ async function findDependencyPath (job, name) {
73
73
  return [globalPath, justInstalled]
74
74
  }
75
75
 
76
+ const noop = () => {}
77
+
78
+ function getSafeJobAndOutput (nullableJob) {
79
+ if (nullableJob) {
80
+ return { job: nullableJob, output: getOutput(nullableJob) }
81
+ }
82
+ return {
83
+ job: {},
84
+ output: {
85
+ debug: noop,
86
+ resolvedPackage: noop,
87
+ packageNotLatest: noop
88
+ }
89
+ }
90
+ }
91
+
92
+ async function checkLatest (nullableJob, name, installedVersion) {
93
+ const { job, output } = getSafeJobAndOutput(nullableJob)
94
+ if (!job.offline) {
95
+ output.debug('npm', `fetching latest version of package ${name}`)
96
+ const latestVersion = await npm(job, 'view', name, 'version')
97
+ if (latestVersion !== installedVersion) {
98
+ output.packageNotLatest(name, latestVersion)
99
+ }
100
+ } else {
101
+ output.debug('npm', `offline=${job.offline}`)
102
+ }
103
+ }
104
+
76
105
  module.exports = {
77
106
  resolveDependencyPath,
107
+ checkLatest,
78
108
 
79
- async resolvePackage (job, name) {
80
- const output = getOutput(job)
109
+ async resolvePackage (nullableJob, name) {
110
+ const { job, output } = getSafeJobAndOutput(nullableJob)
81
111
  let modulePath
82
112
  let justInstalled = false
83
113
  output.debug('npm', `resolving dependency path of package ${name}...`)
@@ -94,14 +124,10 @@ module.exports = {
94
124
  const installedPackage = require(join(modulePath, 'package.json'))
95
125
  const { version: installedVersion } = installedPackage
96
126
  output.resolvedPackage(name, modulePath, installedVersion)
97
- if (!justInstalled && !job.offline) {
98
- output.debug('npm', `fetching latest version of package ${name}`)
99
- const latestVersion = await npm(job, 'view', name, 'version')
100
- if (latestVersion !== installedVersion) {
101
- output.packageNotLatest(name, latestVersion)
102
- }
127
+ if (!justInstalled) {
128
+ checkLatest(job, name, installedVersion)
103
129
  } else {
104
- output.debug('npm', `justInstalled=${justInstalled} offline=${job.offline}`)
130
+ output.debug('npm', `justInstalled=${justInstalled}`)
105
131
  }
106
132
  return modulePath
107
133
  }
package/src/output.js CHANGED
@@ -15,6 +15,10 @@ const interactive = process.stdout.columns !== undefined && !inJest
15
15
  const $output = Symbol('output')
16
16
  const $outputStart = Symbol('output-start')
17
17
  const $outputProgress = Symbol('output-progress')
18
+ const $logServerIncomingCount = Symbol('log-server-incoming')
19
+ const $logServerRedirectedCount = Symbol('log-server-redirected')
20
+ const $logServerClosedCount = Symbol('log-server-closed')
21
+ const $logServerRequests = Symbol('log-server-requests')
18
22
 
19
23
  if (!interactive) {
20
24
  const UTF8_BOM_CODE = '\ufeff'
@@ -270,6 +274,7 @@ function build (job) {
270
274
  if (job.debugDevMode) {
271
275
  log(job, p80()`⚠️ Development mode ⚠️`)
272
276
  }
277
+ return { name, version }
273
278
  },
274
279
 
275
280
  serving: url => {
@@ -294,8 +299,18 @@ function build (job) {
294
299
  }
295
300
  },
296
301
 
297
- redirected: wrap(({ method, url, statusCode, timeSpent }) => {
298
- if (url.startsWith('/_/progress')) {
302
+ logServerIncoming: wrap(({ id, method, url }) => {
303
+ if (url.startsWith('/_/')) {
304
+ return // avoids pollution
305
+ }
306
+ job[$logServerIncomingCount] = (job[$logServerIncomingCount] || 0) + 1
307
+ job[$logServerRequests] ??= {}
308
+ job[$logServerRequests][id] = { method, url }
309
+ log(job, p80()`🛜 INC ${id.toString(36).toUpperCase().padStart(4, ' ')} ${method.padEnd(7, ' ')} ${pad.lt(url)}`)
310
+ }),
311
+
312
+ logServerRedirected: wrap(({ id, method, url, statusCode, timeSpent }) => {
313
+ if (url.startsWith('/_/')) {
299
314
  return // avoids pollution
300
315
  }
301
316
  let statusText
@@ -304,7 +319,33 @@ function build (job) {
304
319
  } else {
305
320
  statusText = statusCode
306
321
  }
307
- log(job, p80()`${method.padEnd(7, ' ')} ${pad.lt(url)} ${statusText} ${timeSpent.toString().padStart(4, ' ')}ms`)
322
+ job[$logServerRedirectedCount] = (job[$logServerRedirectedCount] || 0) + 1
323
+ const request = job[$logServerRequests][id]
324
+ request.redirected = true
325
+ if (request.closed) {
326
+ delete job[$logServerRequests][id]
327
+ }
328
+ log(job, p80()`🛜 SRV ${id.toString(36).toUpperCase().padStart(4, ' ')} ${method.padEnd(7, ' ')} ${pad.lt(url)} ${statusText} ${timeSpent.toString().padStart(4, ' ')}ms`)
329
+ }),
330
+
331
+ logServerClosed: wrap(({ id, method, url }) => {
332
+ if (url.startsWith('/_/')) {
333
+ return // avoids pollution
334
+ }
335
+ job[$logServerClosedCount] = (job[$logServerClosedCount] || 0) + 1
336
+ const request = job[$logServerRequests][id]
337
+ request.closed = true
338
+ if (request.redirected) {
339
+ delete job[$logServerRequests][id]
340
+ }
341
+ log(job, p80()`🛜 CLS ${id.toString(36).toUpperCase().padStart(4, ' ')} ${method.padEnd(7, ' ')} ${pad.lt(url)}`)
342
+ }),
343
+
344
+ logServerSummary: wrap(() => {
345
+ log(job, p80()`🛜 requests: ${job[$logServerIncomingCount] || 0} incoming, ${job[$logServerRedirectedCount] || 0} redirected, ${job[$logServerClosedCount] || 0} closed.`)
346
+ if (job[$logServerRequests] && Object.keys(job[$logServerRequests]).length) {
347
+ log(job, job[$logServerRequests])
348
+ }
308
349
  }),
309
350
 
310
351
  status (status) {
@@ -384,6 +425,10 @@ function build (job) {
384
425
  wrap(() => log(job, `⚠️ [PKGVRS] latest version of ${name} is ${latestVersion}`))()
385
426
  },
386
427
 
428
+ detectedLeakOfHandles () {
429
+ wrap(() => log(job, '⚠️ [HDLEAK] leaking Node.js handle(s) detected. This may cause issues with the shutdown'))()
430
+ },
431
+
387
432
  browserStart (url) {
388
433
  const text = p80()`${getElapsed(job)} >> ${pad.lt(url)} [${filename(url)}]`
389
434
  if (interactive) {
package/src/report.js CHANGED
@@ -53,9 +53,14 @@ module.exports = {
53
53
  job.end = new Date()
54
54
  job.failed = !!job.failed
55
55
  job.testPageHashes = job.testPageUrls.map(url => filename(url))
56
+ output.debug('report', 'saving job...')
56
57
  await save(job)
58
+ output.debug('report', 'job saved.')
59
+ output.debug('report', 'generating text report...')
57
60
  await generateTextReport(job)
61
+ output.debug('report', 'text report generated.')
58
62
  const promises = job.reportGenerator.map(generator => {
63
+ output.debug('report', 'launching', generator, '...')
59
64
  const { promise, resolve } = allocPromise()
60
65
  const childProcess = fork(
61
66
  generator,
@@ -70,13 +75,16 @@ module.exports = {
70
75
  )
71
76
  const buffers = output.monitor(childProcess, false)
72
77
  childProcess.on('close', exitCode => {
78
+ output.debug('report', generator, 'ended with exit code', exitCode)
73
79
  if (exitCode !== 0) {
74
80
  output.reportGeneratorFailed(generator, exitCode, buffers)
75
81
  }
76
82
  resolve()
83
+ output.debug('report', generator, 'resolved')
77
84
  })
78
85
  return promise
79
86
  })
87
+ output.debug('report', 'generators count:', promises.length)
80
88
  await Promise.all(promises)
81
89
  job.status = 'Reports generated'
82
90
  }