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 +3 -4
- package/jest.config.json +1 -0
- package/package.json +9 -9
- package/src/batch.js +3 -3
- package/src/browsers.js +5 -1
- package/src/clean.js +5 -12
- package/src/cors.js +2 -2
- package/src/coverage.js +1 -1
- package/src/defaults/puppeteer.js +11 -0
- package/src/defaults/report/decompress.js +19 -0
- package/src/defaults/report.js +27 -7
- package/src/defaults/scan-ui5.js +7 -1
- package/src/endpoints.js +32 -0
- package/src/handle.js +43 -0
- package/src/inject/jest2qunit.js +289 -0
- package/src/inject/post.js +42 -7
- package/src/inject/qunit-hooks.js +22 -15
- package/src/job.js +15 -4
- package/src/npm.js +3 -1
- package/src/output.js +55 -3
- package/src/reserve.js +1 -1
- package/src/start.js +58 -20
- package/src/tests.js +50 -8
- package/src/ui5.js +105 -102
|
@@ -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
|
+
})()
|
package/src/inject/post.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
const base = window['ui5-test-runner/base-host'] || ''
|
|
10
|
-
const
|
|
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
|
|
68
|
-
'circular:id': id
|
|
69
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
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()
|
|
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]
|
|
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]
|
|
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
|
|