qunitx-cli 0.5.6 → 0.5.8
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/.dockerignore +15 -0
- package/README.md +29 -0
- package/deno.json +5 -0
- package/deno.lock +6 -2
- package/lib/commands/help.js +1 -0
- package/lib/commands/run/tests-in-browser.js +1 -1
- package/lib/commands/run.js +1 -1
- package/lib/setup/default-project-config-values.js +2 -1
- package/lib/setup/file-watcher.js +59 -48
- package/lib/setup/fs-tree.js +14 -8
- package/lib/tap/display-test-result.js +1 -1
- package/lib/utils/parse-cli-flags.js +8 -1
- package/package.json +1 -2
- package/lib/setup/recursive-lookup.d.ts +0 -7
- package/scripts/lint-docs.js +0 -40
package/.dockerignore
ADDED
package/README.md
CHANGED
|
@@ -133,6 +133,34 @@ qunitx some-test.js --debug
|
|
|
133
133
|
qunitx some-test.ts
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
## Configuration
|
|
137
|
+
|
|
138
|
+
All CLI flags can also be set in `package.json` under the `qunitx` key, so you don't have to repeat them on every invocation:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"qunitx": {
|
|
143
|
+
"inputs": ["test/**/*-test.js", "test/**/*-test.ts"],
|
|
144
|
+
"extensions": ["js", "ts"],
|
|
145
|
+
"output": "tmp",
|
|
146
|
+
"timeout": 20000,
|
|
147
|
+
"failFast": false,
|
|
148
|
+
"port": 1234
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Key | Default | Description |
|
|
154
|
+
|-----|---------|-------------|
|
|
155
|
+
| `inputs` | `[]` | Glob patterns, file paths, or directories to use as test entry points. Merged with any paths given on the CLI. |
|
|
156
|
+
| `extensions` | `["js", "ts"]` | File extensions tracked for test discovery (directory scans) and watch-mode rebuild triggers. Add `"mjs"`, `"cjs"`, or any other extension your project uses. |
|
|
157
|
+
| `output` | `"tmp"` | Directory where compiled test bundles are written. |
|
|
158
|
+
| `timeout` | `20000` | Maximum milliseconds to wait for the full test suite before timing out. |
|
|
159
|
+
| `failFast` | `false` | Stop the run after the first failing test. |
|
|
160
|
+
| `port` | `1234` | Preferred HTTP server port. qunitx auto-selects a free port if this one is taken. |
|
|
161
|
+
|
|
162
|
+
CLI flags always override `package.json` values when both are present.
|
|
163
|
+
|
|
136
164
|
## CLI Reference
|
|
137
165
|
|
|
138
166
|
```
|
|
@@ -144,6 +172,7 @@ Options:
|
|
|
144
172
|
--debug Print the server URL; pipe browser console to stdout
|
|
145
173
|
--timeout=<ms> Max ms to wait for the suite to finish [default: 20000]
|
|
146
174
|
--output=<dir> Directory for compiled test assets [default: ./tmp]
|
|
175
|
+
--extensions=<...> Comma-separated file extensions to track [default: js,ts]
|
|
147
176
|
--before=<file> Script to run (and optionally await) before tests start
|
|
148
177
|
--after=<file> Script to run (and optionally await) after tests finish
|
|
149
178
|
--port=<n> HTTP server port (auto-selects a free port if taken)
|
package/deno.json
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"recursive-lookup": "npm:recursive-lookup",
|
|
12
12
|
"ws": "npm:ws"
|
|
13
13
|
},
|
|
14
|
+
"tasks": {
|
|
15
|
+
"bench": "deno bench --allow-all benches/esbuild.bench.ts benches/server.bench.ts benches/tap.bench.ts benches/e2e.bench.ts",
|
|
16
|
+
"bench:check": "deno bench --allow-all --json benches/esbuild.bench.ts benches/server.bench.ts benches/tap.bench.ts benches/e2e.bench.ts | deno run --allow-all scripts/check-benchmarks.ts",
|
|
17
|
+
"bench:update": "deno bench --allow-all --json benches/esbuild.bench.ts benches/server.bench.ts benches/tap.bench.ts benches/e2e.bench.ts | deno run --allow-all scripts/check-benchmarks.ts --save"
|
|
18
|
+
},
|
|
14
19
|
"lint": {
|
|
15
20
|
"rules": {
|
|
16
21
|
"exclude": ["no-process-global", "no-node-globals", "no-window"]
|
package/deno.lock
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "5",
|
|
3
3
|
"specifiers": {
|
|
4
|
+
"jsr:@std/fmt@*": "1.0.9",
|
|
4
5
|
"npm:@types/js-yaml@*": "4.0.9",
|
|
5
6
|
"npm:@types/picomatch@*": "4.0.2",
|
|
6
7
|
"npm:@types/ws@*": "8.18.1",
|
|
@@ -24,10 +25,14 @@
|
|
|
24
25
|
"npm:qunit@^2.25.0": "2.25.0",
|
|
25
26
|
"npm:qunitx@1": "1.0.0",
|
|
26
27
|
"npm:recursive-lookup@*": "1.1.0",
|
|
27
|
-
"npm:recursive-lookup@1.1.0": "1.1.0",
|
|
28
28
|
"npm:ws@*": "8.19.0",
|
|
29
29
|
"npm:ws@^8.19.0": "8.19.0"
|
|
30
30
|
},
|
|
31
|
+
"jsr": {
|
|
32
|
+
"@std/fmt@1.0.9": {
|
|
33
|
+
"integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
31
36
|
"npm": {
|
|
32
37
|
"@babel/code-frame@7.29.0": {
|
|
33
38
|
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
|
@@ -1333,7 +1338,6 @@
|
|
|
1333
1338
|
"npm:puppeteer@^24.38.0",
|
|
1334
1339
|
"npm:qunit@^2.25.0",
|
|
1335
1340
|
"npm:qunitx@1",
|
|
1336
|
-
"npm:recursive-lookup@1.1.0",
|
|
1337
1341
|
"npm:ws@^8.19.0"
|
|
1338
1342
|
]
|
|
1339
1343
|
}
|
package/lib/commands/help.js
CHANGED
|
@@ -23,6 +23,7 @@ ${color('--timeout')} : change default timeout per test case
|
|
|
23
23
|
${color('--output')} : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
|
|
24
24
|
${color('--failFast')} : run the target file or folders with immediate abort if a single test fails
|
|
25
25
|
${color('--port')} : HTTP server port (auto-selects a free port if the given port is taken)[default: 1234]
|
|
26
|
+
${color('--extensions')} : comma-separated file extensions to track for discovery and watch-mode rebuilds[default: js,ts]
|
|
26
27
|
${color('--before')} : run a script before the tests(i.e start a new web server before tests)
|
|
27
28
|
${color('--after')} : run a script after the tests(i.e save test results to a file)
|
|
28
29
|
|
|
@@ -63,7 +63,7 @@ export default async function runTestsInBrowser(
|
|
|
63
63
|
|
|
64
64
|
// In group mode the COUNTER is shared across all groups and managed by run.js.
|
|
65
65
|
if (!config._groupMode) {
|
|
66
|
-
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 };
|
|
66
|
+
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 };
|
|
67
67
|
}
|
|
68
68
|
config.lastRanTestFiles = targetTestFilesToFilter || allTestFilePaths;
|
|
69
69
|
|
package/lib/commands/run.js
CHANGED
|
@@ -71,7 +71,7 @@ export default async function run(config) {
|
|
|
71
71
|
const groups = splitIntoGroups(allFiles, groupCount);
|
|
72
72
|
|
|
73
73
|
// Shared COUNTER so TAP test numbers are globally sequential across all groups.
|
|
74
|
-
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 };
|
|
74
|
+
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 };
|
|
75
75
|
config.lastRanTestFiles = allFiles;
|
|
76
76
|
|
|
77
77
|
const groupConfigs = groups.map((groupFiles, i) => ({
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/** Default qunitx config values: build output directory, test timeout (ms), fail-fast flag,
|
|
1
|
+
/** Default qunitx config values: build output directory, test timeout (ms), fail-fast flag, HTTP server port, and tracked file extensions. */
|
|
2
2
|
export default {
|
|
3
3
|
output: 'tmp',
|
|
4
4
|
timeout: 20000,
|
|
5
5
|
failFast: false,
|
|
6
6
|
port: 1234,
|
|
7
|
+
extensions: ['js', 'ts'],
|
|
7
8
|
};
|
|
@@ -6,52 +6,14 @@ import kleur from 'kleur';
|
|
|
6
6
|
* @returns {object}
|
|
7
7
|
*/
|
|
8
8
|
export default function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
9
|
-
const extensions = ['js', 'ts'];
|
|
9
|
+
const extensions = config.extensions || ['js', 'ts'];
|
|
10
10
|
const fileWatchers = testFileLookupPaths.reduce((watcher, watchPath) => {
|
|
11
11
|
return Object.assign(watcher, {
|
|
12
|
-
[watchPath]: chokidar
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'#',
|
|
18
|
-
kleur
|
|
19
|
-
.magenta()
|
|
20
|
-
.bold('=================================================================='),
|
|
21
|
-
);
|
|
22
|
-
console.log('#', getEventColor(event), path.split(config.projectRoot)[1]);
|
|
23
|
-
console.log(
|
|
24
|
-
'#',
|
|
25
|
-
kleur
|
|
26
|
-
.magenta()
|
|
27
|
-
.bold('=================================================================='),
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
if (!global.chokidarBuild) {
|
|
31
|
-
global.chokidarBuild = true;
|
|
32
|
-
|
|
33
|
-
const result = extensions.some((extension) => path.endsWith(extension))
|
|
34
|
-
? onEventFunc(event, path)
|
|
35
|
-
: null;
|
|
36
|
-
|
|
37
|
-
if (!(result instanceof Promise)) {
|
|
38
|
-
global.chokidarBuild = false;
|
|
39
|
-
|
|
40
|
-
return result;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
result
|
|
44
|
-
.then(() => {
|
|
45
|
-
onFinishFunc ? onFinishFunc(event, path) : null;
|
|
46
|
-
})
|
|
47
|
-
.catch(() => {
|
|
48
|
-
// TODO: make an index.html to display the error
|
|
49
|
-
// error type has to be derived from the error!
|
|
50
|
-
})
|
|
51
|
-
.finally(() => (global.chokidarBuild = false));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}),
|
|
12
|
+
[watchPath]: chokidar
|
|
13
|
+
.watch(watchPath, { ignoreInitial: true })
|
|
14
|
+
.on('all', (event, filePath) =>
|
|
15
|
+
handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc),
|
|
16
|
+
),
|
|
55
17
|
});
|
|
56
18
|
}, {});
|
|
57
19
|
|
|
@@ -65,15 +27,64 @@ export default function setupFileWatchers(testFileLookupPaths, config, onEventFu
|
|
|
65
27
|
};
|
|
66
28
|
}
|
|
67
29
|
|
|
68
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Routes a chokidar event to fsTree mutation and optional rebuild trigger.
|
|
32
|
+
* `unlinkDir` bypasses the extension filter so deleted directories always clean up fsTree.
|
|
33
|
+
* @returns {void}
|
|
34
|
+
*/
|
|
35
|
+
export function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
|
|
36
|
+
const isFileEvent = extensions.some((ext) => filePath.endsWith(`.${ext}`));
|
|
37
|
+
|
|
38
|
+
if (!isFileEvent && event !== 'unlinkDir') return;
|
|
39
|
+
|
|
40
|
+
mutateFSTree(config.fsTree, event, filePath);
|
|
41
|
+
|
|
42
|
+
console.log(
|
|
43
|
+
'#',
|
|
44
|
+
kleur.magenta().bold('=================================================================='),
|
|
45
|
+
);
|
|
46
|
+
console.log('#', getEventColor(event), filePath.split(config.projectRoot)[1]);
|
|
47
|
+
console.log(
|
|
48
|
+
'#',
|
|
49
|
+
kleur.magenta().bold('=================================================================='),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!config._building) {
|
|
53
|
+
config._building = true;
|
|
54
|
+
|
|
55
|
+
const result = onEventFunc(event, filePath);
|
|
56
|
+
|
|
57
|
+
if (!(result instanceof Promise)) {
|
|
58
|
+
config._building = false;
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
result
|
|
64
|
+
.then(() => {
|
|
65
|
+
onFinishFunc ? onFinishFunc(event, filePath) : null;
|
|
66
|
+
})
|
|
67
|
+
.catch(() => {
|
|
68
|
+
// TODO: make an index.html to display the error
|
|
69
|
+
// error type has to be derived from the error!
|
|
70
|
+
})
|
|
71
|
+
.finally(() => (config._building = false));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Mutates `fsTree` in place based on a chokidar file-system event.
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*/
|
|
79
|
+
export function mutateFSTree(fsTree, event, path) {
|
|
69
80
|
if (event === 'add') {
|
|
70
81
|
fsTree[path] = null;
|
|
71
82
|
} else if (event === 'unlink') {
|
|
72
83
|
delete fsTree[path];
|
|
73
84
|
} else if (event === 'unlinkDir') {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
for (const treePath of Object.keys(fsTree)) {
|
|
86
|
+
if (treePath.startsWith(path)) delete fsTree[treePath];
|
|
87
|
+
}
|
|
77
88
|
}
|
|
78
89
|
}
|
|
79
90
|
|
package/lib/setup/fs-tree.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
// @deno-types="npm:@types/picomatch"
|
|
3
4
|
import picomatch from 'picomatch';
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
async function readDirRecursive(dir, filter) {
|
|
7
|
+
const entries = await fs.readdir(dir, { recursive: true, withFileTypes: true });
|
|
8
|
+
return entries
|
|
9
|
+
.filter((e) => e.isFile() && filter(e.name))
|
|
10
|
+
.map((e) => path.join(e.parentPath, e.name));
|
|
11
|
+
}
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* Resolves an array of file paths, directories, or glob patterns into a flat `{ absolutePath: null }` map.
|
|
9
15
|
* @returns {Promise<object>}
|
|
10
16
|
*/
|
|
11
|
-
export default async function buildFSTree(fileAbsolutePaths,
|
|
12
|
-
const targetExtensions = ['js', 'ts'];
|
|
17
|
+
export default async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
18
|
+
const targetExtensions = config.extensions || ['js', 'ts'];
|
|
13
19
|
const fsTree = {};
|
|
14
20
|
|
|
15
21
|
await Promise.all(
|
|
@@ -20,8 +26,8 @@ export default async function buildFSTree(fileAbsolutePaths, _config = {}) {
|
|
|
20
26
|
|
|
21
27
|
try {
|
|
22
28
|
if (glob.isGlob) {
|
|
23
|
-
const fileNames = await
|
|
24
|
-
return targetExtensions.some((extension) =>
|
|
29
|
+
const fileNames = await readDirRecursive(glob.base, (name) => {
|
|
30
|
+
return targetExtensions.some((extension) => name.endsWith(`.${extension}`));
|
|
25
31
|
});
|
|
26
32
|
|
|
27
33
|
fileNames.forEach((fileName) => {
|
|
@@ -35,8 +41,8 @@ export default async function buildFSTree(fileAbsolutePaths, _config = {}) {
|
|
|
35
41
|
if (entry.isFile()) {
|
|
36
42
|
fsTree[fileAbsolutePath] = null;
|
|
37
43
|
} else if (entry.isDirectory()) {
|
|
38
|
-
const fileNames = await
|
|
39
|
-
return targetExtensions.some((extension) =>
|
|
44
|
+
const fileNames = await readDirRecursive(glob.base, (name) => {
|
|
45
|
+
return targetExtensions.some((extension) => name.endsWith(`.${extension}`));
|
|
40
46
|
});
|
|
41
47
|
|
|
42
48
|
fileNames.forEach((fileName) => {
|
|
@@ -26,7 +26,7 @@ export default function TAPDisplayTestResult(COUNTER, details) {
|
|
|
26
26
|
);
|
|
27
27
|
details.assertions.reduce((errorCount, assertion, index) => {
|
|
28
28
|
if (!assertion.passed && assertion.todo === false) {
|
|
29
|
-
COUNTER.errorCount
|
|
29
|
+
COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
|
|
30
30
|
const stack = assertion.stack?.match(/\(.+\)/g);
|
|
31
31
|
|
|
32
32
|
console.log(' ---');
|
|
@@ -13,7 +13,7 @@ export default function parseCliFlags(projectRoot) {
|
|
|
13
13
|
} else if (arg.startsWith('--failfast') || arg.startsWith('--failFast')) {
|
|
14
14
|
return Object.assign(result, { failFast: parseBoolean(arg.split('=')[1]) });
|
|
15
15
|
} else if (arg.startsWith('--timeout')) {
|
|
16
|
-
return Object.assign(result, { timeout: arg.split('=')[1] || 10000 });
|
|
16
|
+
return Object.assign(result, { timeout: Number(arg.split('=')[1]) || 10000 });
|
|
17
17
|
} else if (arg.startsWith('--output')) {
|
|
18
18
|
return Object.assign(result, { output: arg.split('=')[1] });
|
|
19
19
|
} else if (arg.endsWith('.html')) {
|
|
@@ -26,6 +26,13 @@ export default function parseCliFlags(projectRoot) {
|
|
|
26
26
|
return result;
|
|
27
27
|
} else if (arg.startsWith('--port')) {
|
|
28
28
|
return Object.assign(result, { port: Number(arg.split('=')[1]) });
|
|
29
|
+
} else if (arg.startsWith('--extensions')) {
|
|
30
|
+
return Object.assign(result, {
|
|
31
|
+
extensions: arg
|
|
32
|
+
.split('=')[1]
|
|
33
|
+
.split(',')
|
|
34
|
+
.map((e) => e.trim()),
|
|
35
|
+
});
|
|
29
36
|
} else if (arg.startsWith('--before')) {
|
|
30
37
|
return Object.assign(result, { before: parseModule(arg.split('=')[1]) });
|
|
31
38
|
} else if (arg.startsWith('--after')) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qunitx-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.8",
|
|
5
5
|
"description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
6
6
|
"main": "cli.js",
|
|
7
7
|
"author": "Izel Nakri",
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
"kleur": "^4.1.5",
|
|
51
51
|
"picomatch": "^4.0.3",
|
|
52
52
|
"puppeteer": "^24.38.0",
|
|
53
|
-
"recursive-lookup": "1.1.0",
|
|
54
53
|
"ws": "^8.19.0"
|
|
55
54
|
},
|
|
56
55
|
"devDependencies": {
|
package/scripts/lint-docs.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// TODO: REMOVE THIS FILE once deno fixes the missing-return-type regression.
|
|
3
|
-
//
|
|
4
|
-
// WHY THIS FILE EXISTS:
|
|
5
|
-
// `deno doc --lint` has a regression in deno 2.7.x where JSDoc `@returns` tags
|
|
6
|
-
// are silently ignored for the `missing-return-type` check in JavaScript files.
|
|
7
|
-
// The check requires TypeScript-style return type annotations (`: ReturnType`)
|
|
8
|
-
// which are not valid syntax in `.js` files.
|
|
9
|
-
//
|
|
10
|
-
// All 22 `missing-return-type` errors are false positives caused by this bug.
|
|
11
|
-
// Every function has a correct `@returns` JSDoc tag — deno just doesn't read it.
|
|
12
|
-
//
|
|
13
|
-
// FIX OPTIONS (when ready):
|
|
14
|
-
// 1. Wait for deno to fix the regression (check deno changelog).
|
|
15
|
-
// 2. Convert lib/*.js files to lib/*.ts and add TS return type annotations.
|
|
16
|
-
//
|
|
17
|
-
// This script runs `deno doc --lint` and fails only on `missing-jsdoc` errors
|
|
18
|
-
// (i.e., the real quality check: "is every exported symbol documented?").
|
|
19
|
-
import { spawn } from 'node:child_process';
|
|
20
|
-
|
|
21
|
-
const proc = spawn('deno', ['doc', '--lint', 'lib/', 'cli.js'], { encoding: 'utf8' });
|
|
22
|
-
let output = '';
|
|
23
|
-
proc.stdout.on('data', (chunk) => (output += chunk));
|
|
24
|
-
proc.stderr.on('data', (chunk) => (output += chunk));
|
|
25
|
-
proc.on('close', () => {
|
|
26
|
-
// Strip ANSI escape codes so we can match on plain text
|
|
27
|
-
const plain = output.replace(/\x1b\[[0-9;]*m/g, '');
|
|
28
|
-
|
|
29
|
-
// Split into per-error blocks (each block starts with "error[")
|
|
30
|
-
const blocks = plain.split(/(?=^error\[)/m);
|
|
31
|
-
const relevant = blocks.filter((b) => !b.startsWith('error[missing-return-type]'));
|
|
32
|
-
const result = relevant.join('').trim();
|
|
33
|
-
|
|
34
|
-
if (result.includes('error[')) {
|
|
35
|
-
process.stderr.write(result + '\n');
|
|
36
|
-
process.exit(1);
|
|
37
|
-
} else if (result) {
|
|
38
|
-
process.stdout.write(result + '\n');
|
|
39
|
-
}
|
|
40
|
-
});
|