datagrok-tools 4.14.72 → 5.0.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
@@ -4,6 +4,12 @@
4
4
 
5
5
  ### Bug Fixes
6
6
 
7
+ * Include ts-morph in dependencies
8
+
9
+ ## 4.14.72 (2026-01-13)
10
+
11
+ ### Bug Fixes
12
+
7
13
  * Removed local dependency on js-api
8
14
 
9
15
  ## 4.14.71 (2026-01-10)
@@ -14,12 +14,11 @@ var color = _interopRequireWildcard(require("../utils/color-utils"));
14
14
  var Papa = _interopRequireWildcard(require("papaparse"));
15
15
  var _testUtils = _interopRequireWildcard(require("../utils/test-utils"));
16
16
  var testUtils = _testUtils;
17
- var _orderFunctions = require("../utils/order-functions");
18
17
  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); }
19
18
  /* eslint-disable max-len */
20
19
 
21
20
  const testInvocationTimeout = 3600000;
22
- 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'];
21
+ 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'];
23
22
  const curDir = process.cwd();
24
23
  const grokDir = _path.default.join(_os.default.homedir(), '.grok');
25
24
  const confPath = _path.default.join(grokDir, 'config.yaml');
@@ -47,7 +46,11 @@ async function test(args) {
47
46
  }
48
47
  process.env.TARGET_PACKAGE = packageName;
49
48
  const res = await runTesting(args);
50
- if (args.csv) (0, _testUtils.saveCsvResults)([res.csv], csvReportDir);
49
+ if (args.csv) {
50
+ res.csv = (0, _testUtils.addColumnToCsv)(res.csv, 'stress_test', args['stress-test'] ?? false);
51
+ res.csv = (0, _testUtils.addColumnToCsv)(res.csv, 'benchmark', args.benchmark ?? false);
52
+ (0, _testUtils.saveCsvResults)([res.csv], csvReportDir);
53
+ }
51
54
  (0, _testUtils.printBrowsersResult)(res, args.verbose);
