ui5-test-runner 1.1.5 → 2.0.1

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.
Files changed (93) hide show
  1. package/README.md +32 -188
  2. package/index.js +47 -16
  3. package/package.json +28 -10
  4. package/src/add-test-pages.js +35 -0
  5. package/src/add-test-pages.spec.js +95 -0
  6. package/src/browser.spec.js +724 -0
  7. package/src/browsers.js +220 -59
  8. package/src/capabilities/index.js +194 -0
  9. package/src/capabilities/tests/basic/iframe.html +8 -0
  10. package/src/capabilities/tests/basic/index.html +12 -0
  11. package/src/capabilities/tests/basic/index.js +20 -0
  12. package/src/capabilities/tests/basic/ui5.html +24 -0
  13. package/src/capabilities/tests/dynamic-include/index.js +21 -0
  14. package/src/capabilities/tests/dynamic-include/mix.html +11 -0
  15. package/src/capabilities/tests/dynamic-include/one.html +11 -0
  16. package/src/capabilities/tests/dynamic-include/post.js +3 -0
  17. package/src/capabilities/tests/dynamic-include/test.js +1 -0
  18. package/src/capabilities/tests/dynamic-include/two.html +11 -0
  19. package/src/capabilities/tests/index.js +16 -0
  20. package/src/capabilities/tests/local-storage/index.html +16 -0
  21. package/src/capabilities/tests/local-storage/index.js +21 -0
  22. package/src/capabilities/tests/screenshot/index.html +13 -0
  23. package/src/capabilities/tests/screenshot/index.js +18 -0
  24. package/src/capabilities/tests/scripts/index.js +50 -0
  25. package/src/capabilities/tests/scripts/qunit.html +22 -0
  26. package/src/capabilities/tests/scripts/testsuite.html +10 -0
  27. package/src/capabilities/tests/scripts/testsuite.js +8 -0
  28. package/src/capabilities/tests/timeout/index.html +21 -0
  29. package/src/capabilities/tests/timeout/index.js +19 -0
  30. package/src/capabilities/tests/traces/index.html +18 -0
  31. package/src/capabilities/tests/traces/index.js +81 -0
  32. package/src/cors.js +1 -1
  33. package/src/cors.spec.js +41 -0
  34. package/src/coverage.js +30 -18
  35. package/src/coverage.spec.js +79 -0
  36. package/src/csv-reader.js +36 -0
  37. package/src/csv-reader.spec.js +42 -0
  38. package/src/csv-writer.js +52 -0
  39. package/src/csv-writer.spec.js +77 -0
  40. package/src/defaults/browser.js +144 -0
  41. package/src/defaults/jsdom/compatibility.js +95 -0
  42. package/src/defaults/jsdom/debug.js +23 -0
  43. package/src/defaults/jsdom/resource-loader.js +43 -0
  44. package/src/defaults/jsdom/sap.ui.test.matchers.visible.js +39 -0
  45. package/src/defaults/jsdom.js +64 -0
  46. package/src/defaults/junit-xml-report.js +64 -0
  47. package/src/defaults/puppeteer.js +111 -0
  48. package/src/defaults/report/common.js +38 -0
  49. package/src/defaults/report/default.html +84 -0
  50. package/src/defaults/report/main.js +44 -0
  51. package/src/defaults/report/progress.js +49 -0
  52. package/src/defaults/report/styles.css +66 -0
  53. package/src/defaults/report.js +69 -0
  54. package/src/defaults/selenium-webdriver/chrome.js +38 -0
  55. package/src/defaults/selenium-webdriver/edge.js +25 -0
  56. package/src/defaults/selenium-webdriver/firefox.js +31 -0
  57. package/src/defaults/selenium-webdriver.js +138 -0
  58. package/src/endpoints.js +70 -124
  59. package/src/error.js +52 -0
  60. package/src/error.spec.js +17 -0
  61. package/src/get-job-progress.js +69 -0
  62. package/src/get-job-progress.spec.js +175 -0
  63. package/src/inject/post.js +96 -0
  64. package/src/inject/post.spec.js +147 -0
  65. package/src/inject/qunit-hooks.js +6 -21
  66. package/src/inject/qunit-intercept.js +30 -0
  67. package/src/inject/qunit-redirect.js +15 -7
  68. package/src/job-mode.js +45 -0
  69. package/src/job.js +254 -108
  70. package/src/job.spec.js +413 -0
  71. package/src/npm.js +73 -0
  72. package/src/npm.spec.js +98 -0
  73. package/src/options.js +73 -0
  74. package/src/options.spec.js +125 -0
  75. package/src/output.js +450 -131
  76. package/src/qunit-hooks.js +116 -0
  77. package/src/qunit-hooks.spec.js +687 -0
  78. package/src/report.js +47 -0
  79. package/src/reserve.js +3 -4
  80. package/src/simulate.spec.js +466 -0
  81. package/src/symbols.js +8 -0
  82. package/src/tests.js +127 -84
  83. package/src/timeout.spec.js +39 -0
  84. package/src/tools.js +111 -4
  85. package/src/tools.spec.js +90 -0
  86. package/src/ui5.js +3 -3
  87. package/src/unhandled.js +6 -6
  88. package/src/unhandled.spec.js +63 -0
  89. package/defaults/chromium.js +0 -62
  90. package/src/progress.html +0 -71
  91. package/src/proxies.js +0 -8
  92. package/src/report.html +0 -202
  93. /package/{defaults → src/defaults}/nyc.json +0 -0
