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,16 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body>
4
+ <H1>localStorage</H1>
5
+ <p>Check if the localStorage is isolated</p>
6
+ <script>
7
+ const initial = localStorage.value
8
+ const parameters = new URLSearchParams(location.search)
9
+ localStorage.value = parameters.get('value')
10
+ const modified = localStorage.value
11
+ const xhr = new XMLHttpRequest()
12
+ xhr.open('POST', '/_/log')
13
+ xhr.send(JSON.stringify({initial, modified}))
14
+ </script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ const assert = require('assert')
4
+
5
+ module.exports = [{
6
+ label: 'Local storage (1)',
7
+ url: 'local-storage/index.html?value=1',
8
+ endpoint: ({ body }) => {
9
+ const { initial, modified } = body
10
+ assert.ok(initial === undefined, 'The local storage starts empty')
11
+ assert.ok(modified === '1', 'The local storage can be used')
12
+ }
13
+ }, {
14
+ label: 'Local storage (2)',
15
+ url: 'local-storage/index.html?value=2',
16
+ endpoint: ({ body }) => {
17
+ const { initial, modified } = body
18
+ assert.ok(initial === undefined, 'The local storage starts empty')
19
+ assert.ok(modified === '2', 'The local storage can be used')
20
+ }
21
+ }]
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body>
4
+ <H1>screenshot</H1>
5
+ <p>Checks if the browser supports screenshot</p>
6
+ <span style="font-size: 32rem;">&#128521;</span>
7
+ <script>
8
+ const xhr = new XMLHttpRequest()
9
+ xhr.open('POST', '/_/log')
10
+ xhr.send('{}')
11
+ </script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ const assert = require('assert')
4
+ const { screenshot } = require('../../../browsers')
5
+ const { stat } = require('fs/promises')
6
+
7
+ module.exports = {
8
+ label: 'Screenshot',
9
+ for: capabilities => !!capabilities.screenshot,
10
+ url: 'screenshot/index.html',
11
+ endpoint: async function (_, url) {
12
+ const { job } = this
13
+ const fileName = await screenshot(job, url, 'screenshot')
14
+ const fileInfo = await stat(fileName)
15
+ assert.ok(fileInfo.isFile(), 'The file was generated')
16
+ assert.ok(fileInfo.size > 1024, 'The file contains something')
17
+ }
18
+ }
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+
3
+ const assert = require('assert')
4
+
5
+ function qUnitEndpoints (data) {
6
+ const { endpoint } = data
7
+ if (!this.calls) {
8
+ this.calls = {}
9
+ }
10
+ if (!this.calls[endpoint]) {
11
+ this.calls[endpoint] = 1
12
+ } else {
13
+ ++this.calls[endpoint]
14
+ }
15
+ if (endpoint === 'QUnit/done') {
16
+ assert.ok(this.calls['QUnit/begin'], 'QUnit/begin was triggered')
17
+ assert.ok(this.calls['QUnit/log'], 'QUnit/log was triggered')
18
+ assert.ok(this.calls['QUnit/testDone'], 'QUnit/testDone was triggered')
19
+ return true
20
+ }
21
+ return false
22
+ }
23
+
24
+ module.exports = [{
25
+ label: 'Scripts (QUnit)',
26
+ for: capabilities => !!capabilities.scripts,
27
+ url: 'scripts/qunit.html',
28
+ scripts: ['qunit-intercept.js', 'post.js', 'qunit-hooks.js'],
29
+ endpoint: qUnitEndpoints
30
+ }, {
31
+ label: 'Scripts (TestSuite)',
32
+ for: capabilities => !!capabilities.scripts,
33
+ url: 'scripts/testsuite.html',
34
+ scripts: ['post.js', 'qunit-redirect.js'],
35
+ endpoint: function (data) {
36
+ assert(data.endpoint === 'addTestPages', 'addTestPages was triggered')
37
+ assert(data.body.length === 2, 'Two pages received')
38
+ const pages = [
39
+ '/unit/unitTests.qunit.html',
40
+ '/integration/opaTests.iframe.qunit.html'
41
+ ]
42
+ pages.forEach((page, index) => assert(data.body[index].endsWith(page), page))
43
+ }
44
+ }, {
45
+ label: 'Scripts (External QUnit)',
46
+ for: capabilities => !!capabilities.scripts,
47
+ url: 'https://ui5.sap.com/test-resources/sap/m/demokit/orderbrowser/webapp/test/unit/unitTests.qunit.html',
48
+ scripts: ['qunit-intercept.js', 'post.js', 'qunit-hooks.js'],
49
+ endpoint: qUnitEndpoints
50
+ }]
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>qunit</title>
5
+ <script id="sap-ui-bootstrap"
6
+ src="https://ui5.sap.com/resources/sap-ui-core.js"
7
+ ></script>
8
+ <link rel="stylesheet" type="text/css" href="https://ui5.sap.com/resources/sap/ui/thirdparty/qunit-2.css">
9
+ <script src="https://ui5.sap.com/resources/sap/ui/thirdparty/qunit-2.js"></script>
10
+ </head>
11
+ <body>
12
+ <div id="qunit"></div>
13
+ <div id="qunit-fixture"></div>
14
+ <script>
15
+ QUnit.module('module', function () {
16
+ QUnit.test("test", function (assert) {
17
+ assert.ok(true, 'assert')
18
+ })
19
+ })
20
+ </script>
21
+ </body>
22
+ </html>
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>testsuite</title>
5
+ <script src="https://ui5.sap.com/resources/sap/ui/qunit/qunit-redirect.js"></script>
6
+ <script src="testsuite.js" data-sap-ui-testsuite></script>
7
+ </head>
8
+ <body>
9
+ </body>
10
+ </html>
@@ -0,0 +1,8 @@
1
+ window.suite = function () {
2
+ 'use strict'
3
+ const suite = new window.parent.jsUnitTestSuite() // eslint-disable-line new-cap
4
+ const path = window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1)
5
+ suite.addTestPage(`${path}unit/unitTests.qunit.html`)
6
+ suite.addTestPage(`${path}integration/opaTests.iframe.qunit.html`)
7
+ return suite
8
+ }
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body>
4
+ <H1>timeout</H1>
5
+ <p>Check if the ping rate is ensured</p>
6
+ <script>
7
+ const parameters = new URLSearchParams(location.search)
8
+ const rate = parseInt(parameters.get('rate'), 10)
9
+ const wait = parseInt(parameters.get('wait'), 10)
10
+ const steps = []
11
+ setTimeout(() => {
12
+ const xhr = new XMLHttpRequest()
13
+ xhr.open('POST', '/_/log')
14
+ xhr.send(JSON.stringify({ steps }))
15
+ }, wait)
16
+ setInterval(() => {
17
+ steps.push(Date.now())
18
+ }, rate)
19
+ </script>
20
+ </body>
21
+ </html>
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const assert = require('assert')
4
+
5
+ module.exports = [{
6
+ label: 'Timeout (100ms)',
7
+ url: 'timeout/index.html?rate=100&wait=1000',
8
+ endpoint: ({ body }) => {
9
+ const { steps } = body
10
+ assert.ok(steps.length > 8, 'The right number of steps is generated')
11
+ }
12
+ }, {
13
+ label: 'Timeout (250ms)',
14
+ url: 'timeout/index.html?rate=250&wait=1250',
15
+ endpoint: ({ body }) => {
16
+ const { steps } = body
17
+ assert.ok(steps.length > 3, 'The right number of steps is generated')
18
+ }
19
+ }]
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body>
4
+ <H1>console</H1>
5
+ <p>Checks if the browser supports traces</p>
6
+ <script>
7
+ console.log('A simple string')
8
+ console.log('A quoted "string"')
9
+ console.log('complex parameters', 1, true, { property: 'value' })
10
+ console.warn('A warning')
11
+ console.error('An error')
12
+ console.info('An info')
13
+ const xhr = new XMLHttpRequest()
14
+ xhr.open('POST', '/_/log')
15
+ xhr.send('{}')
16
+ </script>
17
+ </body>
18
+ </html>
@@ -0,0 +1,81 @@
1
+ 'use strict'
2
+
3
+ const { $browsers } = require('../../../symbols')
4
+ const { join } = require('path')
5
+ const { stat } = require('fs/promises')
6
+ const { stop } = require('../../../browsers')
7
+ const { buildCsvReader } = require('../../../csv-reader')
8
+ const assert = require('assert')
9
+
10
+ const expectedLogs = [{
11
+ type: 'log',
12
+ text: 'A simple string'
13
+ }, {
14
+ type: 'log',
15
+ text: 'A quoted "string"'
16
+ }, {
17
+ type: 'log',
18
+ text: /^"?complex parameters"? 1 true / // Not sure how objects are handled
19
+ }, {
20
+ type: 'warning',
21
+ text: 'A warning'
22
+ }, {
23
+ type: 'error',
24
+ text: 'An error'
25
+ }, {
26
+ type: /info|log/, // selenium-webdriver
27
+ text: 'An info'
28
+ }]
29
+
30
+ function equal (value, expected, label) {
31
+ if (typeof expected === 'string') {
32
+ return value === expected
33
+ }
34
+ return !!value.match(expected)
35
+ }
36
+
37
+ function unquote (string) {
38
+ if (string.startsWith('"') && string.endsWith('"')) {
39
+ return string.substring(1, string.length - 1)
40
+ .replace(/\\"/g, '"')
41
+ }
42
+ return string
43
+ }
44
+
45
+ module.exports = {
46
+ label: 'Console logs',
47
+ for: capabilities => Array.isArray(capabilities.traces) && capabilities.traces.includes('console'),
48
+ url: 'traces/index.html',
49
+ endpoint: async function (_, url) {
50
+ const { job } = this
51
+ const { reportDir } = job[$browsers][url]
52
+ this.stopExpected = true
53
+ await stop(this.job, url)
54
+ const consolePath = join(reportDir, 'console.csv')
55
+ const consoleStat = await stat(consolePath)
56
+ if (!consoleStat.isFile()) {
57
+ throw new Error('missing console.csv')
58
+ }
59
+ const logsReader = buildCsvReader(consolePath)
60
+ let logIndex = 0
61
+ for await (const log of logsReader) {
62
+ assert.strictEqual(typeof log.timestamp, 'string', 'timestamp exists')
63
+ assert.ok(log.timestamp.match(/^[0-9]+$/), 'timestamp is a number')
64
+ const {
65
+ type: expectedType,
66
+ text: expectedText
67
+ } = expectedLogs[logIndex]
68
+ let { text } = log
69
+ const match = text.match(/^https?[^ ]+ (\d+:\d+ )?(.*)/)
70
+ if (match) {
71
+ text = unquote(match[2])
72
+ }
73
+ if (equal(log.type, expectedType) && equal(text, expectedText)) {
74
+ if (++logIndex === expectedLogs.length) {
75
+ break
76
+ }
77
+ }
78
+ }
79
+ assert.strictEqual(logIndex, expectedLogs.length, 'All expected logs were found')
80
+ }
81
+ }
package/src/cors.js CHANGED
@@ -7,7 +7,7 @@ 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',
10
+ 'Access-Control-Allow-Headers': 'content-type, content-length, x-page-url',
11
11
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
12
12
  'Access-Control-Allow-Credentials': 'true'
13
13
  })
