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.
- package/README.md +32 -188
- package/index.js +47 -16
- package/package.json +28 -10
- package/src/add-test-pages.js +35 -0
- package/src/add-test-pages.spec.js +95 -0
- package/src/browser.spec.js +724 -0
- package/src/browsers.js +220 -59
- package/src/capabilities/index.js +194 -0
- package/src/capabilities/tests/basic/iframe.html +8 -0
- package/src/capabilities/tests/basic/index.html +12 -0
- package/src/capabilities/tests/basic/index.js +20 -0
- package/src/capabilities/tests/basic/ui5.html +24 -0
- package/src/capabilities/tests/dynamic-include/index.js +21 -0
- package/src/capabilities/tests/dynamic-include/mix.html +11 -0
- package/src/capabilities/tests/dynamic-include/one.html +11 -0
- package/src/capabilities/tests/dynamic-include/post.js +3 -0
- package/src/capabilities/tests/dynamic-include/test.js +1 -0
- package/src/capabilities/tests/dynamic-include/two.html +11 -0
- package/src/capabilities/tests/index.js +16 -0
- package/src/capabilities/tests/local-storage/index.html +16 -0
- package/src/capabilities/tests/local-storage/index.js +21 -0
- package/src/capabilities/tests/screenshot/index.html +13 -0
- package/src/capabilities/tests/screenshot/index.js +18 -0
- package/src/capabilities/tests/scripts/index.js +50 -0
- package/src/capabilities/tests/scripts/qunit.html +22 -0
- package/src/capabilities/tests/scripts/testsuite.html +10 -0
- package/src/capabilities/tests/scripts/testsuite.js +8 -0
- package/src/capabilities/tests/timeout/index.html +21 -0
- package/src/capabilities/tests/timeout/index.js +19 -0
- package/src/capabilities/tests/traces/index.html +18 -0
- package/src/capabilities/tests/traces/index.js +81 -0
- package/src/cors.js +1 -1
- package/src/cors.spec.js +41 -0
- package/src/coverage.js +30 -18
- package/src/coverage.spec.js +79 -0
- package/src/csv-reader.js +36 -0
- package/src/csv-reader.spec.js +42 -0
- package/src/csv-writer.js +52 -0
- package/src/csv-writer.spec.js +77 -0
- package/src/defaults/browser.js +144 -0
- package/src/defaults/jsdom/compatibility.js +95 -0
- package/src/defaults/jsdom/debug.js +23 -0
- package/src/defaults/jsdom/resource-loader.js +43 -0
- package/src/defaults/jsdom/sap.ui.test.matchers.visible.js +39 -0
- package/src/defaults/jsdom.js +64 -0
- package/src/defaults/junit-xml-report.js +64 -0
- package/src/defaults/puppeteer.js +111 -0
- package/src/defaults/report/common.js +38 -0
- package/src/defaults/report/default.html +84 -0
- package/src/defaults/report/main.js +44 -0
- package/src/defaults/report/progress.js +49 -0
- package/src/defaults/report/styles.css +66 -0
- package/src/defaults/report.js +69 -0
- package/src/defaults/selenium-webdriver/chrome.js +38 -0
- package/src/defaults/selenium-webdriver/edge.js +25 -0
- package/src/defaults/selenium-webdriver/firefox.js +31 -0
- package/src/defaults/selenium-webdriver.js +138 -0
- package/src/endpoints.js +70 -124
- package/src/error.js +52 -0
- package/src/error.spec.js +17 -0
- package/src/get-job-progress.js +69 -0
- package/src/get-job-progress.spec.js +175 -0
- package/src/inject/post.js +96 -0
- package/src/inject/post.spec.js +147 -0
- package/src/inject/qunit-hooks.js +6 -21
- package/src/inject/qunit-intercept.js +30 -0
- package/src/inject/qunit-redirect.js +15 -7
- package/src/job-mode.js +45 -0
- package/src/job.js +254 -108
- package/src/job.spec.js +413 -0
- package/src/npm.js +73 -0
- package/src/npm.spec.js +98 -0
- package/src/options.js +73 -0
- package/src/options.spec.js +125 -0
- package/src/output.js +450 -131
- package/src/qunit-hooks.js +116 -0
- package/src/qunit-hooks.spec.js +687 -0
- package/src/report.js +47 -0
- package/src/reserve.js +3 -4
- package/src/simulate.spec.js +466 -0
- package/src/symbols.js +8 -0
- package/src/tests.js +127 -84
- package/src/timeout.spec.js +39 -0
- package/src/tools.js +111 -4
- package/src/tools.spec.js +90 -0
- package/src/ui5.js +3 -3
- package/src/unhandled.js +6 -6
- package/src/unhandled.spec.js +63 -0
- package/defaults/chromium.js +0 -62
- package/src/progress.html +0 -71
- package/src/proxies.js +0 -8
- package/src/report.html +0 -202
- /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;">😉</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,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
|
})
|
package/src/cors.spec.js
ADDED
|
@@ -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
|
|
8
|
+
const { getOutput } = require('./output')
|
|
9
|
+
const { resolvePackage } = require('./npm')
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const $nycSettingsPath = Symbol('nycSettingsPath')
|
|
11
12
|
|
|
12
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
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.
|
|
55
|
-
settings.exclude.push(join(job.
|
|
56
|
-
await writeFile(job
|
|
57
|
-
await nyc('instrument', job.webapp, join(job.
|
|
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.
|
|
63
|
-
await nyc('merge', job.
|
|
64
|
-
const reporters = job.
|
|
65
|
-
await nyc('report', ...reporters, '--temp-dir', job.
|
|
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.
|
|
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
|
+
}
|