@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/component-linter.d.ts +12 -7
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +386 -351
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts +1 -4
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +404 -79
- package/dist/lib/component-runner.js.map +1 -1
- package/dist/lib/library-lint-cache.d.ts +4 -0
- package/dist/lib/library-lint-cache.d.ts.map +1 -1
- package/dist/lib/library-lint-cache.js +61 -3
- package/dist/lib/library-lint-cache.js.map +1 -1
- package/dist/lib/test-broken-7.d.ts +2 -0
- package/dist/lib/test-broken-7.d.ts.map +1 -0
- package/dist/lib/test-broken-7.js +73 -0
- package/dist/lib/test-broken-7.js.map +1 -0
- package/dist/lib/test-harness.d.ts.map +1 -1
- package/dist/lib/test-harness.js +6 -4
- package/dist/lib/test-harness.js.map +1 -1
- package/package.json +3 -3
|
@@ -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
|
|
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
|
-
|
|
135
|
-
if (
|
|
136
|
-
console.log(' -
|
|
137
|
-
|
|
138
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
463
|
-
errors.push(...
|
|
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
|
-
|
|
491
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
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
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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 (...
|
|
867
|
+
window.React.createElement = function (type, props, ...children) {
|
|
758
868
|
window.__testHarnessRenderCount++;
|
|
759
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
|
835
|
-
if (
|
|
836
|
-
|
|
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
|
|
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
|
}
|