@memberjunction/react-test-harness 2.95.0 → 2.97.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.
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  import { BrowserManager } from './browser-context';
3
3
  import type { UserInfo } from '@memberjunction/core';
4
- import { FixSuggestion, Violation } from './component-linter';
4
+ import { Violation } from './component-linter';
5
5
  import { ComponentSpec, ComponentUtilities, SimpleAITools } from '@memberjunction/interactive-component-types';
6
6
  export interface ComponentExecutionOptions {
7
7
  componentSpec: ComponentSpec;
@@ -31,7 +31,6 @@ export interface ComponentExecutionResult {
31
31
  executionTime: number;
32
32
  renderCount?: number;
33
33
  lintViolations?: Violation[];
34
- fixSuggestions?: FixSuggestion[];
35
34
  }
36
35
  /**
37
36
  * ComponentRunner that uses the actual React runtime via Playwright UMD bundle
@@ -46,7 +45,6 @@ export declare class ComponentRunner {
46
45
  */
47
46
  lintComponent(componentCode: string, componentName: string, componentSpec?: any, isRootComponent?: boolean, contextUser?: UserInfo, options?: any): Promise<{
48
47
  violations: Violation[];
49
- suggestions: FixSuggestion[];
50
48
  hasErrors: boolean;
51
49
  }>;
52
50
  executeComponent(options: ComponentExecutionOptions): Promise<ComponentExecutionResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"component-runner.d.ts","sourceRoot":"","sources":["../../src/lib/component-runner.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,KAAK,EAAiC,QAAQ,EAAyD,MAAM,sBAAsB,CAAC;AAC3I,OAAO,EAAmB,aAAa,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,aAAa,EAMd,MAAM,6CAA6C,CAAC;AAMrD,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,GAAG,kBAAkB,GAAG,aAAa,CAAC;IAC/D,WAAW,EAAE,QAAQ,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,SAAS,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CAClC;AAED;;GAEG;AACH,qBAAa,eAAe;IAkBd,OAAO,CAAC,cAAc;IAhBlC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAS/C;IAKF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAE7B,cAAc,EAAE,cAAc;IAElD;;OAEG;IACG,aAAa,CACjB,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,GAAG,EACnB,eAAe,CAAC,EAAE,OAAO,EACzB,WAAW,CAAC,EAAE,QAAQ,EACtB,OAAO,CAAC,EAAE,GAAG,GACZ,OAAO,CAAC;QAAE,UAAU,EAAE,SAAS,EAAE,CAAC;QAAC,WAAW,EAAE,aAAa,EAAE,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAoBnF,gBAAgB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IA+lB7F;;;OAGG;YACW,oBAAoB;IAiElC;;OAEG;YACW,sBAAsB;IAqHpC;;OAEG;YACW,kBAAkB;IAyEhC;;OAEG;YACW,oBAAoB;IAmClC;;OAEG;YACW,eAAe;IAkB7B;;OAEG;IACH,OAAO,CAAC,mBAAmB;YA+Bb,qBAAqB;cAqBnB,uBAAuB,CAAC,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAC;IA2JtF;;OAEG;YACW,iBAAiB;IAsT/B;;OAEG;IACH,OAAO,CAAC,aAAa;CA6BtB"}
1
+ {"version":3,"file":"component-runner.d.ts","sourceRoot":"","sources":["../../src/lib/component-runner.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,KAAK,EAAiC,QAAQ,EAAyD,MAAM,sBAAsB,CAAC;AAC3I,OAAO,EAAmB,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,aAAa,EAMd,MAAM,6CAA6C,CAAC;AAMrD,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,GAAG,kBAAkB,GAAG,aAAa,CAAC;IAC/D,WAAW,EAAE,QAAQ,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,SAAS,EAAE,CAAC;CAC9B;AAED;;GAEG;AACH,qBAAa,eAAe;IAkBd,OAAO,CAAC,cAAc;IAhBlC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAS/C;IAKF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAE7B,cAAc,EAAE,cAAc;IAElD;;OAEG;IACG,aAAa,CACjB,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,GAAG,EACnB,eAAe,CAAC,EAAE,OAAO,EACzB,WAAW,CAAC,EAAE,QAAQ,EACtB,OAAO,CAAC,EAAE,GAAG,GACZ,OAAO,CAAC;QAAE,UAAU,EAAE,SAAS,EAAE,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAmBrD,gBAAgB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IA6sB7F;;;OAGG;YACW,oBAAoB;IA0ElC;;OAEG;YACW,sBAAsB;IAqHpC;;OAEG;YACW,kBAAkB;IAiFhC;;OAEG;YACW,oBAAoB;IA8ClC;;OAEG;YACW,eAAe;IAkB7B;;OAEG;IACH,OAAO,CAAC,mBAAmB;YA+Bb,qBAAqB;cAqBnB,uBAAuB,CAAC,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAC;IA2JtF;;OAEG;YACW,iBAAiB;IAsT/B;;OAEG;IACH,OAAO,CAAC,aAAa;CA6BtB"}
@@ -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
  }
@@ -131,22 +130,25 @@ class ComponentRunner {
131
130
  console.log(' - spec.name:', options.componentSpec.name);
132
131
  console.log(' - spec.code length:', options.componentSpec.code?.length || 0);
133
132
  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
133
+ // Show spec-specific libraries, not all available libraries
134
+ if (options.componentSpec.libraries && options.componentSpec.libraries.length > 0) {
135
+ console.log(' - spec requires libraries:', options.componentSpec.libraries.map(lib => ({
136
+ name: lib.name,
137
+ globalVariable: lib.globalVariable,
138
+ version: lib.version
139
139
  })));
140
140
  }
141
+ else {
142
+ console.log(' - spec requires libraries: none');
143
+ }
144
+ // Total available libraries in metadata (for context only)
145
+ console.log(' - total available libraries in metadata:', allLibraries?.length || 0);
141
146
  }
142
147
  // Execute the component using the real React runtime with timeout (Recommendation #1)
143
148
  const executionPromise = page.evaluate(async ({ spec, props, debug, componentLibraries }) => {
144
149
  if (debug) {
145
150
  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
- }
151
+ console.log('📚 BROWSER: Component libraries available for loading:', componentLibraries?.length || 0);
150
152
  }
151
153
  // Declare renderCheckInterval at the top scope for cleanup
152
154
  let renderCheckInterval;
@@ -215,8 +217,15 @@ class ComponentRunner {
215
217
  // The spec might not have globalVariable, but we need it for the runtime to work
216
218
  if (spec.libraries && componentLibraries) {
217
219
  for (const specLib of spec.libraries) {
220
+ // Skip if library entry is invalid
221
+ if (!specLib || !specLib.name) {
222
+ if (debug) {
223
+ console.warn(' ⚠️ Skipping invalid library entry (missing name):', specLib);
224
+ }
225
+ continue;
226
+ }
218
227
  if (!specLib.globalVariable) {
219
- const libDef = componentLibraries.find(l => l.Name.toLowerCase() === specLib.name.toLowerCase());
228
+ const libDef = componentLibraries.find(l => l && l.Name && l.Name.toLowerCase() === specLib.name.toLowerCase());
220
229
  if (libDef && libDef.GlobalVariable) {
221
230
  specLib.globalVariable = libDef.GlobalVariable;
222
231
  if (debug) {
@@ -257,10 +266,17 @@ class ComponentRunner {
257
266
  message: `Component registration failed: ${registrationError.message || registrationError}`,
258
267
  stack: registrationError.stack,
259
268
  type: 'registration-error',
260
- phase: 'component-compilation'
269
+ phase: 'component-compilation',
270
+ source: 'runtime-wrapper'
261
271
  });
262
272
  window.__testHarnessTestFailed = true;
263
- throw registrationError; // Re-throw for outer handler
273
+ // Don't re-throw - let execution continue to collect this error properly
274
+ // Return a failure result so the Promise resolves properly
275
+ return {
276
+ success: false,
277
+ error: `Component registration failed: ${registrationError.message || registrationError}`,
278
+ componentCount: 0
279
+ };
264
280
  }
265
281
  if (debug && !registrationResult.success) {
266
282
  console.log('❌ Registration failed:', registrationResult.errors);
@@ -319,7 +335,8 @@ class ComponentRunner {
319
335
  window.__testHarnessRuntimeErrors = window.__testHarnessRuntimeErrors || [];
320
336
  window.__testHarnessRuntimeErrors.push({
321
337
  message: `Likely infinite render loop: ${currentRenderCount} createElement calls (max: ${MAX_RENDERS_ALLOWED})`,
322
- type: 'render-loop'
338
+ type: 'render-loop',
339
+ source: 'test-harness'
323
340
  });
324
341
  // Try to unmount to stop the madness
325
342
  try {
@@ -355,18 +372,29 @@ class ComponentRunner {
355
372
  static getDerivedStateFromError(error) {
356
373
  // Capture the actual error message IMMEDIATELY
357
374
  window.__testHarnessRuntimeErrors = window.__testHarnessRuntimeErrors || [];
375
+ // Check if this is a minified React error
376
+ const errorMessage = error.message || error.toString();
377
+ let enhancedMessage = errorMessage;
378
+ if (errorMessage.includes('Minified React error')) {
379
+ // Extract error number if present
380
+ const match = errorMessage.match(/#(\d+)/);
381
+ if (match) {
382
+ enhancedMessage = `React Error #${match[1]} - Visit https://react.dev/errors/${match[1]} for details. ${errorMessage}`;
383
+ }
384
+ }
358
385
  window.__testHarnessRuntimeErrors.push({
359
- message: error.message || error.toString(),
386
+ message: enhancedMessage,
360
387
  stack: error.stack,
361
388
  type: 'react-render-error',
362
- phase: 'component-render'
389
+ phase: 'component-render',
390
+ source: 'user-component' // This is the actual error from user's component
363
391
  });
364
392
  window.__testHarnessTestFailed = true;
365
393
  return { hasError: true, error };
366
394
  }
367
395
  componentDidCatch(error, errorInfo) {
368
- console.error('React Error Boundary caught:', error, errorInfo);
369
- // Update the last error with component stack info
396
+ // Don't log here - it creates duplicate messages
397
+ // Just update the last error with component stack info
370
398
  const errors = window.__testHarnessRuntimeErrors || [];
371
399
  if (errors.length > 0) {
372
400
  const lastError = errors[errors.length - 1];
@@ -378,8 +406,8 @@ class ComponentRunner {
378
406
  render() {
379
407
  if (this.state.hasError) {
380
408
  // DON'T re-throw - this causes "Script error"
381
- // Instead, render an error message
382
- return window.React.createElement('div', { style: { color: 'red', padding: '20px' } }, 'Component failed to render: ' + (this.state.error?.message || 'Unknown error'));
409
+ // Instead, render a simple error indicator
410
+ return null; // Don't render anything - the error is already captured
383
411
  }
384
412
  return this.props.children;
385
413
  }
@@ -411,7 +439,8 @@ class ComponentRunner {
411
439
  window.__testHarnessRuntimeErrors.push({
412
440
  message: error.message || String(error),
413
441
  stack: error.stack,
414
- type: 'execution-error'
442
+ type: 'execution-error',
443
+ source: 'runtime-wrapper'
415
444
  });
416
445
  window.__testHarnessTestFailed = true;
417
446
  return {
@@ -429,17 +458,34 @@ class ComponentRunner {
429
458
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Component execution timeout after ${globalTimeout}ms`)), globalTimeout));
430
459
  // Race between execution and timeout
431
460
  let executionResult;
461
+ let hasTimeout = false;
432
462
  try {
433
463
  executionResult = await Promise.race([executionPromise, timeoutPromise]);
434
464
  }
435
465
  catch (timeoutError) {
436
466
  // Handle timeout gracefully
467
+ hasTimeout = true;
437
468
  errors.push(`Component execution timed out after ${globalTimeout}ms`);
438
469
  executionResult = {
439
470
  success: false,
440
471
  error: timeoutError instanceof Error ? timeoutError.message : 'Execution timeout'
441
472
  };
442
473
  }
474
+ // Ensure executionResult has proper shape
475
+ if (!executionResult) {
476
+ executionResult = {
477
+ success: false,
478
+ error: 'Component execution returned no result'
479
+ };
480
+ errors.push('Component execution failed to return a result');
481
+ }
482
+ else if (!executionResult.success && executionResult.error) {
483
+ // Add the execution error if it hasn't been captured elsewhere
484
+ const errorMsg = `Component execution failed: ${executionResult.error}`;
485
+ if (!errors.some(e => e.includes(executionResult.error))) {
486
+ errors.push(errorMsg);
487
+ }
488
+ }
443
489
  if (debug) {
444
490
  console.log('Execution result:', executionResult);
445
491
  }
@@ -448,9 +494,9 @@ class ComponentRunner {
448
494
  await page.waitForTimeout(renderWaitTime);
449
495
  // Get render count
450
496
  renderCount = await page.evaluate(() => window.__testHarnessRenderCount || 0);
451
- // Collect all errors
452
- const runtimeErrors = await this.collectRuntimeErrors(page);
453
- errors.push(...runtimeErrors);
497
+ // Collect all errors with source information
498
+ const runtimeErrorsWithSource = await this.collectRuntimeErrors(page);
499
+ errors.push(...runtimeErrorsWithSource.map(e => e.message)); // Extract messages for backward compat
454
500
  // Collect warnings (separate from errors)
455
501
  const collectedWarnings = await this.collectWarnings(page);
456
502
  warnings.push(...collectedWarnings);
@@ -460,8 +506,9 @@ class ComponentRunner {
460
506
  const asyncErrors = await this.collectRuntimeErrors(page);
461
507
  // Only add new errors
462
508
  asyncErrors.forEach(err => {
463
- if (!errors.includes(err)) {
464
- errors.push(err);
509
+ if (!errors.includes(err.message)) {
510
+ errors.push(err.message);
511
+ runtimeErrorsWithSource.push(err); // Keep the structured version too
465
512
  }
466
513
  });
467
514
  // Also check for new warnings
@@ -475,26 +522,65 @@ class ComponentRunner {
475
522
  const html = await page.content();
476
523
  // Take screenshot
477
524
  const screenshot = await page.screenshot();
525
+ // Check for excessive render count first
526
+ const hasRenderLoop = renderCount > ComponentRunner.MAX_RENDER_COUNT;
527
+ if (hasRenderLoop) {
528
+ errors.push(`Possible render loop: ${renderCount} createElement calls detected (likely infinite loop)`);
529
+ }
478
530
  // Determine success
479
531
  const success = errors.length === 0 &&
480
532
  criticalWarnings.length === 0 &&
481
- renderCount <= ComponentRunner.MAX_RENDER_COUNT &&
533
+ !hasRenderLoop &&
534
+ !hasTimeout &&
482
535
  executionResult.success;
483
- if (renderCount > ComponentRunner.MAX_RENDER_COUNT) {
484
- errors.push(`Possible render loop: ${renderCount} createElement calls detected (likely infinite loop)`);
485
- }
486
536
  // Combine runtime errors with data errors
487
537
  const allErrors = [...errors, ...dataErrors];
488
- const result = {
489
- success: success && dataErrors.length === 0, // Fail if we have data errors
490
- html,
491
- errors: allErrors.map(e => ({
538
+ // Map runtime errors with source info, data errors don't have source
539
+ const errorViolations = runtimeErrorsWithSource.map(e => ({
540
+ message: e.message,
541
+ severity: 'critical',
542
+ rule: 'runtime-error',
543
+ line: 0,
544
+ column: 0,
545
+ source: e.source
546
+ }));
547
+ // Add timeout error if detected
548
+ if (hasTimeout) {
549
+ errorViolations.push({
550
+ message: `Component execution timed out after ${globalTimeout}ms`,
551
+ severity: 'critical',
552
+ rule: 'timeout',
553
+ line: 0,
554
+ column: 0,
555
+ source: 'test-harness' // This is a test harness timeout
556
+ });
557
+ }
558
+ // Add render loop error if detected
559
+ if (hasRenderLoop) {
560
+ errorViolations.push({
561
+ message: `Possible render loop: ${renderCount} createElement calls detected (likely infinite loop)`,
562
+ severity: 'critical',
563
+ rule: 'render-loop',
564
+ line: 0,
565
+ column: 0,
566
+ source: 'test-harness' // This is a test harness detection
567
+ });
568
+ }
569
+ // Add data errors without source
570
+ dataErrors.forEach(e => {
571
+ errorViolations.push({
492
572
  message: e,
493
573
  severity: 'critical',
494
574
  rule: 'runtime-error',
495
575
  line: 0,
496
- column: 0
497
- })),
576
+ column: 0,
577
+ source: 'user-component' // Data errors are from user's data access code
578
+ });
579
+ });
580
+ const result = {
581
+ success: success && dataErrors.length === 0, // Fail if we have data errors
582
+ html,
583
+ errors: errorViolations,
498
584
  warnings: warnings.map(w => ({
499
585
  message: w,
500
586
  severity: 'low',
@@ -514,19 +600,35 @@ class ComponentRunner {
514
600
  return result;
515
601
  }
516
602
  catch (error) {
517
- errors.push(error instanceof Error ? error.message : String(error));
518
- // Combine runtime errors with data errors
519
- const allErrors = [...errors, ...dataErrors];
520
- const result = {
521
- success: false,
522
- html: '',
523
- errors: allErrors.map(e => ({
603
+ // For catch block errors, we need to handle them specially
604
+ const catchError = {
605
+ message: error instanceof Error ? error.message : String(error),
606
+ source: 'test-harness' // Errors caught here are usually test harness issues
607
+ };
608
+ // Create error violations including the catch error
609
+ const errorViolations = [{
610
+ message: catchError.message,
611
+ severity: 'critical',
612
+ rule: 'runtime-error',
613
+ line: 0,
614
+ column: 0,
615
+ source: catchError.source
616
+ }];
617
+ // Add any data errors
618
+ dataErrors.forEach(e => {
619
+ errorViolations.push({
524
620
  message: e,
525
621
  severity: 'critical',
526
622
  rule: 'runtime-error',
527
623
  line: 0,
528
- column: 0
529
- })),
624
+ column: 0,
625
+ source: 'user-component'
626
+ });
627
+ });
628
+ const result = {
629
+ success: false,
630
+ html: '',
631
+ errors: errorViolations,
530
632
  warnings: warnings.map(w => ({
531
633
  message: w,
532
634
  severity: 'low',
@@ -565,7 +667,12 @@ class ComponentRunner {
565
667
  * Component-specific libraries are loaded by the runtime's ComponentCompiler
566
668
  */
567
669
  async loadRuntimeLibraries(page, debug = false) {
568
- // Helper function to load scripts with timeout (Recommendation #3)
670
+ // Import getCoreRuntimeLibraries to get the correct URLs for React libraries
671
+ // We can't use LibraryLoader.loadAllLibraries() here because it expects to run in a browser
672
+ // environment with window/document, but we're in Node.js with Playwright
673
+ const { getCoreRuntimeLibraries } = await Promise.resolve().then(() => __importStar(require('@memberjunction/react-runtime')));
674
+ const coreLibraries = getCoreRuntimeLibraries(debug);
675
+ // Helper function to load scripts with timeout
569
676
  const loadScriptWithTimeout = async (url, timeout = 10000) => {
570
677
  try {
571
678
  await Promise.race([
@@ -577,11 +684,15 @@ class ComponentRunner {
577
684
  throw new Error(`Failed to load script ${url}: ${error instanceof Error ? error.message : String(error)}`);
578
685
  }
579
686
  };
580
- // Load React and ReactDOM with timeout protection
581
- await loadScriptWithTimeout('https://unpkg.com/react@18/umd/react.development.js');
582
- await loadScriptWithTimeout('https://unpkg.com/react-dom@18/umd/react-dom.development.js');
583
- // Load Babel for JSX transformation with timeout
584
- await loadScriptWithTimeout('https://unpkg.com/@babel/standalone/babel.min.js');
687
+ // Load the core libraries (React, ReactDOM, Babel) into the Playwright page context
688
+ for (const lib of coreLibraries) {
689
+ if (lib.cdnUrl) {
690
+ await loadScriptWithTimeout(lib.cdnUrl);
691
+ if (debug) {
692
+ console.log(` ✓ Loaded ${lib.displayName} (${lib.globalVariable})`);
693
+ }
694
+ }
695
+ }
585
696
  // Load the real MemberJunction React Runtime UMD bundle
586
697
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
587
698
  // Resolve the path to the UMD bundle
@@ -768,10 +879,16 @@ class ComponentRunner {
768
879
  };
769
880
  // Global error handler
770
881
  window.addEventListener('error', (event) => {
882
+ // Determine source based on error content
883
+ let source = 'user-component'; // Default to user component
884
+ if (event.message && event.message.includes('Script error')) {
885
+ source = 'runtime-wrapper';
886
+ }
771
887
  window.__testHarnessRuntimeErrors.push({
772
888
  message: event.error?.message || event.message,
773
889
  stack: event.error?.stack,
774
- type: 'runtime'
890
+ type: 'runtime',
891
+ source: source
775
892
  });
776
893
  window.__testHarnessTestFailed = true;
777
894
  });
@@ -780,7 +897,8 @@ class ComponentRunner {
780
897
  window.__testHarnessRuntimeErrors.push({
781
898
  message: 'Unhandled Promise Rejection: ' + (event.reason?.message || event.reason),
782
899
  stack: event.reason?.stack,
783
- type: 'promise-rejection'
900
+ type: 'promise-rejection',
901
+ source: 'user-component' // Async errors are likely from user code
784
902
  });
785
903
  window.__testHarnessTestFailed = true;
786
904
  event.preventDefault();
@@ -799,23 +917,31 @@ class ComponentRunner {
799
917
  };
800
918
  });
801
919
  const errors = [];
802
- // Only add "test failed" message if there are actual errors
803
- if (errorData.testFailed && (errorData.runtimeErrors.length > 0 || errorData.consoleErrors.length > 0)) {
804
- errors.push('Test marked as failed by error handlers');
805
- }
920
+ // Check if we have any specific React render errors
921
+ const hasSpecificReactError = errorData.runtimeErrors.some((error) => error.type === 'react-render-error' &&
922
+ !error.message.includes('Script error'));
923
+ // Process runtime errors with their source information
806
924
  errorData.runtimeErrors.forEach((error) => {
807
- // Include phase information if available to help identify where the error occurred
925
+ // Skip generic "Script error" messages if we have more specific React errors
926
+ if (hasSpecificReactError &&
927
+ error.type === 'runtime' &&
928
+ error.message === 'Script error.') {
929
+ return; // Skip this generic error
930
+ }
808
931
  const phase = error.phase ? ` (during ${error.phase})` : '';
809
932
  const errorMsg = `${error.type} error: ${error.message}${phase}`;
810
- if (!errors.includes(errorMsg)) {
811
- errors.push(errorMsg);
812
- }
933
+ errors.push({
934
+ message: errorMsg,
935
+ source: error.source
936
+ });
813
937
  });
938
+ // Console errors don't have source info
814
939
  errorData.consoleErrors.forEach((error) => {
815
940
  const errorMsg = `Console error: ${error}`;
816
- if (!errors.includes(errorMsg)) {
817
- errors.push(errorMsg);
818
- }
941
+ errors.push({
942
+ message: errorMsg,
943
+ source: 'react-framework' // Console errors from React are framework level
944
+ });
819
945
  });
820
946
  return errors;
821
947
  }