@@ -0,0 +1,41 @@
1
+ 'use script'
2
+
3
+ const Request = require('reserve/mock/Request')
4
+ const Response = require('reserve/mock/Response')
5
+ const cors = require('./cors')
6
+
7
+ const origin = 'npmjs.com'
8
+
9
+ describe('src/cors', () => {
10
+ it('handles CORS attributes', async () => {
11
+ const request = new Request('GET', '/', { origin })
12
+ const response = new Response()
13
+ cors.custom(request, response)
14
+ expect(response.statusCode).toBeUndefined()
15
+ expect(response.headers).toMatchObject({
16
+ 'access-control-allow-origin': origin,
17
+ 'access-control-allow-credentials': 'true'
18
+ })
19
+ })
20
+
21
+ it('does not impact response if origin is missing', async () => {
22
+ const request = new Request('GET', '/')
23
+ const response = new Response()
24
+ cors.custom(request, response)
25
+ expect(response.statusCode).toBeUndefined()
26
+ expect(response.headers).toMatchObject({})
27
+ })
28
+
29
+ it('handles CORS preflight', async () => {
30
+ const request = new Request('OPTIONS', '/', { origin })
31
+ const response = new Response()
32
+ cors.custom(request, response)
33
+ expect(response.statusCode).toStrictEqual(200)
34
+ expect(response.headers).toMatchObject({
35
+ 'access-control-allow-origin': origin,
36
+ 'access-control-allow-headers': 'content-type, content-length, x-page-url',
37
+ 'access-control-allow-methods': 'GET, POST, PUT, DELETE, OPTIONS',
38
+ 'access-control-allow-credentials': 'true'
39
+ })
40
+ })
41
+ })
package/src/coverage.js CHANGED
@@ -2,14 +2,18 @@
2
2
 
