datagrok-tools 6.4.8 → 6.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/CHANGELOG.md +9 -1
- package/README.md +2 -0
- package/bin/commands/help.js +2 -0
- package/bin/commands/test.js +46 -6
- package/bin/utils/node-test-runner.js +121 -0
- package/bin/utils/node-test-worker.js +210 -0
- package/bin/utils/test-utils.js +162 -142
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
+
## 6.5.1 (2026-07-22)
|
|
4
|
+
|
|
5
|
+
* `grok test` — the Node-pass report now merges into the browser report by column name. The line-wise merge assumed identical column order, so node rows landed misaligned (string values in the integer `ms` column) and the CI test-report upload failed with `invalid input syntax for type integer`.
|
|
6
|
+
|
|
7
|
+
## 6.5.0 (WIP)
|
|
8
|
+
|
|
9
|
+
* `grok test` — added a Node (browserless) pass: tests annotated `{node: true}` run headless under the js-api Node runtime before the browser launches; the browser pass excludes them and is skipped entirely when nothing browser-only matches. New flags: `--skip-node`, `--node-only`. Packages opt in by exporting `testNode()` from `package-test.ts`; others keep the previous behavior.
|
|
10
|
+
|
|
3
11
|
## 6.4.8 (2026-07-22)
|
|
4
12
|
|
|
5
13
|
* GROK-20452: `grok publish` — always `docker build` the tools-generated worker dirs (`dockerfiles/queue`, `dockerfiles/celery`, `dockerfiles/<app>-celery`) instead of reusing a cached local tag. The "found local image" shortcut froze the worker at whatever stock base the daemon had when the tag was first built (CI ran day-old worker code with a fresh base available); the layer cache keeps an unchanged rebuild near-instant.
|
|
14
|
+
* `grok test` — `--no-retry` is now honored for Playwright runs. minimist parsed `--no-retry` as `{retry:false}`, so the flag was silently dropped and failed specs were still retried once; normalized so `--retries=0` reaches Playwright.
|
|
6
15
|
|
|
7
16
|
## 6.4.7 (2026-07-22)
|
|
8
17
|
|
|
@@ -11,7 +20,6 @@
|
|
|
11
20
|
## 6.4.6 (2026-07-22)
|
|
12
21
|
|
|
13
22
|
* `grok test` — the whole-run Puppeteer cap (60 min) is now overridable via `GROK_TEST_INVOCATION_TIMEOUT_MS`. When the cap fires mid-pass all collected results are discarded (empty `test-report.csv`, "Passed tests: 0"), which CI reports as "no results produced"; loaded CI agents can now raise the cap instead.
|
|
14
|
-
* `grok test` — `--no-retry` is now honored for Playwright runs. minimist parsed `--no-retry` as `{retry:false}`, so the flag was silently dropped and failed specs were still retried once; normalized so `--retries=0` reaches Playwright.
|
|
15
23
|
|
|
16
24
|
## 6.4.5 (2026-06-23)
|
|
17
25
|
|
package/README.md
CHANGED
|
@@ -135,6 +135,8 @@ For more information on configuring connections, refer to the [Connections](http
|
|
|
135
135
|
| report | Sends a report on test results |
|
|
136
136
|
| skip-build | Skips package build step |
|
|
137
137
|
| skip-publish | Skips package publish step |
|
|
138
|
+
| skip-node | Skips the Node (browserless) pass; runs all tests in the browser |
|
|
139
|
+
| node-only | Runs only tests annotated `{node: true}` headless under Node, without a browser |
|
|
138
140
|
| verbose | Prints detailed information about passed and skipped tests in the console |
|
|
139
141
|
| platform | Runs only platform tests (applicable for ApiTests package only) |
|
|
140
142
|
| core | Runs package & core tests & autotests |
|
package/bin/commands/help.js
CHANGED
|
@@ -207,6 +207,8 @@ Options:
|
|
|
207
207
|
--skip-publish Skip the package publication step
|
|
208
208
|
--skip-puppeteer Skip the Puppeteer/DG.Test pass; only run Playwright (for playwright-only test directories)
|
|
209
209
|
--skip-playwright Skip the Playwright pass; only run Puppeteer/DG.Test
|
|
210
|
+
--skip-node Skip the Node (browserless) pass; run all tests in the browser
|
|
211
|
+
--node-only Run only tests annotated {node: true} headless under Node, no browser
|
|
210
212
|
--link Link the package to local utils
|
|
211
213
|
--record Records the test execution process in mp4 format
|
|
212
214
|
--platform Runs only platform tests (applicable for ApiTests package only)
|
package/bin/commands/test.js
CHANGED
|
@@ -20,6 +20,7 @@ var Papa = _interopRequireWildcard(require("papaparse"));
|
|
|
20
20
|
var _testUtils = _interopRequireWildcard(require("../utils/test-utils"));
|
|
21
21
|
var testUtils = _testUtils;
|
|
22
22
|
var playwrightRunner = _interopRequireWildcard(require("../utils/playwright-runner"));
|
|
23
|
+
var _nodeTestRunner = require("../utils/node-test-runner");
|
|
23
24
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
24
25
|
/* eslint-disable max-len */
|
|
25
26
|
|
|
@@ -29,7 +30,7 @@ const execFileAsync = (0, _util.promisify)(_child_process.execFile);
|
|
|
29
30
|
// are discarded (they live in the page until a pass completes). CI runners on
|
|
30
31
|
// loaded agents can exceed the 1h default — override via env.
|
|
31
32
|
const testInvocationTimeout = parseInt(process.env['GROK_TEST_INVOCATION_TIMEOUT_MS'] ?? '', 10) || 3600000;
|
|
32
|
-
const availableCommandOptions = ['host', 'package', 'csv', 'gui', 'catchUnhandled', 'platform', 'core', 'report', 'skip-build', 'skip-publish', 'path', 'record', 'verbose', 'benchmark', 'category', 'test', 'stress-test', 'link', 'tag', 'ci-cd', 'debug', 'no-retry', 'dartium', 'f', 'params', 'logfailed', 'skip-playwright', 'skip-puppeteer'];
|
|
33
|
+
const availableCommandOptions = ['host', 'package', 'csv', 'gui', 'catchUnhandled', 'platform', 'core', 'report', 'skip-build', 'skip-publish', 'path', 'record', 'verbose', 'benchmark', 'category', 'test', 'stress-test', 'link', 'tag', 'ci-cd', 'debug', 'no-retry', 'dartium', 'f', 'params', 'logfailed', 'skip-playwright', 'skip-puppeteer', 'skip-node', 'node-only'];
|
|
33
34
|
const curDir = process.cwd();
|
|
34
35
|
|
|
35
36
|
/** Expands camelCase to space-separated lowercase: "dataManipulation" → "data manipulation" */
|
|
@@ -228,12 +229,34 @@ async function test(args) {
|
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
process.env.TARGET_PACKAGE = packageName;
|
|
232
|
+
|
|
233
|
+
// Node (browserless) pass: runs tests annotated {node: true} headless under the
|
|
234
|
+
// js-api Node runtime; the browser pass then excludes them. Skipped for --gui/--debug
|
|
235
|
+
// so every test stays visible in the browser there.
|
|
236
|
+
let nodeRes = undefined;
|
|
237
|
+
if (!args['skip-node'] && !args['skip-puppeteer'] && !args.gui && !args.debug && !args.package) {
|
|
238
|
+
nodeRes = await (0, _nodeTestRunner.runNodeTests)({
|
|
239
|
+
packageDir: curDir,
|
|
240
|
+
packageName: packageName,
|
|
241
|
+
host: args.host ?? '',
|
|
242
|
+
category: args.category,
|
|
243
|
+
test: args.test,
|
|
244
|
+
stressTest: args['stress-test'],
|
|
245
|
+
benchmark: args.benchmark,
|
|
246
|
+
verbose: args.verbose
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (args['node-only'] && nodeRes == null) {
|
|
250
|
+
color.error('--node-only: the Node test pass is not available for this package.');
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
231
253
|
let res;
|
|
232
|
-
if (args['skip-puppeteer']) {
|
|
254
|
+
if (args['skip-puppeteer'] || args['node-only'] || nodeRes != null && nodeRes.browserTestsRemaining === 0) {
|
|
233
255
|
// Playwright-only mode: skip the Puppeteer browser launch + DG.Test runner.
|
|
234
256
|
// Used by Playwright-only test directories (e.g. public/playwright-public)
|
|
235
257
|
// that have a `playwrightTests` field in package.json but no Dart/JS package
|
|
236
|
-
// tests on the server.
|
|
258
|
+
// tests on the server. Also taken when the Node pass covered every matched test.
|
|
259
|
+
if (nodeRes != null && nodeRes.browserTestsRemaining === 0 && !args['node-only'] && !args['skip-puppeteer']) color.info('All matched tests ran in the Node pass; skipping the browser run.');
|
|
237
260
|
res = {
|
|
238
261
|
failed: false,
|
|
239
262
|
verbosePassed: '',
|
|
@@ -246,7 +269,7 @@ async function test(args) {
|
|
|
246
269
|
};
|
|
247
270
|
} else {
|
|
248
271
|
try {
|
|
249
|
-
res = await runTesting(args);
|
|
272
|
+
res = await runTesting(args, nodeRes != null);
|
|
250
273
|
} catch (e) {
|
|
251
274
|
// Don't let Puppeteer-side failures (login error, browser crash) skip the
|
|
252
275
|
// Playwright pass — the two suites have independent auth and runtime paths,
|
|
@@ -265,7 +288,21 @@ async function test(args) {
|
|
|
265
288
|
};
|
|
266
289
|
}
|
|
267
290
|
}
|
|
268
|
-
if (
|
|
291
|
+
if (nodeRes != null) {
|
|
292
|
+
// Merge the Node pass into the browser result. CSVs merge by column NAME —
|
|
293
|
+
// the two passes emit different column orders, and a line-wise concat would
|
|
294
|
+
// misalign rows (string values in integer columns break the report upload).
|
|
295
|
+
res.csv = testUtils.mergeCsvByHeader(res.csv, nodeRes.csv);
|
|
296
|
+
res.passedAmount += nodeRes.passedAmount;
|
|
297
|
+
res.failedAmount += nodeRes.failedAmount;
|
|
298
|
+
res.skippedAmount += nodeRes.skippedAmount;
|
|
299
|
+
res.failed = res.failed || nodeRes.failed;
|
|
300
|
+
res.verbosePassed = (res.verbosePassed || '') + nodeRes.verbosePassed;
|
|
301
|
+
res.verboseFailed = (res.verboseFailed || '') + nodeRes.verboseFailed;
|
|
302
|
+
res.verboseSkipped = (res.verboseSkipped || '') + nodeRes.verboseSkipped;
|
|
303
|
+
res.modernOutput = res.modernOutput || nodeRes.modernOutput;
|
|
304
|
+
}
|
|
305
|
+
if (!args['skip-playwright'] && !args['node-only']) {
|
|
269
306
|
const ptDir = playwrightRunner.hasPlaywrightTests(curDir);
|
|
270
307
|
if (ptDir) {
|
|
271
308
|
const ptRes = await playwrightRunner.runPlaywrightTests(curDir, ptDir, args, args.host ?? '');
|
|
@@ -312,7 +349,7 @@ const retriedTests = new Set();
|
|
|
312
349
|
let totalRetries = 0;
|
|
313
350
|
const MAX_RETRIES_PER_SESSION = 10;
|
|
314
351
|
let retryEnabled = true;
|
|
315
|
-
async function runTesting(args) {
|
|
352
|
+
async function runTesting(args, excludeNodeTests = false) {
|
|
316
353
|
retryEnabled = args['retry'] ?? true;
|
|
317
354
|
if (args.test || args.category) retryEnabled = false;
|
|
318
355
|
let organized = {
|
|
@@ -320,6 +357,7 @@ async function runTesting(args) {
|
|
|
320
357
|
params: {
|
|
321
358
|
category: args.category ?? '',
|
|
322
359
|
test: args.test ?? '',
|
|
360
|
+
excludeNodeTests: excludeNodeTests || undefined,
|
|
323
361
|
options: {
|
|
324
362
|
catchUnhandled: args.catchUnhandled,
|
|
325
363
|
report: args.report
|
|
@@ -956,6 +994,8 @@ function buildTestArgs(args) {
|
|
|
956
994
|
if (args['stress-test']) parts.push('--stress-test');
|
|
957
995
|
if (args['ci-cd']) parts.push('--ci-cd');
|
|
958
996
|
if (args['no-retry']) parts.push('--no-retry');
|
|
997
|
+
if (args['skip-node']) parts.push('--skip-node');
|
|
998
|
+
if (args['node-only']) parts.push('--node-only');
|
|
959
999
|
return parts;
|
|
960
1000
|
}
|
|
961
1001
|
function parseTestOutput(stdout) {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.runNodeTests = runNodeTests;
|
|
8
|
+
var _child_process = require("child_process");
|
|
9
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
10
|
+
var _os = _interopRequireDefault(require("os"));
|
|
11
|
+
var _path = _interopRequireDefault(require("path"));
|
|
12
|
+
var _readline = _interopRequireDefault(require("readline"));
|
|
13
|
+
var color = _interopRequireWildcard(require("./color-utils"));
|
|
14
|
+
var _testUtils = require("./test-utils");
|
|
15
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
16
|
+
/* eslint-disable max-len */
|
|
17
|
+
/**
|
|
18
|
+
* CLI side of the `grok test` Node (browserless) pass: spawns node-test-worker.js
|
|
19
|
+
* with cwd = the package directory, streams its progress output through the shared
|
|
20
|
+
* TestProgressReporter, and returns a ResultObject-compatible report.
|
|
21
|
+
*
|
|
22
|
+
* Returns undefined when the package doesn't support the Node pass (no testNode export,
|
|
23
|
+
* no datagrok-api Node runtime, not published) or the worker fails — the caller then
|
|
24
|
+
* runs everything in the browser as before.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const workerTimeout = 3600000;
|
|
28
|
+
async function runNodeTests(opts) {
|
|
29
|
+
// Cheap opt-in gate: don't pay the worker spawn + runtime init for the vast
|
|
30
|
+
// majority of packages that don't export testNode() from package-test.ts.
|
|
31
|
+
try {
|
|
32
|
+
const testSrc = ['ts', 'js'].map(ext => _path.default.join(opts.packageDir, 'src', `package-test.${ext}`)).find(p => _fs.default.existsSync(p));
|
|
33
|
+
if (testSrc != null && !_fs.default.readFileSync(testSrc, 'utf8').includes('testNode')) return undefined;
|
|
34
|
+
} catch {}
|
|
35
|
+
let url;
|
|
36
|
+
let key;
|
|
37
|
+
try {
|
|
38
|
+
({
|
|
39
|
+
url,
|
|
40
|
+
key
|
|
41
|
+
} = (0, _testUtils.getDevKey)(opts.host));
|
|
42
|
+
} catch (e) {
|
|
43
|
+
color.warn(`Node test pass skipped: ${e?.message ?? e}`);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const token = await (0, _testUtils.getToken)(url, key);
|
|
47
|
+
const workerPath = _path.default.join(__dirname, 'node-test-worker.js');
|
|
48
|
+
const outPath = _path.default.join(_os.default.tmpdir(), `grok-node-test-${Date.now()}-${process.pid}.json`);
|
|
49
|
+
const args = [workerPath, `--out=${outPath}`];
|
|
50
|
+
if (opts.category) args.push(`--category=${opts.category}`);
|
|
51
|
+
if (opts.test) args.push(`--test=${opts.test}`);
|
|
52
|
+
if (opts.stressTest) args.push('--stress-test');
|
|
53
|
+
if (opts.benchmark) args.push('--benchmark');
|
|
54
|
+
if (opts.verbose) args.push('--verbose');
|
|
55
|
+
color.info('Running Node (browserless) test pass...');
|
|
56
|
+
const reporter = new _testUtils.TestProgressReporter(opts.verbose ?? false);
|
|
57
|
+
const stderrTail = [];
|
|
58
|
+
const exitCode = await new Promise(resolve => {
|
|
59
|
+
const child = (0, _child_process.spawn)(process.execPath, args, {
|
|
60
|
+
cwd: opts.packageDir,
|
|
61
|
+
env: {
|
|
62
|
+
...process.env,
|
|
63
|
+
DG_API_URL: url,
|
|
64
|
+
DG_API_TOKEN: token,
|
|
65
|
+
TARGET_PACKAGE: opts.packageName
|
|
66
|
+
},
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
68
|
+
});
|
|
69
|
+
const killTimer = setTimeout(() => {
|
|
70
|
+
color.error(`Node test pass timed out after ${workerTimeout / 1000}s. Killing worker.`);
|
|
71
|
+
child.kill();
|
|
72
|
+
}, workerTimeout);
|
|
73
|
+
const rlOut = _readline.default.createInterface({
|
|
74
|
+
input: child.stdout
|
|
75
|
+
});
|
|
76
|
+
rlOut.on('line', line => {
|
|
77
|
+
if (line.startsWith('Package testing: ')) reporter.onLine(line);else if (line.startsWith('Node pass unavailable: ')) color.info(line);else if (opts.verbose) console.log(line);
|
|
78
|
+
});
|
|
79
|
+
const rlErr = _readline.default.createInterface({
|
|
80
|
+
input: child.stderr
|
|
81
|
+
});
|
|
82
|
+
rlErr.on('line', line => {
|
|
83
|
+
stderrTail.push(line);
|
|
84
|
+
if (stderrTail.length > 50) stderrTail.shift();
|
|
85
|
+
if (opts.verbose) console.error(line);
|
|
86
|
+
});
|
|
87
|
+
child.on('close', code => {
|
|
88
|
+
clearTimeout(killTimer);
|
|
89
|
+
rlOut.close();
|
|
90
|
+
rlErr.close();
|
|
91
|
+
resolve(code ?? 2);
|
|
92
|
+
});
|
|
93
|
+
child.on('error', e => {
|
|
94
|
+
clearTimeout(killTimer);
|
|
95
|
+
color.warn(`Failed to spawn Node test worker: ${e.message}`);
|
|
96
|
+
resolve(2);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
reporter.finish();
|
|
100
|
+
if (exitCode === 3) {
|
|
101
|
+
color.info('Node test pass is not supported here; running all tests in the browser.');
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
if (exitCode !== 0) {
|
|
105
|
+
if (!opts.verbose && stderrTail.length > 0) console.error(stderrTail.join('\n'));
|
|
106
|
+
color.warn(`Node test pass failed (exit code ${exitCode}); running all tests in the browser.`);
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const report = JSON.parse(_fs.default.readFileSync(outPath, 'utf8'));
|
|
111
|
+
_fs.default.unlinkSync(outPath);
|
|
112
|
+
color.info(`Node pass: ${report.passedAmount} passed, ${report.failedAmount} failed, ${report.skippedAmount} skipped; ` + `${report.browserTestsRemaining} test(s) left for the browser.`);
|
|
113
|
+
return {
|
|
114
|
+
...report,
|
|
115
|
+
modernOutput: true
|
|
116
|
+
};
|
|
117
|
+
} catch (e) {
|
|
118
|
+
color.warn(`Could not read Node test report: ${e?.message ?? e}; running all tests in the browser.`);
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var vm = _interopRequireWildcard(require("vm"));
|
|
4
|
+
var fs = _interopRequireWildcard(require("fs"));
|
|
5
|
+
var path = _interopRequireWildcard(require("path"));
|
|
6
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
7
|
+
/* eslint-disable max-len */
|
|
8
|
+
/**
|
|
9
|
+
* Standalone worker for the `grok test` Node (browserless) pass.
|
|
10
|
+
*
|
|
11
|
+
* Spawned by node-test-runner.ts with cwd = the package directory:
|
|
12
|
+
* 1. loads the js-api Node runtime (`datagrok-api/datagrok`) from the package's own node_modules,
|
|
13
|
+
* 2. resolves the published package version the platform would serve and evaluates its
|
|
14
|
+
* `dist/package-test.js` bundle (webpack var-library `<name>_test`),
|
|
15
|
+
* 3. runs init-tagged functions of the test bundle, then calls its exported `testNode()`
|
|
16
|
+
* which executes only tests marked `{node: true}`,
|
|
17
|
+
* 4. counts the tests the browser pass still has to run, and writes a JSON report to --out.
|
|
18
|
+
*
|
|
19
|
+
* Exit codes: 0 = pass executed (test failures are conveyed in the report),
|
|
20
|
+
* 3 = Node pass unsupported for this package (caller falls back to the browser for everything),
|
|
21
|
+
* anything else = infrastructure error.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const UNSUPPORTED_EXIT_CODE = 3;
|
|
25
|
+
const RESULT_COLUMNS = ['date', 'category', 'name', 'success', 'result', 'ms', 'skipped', 'logs', 'owner', 'package', 'widgetsDifference', 'flaking'];
|
|
26
|
+
function getArg(name) {
|
|
27
|
+
const prefix = `--${name}=`;
|
|
28
|
+
const arg = process.argv.find(a => a.startsWith(prefix));
|
|
29
|
+
return arg ? arg.slice(prefix.length) : undefined;
|
|
30
|
+
}
|
|
31
|
+
function hasFlag(name) {
|
|
32
|
+
return process.argv.includes(`--${name}`);
|
|
33
|
+
}
|
|
34
|
+
function unsupported(reason) {
|
|
35
|
+
console.log(`Node pass unavailable: ${reason}`);
|
|
36
|
+
process.exit(UNSUPPORTED_EXIT_CODE);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A crashing async callback (e.g. a socket handler inside the Dart runtime) must not
|
|
40
|
+
// kill the whole pass — log it and let the per-test timeout fail the affected test.
|
|
41
|
+
process.on('uncaughtException', err => {
|
|
42
|
+
console.error('Uncaught exception:', err?.stack ?? err);
|
|
43
|
+
});
|
|
44
|
+
process.on('unhandledRejection', reason => {
|
|
45
|
+
console.error('Unhandled rejection:', reason);
|
|
46
|
+
});
|
|
47
|
+
async function main() {
|
|
48
|
+
const apiUrl = process.env.DG_API_URL;
|
|
49
|
+
const token = process.env.DG_API_TOKEN;
|
|
50
|
+
const packageName = process.env.TARGET_PACKAGE;
|
|
51
|
+
const outPath = getArg('out');
|
|
52
|
+
const categoryArg = getArg('category');
|
|
53
|
+
const testArg = getArg('test');
|
|
54
|
+
const stressTest = hasFlag('stress-test');
|
|
55
|
+
const benchmark = hasFlag('benchmark');
|
|
56
|
+
const verbose = hasFlag('verbose');
|
|
57
|
+
let dgEntry;
|
|
58
|
+
try {
|
|
59
|
+
dgEntry = require.resolve('datagrok-api/datagrok', {
|
|
60
|
+
paths: [process.cwd()]
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
unsupported('the installed datagrok-api has no Node runtime (datagrok-api/datagrok)');
|
|
64
|
+
}
|
|
65
|
+
const dgApi = require(dgEntry);
|
|
66
|
+
if (typeof dgApi.startDatagrok !== 'function') unsupported('the installed datagrok-api has no startDatagrok()');
|
|
67
|
+
await dgApi.startDatagrok({
|
|
68
|
+
apiUrl,
|
|
69
|
+
apiToken: token
|
|
70
|
+
});
|
|
71
|
+
const g = globalThis;
|
|
72
|
+
const grok = g.grok;
|
|
73
|
+
const DG = g.DG;
|
|
74
|
+
|
|
75
|
+
// Package bundles map webpack externals to browser globals (rxjs, rxjs.operators,
|
|
76
|
+
// dayjs, utc, wu, $). The Node runtime guarantees DG/grok/ui; provide the rest
|
|
77
|
+
// from datagrok-api's own dependencies so bundle evaluation doesn't throw.
|
|
78
|
+
const req = id => require(require.resolve(id, {
|
|
79
|
+
paths: [path.dirname(dgEntry), process.cwd()]
|
|
80
|
+
}));
|
|
81
|
+
const ensureGlobal = (name, resolve) => {
|
|
82
|
+
if (g[name] == null) try {
|
|
83
|
+
g[name] = resolve();
|
|
84
|
+
} catch {}
|
|
85
|
+
};
|
|
86
|
+
ensureGlobal('rxjs', () => req('rxjs'));
|
|
87
|
+
if (g.rxjs != null && g.rxjs.operators == null) try {
|
|
88
|
+
g.rxjs = Object.assign({}, g.rxjs, {
|
|
89
|
+
operators: req('rxjs/operators')
|
|
90
|
+
});
|
|
91
|
+
} catch {}
|
|
92
|
+
ensureGlobal('dayjs', () => req('dayjs'));
|
|
93
|
+
ensureGlobal('utc', () => req('dayjs/plugin/utc'));
|
|
94
|
+
ensureGlobal('wu', () => req('wu'));
|
|
95
|
+
ensureGlobal('$', () => req('cash-dom'));
|
|
96
|
+
|
|
97
|
+
// Resolve the published version the platform would serve: the caller's debug version
|
|
98
|
+
// if one exists, else the current one (mirrors js-api's Node loadPackage()).
|
|
99
|
+
const headers = {
|
|
100
|
+
Authorization: token
|
|
101
|
+
};
|
|
102
|
+
const listResp = await fetch(`${apiUrl}/packages?text=${encodeURIComponent(packageName)}`, {
|
|
103
|
+
headers
|
|
104
|
+
});
|
|
105
|
+
if (!listResp.ok) throw new Error(`Failed to list packages: HTTP ${listResp.status}`);
|
|
106
|
+
const packages = await listResp.json();
|
|
107
|
+
const pkgInfo = packages.find(p => p.name?.toLowerCase() === packageName.toLowerCase());
|
|
108
|
+
if (!pkgInfo) unsupported(`package "${packageName}" is not published on ${apiUrl}`);
|
|
109
|
+
const versions = pkgInfo.publishedVersions ?? [];
|
|
110
|
+
const login = (await grok.dapi.users.current()).login;
|
|
111
|
+
const cur = versions.find(v => v.debug && v.version === login) ?? versions.find(v => v.isCurrent) ?? versions.find(v => v.isLatest) ?? versions[0];
|
|
112
|
+
if (!cur) unsupported(`package "${packageName}" has no published versions`);
|
|
113
|
+
const webRoot = `${apiUrl}/packages/published/files/${pkgInfo.name}/${cur.version}/${cur.buildHash}/${cur.buildNumber}/`;
|
|
114
|
+
const jsUrl = `${webRoot}dist/package-test.js`;
|
|
115
|
+
const codeResp = await fetch(jsUrl, {
|
|
116
|
+
headers
|
|
117
|
+
});
|
|
118
|
+
if (!codeResp.ok) unsupported(`no test bundle at ${jsUrl} (HTTP ${codeResp.status})`);
|
|
119
|
+
vm.runInThisContext(await codeResp.text(), {
|
|
120
|
+
filename: jsUrl
|
|
121
|
+
});
|
|
122
|
+
const moduleName = `${packageName.toLowerCase()}_test`;
|
|
123
|
+
const testModule = g[moduleName] ?? g[Object.keys(g).find(k => k.endsWith('_test') && g[k]?.tests) ?? ''];
|
|
124
|
+
if (!testModule?.tests) unsupported(`test bundle did not define a "${moduleName}" module`);
|
|
125
|
+
if (typeof testModule.testNode !== 'function') unsupported('package-test.ts does not export testNode()');
|
|
126
|
+
if (testModule._package != null) {
|
|
127
|
+
testModule._package.webRoot = webRoot;
|
|
128
|
+
testModule._package.name = pkgInfo.name;
|
|
129
|
+
testModule._package.version = cur.version;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The package must carry the resolved published-version id: functions are attached to
|
|
133
|
+
// package versions, and runTests' initAutoTests filters `package.id = <id>` to register
|
|
134
|
+
// the annotation-driven auto tests. The family-level entity id matches nothing.
|
|
135
|
+
const pkg = (await grok.dapi.packages.find(cur.id)) ?? (await grok.dapi.packages.filter(`shortName = "${pkgInfo.name}"`).first());
|
|
136
|
+
if (!pkg) unsupported(`package entity "${pkgInfo.name}" not found`);
|
|
137
|
+
try {
|
|
138
|
+
if (pkg.name !== pkgInfo.name) pkg.name = pkgInfo.name;
|
|
139
|
+
} catch {}
|
|
140
|
+
if (benchmark) DG.Test.isInBenchmark = true;
|
|
141
|
+
const results = await testModule.testNode(pkg, {
|
|
142
|
+
category: categoryArg,
|
|
143
|
+
test: testArg,
|
|
144
|
+
stressTest,
|
|
145
|
+
verbose
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Count what the browser pass still has to run. testNode() ran initAutoTests(), so the
|
|
149
|
+
// registry now also holds the annotation-driven auto/demo/detector tests.
|
|
150
|
+
let browserTestsRemaining = 0;
|
|
151
|
+
for (const [catName, cat] of Object.entries(testModule.tests)) {
|
|
152
|
+
if (categoryArg != null && !catName.toLowerCase().startsWith(categoryArg.toLowerCase().trim())) continue;
|
|
153
|
+
for (const t of cat.tests ?? []) {
|
|
154
|
+
if (testArg != null && t.name.toLowerCase() !== testArg.toLowerCase()) continue;
|
|
155
|
+
if (benchmark && !t.options?.benchmark) continue;
|
|
156
|
+
if (stressTest && !t.options?.stressTest) continue;
|
|
157
|
+
if (!(t.options?.node ?? cat.node ?? false)) browserTestsRemaining++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Shape the report like the browser pass does — same CSV columns in the same order,
|
|
162
|
+
// so the two passes merge row-wise.
|
|
163
|
+
const Papa = require('papaparse');
|
|
164
|
+
const rows = results.map(r => {
|
|
165
|
+
const row = {};
|
|
166
|
+
for (const c of RESULT_COLUMNS) row[c] = r[c] ?? (c === 'widgetsDifference' ? 0 : '');
|
|
167
|
+
return row;
|
|
168
|
+
});
|
|
169
|
+
let failed = false;
|
|
170
|
+
let passedAmount = 0;
|
|
171
|
+
let skippedAmount = 0;
|
|
172
|
+
let failedAmount = 0;
|
|
173
|
+
let verbosePassed = '';
|
|
174
|
+
let verboseSkipped = '';
|
|
175
|
+
let verboseFailed = '';
|
|
176
|
+
for (const r of results) {
|
|
177
|
+
const line = `${r.category}: ${r.name} (${r.ms} ms) : ${r.result}\n`;
|
|
178
|
+
if (r.skipped) {
|
|
179
|
+
skippedAmount++;
|
|
180
|
+
verboseSkipped += line;
|
|
181
|
+
} else if (r.success) {
|
|
182
|
+
passedAmount++;
|
|
183
|
+
verbosePassed += line;
|
|
184
|
+
} else {
|
|
185
|
+
failedAmount++;
|
|
186
|
+
verboseFailed += line;
|
|
187
|
+
failed = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const report = {
|
|
191
|
+
failed,
|
|
192
|
+
verbosePassed,
|
|
193
|
+
verboseSkipped,
|
|
194
|
+
verboseFailed,
|
|
195
|
+
passedAmount,
|
|
196
|
+
skippedAmount,
|
|
197
|
+
failedAmount,
|
|
198
|
+
csv: rows.length ? Papa.unparse(rows, {
|
|
199
|
+
columns: RESULT_COLUMNS
|
|
200
|
+
}) : '',
|
|
201
|
+
browserTestsRemaining,
|
|
202
|
+
nodeTestsRun: results.length
|
|
203
|
+
};
|
|
204
|
+
fs.writeFileSync(outPath, JSON.stringify(report), 'utf8');
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
main().catch(e => {
|
|
208
|
+
console.error(e?.stack ?? e?.message ?? String(e));
|
|
209
|
+
process.exit(2);
|
|
210
|
+
});
|
package/bin/utils/test-utils.js
CHANGED
|
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
-
exports.TestContext = void 0;
|
|
7
|
+
exports.TestProgressReporter = exports.TestContext = void 0;
|
|
8
8
|
exports.addColumnToCsv = addColumnToCsv;
|
|
9
9
|
exports.addLogsToFile = addLogsToFile;
|
|
10
10
|
exports.defaultLaunchParameters = void 0;
|
|
@@ -20,6 +20,7 @@ exports.loadPackage = loadPackage;
|
|
|
20
20
|
exports.loadPackages = loadPackages;
|
|
21
21
|
exports.loadTestsList = loadTestsList;
|
|
22
22
|
exports.mergeBrowsersResults = mergeBrowsersResults;
|
|
23
|
+
exports.mergeCsvByHeader = mergeCsvByHeader;
|
|
23
24
|
exports.printBrowsersResult = printBrowsersResult;
|
|
24
25
|
exports.runBrowser = runBrowser;
|
|
25
26
|
exports.runWithTimeout = runWithTimeout;
|
|
@@ -363,6 +364,132 @@ async function loadTestsList(packages, core = false, record = false) {
|
|
|
363
364
|
function addLogsToFile(filePath, stringToSave) {
|
|
364
365
|
_fs.default.appendFileSync(filePath, `${stringToSave}`);
|
|
365
366
|
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Parses the `Package testing: ...` progress lines emitted by the
|
|
370
|
+
* @datagrok-libraries/test runner and prints live per-category summaries.
|
|
371
|
+
* Used for both the Puppeteer console stream and the Node-pass worker stdout.
|
|
372
|
+
*/
|
|
373
|
+
class TestProgressReporter {
|
|
374
|
+
modernOutput = false;
|
|
375
|
+
categoryResults = new Map();
|
|
376
|
+
currentCategory = null;
|
|
377
|
+
pendingFailures = [];
|
|
378
|
+
pendingSkipped = [];
|
|
379
|
+
pendingBeforeFailure = null;
|
|
380
|
+
pendingAfterFailure = null;
|
|
381
|
+
constructor(verbose = false) {
|
|
382
|
+
this.verbose = verbose;
|
|
383
|
+
}
|
|
384
|
+
printCategorySummary(category) {
|
|
385
|
+
const results = this.categoryResults.get(category);
|
|
386
|
+
if (!results) return;
|
|
387
|
+
const passedCount = results.passed;
|
|
388
|
+
const skippedSuffix = results.skipped > 0 ? `, \x1b[33m${results.skipped} skipped\x1b[0m` : '';
|
|
389
|
+
if (results.failed > 0 || this.pendingBeforeFailure || this.pendingAfterFailure) {
|
|
390
|
+
console.log(`\x1b[31m❌ ${category}\x1b[31m (\x1b[32m${passedCount} passed${skippedSuffix}\x1b[31m)\x1b[0m`);
|
|
391
|
+
if (this.pendingBeforeFailure) {
|
|
392
|
+
console.log(` \x1b[31m❌ before\x1b[0m`);
|
|
393
|
+
if (this.pendingBeforeFailure.error) console.log(` \x1b[31m${this.pendingBeforeFailure.error}\x1b[0m`);
|
|
394
|
+
console.log(` \x1b[33mTo run this category separately use --category "${category}"\x1b[0m`);
|
|
395
|
+
}
|
|
396
|
+
if (this.pendingAfterFailure) {
|
|
397
|
+
console.log(` \x1b[31m❌ after\x1b[0m`);
|
|
398
|
+
if (this.pendingAfterFailure.error) console.log(` \x1b[31m${this.pendingAfterFailure.error}\x1b[0m`);
|
|
399
|
+
console.log(` \x1b[33mTo run this category separately use --category "${category}"\x1b[0m`);
|
|
400
|
+
}
|
|
401
|
+
for (const skippedName of this.pendingSkipped) console.log(` \x1b[33m⊘ ${skippedName}\x1b[0m`);
|
|
402
|
+
for (const failure of this.pendingFailures) {
|
|
403
|
+
console.log(` \x1b[31m❌ ${failure.testName}\x1b[0m`);
|
|
404
|
+
if (failure.error) console.log(` \x1b[31m${failure.error}\x1b[0m`);
|
|
405
|
+
console.log(` \x1b[33mTo run this test separately use --category "${category}" --test "${failure.testName}"\x1b[0m`);
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
console.log(`\x1b[32m✅ ${category} (${passedCount} passed${skippedSuffix}\x1b[32m)\x1b[0m`);
|
|
409
|
+
for (const skippedName of this.pendingSkipped) console.log(` \x1b[33m⊘ ${skippedName}\x1b[0m`);
|
|
410
|
+
}
|
|
411
|
+
this.pendingFailures = [];
|
|
412
|
+
this.pendingSkipped = [];
|
|
413
|
+
this.pendingBeforeFailure = null;
|
|
414
|
+
this.pendingAfterFailure = null;
|
|
415
|
+
}
|
|
416
|
+
onLine(text) {
|
|
417
|
+
if (!text.startsWith('Package testing: ')) return;
|
|
418
|
+
this.modernOutput = true;
|
|
419
|
+
// Extract tokens in {{...}} format
|
|
420
|
+
const tokens = [];
|
|
421
|
+
const tokenRegex = /\{\{([^}]+)\}\}/g;
|
|
422
|
+
let match;
|
|
423
|
+
while ((match = tokenRegex.exec(text)) !== null) tokens.push(match[1]);
|
|
424
|
+
|
|
425
|
+
// Category start: "Package testing: Started {{Category}}" or "Package testing: Started {{Category}} skipped {{N}}"
|
|
426
|
+
if (text.includes('Started') && (tokens.length === 1 || tokens.length === 2 && text.includes('skipped'))) {
|
|
427
|
+
if (this.currentCategory && this.categoryResults.has(this.currentCategory)) this.printCategorySummary(this.currentCategory);
|
|
428
|
+
this.currentCategory = tokens[0];
|
|
429
|
+
const skippedCount = tokens.length === 2 ? parseInt(tokens[1]) || 0 : 0;
|
|
430
|
+
this.categoryResults.set(this.currentCategory, {
|
|
431
|
+
passed: 0,
|
|
432
|
+
failed: 0,
|
|
433
|
+
skipped: skippedCount
|
|
434
|
+
});
|
|
435
|
+
this.pendingFailures = [];
|
|
436
|
+
this.pendingSkipped = [];
|
|
437
|
+
this.pendingBeforeFailure = null;
|
|
438
|
+
this.pendingAfterFailure = null;
|
|
439
|
+
} else if (text.includes('Skipped') && tokens.length === 2 && !text.includes('benchmark')) {
|
|
440
|
+
// Individual skipped test: "Package testing: Skipped {{Category}} {{TestName}}"
|
|
441
|
+
if (tokens[0] === this.currentCategory) this.pendingSkipped.push(tokens[1]);
|
|
442
|
+
} else if (text.includes('Started') && tokens.length === 2) {
|
|
443
|
+
// Test start: "Package testing: Started {{Category}} {{TestName}}"
|
|
444
|
+
process.stdout.write(`${tokens[0]}: ${tokens[1]}...`);
|
|
445
|
+
} else if (text.includes('Finished') && tokens.length === 3) {
|
|
446
|
+
// Test finish: "Package testing: Finished {{Category}} {{TestName}} with {{success/error}} for X ms"
|
|
447
|
+
const results = this.categoryResults.get(tokens[0]);
|
|
448
|
+
if (!this.verbose) process.stdout.write('\r\x1b[K');
|
|
449
|
+
if (tokens[2] === 'success') {
|
|
450
|
+
if (results) results.passed++;
|
|
451
|
+
} else {
|
|
452
|
+
if (results) results.failed++;
|
|
453
|
+
this.pendingFailures.push({
|
|
454
|
+
testName: tokens[1],
|
|
455
|
+
error: null
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
} else if (text.includes('Result for') && tokens.length === 2) {
|
|
459
|
+
// Error result: "Package testing: Result for {{Category}} {{TestName}}: error message"
|
|
460
|
+
const errorMsg = text.split(': ').slice(-1)[0];
|
|
461
|
+
if (this.pendingFailures.length > 0) this.pendingFailures[this.pendingFailures.length - 1].error = errorMsg;
|
|
462
|
+
} else if (text.includes('Category before()') && text.includes('failed') && tokens.length >= 1) {
|
|
463
|
+
// Category before() failed: "Package testing: Category before() {{Category}} failed"
|
|
464
|
+
process.stdout.write('\r\x1b[K');
|
|
465
|
+
this.pendingBeforeFailure = {
|
|
466
|
+
error: null
|
|
467
|
+
};
|
|
468
|
+
} else if (text.includes('Result for') && text.includes('before:') && tokens.length >= 1) {
|
|
469
|
+
// Before error result: "Package testing: Result for {{Category}} before: error message"
|
|
470
|
+
const errorMsg = text.split('before: ').slice(-1)[0];
|
|
471
|
+
if (this.pendingBeforeFailure) this.pendingBeforeFailure.error = errorMsg;
|
|
472
|
+
} else if (text.includes('Category after()') && text.includes('failed') && tokens.length >= 1) {
|
|
473
|
+
// Category after() failed: "Package testing: Category after() {{Category}} failed"
|
|
474
|
+
process.stdout.write('\r\x1b[K');
|
|
475
|
+
this.pendingAfterFailure = {
|
|
476
|
+
error: null
|
|
477
|
+
};
|
|
478
|
+
} else if (text.includes('Result for') && text.includes('after:') && tokens.length >= 1) {
|
|
479
|
+
// After error result: "Package testing: Result for {{Category}} after: error message"
|
|
480
|
+
const errorMsg = text.split('after: ').slice(-1)[0];
|
|
481
|
+
if (this.pendingAfterFailure) this.pendingAfterFailure.error = errorMsg;
|
|
482
|
+
} else if (text.includes('Unhandled Exception')) {
|
|
483
|
+
process.stdout.write('\r\x1b[K\n');
|
|
484
|
+
const errorMsg = text.replace('Package testing: Unhandled Exception: ', '');
|
|
485
|
+
if (errorMsg && errorMsg !== 'null') console.log(`\x1b[31m❌ Unhandled Exception: ${errorMsg}\x1b[0m`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
finish() {
|
|
489
|
+
if (this.currentCategory && this.categoryResults.has(this.currentCategory)) this.printCategorySummary(this.currentCategory);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
exports.TestProgressReporter = TestProgressReporter;
|
|
366
493
|
function printBrowsersResult(browserResult, verbose = false) {
|
|
367
494
|
// Skip detailed summary if modernOutput was used (already printed per-category)
|
|
368
495
|
if (!browserResult.modernOutput) {
|
|
@@ -414,8 +541,9 @@ async function runTests(testParams) {
|
|
|
414
541
|
let countSkipped = 0;
|
|
415
542
|
let countFailed = 0;
|
|
416
543
|
try {
|
|
417
|
-
// Check
|
|
544
|
+
// Check feature support by looking at the test function's declared inputs
|
|
418
545
|
let retrySupported = false;
|
|
546
|
+
let excludeNodeSupported = false;
|
|
419
547
|
try {
|
|
420
548
|
const funcs = window.DG.Func.find({
|
|
421
549
|
package: testParams.package,
|
|
@@ -424,6 +552,7 @@ async function runTests(testParams) {
|
|
|
424
552
|
if (funcs && funcs.length > 0) {
|
|
425
553
|
const testFunc = funcs[0];
|
|
426
554
|
retrySupported = testFunc.inputs?.some(input => input.name === 'skipToCategory') === true;
|
|
555
|
+
excludeNodeSupported = testFunc.inputs?.some(input => input.name === 'excludeNodeTests') === true;
|
|
427
556
|
}
|
|
428
557
|
} catch (e) {
|
|
429
558
|
retrySupported = false;
|
|
@@ -431,6 +560,8 @@ async function runTests(testParams) {
|
|
|
431
560
|
const testCallParams = {};
|
|
432
561
|
if (testParams.params?.category) testCallParams.category = testParams.params.category;
|
|
433
562
|
if (testParams.params?.test) testCallParams.test = testParams.params.test;
|
|
563
|
+
// Skip node-marked tests in the browser only when the Node pass already ran them
|
|
564
|
+
if (testParams.params?.excludeNodeTests && excludeNodeSupported) testCallParams.excludeNodeTests = true;
|
|
434
565
|
// Only pass retry-related params if supported
|
|
435
566
|
if (retrySupported) {
|
|
436
567
|
if (testParams.params?.skipToCategory) testCallParams.skipToCategory = testParams.params.skipToCategory;
|
|
@@ -692,54 +823,8 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
692
823
|
});
|
|
693
824
|
}
|
|
694
825
|
|
|
695
|
-
//
|
|
696
|
-
const
|
|
697
|
-
let currentCategory = null;
|
|
698
|
-
let currentTestName = null;
|
|
699
|
-
// Store failed tests with their errors to print after category header
|
|
700
|
-
let pendingFailures = [];
|
|
701
|
-
let pendingSkipped = [];
|
|
702
|
-
let pendingBeforeFailure = null;
|
|
703
|
-
let pendingAfterFailure = null;
|
|
704
|
-
let modernOutput = false;
|
|
705
|
-
const printCategorySummary = category => {
|
|
706
|
-
const results = categoryResults.get(category);
|
|
707
|
-
if (!results) return;
|
|
708
|
-
const formattedCategory = category;
|
|
709
|
-
const passedCount = results.passed;
|
|
710
|
-
const skippedSuffix = results.skipped > 0 ? `, \x1b[33m${results.skipped} skipped\x1b[0m` : '';
|
|
711
|
-
if (results.failed > 0 || pendingBeforeFailure || pendingAfterFailure) {
|
|
712
|
-
console.log(`\x1b[31m❌ ${formattedCategory}\x1b[31m (\x1b[32m${passedCount} passed${skippedSuffix}\x1b[31m)\x1b[0m`);
|
|
713
|
-
// Print before() failure first
|
|
714
|
-
if (pendingBeforeFailure) {
|
|
715
|
-
console.log(` \x1b[31m❌ before\x1b[0m`);
|
|
716
|
-
if (pendingBeforeFailure.error) console.log(` \x1b[31m${pendingBeforeFailure.error}\x1b[0m`);
|
|
717
|
-
console.log(` \x1b[33mTo run this category separately use --category "${category}"\x1b[0m`);
|
|
718
|
-
}
|
|
719
|
-
// Print after() failure
|
|
720
|
-
if (pendingAfterFailure) {
|
|
721
|
-
console.log(` \x1b[31m❌ after\x1b[0m`);
|
|
722
|
-
if (pendingAfterFailure.error) console.log(` \x1b[31m${pendingAfterFailure.error}\x1b[0m`);
|
|
723
|
-
console.log(` \x1b[33mTo run this category separately use --category "${category}"\x1b[0m`);
|
|
724
|
-
}
|
|
725
|
-
// Print skipped tests
|
|
726
|
-
for (const skippedName of pendingSkipped) console.log(` \x1b[33m⊘ ${skippedName}\x1b[0m`);
|
|
727
|
-
// Print test failures
|
|
728
|
-
for (const failure of pendingFailures) {
|
|
729
|
-
console.log(` \x1b[31m❌ ${failure.testName}\x1b[0m`);
|
|
730
|
-
if (failure.error) console.log(` \x1b[31m${failure.error}\x1b[0m`);
|
|
731
|
-
console.log(` \x1b[33mTo run this test separately use --category "${category}" --test "${failure.testName}"\x1b[0m`);
|
|
732
|
-
}
|
|
733
|
-
} else {
|
|
734
|
-
console.log(`\x1b[32m✅ ${formattedCategory} (${passedCount} passed${skippedSuffix}\x1b[32m)\x1b[0m`);
|
|
735
|
-
// Print skipped tests
|
|
736
|
-
for (const skippedName of pendingSkipped) console.log(` \x1b[33m⊘ ${skippedName}\x1b[0m`);
|
|
737
|
-
}
|
|
738
|
-
pendingFailures = [];
|
|
739
|
-
pendingSkipped = [];
|
|
740
|
-
pendingBeforeFailure = null;
|
|
741
|
-
pendingAfterFailure = null;
|
|
742
|
-
};
|
|
826
|
+
// Live test-output formatting (shared with the Node pass)
|
|
827
|
+
const reporter = new TestProgressReporter(browserOptions.verbose ?? false);
|
|
743
828
|
|
|
744
829
|
// Print all console messages when verbose mode is enabled
|
|
745
830
|
if (browserOptions.verbose) {
|
|
@@ -756,95 +841,7 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
756
841
|
|
|
757
842
|
// Subscribe to page console events for modern output formatting
|
|
758
843
|
// On retry, old listeners were removed so we need to re-attach
|
|
759
|
-
page.on('console', msg =>
|
|
760
|
-
const text = msg.text();
|
|
761
|
-
if (!text.startsWith('Package testing: ')) return;
|
|
762
|
-
modernOutput = true;
|
|
763
|
-
// Extract tokens in {{...}} format
|
|
764
|
-
const tokens = [];
|
|
765
|
-
const tokenRegex = /\{\{([^}]+)\}\}/g;
|
|
766
|
-
let match;
|
|
767
|
-
while ((match = tokenRegex.exec(text)) !== null) tokens.push(match[1]);
|
|
768
|
-
|
|
769
|
-
// Category start: "Package testing: Started {{Category}}" or "Package testing: Started {{Category}} skipped {{N}}"
|
|
770
|
-
if (text.includes('Started') && (tokens.length === 1 || tokens.length === 2 && text.includes('skipped'))) {
|
|
771
|
-
// Print summary of previous category if exists
|
|
772
|
-
if (currentCategory && categoryResults.has(currentCategory)) printCategorySummary(currentCategory);
|
|
773
|
-
currentCategory = tokens[0];
|
|
774
|
-
const skippedCount = tokens.length === 2 ? parseInt(tokens[1]) || 0 : 0;
|
|
775
|
-
categoryResults.set(currentCategory, {
|
|
776
|
-
passed: 0,
|
|
777
|
-
failed: 0,
|
|
778
|
-
skipped: skippedCount
|
|
779
|
-
});
|
|
780
|
-
pendingFailures = [];
|
|
781
|
-
pendingSkipped = [];
|
|
782
|
-
pendingBeforeFailure = null;
|
|
783
|
-
pendingAfterFailure = null;
|
|
784
|
-
} else if (text.includes('Skipped') && tokens.length === 2 && !text.includes('benchmark')) {
|
|
785
|
-
// Individual skipped test: "Package testing: Skipped {{Category}} {{TestName}}"
|
|
786
|
-
// Only collect if the test belongs to the current active category
|
|
787
|
-
if (tokens[0] === currentCategory) pendingSkipped.push(tokens[1]);
|
|
788
|
-
} else if (text.includes('Started') && tokens.length === 2) {
|
|
789
|
-
// Test start: "Package testing: Started {{Category}} {{TestName}}"
|
|
790
|
-
const category = tokens[0];
|
|
791
|
-
currentTestName = tokens[1];
|
|
792
|
-
process.stdout.write(`${category}: ${currentTestName}...`);
|
|
793
|
-
} else if (text.includes('Finished') && tokens.length === 3) {
|
|
794
|
-
// Test finish: "Package testing: Finished {{Category}} {{TestName}} with {{success/error}} for X ms"
|
|
795
|
-
const category = tokens[0];
|
|
796
|
-
const testName = tokens[1];
|
|
797
|
-
const status = tokens[2];
|
|
798
|
-
const results = categoryResults.get(category);
|
|
799
|
-
|
|
800
|
-
// Clear the current test line
|
|
801
|
-
if (!browserOptions.verbose) process.stdout.write('\r\x1b[K');
|
|
802
|
-
if (status === 'success') {
|
|
803
|
-
if (results) results.passed++;
|
|
804
|
-
} else {
|
|
805
|
-
if (results) results.failed++;
|
|
806
|
-
pendingFailures.push({
|
|
807
|
-
testName,
|
|
808
|
-
error: null
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
currentTestName = null;
|
|
812
|
-
} else if (text.includes('Result for') && tokens.length === 2) {
|
|
813
|
-
// Error result: "Package testing: Result for {{Category}} {{TestName}}: error message"
|
|
814
|
-
const errorMsg = text.split(': ').slice(-1)[0];
|
|
815
|
-
// Attach error to the last pending failure
|
|
816
|
-
if (pendingFailures.length > 0) pendingFailures[pendingFailures.length - 1].error = errorMsg;
|
|
817
|
-
} else if (text.includes('Category before()') && text.includes('failed') && tokens.length >= 1) {
|
|
818
|
-
// Category before() failed: "Package testing: Category before() {{Category}} failed"
|
|
819
|
-
process.stdout.write('\r\x1b[K');
|
|
820
|
-
pendingBeforeFailure = {
|
|
821
|
-
error: null
|
|
822
|
-
};
|
|
823
|
-
} else if (text.includes('Result for') && text.includes('before:') && tokens.length >= 1) {
|
|
824
|
-
// Before error result: "Package testing: Result for {{Category}} before: error message"
|
|
825
|
-
const errorMsg = text.split('before: ').slice(-1)[0];
|
|
826
|
-
if (pendingBeforeFailure) pendingBeforeFailure.error = errorMsg;
|
|
827
|
-
} else if (text.includes('Category after()') && text.includes('failed') && tokens.length >= 1) {
|
|
828
|
-
// Category after() failed: "Package testing: Category after() {{Category}} failed"
|
|
829
|
-
process.stdout.write('\r\x1b[K');
|
|
830
|
-
pendingAfterFailure = {
|
|
831
|
-
error: null
|
|
832
|
-
};
|
|
833
|
-
} else if (text.includes('Result for') && text.includes('after:') && tokens.length >= 1) {
|
|
834
|
-
// After error result: "Package testing: Result for {{Category}} after: error message"
|
|
835
|
-
const errorMsg = text.split('after: ').slice(-1)[0];
|
|
836
|
-
if (pendingAfterFailure) pendingAfterFailure.error = errorMsg;
|
|
837
|
-
} else if (text.includes('Unhandled Exception')) {
|
|
838
|
-
// Unhandled exception: "Package testing: Unhandled Exception: ..."
|
|
839
|
-
// Clear any pending test line and move to new line
|
|
840
|
-
process.stdout.write('\r\x1b[K\n');
|
|
841
|
-
const errorMsg = text.replace('Package testing: Unhandled Exception: ', '');
|
|
842
|
-
if (errorMsg && errorMsg !== 'null') console.log(`\x1b[31m❌ Unhandled Exception: ${errorMsg}\x1b[0m`);
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
const printFinalCategorySummary = () => {
|
|
846
|
-
if (currentCategory && categoryResults.has(currentCategory)) printCategorySummary(currentCategory);
|
|
847
|
-
};
|
|
844
|
+
page.on('console', msg => reporter.onLine(msg.text()));
|
|
848
845
|
const testingResults = await page.evaluate((testData, options) => {
|
|
849
846
|
if (options.benchmark) window.DG.Test.isInBenchmark = true;
|
|
850
847
|
if (options.reproduce) window.DG.Test.isReproducing = true;
|
|
@@ -889,9 +886,9 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
889
886
|
}, testsToRun, browserOptions);
|
|
890
887
|
|
|
891
888
|
// Print the final category summary
|
|
892
|
-
|
|
889
|
+
reporter.finish();
|
|
893
890
|
if (browserOptions.record && !existingBrowserSession) await recorder?.stop();
|
|
894
|
-
if (modernOutput) {
|
|
891
|
+
if (reporter.modernOutput) {
|
|
895
892
|
testingResults.verbosePassed = '';
|
|
896
893
|
testingResults.verboseSkipped = '';
|
|
897
894
|
testingResults.verboseFailed = '';
|
|
@@ -908,7 +905,7 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
908
905
|
page,
|
|
909
906
|
webUrl
|
|
910
907
|
} : undefined,
|
|
911
|
-
modernOutput
|
|
908
|
+
modernOutput: reporter.modernOutput
|
|
912
909
|
};
|
|
913
910
|
}, testInvocationTimeout);
|
|
914
911
|
}
|
|
@@ -950,6 +947,29 @@ class TestContext {
|
|
|
950
947
|
}
|
|
951
948
|
}
|
|
952
949
|
exports.TestContext = TestContext;
|
|
950
|
+
/**
|
|
951
|
+
* Merges two test-result CSVs by column NAME (union of headers), unlike
|
|
952
|
+
* mergeBrowsersResults' line concatenation which assumes identical column order.
|
|
953
|
+
* Used to merge the Node-pass report into the browser report.
|
|
954
|
+
*/
|
|
955
|
+
function mergeCsvByHeader(a, b) {
|
|
956
|
+
const hasRows = csv => csv && csv.trim().split('\n').length >= 2;
|
|
957
|
+
if (!hasRows(a)) return hasRows(b) ? b : a || b || '';
|
|
958
|
+
if (!hasRows(b)) return a;
|
|
959
|
+
const pa = _papaparse.default.parse(a.trim(), {
|
|
960
|
+
header: true,
|
|
961
|
+
skipEmptyLines: true
|
|
962
|
+
});
|
|
963
|
+
const pb = _papaparse.default.parse(b.trim(), {
|
|
964
|
+
header: true,
|
|
965
|
+
skipEmptyLines: true
|
|
966
|
+
});
|
|
967
|
+
const columns = [...(pa.meta.fields ?? [])];
|
|
968
|
+
for (const f of pb.meta.fields ?? []) if (!columns.includes(f)) columns.push(f);
|
|
969
|
+
return _papaparse.default.unparse([...pa.data, ...pb.data], {
|
|
970
|
+
columns
|
|
971
|
+
});
|
|
972
|
+
}
|
|
953
973
|
function addColumnToCsv(csv, columnName, defaultValue) {
|
|
954
974
|
const results = _papaparse.default.parse(csv, {
|
|
955
975
|
header: true,
|
package/package.json
CHANGED