ui5-test-runner 4.4.0 → 4.5.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/package.json +13 -30
- package/src/add-test-pages.js +44 -18
- package/src/browsers.js +1 -2
- package/src/capabilities/index.js +21 -25
- package/src/capabilities/tests/scripts/index.js +3 -2
- package/src/defaults/junit-xml-report.js +10 -1
- package/src/defaults/selenium-webdriver/chrome.js +6 -5
- package/src/inject/post.js +1 -1
- package/src/inject/qunit-hooks.js +2 -1
- package/src/inject/qunit-redirect.js +33 -5
- package/src/job-mode.js +2 -1
- package/src/job.js +22 -2
- package/src/output.js +23 -6
- package/src/parallelize.js +61 -0
- package/src/qunit-hooks.js +16 -11
- package/src/symbols.js +0 -4
- package/src/tests.js +38 -73
- package/src/ui5.js +7 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ui5-test-runner",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.1",
|
|
4
4
|
"description": "Standalone test runner for UI5",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,34 +11,15 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"lint": "standard --fix",
|
|
14
|
-
"test": "npm run test:unit && npm run test:
|
|
15
|
-
"test:
|
|
16
|
-
"test:samples": "npm run test:
|
|
17
|
-
"test:
|
|
18
|
-
"test:samples:ts": "npm run test:sample:ts:remote && npm run test:sample:ts:coverage:remote",
|
|
19
|
-
"test:auth-sample": "npm run test:auth-sample:remote",
|
|
20
|
-
"test:coverall": "rimraf .nyc_output && jest --coverageDirectory .nyc_output --coverageReporters json && nyc --silent --no-clean npm run test:integration:jsdom && nyc --silent --no-clean npm run test:integration:puppeteer && nyc --silent --no-clean npm run test:integration:selenium-webdriver-chrome && nyc --silent --no-clean npm run test:integration:playwright && nyc merge .nyc_output .nyc_output/final/coverage.json && nyc report --temp-dir .nyc_output/final/ --report-dir coverage --branches 80 --functions 80 --lines 80 --statements 80",
|
|
14
|
+
"test": "npm run test:unit && npm run test:e2e",
|
|
15
|
+
"test:samples": "npm run test:samples:js && npm run test:samples:ts",
|
|
16
|
+
"test:samples:js": "npm run test:sample:js:legacy && npm run test:sample:js:coverage:legacy && npm run test:sample:js:legacy-remote && npm run test:sample:js:coverage:legacy-remote && npm run test:sample:js:remote && npm run test:sample:js:coverage:remote && npm run test:sample:js:basic-authent && npm run test:sample:js:legacy:split-opa && npm run test:sample:js:remote:split-opa",
|
|
17
|
+
"test:coverall": "rimraf .nyc_output && jest --coverageDirectory .nyc_output --coverageReporters json && nyc --silent --no-clean npm run test:e2e && nyc merge .nyc_output .nyc_output/final/coverage.json && nyc report --temp-dir .nyc_output/final/ --report-dir coverage --branches 80 --functions 80 --lines 80 --statements 80",
|
|
21
18
|
"test:unit": "jest",
|
|
22
|
-
"test:unit:debug": "jest --runInBand",
|
|
23
|
-
"test:
|
|
24
|
-
"test:integration:selenium-webdriver-chrome": "node . --capabilities --browser $/selenium-webdriver.js -- --browser chrome",
|
|
25
|
-
"test:integration:jsdom": "node . --capabilities --browser $/jsdom.js",
|
|
26
|
-
"test:integration:playwright": "node . --capabilities --browser $/playwright.js",
|
|
19
|
+
"test:unit:debug": "node --inspect node_modules/jest/bin/jest.js --runInBand --no-coverage",
|
|
20
|
+
"test:e2e": "node test/e2e",
|
|
27
21
|
"test:report": "node ./src/defaults/report.js ./test/report && reserve --config ./test/report/reserve.json",
|
|
28
22
|
"test:text-report": "node ./src/defaults/text-report.js ./test/report",
|
|
29
|
-
"test:sample:js:legacy": "node . --cwd ./test/sample.js",
|
|
30
|
-
"test:sample:js:coverage:legacy": "node . --cwd ./test/sample.js --coverage --coverage-settings nyc.json --coverage-check-statements 67",
|
|
31
|
-
"test:sample:js:legacy-remote": "node . --port 8081 --cwd ./test/sample.js --url http://localhost:8081/test/testsuite.qunit.html",
|
|
32
|
-
"test:sample:js:coverage:legacy-remote": "node . --port 8081 --cwd ./test/sample.js --url http://localhost:8081/test/testsuite.qunit.html --coverage --coverage-settings nyc.json --coverage-check-statements 67",
|
|
33
|
-
"test:sample:js:remote": "start-server-and-test 'npm run serve:sample:js' http://localhost:8080 'node . --url http://localhost:8080/test/testsuite.qunit.html'",
|
|
34
|
-
"test:sample:js:coverage:remote": "start-server-and-test 'npm run serve:sample:js' http://localhost:8080 'node . --url http://localhost:8080/test/testsuite.qunit.html --coverage --coverage-check-statements 67'",
|
|
35
|
-
"serve:sample:js": "ui5 serve --config ./test/sample.js/ui5.yaml",
|
|
36
|
-
"test:sample:ts:remote": "start-server-and-test 'npm run serve:sample:ts' http://localhost:8080 'node . --url http://localhost:8080/test/testsuite.qunit.html'",
|
|
37
|
-
"serve:sample:ts": "cd ./test/sample.ts && node ui5.cjs serve",
|
|
38
|
-
"test:sample:ts:coverage:remote": "start-server-and-test 'npm run serve:sample:ts:coverage' http://localhost:8080 'node . --url http://localhost:8080/test/testsuite.qunit.html --coverage --coverage-check-statements 67'",
|
|
39
|
-
"serve:sample:ts:coverage": "cd ./test/sample.ts && node ui5.cjs serve --config ui5-coverage.yaml",
|
|
40
|
-
"test:auth-sample:remote": "start-server-and-test 'npm run serve:auth-sample' http://localhost:8080 'node . --url http://localhost:8080/test/testsuite.qunit.html --browser $/puppeteer.js --browser-args --basic-auth-username testUsername --browser-args --basic-auth-password testPassword'",
|
|
41
|
-
"serve:auth-sample": "cd ./test/auth_sample.js && reserve --config ./reserve.json",
|
|
42
23
|
"build:doc": "node build/doc",
|
|
43
24
|
"clean": "npm uninstall -g ui5-test-runner puppeteer nyc selenium-webdriver playwright webdriverio"
|
|
44
25
|
},
|
|
@@ -66,18 +47,20 @@
|
|
|
66
47
|
"mime": "^3.0.0",
|
|
67
48
|
"punybind": "^1.2.1",
|
|
68
49
|
"punyexpr": "^1.0.4",
|
|
69
|
-
"reserve": "^1.15.
|
|
50
|
+
"reserve": "^1.15.9"
|
|
70
51
|
},
|
|
71
52
|
"devDependencies": {
|
|
72
|
-
"@openui5/types": "^1.
|
|
73
|
-
"@ui5/cli": "^3.9.
|
|
53
|
+
"@openui5/types": "^1.123.0",
|
|
54
|
+
"@ui5/cli": "^3.9.2",
|
|
74
55
|
"@ui5/middleware-code-coverage": "^1.1.1",
|
|
56
|
+
"dotenv": "^16.4.5",
|
|
75
57
|
"jest": "^29.7.0",
|
|
76
58
|
"nock": "^13.5.4",
|
|
77
59
|
"nyc": "^15.1.0",
|
|
60
|
+
"rimraf": "^5.0.5",
|
|
78
61
|
"standard": "^17.1.0",
|
|
79
62
|
"start-server-and-test": "^2.0.3",
|
|
80
|
-
"typescript": "^5.4.
|
|
63
|
+
"typescript": "^5.4.5",
|
|
81
64
|
"ui5-tooling-transpile": "^3.3.7"
|
|
82
65
|
},
|
|
83
66
|
"optionalDependencies": {
|
package/src/add-test-pages.js
CHANGED
|
@@ -5,34 +5,60 @@ const { URL } = require('url')
|
|
|
5
5
|
const { getOutput } = require('./output')
|
|
6
6
|
const { stripUrlHash } = require('./tools')
|
|
7
7
|
|
|
8
|
+
const addUrlParam = (url, param) => {
|
|
9
|
+
if (url.includes(param)) {
|
|
10
|
+
return url
|
|
11
|
+
}
|
|
12
|
+
if (url.includes('?')) {
|
|
13
|
+
return url + '&' + param
|
|
14
|
+
}
|
|
15
|
+
return url + '?' + param
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
module.exports = {
|
|
9
|
-
async addTestPages (job, url,
|
|
10
|
-
|
|
19
|
+
async addTestPages (job, url, data) {
|
|
20
|
+
const { type, opa, modules, pages, page } = data
|
|
21
|
+
getOutput(job).debug('probe', `addTestPages from ${url}`, data)
|
|
11
22
|
let testPageUrls
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return stripUrlHash(absoluteUrl.toString())
|
|
15
|
-
})
|
|
16
|
-
if (job.pageFilter) {
|
|
17
|
-
const filter = new RegExp(job.pageFilter)
|
|
18
|
-
testPageUrls = pages.filter(name => name.match(filter))
|
|
23
|
+
if (type === 'none') {
|
|
24
|
+
testPageUrls = []
|
|
19
25
|
} else {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
let receivedPages
|
|
27
|
+
if (type === 'qunit') {
|
|
28
|
+
if (job.splitOpa && opa && modules && modules.length > 1) {
|
|
29
|
+
receivedPages = modules.map(moduleId => addUrlParam(stripUrlHash(page), `moduleId=${moduleId}`))
|
|
30
|
+
} else {
|
|
31
|
+
receivedPages = [page]
|
|
26
32
|
}
|
|
27
|
-
|
|
33
|
+
} else {
|
|
34
|
+
receivedPages = pages
|
|
35
|
+
}
|
|
36
|
+
receivedPages = receivedPages.map(relativeUrl => {
|
|
37
|
+
const absoluteUrl = new URL(relativeUrl, url)
|
|
38
|
+
return stripUrlHash(absoluteUrl.toString())
|
|
28
39
|
})
|
|
40
|
+
if (job.pageFilter) {
|
|
41
|
+
const filter = new RegExp(job.pageFilter)
|
|
42
|
+
testPageUrls = receivedPages.filter(name => name.match(filter))
|
|
43
|
+
} else {
|
|
44
|
+
testPageUrls = receivedPages
|
|
45
|
+
}
|
|
46
|
+
if (job.pageParams) {
|
|
47
|
+
testPageUrls = testPageUrls.map(url => addUrlParam(url, job.pageParams))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let member
|
|
51
|
+
if (type === 'suite' && job.splitOpa) {
|
|
52
|
+
member = 'url'
|
|
53
|
+
} else {
|
|
54
|
+
member = 'testPageUrls'
|
|
29
55
|
}
|
|
30
|
-
job
|
|
56
|
+
job[member] = testPageUrls.reduce((uniques, url) => {
|
|
31
57
|
if (!uniques.includes(url)) {
|
|
32
58
|
uniques.push(url)
|
|
33
59
|
}
|
|
34
60
|
return uniques
|
|
35
|
-
}, job
|
|
61
|
+
}, job[member] || [])
|
|
36
62
|
stop(job, url)
|
|
37
63
|
}
|
|
38
64
|
}
|
package/src/browsers.js
CHANGED
|
@@ -119,8 +119,7 @@ async function start (job, url, scripts = []) {
|
|
|
119
119
|
resolvedScripts.unshift(`window['ui5-test-runner/base-host'] = 'http://localhost:${job.port}'
|
|
120
120
|
`)
|
|
121
121
|
}
|
|
122
|
-
const progress = newProgress(job)
|
|
123
|
-
progress.label = url
|
|
122
|
+
const progress = newProgress(job, url)
|
|
124
123
|
const pageBrowser = {
|
|
125
124
|
url,
|
|
126
125
|
reportDir,
|
|
@@ -6,8 +6,9 @@ const { join } = require('path')
|
|
|
6
6
|
const { getOutput } = require('../output')
|
|
7
7
|
const { performance } = require('perf_hooks')
|
|
8
8
|
const { cleanDir, allocPromise, filename } = require('../tools')
|
|
9
|
-
const { $
|
|
9
|
+
const { $statusProgressTotal, $statusProgressCount } = require('../symbols')
|
|
10
10
|
const tests = require('./tests')
|
|
11
|
+
const parallelize = require('../parallelize')
|
|
11
12
|
|
|
12
13
|
async function capabilities (job) {
|
|
13
14
|
const output = getOutput(job)
|
|
@@ -17,9 +18,9 @@ async function capabilities (job) {
|
|
|
17
18
|
job.status = 'Serving'
|
|
18
19
|
} else {
|
|
19
20
|
if (job.debugKeepReport) {
|
|
20
|
-
output.
|
|
21
|
+
output.log('Report folder', job.reportDir, 'not cleaned because of --debug-keep-report.')
|
|
21
22
|
} else if (code !== 0) {
|
|
22
|
-
output.
|
|
23
|
+
output.error('Report folder', job.reportDir, 'not cleaned because of errors.')
|
|
23
24
|
} else {
|
|
24
25
|
await cleanDir(job.reportDir)
|
|
25
26
|
}
|
|
@@ -33,7 +34,7 @@ async function capabilities (job) {
|
|
|
33
34
|
try {
|
|
34
35
|
await probe(job)
|
|
35
36
|
} catch (e) {
|
|
36
|
-
output.
|
|
37
|
+
output.error('Unable to probe')
|
|
37
38
|
exit(-1)
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -90,21 +91,13 @@ async function capabilities (job) {
|
|
|
90
91
|
const filteredTests = tests
|
|
91
92
|
.filter((test) => !test.for || test.for(job.browserCapabilities))
|
|
92
93
|
.filter(({ name }) => !job.debugCapabilitiesTest || name.startsWith(job.debugCapabilitiesTest))
|
|
93
|
-
output.
|
|
94
|
+
output.log('Number of tests :', filteredTests.length)
|
|
94
95
|
|
|
95
96
|
let errors = 0
|
|
96
97
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (filteredTests.length === 0) {
|
|
101
|
-
if (!job[$browsers] || Object.keys(job[$browsers]).length === 0) {
|
|
102
|
-
output.wrap(() => console.log('Done.'))
|
|
103
|
-
exit(errors)
|
|
104
|
-
}
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
const { label, url, scripts, endpoint = () => { } } = filteredTests.shift()
|
|
98
|
+
const task = async (test) => {
|
|
99
|
+
const { promise, resolve } = allocPromise()
|
|
100
|
+
const { label, url, scripts, endpoint = () => { } } = test
|
|
108
101
|
|
|
109
102
|
const listenerIndex = listeners.length
|
|
110
103
|
let pageUrl
|
|
@@ -130,6 +123,7 @@ async function capabilities (job) {
|
|
|
130
123
|
if (done.called) {
|
|
131
124
|
return
|
|
132
125
|
}
|
|
126
|
+
++job[$statusProgressCount]
|
|
133
127
|
done.called = true
|
|
134
128
|
if (timeoutId) {
|
|
135
129
|
clearTimeout(timeoutId)
|
|
@@ -137,15 +131,15 @@ async function capabilities (job) {
|
|
|
137
131
|
await stop(job, pageUrl)
|
|
138
132
|
const timeSpent = Math.floor(performance.now() - now)
|
|
139
133
|
if (error) {
|
|
140
|
-
output.
|
|
134
|
+
output.log('❌', label, `[${filename(pageUrl)}]`, error)
|
|
141
135
|
if (job.failFast) {
|
|
142
136
|
exit(1)
|
|
143
137
|
}
|
|
144
138
|
++errors
|
|
145
139
|
} else {
|
|
146
|
-
output.
|
|
140
|
+
output.log('✔️ ', label, timeSpent, 'ms')
|
|
147
141
|
}
|
|
148
|
-
|
|
142
|
+
resolve()
|
|
149
143
|
}
|
|
150
144
|
|
|
151
145
|
const context = {
|
|
@@ -168,6 +162,8 @@ async function capabilities (job) {
|
|
|
168
162
|
done('Failed')
|
|
169
163
|
}
|
|
170
164
|
})
|
|
165
|
+
|
|
166
|
+
return promise
|
|
171
167
|
}
|
|
172
168
|
|
|
173
169
|
let parallel
|
|
@@ -177,14 +173,14 @@ async function capabilities (job) {
|
|
|
177
173
|
parallel = job.parallel
|
|
178
174
|
}
|
|
179
175
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
176
|
+
job[$statusProgressTotal] = filteredTests.length
|
|
177
|
+
job[$statusProgressCount] = 0
|
|
178
|
+
await parallelize(task, filteredTests, parallel)
|
|
184
179
|
|
|
185
|
-
|
|
180
|
+
output.log('Done.')
|
|
181
|
+
exit(errors)
|
|
186
182
|
} catch (error) {
|
|
187
|
-
output.
|
|
183
|
+
output.error(error)
|
|
188
184
|
exit(-1)
|
|
189
185
|
}
|
|
190
186
|
}
|
|
@@ -34,12 +34,13 @@ module.exports = [{
|
|
|
34
34
|
scripts: ['post.js', 'qunit-redirect.js'],
|
|
35
35
|
endpoint: function (data) {
|
|
36
36
|
assert(data.endpoint === 'addTestPages', 'addTestPages was triggered')
|
|
37
|
-
assert(data.body.
|
|
37
|
+
assert(data.body.type === 'suite', 'type = suite')
|
|
38
|
+
assert(data.body.pages.length === 2, 'Two pages received')
|
|
38
39
|
const pages = [
|
|
39
40
|
'/unit/unitTests.qunit.html',
|
|
40
41
|
'/integration/opaTests.iframe.qunit.html'
|
|
41
42
|
]
|
|
42
|
-
pages.forEach((page, index) => assert(data.body[index].endsWith(page), page))
|
|
43
|
+
pages.forEach((page, index) => assert(data.body.pages[index].endsWith(page), page))
|
|
43
44
|
}
|
|
44
45
|
}, {
|
|
45
46
|
label: 'Scripts (External QUnit)',
|
|
@@ -34,8 +34,17 @@ async function main () {
|
|
|
34
34
|
tests="${module.tests.length}"
|
|
35
35
|
>`)
|
|
36
36
|
for (const test of module.tests) {
|
|
37
|
+
let time
|
|
38
|
+
if (test.start && test.end) {
|
|
39
|
+
time = (new Date(test.end) - new Date(test.start)) / 1000
|
|
40
|
+
}
|
|
37
41
|
o(` <testcase
|
|
38
|
-
name="${xmlEscape(test.name)}"
|
|
42
|
+
name="${xmlEscape(test.name)}" ${
|
|
43
|
+
time === undefined
|
|
44
|
+
? ''
|
|
45
|
+
: `
|
|
46
|
+
time="${time}"`
|
|
47
|
+
}
|
|
39
48
|
>`)
|
|
40
49
|
if (test.skip) {
|
|
41
50
|
o(' <skipped></skipped>')
|
|
@@ -10,16 +10,17 @@ module.exports = async ({
|
|
|
10
10
|
const chrome = require(join(settings.modules['selenium-webdriver'], 'chrome'))
|
|
11
11
|
|
|
12
12
|
const chromeOptions = new chrome.Options()
|
|
13
|
-
chromeOptions.excludeSwitches('enable-logging')
|
|
14
13
|
if (!options.visible) {
|
|
15
|
-
chromeOptions.addArguments('headless')
|
|
14
|
+
chromeOptions.addArguments('--headless=new')
|
|
15
|
+
chromeOptions.addArguments('--log-level=3')
|
|
16
16
|
}
|
|
17
|
-
chromeOptions.addArguments('start-maximized')
|
|
18
|
-
chromeOptions.addArguments('disable-extensions')
|
|
17
|
+
chromeOptions.addArguments('--start-maximized')
|
|
18
|
+
chromeOptions.addArguments('--disable-extensions')
|
|
19
19
|
chromeOptions.setLoggingPrefs(loggingPreferences)
|
|
20
20
|
if (options.binary) {
|
|
21
|
-
chromeOptions.
|
|
21
|
+
chromeOptions.setChromeBinaryPath(options.binary)
|
|
22
22
|
}
|
|
23
|
+
chromeOptions.excludeSwitches('--enable-logging')
|
|
23
24
|
|
|
24
25
|
const builder = new Builder()
|
|
25
26
|
.forBrowser(Browser.CHROME)
|
package/src/inject/post.js
CHANGED
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
function getModules () {
|
|
22
22
|
if (QUnit.config && QUnit.config.modules) {
|
|
23
|
-
return QUnit.config.modules.map(({ name, tests }) => ({
|
|
23
|
+
return QUnit.config.modules.map(({ name, moduleId, tests }) => ({
|
|
24
24
|
name,
|
|
25
|
+
moduleId,
|
|
25
26
|
tests: tests.map(({ name, testId, skip }) => ({ name, testId, skip }))
|
|
26
27
|
}))
|
|
27
28
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
return // already installed
|
|
7
7
|
}
|
|
8
8
|
window[MODULE] = true
|
|
9
|
+
window['ui5-test-runner/qunit-hooks'] = true // prevents qunit-hooks
|
|
9
10
|
|
|
10
11
|
/* global suite */
|
|
11
12
|
|
|
@@ -21,14 +22,41 @@
|
|
|
21
22
|
|
|
22
23
|
window.jsUnitTestSuite = jsUnitTestSuite
|
|
23
24
|
|
|
25
|
+
let QUnit
|
|
26
|
+
|
|
27
|
+
Object.defineProperty(window, 'QUnit', {
|
|
28
|
+
get: function () {
|
|
29
|
+
return QUnit
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
set: function (value) {
|
|
33
|
+
QUnit = value
|
|
34
|
+
|
|
35
|
+
const { test } = QUnit
|
|
36
|
+
QUnit.test = (label) => test(label, (assert) => assert.ok(true, label))
|
|
37
|
+
|
|
38
|
+
let timeoutId
|
|
39
|
+
QUnit.moduleDone(function () {
|
|
40
|
+
if (timeoutId) {
|
|
41
|
+
clearTimeout(timeoutId)
|
|
42
|
+
}
|
|
43
|
+
timeoutId = setTimeout(notify, 10)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function notify () {
|
|
47
|
+
const modules = QUnit.config.modules.map(({ moduleId }) => moduleId)
|
|
48
|
+
const opa = !!window?.sap?.ui?.test?.Opa5
|
|
49
|
+
post('addTestPages', { type: 'qunit', opa, modules, page: location.toString() })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
24
54
|
window.addEventListener('load', function () {
|
|
25
55
|
if (typeof suite === 'function') {
|
|
26
56
|
suite()
|
|
27
|
-
post('addTestPages', pages)
|
|
28
|
-
} else if (
|
|
29
|
-
post('addTestPages',
|
|
30
|
-
} else {
|
|
31
|
-
post('addTestPages', []) // No page
|
|
57
|
+
post('addTestPages', { type: 'suite', pages })
|
|
58
|
+
} else if (!QUnit) {
|
|
59
|
+
post('addTestPages', { type: 'none ' })
|
|
32
60
|
}
|
|
33
61
|
})
|
|
34
62
|
}())
|
package/src/job-mode.js
CHANGED
package/src/job.js
CHANGED
|
@@ -99,7 +99,7 @@ function getCommand (cwd) {
|
|
|
99
99
|
.option('-fo, --fail-opa-fast [flag]', '[💻🔗] Stop the OPA page execution after the first failing test', boolean, false)
|
|
100
100
|
.option('-k, --keep-alive [flag]', '[💻🔗🧪] Keep the server alive', boolean, false)
|
|
101
101
|
.option('-l, --log-server [flag]', '[💻🔗🧪] Log inner server traces', boolean, false)
|
|
102
|
-
.option('-p, --parallel <count>', '[💻🔗🧪] Number of parallel tests executions', 2)
|
|
102
|
+
.option('-p, --parallel <count>', '[💻🔗🧪] Number of parallel tests executions', integer, 2)
|
|
103
103
|
.option('-b, --browser <command>', '[💻🔗🧪] Browser instantiation command (relative to cwd or use $/ for provided ones)', '$/puppeteer.js')
|
|
104
104
|
.option('--browser-args <argument...>', '[💻🔗🧪] Browser instantiation command parameters (use -- instead)')
|
|
105
105
|
.option('--alternate-npm-path <path>', '[💻🔗] Alternate NPM path to look for packages (priority: local, alternate, global)')
|
|
@@ -118,8 +118,9 @@ function getCommand (cwd) {
|
|
|
118
118
|
.option('--screenshot [flag]', '[💻🔗] Take screenshots during the tests execution (if supported by the browser)', boolean, true)
|
|
119
119
|
.option('--no-screenshot', '[💻🔗] Disable screenshots')
|
|
120
120
|
.option('-st, --screenshot-timeout <timeout>', '[💻🔗] Maximum waiting time for browser screenshot', timeout, 5000)
|
|
121
|
+
.option('-so, --split-opa', '[💻🔗] Split OPA tests using QUnit modules', boolean, false)
|
|
121
122
|
.option('-rg, --report-generator <path...>', '[💻🔗] Report generator paths (relative to cwd or use $/ for provided ones)', ['$/report.js'])
|
|
122
|
-
.option('--progress-page <path>', '[💻🔗]
|
|
123
|
+
.option('--progress-page <path>', '[💻🔗] Progress page path (relative to cwd or use $/ for provided ones)', '$/report/default.html')
|
|
123
124
|
|
|
124
125
|
.option('--coverage [flag]', '[💻🔗] Enable or disable code coverage', boolean)
|
|
125
126
|
.option('--no-coverage', '[💻🔗] Disable code coverage')
|
|
@@ -321,6 +322,25 @@ function finalize (job) {
|
|
|
321
322
|
enumerable: false,
|
|
322
323
|
configurable: false
|
|
323
324
|
})
|
|
325
|
+
|
|
326
|
+
/* istanbul ignore next */
|
|
327
|
+
if (process.env.DEBUG_ON_FAILED) {
|
|
328
|
+
let failed
|
|
329
|
+
Object.defineProperty(job, 'failed', {
|
|
330
|
+
get () {
|
|
331
|
+
return failed
|
|
332
|
+
},
|
|
333
|
+
set (value) {
|
|
334
|
+
if (value) {
|
|
335
|
+
// eslint-disable-next-line no-debugger
|
|
336
|
+
debugger
|
|
337
|
+
}
|
|
338
|
+
failed = value
|
|
339
|
+
},
|
|
340
|
+
enumerable: true,
|
|
341
|
+
configurable: false
|
|
342
|
+
})
|
|
343
|
+
}
|
|
324
344
|
}
|
|
325
345
|
|
|
326
346
|
function fromCmdLine (cwd, args) {
|
package/src/output.js
CHANGED
|
@@ -79,7 +79,7 @@ function * bar (ratio, msg) {
|
|
|
79
79
|
}
|
|
80
80
|
yield '] '
|
|
81
81
|
} else {
|
|
82
|
-
const filled = Math.floor(BAR_WIDTH * ratio)
|
|
82
|
+
const filled = Math.floor(BAR_WIDTH * Math.min(ratio, 1))
|
|
83
83
|
yield ''.padEnd(filled, '\u2588')
|
|
84
84
|
yield ''.padEnd(BAR_WIDTH - filled, '\u2591')
|
|
85
85
|
yield '] '
|
|
@@ -117,6 +117,13 @@ class Progress {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
function progress (job, cleanFirst = true) {
|
|
120
|
+
if (process.send) {
|
|
121
|
+
process.send({
|
|
122
|
+
type: 'progress',
|
|
123
|
+
count: job[$statusProgressCount],
|
|
124
|
+
total: job[$statusProgressTotal]
|
|
125
|
+
})
|
|
126
|
+
}
|
|
120
127
|
const sequence = []
|
|
121
128
|
if (interactive) {
|
|
122
129
|
if (cleanFirst) {
|
|
@@ -250,8 +257,6 @@ function build (job) {
|
|
|
250
257
|
reportIntervalId: undefined,
|
|
251
258
|
lines: 0,
|
|
252
259
|
|
|
253
|
-
wrap: wrap(callback => callback()),
|
|
254
|
-
|
|
255
260
|
version: () => {
|
|
256
261
|
const { name, version } = require(join(__dirname, '../package.json'))
|
|
257
262
|
log(job, p80()`${name}@${version}`)
|
|
@@ -261,6 +266,14 @@ function build (job) {
|
|
|
261
266
|
log(job, p80()`Server running at ${pad.lt(url)}`)
|
|
262
267
|
},
|
|
263
268
|
|
|
269
|
+
log: wrap((...texts) => {
|
|
270
|
+
log(job, ...texts)
|
|
271
|
+
}),
|
|
272
|
+
|
|
273
|
+
error: wrap((...texts) => {
|
|
274
|
+
err(job, ...texts)
|
|
275
|
+
}),
|
|
276
|
+
|
|
264
277
|
debug: (moduleSpecifier, ...args) => {
|
|
265
278
|
const [mainModule] = moduleSpecifier.split('/')
|
|
266
279
|
if (job.debugVerbose && (job.debugVerbose.includes(moduleSpecifier) || job.debugVerbose.includes(mainModule))) {
|
|
@@ -310,7 +323,7 @@ function build (job) {
|
|
|
310
323
|
reportOnJobProgress () {
|
|
311
324
|
if (interactive) {
|
|
312
325
|
this.reportIntervalId = setInterval(progress.bind(null, job), 250)
|
|
313
|
-
} else if (job.outputInterval) {
|
|
326
|
+
} else if (job.outputInterval && !inJest) {
|
|
314
327
|
this.reportIntervalId = setInterval(progress.bind(null, job), job.outputInterval)
|
|
315
328
|
}
|
|
316
329
|
},
|
|
@@ -563,6 +576,8 @@ function build (job) {
|
|
|
563
576
|
}
|
|
564
577
|
|
|
565
578
|
module.exports = {
|
|
579
|
+
interactive,
|
|
580
|
+
|
|
566
581
|
getOutput (job) {
|
|
567
582
|
if (!job[$output]) {
|
|
568
583
|
job[$output] = build(job)
|
|
@@ -570,7 +585,9 @@ module.exports = {
|
|
|
570
585
|
return job[$output]
|
|
571
586
|
},
|
|
572
587
|
|
|
573
|
-
newProgress (job) {
|
|
574
|
-
|
|
588
|
+
newProgress (job, label, total, count) {
|
|
589
|
+
const progress = new Progress(job)
|
|
590
|
+
Object.assign(progress, { label, total, count })
|
|
591
|
+
return progress
|
|
575
592
|
}
|
|
576
593
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const { allocPromise } = require('./tools')
|
|
2
|
+
|
|
3
|
+
function complete (task) {
|
|
4
|
+
++task.completed
|
|
5
|
+
if (--task.active === 0) {
|
|
6
|
+
task.resolve()
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function run (task) {
|
|
11
|
+
const {
|
|
12
|
+
method,
|
|
13
|
+
list,
|
|
14
|
+
parallel,
|
|
15
|
+
started,
|
|
16
|
+
completed,
|
|
17
|
+
stop,
|
|
18
|
+
reject
|
|
19
|
+
} = task
|
|
20
|
+
const { length } = list
|
|
21
|
+
if (stop || completed === length || started === length) {
|
|
22
|
+
complete(task)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
if (task.active < parallel && length - started > task.active) {
|
|
26
|
+
++task.active
|
|
27
|
+
run(task)
|
|
28
|
+
}
|
|
29
|
+
const index = task.started++
|
|
30
|
+
const parameter = list[index]
|
|
31
|
+
try {
|
|
32
|
+
await method(parameter, index, list)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
task.stop = true
|
|
35
|
+
reject(error)
|
|
36
|
+
}
|
|
37
|
+
let remaining = list.length - index - 1
|
|
38
|
+
while (task.active < (parallel + 1) && remaining) {
|
|
39
|
+
--remaining
|
|
40
|
+
++task.active
|
|
41
|
+
run(task)
|
|
42
|
+
}
|
|
43
|
+
complete(task)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = function parallelize (method, list, parallel) {
|
|
47
|
+
const { promise, resolve, reject } = allocPromise()
|
|
48
|
+
const task = {
|
|
49
|
+
method,
|
|
50
|
+
list,
|
|
51
|
+
parallel,
|
|
52
|
+
started: 0,
|
|
53
|
+
completed: 0,
|
|
54
|
+
active: 1,
|
|
55
|
+
stop: false,
|
|
56
|
+
resolve,
|
|
57
|
+
reject
|
|
58
|
+
}
|
|
59
|
+
run(task)
|
|
60
|
+
return promise
|
|
61
|
+
}
|
package/src/qunit-hooks.js
CHANGED
|
@@ -21,7 +21,6 @@ function invalidTestId (job, url, testId) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function merge (targetModules, modules) {
|
|
24
|
-
let count = 0
|
|
25
24
|
modules.forEach(module => {
|
|
26
25
|
const { name } = module
|
|
27
26
|
const targetModule = targetModules.filter(({ name: targetName }) => name === targetName)[0]
|
|
@@ -35,9 +34,16 @@ function merge (targetModules, modules) {
|
|
|
35
34
|
}
|
|
36
35
|
})
|
|
37
36
|
}
|
|
38
|
-
count += module.tests.length
|
|
39
37
|
})
|
|
40
|
-
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function filterModules (modules, url) {
|
|
41
|
+
const moduleMatch = url.match(/\?.*\bmoduleId=([^&]+)/)
|
|
42
|
+
if (moduleMatch) {
|
|
43
|
+
const [, moduleId] = moduleMatch
|
|
44
|
+
return modules.filter(module => module.moduleId === moduleId)
|
|
45
|
+
}
|
|
46
|
+
return modules
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
function get (job, urlWithHash, { testId, modules, isOpa } = {}) {
|
|
@@ -47,9 +53,8 @@ function get (job, urlWithHash, { testId, modules, isOpa } = {}) {
|
|
|
47
53
|
if (!page) {
|
|
48
54
|
error(job, url, `No QUnit page found for ${urlWithHash}`)
|
|
49
55
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
56
|
+
merge(page.modules, filterModules(modules || [], url))
|
|
57
|
+
progress.total = page.count = page.modules.reduce((total, { tests }) => total + tests.length, 0)
|
|
53
58
|
if (!page.isOpa && isOpa) {
|
|
54
59
|
page.isOpa = true
|
|
55
60
|
}
|
|
@@ -104,7 +109,7 @@ module.exports = {
|
|
|
104
109
|
|
|
105
110
|
async begin (job, urlWithHash, details) {
|
|
106
111
|
getOutput(job).debug('qunit/begin', 'begin', urlWithHash, details)
|
|
107
|
-
const { isOpa,
|
|
112
|
+
const { isOpa, modules } = details
|
|
108
113
|
const url = stripUrlHash(urlWithHash)
|
|
109
114
|
if (!job.qunitPages) {
|
|
110
115
|
job.qunitPages = {}
|
|
@@ -115,13 +120,13 @@ module.exports = {
|
|
|
115
120
|
isOpa: !!isOpa,
|
|
116
121
|
failed: 0,
|
|
117
122
|
passed: 0,
|
|
118
|
-
count:
|
|
119
|
-
modules
|
|
123
|
+
count: 0,
|
|
124
|
+
modules: []
|
|
120
125
|
}
|
|
121
126
|
job.qunitPages[url] = qunitPage
|
|
122
|
-
const { progress } = get(job, url)
|
|
127
|
+
const { page, progress } = get(job, url, { modules })
|
|
123
128
|
progress.count = 0
|
|
124
|
-
progress.total =
|
|
129
|
+
progress.total = page.count
|
|
125
130
|
},
|
|
126
131
|
|
|
127
132
|
async testStart (job, urlWithHash, details) {
|
package/src/symbols.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
$browsers: Symbol('browsers'),
|
|
3
|
-
$probeUrlsStarted: Symbol('probeUrlsStarted'),
|
|
4
|
-
$probeUrlsCompleted: Symbol('probeUrlsCompleted'),
|
|
5
|
-
$testPagesStarted: Symbol('testPagesStarted'),
|
|
6
|
-
$testPagesCompleted: Symbol('testPagesCompleted'),
|
|
7
3
|
$valueSources: Symbol('valueSources'),
|
|
8
4
|
$remoteOnLegacy: Symbol('remoteOnLegacy'),
|
|
9
5
|
$proxifiedUrls: Symbol('proxifiedUrls'),
|
package/src/tests.js
CHANGED
|
@@ -1,68 +1,49 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { probe, start } = require('./browsers')
|
|
3
|
+
const { probe: probeBrowser, start } = require('./browsers')
|
|
4
4
|
const { instrument } = require('./coverage')
|
|
5
5
|
const { recreateDir } = require('./tools')
|
|
6
6
|
const { globallyTimedOut } = require('./timeout')
|
|
7
7
|
const { save, generate } = require('./report')
|
|
8
8
|
const { getOutput } = require('./output')
|
|
9
9
|
const {
|
|
10
|
-
$probeUrlsStarted,
|
|
11
|
-
$probeUrlsCompleted,
|
|
12
|
-
$testPagesStarted,
|
|
13
|
-
$testPagesCompleted,
|
|
14
10
|
$statusProgressTotal,
|
|
15
|
-
$statusProgressCount
|
|
11
|
+
$statusProgressCount,
|
|
12
|
+
$proxifiedUrls
|
|
16
13
|
} = require('./symbols')
|
|
17
14
|
const { UTRError } = require('./error')
|
|
18
|
-
const { $proxifiedUrls } = require('./symbols')
|
|
19
15
|
const { preload } = require('./ui5')
|
|
16
|
+
const parallelize = require('./parallelize')
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
method
|
|
27
|
-
} = task
|
|
28
|
-
const output = getOutput(job)
|
|
29
|
-
const urls = job[urlsMember]
|
|
30
|
-
const { length } = urls
|
|
31
|
-
if (job[$statusProgressTotal] === undefined) {
|
|
18
|
+
function task (job, method) {
|
|
19
|
+
return async (url, index, { length }) => {
|
|
20
|
+
if (job[$statusProgressCount] === undefined) {
|
|
21
|
+
job[$statusProgressCount] = 0
|
|
22
|
+
}
|
|
32
23
|
job[$statusProgressTotal] = length
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
if (job[startedMember] === length) {
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
const index = job[startedMember]++
|
|
42
|
-
const url = urls[index]
|
|
43
|
-
if (globallyTimedOut(job)) {
|
|
44
|
-
output.globalTimeout(url)
|
|
45
|
-
job.failed = true
|
|
46
|
-
job.timedOut = true
|
|
47
|
-
} else if (job.failFast && job.failed) {
|
|
48
|
-
output.failFast(url)
|
|
49
|
-
} else {
|
|
50
|
-
try {
|
|
51
|
-
await method(job, url)
|
|
52
|
-
} catch (error) {
|
|
24
|
+
const output = getOutput(job)
|
|
25
|
+
if (globallyTimedOut(job)) {
|
|
26
|
+
output.globalTimeout(url)
|
|
53
27
|
job.failed = true
|
|
28
|
+
job.timedOut = true
|
|
29
|
+
} else if (job.failFast && job.failed) {
|
|
30
|
+
output.failFast(url)
|
|
31
|
+
} else {
|
|
32
|
+
try {
|
|
33
|
+
await method(job, url)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
job.failed = true
|
|
36
|
+
}
|
|
54
37
|
}
|
|
38
|
+
++job[$statusProgressCount]
|
|
55
39
|
}
|
|
56
|
-
++job[completedMember]
|
|
57
|
-
++job[$statusProgressCount]
|
|
58
|
-
return run(task, job)
|
|
59
40
|
}
|
|
60
41
|
|
|
61
42
|
async function probeUrl (job, url) {
|
|
62
43
|
const output = getOutput(job)
|
|
63
44
|
try {
|
|
64
45
|
let scripts
|
|
65
|
-
if (job.
|
|
46
|
+
if (job.browserCapabilities.scripts) {
|
|
66
47
|
scripts = [
|
|
67
48
|
'post.js',
|
|
68
49
|
'qunit-redirect.js'
|
|
@@ -111,48 +92,32 @@ async function runTestPage (job, url) {
|
|
|
111
92
|
}
|
|
112
93
|
}
|
|
113
94
|
|
|
114
|
-
function parallelize (task, job) {
|
|
115
|
-
const {
|
|
116
|
-
urlsMember,
|
|
117
|
-
completedMember,
|
|
118
|
-
startedMember
|
|
119
|
-
} = task
|
|
120
|
-
job[startedMember] = 0
|
|
121
|
-
job[completedMember] = 0
|
|
122
|
-
const max = Math.min(job.parallel, job[urlsMember].length)
|
|
123
|
-
const promises = []
|
|
124
|
-
for (let i = 0; i < max; ++i) {
|
|
125
|
-
promises.push(run(task, job))
|
|
126
|
-
}
|
|
127
|
-
return Promise.all(promises)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
95
|
async function process (job) {
|
|
131
96
|
const output = getOutput(job)
|
|
132
97
|
job.start = new Date()
|
|
133
|
-
|
|
98
|
+
job.failed = false
|
|
134
99
|
await instrument(job)
|
|
135
100
|
await save(job)
|
|
136
101
|
job.testPageUrls = []
|
|
137
102
|
|
|
138
103
|
job.status = 'Probing urls'
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
104
|
+
try {
|
|
105
|
+
await parallelize(task(job, probeUrl), job.url, job.parallel)
|
|
106
|
+
} catch (e) {
|
|
107
|
+
output.genericError(e)
|
|
108
|
+
job.failed = true
|
|
109
|
+
}
|
|
145
110
|
|
|
146
111
|
/* istanbul ignore else */
|
|
147
|
-
if (!job.debugProbeOnly) {
|
|
112
|
+
if (!job.debugProbeOnly && !job.failed) {
|
|
148
113
|
if (job.testPageUrls.length !== 0) {
|
|
149
114
|
job.status = 'Executing test pages'
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
115
|
+
try {
|
|
116
|
+
await parallelize(task(job, runTestPage), job.testPageUrls, job.parallel)
|
|
117
|
+
} catch (e) {
|
|
118
|
+
output.genericError(e)
|
|
119
|
+
job.failed = true
|
|
120
|
+
}
|
|
156
121
|
} else if (Object.keys(job.qunitPages || []).length === 0) {
|
|
157
122
|
output.noTestPageFound()
|
|
158
123
|
job.failed = true
|
|
@@ -169,7 +134,7 @@ module.exports = {
|
|
|
169
134
|
if (job.preload) {
|
|
170
135
|
await preload(job)
|
|
171
136
|
}
|
|
172
|
-
await
|
|
137
|
+
await probeBrowser(job)
|
|
173
138
|
if (job.mode !== 'url') {
|
|
174
139
|
job.url = [`http://localhost:${job.port}/${job.testsuite}`]
|
|
175
140
|
} else if (!job.browserCapabilities.scripts) {
|
package/src/ui5.js
CHANGED
|
@@ -5,8 +5,9 @@ const { createWriteStream } = require('fs')
|
|
|
5
5
|
const { mkdir, unlink, stat } = require('fs').promises
|
|
6
6
|
const { capture } = require('reserve')
|
|
7
7
|
const { getOutput, newProgress } = require('./output')
|
|
8
|
-
const { download
|
|
8
|
+
const { download } = require('./tools')
|
|
9
9
|
const { $statusProgressCount, $statusProgressTotal } = require('./symbols')
|
|
10
|
+
const parallelize = require('./parallelize')
|
|
10
11
|
|
|
11
12
|
const buildCacheBase = job => {
|
|
12
13
|
const [, hostName] = /https?:\/\/([^/]*)/.exec(job.ui5)
|
|
@@ -34,31 +35,15 @@ module.exports = {
|
|
|
34
35
|
const lib = async name => {
|
|
35
36
|
progress.label = name
|
|
36
37
|
progress.count = 0
|
|
37
|
-
const { promise, resolve/*, reject */ } = allocPromise()
|
|
38
38
|
const libPath = name.replace(/\./g, '/') + '/'
|
|
39
39
|
const { resources } = require(await get(libPath + 'resources.json'))
|
|
40
40
|
progress.total = resources.length
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const task = async () => {
|
|
45
|
-
++active
|
|
46
|
-
const { name, size } = resources[index++]
|
|
41
|
+
progress.label = `${name} (${resources.length} files)`
|
|
42
|
+
await parallelize(async ({ name, size }) => {
|
|
47
43
|
await get(libPath + name, size)
|
|
48
44
|
++progress.count
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
if (--active === 0) {
|
|
53
|
-
resolve()
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
for (let parallel = 0; parallel < 8; ++parallel) {
|
|
58
|
-
task()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return promise
|
|
45
|
+
}, resources, 8)
|
|
46
|
+
++job[$statusProgressCount]
|
|
62
47
|
}
|
|
63
48
|
|
|
64
49
|
job.status = 'Preloading UI5'
|
|
@@ -67,11 +52,7 @@ module.exports = {
|
|
|
67
52
|
await get('sap-ui-version.json')
|
|
68
53
|
await get('sap-ui-core.js')
|
|
69
54
|
const progress = newProgress(job)
|
|
70
|
-
await lib
|
|
71
|
-
for (const name of job.preload) {
|
|
72
|
-
++job[$statusProgressCount]
|
|
73
|
-
await lib(name)
|
|
74
|
-
}
|
|
55
|
+
await parallelize(lib, ['sap.ui.core', ...job.preload], 1)
|
|
75
56
|
progress.done()
|
|
76
57
|
},
|
|
77
58
|
|