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.
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.1",
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/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
+ }
@@ -0,0 +1,289 @@
1
+ /* global sinon */
2
+ (() => {
3
+ class Jest2QUnitError extends SyntaxError {
4
+ static throw (reason) {
5
+ throw new Jest2QUnitError(reason)
6
+ }
7
+
8
+ constructor (reason) {
9
+ super('jest2qunit failure : ' + reason)
10
+ this.name = 'Jest2QUnitError'
11
+ }
12
+ }
13
+
14
+ const $raw = Symbol('raw')
15
+ const unproxify = (value) => value && (value[$raw] ?? value)
16
+
17
+ const get = function (target, property) {
18
+ if (target[property] !== undefined) {
19
+ return target[property]
20
+ }
21
+ if (typeof property === 'symbol') {
22
+ return undefined // otherwise returned previously
23
+ }
24
+ if (typeof this[property] === 'function') {
25
+ return this[property](target)
26
+ }
27
+ Jest2QUnitError.throw(`${this._type}.${property} is missing`)
28
+ }
29
+
30
+ let _sinonSandbox
31
+ const jestSpy = (sinonStub) => new Proxy(Object.assign(sinonStub, {
32
+ [$raw]: sinonStub,
33
+ mockImplementation (callback) { sinonStub.callsFake(callback); return this },
34
+ mockImplementationOnce (callback) { sinonStub.onCall(0).callsFake(callback); return this },
35
+ mockResolvedValue (value) { sinonStub.returns(Promise.resolve(value)); return this },
36
+ mockResolvedValueOnce (value) { sinonStub.onCall(0).returns(Promise.resolve(value)); return this },
37
+ mockRejectedValue (value) { sinonStub.returns(Promise.reject(value)); return this },
38
+ mockRejectedValueOnce (value) { sinonStub.onCall(0).returns(Promise.reject(value)); return this },
39
+ mockReturnValue (value) { sinonStub.returns(value); return this },
40
+ mockReturnValueOnce (value) { sinonStub.onCall(0).returns(value); return this },
41
+ mockRestore () { sinonStub.restore() },
42
+ get mock () {
43
+ return new Proxy({}, {
44
+ _type: 'jestSpy.mock',
45
+ get,
46
+ calls () { return sinonStub.args },
47
+ results () { return sinonStub.returnValues.map(value => ({ value })) }
48
+ })
49
+ }
50
+ }), { _type: 'jestSpy', get })
51
+
52
+ const jest = new Proxy({
53
+ fn (impl) {
54
+ const stub = jestSpy(_sinonSandbox.stub())
55
+ if (impl) {
56
+ stub.mockImplementation(impl)
57
+ }
58
+ return stub
59
+ },
60
+ spyOn (object, property) {
61
+ const impl = object[property]
62
+ if (impl[$raw]) { // already looks like a spy
63
+ return impl
64
+ }
65
+ const spy = jestSpy(_sinonSandbox.stub(object, property))
66
+ spy.mockImplementation(impl)
67
+ return spy
68
+ },
69
+ clearAllMocks () { _sinonSandbox.resetHistory() }
70
+ }, { _type: 'jest', get })
71
+
72
+ const stringify = value => value === undefined
73
+ ? 'undefined'
74
+ : typeof value === 'function'
75
+ ? Function.prototype.toString.call(value)
76
+ : value && value instanceof RegExp
77
+ ? value.toString()
78
+ : JSON.stringify(value)
79
+
80
+ const negate = method => method.startsWith('not')
81
+ ? method.charAt(3).toLowerCase() + method.substring(4)
82
+ : 'not' + method.charAt(0).toUpperCase() + method.substring(1)
83
+
84
+ const expectQUnit = params => {
85
+ let { label = 'expect(result)', not, method, assert = 'ok', value, compute, expected, expectedLabel = stringify(expected) } = params
86
+ if (expected) {
87
+ expected = unproxify(expected)
88
+ }
89
+ if (not) {
90
+ assert = negate(assert)
91
+ }
92
+ const message = `${label}${not ? '.not' : ''}.${method}${!method.includes('(') ? '(' + expectedLabel + ')' : ''}`
93
+ const parameters = []
94
+ if ('expected' in params && !['ok', 'notOk'].includes(assert)) {
95
+ parameters.push(expected)
96
+ }
97
+ parameters.push(message)
98
+ return value && value.then
99
+ ? value.then(resolvedValue => QUnit.assert[assert](compute ? compute(resolvedValue) : resolvedValue, ...parameters))
100
+ : QUnit.assert[assert](compute ? compute(value) : value, ...parameters)
101
+ }
102
+
103
+ const expect = (value, label) => {
104
+ value = unproxify(value)
105
+ let not = false
106
+ const proxy = new Proxy({
107
+ toBe: (expected) => expectQUnit({ label, not, method: 'toBe', assert: 'equal', value, expected }),
108
+ toBeDefined: () => expectQUnit({ label, not, method: 'toBeDefined()', assert: 'notStrictEqual', value, expected: undefined }),
109
+ toBeUndefined: () => expectQUnit({ label, not, method: 'toBeUndefined()', assert: 'strictEqual', value, expected: undefined }),
110
+ toBeNull: () => expectQUnit({ label, not, method: 'toBeNull()', assert: 'strictEqual', value, expected: null }),
111
+ toBeNaN: () => expectQUnit({ label, not, method: 'toBeNaN()', value, compute: value => isNaN(value) }),
112
+ toBeTruthy: () => expectQUnit({ label, not, method: 'toBeTruthy()', value }),
113
+ toBeFalsy: () => expectQUnit({ label, not, method: 'toBeFalsy()', value, compute: value => !value }),
114
+ toEqual: (expected) => expectQUnit({ label, not, method: 'toEqual', assert: 'deepEqual', value, expected }),
115
+ toStrictEqual: (expected) => expectQUnit({ label, not, method: 'toStrictEqual', assert: 'deepEqual', value, expected }),
116
+ toBeGreaterThan: (expected) => expectQUnit({ label, not, method: 'toBeGreaterThan', value, compute: value => value > expected, expected }),
117
+ toBeGreaterThanOrEqual: (expected) => expectQUnit({ label, not, method: 'toBeGreaterThanOrEqual', value, compute: value => value >= expected, expected }),
118
+ toBeLessThan: (expected) => expectQUnit({ label, not, method: 'toBeLessThan', value, compute: value => value < expected, expected }),
119
+ toBeLessThanOrEqual: (expected) => expectQUnit({ label, not, method: 'toBeLessThanOrEqual', value, compute: value => value <= expected, expected }),
120
+ toMatch: (expected) => expectQUnit({ label, not, method: 'toMatch', value, compute: value => expected.test(value), expected }),
121
+ toContain: (expected) => expectQUnit({ label, not, method: 'toContain', value, compute: value => value.includes(expected), expected }),
122
+ // TODO: handle
123
+ toThrow: (expected) => expectQUnit({
124
+ label,
125
+ not,
126
+ method: 'toThrow',
127
+ value,
128
+ compute: value => {
129
+ try {
130
+ value()
131
+ return false
132
+ } catch (e) {
133
+ if (typeof expected === 'string') {
134
+ return e.message === expected
135
+ }
136
+ if (expected instanceof RegExp) {
137
+ return expected.test(e.message)
138
+ }
139
+ return true
140
+ }
141
+ },
142
+ expected,
143
+ expectedLabel: typeof expected === 'function' && expected.name ? expected.name : undefined
144
+ }),
145
+ toBeCloseTo: (expected, numDigits = 2) => {
146
+ const factor = 10 ** numDigits
147
+ const round = (x) => Math.floor(x * factor) / factor
148
+ return expectQUnit({ label, not, method: 'toBeCloseTo', assert: 'strictEqual', value: round(value), expected: round(expected) })
149
+ },
150
+ // Not async
151
+ toHaveBeenCalled: () => expectQUnit({ label, not, method: 'toHaveBeenCalled()', value, compute: value => value.called }),
152
+ toHaveBeenCalledTimes: (n) => expectQUnit({ label, not, method: 'toHaveBeenCalledTimes', assert: 'strictEqual', value, compute: value => value.callCount, expected: n }),
153
+ toHaveBeenCalledWith: (...args) => expectQUnit({ label, not, method: `toHaveBeenCalledWith(${args.map(stringify).join(', ')})`, value, compute: value => value.calledWith(...args) })
154
+ }, {
155
+ _type: 'expect',
156
+ get,
157
+ not () {
158
+ not = !not
159
+ return proxy
160
+ },
161
+ resolves () {
162
+ if (not) {
163
+ Jest2QUnitError.throw('expect.resolves cannot be negated')
164
+ }
165
+ if (typeof value.then !== 'function') {
166
+ Jest2QUnitError.throw('expected value must be thenable')
167
+ }
168
+ return expect(value.then(value => value, reason => QUnit.assert.ok(false, reason)), 'expect(result).resolves')
169
+ },
170
+ rejects () {
171
+ if (not) {
172
+ Jest2QUnitError.throw('expect.resolves cannot be negated')
173
+ }
174
+ if (typeof value.then !== 'function') {
175
+ Jest2QUnitError.throw('expected value must be thenable')
176
+ }
177
+ return expect(value.then(() => QUnit.assert.ok(false, 'Promise should not be fulfilled'), reason => reason), 'expect(result).rejects')
178
+ }
179
+ })
180
+ return proxy
181
+ }
182
+
183
+ const rootDescribe = {}
184
+ let currentDescribe = rootDescribe
185
+
186
+ const bddApi = (type, data) => {
187
+ if (!currentDescribe[type]) {
188
+ currentDescribe[type] = []
189
+ }
190
+ currentDescribe[type].push(data)
191
+ }
192
+
193
+ const beforeAll = (callback) => bddApi('beforeAll', callback)
194
+ const before = (callback) => bddApi('beforeAll', callback)
195
+ const beforeEach = (callback) => bddApi('beforeEach', callback)
196
+ const afterEach = (callback) => bddApi('afterEach', callback)
197
+ const afterAll = (callback) => bddApi('afterAll', callback)
198
+ const after = (callback) => bddApi('afterAll', callback)
199
+ const it = (label, callback) => bddApi('it', { label, callback })
200
+ it.only = (label, callback) => bddApi('it', { label, callback, only: true })
201
+ it.skip = (label, callback) => bddApi('it', { label, callback, skip: true })
202
+ it.todo = (label, callback) => bddApi('it', { label, callback, todo: true })
203
+ const test = (label, callback) => bddApi('it', { label, callback })
204
+ test.only = (label, callback) => bddApi('it', { label, callback, only: true })
205
+ test.skip = (label, callback) => bddApi('it', { label, callback, skip: true })
206
+ test.todo = (label, callback) => bddApi('it', { label, callback, todo: true })
207
+
208
+ const $alreadyConvertedToQUnit = Symbol('alreadyConvertedToQUnit')
209
+ const toQUnit = (describe) => {
210
+ if (describe[$alreadyConvertedToQUnit]) {
211
+ return
212
+ }
213
+ describe[$alreadyConvertedToQUnit] = true
214
+ QUnit.module(describe.label ?? '(root)', function (hooks) {
215
+ if (describe.beforeAll) {
216
+ hooks.before(async () => Promise.all(describe.beforeAll.map(callback => callback())))
217
+ }
218
+ if (describe.beforeEach) {
219
+ hooks.beforeEach(async () => Promise.all(describe.beforeEach.map(callback => callback())))
220
+ }
221
+ if (describe.afterEach) {
222
+ hooks.afterEach(async () => Promise.all(describe.afterEach.map(callback => callback())))
223
+ }
224
+ if (describe.afterAll) {
225
+ hooks.after(async () => Promise.all(describe.afterAll.map(callback => callback())))
226
+ }
227
+ if (describe.it) {
228
+ for (const { label, callback, skip, todo, only } of describe.it) {
229
+ if (todo) {
230
+ QUnit.test('[todo] ' + label, (assert) => assert.expect(0))
231
+ } else if (skip) {
232
+ QUnit.test('[skip] ' + label, (assert) => assert.expect(0))
233
+ } else if (only) {
234
+ QUnit.only(label, callback)
235
+ } else {
236
+ QUnit.test(label, callback)
237
+ }
238
+ }
239
+ }
240
+ if (describe.describe) {
241
+ for (const subDescribe of describe.describe) {
242
+ toQUnit(subDescribe)
243
+ }
244
+ }
245
+ })
246
+ }
247
+
248
+ const describe = (label, callback) => {
249
+ QUnit.config.reorder = false // By default in Jest
250
+ if (typeof sinon !== 'object') {
251
+ Jest2QUnitError.throw('sinon is missing')
252
+ }
253
+ if (sinon.createSandbox === undefined) {
254
+ Jest2QUnitError.throw('sinon 4 is expected')
255
+ }
256
+ _sinonSandbox = sinon.createSandbox()
257
+
258
+ const parentDescribe = currentDescribe
259
+ if (!parentDescribe.describe) {
260
+ parentDescribe.describe = []
261
+ }
262
+ currentDescribe = {
263
+ label
264
+ }
265
+ parentDescribe.describe.push(currentDescribe)
266
+
267
+ callback()
268
+
269
+ currentDescribe = parentDescribe
270
+ if (currentDescribe === rootDescribe) {
271
+ rootDescribe[$alreadyConvertedToQUnit] = false
272
+ toQUnit(rootDescribe)
273
+ }
274
+ }
275
+
276
+ Object.assign(globalThis, {
277
+ jest,
278
+ expect,
279
+ beforeAll,
280
+ before,
281
+ beforeEach,
282
+ afterEach,
283
+ afterAll,
284
+ after,
285
+ it,
286
+ test,
287
+ describe
288
+ })
289
+ })()