datagrok-tools 6.4.8 → 6.5.0
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 +34 -30
- package/README.md +2 -0
- package/bin/commands/help.js +2 -0
- package/bin/commands/test.js +50 -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 +138 -142
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,37 +1,41 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
+
## 6.5.0 (2026-07-22)
|
|
4
|
+
|
|
5
|
+
* `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.
|
|
6
|
+
|
|
3
7
|
## 6.4.8 (2026-07-22)
|
|
4
8
|
|
|
5
|
-
* GROK-20452: `grok publish`
|
|
9
|
+
* 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.
|
|
10
|
+
* `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
11
|
|
|
7
12
|
## 6.4.7 (2026-07-22)
|
|
8
13
|
|
|
9
|
-
* GROK-20452: `grok publish`
|
|
14
|
+
* GROK-20452: `grok publish` ??? generate `dockerfiles/celery` / `dockerfiles/queue` **before** gathering files for the zip. They were generated after, so the generated Dockerfile never reached the server: `meta.queue` functions fell back to a server-side container named `<pkg>-queue-celery` while the client had built `<pkg>-queue` ??? image validation 404'd and the worker container never started ("Container is not started" on every call).
|
|
10
15
|
|
|
11
16
|
## 6.4.6 (2026-07-22)
|
|
12
17
|
|
|
13
|
-
* `grok test`
|
|
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.
|
|
18
|
+
* `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.
|
|
15
19
|
|
|
16
20
|
## 6.4.5 (2026-06-23)
|
|
17
21
|
|
|
18
|
-
* `grok test`
|
|
22
|
+
* `grok test` ??? screen recording (`--record`) is now optional: if `page.screencast()` can't start (e.g. `ffmpeg` is missing on the runner, `spawnSync ffmpeg ENOENT`), the run warns and continues instead of failing the whole Puppeteer pass with 0 tests executed.
|
|
19
23
|
|
|
20
24
|
## 6.4.4 (2026-06-22)
|
|
21
25
|
|
|
22
|
-
* `grok publish`
|
|
26
|
+
* `grok publish` ??? fixed every publish failing with a silent `exit 1` after a successful upload: a stray `fs.unlinkSync('zip')` threw `ENOENT` (the archive is streamed in-memory, no `zip` file is ever written), and the surrounding `catch` only logged under `--verbose`. The errant unlink is removed and publish errors are now always surfaced.
|
|
23
27
|
|
|
24
28
|
## 6.4.3 (2026-06-22)
|
|
25
29
|
|
|
26
|
-
* `grok api`
|
|
30
|
+
* `grok api` ??? numeric IVP model inputs are now generated with `nullable: false`, so an emptied input field fails form validation instead of running the solver with a null.
|
|
27
31
|
|
|
28
32
|
## 6.4.2 (2026-06-18)
|
|
29
33
|
|
|
30
|
-
* `grok publish`
|
|
34
|
+
* `grok publish` ??? registry-aware Docker fallback: when a package's image isn't built locally and the target server has no compatible record, `grok publish` now checks the configured registry and Docker Hub (`docker manifest inspect`) for the expected `datagrok/<name>:<version>` (and content-hashed) tag and uses it, instead of reporting "No fallback available" and failing. Fixes dependency publishes (e.g. Bio ??? @datagrok/chem) on CI runners where the image exists in the registry but not locally.
|
|
31
35
|
|
|
32
36
|
## 6.4.1 (2026-06-18)
|
|
33
37
|
|
|
34
|
-
* Fixed `grok` failing with `Cannot find module './commands/build'`
|
|
38
|
+
* Fixed `grok` failing with `Cannot find module './commands/build'` ??? the `.npmignore` `build.js` rule was unanchored and excluded the compiled `bin/commands/build.js` from the published package; anchored it to `/build.js`.
|
|
35
39
|
* Pinned `ignore-walk` to ^6.0.5 so the package still supports Node 18 (9.x requires Node 22+).
|
|
36
40
|
|
|
37
41
|
## 6.4.0 (2026-06-18)
|
|
@@ -43,46 +47,46 @@
|
|
|
43
47
|
|
|
44
48
|
## 6.3.3 (2026-06-16)
|
|
45
49
|
|
|
46
|
-
* Fixed Celery Docker image generation
|
|
50
|
+
* Fixed Celery Docker image generation ??? the image wasn't built locally on publish.
|
|
47
51
|
|
|
48
52
|
## 6.3.2 (2026-06-15)
|
|
49
53
|
|
|
50
|
-
* `func-gen` webpack plugin
|
|
51
|
-
* `func-gen` webpack plugin
|
|
52
|
-
* `grok stresstest`
|
|
54
|
+
* `func-gen` webpack plugin ??? generated RichFunctionView model inputs now use the script-form names (argument bounds/step `_t0`/`_t1`/`_h`, loop count `_count`) instead of the deprefixed forms, so the run, fitting, and sensitivity-analysis paths share one set of input names with diff-grok's pipeline. Fixes `Inconsistent inputs: "_t0" is missing` when starting fitting/SA from a Rich Function View.
|
|
55
|
+
* `func-gen` webpack plugin ??? the generated RichFunctionView model output annotation appends the `DiffStudio Facet` viewer (last), alongside Grid and Line chart.
|
|
56
|
+
* `grok stresstest` ??? run the ApiTests Node test runner directly via tsx (`src/package-test-node.ts`) instead of a compiled `dist-node` bundle; dropped the obsolete `npm run build-node` step and the `tsconfig-paths-bootstrap.js` / `dist-node` invocation.
|
|
53
57
|
|
|
54
58
|
## 6.3.0 (2026-06-12)
|
|
55
59
|
|
|
56
|
-
* `func-gen` webpack plugin
|
|
60
|
+
* `func-gen` webpack plugin ??? generates RichFunctionView model wrappers from Diff Studio `#meta.role: model` `.ivp` files. Inputs/output are derived from the parsed IVP, `#meta.icon` becomes the model icon, and `#meta.inputs` lookups are emitted as real `propagateChoice: all` inputs (rendered natively by the Rich Function View, unlike `meta.inputs`). Ships a prebuilt, tree-shaken CJS bundle of diff-grok's IVP parser (`plugins/ivp-parser.bundle.cjs`); regenerate with `npm run update:ivp-parser`.
|
|
57
61
|
|
|
58
62
|
## 6.2.6 (2026-05-26)
|
|
59
63
|
|
|
60
|
-
* `grok s tables upload`
|
|
64
|
+
* `grok s tables upload` ??? accepts `.d42` binary blobs in addition to `.csv`. Content-Type is auto-detected from the file extension (`application/octet-stream` for d42, `text/csv` otherwise); server content-negotiates and persists either form against the same `/public/v1/tables/{name}` endpoint.
|
|
61
65
|
|
|
62
66
|
## 6.2.5 (2026-05-21)
|
|
63
67
|
|
|
64
|
-
* `grok report read`
|
|
65
|
-
* `grok report read`
|
|
68
|
+
* `grok report read` ??? renamed `--extract-actions` to `--extract-client-log`; sidecar is now `<stem>_client_log.json`. The old flag is no longer accepted.
|
|
69
|
+
* `grok report read` ??? legacy `actions` field in pre-consolidation report zips is folded into `clientLog` at read time (stderr warning emitted), so downstream consumers see one canonical field. Companion to the platform-side merge that drops `reports_data.actions` in favor of `client_log`.
|
|
66
70
|
|
|
67
71
|
## 6.2.4 (2026-05-13)
|
|
68
72
|
|
|
69
|
-
* GROK-20097: package template
|
|
73
|
+
* GROK-20097: package template ??? replaced deprecated `"moduleResolution": "node"` in `tsconfig.json` with `"bundler"`, suppressing the TypeScript warning for new packages.
|
|
70
74
|
|
|
71
75
|
## 6.2.3 (2026-05-06)
|
|
72
76
|
|
|
73
|
-
* `grok test`
|
|
77
|
+
* `grok test` ??? added `--skip-puppeteer` flag (symmetric counterpart of `--skip-playwright`). Bypasses `loadPackages()` and the Puppeteer/`DG.Test` runner so Playwright-only test directories (e.g. `public/playwright-public`) can run end-to-end via `grok test --skip-puppeteer` without tripping the Dart/JS package loader.
|
|
74
78
|
|
|
75
79
|
## 6.2.2 (2026-05-05)
|
|
76
80
|
|
|
77
|
-
* `grok test`
|
|
81
|
+
* `grok test` ??? Playwright runner now writes `test-report-playwright.csv` next to the existing merged `test-report.csv`, so CI can ship Playwright rows to a dedicated Datlas reporting bucket without disturbing the legacy `package` flow.
|
|
78
82
|
|
|
79
83
|
## 6.2.1 (2026-05-05)
|
|
80
84
|
|
|
81
|
-
* Reports: `grok report attach <ticket> <file>`
|
|
85
|
+
* Reports: `grok report attach <ticket> <file>` ??? upload a file as a JIRA issue attachment via REST v2 multipart POST.
|
|
82
86
|
|
|
83
87
|
## 6.2.0 (2026-05-04)
|
|
84
88
|
|
|
85
|
-
* `grok test`
|
|
89
|
+
* `grok test` ??? Playwright support: when a package's `package.json` declares `"playwrightTests": "<path>"`, `grok test` runs `npx playwright test` against that directory in addition to the existing Puppeteer pass and merges results into a single `test-report.csv`. Auth is unified with the Puppeteer pass (dev key from `~/.grok/config.yaml` ??? session token ??? cookie + `localStorage` injection ??? no login form). Optional `DATAGROK_DEV_KEY_2` env var enables a second-user identity for specs that need it (`DATAGROK_AUTH_TOKEN_2` exposed to specs). New `--skip-playwright` flag opts out of the Playwright pass for a single run.
|
|
86
90
|
|
|
87
91
|
## 6.1.14 (2026-05-01)
|
|
88
92
|
|
|
@@ -94,17 +98,17 @@
|
|
|
94
98
|
|
|
95
99
|
## 6.1.11 (2026-04-27)
|
|
96
100
|
|
|
97
|
-
* `grok report read <path | instance number>`
|
|
98
|
-
* `grok report fetch`
|
|
101
|
+
* `grok report read <path | instance number>` ??? normalize a report zip/json into one JSON object on stdout (envelope unwrap, `_meta.json` merge, optional `--extract-screenshot` / `--extract-d42` / `--extract-actions`)
|
|
102
|
+
* `grok report fetch` ??? single round-trip via the new `/reports/by-number/<number>/zip` endpoint, with fallback to the legacy search-then-download path on 404
|
|
99
103
|
|
|
100
104
|
## 6.1.10 (2026-04-17)
|
|
101
105
|
|
|
102
|
-
* `grok s connections save`
|
|
103
|
-
* `grok s connections test`
|
|
104
|
-
* `grok s users save`
|
|
105
|
-
* `grok s groups save`
|
|
106
|
-
* `grok s shares add`
|
|
107
|
-
* `grok s shares list`
|
|
106
|
+
* `grok s connections save` ??? create/update a connection from a JSON file (`--save-credentials` optional)
|
|
107
|
+
* `grok s connections test` ??? test connectivity by JSON body or by existing id/name
|
|
108
|
+
* `grok s users save` ??? create/update a user from a JSON file
|
|
109
|
+
* `grok s groups save` ??? create/update a group from a JSON file (`--save-relations` optional)
|
|
110
|
+
* `grok s shares add` ??? share an entity with one or more groups (`--access View|Edit`)
|
|
111
|
+
* `grok s shares list` ??? list who an entity is shared with
|
|
108
112
|
* Tools: Normalize package name to lowercase in grok create, preserve original as friendlyName
|
|
109
113
|
|
|
110
114
|
## 5.1.3 (2026-02-03)
|
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,25 @@ 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 (same CSV columns/order).
|
|
293
|
+
if (!res.csv || res.csv.trim().split('\n').length < 2) {
|
|
294
|
+
res.csv = nodeRes.csv;
|
|
295
|
+
res.passedAmount += nodeRes.passedAmount;
|
|
296
|
+
res.failedAmount += nodeRes.failedAmount;
|
|
297
|
+
res.skippedAmount += nodeRes.skippedAmount;
|
|
298
|
+
res.failed = res.failed || nodeRes.failed;
|
|
299
|
+
res.verbosePassed = (res.verbosePassed || '') + nodeRes.verbosePassed;
|
|
300
|
+
res.verboseFailed = (res.verboseFailed || '') + nodeRes.verboseFailed;
|
|
301
|
+
res.verboseSkipped = (res.verboseSkipped || '') + nodeRes.verboseSkipped;
|
|
302
|
+
res.modernOutput = res.modernOutput || nodeRes.modernOutput;
|
|
303
|
+
} else if (nodeRes.csv && nodeRes.csv.trim().split('\n').length >= 2) {
|
|
304
|
+
res = await (0, _testUtils.mergeBrowsersResults)([res, nodeRes]);
|
|
305
|
+
} else {
|
|
306
|
+
res.failed = res.failed || nodeRes.failed;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!args['skip-playwright'] && !args['node-only']) {
|
|
269
310
|
const ptDir = playwrightRunner.hasPlaywrightTests(curDir);
|
|
270
311
|
if (ptDir) {
|
|
271
312
|
const ptRes = await playwrightRunner.runPlaywrightTests(curDir, ptDir, args, args.host ?? '');
|
|
@@ -312,7 +353,7 @@ const retriedTests = new Set();
|
|
|
312
353
|
let totalRetries = 0;
|
|
313
354
|
const MAX_RETRIES_PER_SESSION = 10;
|
|
314
355
|
let retryEnabled = true;
|
|
315
|
-
async function runTesting(args) {
|
|
356
|
+
async function runTesting(args, excludeNodeTests = false) {
|
|
316
357
|
retryEnabled = args['retry'] ?? true;
|
|
317
358
|
if (args.test || args.category) retryEnabled = false;
|
|
318
359
|
let organized = {
|
|
@@ -320,6 +361,7 @@ async function runTesting(args) {
|
|
|
320
361
|
params: {
|
|
321
362
|
category: args.category ?? '',
|
|
322
363
|
test: args.test ?? '',
|
|
364
|
+
excludeNodeTests: excludeNodeTests || undefined,
|
|
323
365
|
options: {
|
|
324
366
|
catchUnhandled: args.catchUnhandled,
|
|
325
367
|
report: args.report
|
|
@@ -956,6 +998,8 @@ function buildTestArgs(args) {
|
|
|
956
998
|
if (args['stress-test']) parts.push('--stress-test');
|
|
957
999
|
if (args['ci-cd']) parts.push('--ci-cd');
|
|
958
1000
|
if (args['no-retry']) parts.push('--no-retry');
|
|
1001
|
+
if (args['skip-node']) parts.push('--skip-node');
|
|
1002
|
+
if (args['node-only']) parts.push('--node-only');
|
|
959
1003
|
return parts;
|
|
960
1004
|
}
|
|
961
1005
|
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;
|
|
@@ -363,6 +363,132 @@ async function loadTestsList(packages, core = false, record = false) {
|
|
|
363
363
|
function addLogsToFile(filePath, stringToSave) {
|
|
364
364
|
_fs.default.appendFileSync(filePath, `${stringToSave}`);
|
|
365
365
|
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Parses the `Package testing: ...` progress lines emitted by the
|
|
369
|
+
* @datagrok-libraries/test runner and prints live per-category summaries.
|
|
370
|
+
* Used for both the Puppeteer console stream and the Node-pass worker stdout.
|
|
371
|
+
*/
|
|
372
|
+
class TestProgressReporter {
|
|
373
|
+
modernOutput = false;
|
|
374
|
+
categoryResults = new Map();
|
|
375
|
+
currentCategory = null;
|
|
376
|
+
pendingFailures = [];
|
|
377
|
+
pendingSkipped = [];
|
|
378
|
+
pendingBeforeFailure = null;
|
|
379
|
+
pendingAfterFailure = null;
|
|
380
|
+
constructor(verbose = false) {
|
|
381
|
+
this.verbose = verbose;
|
|
382
|
+
}
|
|
383
|
+
printCategorySummary(category) {
|
|
384
|
+
const results = this.categoryResults.get(category);
|
|
385
|
+
if (!results) return;
|
|
386
|
+
const passedCount = results.passed;
|
|
387
|
+
const skippedSuffix = results.skipped > 0 ? `, \x1b[33m${results.skipped} skipped\x1b[0m` : '';
|
|
388
|
+
if (results.failed > 0 || this.pendingBeforeFailure || this.pendingAfterFailure) {
|
|
389
|
+
console.log(`\x1b[31m❌ ${category}\x1b[31m (\x1b[32m${passedCount} passed${skippedSuffix}\x1b[31m)\x1b[0m`);
|
|
390
|
+
if (this.pendingBeforeFailure) {
|
|
391
|
+
console.log(` \x1b[31m❌ before\x1b[0m`);
|
|
392
|
+
if (this.pendingBeforeFailure.error) console.log(` \x1b[31m${this.pendingBeforeFailure.error}\x1b[0m`);
|
|
393
|
+
console.log(` \x1b[33mTo run this category separately use --category "${category}"\x1b[0m`);
|
|
394
|
+
}
|
|
395
|
+
if (this.pendingAfterFailure) {
|
|
396
|
+
console.log(` \x1b[31m❌ after\x1b[0m`);
|
|
397
|
+
if (this.pendingAfterFailure.error) console.log(` \x1b[31m${this.pendingAfterFailure.error}\x1b[0m`);
|
|
398
|
+
console.log(` \x1b[33mTo run this category separately use --category "${category}"\x1b[0m`);
|
|
399
|
+
}
|
|
400
|
+
for (const skippedName of this.pendingSkipped) console.log(` \x1b[33m⊘ ${skippedName}\x1b[0m`);
|
|
401
|
+
for (const failure of this.pendingFailures) {
|
|
402
|
+
console.log(` \x1b[31m❌ ${failure.testName}\x1b[0m`);
|
|
403
|
+
if (failure.error) console.log(` \x1b[31m${failure.error}\x1b[0m`);
|
|
404
|
+
console.log(` \x1b[33mTo run this test separately use --category "${category}" --test "${failure.testName}"\x1b[0m`);
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
console.log(`\x1b[32m✅ ${category} (${passedCount} passed${skippedSuffix}\x1b[32m)\x1b[0m`);
|
|
408
|
+
for (const skippedName of this.pendingSkipped) console.log(` \x1b[33m⊘ ${skippedName}\x1b[0m`);
|
|
409
|
+
}
|
|
410
|
+
this.pendingFailures = [];
|
|
411
|
+
this.pendingSkipped = [];
|
|
412
|
+
this.pendingBeforeFailure = null;
|
|
413
|
+
this.pendingAfterFailure = null;
|
|
414
|
+
}
|
|
415
|
+
onLine(text) {
|
|
416
|
+
if (!text.startsWith('Package testing: ')) return;
|
|
417
|
+
this.modernOutput = true;
|
|
418
|
+
// Extract tokens in {{...}} format
|
|
419
|
+
const tokens = [];
|
|
420
|
+
const tokenRegex = /\{\{([^}]+)\}\}/g;
|
|
421
|
+
let match;
|
|
422
|
+
while ((match = tokenRegex.exec(text)) !== null) tokens.push(match[1]);
|
|
423
|
+
|
|
424
|
+
// Category start: "Package testing: Started {{Category}}" or "Package testing: Started {{Category}} skipped {{N}}"
|
|
425
|
+
if (text.includes('Started') && (tokens.length === 1 || tokens.length === 2 && text.includes('skipped'))) {
|
|
426
|
+
if (this.currentCategory && this.categoryResults.has(this.currentCategory)) this.printCategorySummary(this.currentCategory);
|
|
427
|
+
this.currentCategory = tokens[0];
|
|
428
|
+
const skippedCount = tokens.length === 2 ? parseInt(tokens[1]) || 0 : 0;
|
|
429
|
+
this.categoryResults.set(this.currentCategory, {
|
|
430
|
+
passed: 0,
|
|
431
|
+
failed: 0,
|
|
432
|
+
skipped: skippedCount
|
|
433
|
+
});
|
|
434
|
+
this.pendingFailures = [];
|
|
435
|
+
this.pendingSkipped = [];
|
|
436
|
+
this.pendingBeforeFailure = null;
|
|
437
|
+
this.pendingAfterFailure = null;
|
|
438
|
+
} else if (text.includes('Skipped') && tokens.length === 2 && !text.includes('benchmark')) {
|
|
439
|
+
// Individual skipped test: "Package testing: Skipped {{Category}} {{TestName}}"
|
|
440
|
+
if (tokens[0] === this.currentCategory) this.pendingSkipped.push(tokens[1]);
|
|
441
|
+
} else if (text.includes('Started') && tokens.length === 2) {
|
|
442
|
+
// Test start: "Package testing: Started {{Category}} {{TestName}}"
|
|
443
|
+
process.stdout.write(`${tokens[0]}: ${tokens[1]}...`);
|
|
444
|
+
} else if (text.includes('Finished') && tokens.length === 3) {
|
|
445
|
+
// Test finish: "Package testing: Finished {{Category}} {{TestName}} with {{success/error}} for X ms"
|
|
446
|
+
const results = this.categoryResults.get(tokens[0]);
|
|
447
|
+
if (!this.verbose) process.stdout.write('\r\x1b[K');
|
|
448
|
+
if (tokens[2] === 'success') {
|
|
449
|
+
if (results) results.passed++;
|
|
450
|
+
} else {
|
|
451
|
+
if (results) results.failed++;
|
|
452
|
+
this.pendingFailures.push({
|
|
453
|
+
testName: tokens[1],
|
|
454
|
+
error: null
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
} else if (text.includes('Result for') && tokens.length === 2) {
|
|
458
|
+
// Error result: "Package testing: Result for {{Category}} {{TestName}}: error message"
|
|
459
|
+
const errorMsg = text.split(': ').slice(-1)[0];
|
|
460
|
+
if (this.pendingFailures.length > 0) this.pendingFailures[this.pendingFailures.length - 1].error = errorMsg;
|
|
461
|
+
} else if (text.includes('Category before()') && text.includes('failed') && tokens.length >= 1) {
|
|
462
|
+
// Category before() failed: "Package testing: Category before() {{Category}} failed"
|
|
463
|
+
process.stdout.write('\r\x1b[K');
|
|
464
|
+
this.pendingBeforeFailure = {
|
|
465
|
+
error: null
|
|
466
|
+
};
|
|
467
|
+
} else if (text.includes('Result for') && text.includes('before:') && tokens.length >= 1) {
|
|
468
|
+
// Before error result: "Package testing: Result for {{Category}} before: error message"
|
|
469
|
+
const errorMsg = text.split('before: ').slice(-1)[0];
|
|
470
|
+
if (this.pendingBeforeFailure) this.pendingBeforeFailure.error = errorMsg;
|
|
471
|
+
} else if (text.includes('Category after()') && text.includes('failed') && tokens.length >= 1) {
|
|
472
|
+
// Category after() failed: "Package testing: Category after() {{Category}} failed"
|
|
473
|
+
process.stdout.write('\r\x1b[K');
|
|
474
|
+
this.pendingAfterFailure = {
|
|
475
|
+
error: null
|
|
476
|
+
};
|
|
477
|
+
} else if (text.includes('Result for') && text.includes('after:') && tokens.length >= 1) {
|
|
478
|
+
// After error result: "Package testing: Result for {{Category}} after: error message"
|
|
479
|
+
const errorMsg = text.split('after: ').slice(-1)[0];
|
|
480
|
+
if (this.pendingAfterFailure) this.pendingAfterFailure.error = errorMsg;
|
|
481
|
+
} else if (text.includes('Unhandled Exception')) {
|
|
482
|
+
process.stdout.write('\r\x1b[K\n');
|
|
483
|
+
const errorMsg = text.replace('Package testing: Unhandled Exception: ', '');
|
|
484
|
+
if (errorMsg && errorMsg !== 'null') console.log(`\x1b[31m❌ Unhandled Exception: ${errorMsg}\x1b[0m`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
finish() {
|
|
488
|
+
if (this.currentCategory && this.categoryResults.has(this.currentCategory)) this.printCategorySummary(this.currentCategory);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
exports.TestProgressReporter = TestProgressReporter;
|
|
366
492
|
function printBrowsersResult(browserResult, verbose = false) {
|
|
367
493
|
// Skip detailed summary if modernOutput was used (already printed per-category)
|
|
368
494
|
if (!browserResult.modernOutput) {
|
|
@@ -414,8 +540,9 @@ async function runTests(testParams) {
|
|
|
414
540
|
let countSkipped = 0;
|
|
415
541
|
let countFailed = 0;
|
|
416
542
|
try {
|
|
417
|
-
// Check
|
|
543
|
+
// Check feature support by looking at the test function's declared inputs
|
|
418
544
|
let retrySupported = false;
|
|
545
|
+
let excludeNodeSupported = false;
|
|
419
546
|
try {
|
|
420
547
|
const funcs = window.DG.Func.find({
|
|
421
548
|
package: testParams.package,
|
|
@@ -424,6 +551,7 @@ async function runTests(testParams) {
|
|
|
424
551
|
if (funcs && funcs.length > 0) {
|
|
425
552
|
const testFunc = funcs[0];
|
|
426
553
|
retrySupported = testFunc.inputs?.some(input => input.name === 'skipToCategory') === true;
|
|
554
|
+
excludeNodeSupported = testFunc.inputs?.some(input => input.name === 'excludeNodeTests') === true;
|
|
427
555
|
}
|
|
428
556
|
} catch (e) {
|
|
429
557
|
retrySupported = false;
|
|
@@ -431,6 +559,8 @@ async function runTests(testParams) {
|
|
|
431
559
|
const testCallParams = {};
|
|
432
560
|
if (testParams.params?.category) testCallParams.category = testParams.params.category;
|
|
433
561
|
if (testParams.params?.test) testCallParams.test = testParams.params.test;
|
|
562
|
+
// Skip node-marked tests in the browser only when the Node pass already ran them
|
|
563
|
+
if (testParams.params?.excludeNodeTests && excludeNodeSupported) testCallParams.excludeNodeTests = true;
|
|
434
564
|
// Only pass retry-related params if supported
|
|
435
565
|
if (retrySupported) {
|
|
436
566
|
if (testParams.params?.skipToCategory) testCallParams.skipToCategory = testParams.params.skipToCategory;
|
|
@@ -692,54 +822,8 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
692
822
|
});
|
|
693
823
|
}
|
|
694
824
|
|
|
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
|
-
};
|
|
825
|
+
// Live test-output formatting (shared with the Node pass)
|
|
826
|
+
const reporter = new TestProgressReporter(browserOptions.verbose ?? false);
|
|
743
827
|
|
|
744
828
|
// Print all console messages when verbose mode is enabled
|
|
745
829
|
if (browserOptions.verbose) {
|
|
@@ -756,95 +840,7 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
756
840
|
|
|
757
841
|
// Subscribe to page console events for modern output formatting
|
|
758
842
|
// 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
|
-
};
|
|
843
|
+
page.on('console', msg => reporter.onLine(msg.text()));
|
|
848
844
|
const testingResults = await page.evaluate((testData, options) => {
|
|
849
845
|
if (options.benchmark) window.DG.Test.isInBenchmark = true;
|
|
850
846
|
if (options.reproduce) window.DG.Test.isReproducing = true;
|
|
@@ -889,9 +885,9 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
889
885
|
}, testsToRun, browserOptions);
|
|
890
886
|
|
|
891
887
|
// Print the final category summary
|
|
892
|
-
|
|
888
|
+
reporter.finish();
|
|
893
889
|
if (browserOptions.record && !existingBrowserSession) await recorder?.stop();
|
|
894
|
-
if (modernOutput) {
|
|
890
|
+
if (reporter.modernOutput) {
|
|
895
891
|
testingResults.verbosePassed = '';
|
|
896
892
|
testingResults.verboseSkipped = '';
|
|
897
893
|
testingResults.verboseFailed = '';
|
|
@@ -908,7 +904,7 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
908
904
|
page,
|
|
909
905
|
webUrl
|
|
910
906
|
} : undefined,
|
|
911
|
-
modernOutput
|
|
907
|
+
modernOutput: reporter.modernOutput
|
|
912
908
|
};
|
|
913
909
|
}, testInvocationTimeout);
|
|
914
910
|
}
|
package/package.json
CHANGED