ui5-test-runner 5.9.0 → 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.
package/index.js CHANGED
@@ -99,10 +99,9 @@ async function main () {
99
99
  output.serving(url)
100
100
  serverReady()
101
101
  })
102
- .on('error', error => {
103
- output.serverError(error)
104
- send({ msg: 'error', error })
105
- serverError()
102
+ .on('error', ({ reason }) => {
103
+ send({ msg: 'error', reason })
104
+ serverError(reason)
106
105
  })
107
106
  await serverStarted
108
107
  if (job.preload) {
package/jest.config.json CHANGED
@@ -14,6 +14,7 @@
14
14
  "coveragePathIgnorePatterns": [
15
15
  "\\.spec\\.js",
16
16
  "output\\.js",
17
+ "handle\\.js",
17
18
  "b\\capabilities\\b"
18
19
  ],
19
20
  "coverageThreshold": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "5.9.0",
3
+ "version": "5.10.0",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -46,24 +46,24 @@
46
46
  "homepage": "https://github.com/ArnaudBuchholz/ui5-test-runner#readme",
47
47
  "dependencies": {
48
48
  "commander": "^12.1.0",
49
- "ps-tree": "^1.2.0",
49
+ "pidtree": "^0.6.0",
50
50
  "punybind": "^1.2.1",
51
- "punyexpr": "1.1.1",
51
+ "punyexpr": "1.2.0",
52
52
  "reserve": "2.3.3"
53
53
  },
54
54
  "devDependencies": {
55
- "@openui5/types": "^1.136.1",
56
- "@ui5/cli": "^4.0.19",
55
+ "@openui5/types": "^1.140.0",
56
+ "@ui5/cli": "^4.0.26",
57
57
  "@ui5/middleware-code-coverage": "^2.0.1",
58
58
  "dotenv": "^16.5.0",
59
59
  "jest": "^29.7.0",
60
- "nock": "^14.0.5",
61
- "npm-check-updates": "^18.0.1",
60
+ "nock": "^14.0.10",
61
+ "npm-check-updates": "^18.3.0",
62
62
  "nyc": "^17.1.0",
63
63
  "rimraf": "^6.0.1",
64
64
  "standard": "^17.1.2",
65
- "typescript": "^5.8.3",
66
- "ui5-tooling-transpile": "^3.8.0"
65
+ "typescript": "^5.9.2",
66
+ "ui5-tooling-transpile": "^3.9.2"
67
67
  },
68
68
  "optionalDependencies": {
69
69
  "fsevents": "^2.3.3"
package/src/batch.js CHANGED
@@ -70,10 +70,10 @@ const task = async function (batchItem) {
70
70
  .filter(name => job[$valueSources][name] === 'cli')
71
71
  .forEach(name => {
72
72
  const longName = toLongName(name)
73
- const option = batchParameters[longName]
73
+ const option = batchParameters[longName] || batchParameters['no-' + longName]
74
74
  if (option) {
75
- parameters.push(`--${longName}`)
76
- if (!option.optional && !option.negate) {
75
+ parameters.push(option.long)
76
+ if (!option.negate) {
77
77
  if (name === 'env') {
78
78
  Object.keys(job.env).forEach(key => {
79
79
  parameters.push(`${key}=${job.env[key]}`)
package/src/browsers.js CHANGED
@@ -14,8 +14,9 @@ let lastScreenshotId = 0
14
14
  const screenshots = {}
15
15
 
16
16
  async function instantiate (job, config) {
17
+ const output = getOutput(job)
17
18
  if (job.browserArgs.some((arg) => !arg.trim())) {
18
- getOutput(job).emptyBrowserArg()
19
+ output.emptyBrowserArg()
19
20
  job.browserArgs = job.browserArgs.filter((arg) => arg.trim())
20
21
  }
21
22
  const { dir, url } = config
@@ -48,6 +49,9 @@ async function instantiate (job, config) {
48
49
  }
49
50
  resolve(code)
50
51
  })
52
+ childProcess.on('error', err => {
53
+ output.browserChildProcessError(url, err)
54
+ })
51
55
  childProcess.closed = promise
52
56
  childProcess.stdoutFilename = stdoutFilename
53
57
  childProcess.stderrFilename = stderrFilename
package/src/clean.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { getOutput } = require('./output')
2
+ const { describeHandle } = require('./handle')
2
3
 
3
4
  module.exports = {
4
5
  cleanHandles (job) {
@@ -6,22 +7,14 @@ module.exports = {
6
7
  const activeHandles = process._getActiveHandles ? process._getActiveHandles() : []
7
8
  let displayWarning = true
8
9
  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') {
10
+ const { className, label } = describeHandle(handle)
11
+ output.debug('handle', 'active handle', label)
12
+ if (className === 'TLSSocket' || className === 'Socket') {
12
13
  if (displayWarning) {
13
14
  displayWarning = false
14
15
  output.detectedLeakOfHandles()
15
16
  }
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)
17
+ output.debug('handle', className, label)
25
18
  handle.destroy()
26
19
  }
27
20
  }
package/src/cors.js CHANGED
@@ -7,8 +7,8 @@ module.exports = {
7
7
  response.writeHead(200, {
8
8
  'Access-Control-Allow-Origin': origin,
9
9
  Vary: 'Origin',
10
- 'Access-Control-Allow-Headers': 'content-type, content-length, x-page-url',
11
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
10
+ 'Access-Control-Allow-Headers': '*',
11
+ 'Access-Control-Allow-Methods': '*',
12
12
  'Access-Control-Allow-Credentials': 'true'
13
13
  })
14
14
  response.end()
package/src/coverage.js CHANGED
@@ -169,7 +169,7 @@ async function buildAllIndex (job) {
169
169
  await writeFile(join(job.coverageTempDir, 'all-index.json'), `{${index.join(',')}}`)
170
170
  }
171
171
  } catch (e) {
172
- output.genericError(e)
172
+ output.genericError(e, e.url)
173
173
  output.noInfoForAllCoverage()
174
174
  } finally {
175
175
  progress.done()
@@ -3,6 +3,8 @@
3
3
  let browser
4
4
  let page
5
5
 
6
+ console.time('⏲ run ')
7
+
6
8
  require('./browser')({
7
9
  metadata: {
8
10
  name: 'puppeteer',
@@ -52,7 +54,10 @@ require('./browser')({
52
54
  consoleWriter,
53
55
  networkWriter
54
56
  }) {
57
+ console.timeEnd('⏲ run ')
58
+ console.time('⏲ require ')
55
59
  const puppeteer = require(modules.puppeteer)
60
+ console.timeEnd('⏲ require ')
56
61
 
57
62
  let args = []
58
63
  let product
@@ -62,6 +67,7 @@ require('./browser')({
62
67
  args = options.chromeArgs()
63
68
  }
64
69
 
70
+ console.time('⏲ launch ')
65
71
  browser = await puppeteer.launch({
66
72
  product,
67
73
  executablePath: options.binary,
@@ -69,6 +75,7 @@ require('./browser')({
69
75
  defaultViewport: null,
70
76
  args
71
77
  })
78
+ console.timeEnd('⏲ launch ')
72
79
 
73
80
  page = (await browser.pages())[0]
74
81
 
@@ -94,14 +101,18 @@ require('./browser')({
94
101
  })
95
102
  })
96
103
 
104
+ console.time('⏲ scripts ')
97
105
  if (scripts && scripts.length) {
98
106
  for (const script of scripts) {
99
107
  await page.evaluateOnNewDocument(script)
100
108
  }
101
109
  }
110
+ console.timeEnd('⏲ scripts ')
102
111
 
103
112
  await page.setDefaultNavigationTimeout(0)
113
+ console.time('⏲ navigate ')
104
114
  await page.goto(url)
115
+ console.timeEnd('⏲ navigate ')
105
116
  },
106
117
 
107
118
  async error ({ error: e, exit }) {
@@ -0,0 +1,19 @@
1
+ /* global DecompressionStream */
2
+ /* eslint-disable-next-line no-unused-vars */
3
+ async function decompress (base64) {
4
+ const bin = atob(base64)
5
+ const uint8Array = new Uint8Array(bin.length)
6
+ for (let i = 0; i < bin.length; ++i) {
7
+ uint8Array[i] = bin.charCodeAt(i)
8
+ }
9
+ const readableStream = new ReadableStream({
10
+ start (ctl) {
11
+ ctl.enqueue(uint8Array)
12
+ ctl.close()
13
+ }
14
+ })
15
+ const decompressionStream = new DecompressionStream('gzip')
16
+ const decompressedStream = readableStream.pipeThrough(decompressionStream)
17
+ const jsonString = await new Response(decompressedStream).text()
18
+ return JSON.parse(jsonString)
19
+ }
@@ -1,10 +1,16 @@
1
1
  'use strict'
2
2
 
3
- const { join } = require('path')
3
+ const { join, isAbsolute } = require('path')
4
4
  const { readFile, writeFile } = require('fs').promises
5
+ const zlib = require('zlib')
5
6
  const [,, reportDir] = process.argv
7
+ const verbose = process.argv.includes('--verbose')
6
8
  const { resolveDependencyPath } = require('../npm.js')
7
9
 
10
+ const log = verbose ? console.log : () => {}
11
+
12
+ log('🏗 Building report...')
13
+
8
14
  const defaultDir = join(__dirname, 'report')
9
15
 
10
16
  async function readDefault (name) {
@@ -35,30 +41,44 @@ function minifyJs (src) {
35
41
 
36
42
  async function main () {
37
43
  const html = await readDefault('default.html')
44
+ log('📦 default.html :', html.length)
38
45
  const styles = (await readDefault('styles.css'))
39
46
  .replace(/\{\r?\n\s+/g, '{')
40
47
  .replace(/\}(\r?\n)+/g, '} ')
41
48
  .replace(/;\r?\n\s*/g, ';')
42
49
  .replace(/(:|,)\s*/g, (_, c) => c)
50
+ log('📦 styles.css :', styles.length)
43
51
 
44
52
  const punyexpr = await readDependency('punyexpr')
53
+ log('📦 punyexpr :', punyexpr.length)
45
54
  const punybind = await readDependency('punybind')
55
+ log('📦 punybind :', punybind.length)
46
56
  const common = minifyJs(await readDefault('common.js'))
57
+ log('📦 common :', common.length)
47
58
  const main = minifyJs(await readDefault('main.js'))
59
+ log('📦 main :', main.length)
48
60
 
49
- const job = (await readFile(join(reportDir, 'job.js'))).toString()
50
- .replace(/(\{|,|\[)\r?\n\s*/g, (_, c) => c)
51
- .replace(/\r?\n\s*(\}|\])/g, (_, c) => c)
52
- .replace(/": "/g, '":"')
61
+ const decompress = minifyJs(await readDefault('decompress.js'))
62
+ log('📦 decompress :', decompress.length)
63
+ const jobPath = isAbsolute(reportDir) ? reportDir : join(process.cwd(), reportDir)
64
+ log('📦 job path :', jobPath)
65
+ const rawJob = require(join(jobPath, 'job.js'))
66
+ const json = JSON.stringify(rawJob)
67
+ log('📦 json :', json.length)
68
+ const buffer = zlib.gzipSync(json)
69
+ const base64 = buffer.toString('base64')
70
+ log('📦 json (Gzip/b64) :', base64.length)
71
+ log('📦 compression :', (100 * base64.length / json.length).toFixed(2) + '%')
53
72
 
54
- return await writeFile(join(reportDir, 'report.html'), html
73
+ await writeFile(join(reportDir, 'report.html'), html
55
74
  .replace(/(>|\}\})\r?\n\s*</g, (_, c) => `${c}<`)
56
75
  .replace('<link rel="stylesheet" href="/_/report/styles.css">', `<style>${styles}</style>`)
57
76
  .replace('<script src="/_/punyexpr.js"></script>', `<script>${punyexpr}</script>`)
58
77
  .replace('<script src="/_/punybind.js"></script>', `<script>${punybind}</script>`)
59
78
  .replace('<script src="/_/report/common.js"></script>', `<script>${common}</script>`)
60
- .replace('<script src="/_/report/main.js"></script>', `<script>const module={};${job};const job=module.exports;${main}</script>`)
79
+ .replace('<script src="/_/report/main.js"></script>', `<script>${decompress};let job={};decompress("${base64}").then(json=>{job=json;report.ready.then(update=>update({...json,elapsed:report.elapsed}))});${main}</script>`)
61
80
  )
81
+ log('✅ generated.')
62
82
  }
63
83
 
64
84
  main()
@@ -2,7 +2,13 @@ module.exports = async function scanUI5 (url, onFolder, onFile) {
2
2
  if (url.match(/\/((?:test-)?resources\/.*)/)) {
3
3
  return // ignore UI5 resources
4
4
  }
5
- const html = await (await fetch(url)).text()
5
+ let html
6
+ try {
7
+ html = await (await fetch(url)).text()
8
+ } catch (e) {
9
+ e.url = url
10
+ throw e
11
+ }
6
12
  const items = [...html.matchAll(/<a href="([^"]+)" class="icon/ig)]
7
13
  .map(([_, item]) => item)
8
14
  .filter(item => item.endsWith('/') || item.endsWith('.js') || item.endsWith('.ts'))
package/src/endpoints.js CHANGED
@@ -123,6 +123,38 @@ module.exports = job => {
123
123
  match: '^/_/QUnit/done',
124
124
  custom: endpoint('QUnit/done', async (url, report) => done(job, url, report))
125
125
  }, {
126
+ // Fast batch endpoint for QUnit (no screenshot)
127
+ match: '^/_/QUnit/batch',
128
+ custom: async (request, response) => {
129
+ const url = extractPageUrl(request.headers)
130
+ const data = await body(request)
131
+ response.writeHead(200)
132
+ response.end()
133
+ try {
134
+ for (const [hook, hookData] of data) {
135
+ switch (hook) {
136
+ case 'QUnit/begin':
137
+ await begin(job, url, hookData)
138
+ break
139
+ case 'QUnit/testStart':
140
+ await testStart(job, url, hookData)
141
+ break
142
+ case 'QUnit/log':
143
+ await log(job, url, hookData)
144
+ break
145
+ case 'QUnit/testDone':
146
+ await testDone(job, url, hookData)
147
+ break
148
+ case 'QUnit/done':
149
+ await done(job, url, hookData)
150
+ break
151
+ }
152
+ }
153
+ } catch (error) {
154
+ getOutput(job).endpointError({ api: 'QUnit/batch', url, data, error })
155
+ }
156
+ }
157
+ }, {
126
158
  // UI to follow progress
127
159
  match: '^/_/progress.html',
128
160
  cwd: dirname(job.progressPage),
package/src/handle.js ADDED
@@ -0,0 +1,43 @@
1
+ const { ServerResponse, ClientRequest } = require('node:http')
2
+
3
+ module.exports = {
4
+ describeHandle (handle) {
5
+ const className = handle && handle.constructor && handle.constructor.name
6
+ let label = className
7
+ if (['TLSSocket', 'Socket'].includes(className)) {
8
+ if (handle._httpMessage instanceof ServerResponse) {
9
+ const { method, url } = handle._httpMessage.req
10
+ label += ` IncomingRequest ${method} ${url}`
11
+ } else if (handle._httpMessage instanceof ClientRequest) {
12
+ const { path, method, host, protocol } = handle._httpMessage
13
+ label += ` ClientRequest ${method} ${protocol}//${host}${path}`
14
+ } else if (handle.localAddress) {
15
+ const { localAddress, localPort, remoteAddress, remotePort } = handle
16
+ if (remoteAddress === undefined) {
17
+ label += ` local ${localAddress}:${localPort}`
18
+ } else {
19
+ label += ` local ${localAddress}:${localPort} remote ${remoteAddress}:${remotePort}`
20
+ }
21
+ } else if (handle._handle) {
22
+ const underlyingHandle = handle._handle
23
+ const underlyingClassName = underlyingHandle && underlyingHandle.constructor && underlyingHandle.constructor.name
24
+ label += ` <-> ${underlyingClassName || 'unknown'}`
25
+ } else {
26
+ label += ' unknown'
27
+ }
28
+ } else if (className === 'WriteStream') {
29
+ const fd = ['stdin', 'stdout', 'stderr'][handle.fd] || `fd: ${handle.fd}`
30
+ label += ` ${fd} ${handle.columns}x${handle.rows} isTTY: ${handle.isTTY}`
31
+ } else if (className === 'Server') {
32
+ label += ` connections: ${handle._connections} events: ${handle._eventsCount}`
33
+ } else if (className === 'ChildProcess') {
34
+ label += ` pid: ${handle.pid}`
35
+ if (handle.spawnargs) {
36
+ label += ` ${handle.spawnargs.map(value => ('' + value).replaceAll(/ /g, '␣'))}`
37
+ } else {
38
+ label += ' unknown'
39
+ }
40
+ }
41
+ return { className, label }
42
+ }
43
+ }