52
55
  if (res.failed) {
53
56
  if (res.verboseFailed === 'Package not found') testUtils.exitWithCode(0);
@@ -60,62 +63,51 @@ function isArgsValid(args) {
60
63
  if (args['_'].length > 1 || options.length > availableCommandOptions.length || options.length > 0 && !options.every(op => availableCommandOptions.includes(op))) return false;
61
64
  return true;
62
65
  }
66
+
67
+ // Retry state - persists across test runs in the session
68
+ const retriedTests = new Set();
69
+ let totalRetries = 0;
70
+ const MAX_RETRIES_PER_SESSION = 10;
71
+ let retryEnabled = true;
63
72
  async function runTesting(args) {
64
- color.info('Loading tests...');
65
- const loadedTests = await (0, _testUtils.loadTestsList)([process.env.TARGET_PACKAGE ?? ''], args.core);
66
- let testsObj = [];
67
- if (args['stress-test'] || args.benchmark) {
68
- for (const element of loadedTests) {
69
- if (args.benchmark && !element.options.benchmark || args['stress-test'] && !element.options.stressTest) continue;
70
- testsObj.push(element);
71
- }
72
- } else testsObj = loadedTests;
73
- const parsed = (0, _orderFunctions.setAlphabeticalOrder)(testsObj, 1, 1);
74
- if (parsed.length == 0) {
75
- return {
76
- failed: true,
77
- error: '',
78
- verbosePassed: 'Package not found',
79
- verboseSkipped: 'Package not found',
80
- verboseFailed: 'Package not found',
81
- passedAmount: 0,
82
- skippedAmount: 0,
83
- failedAmount: 0,
84
- csv: ''
85
- };
86
- }
87
- let organized = parsed[0].map(testObj => ({
88
- package: testObj.packageName,
73
+ retryEnabled = args['retry'] ?? true;
74
+ let organized = {
75
+ package: process.env.TARGET_PACKAGE ?? '',
89
76
  params: {
90
- category: testObj.category,
91
- test: testObj.name,
77
+ category: args.category ?? '',
78
+ test: args.test ?? '',
92
79
  options: {
93
80
  catchUnhandled: args.catchUnhandled,
94
81
  report: args.report
95
82
  }
96
83
  }
97
- }));
98
- const filtered = [];
99
- const categoryRegex = new RegExp(`${args.category?.replaceAll(' ', '')}.*`);
100
- if (args.category) {
101
- for (const element of organized) {
102
- if (categoryRegex.test(element.params.category.replaceAll(' ', ''))) {
103
- if (element.params.test === args.test || !args.test) filtered.push(element);
104
- }
105
- }
106
- organized = filtered;
107
- }
108
- if (args.verbose) {
109
- console.log(organized);
110
- console.log(`Tests total: ${organized.length}`);
111
- }
84
+ };
112
85
  color.info('Starting tests...');
113
86
  const testsResults = [];
114
87
  let r;
115
88
  let browserId = 1;
89
+ let retrySupported = undefined; // Will be set after first run
90
+ let browserSession = undefined;
116
91
  await (0, _testUtils.timeout)(async () => {
117
- do {
118
- r = await (0, _testUtils.runBrowser)(organized, {
92
+ let shouldRetry = true;
93
+ let currentSkipToCategory = undefined;
94
+ let currentSkipToTest = undefined;
95
+ let oneLastTry = false;
96
+ while (shouldRetry || oneLastTry) {
97
+ shouldRetry = false;
98
+ oneLastTry = false;
99
+ // On first run, assume retry is supported; after first run, use actual value
100
+ const useRetry = retryEnabled && (retrySupported === undefined || retrySupported);
101
+ const testParams = {
102
+ ...organized,
103
+ params: {
104
+ ...organized.params,
105
+ skipToCategory: currentSkipToCategory,
106
+ skipToTest: currentSkipToTest,
107
+ returnOnFail: useRetry
108
+ }
109
+ };
110
+ r = await (0, _testUtils.runBrowser)(testParams, {
119
111
  benchmark: args.benchmark ?? false,
120
112
  stressTest: args['stress-test'] ?? false,
121
113
  catchUnhandled: args.catchUnhandled ?? false,
@@ -124,34 +116,77 @@ async function runTesting(args) {
124
116
  report: args.report ?? false,
125
117
  verbose: args.verbose ?? false,
126
118
  ciCd: args['ci-cd'] ?? false,
127
- stopOnTimeout: true,
128
- debug: args['debug'] ?? false
129
- }, browserId, testInvocationTimeout);
130
- const testsLeft = [];
131
- const testsToReproduce = [];
132
- for (const testData of organized) {
133
- if (!r.verbosePassed.includes(`${testData.params.category}: ${testData.params.test}`) && !r.verboseSkipped.includes(`${testData.params.category}: ${testData.params.test}`) && !r.verboseFailed.includes(`${testData.params.category}: ${testData.params.test}`) && !new RegExp(`${testUtils.escapeRegex(testData.params.category.trim())}[^\n]*: *?(before|after)(\\(\\))?`).test(r.verboseFailed)) testsLeft.push(testData);
134
- if (r.verboseFailed.includes(`${testData.params.category}: ${testData.params.test} : Error:`)) testsToReproduce.push(testData);
135
- }
136
- if (testsToReproduce.length > 0) {
137
- const reproduced = await reproducedTest(args, testsToReproduce);
138
- for (const test of testsToReproduce) {
139
- const reproducedTest = reproduced.get(test);
140
- if (reproducedTest && !reproducedTest.failed) r = await updateResultsByReproduced(r, reproducedTest, test);
141
- }
142
- }
143
- r.csv = (0, _testUtils.addColumnToCsv)(r.csv, 'stress_test', args['stress-test'] ?? false);
144
- r.csv = (0, _testUtils.addColumnToCsv)(r.csv, 'benchmark', args.benchmark ?? false);
145
- testsResults.push(r);
146
- organized = testsLeft;
147
- browserId++;
119
+ stopOnTimeout: false,
120
+ debug: args['debug'] ?? false,
121
+ skipToCategory: currentSkipToCategory,
122
+ skipToTest: currentSkipToTest,
123
+ keepBrowserOpen: useRetry // Keep browser open if retry is enabled
124
+ }, browserId, testInvocationTimeout, browserSession);
125
+
126
+ // Store browser session for potential reuse
127
+ if (r.browserSession) browserSession = r.browserSession;
148
128
  if (r.error) {
149
129
  console.log(`\nexecution error:`);
150
130
  console.log(r.error);
151
- break;
131
+ // Close browser on error
132
+ if (browserSession?.browser) await browserSession.browser.close();
133
+ // Add the failed result before returning
134
+ testsResults.push(r);
135
+ return;
136
+ }
137
+ // Check retry support from first run result
138
+ if (useRetry && r.failed && r.lastFailedTest && retrySupported === undefined) {
139
+ retrySupported = r.retrySupported === true;
140
+ if (!retrySupported) color.warn('Retry not supported: test() function does not have skipToCategory parameter');
141
+ }
142
+
143
+ // Check if we should retry on failure
144
+ if (useRetry && r.failed && r.lastFailedTest && retryEnabled && retrySupported) {
145
+ const testKey = `${r.lastFailedTest.category}::${r.lastFailedTest.test}`;
146
+ if (totalRetries >= MAX_RETRIES_PER_SESSION) {
147
+ color.warn(`Maximum retries (${MAX_RETRIES_PER_SESSION}) reached. Disabling retries.`);
148
+ retryEnabled = false;
149
+ oneLastTry = true;
150
+ } else {
151
+ // Retry this test
152
+ retriedTests.add(testKey);
153
+ totalRetries++;
154
+ console.log('Refreshing page for retry...');
155
+ color.info(`Retrying from "${r.lastFailedTest.category}: ${r.lastFailedTest.test}" (retry ${totalRetries}/${MAX_RETRIES_PER_SESSION})...`);
156
+
157
+ // Store results from previous run
158
+ testsResults.push(r);
159
+
160
+ // Set skip params for next run
161
+ currentSkipToCategory = r.lastFailedTest.category;
162
+ currentSkipToTest = r.lastFailedTest.test;
163
+ shouldRetry = true;
164
+ browserId++;
165
+ }
152
166
  }
153
- } while (r.failed);
167
+ if (!shouldRetry) {
168
+ testsResults.push(r);
169
+ }
170
+ }
171
+
172
+ // Close browser after all retries are done
173
+ if (browserSession?.browser) await browserSession.browser.close();
154
174
  }, testInvocationTimeout);
175
+
176
+ // Handle empty results (shouldn't happen but safety check)
177
+ if (testsResults.length === 0) {
178
+ return {
179
+ failed: true,
180
+ verbosePassed: '',
181
+ verboseSkipped: '',
182
+ verboseFailed: 'No test results collected',
183
+ passedAmount: 0,
184
+ skippedAmount: 0,
185
+ failedAmount: 0,
186
+ csv: '',
187
+ error: 'No test results collected'
188
+ };
189
+ }
155
190
  return await (0, _testUtils.mergeBrowsersResults)(testsResults);
156
191
  }
157
192
  async function reproducedTest(args, testsToReproduce) {
@@ -14,6 +14,7 @@ exports.getBrowserPage = getBrowserPage;
14
14
  exports.getDevKey = getDevKey;
15
15
  exports.getToken = getToken;
16
16
  exports.getWebUrl = getWebUrl;
17
+ exports.getWebUrlFromPage = getWebUrlFromPage;
17
18
  exports.loadPackage = loadPackage;
18
19
  exports.loadPackages = loadPackages;
19
20
  exports.loadTestsList = loadTestsList;
@@ -61,6 +62,12 @@ async function getWebUrl(url, token) {
61
62
  const json = await response.json();
62
63
  return json.settings.webRoot;
63
64
  }
65
+ function getWebUrlFromPage(page) {
66
+ const url = page.url();
67
+ // Extract the base URL (protocol + host)
68
+ const urlObj = new URL(url);
69
+ return `${urlObj.protocol}//${urlObj.host}`;
70
+ }
64
71
  function getDevKey(hostKey) {
65
72
  const config = _jsYaml.default.load(_fs.default.readFileSync(confPath, 'utf8'));
66
73
  let host = hostKey == '' ? config.default : hostKey;
@@ -310,23 +317,26 @@ function addLogsToFile(filePath, stringToSave) {
310
317
  _fs.default.appendFileSync(filePath, `${stringToSave}`);
311
318
  }
312
319
  function printBrowsersResult(browserResult, verbose = false) {
313
- if (verbose) {
314
- if ((browserResult.passedAmount ?? 0) > 0 && (browserResult.verbosePassed ?? []).length > 0) {
315
- console.log('Passed: ');
316
- console.log(browserResult.verbosePassed);
320
+ // Skip detailed summary if modernOutput was used (already printed per-category)
321
+ if (!browserResult.modernOutput) {
322
+ if (verbose) {
323
+ if ((browserResult.passedAmount ?? 0) > 0 && (browserResult.verbosePassed ?? []).length > 0) {
324
+ console.log('Passed: ');
325
+ console.log(browserResult.verbosePassed);
326
+ }
327
+ if ((browserResult.skippedAmount ?? 0) > 0 && (browserResult.verboseSkipped ?? []).length > 0) {
328
+ console.log('Skipped: ');
329
+ console.log(browserResult.verboseSkipped);
330
+ }
317
331
  }
318
- if ((browserResult.skippedAmount ?? 0) > 0 && (browserResult.verboseSkipped ?? []).length > 0) {
319
- console.log('Skipped: ');
320
- console.log(browserResult.verboseSkipped);
332
+ if ((browserResult.failedAmount ?? 0) > 0 && (browserResult.verboseFailed ?? []).length > 0) {
333
+ console.log('Failed: ');
334
+ console.log(browserResult.verboseFailed);
321
335
  }
336
+ console.log('Passed amount: ' + browserResult?.passedAmount);
337
+ console.log('Skipped amount: ' + browserResult?.skippedAmount);
338
+ console.log('Failed amount: ' + browserResult?.failedAmount);
322
339
  }
323
- if ((browserResult.failedAmount ?? 0) > 0 && (browserResult.verboseFailed ?? []).length > 0) {
324
- console.log('Failed: ');
325
- console.log(browserResult.verboseFailed);
326
- }
327
- console.log('Passed amount: ' + browserResult?.passedAmount);
328
- console.log('Skipped amount: ' + browserResult?.skippedAmount);
329
- console.log('Failed amount: ' + browserResult?.failedAmount);
330
340
  if (browserResult.failed) {
331
341
  if (browserResult.verboseFailed === 'Package not found') color.fail('Tests not found');else color.fail('Tests failed.');
332
342
  } else color.success('Tests passed.');
@@ -339,146 +349,256 @@ function saveCsvResults(stringToSave, csvReportDir) {
339
349
  _fs.default.writeFileSync(csvReportDir, modifiedStrings.join('\n'), 'utf8');
340
350
  color.info('Saved `test-report.csv`\n');
341
351
  }
342
- async function runTests(testsParams, stopOnFail) {
352
+ async function runTests(testParams) {
343
353
  let failed = false;
344
354
  let verbosePassed = '';
345
355
  let verboseSkipped = '';
346
356
  let verboseFailed = '';
347
- let error = '';
348
357
  let countPassed = 0;
349
358
  let countSkipped = 0;
350
359
  let countFailed = 0;
351
- let resultDF = undefined;
352
- let lastTest = null;
353
- let res = '';
354
360
  try {
355
- for (const testParam of testsParams) {
356
- lastTest = testParam;
357
- const df = await window.grok.functions.call(testParam.package + ':test', testParam.params);
358
- if (!df.getCol('flaking')) {
359
- const flakingCol = window.DG.Column.fromType(window.DG.COLUMN_TYPE.BOOL, 'flaking', df.rowCount);
360
- df.columns.add(flakingCol);
361
- }
362
- if (!df.getCol('package')) {
363
- const packageNameCol = window.DG.Column.fromList(window.DG.COLUMN_TYPE.STRING, 'package', Array(df.rowCount).fill(testParam.package));
364
- df.columns.add(packageNameCol);
365
- }
366
- df.columns.setOrder(['date', 'category', 'name', 'success', 'result', 'ms', 'skipped', 'logs', 'owner', 'package', 'widgetsDifference', 'flaking']);
367
- // addColumn('flaking', (<any>window).DG.Column.fromType((<any>window).DG.COLUMN_TYPE.BOOL, 'flaking', df.rowCount), df);
368
- // addColumn('package', (<any>window).DG.Column.fromType((<any>window).DG.COLUMN_TYPE.BOOL, 'flaking', df.rowCount), df);
369
- // if (!df.getCol('flaking')) {
370
- // const flakingCol = (<any>window).DG.Column.fromType((<any>window).DG.COLUMN_TYPE.BOOL, 'flaking', df.rowCount);
371
- // df.columns.add(flakingCol);
372
- // }
373
- // if (!df.getCol('package')) {
374
- // const packageNameCol =
375
- // (<any>window).DG.Column.fromList((<any>window).DG.COLUMN_TYPE.STRING, 'package', Array(df.rowCount).fill(testParam.package));
376
- // df.columns.add(packageNameCol);
377
- // }
378
- if (df.rowCount === 0) {
379
- verboseFailed += `Test result : Invocation Fail : ${testParam.params.category}: ${testParam.params.test}\n`;
380
- countFailed += 1;
381
- failed = true;
382
- continue;
383
- }
384
- let row = df.rows.get(0);
385
- console.log(`DEBUG: runTests: IN A LOOP: rows in df ${df.rowCount}`);
386
- if (df.rowCount > 1) {
387
- const unhandledErrorRow = df.rows.get(1);
388
- if (!unhandledErrorRow.get('success')) {
389
- unhandledErrorRow['category'] = row.get('category');
390
- unhandledErrorRow['name'] = row.get('name');
391
- row = unhandledErrorRow;
392
- }
361
+ // Check if retry is supported by looking for skipToCategory parameter
362
+ let retrySupported = false;
363
+ try {
364
+ const funcs = window.DG.Func.find({
365
+ package: testParams.package,
366
+ name: 'test'
367
+ });
368
+ if (funcs && funcs.length > 0) {
369
+ const testFunc = funcs[0];
370
+ retrySupported = testFunc.inputs?.some(input => input.name === 'skipToCategory') === true;
393
371
  }
394
- const category = row.get('category');
395
- const testName = row.get('name');
396
- const time = row.get('ms');
397
- const result = row.get('result');
398
- const success = row.get('success');
399
- const skipped = row.get('skipped');
400
- row['flaking'] = success && window.DG.Test.isReproducing;
401
- df.changeColumnType('result', window.DG.COLUMN_TYPE.STRING);
402
- df.changeColumnType('logs', window.DG.COLUMN_TYPE.STRING);
403
- if (df.col('widgetsDifference')) df.changeColumnType('widgetsDifference', window.DG.COLUMN_TYPE.STRING);
404
- // df.changeColumnType('memoryDelta', (<any>window).DG.COLUMN_TYPE.BIG_INT);
372
+ } catch (e) {
373
+ retrySupported = false;
374
+ }
375
+ const testCallParams = {};
376
+ if (testParams.params?.category) testCallParams.category = testParams.params.category;
377
+ if (testParams.params?.test) testCallParams.test = testParams.params.test;
378
+ // Only pass retry-related params if supported
379
+ if (retrySupported) {
380
+ if (testParams.params?.skipToCategory) testCallParams.skipToCategory = testParams.params.skipToCategory;
381
+ if (testParams.params?.skipToTest) testCallParams.skipToTest = testParams.params.skipToTest;
382
+ if (testParams.params?.returnOnFail) testCallParams.returnOnFail = testParams.params.returnOnFail;
383
+ }
384
+ const df = await window.grok.functions.call(testParams.package + ':test', Object.keys(testCallParams).length > 0 ? testCallParams : undefined);
385
+ if (!df.getCol('flaking')) {
386
+ const flakingCol = window.DG.Column.fromType(window.DG.COLUMN_TYPE.BOOL, 'flaking', df.rowCount);
387
+ df.columns.add(flakingCol);
388
+ }
389
+ if (!df.getCol('package')) {
390
+ const packageNameCol = window.DG.Column.fromList(window.DG.COLUMN_TYPE.STRING, 'package', Array(df.rowCount).fill(testParams.package));
391
+ df.columns.add(packageNameCol);
392
+ }
393
+ df.columns.setOrder(['date', 'category', 'name', 'success', 'result', 'ms', 'skipped', 'logs', 'owner', 'package', 'widgetsDifference', 'flaking']);
394
+ df.changeColumnType('result', window.DG.COLUMN_TYPE.STRING);
395
+ df.changeColumnType('logs', window.DG.COLUMN_TYPE.STRING);
396
+ if (df.col('widgetsDifference')) df.changeColumnType('widgetsDifference', window.DG.COLUMN_TYPE.STRING);
397
+ // df.changeColumnType('memoryDelta', (<any>window).DG.COLUMN_TYPE.BIG_INT);
405
398
 
406
- if (resultDF === undefined) resultDF = df;else {
407
- console.log(`DEBUG: COLUMN NAMES IN RESULT_DF: ${resultDF.columns.names()}`);
408
- console.log(`DEBUG: COLUMN TYPES IN RESULT_DF: ${resultDF.columns.names().map(c => `${c}: ${resultDF.col(c)?.type}`)}`);
409
- console.log(`DEBUG: COLUMN NAMES IN DF: ${df.columns.names()}`);
410
- console.log(`DEBUG: COLUMN TYPES IN DF: ${df.columns.names().map(c => `${c}: ${df.col(c)?.type}`)}`);
411
- resultDF = resultDF.append(df);
412
- }
399
+ let lastFailedTest = undefined;
400
+ for (let row of df.rows) {
413
401
  if (row['skipped']) {
414
- verboseSkipped += `Test result : Skipped : ${time} : ${category}: ${testName} : ${result}\n`;
402
+ verboseSkipped += `${row['category']}: ${row['name']} (${row['ms']} ms) : ${row['result']}\n`;
415
403
  countSkipped += 1;
416
404
  } else if (row['success']) {
417
- verbosePassed += `Test result : Success : ${time} : ${category}: ${testName} : ${result}\n`;
405
+ verbosePassed += `${row['category']}: ${row['name']} (${row['ms']} ms) : ${row['result']}\n`;
418
406
  countPassed += 1;
419
407
  } else {
420
- verboseFailed += `Test result : Failed : ${time} : ${category}: ${testName} : ${result}\n`;
408
+ verboseFailed += `${row['category']}: ${row['name']} (${row['ms']} ms) : ${row['result']}\n`;
421
409
  countFailed += 1;
422
410
  failed = true;
411
+ if (row['category'] != 'Unhandled exceptions' && row['name'] != 'before' && row['name'] != 'after') lastFailedTest = {
412
+ category: row['category'],
413
+ test: row['name']
414
+ };
423
415
  }
424
- if (success !== true && skipped !== true && stopOnFail) {
425
- break;
426
- }
427
- }
428
- if (window.DG.Test.isInDebug) {
429
- console.log('on browser closing debug point');
430
- debugger;
431
- }
432
- res = '';
433
- console.log(`DEBUG: runTests: AFTER THE LOOP: rows in resultDF ${resultDF?.rowCount}`);
434
- if (resultDF) {
435
- const bs = window.DG.BitSet.create(resultDF.rowCount);
436
- bs.setAll(true);
437
- for (let i = 0; i < resultDF.rowCount; i++) {
438
- if (resultDF.rows.get(i).get('category') === 'Unhandled exceptions') bs.set(i, false);
439
- }
440
- resultDF = resultDF.clone(bs);
441
- console.log(`DEBUG: runTests: IN IF CONDITION: rows in resultDF ${resultDF.rowCount}`);
442
- res = resultDF.toCsv();
443
- console.log(`DEBUG: runTests: IN IF CONDITION: csv length ${res?.length}`);
444
416
  }
417
+ return {
418
+ verbosePassed: verbosePassed,
419
+ verboseSkipped: verboseSkipped,
420
+ verboseFailed: verboseFailed,
421
+ passedAmount: countPassed,
422
+ skippedAmount: countSkipped,
423
+ failedAmount: countFailed,
424
+ csv: df.toCsv(),
425
+ failed: failed,
426
+ lastFailedTest: lastFailedTest,
427
+ retrySupported: retrySupported
428
+ // df: resultDF?.toJson()
429
+ };
445
430
  } catch (e) {
446
- failed = true;
447
431
  console.log(`DEBUG: runTests: IN CATCH: ERROR: ${e}`);
448
- error = lastTest ? `category: ${lastTest.params.category}, name: ${lastTest.params.test}, error: ${e}, ${await window.DG.Logger.translateStackTrace(e.stack)}` : `test: null, error: ${e}, ${await window.DG.Logger.translateStackTrace(e.stack)}`;
432
+ return {
433
+ failed: true,
434
+ retrySupported: false,
435
+ error: `${e}, ${await window.DG.Logger.translateStackTrace(e.stack)}`
436
+ };
449
437
  }
450
- console.log(`DEBUG: runTests: BEFORE RETURN: RES: ${res.length}, FAILED: ${failed}, ERROR: ${error}`);
451
- return {
452
- failed: failed,
453
- verbosePassed: verbosePassed,
454
- verboseSkipped: verboseSkipped,
455
- verboseFailed: verboseFailed,
456
- passedAmount: countPassed,
457
- skippedAmount: countSkipped,
458
- failedAmount: countFailed,
459
- error: error,
460
- csv: res
461
- // df: resultDF?.toJson()
462
- };
438
+
439
+ /*
440
+ for (const testParam of testsParams) {
441
+ lastTest = testParam;
442
+ console.log(testParam);
443
+ const df: DG.DataFrame = await (<any>window).grok.functions.call(testParam.package + ':test', testParam.params);
444
+ if (!df.getCol('flaking')) {
445
+ const flakingCol = (<any>window).DG.Column.fromType((<any>window).DG.COLUMN_TYPE.BOOL, 'flaking', df.rowCount);
446
+ df.columns.add(flakingCol);
447
+ }
448
+ if (!df.getCol('package')) {
449
+ const packageNameCol =
450
+ (<any>window).DG.Column.fromList((<any>window).DG.COLUMN_TYPE.STRING, 'package', Array(df.rowCount).fill(testParam.package));
451
+ df.columns.add(packageNameCol);
452
+ }
453
+ df.columns
454
+ .setOrder([
455
+ 'date', 'category', 'name', 'success', 'result', 'ms', 'skipped', 'logs', 'owner', 'package', 'widgetsDifference', 'flaking']);
456
+ // addColumn('flaking', (<any>window).DG.Column.fromType((<any>window).DG.COLUMN_TYPE.BOOL, 'flaking', df.rowCount), df);
457
+ // addColumn('package', (<any>window).DG.Column.fromType((<any>window).DG.COLUMN_TYPE.BOOL, 'flaking', df.rowCount), df);
458
+ // if (!df.getCol('flaking')) {
459
+ // const flakingCol = (<any>window).DG.Column.fromType((<any>window).DG.COLUMN_TYPE.BOOL, 'flaking', df.rowCount);
460
+ // df.columns.add(flakingCol);
461
+ // }
462
+ // if (!df.getCol('package')) {
463
+ // const packageNameCol =
464
+ // (<any>window).DG.Column.fromList((<any>window).DG.COLUMN_TYPE.STRING, 'package', Array(df.rowCount).fill(testParam.package));
465
+ // df.columns.add(packageNameCol);
466
+ // }
467
+ if (df.rowCount === 0) {
468
+ verboseFailed += `Test result : Invocation Fail : ${testParam.params.category}: ${testParam.params.test}\n`;
469
+ countFailed += 1;
470
+ failed = true;
471
+ continue;
472
+ }
473
+ let row = df.rows.get(0);
474
+ console.log(`DEBUG: runTests: IN A LOOP: rows in df ${df.rowCount}`);
475
+ if (df.rowCount > 1) {
476
+ const unhandledErrorRow = df.rows.get(1);
477
+ if (!unhandledErrorRow.get('success')) {
478
+ unhandledErrorRow['category'] = row.get('category');
479
+ unhandledErrorRow['name'] = row.get('name');
480
+ row = unhandledErrorRow;
481
+ }
482
+ }
483
+ const category = row.get('category');
484
+ const testName = row.get('name');
485
+ const time = row.get('ms');
486
+ const result = row.get('result');
487
+ const success = row.get('success');
488
+ const skipped = row.get('skipped');
489
+ row['flaking'] = success && (<any>window).DG.Test.isReproducing;
490
+ df.changeColumnType('result', (<any>window).DG.COLUMN_TYPE.STRING);
491
+ df.changeColumnType('logs', (<any>window).DG.COLUMN_TYPE.STRING);
492
+ if (df.col('widgetsDifference'))
493
+ df.changeColumnType('widgetsDifference', (<any>window).DG.COLUMN_TYPE.STRING);
494
+ // df.changeColumnType('memoryDelta', (<any>window).DG.COLUMN_TYPE.BIG_INT);
495
+ if (resultDF === undefined)
496
+ resultDF = df;
497
+ else {
498
+ console.log(`DEBUG: COLUMN NAMES IN RESULT_DF: ${resultDF.columns.names()}`);
499
+ console.log(`DEBUG: COLUMN TYPES IN RESULT_DF: ${resultDF.columns.names().map((c) => `${c}: ${resultDF.col(c)?.type}`)}`);
500
+ console.log(`DEBUG: COLUMN NAMES IN DF: ${df.columns.names()}`);
501
+ console.log(`DEBUG: COLUMN TYPES IN DF: ${df.columns.names().map((c) => `${c}: ${df.col(c)?.type}`)}`);
502
+ resultDF = resultDF.append(df);
503
+ }
504
+ if (row['skipped']) {
505
+ verboseSkipped += `Test result : Skipped : ${time} : ${category}: ${testName} : ${result}\n`;
506
+ countSkipped += 1;
507
+ } else if (row['success']) {
508
+ verbosePassed += `Test result : Success : ${time} : ${category}: ${testName} : ${result}\n`;
509
+ countPassed += 1;
510
+ } else {
511
+ verboseFailed += `Test result : Failed : ${time} : ${category}: ${testName} : ${result}\n`;
512
+ countFailed += 1;
513
+ failed = true;
514
+ }
515
+ if ((success !== true && skipped !== true) && stopOnFail) {
516
+ break;
517
+ }
518
+ }
519
+ if ((<any>window).DG.Test.isInDebug) {
520
+ console.log('on browser closing debug point');
521
+ debugger;
522
+ }
523
+ res = '';
524
+ console.log(`DEBUG: runTests: AFTER THE LOOP: rows in resultDF ${resultDF?.rowCount}`);
525
+ if (resultDF) {
526
+ const bs = (<any>window).DG.BitSet.create(resultDF.rowCount);
527
+ bs.setAll(true);
528
+ for (let i = 0; i < resultDF.rowCount; i++) {
529
+ if (resultDF.rows.get(i).get('category') === 'Unhandled exceptions')
530
+ bs.set(i, false);
531
+ }
532
+ resultDF = resultDF.clone(bs);
533
+ console.log(`DEBUG: runTests: IN IF CONDITION: rows in resultDF ${resultDF.rowCount}`);
534
+ res = resultDF.toCsv();
535
+ console.log(`DEBUG: runTests: IN IF CONDITION: csv length ${res?.length}`);
536
+ }
537
+ } catch (e) {
538
+ failed = true;
539
+ console.log(`DEBUG: runTests: IN CATCH: ERROR: ${e}`);
540
+ error = lastTest ?
541
+ `category: ${
542
+ lastTest.params.category}, name: ${
543
+ lastTest.params.test}, error: ${e}, ${await (<any>window).DG.Logger.translateStackTrace((e as any).stack)}` :
544
+ `test: null, error: ${e}, ${await (<any>window).DG.Logger.translateStackTrace((e as any).stack)}`;
545
+ }
546
+ console.log(`DEBUG: runTests: BEFORE RETURN: RES: ${res.length}, FAILED: ${failed}, ERROR: ${error}`);
547
+ return {
548
+ failed: failed,
549
+ verbosePassed: verbosePassed,
550
+ verboseSkipped: verboseSkipped,
551
+ verboseFailed: verboseFailed,
552
+ passedAmount: countPassed,
553
+ skippedAmount: countSkipped,
554
+ failedAmount: countFailed,
555
+ error: error,
556
+ csv: res,
557
+ // df: resultDF?.toJson()
558
+ };*/
463
559
  }
464
- async function runBrowser(testExecutionData, browserOptions, browsersId, testInvocationTimeout = 3600000) {
560
+ async function runBrowser(testExecutionData, browserOptions, browsersId, testInvocationTimeout = 3600000, existingBrowserSession) {
465
561
  const testsToRun = {
466
562
  func: runTests.toString(),
467
- tests: testExecutionData
563
+ testParams: testExecutionData
468
564
  };
469
565
  return await timeout(async () => {
470
- const params = Object.assign({
471
- devtools: browserOptions.debug
472
- }, defaultLaunchParameters);
473
- if (browserOptions.gui) params['headless'] = false;
474
- const out = await getBrowserPage(_puppeteer.default, params);
475
- const browser = out.browser;
476
- const page = out.page;
566
+ let browser;
567
+ let page;
568
+ let webUrl;
569
+ if (existingBrowserSession) {
570
+ // Reuse existing browser - just refresh the page
571
+ browser = existingBrowserSession.browser;
572
+ page = existingBrowserSession.page;
573
+ webUrl = existingBrowserSession.webUrl;
574
+
575
+ // Remove old console listeners before refresh to avoid stale state references
576
+ page.removeAllListeners('console');
577
+ await page.goto(webUrl);
578
+ try {
579
+ await page.waitForFunction(() => document.querySelector('.grok-preloader') == null, {
580
+ timeout: 3600000
581
+ });
582
+ } catch (error) {
583
+ throw error;
584
+ }
585
+ } else {
586
+ // Create new browser
587
+ const params = Object.assign({
588
+ devtools: browserOptions.debug
589
+ }, defaultLaunchParameters);
590
+ if (browserOptions.gui) params['headless'] = false;
591
+ const out = await getBrowserPage(_puppeteer.default, params);
592
+ browser = out.browser;
593
+ page = out.page;
594
+ webUrl = await getWebUrlFromPage(page);
595
+ }
477
596
  const recorder = new _puppeteerScreenRecorder.PuppeteerScreenRecorder(page, recorderConfig);
478
597
  const currentBrowserNum = browsersId;
479
598
  const logsDir = `./test-console-output-${currentBrowserNum}.log`;
480
599
  const recordDir = `./test-record-${currentBrowserNum}.mp4`;
481
- if (browserOptions.record) {
600
+ if (browserOptions.record && !existingBrowserSession) {
601
+ // Only set up recording on initial browser creation, not on retry
482
602
  await recorder.start(recordDir);
483
603
  await page.exposeFunction('addLogsToFile', addLogsToFile);
484
604
  _fs.default.writeFileSync(logsDir, ``);
@@ -492,6 +612,130 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
492
612
  addLogsToFile(logsDir, `CONSOLE LOG REQUEST: ${response.status()}, ${response.url()}\n`);
493
613
  });
494
614
  }
615
+
616
+ // State tracking for test output formatting
617
+ const categoryResults = new Map();
618
+ let currentCategory = null;
619
+ let currentTestName = null;
620
+ // Store failed tests with their errors to print after category header
621
+ let pendingFailures = [];
622
+ let pendingBeforeFailure = null;
623
+ let pendingAfterFailure = null;
624
+ let modernOutput = false;
625
+ const printCategorySummary = category => {
626
+ const results = categoryResults.get(category);
627
+ if (!results) return;
628
+ const formattedCategory = category.replace(/: /g, ', ');
629
+ const passedCount = results.passed;
630
+ if (results.failed > 0 || pendingBeforeFailure || pendingAfterFailure) {
631
+ console.log(`\x1b[31m❌ ${formattedCategory} (${passedCount} passed)\x1b[0m`);
632
+ // Print before() failure first
633
+ if (pendingBeforeFailure) {
634
+ console.log(` \x1b[31m❌ before\x1b[0m`);
635
+ if (pendingBeforeFailure.error) console.log(` \x1b[31m${pendingBeforeFailure.error}\x1b[0m`);
636
+ }
637
+ // Print after() failure
638
+ if (pendingAfterFailure) {
639
+ console.log(` \x1b[31m❌ after\x1b[0m`);
640
+ if (pendingAfterFailure.error) console.log(` \x1b[31m${pendingAfterFailure.error}\x1b[0m`);
641
+ }
642
+ // Print test failures
643
+ for (const failure of pendingFailures) {
644
+ console.log(` \x1b[31m❌ ${failure.testName}\x1b[0m`);
645
+ if (failure.error) console.log(` \x1b[31m${failure.error}\x1b[0m`);
646
+ }
647
+ } else {
648
+ console.log(`\x1b[32m✅ ${formattedCategory} (${passedCount} passed)\x1b[0m`);
649
+ }
650
+ pendingFailures = [];
651
+ pendingBeforeFailure = null;
652
+ pendingAfterFailure = null;
653
+ };
654
+
655
+ // Subscribe to page console events for modern output formatting
656
+ // On retry, old listeners were removed so we need to re-attach
657
+ page.on('console', msg => {
658
+ const text = msg.text();
659
+ if (!text.startsWith('Package testing: ')) return;
660
+ modernOutput = true;
661
+ // Extract tokens in {{...}} format
662
+ const tokens = [];
663
+ const tokenRegex = /\{\{([^}]+)\}\}/g;
664
+ let match;
665
+ while ((match = tokenRegex.exec(text)) !== null) tokens.push(match[1]);
666
+
667
+ // Category start: "Package testing: Started {{Category}}"
668
+ if (text.includes('Started') && tokens.length === 1) {
669
+ // Print summary of previous category if exists
670
+ if (currentCategory && categoryResults.has(currentCategory)) printCategorySummary(currentCategory);
671
+ currentCategory = tokens[0];
672
+ categoryResults.set(currentCategory, {
673
+ passed: 0,
674
+ failed: 0
675
+ });
676
+ pendingFailures = [];
677
+ pendingBeforeFailure = null;
678
+ pendingAfterFailure = null;
679
+ } else if (text.includes('Started') && tokens.length === 2) {
680
+ // Test start: "Package testing: Started {{Category}} {{TestName}}"
681
+ const category = tokens[0].replace(/: /g, ', ');
682
+ currentTestName = tokens[1];
683
+ process.stdout.write(`${category}: ${currentTestName}...`);
684
+ } else if (text.includes('Finished') && tokens.length === 3) {
685
+ // Test finish: "Package testing: Finished {{Category}} {{TestName}} with {{success/error}} for X ms"
686
+ const category = tokens[0];
687
+ const testName = tokens[1];
688
+ const status = tokens[2];
689
+ const results = categoryResults.get(category);
690
+
691
+ // Clear the current test line
692
+ process.stdout.write('\r\x1b[K');
693
+ if (status === 'success') {
694
+ if (results) results.passed++;
695
+ } else {
696
+ if (results) results.failed++;
697
+ pendingFailures.push({
698
+ testName,
699
+ error: null
700
+ });
701
+ }
702
+ currentTestName = null;
703
+ } else if (text.includes('Result for') && tokens.length === 2) {
704
+ // Error result: "Package testing: Result for {{Category}} {{TestName}}: error message"
705
+ const errorMsg = text.split(': ').slice(-1)[0];
706
+ // Attach error to the last pending failure
707
+ if (pendingFailures.length > 0) pendingFailures[pendingFailures.length - 1].error = errorMsg;
708
+ } else if (text.includes('Category before()') && text.includes('failed') && tokens.length >= 1) {
709
+ // Category before() failed: "Package testing: Category before() {{Category}} failed"
710
+ process.stdout.write('\r\x1b[K');
711
+ pendingBeforeFailure = {
712
+ error: null
713
+ };
714
+ } else if (text.includes('Result for') && text.includes('before:') && tokens.length >= 1) {
715
+ // Before error result: "Package testing: Result for {{Category}} before: error message"
716
+ const errorMsg = text.split('before: ').slice(-1)[0];
717
+ if (pendingBeforeFailure) pendingBeforeFailure.error = errorMsg;
718
+ } else if (text.includes('Category after()') && text.includes('failed') && tokens.length >= 1) {
719
+ // Category after() failed: "Package testing: Category after() {{Category}} failed"
720
+ process.stdout.write('\r\x1b[K');
721
+ pendingAfterFailure = {
722
+ error: null
723
+ };
724
+ } else if (text.includes('Result for') && text.includes('after:') && tokens.length >= 1) {
725
+ // After error result: "Package testing: Result for {{Category}} after: error message"
726
+ const errorMsg = text.split('after: ').slice(-1)[0];
727
+ if (pendingAfterFailure) pendingAfterFailure.error = errorMsg;
728
+ } else if (text.includes('Unhandled Exception')) {
729
+ // Unhandled exception: "Package testing: Unhandled Exception: ..."
730
+ // Clear any pending test line and move to new line
731
+ process.stdout.write('\r\x1b[K\n');
732
+ const errorMsg = text.replace('Package testing: Unhandled Exception: ', '');
733
+ if (errorMsg && errorMsg !== 'null') console.log(`\x1b[31m❌ Unhandled Exception: ${errorMsg}\x1b[0m`);
734
+ }
735
+ });
736
+ const printFinalCategorySummary = () => {
737
+ if (currentCategory && categoryResults.has(currentCategory)) printCategorySummary(currentCategory);
738
+ };
495
739
  const testingResults = await page.evaluate((testData, options) => {
496
740
  if (options.benchmark) window.DG.Test.isInBenchmark = true;
497
741
  if (options.reproduce) window.DG.Test.isReproducing = true;
@@ -499,7 +743,7 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
499
743
  if (options.debug) window.DG.Test.isInDebug = true;
500
744
  return new Promise((resolve, reject) => {
501
745
  window.runTests = eval('(' + testData.func + ')');
502
- window.runTests(testData.tests, options.stopOnTimeout).then(results => {
746
+ window.runTests(testData.testParams, options.stopOnTimeout).then(results => {
503
747
  resolve(results);
504
748
  }).catch(e => {
505
749
  resolve({
@@ -535,9 +779,29 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
535
779
  // });
536
780
  });
537
781
  }, testsToRun, browserOptions);
538
- if (browserOptions.record) await recorder.stop();
539
- if (browser != null) await browser.close();
540
- return testingResults;
782
+
783
+ // Print the final category summary
784
+ printFinalCategorySummary();
785
+ if (browserOptions.record && !existingBrowserSession) await recorder.stop();
786
+ if (modernOutput) {
787
+ testingResults.verbosePassed = '';
788
+ testingResults.verboseSkipped = '';
789
+ testingResults.verboseFailed = '';
790
+ }
791
+
792
+ // Only close browser if not keeping it open for retry
793
+ if (!browserOptions.keepBrowserOpen && browser != null) await browser.close();
794
+
795
+ // Return browser session for potential reuse
796
+ return {
797
+ ...testingResults,
798
+ browserSession: browserOptions.keepBrowserOpen ? {
799
+ browser,
800
+ page,
801
+ webUrl
802
+ } : undefined,
803
+ modernOutput
804
+ };
541
805
  }, testInvocationTimeout);
542
806
  }
543
807
  async function mergeBrowsersResults(browsersResults) {
@@ -550,7 +814,8 @@ async function mergeBrowsersResults(browsersResults) {
550
814
  skippedAmount: browsersResults[0].skippedAmount,
551
815
  failedAmount: browsersResults[0].failedAmount,
552
816
  csv: browsersResults[0].csv,
553
- error: ''
817
+ error: '',
818
+ modernOutput: browsersResults.some(r => r.modernOutput)
554
819
  };
555
820
  for (const browsersResult of browsersResults) {
556
821
  if (mergedResult.csv === browsersResult.csv) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "4.14.72",
3
+ "version": "5.0.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": {
@@ -13,7 +13,6 @@
13
13
  "archiver-promise": "^1.0.0",
14
14
  "datagrok-api": "^1.26.0",
15
15
  "estraverse": "^5.3.0",
16
- "fs": "^0.0.1-security",
17
16
  "glob": "^11.0.2",
18
17
  "ignore-walk": "^3.0.4",
19
18
  "inquirer": "^7.3.3",
@@ -25,7 +24,8 @@
25
24
  "papaparse": "^5.4.1",
26
25
  "path": "^0.12.7",
27
26
  "puppeteer": "22.10.0",
28
- "puppeteer-screen-recorder": "3.0.3"
27
+ "puppeteer-screen-recorder": "3.0.3",
28
+ "ts-morph": "^27.0.2"
29
29
  },
30
30
  "scripts": {
31
31
  "link": "npm link",
@@ -75,7 +75,6 @@
75
75
  "@typescript-eslint/parser": "^5.62.0",
76
76
  "eslint": "^8.56.0",
77
77
  "eslint-config-google": "^0.14.0",
78
- "ts-morph": "^27.0.2",
79
78
  "typescript": "^5.3.3",
80
79
  "webpack": "^5.89.0",
81
80
  "webpack-cli": "^5.1.4"