@memberjunction/react-test-harness 2.96.0 → 2.98.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.
@@ -45,7 +45,6 @@ class ComponentRunner {
45
45
  const hasErrors = lintResult.violations.some(v => v.severity === 'critical' || v.severity === 'high');
46
46
  return {
47
47
  violations: lintResult.violations,
48
- suggestions: lintResult.suggestions,
49
48
  hasErrors
50
49
  };
51
50
  }
@@ -53,7 +52,6 @@ class ComponentRunner {
53
52
  const startTime = Date.now();
54
53
  const errors = [];
55
54
  const warnings = [];
56
- const criticalWarnings = [];
57
55
  const consoleLogs = [];
58
56
  const dataErrors = []; // Track data access errors from RunView/RunQuery
59
57
  let renderCount = 0;
@@ -121,9 +119,9 @@ class ComponentRunner {
121
119
  throw new Error('Failed to inject MJReactRuntime into page context');
122
120
  }
123
121
  // Set up error tracking
124
- await this.setupErrorTracking(page);
122
+ await this.setupErrorTracking(page, options.componentSpec, allLibraries);
125
123
  // Set up console logging
126
- this.setupConsoleLogging(page, consoleLogs, warnings, criticalWarnings);
124
+ this.setupConsoleLogging(page, consoleLogs, warnings);
127
125
  // Expose MJ utilities to the page
128
126
  await this.exposeMJUtilities(page, options, dataErrors, debug);
129
127
  if (debug) {
@@ -131,22 +129,25 @@ class ComponentRunner {
131
129
  console.log(' - spec.name:', options.componentSpec.name);
132
130
  console.log(' - spec.code length:', options.componentSpec.code?.length || 0);
133
131
  console.log(' - props:', JSON.stringify(options.props || {}, null, 2));
134
- console.log(' - componentLibraries count:', allLibraries?.length || 0);
135
- if (allLibraries && allLibraries.length > 0) {
136
- console.log(' - First few libraries:', allLibraries.slice(0, 3).map(lib => ({
137
- Name: lib.Name,
138
- GlobalVariable: lib.GlobalVariable
132
+ // Show spec-specific libraries, not all available libraries
133
+ if (options.componentSpec.libraries && options.componentSpec.libraries.length > 0) {
134
+ console.log(' - spec requires libraries:', options.componentSpec.libraries.map(lib => ({
135
+ name: lib.name,
136
+ globalVariable: lib.globalVariable,
137
+ version: lib.version
139
138
  })));
140
139
  }
140
+ else {
141
+ console.log(' - spec requires libraries: none');
142
+ }
143
+ // Total available libraries in metadata (for context only)
144
+ console.log(' - total available libraries in metadata:', allLibraries?.length || 0);
141
145
  }
142
146
  // Execute the component using the real React runtime with timeout (Recommendation #1)
143
147
  const executionPromise = page.evaluate(async ({ spec, props, debug, componentLibraries }) => {
144
148
  if (debug) {
145
149
  console.log('šŸŽÆ Starting component execution');
146
- console.log('šŸ“š BROWSER: Received componentLibraries:', componentLibraries?.length || 0);
147
- if (componentLibraries?.length > 0) {
148
- console.log(' First library:', componentLibraries[0]);
149
- }
150
+ console.log('šŸ“š BROWSER: Component libraries available for loading:', componentLibraries?.length || 0);
150
151
  }
151
152
  // Declare renderCheckInterval at the top scope for cleanup
152
153
  let renderCheckInterval;
@@ -215,8 +216,15 @@ class ComponentRunner {
215
216
  // The spec might not have globalVariable, but we need it for the runtime to work
216
217
  if (spec.libraries && componentLibraries) {
217
218
  for (const specLib of spec.libraries) {
219
+ // Skip if library entry is invalid
220
+ if (!specLib || !specLib.name) {
221
+ if (debug) {
222
+ console.warn(' āš ļø Skipping invalid library entry (missing name):', specLib);
223
+ }
224
+ continue;
225
+ }
218
226
  if (!specLib.globalVariable) {
219
- const libDef = componentLibraries.find(l => l.Name.toLowerCase() === specLib.name.toLowerCase());
227
+ const libDef = componentLibraries.find(l => l && l.Name && l.Name.toLowerCase() === specLib.name.toLowerCase());
220
228
  if (libDef && libDef.GlobalVariable) {
221
229
  specLib.globalVariable = libDef.GlobalVariable;
222
230
  if (debug) {
@@ -257,10 +265,17 @@ class ComponentRunner {
257
265
  message: `Component registration failed: ${registrationError.message || registrationError}`,
258
266
  stack: registrationError.stack,
259
267
  type: 'registration-error',
260
- phase: 'component-compilation'
268
+ phase: 'component-compilation',
269
+ source: 'runtime-wrapper'
261
270
  });
262
271
  window.__testHarnessTestFailed = true;
263
- throw registrationError; // Re-throw for outer handler
272
+ // Don't re-throw - let execution continue to collect this error properly
273
+ // Return a failure result so the Promise resolves properly
274
+ return {
275
+ success: false,
276
+ error: `Component registration failed: ${registrationError.message || registrationError}`,
277
+ componentCount: 0
278
+ };
264
279
  }
265
280
  if (debug && !registrationResult.success) {
266
281
  console.log('āŒ Registration failed:', registrationResult.errors);
@@ -319,7 +334,8 @@ class ComponentRunner {
319
334
  window.__testHarnessRuntimeErrors = window.__testHarnessRuntimeErrors || [];
320
335
  window.__testHarnessRuntimeErrors.push({
321
336
  message: `Likely infinite render loop: ${currentRenderCount} createElement calls (max: ${MAX_RENDERS_ALLOWED})`,
322
- type: 'render-loop'
337
+ type: 'render-loop',
338
+ source: 'test-harness'
323
339
  });
324
340
  // Try to unmount to stop the madness
325
341
  try {
@@ -369,7 +385,8 @@ class ComponentRunner {
369
385
  message: enhancedMessage,
370
386
  stack: error.stack,
371
387
  type: 'react-render-error',
372
- phase: 'component-render'
388
+ phase: 'component-render',
389
+ source: 'user-component' // This is the actual error from user's component
373
390
  });
374
391
  window.__testHarnessTestFailed = true;
375
392
  return { hasError: true, error };
@@ -421,7 +438,8 @@ class ComponentRunner {
421
438
  window.__testHarnessRuntimeErrors.push({
422
439
  message: error.message || String(error),
423
440
  stack: error.stack,
424
- type: 'execution-error'
441
+ type: 'execution-error',
442
+ source: 'runtime-wrapper'
425
443
  });
426
444
  window.__testHarnessTestFailed = true;
427
445
  return {
@@ -439,17 +457,34 @@ class ComponentRunner {
439
457
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Component execution timeout after ${globalTimeout}ms`)), globalTimeout));
440
458
  // Race between execution and timeout
441
459
  let executionResult;
460
+ let hasTimeout = false;
442
461
  try {
443
462
  executionResult = await Promise.race([executionPromise, timeoutPromise]);
444
463
  }
445
464
  catch (timeoutError) {
446
465
  // Handle timeout gracefully
466
+ hasTimeout = true;
447
467
  errors.push(`Component execution timed out after ${globalTimeout}ms`);
448
468
  executionResult = {
449
469
  success: false,
450
470
  error: timeoutError instanceof Error ? timeoutError.message : 'Execution timeout'
451
471
  };
452
472
  }
473
+ // Ensure executionResult has proper shape
474
+ if (!executionResult) {
475
+ executionResult = {
476
+ success: false,
477
+ error: 'Component execution returned no result'
478
+ };
479
+ errors.push('Component execution failed to return a result');
480
+ }
481
+ else if (!executionResult.success && executionResult.error) {
482
+ // Add the execution error if it hasn't been captured elsewhere
483
+ const errorMsg = `Component execution failed: ${executionResult.error}`;
484
+ if (!errors.some(e => e.includes(executionResult.error))) {
485
+ errors.push(errorMsg);
486
+ }
487
+ }
453
488
  if (debug) {
454
489
  console.log('Execution result:', executionResult);
455
490
  }
@@ -458,9 +493,9 @@ class ComponentRunner {
458
493
  await page.waitForTimeout(renderWaitTime);
459
494
  // Get render count
460
495
  renderCount = await page.evaluate(() => window.__testHarnessRenderCount || 0);
461
- // Collect all errors
462
- const runtimeErrors = await this.collectRuntimeErrors(page);
463
- errors.push(...runtimeErrors);
496
+ // Collect all errors with source information
497
+ const runtimeErrorsWithSource = await this.collectRuntimeErrors(page);
498
+ errors.push(...runtimeErrorsWithSource.map(e => e.message)); // Extract messages for backward compat
464
499
  // Collect warnings (separate from errors)
465
500
  const collectedWarnings = await this.collectWarnings(page);
466
501
  warnings.push(...collectedWarnings);
@@ -470,8 +505,9 @@ class ComponentRunner {
470
505
  const asyncErrors = await this.collectRuntimeErrors(page);
471
506
  // Only add new errors
472
507
  asyncErrors.forEach(err => {
473
- if (!errors.includes(err)) {
474
- errors.push(err);
508
+ if (!errors.includes(err.message)) {
509
+ errors.push(err.message);
510
+ runtimeErrorsWithSource.push(err); // Keep the structured version too
475
511
  }
476
512
  });
477
513
  // Also check for new warnings
@@ -485,34 +521,93 @@ class ComponentRunner {
485
521
  const html = await page.content();
486
522
  // Take screenshot
487
523
  const screenshot = await page.screenshot();
524
+ // Check for excessive render count first
525
+ const hasRenderLoop = renderCount > ComponentRunner.MAX_RENDER_COUNT;
526
+ if (hasRenderLoop) {
527
+ errors.push(`Possible render loop: ${renderCount} createElement calls detected (likely infinite loop)`);
528
+ }
488
529
  // Determine success
489
530
  const success = errors.length === 0 &&
490
- criticalWarnings.length === 0 &&
491
- renderCount <= ComponentRunner.MAX_RENDER_COUNT &&
531
+ !hasRenderLoop &&
532
+ !hasTimeout &&
492
533
  executionResult.success;
493
- if (renderCount > ComponentRunner.MAX_RENDER_COUNT) {
494
- errors.push(`Possible render loop: ${renderCount} createElement calls detected (likely infinite loop)`);
495
- }
496
534
  // Combine runtime errors with data errors
497
535
  const allErrors = [...errors, ...dataErrors];
498
- const result = {
499
- success: success && dataErrors.length === 0, // Fail if we have data errors
500
- html,
501
- errors: allErrors.map(e => ({
536
+ // Map runtime errors with source info and specific rules
537
+ const errorViolations = runtimeErrorsWithSource.map(e => ({
538
+ message: e.message,
539
+ severity: 'critical',
540
+ rule: e.rule || 'runtime-error', // Use specific rule from collectRuntimeErrors
541
+ line: 0,
542
+ column: 0,
543
+ source: e.source
544
+ }));
545
+ // Add timeout error if detected
546
+ if (hasTimeout) {
547
+ errorViolations.push({
548
+ message: `Component execution timed out after ${globalTimeout}ms`,
549
+ severity: 'critical',
550
+ rule: 'timeout',
551
+ line: 0,
552
+ column: 0,
553
+ source: 'test-harness' // This is a test harness timeout
554
+ });
555
+ }
556
+ // Add render loop error if detected
557
+ if (hasRenderLoop) {
558
+ errorViolations.push({
559
+ message: `Possible render loop: ${renderCount} createElement calls detected (likely infinite loop)`,
560
+ severity: 'critical',
561
+ rule: 'render-loop',
562
+ line: 0,
563
+ column: 0,
564
+ source: 'test-harness' // This is a test harness detection
565
+ });
566
+ }
567
+ // Add data errors without source
568
+ dataErrors.forEach(e => {
569
+ errorViolations.push({
502
570
  message: e,
503
571
  severity: 'critical',
504
572
  rule: 'runtime-error',
505
573
  line: 0,
506
- column: 0
507
- })),
508
- warnings: warnings.map(w => ({
574
+ column: 0,
575
+ source: 'user-component' // Data errors are from user's data access code
576
+ });
577
+ });
578
+ // Check warnings for critical patterns and move them to errors
579
+ const criticalWarningViolations = [];
580
+ const regularWarnings = [];
581
+ warnings.forEach(w => {
582
+ if (ComponentRunner.CRITICAL_WARNING_PATTERNS.some(pattern => pattern.test(w))) {
583
+ // This is a critical warning - add to errors
584
+ criticalWarningViolations.push({
585
+ message: w,
586
+ severity: 'critical',
587
+ rule: 'critical-react-warning',
588
+ line: 0,
589
+ column: 0,
590
+ source: 'react-framework'
591
+ });
592
+ }
593
+ else {
594
+ // Regular warning
595
+ regularWarnings.push(w);
596
+ }
597
+ });
598
+ // Combine all error violations
599
+ const allErrorViolations = [...errorViolations, ...criticalWarningViolations];
600
+ const result = {
601
+ success: success && dataErrors.length === 0 && criticalWarningViolations.length === 0, // Fail on critical warnings too
602
+ html,
603
+ errors: allErrorViolations,
604
+ warnings: regularWarnings.map(w => ({
509
605
  message: w,
510
606
  severity: 'low',
511
607
  rule: 'warning',
512
608
  line: 0,
513
609
  column: 0
514
610
  })),
515
- criticalWarnings,
516
611
  console: consoleLogs,
517
612
  screenshot,
518
613
  executionTime: Date.now() - startTime,
@@ -524,19 +619,35 @@ class ComponentRunner {
524
619
  return result;
525
620
  }
526
621
  catch (error) {
527
- errors.push(error instanceof Error ? error.message : String(error));
528
- // Combine runtime errors with data errors
529
- const allErrors = [...errors, ...dataErrors];
530
- const result = {
531
- success: false,
532
- html: '',
533
- errors: allErrors.map(e => ({
622
+ // For catch block errors, we need to handle them specially
623
+ const catchError = {
624
+ message: error instanceof Error ? error.message : String(error),
625
+ source: 'test-harness' // Errors caught here are usually test harness issues
626
+ };
627
+ // Create error violations including the catch error
628
+ const errorViolations = [{
629
+ message: catchError.message,
630
+ severity: 'critical',
631
+ rule: 'runtime-error',
632
+ line: 0,
633
+ column: 0,
634
+ source: catchError.source
635
+ }];
636
+ // Add any data errors
637
+ dataErrors.forEach(e => {
638
+ errorViolations.push({
534
639
  message: e,
535
640
  severity: 'critical',
536
641
  rule: 'runtime-error',
537
642
  line: 0,
538
- column: 0
539
- })),
643
+ column: 0,
644
+ source: 'user-component'
645
+ });
646
+ });
647
+ const result = {
648
+ success: false,
649
+ html: '',
650
+ errors: errorViolations,
540
651
  warnings: warnings.map(w => ({
541
652
  message: w,
542
653
  severity: 'low',
@@ -544,7 +655,6 @@ class ComponentRunner {
544
655
  line: 0,
545
656
  column: 0
546
657
  })),
547
- criticalWarnings,
548
658
  console: consoleLogs,
549
659
  executionTime: Date.now() - startTime,
550
660
  renderCount
@@ -743,20 +853,91 @@ class ComponentRunner {
743
853
  /**
744
854
  * Set up error tracking in the page
745
855
  */
746
- async setupErrorTracking(page) {
747
- await page.evaluate(() => {
856
+ async setupErrorTracking(page, componentSpec, allLibraries) {
857
+ await page.evaluate(({ spec, availableLibraries }) => {
748
858
  // Initialize error tracking
749
859
  window.__testHarnessRuntimeErrors = [];
750
860
  window.__testHarnessConsoleErrors = [];
751
861
  window.__testHarnessConsoleWarnings = [];
752
862
  window.__testHarnessTestFailed = false;
753
863
  window.__testHarnessRenderCount = 0;
754
- // Track renders
864
+ // Track renders and detect invalid element types
755
865
  const originalCreateElement = window.React?.createElement;
756
866
  if (originalCreateElement) {
757
- window.React.createElement = function (...args) {
867
+ window.React.createElement = function (type, props, ...children) {
758
868
  window.__testHarnessRenderCount++;
759
- return originalCreateElement.apply(this, args);
869
+ // Enhanced error detection for invalid element types
870
+ if (type !== null && type !== undefined) {
871
+ const typeOf = typeof type;
872
+ // Check for the common "object instead of component" error
873
+ if (typeOf === 'object' && !window.React.isValidElement(type)) {
874
+ // Try to get a meaningful name for the object
875
+ let objectInfo = 'unknown object';
876
+ try {
877
+ if (type.constructor && type.constructor.name) {
878
+ objectInfo = type.constructor.name;
879
+ }
880
+ else if (type.name) {
881
+ objectInfo = type.name;
882
+ }
883
+ else {
884
+ // Try to show what properties it has
885
+ const keys = Object.keys(type).slice(0, 5);
886
+ if (keys.length > 0) {
887
+ objectInfo = `object with properties: ${keys.join(', ')}`;
888
+ }
889
+ }
890
+ }
891
+ catch (e) {
892
+ // Ignore errors in trying to get object info
893
+ }
894
+ // Generate helpful error message
895
+ const errorMsg = [
896
+ `Invalid JSX element type: React received an object (${objectInfo}) instead of a React component function.`,
897
+ '',
898
+ 'This often occurs when JSX elements or React.createElement receive an object instead of a valid component function.',
899
+ '',
900
+ 'Inspect all instances where you are using JSX elements that come from libraries or components to ensure they are properly referenced.',
901
+ '',
902
+ 'The exact fix depends on the specific library or component structure.'
903
+ ].join('\\n');
904
+ // Log to both console and error tracking
905
+ console.error('šŸ”“ Invalid JSX Element Type Detected:', errorMsg);
906
+ // Store the error for later collection
907
+ window.__testHarnessRuntimeErrors = window.__testHarnessRuntimeErrors || [];
908
+ window.__testHarnessRuntimeErrors.push({
909
+ message: errorMsg,
910
+ type: 'invalid-element-type',
911
+ phase: 'createElement',
912
+ source: 'enhanced-detection',
913
+ elementInfo: objectInfo
914
+ });
915
+ // Still try to call the original to get React's error too
916
+ // This will provide the component stack trace
917
+ }
918
+ }
919
+ else if (type === undefined) {
920
+ // Undefined component - likely a failed destructure or missing import
921
+ const errorMsg = [
922
+ 'Invalid JSX element type: component is undefined.',
923
+ '',
924
+ 'This occurs when a JSX element references a component that is undefined at runtime.',
925
+ '',
926
+ 'Inspect how this component is being accessed - it may not exist in the expected location or may have a different name.',
927
+ '',
928
+ 'Check that the component exists in your dependencies or libraries and is properly referenced.'
929
+ ].join('\\n');
930
+ console.error('šŸ”“ Undefined JSX Component:', errorMsg);
931
+ window.__testHarnessRuntimeErrors = window.__testHarnessRuntimeErrors || [];
932
+ window.__testHarnessRuntimeErrors.push({
933
+ message: errorMsg,
934
+ type: 'undefined-component',
935
+ phase: 'createElement',
936
+ source: 'enhanced-detection'
937
+ });
938
+ }
939
+ // Call original createElement
940
+ return originalCreateElement.call(this, type, props, ...children);
760
941
  };
761
942
  }
762
943
  // Override console.error
@@ -785,12 +966,91 @@ class ComponentRunner {
785
966
  }
786
967
  originalConsoleError.apply(console, args);
787
968
  };
969
+ // Helper function to analyze undefined identifiers
970
+ const analyzeUndefinedIdentifier = (identifier, spec, availableLibraries) => {
971
+ const result = {
972
+ isInSpecLibraries: false,
973
+ isInSpecDependencies: false,
974
+ isAvailableLibrary: false,
975
+ matchedLibrary: null,
976
+ specLibraries: spec?.libraries || [],
977
+ specDependencies: spec?.dependencies || []
978
+ };
979
+ // Check if it's in spec libraries
980
+ result.isInSpecLibraries = result.specLibraries.some((lib) => lib.globalVariable === identifier);
981
+ // Check if it's in spec dependencies
982
+ result.isInSpecDependencies = result.specDependencies.some((dep) => dep.name === identifier);
983
+ // Check against ALL available libraries (case-insensitive)
984
+ if (availableLibraries) {
985
+ const availableLib = availableLibraries.find((lib) => lib.GlobalVariable &&
986
+ lib.GlobalVariable.toLowerCase() === identifier.toLowerCase());
987
+ if (availableLib) {
988
+ result.isAvailableLibrary = true;
989
+ result.matchedLibrary = availableLib;
990
+ }
991
+ }
992
+ return result;
993
+ };
994
+ // Helper function to generate guidance message
995
+ const generateGuidance = (identifier, analysis) => {
996
+ // Case 1: Trying to use a library not in their spec
997
+ if (analysis.isAvailableLibrary && !analysis.isInSpecLibraries) {
998
+ const libList = analysis.specLibraries.length > 0
999
+ ? analysis.specLibraries.map((l) => l.globalVariable || l.name).filter(Boolean).join(', ')
1000
+ : 'no third-party libraries';
1001
+ return `${identifier} is not defined. It appears you're trying to use the ${analysis.matchedLibrary.Name} library. ` +
1002
+ `You do NOT have access to this library. ` +
1003
+ `Your architect gave you access to: ${libList}. ` +
1004
+ `You must work within these constraints and cannot load additional libraries.`;
1005
+ }
1006
+ // Case 2: Should be a component but not properly accessed
1007
+ if (analysis.isInSpecDependencies) {
1008
+ return `${identifier} is not defined. This component exists in your dependencies. ` +
1009
+ `Ensure you've destructured it: const { ${identifier} } = components; ` +
1010
+ `or accessed it as: components.${identifier}`;
1011
+ }
1012
+ // Case 3: Not a valid library or component
1013
+ const libList = analysis.specLibraries.length > 0
1014
+ ? `Available libraries: ${analysis.specLibraries.map((l) => l.globalVariable || l.name).filter(Boolean).join(', ')}`
1015
+ : 'No third-party libraries are available';
1016
+ const depList = analysis.specDependencies.length > 0
1017
+ ? `Available components: ${analysis.specDependencies.map((d) => d.name).join(', ')}`
1018
+ : 'No component dependencies are available';
1019
+ return `${identifier} is not defined. This is not a valid library or component in your specification. ` +
1020
+ `${libList}. ${depList}. ` +
1021
+ `You must only use the libraries and components specified in your component specification.`;
1022
+ };
788
1023
  // Global error handler
789
1024
  window.addEventListener('error', (event) => {
1025
+ // Check for "X is not defined" errors
1026
+ const notDefinedMatch = event.message?.match(/^(\w+) is not defined$/);
1027
+ if (notDefinedMatch) {
1028
+ const identifier = notDefinedMatch[1];
1029
+ // Analyze what this identifier might be
1030
+ const analysis = analyzeUndefinedIdentifier(identifier, spec, availableLibraries);
1031
+ // Generate specific guidance
1032
+ const guidance = generateGuidance(identifier, analysis);
1033
+ // Store enhanced error with specific guidance
1034
+ window.__testHarnessRuntimeErrors.push({
1035
+ message: guidance,
1036
+ stack: event.error?.stack,
1037
+ type: 'undefined-identifier',
1038
+ source: 'user-component',
1039
+ identifier: identifier
1040
+ });
1041
+ window.__testHarnessTestFailed = true;
1042
+ return;
1043
+ }
1044
+ // Handle other errors as before
1045
+ let source = 'user-component'; // Default to user component
1046
+ if (event.message && event.message.includes('Script error')) {
1047
+ source = 'runtime-wrapper';
1048
+ }
790
1049
  window.__testHarnessRuntimeErrors.push({
791
1050
  message: event.error?.message || event.message,
792
1051
  stack: event.error?.stack,
793
- type: 'runtime'
1052
+ type: 'runtime',
1053
+ source: source
794
1054
  });
795
1055
  window.__testHarnessTestFailed = true;
796
1056
  });
@@ -799,12 +1059,13 @@ class ComponentRunner {
799
1059
  window.__testHarnessRuntimeErrors.push({
800
1060
  message: 'Unhandled Promise Rejection: ' + (event.reason?.message || event.reason),
801
1061
  stack: event.reason?.stack,
802
- type: 'promise-rejection'
1062
+ type: 'promise-rejection',
1063
+ source: 'user-component' // Async errors are likely from user code
803
1064
  });
804
1065
  window.__testHarnessTestFailed = true;
805
1066
  event.preventDefault();
806
1067
  });
807
- });
1068
+ }, { spec: componentSpec, availableLibraries: allLibraries || [] });
808
1069
  }
809
1070
  /**
810
1071
  * Collect runtime errors from the page
@@ -817,25 +1078,99 @@ class ComponentRunner {
817
1078
  testFailed: window.__testHarnessTestFailed || false
818
1079
  };
819
1080
  });
820
- const errors = [];
821
- // Only add "test failed" message if there are actual errors
822
- if (errorData.testFailed && (errorData.runtimeErrors.length > 0 || errorData.consoleErrors.length > 0)) {
823
- errors.push('Test marked as failed by error handlers');
824
- }
1081
+ // Track unique errors and their counts
1082
+ const errorMap = new Map();
1083
+ // Check if we have any specific React render errors
1084
+ const hasSpecificReactError = errorData.runtimeErrors.some((error) => error.type === 'react-render-error' &&
1085
+ !error.message.includes('Script error'));
1086
+ // Process runtime errors with their source information
825
1087
  errorData.runtimeErrors.forEach((error) => {
826
- // Include phase information if available to help identify where the error occurred
827
- const phase = error.phase ? ` (during ${error.phase})` : '';
828
- const errorMsg = `${error.type} error: ${error.message}${phase}`;
829
- if (!errors.includes(errorMsg)) {
830
- errors.push(errorMsg);
1088
+ // Skip generic "Script error" messages if we have more specific React errors
1089
+ if (hasSpecificReactError &&
1090
+ error.type === 'runtime' &&
1091
+ error.message === 'Script error.') {
1092
+ return; // Skip this generic error
1093
+ }
1094
+ // Map error types to specific rule names
1095
+ let rule = 'runtime-error';
1096
+ switch (error.type) {
1097
+ case 'invalid-element-type':
1098
+ rule = 'invalid-jsx-element';
1099
+ break;
1100
+ case 'undefined-component':
1101
+ rule = 'undefined-jsx-component';
1102
+ break;
1103
+ case 'undefined-identifier':
1104
+ rule = 'undefined-identifier';
1105
+ break;
1106
+ case 'react-render-error':
1107
+ rule = 'react-render-error';
1108
+ break;
1109
+ case 'render-loop':
1110
+ rule = 'infinite-render-loop';
1111
+ break;
1112
+ case 'registration-error':
1113
+ rule = 'component-registration-error';
1114
+ break;
1115
+ case 'execution-error':
1116
+ rule = 'execution-error';
1117
+ break;
1118
+ case 'runtime':
1119
+ rule = 'runtime-error';
1120
+ break;
1121
+ case 'promise-rejection':
1122
+ rule = 'unhandled-promise-rejection';
1123
+ break;
1124
+ }
1125
+ // Create a key for deduplication based on message and type
1126
+ const key = `${error.type}:${error.message}`;
1127
+ if (errorMap.has(key)) {
1128
+ // Increment count for duplicate
1129
+ errorMap.get(key).count++;
1130
+ }
1131
+ else {
1132
+ // Add new error
1133
+ errorMap.set(key, {
1134
+ error: {
1135
+ message: error.message,
1136
+ source: error.source,
1137
+ type: error.type,
1138
+ rule: rule
1139
+ },
1140
+ count: 1
1141
+ });
831
1142
  }
832
1143
  });
1144
+ // Process console errors
833
1145
  errorData.consoleErrors.forEach((error) => {
834
- const errorMsg = `Console error: ${error}`;
835
- if (!errors.includes(errorMsg)) {
836
- errors.push(errorMsg);
1146
+ const key = `console-error:${error}`;
1147
+ if (errorMap.has(key)) {
1148
+ errorMap.get(key).count++;
1149
+ }
1150
+ else {
1151
+ errorMap.set(key, {
1152
+ error: {
1153
+ message: error,
1154
+ source: 'react-framework',
1155
+ type: 'console-error',
1156
+ rule: 'console-error'
1157
+ },
1158
+ count: 1
1159
+ });
837
1160
  }
838
1161
  });
1162
+ // Convert map to array with occurrence counts
1163
+ const errors = [];
1164
+ errorMap.forEach(({ error, count }) => {
1165
+ // Append count if > 1
1166
+ const message = count > 1
1167
+ ? `${error.message} (occurred ${count} times)`
1168
+ : error.message;
1169
+ errors.push({
1170
+ ...error,
1171
+ message
1172
+ });
1173
+ });
839
1174
  return errors;
840
1175
  }
841
1176
  /**
@@ -858,7 +1193,7 @@ class ComponentRunner {
858
1193
  /**
859
1194
  * Set up console logging
860
1195
  */
861
- setupConsoleLogging(page, consoleLogs, warnings, criticalWarnings) {
1196
+ setupConsoleLogging(page, consoleLogs, warnings) {
862
1197
  page.on('console', (msg) => {
863
1198
  const type = msg.type();
864
1199
  const text = msg.text();
@@ -869,10 +1204,6 @@ class ComponentRunner {
869
1204
  if (!warnings.includes(text)) {
870
1205
  warnings.push(text);
871
1206
  }
872
- // Check if it's a critical warning that should fail the test
873
- if (ComponentRunner.CRITICAL_WARNING_PATTERNS.some(pattern => pattern.test(text))) {
874
- criticalWarnings.push(text);
875
- }
876
1207
  }
877
1208
  });
878
1209
  page.on('pageerror', (error) => {
@@ -1333,12 +1664,6 @@ class ComponentRunner {
1333
1664
  console.log(` ${i + 1}. ${warn.message}`);
1334
1665
  });
1335
1666
  }
1336
- if (result.criticalWarnings && result.criticalWarnings.length > 0) {
1337
- console.log('\nšŸ”“ Critical Warnings:', result.criticalWarnings.length);
1338
- result.criticalWarnings.forEach((warn, i) => {
1339
- console.log(` ${i + 1}. ${warn}`);
1340
- });
1341
- }
1342
1667
  console.log('\n========================================\n');
1343
1668
  }
1344
1669
  }