@@ -0,0 +1,413 @@
1
+ const { dirname, join } = require('path')
2
+ const { fromObject, fromCmdLine } = require('./job')
3
+ const normalizePath = path => path.replace(/\\/g, '/') // win -> unix
4
+ const { $valueSources } = require('./symbols')
5
+ const { UTRError } = require('./error')
6
+
7
+ const cwd = join(__dirname, '../test/project')
8
+
9
+ function buildJob (parameters) {
10
+ return fromObject(cwd, parameters)
11
+ }
12
+
13
+ describe('job', () => {
14
+ describe('parameter parsing', () => {
15
+ it('provides default values', () => {
16
+ const job = buildJob({})
17
+ expect(job.cwd).toStrictEqual(cwd)
18
+ expect(job.port).toStrictEqual(0)
19
+ expect(job.ui5).toStrictEqual('https://ui5.sap.com')
20
+ expect(job.browser.startsWith(dirname(dirname(__dirname)))).toStrictEqual(true)
21
+ expect(normalizePath(job.browser).endsWith('defaults/puppeteer.js')).toStrictEqual(true)
22
+ expect(normalizePath(job.webapp).endsWith('/test/project/webapp')).toStrictEqual(true)
23
+ expect(job.keepAlive).toStrictEqual(false)
24
+ expect(job.screenshot).toStrictEqual(true)
25
+ expect(job[$valueSources]).toMatchObject({
26
+ cwd: 'default',
27
+ port: 'default',
28
+ ui5: 'default',
29
+ browser: 'default'
30
+ })
31
+ })
32
+
33
+ it('parses parameters', () => {
34
+ const job = buildJob({
35
+ cwd: '../project2',
36
+ port: 8080,
37
+ keepAlive: null,
38
+ ui5: 'http://localhost:8088/ui5'
39
+ })
40
+ expect(normalizePath(job.cwd).endsWith('/test/project2')).toStrictEqual(true)
41
+ expect(job.port).toStrictEqual(8080)
42
+ expect(job.keepAlive).toStrictEqual(true)
43
+ expect(job.ui5).toStrictEqual('http://localhost:8088/ui5')
44
+ expect(normalizePath(job.webapp).endsWith('/test/project2/webapp')).toStrictEqual(true)
45
+ expect(job[$valueSources]).toMatchObject({
46
+ cwd: 'cli',
47
+ port: 'cli',
48
+ keepAlive: 'cli',
49
+ ui5: 'cli',
50
+ browser: 'default'
51
+ })
52
+ })
53
+
54
+ describe('complex parameter parsing', () => {
55
+ it('implements boolean flag', () => {
56
+ const job = buildJob({
57
+ keepAlive: false,
58
+ coverage: null,
59
+ logServer: null
60
+ })
61
+ expect(job.keepAlive).toStrictEqual(false)
62
+ expect(job.coverage).toStrictEqual(true)
63
+ expect(job.logServer).toStrictEqual(true)
64
+ })
65
+
66
+ it('implements boolean switch off', () => {
67
+ const job = buildJob({
68
+ noCoverage: null,
69
+ noScreenshot: null
70
+ })
71
+ expect(job.coverage).toStrictEqual(false)
72
+ expect(job.screenshot).toStrictEqual(false)
73
+ })
74
+
75
+ it('url disables webapp checking and coverage', () => {
76
+ const job = buildJob({
77
+ webapp: 'not_a_folder',
78
+ url: 'http://localhost:8080'
79
+ })
80
+ expect(job.url).toStrictEqual(['http://localhost:8080'])
81
+ expect(job.coverage).toStrictEqual(false)
82
+ })
83
+
84
+ it('url still allows coverage', () => {
85
+ const job = buildJob({
86
+ webapp: 'not_a_folder',
87
+ url: 'http://localhost:8080',
88
+ coverage: true
89
+ })
90
+ expect(job.url).toStrictEqual(['http://localhost:8080'])
91
+ expect(job.coverage).toStrictEqual(true)
92
+ })
93
+
94
+ describe('multi values', () => {
95
+ const absoluteLibPath = join(__dirname, '../test/project/webapp/lib')
96
+
97
+ describe('url', () => {
98
+ it('accepts multiple urls', () => {
99
+ const job = buildJob({
100
+ url: [
101
+ 'http://localhost:8080/page1.html',
102
+ 'http://localhost:8080/page2.html'
103
+ ]
104
+ })
105
+ expect(job.url).toMatchObject([
106
+ 'http://localhost:8080/page1.html',
107
+ 'http://localhost:8080/page2.html'
108
+ ])
109
+ })
110
+ })
111
+
112
+ describe('libs', () => {
113
+ it('accepts one library', () => {
114
+ const job = buildJob({
115
+ libs: [absoluteLibPath]
116
+ })
117
+ expect(job.libs).toMatchObject([{
118
+ relative: '',
119
+ source: absoluteLibPath
120
+ }])
121
+ })
122
+
123
+ it('accepts two libraries', () => {
124
+ const project2Path = join(__dirname, '../test/project2')
125
+ const job = buildJob({
126
+ libs: [absoluteLibPath, 'project2/=../project2']
127
+ })
128
+ expect(job.libs).toMatchObject([{
129
+ relative: '',
130
+ source: absoluteLibPath
131
+ }, {
132
+ relative: 'project2/',
133
+ source: project2Path
134
+ }])
135
+ })
136
+ })
137
+
138
+ describe('browser parameters', () => {
139
+ it('allows passing extra parameter', () => {
140
+ const job = buildJob({
141
+ '--': ['--visible']
142
+ })
143
+ expect(job.browserArgs).toEqual(['--visible'])
144
+ })
145
+
146
+ it('allows passing extra parameters', () => {
147
+ const job = buildJob({
148
+ '--': ['--visible', '--verbose']
149
+ })
150
+ expect(job.browserArgs).toEqual(['--visible', '--verbose'])
151
+ })
152
+ })
153
+ })
154
+
155
+ describe('parameters using $/', () => {
156
+ const job = buildJob({
157
+ cwd,
158
+ browser: '$/selenium-webdriver.js'
159
+ })
160
+ expect(job.browser).toStrictEqual(join(__dirname, './defaults/selenium-webdriver.js'))
161
+ expect(job.coverageSettings).toStrictEqual(join(__dirname, './defaults/nyc.json'))
162
+ expect(job.reportGenerator).toEqual([join(__dirname, './defaults/report.js')])
163
+ })
164
+
165
+ describe('custom mappings', () => {
166
+ it('offers custom mappings', () => {
167
+ const job = buildJob({
168
+ cwd,
169
+ mappings: [
170
+ '^/otherlib/(.+)=file(./otherfolder/otherlib/$1)',
171
+ '^/ui/oDataService/v1/odata/v4/ServiceName/(.+)=url(http://localhost:18082/odata/v4/ServiceName/$1)'
172
+ ]
173
+ })
174
+ expect(job.mappings).toEqual([
175
+ {
176
+ match: '^/otherlib/(.+)',
177
+ file: './otherfolder/otherlib/$1'
178
+ }, {
179
+ match: '^/ui/oDataService/v1/odata/v4/ServiceName/(.+)',
180
+ url: 'http://localhost:18082/odata/v4/ServiceName/$1'
181
+ }
182
+ ])
183
+ })
184
+
185
+ it('rejects invalid mapping', () => {
186
+ expect(() => buildJob({
187
+ cwd,
188
+ mappings: [
189
+ '^/otherlib/(.+)=custom(./otherfolder/otherlib/$1)'
190
+ ]
191
+ })).toThrowError()
192
+ })
193
+ })
194
+ })
195
+ })
196
+
197
+ describe('validation', () => {
198
+ it('fails on negative integers', () => {
199
+ expect(() => buildJob({
200
+ port: -1
201
+ })).toThrow()
202
+ })
203
+
204
+ it('fails on invalid URL', () => {
205
+ expect(() => buildJob({
206
+ ui5: 'not_an_url'
207
+ })).toThrow()
208
+ })
209
+
210
+ it('fails on a missing file (does not exist)', () => {
211
+ expect(() => buildJob({
212
+ testsuite: 'not_a_file'
213
+ })).toThrow()
214
+ })
215
+
216
+ it('fails on a missing file (points to a folder)', () => {
217
+ expect(() => buildJob({
218
+ testsuite: 'lib'
219
+ })).toThrow()
220
+ })
221
+
222
+ it('fails on a missing folder (does not exist)', () => {
223
+ expect(() => buildJob({
224
+ webapp: 'not_a_folder'
225
+ })).toThrow()
226
+ })
227
+
228
+ it('fails on a missing folder (points to a file)', () => {
229
+ expect(() => buildJob({
230
+ webapp: 'webapp/lib/README.md'
231
+ })).toThrow()
232
+ })
233
+
234
+ describe('Path parameters validation', () => {
235
+ const parameters = ['webapp', 'browser', 'testsuite']
236
+
237
+ parameters.forEach(parameter => {
238
+ it(`fails on invalid path for ${parameter}`, () => {
239
+ expect(() => buildJob({ [parameter]: 'nope' })).toThrow()
240
+ })
241
+ })
242
+ })
243
+
244
+ describe('libs', () => {
245
+ it('fails on invalid lib path (absolute)', () => {
246
+ const absoluteLibPath = join(__dirname, '../test/project/webapp/lib2')
247
+ expect(() => buildJob({
248
+ libs: absoluteLibPath
249
+ })).toThrow()
250
+ })
251
+
252
+ it('fails on invalid lib path (relative)', () => {
253
+ expect(() => buildJob({
254
+ libs: '../project3'
255
+ })).toThrow()
256
+ })
257
+ })
258
+ })
259
+
260
+ describe('Using ui5-test-runner.json', () => {
261
+ const project2 = join(__dirname, '../test/project2')
262
+
263
+ it('enables option overriding at the command level', () => {
264
+ const job = fromCmdLine(cwd, [
265
+ '--port', '1',
266
+ '--port', '2',
267
+ '-k', 'true',
268
+ '-k', 'false'
269
+ ])
270
+ expect(job.port).toStrictEqual(2)
271
+ expect(job.keepAlive).toStrictEqual(false)
272
+ })
273
+
274
+ it('preload settings', () => {
275
+ const job = buildJob({
276
+ cwd: project2
277
+ })
278
+ expect(job.pageTimeout).toStrictEqual(900000)
279
+ expect(job.globalTimeout).toStrictEqual(3600000)
280
+ expect(job.failFast).toStrictEqual(true)
281
+ expect(job.libs).toEqual([{
282
+ relative: 'lib/',
283
+ source: join(project2, 'webapp')
284
+ }])
285
+ expect(job.browserArgs).toEqual(['-1'])
286
+ })
287
+
288
+ it('allows command line override', () => {
289
+ const job = buildJob({
290
+ cwd: project2,
291
+ globalTimeout: 900000
292
+ })
293
+ expect(job.pageTimeout).toStrictEqual(900000)
294
+ expect(job.globalTimeout).toStrictEqual(900000)
295
+ expect(job.failFast).toStrictEqual(true)
296
+ expect(job.libs).toEqual([{
297
+ relative: 'lib/',
298
+ source: join(project2, 'webapp')
299
+ }])
300
+ expect(job.ui5).toStrictEqual('https://ui5.sap.com')
301
+ })
302
+
303
+ it('preloads and overrides command line settings', () => {
304
+ const job = buildJob({
305
+ cwd: project2,
306
+ pageTimeout: 60000,
307
+ globalTimeout: 900000,
308
+ libs: 'project2/=../project2'
309
+ })
310
+ expect(job.pageTimeout).toStrictEqual(900000)
311
+ expect(job.globalTimeout).toStrictEqual(900000)
312
+ expect(job.failFast).toStrictEqual(true)
313
+ expect(job.libs).toEqual([{
314
+ relative: 'lib/',
315
+ source: join(project2, 'webapp')
316
+ }, {
317
+ relative: 'project2/',
318
+ source: join(cwd, '../project2')
319
+ }])
320
+ expect(job.ui5).toStrictEqual('https://ui5.sap.com')
321
+ })
322
+
323
+ it('preloads and concatenates browser settings', () => {
324
+ const job = buildJob({
325
+ cwd: project2,
326
+ '--': [-2]
327
+ })
328
+ expect(job.browserArgs).toEqual(['-1', '-2'])
329
+ })
330
+ })
331
+
332
+ describe('mode', () => {
333
+ it('returns legacy by default', () => {
334
+ expect(fromObject(cwd, {}).mode).toStrictEqual('legacy')
335
+ })
336
+
337
+ describe('url', () => {
338
+ it('enables testing external projects', () => {
339
+ expect(fromObject(cwd, {
340
+ url: ['http://myserver.remote.url/ui5-app.html']
341
+ }).mode).toStrictEqual('url')
342
+ })
343
+
344
+ // Assuming url could be used to access 'local' server, most options are supported
345
+
346
+ describe('incompatible options', () => {
347
+ const incompatible = {
348
+ testsuite: '../project2'
349
+ }
350
+
351
+ Object.keys(incompatible).forEach(option => {
352
+ it(`is incompatible with ${option}`, () => {
353
+ expect(() => fromObject(cwd, {
354
+ url: ['http://myserver.remote.url/ui5-app.html'],
355
+ [option]: incompatible[option]
356
+ })).toThrow(UTRError.MODE_INCOMPATIBLE_OPTION(option))
357
+ })
358
+ })
359
+ })
360
+ })
361
+
362
+ describe('capabilities', () => {
363
+ it('triggers the capabilities tester', () => {
364
+ expect(fromObject(cwd, {
365
+ capabilities: true
366
+ }).mode).toStrictEqual('capabilities')
367
+ })
368
+
369
+ it('supports cwd, port, logServer, browser, parallel, reportDir, pageTimeout, browserCloseTimeout, failFast and keepAlive', () => {
370
+ expect(fromObject('.', {
371
+ capabilities: true,
372
+ cwd,
373
+ port: 8080,
374
+ logServer: true,
375
+ browser: '$/selenium-webdriver.js',
376
+ parallel: 2,
377
+ reportDir: join(cwd, '.report'),
378
+ pageTimeout: 1000,
379
+ browserCloseTimeout: 1000,
380
+ failFast: true,
381
+ keepAlive: true
382
+ }).mode).toStrictEqual('capabilities')
383
+ })
384
+
385
+ describe('incompatible options', () => {
386
+ const incompatible = {
387
+ libs: '../project2',
388
+ ui5: 'http://localhost:8088/ui5',
389
+ cache: join(cwd, '.cache'),
390
+ webapp: 'webapp',
391
+ testsuite: 'test/testsuite.qunit.html',
392
+ pageFilter: '.*',
393
+ pageParams: 'sap-ui-debug=true',
394
+ coverage: true,
395
+ coverageSettings: '$/nyc.json',
396
+ coverageTempDir: '.nyc_output',
397
+ coverageReportDir: 'coverage',
398
+ coverageReporters: 'lcov',
399
+ globalTimeout: 1000
400
+ }
401
+
402
+ Object.keys(incompatible).forEach(option => {
403
+ it(`is incompatible with ${option}`, () => {
404
+ expect(() => fromObject(cwd, {
405
+ capabilities: true,
406
+ [option]: incompatible[option]
407
+ })).toThrow(UTRError.MODE_INCOMPATIBLE_OPTION(option))
408
+ })
409
+ })
410
+ })
411
+ })
412
+ })
413
+ })
package/src/npm.js ADDED
@@ -0,0 +1,73 @@
1
+ const { exec } = require('child_process')
2
+ const { join } = require('path')
3
+ const { stat, readFile } = require('fs/promises')
4
+ const { UTRError } = require('./error')
5
+ const { getOutput } = require('./output')
6
+
7
+ function npm (job, ...args) {
8
+ return new Promise((resolve, reject) => {
9
+ const childProcess = exec(`npm ${args.join(' ')}`, (err, stdout, stderr) => {
10
+ if (err) {
11
+ reject(UTRError.NPM_FAILED(stderr))
12
+ } else {
13
+ resolve(stdout.trim())
14
+ }
15
+ })
16
+ if (args[0] === 'install') {
17
+ getOutput(job).monitor(childProcess)
18
+ }
19
+ })
20
+ }
21
+
22
+ async function folderExists (path) {
23
+ try {
24
+ const result = await stat(path)
25
+ return result.isDirectory()
26
+ } catch (e) {
27
+ return false
28
+ }
29
+ }
30
+
31
+ let localRoot
32
+ let globalRoot
33
+
34
+ module.exports = {
35
+ async resolvePackage (job, name) {
36
+ if (!localRoot) {
37
+ [localRoot, globalRoot] = await Promise.all([
38
+ npm(job, 'root'),
39
+ npm(job, 'root', '--global')
40
+ ])
41
+ }
42
+ let modulePath
43
+ let justInstalled = false
44
+ const localPath = join(localRoot, name)
45
+ if (await folderExists(localPath)) {
46
+ modulePath = localPath
47
+ } else {
48
+ const globalPath = join(globalRoot, name)
49
+ if (!await folderExists(globalPath)) {
50
+ if (!job.npmInstall) {
51
+ throw UTRError.NPM_DEPENDENCY_NOT_FOUND(name)
52
+ }
53
+ const previousStatus = job.status
54
+ job.status = `Installing ${name}...`
55
+ await npm(job, 'install', name, '-g')
56
+ justInstalled = true
57
+ job.status = previousStatus
58
+ }
59
+ modulePath = globalPath
60
+ }
61
+ const output = getOutput(job)
62
+ const installedPackage = JSON.parse((await readFile(join(modulePath, 'package.json'))).toString())
63
+ const { version: installedVersion } = installedPackage
64
+ output.resolvedPackage(name, modulePath, installedVersion)
65
+ if (!justInstalled) {
66
+ const latestVersion = await npm(job, 'view', name, 'version')
67
+ if (latestVersion !== installedVersion) {
68
+ output.packageNotLatest(name, latestVersion)
69
+ }
70
+ }
71
+ return modulePath
72
+ }
73
+ }
@@ -0,0 +1,98 @@
1
+ const { join } = require('path')
2
+ const { resolvePackage } = require('./npm')
3
+ const { mock } = require('child_process')
4
+ const { cleanDir, createDir, recreateDir } = require('./tools')
5
+ const { writeFile } = require('fs/promises')
6
+ const { fromObject } = require('./job')
7
+ const { getOutput } = require('./output')
8
+ const { UTRError } = require('./error')
9
+
10
+ const tmp = join(__dirname, '../tmp')
11
+ const npmGlobal = join(tmp, 'npm/global')
12
+
13
+ describe('src/npm', () => {
14
+ const cwd = join(__dirname, '../test/project')
15
+ const reportDir = join(__dirname, '../tmp/npm/report')
16
+
17
+ let job
18
+ let output
19
+
20
+ beforeAll(async () => {
21
+ await createDir(join(npmGlobal, 'existing_global'))
22
+ await writeFile(join(npmGlobal, 'existing_global', 'package.json'), `{
23
+ "version": "1.0.0"
24
+ }`)
25
+ await recreateDir(reportDir)
26
+ await cleanDir(join(npmGlobal, 'not_existing'))
27
+ })
28
+
29
+ beforeEach(() => {
30
+ job = fromObject(cwd, {
31
+ reportDir,
32
+ coverage: false
33
+ })
34
+ job.status = 'Testing'
35
+ output = getOutput(job)
36
+ jest.spyOn(output, 'status')
37
+ jest.spyOn(output, 'resolvedPackage')
38
+ jest.spyOn(output, 'packageNotLatest')
39
+ })
40
+
41
+ it('detects already installed local package', async () => {
42
+ const path = await resolvePackage(job, 'reserve')
43
+ expect(path).toStrictEqual(join(__dirname, '../node_modules/reserve'))
44
+ expect(output.resolvedPackage).toHaveBeenCalledTimes(1)
45
+ expect(output.packageNotLatest).not.toHaveBeenCalled()
46
+ })
47
+
48
+ it('detects already installed global package (but warn as not the latest)', async () => {
49
+ mock({
50
+ api: 'exec',
51
+ scriptPath: 'npm',
52
+ args: ['view', 'existing_global', 'version'],
53
+ exec: async childProcess => childProcess.stdout.write('1.0.1\n'),
54
+ persist: true
55
+ })
56
+ const path = await resolvePackage(job, 'existing_global')
57
+ expect(path).toStrictEqual(join(npmGlobal, 'existing_global'))
58
+ expect(output.resolvedPackage).toHaveBeenCalledTimes(1)
59
+ expect(output.packageNotLatest).toHaveBeenCalled()
60
+ })
61
+
62
+ it('fails if --no-npm-install is set', async () => {
63
+ await expect(resolvePackage({
64
+ ...job,
65
+ npmInstall: false
66
+ }, 'not_existing')).rejects.toThrowError(UTRError.NPM_DEPENDENCY_NOT_FOUND('not_existing'))
67
+ })
68
+
69
+ it('installs missing package globally', async () => {
70
+ mock({
71
+ api: 'exec',
72
+ scriptPath: 'npm',
73
+ args: ['install', 'not_existing', '-g'],
74
+ exec: async childProcess => {
75
+ await createDir(join(npmGlobal, 'not_existing'))
76
+ await writeFile(join(npmGlobal, 'not_existing', 'package.json'), `{
77
+ "version": "1.0.0"
78
+ }`)
79
+ childProcess.stdout.write('OK installed')
80
+ }
81
+ })
82
+ const path = await resolvePackage(job, 'not_existing')
83
+ expect(path).toStrictEqual(join(npmGlobal, 'not_existing'))
84
+ expect(output.resolvedPackage).toHaveBeenCalledTimes(1)
85
+ expect(output.packageNotLatest).not.toHaveBeenCalled()
86
+ })
87
+
88
+ it('fails if the package cannot be installed', async () => {
89
+ mock({
90
+ api: 'exec',
91
+ scriptPath: 'npm',
92
+ args: ['install', 'fail_to_install', '-g'],
93
+ exec: childProcess => { throw new Error('KO failed') }
94
+ })
95
+ await expect(resolvePackage(job, 'fail_to_install')).rejects.toThrowError(UTRError.NPM_FAILED('Error: KO failed'))
96
+ expect(output.status).toHaveBeenCalledTimes(1) // Won't restore previous status
97
+ })
98
+ })
package/src/options.js ADDED
@@ -0,0 +1,73 @@
1
+ 'use strict'
2
+
3
+ const { InvalidArgumentError } = require('commander')
4
+
5
+ function integer (value) {
6
+ const result = parseInt(value, 10)
7
+ if (isNaN(result)) {
8
+ throw new InvalidArgumentError('Invalid integer')
9
+ }
10
+ if (result < 0) {
11
+ throw new InvalidArgumentError('Only >= 0')
12
+ }
13
+ return result
14
+ }
15
+
16
+ module.exports = {
17
+ any (value) {
18
+ return value
19
+ },
20
+
21
+ boolean (value, defaultValue) {
22
+ if (value === undefined) {
23
+ return !defaultValue
24
+ }
25
+ if (['true', 'yes', 'on'].includes(value)) {
26
+ return true
27
+ }
28
+ if (['false', 'no', 'off'].includes(value)) {
29
+ return false
30
+ }
31
+ throw new InvalidArgumentError('Invalid boolean')
32
+ },
33
+
34
+ integer,
35
+
36
+ timeout (value) {
37
+ const int = integer(value)
38
+ if (value.endsWith('ms')) {
39
+ return int
40
+ }
41
+ const specifier = value.substring(int.toString().length)
42
+ if (['s', 'sec'].includes(specifier)) {
43
+ return int * 1000
44
+ }
45
+ if (['m', 'min'].includes(specifier)) {
46
+ return int * 60 * 1000
47
+ }
48
+ if (specifier) {
49
+ throw new InvalidArgumentError('Invalid timeout')
50
+ }
51
+ return int
52
+ },
53
+
54
+ url (value) {
55
+ if (!value.match(/^https?:\/\/[^ "]+$/)) {
56
+ throw new InvalidArgumentError('Invalid URL')
57
+ }
58
+ return value
59
+ },
60
+
61
+ arrayOf (typeValidator) {
62
+ return function (value, previousValue) {
63
+ let result
64
+ if (previousValue === undefined) {
65
+ result = []
66
+ } else {
67
+ result = [...previousValue]
68
+ }
69
+ result.push(typeValidator(value))
70
+ return result
71
+ }
72
+ }
73
+ }