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 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` 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.
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` 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).
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` 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.
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` 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.
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` 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.
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` 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.
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` 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.
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'` the `.npmignore` `build.js` rule was unanchored and excluded the compiled `bin/commands/build.js` from the published package; anchored it to `/build.js`.
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 the image wasn't built locally on publish.
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 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.
51
- * `func-gen` webpack plugin the generated RichFunctionView model output annotation appends the `DiffStudio Facet` viewer (last), alongside Grid and Line chart.
52
- * `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.
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 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`.
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` 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.
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` renamed `--extract-actions` to `--extract-client-log`; sidecar is now `<stem>_client_log.json`. The old flag is no longer accepted.
65
- * `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`.
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 replaced deprecated `"moduleResolution": "node"` in `tsconfig.json` with `"bundler"`, suppressing the TypeScript warning for new packages.
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` 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.
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` 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.
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>` upload a file as a JIRA issue attachment via REST v2 multipart POST.
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` 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.
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>` normalize a report zip/json into one JSON object on stdout (envelope unwrap, `_meta.json` merge, optional `--extract-screenshot` / `--extract-d42` / `--extract-actions`)
98
- * `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
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` create/update a connection from a JSON file (`--save-credentials` optional)
103
- * `grok s connections test` test connectivity by JSON body or by existing id/name
104
- * `grok s users save` create/update a user from a JSON file
105
- * `grok s groups save` create/update a group from a JSON file (`--save-relations` optional)
106
- * `grok s shares add` share an entity with one or more groups (`--access View|Edit`)
107
- * `grok s shares list` list who an entity is shared with
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 |
@@ -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)
@@ -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 (!args['skip-playwright']) {
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
+ });
@@ -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 if retry is supported by looking for skipToCategory parameter
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
- // State tracking for test output formatting
696
- const categoryResults = new Map();
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
- printFinalCategorySummary();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.4.8",
3
+ "version": "6.5.0",
4
4
  "description": "Utility to upload and publish packages to Datagrok",
5
5
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
6
6
  "dependencies": {