3
3
  const { join } = require('path')
4
4
  const { fork } = require('child_process')
5
- const { cleanDir, createDir } = require('./tools')
5
+ const { cleanDir, createDir, filename } = require('./tools')
6
6
  const { readdir, readFile, stat, writeFile } = require('fs').promises
7
7
  const { Readable } = require('stream')
8
- const output = require('./output')
8
+ const { getOutput } = require('./output')
9
+ const { resolvePackage } = require('./npm')
9
10
 
10
- const nycScript = require.resolve('nyc/bin/nyc.js')
11
+ const $nycSettingsPath = Symbol('nycSettingsPath')
11
12
 
12
- function nyc (...args) {
13
+ let nycScript
14
+
15
+ async function nyc (job, ...args) {
16
+ const output = getOutput(job)
13
17
  output.nyc(...args)
14
18
  const childProcess = fork(nycScript, args, { stdio: 'pipe' })
15
19
  output.monitor(childProcess)
@@ -39,39 +43,47 @@ const customFileSystem = {
39
43
 
40
44
  async function instrument (job) {
41
45
  job.status = 'Instrumenting'
42
- job.nycSettingsPath = join(job.covTempDir, 'settings/nyc.json')
43
- await cleanDir(job.covTempDir)
44
- await createDir(join(job.covTempDir, 'settings'))
45
- const settings = JSON.parse((await readFile(job.covSettings)).toString())
46
+ if (!nycScript) {
47
+ const nyc = await resolvePackage(job, 'nyc')
48
+ nycScript = join(nyc, 'bin/nyc.js')
49
+ }
50
+ job[$nycSettingsPath] = join(job.coverageTempDir, 'settings/nyc.json')
51
+ await cleanDir(job.coverageTempDir)
52
+ await createDir(join(job.coverageTempDir, 'settings'))
53
+ const settings = JSON.parse((await readFile(job.coverageSettings)).toString())
46
54
  settings.cwd = job.cwd
47
55
  if (!settings.exclude) {
48
56
  settings.exclude = []
49
57
  }
50
- settings.exclude.push(join(job.covTempDir, '**'))
58
+ settings.exclude.push(join(job.coverageTempDir, '**'))
51
59
  if (job.cache) {
52
60
  settings.exclude.push(join(job.cache, '**'))
53
61
  }
54
- settings.exclude.push(join(job.tstReportDir, '**'))
55
- settings.exclude.push(join(job.covReportDir, '**'))
56
- await writeFile(job.nycSettingsPath, JSON.stringify(settings))
57
- await nyc('instrument', job.webapp, join(job.covTempDir, 'instrumented'), '--nycrc-path', job.nycSettingsPath)
62
+ settings.exclude.push(join(job.reportDir, '**'))
63
+ settings.exclude.push(join(job.coverageReportDir, '**'))
64
+ await writeFile(job[$nycSettingsPath], JSON.stringify(settings))
65
+ await nyc(job, 'instrument', job.webapp, join(job.coverageTempDir, 'instrumented'), '--nycrc-path', job[$nycSettingsPath])
58
66
  }
59
67
 
60
68
  async function generateCoverageReport (job) {
61
69
  job.status = 'Generating coverage report'
62
- await cleanDir(job.covReportDir)
63
- await nyc('merge', job.covTempDir, join(job.covTempDir, 'coverage.json'))
64
- const reporters = job.covReporters.split(',').map(reporter => `--reporter=${reporter}`)
65
- await nyc('report', ...reporters, '--temp-dir', job.covTempDir, '--report-dir', job.covReportDir, '--nycrc-path', job.nycSettingsPath)
70
+ await cleanDir(job.coverageReportDir)
71
+ await nyc(job, 'merge', job.coverageTempDir, join(job.coverageTempDir, 'coverage.json'))
72
+ const reporters = job.coverageReporters.map(reporter => `--reporter=${reporter}`)
73
+ await nyc(job, 'report', ...reporters, '--temp-dir', job.coverageTempDir, '--report-dir', job.coverageReportDir, '--nycrc-path', job[$nycSettingsPath])
66
74
  }
67
75
 
68
76
  module.exports = {
69
77
  instrument: job => job.coverage && instrument(job),
78
+ async collect (job, url, coverageData) {
79
+ const coverageFileName = join(job.coverageTempDir, `${filename(url)}.json`)
80
+ await writeFile(coverageFileName, JSON.stringify(coverageData))
81
+ },
70
82
  generateCoverageReport: job => job.coverage && generateCoverageReport(job),
71
83
  mappings: job => job.coverage
72
84
  ? [{
73
85
  match: /^\/(.*\.js)$/,
74
- file: join(job.covTempDir, 'instrumented', '$1'),
86
+ file: join(job.coverageTempDir, 'instrumented', '$1'),
75
87
  'ignore-if-not-found': true,
76
88
  'custom-file-system': customFileSystem
77
89
  }]
@@ -0,0 +1,79 @@
1
+ const { join } = require('path')
2
+ const { fromObject } = require('./job')
3
+ const { instrument, generateCoverageReport, mappings } = require('./coverage')
4
+ const { stat } = require('fs/promises')
5
+ const { cleanDir, createDir } = require('./tools')
6
+
7
+ describe('src/coverage', () => {
8
+ const cwd = join(__dirname, '../test/project')
9
+ const path = join(__dirname, '../tmp/coverage')
10
+
11
+ beforeAll(() => {
12
+ return cleanDir(path)
13
+ })
14
+
15
+ describe('disabled', () => {
16
+ const basePath = join(path, 'disabled')
17
+ let job
18
+
19
+ beforeAll(async () => {
20
+ const reportDir = join(basePath, 'report')
21
+ await createDir(reportDir)
22
+ job = fromObject(cwd, {
23
+ reportDir,
24
+ coverageTempDir: join(basePath, 'coverage/temp'),
25
+ coverageReportDir: join(basePath, 'coverage/report'),
26
+ coverage: false
27
+ })
28
+ })
29
+
30
+ it('does not instrument sources', async () => {
31
+ await instrument(job)
32
+ await expect(() => stat(join(basePath, 'coverage/temp/settings/nyc.json'))).rejects.toThrow()
33
+ })
34
+
35
+ it('does not generate a report', async () => {
36
+ await generateCoverageReport(job)
37
+ await expect(() => stat(join(basePath, 'coverage/temp/coverage.json'))).rejects.toThrow()
38
+ await expect(() => stat(join(basePath, 'coverage/report'))).rejects.toThrow()
39
+ })
40
+
41
+ it('does not create a mapping', async () => {
42
+ const coverageMappings = mappings(job)
43
+ expect(coverageMappings.length).toStrictEqual(0)
44
+ })
45
+ })
46
+
47
+ describe('enabled', () => {
48
+ const basePath = join(path, 'enabled')
49
+ let job
50
+
51
+ beforeAll(async function () {
52
+ const reportDir = join(basePath, 'report')
53
+ await createDir(reportDir)
54
+ job = fromObject(cwd, {
55
+ reportDir,
56
+ coverageTempDir: join(basePath, 'coverage/temp'),
57
+ coverageReportDir: join(basePath, 'coverage/report'),
58
+ coverage: true
59
+ })
60
+ })
61
+
62
+ it('instruments sources', async () => {
63
+ await instrument(job)
64
+ const nycJsonStat = await stat(join(basePath, 'coverage/temp/settings/nyc.json'))
65
+ expect(nycJsonStat.isFile()).toStrictEqual(true)
66
+ })
67
+
68
+ it('generates a report', async () => {
69
+ await generateCoverageReport(job)
70
+ const reportStat = await stat(join(basePath, 'coverage/report'))
71
+ expect(reportStat.isDirectory()).toStrictEqual(true)
72
+ })
73
+
74
+ it('creates a mapping', async () => {
75
+ const coverageMappings = mappings(job)
76
+ expect(coverageMappings.length).toStrictEqual(1)
77
+ })
78
+ })
79
+ })
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const { createReadStream } = require('fs')
4
+ const readline = require('readline')
5
+
6
+ const unescape = value => {
7
+ if (value.startsWith('"') && value.endsWith('"')) {
8
+ return JSON.parse(value)
9
+ }
10
+ return value
11
+ }
12
+
13
+ async function * buildCsvReader (fileName) {
14
+ const fileStream = createReadStream(fileName)
15
+ const rl = readline.createInterface({
16
+ input: fileStream,
17
+ crlfDelay: Infinity
18
+ })
19
+ let headers
20
+ for await (const line of rl) {
21
+ const fields = line.split('\t')
22
+ if (headers === undefined) {
23
+ headers = fields
24
+ continue
25
+ }
26
+ const record = {}
27
+ headers.forEach((field, index) => {
28
+ record[field] = unescape(fields[index])
29
+ })
30
+ yield record
31
+ }
32
+ }
33
+
34
+ module.exports = {
35
+ buildCsvReader
36
+ }
@@ -0,0 +1,42 @@
1
+ const { buildCsvReader } = require('./csv-reader')
2
+ const { createReadStream } = require('fs')
3
+ const { Readable } = require('stream')
4
+
5
+ jest.mock('fs')
6
+
7
+ describe('src/csv-reader', () => {
8
+ beforeEach(() => {
9
+ createReadStream.mockReset()
10
+ createReadStream.mockImplementation(() => {
11
+ const s = new Readable()
12
+ s._read = () => {}
13
+ s.push(`timestamp\ttext
14
+ 1\tabc
15
+ 2\thello world
16
+ 3\t"abc\\td"
17
+ `)
18
+ s.push(null)
19
+ return s
20
+ })
21
+ })
22
+
23
+ it('allocates an iterator', () => {
24
+ const reader = buildCsvReader('test.csv')
25
+ expect(reader).not.toBeUndefined()
26
+ expect(typeof reader.next).toBe('function')
27
+ expect(createReadStream).not.toHaveBeenCalled()
28
+ })
29
+
30
+ it('reads records', async () => {
31
+ const reader = buildCsvReader('test.csv')
32
+ const records = []
33
+ for await (const record of reader) {
34
+ records.push(record)
35
+ }
36
+ expect(records).toEqual([
37
+ { timestamp: '1', text: 'abc' },
38
+ { timestamp: '2', text: 'hello world' },
39
+ { timestamp: '3', text: 'abc\td' }
40
+ ])
41
+ })
42
+ })
@@ -0,0 +1,52 @@
1
+ 'use strict'
2
+
3
+ const { writeFile } = require('fs/promises')
4
+
5
+ const append = (fileName, line) => writeFile(fileName, line + '\n', { flag: 'a+' })
6
+ const escape = value => {
7
+ const stringValue = value.toString()
8
+ if (stringValue.match(/\r|\n|\t|"/)) {
9
+ return JSON.stringify(stringValue)
10
+ }
11
+ return stringValue
12
+ }
13
+
14
+ class CsvWriter {
15
+ #fileName
16
+ #ready
17
+ #fields
18
+
19
+ constructor (fileName) {
20
+ this.#fileName = fileName
21
+ this.#ready = Promise.resolve()
22
+ this.#fields = []
23
+ }
24
+
25
+ get ready () {
26
+ return this.#ready
27
+ }
28
+
29
+ append (records) {
30
+ if (!Array.isArray(records)) {
31
+ records = [records]
32
+ }
33
+ if (this.#fields.length === 0) {
34
+ this.#fields = Object.keys(records[0]).filter(name => name !== 'timestamp')
35
+ this.#ready = this.#ready.then(() => append(this.#fileName, `timestamp\t${this.#fields.join('\t')}`))
36
+ }
37
+ const lines = records.map(record => {
38
+ const { timestamp = Date.now() } = record
39
+ return [
40
+ timestamp,
41
+ ...this.#fields.map(name => escape(record[name]))
42
+ ].join('\t')
43
+ }).join('\n')
44
+ this.#ready = this.#ready.then(() => append(this.#fileName, lines))
45
+ }
46
+ }
47
+
48
+ module.exports = {
49
+ buildCsvWriter (fileName) {
50
+ return new CsvWriter(fileName)
51
+ }
52
+ }