@memberjunction/react-test-harness 2.90.0 → 2.92.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,22 +1,44 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
26
  exports.ComponentRunner = void 0;
4
- const react_runtime_1 = require("@memberjunction/react-runtime");
5
27
  const core_1 = require("@memberjunction/core");
6
28
  const component_linter_1 = require("./component-linter");
29
+ const core_entities_1 = require("@memberjunction/core-entities");
30
+ /**
31
+ * ComponentRunner that uses the actual React runtime via Playwright UMD bundle
32
+ */
7
33
  class ComponentRunner {
8
34
  constructor(browserManager) {
9
35
  this.browserManager = browserManager;
10
- this.compiler = new react_runtime_1.ComponentCompiler();
11
- this.registry = new react_runtime_1.ComponentRegistry();
12
- // Set up runtime context (will be populated in browser)
13
- this.runtimeContext = {};
14
36
  }
15
37
  /**
16
38
  * Lint component code before execution
17
39
  */
18
- async lintComponent(componentCode, componentName, componentSpec, isRootComponent) {
19
- const lintResult = await component_linter_1.ComponentLinter.lintComponent(componentCode, componentName, componentSpec, isRootComponent);
40
+ async lintComponent(componentCode, componentName, componentSpec, isRootComponent, contextUser) {
41
+ const lintResult = await component_linter_1.ComponentLinter.lintComponent(componentCode, componentName, componentSpec, isRootComponent, contextUser);
20
42
  const hasErrors = lintResult.violations.some(v => v.severity === 'critical' || v.severity === 'high');
21
43
  return {
22
44
  violations: lintResult.violations,
@@ -30,65 +52,454 @@ class ComponentRunner {
30
52
  const warnings = [];
31
53
  const criticalWarnings = [];
32
54
  const consoleLogs = [];
55
+ const dataErrors = []; // Track data access errors from RunView/RunQuery
33
56
  let renderCount = 0;
34
- // Default debug to true
35
- const debug = options.debug !== false;
57
+ const debug = options.debug !== false; // Default to true for debugging
58
+ const globalTimeout = options.timeout || 30000; // Default 30 seconds total timeout
36
59
  if (debug) {
37
60
  console.log('\nšŸ” === Component Execution Debug Mode ===');
38
61
  console.log('Component:', options.componentSpec.name);
39
62
  console.log('Props:', JSON.stringify(options.props || {}, null, 2));
40
63
  }
64
+ // Get a fresh page for this test execution
65
+ const page = await this.browserManager.getPage();
66
+ // Set default timeout for all page operations (Recommendation #2)
67
+ page.setDefaultTimeout(globalTimeout);
68
+ // Load component metadata and libraries first (needed for library loading)
69
+ await core_entities_1.ComponentMetadataEngine.Instance.Config(false, options.contextUser);
70
+ const allLibraries = core_entities_1.ComponentMetadataEngine.Instance.ComponentLibraries.map(c => c.GetAll());
41
71
  try {
42
- const page = await this.browserManager.getPage();
43
- // Clear the page before each test for isolation
72
+ // Navigate to a blank page FIRST, then load runtime
44
73
  await page.goto('about:blank');
45
- // Expose MJ utilities to the page
46
- await this.exposeMJUtilities(page, options.contextUser);
47
- // Then set up monitoring
74
+ // Set up the basic HTML structure
75
+ await page.setContent(`
76
+ <!DOCTYPE html>
77
+ <html>
78
+ <head>
79
+ <meta charset="utf-8">
80
+ <title>React Component Test (V2)</title>
81
+ <style>
82
+ body {
83
+ margin: 0;
84
+ padding: 20px;
85
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
86
+ }
87
+ #root {
88
+ min-height: 100vh;
89
+ background-color: white;
90
+ padding: 20px;
91
+ }
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <div id="root" data-testid="react-root"></div>
96
+ </body>
97
+ </html>
98
+ `);
99
+ // Always load runtime libraries after setting content
100
+ // This ensures they persist in the current page context
101
+ // allLibraries is loaded above from ComponentMetadataEngine
102
+ await this.loadRuntimeLibraries(page, options.componentSpec, allLibraries, debug);
103
+ // Verify the runtime is actually loaded
104
+ const runtimeCheck = await page.evaluate(() => {
105
+ return {
106
+ hasMJRuntime: typeof window.MJReactRuntime !== 'undefined',
107
+ hasReact: typeof window.React !== 'undefined',
108
+ hasReactDOM: typeof window.ReactDOM !== 'undefined',
109
+ hasBabel: typeof window.Babel !== 'undefined',
110
+ mjRuntimeKeys: window.MJReactRuntime ? Object.keys(window.MJReactRuntime) : []
111
+ };
112
+ });
113
+ if (debug) {
114
+ console.log('Runtime loaded successfully');
115
+ }
116
+ if (!runtimeCheck.hasMJRuntime) {
117
+ throw new Error('Failed to inject MJReactRuntime into page context');
118
+ }
119
+ // Set up error tracking
120
+ await this.setupErrorTracking(page);
121
+ // Set up console logging
48
122
  this.setupConsoleLogging(page, consoleLogs, warnings, criticalWarnings);
49
- this.setupErrorHandling(page, errors);
50
- await this.injectRenderTracking(page);
51
- // Create and load the component
52
- const htmlContent = this.createHTMLTemplate(options);
53
- await page.goto(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);
54
- // Wait for render with timeout detection
55
- const renderSuccess = await this.waitForRender(page, options, errors);
123
+ // Expose MJ utilities to the page
124
+ await this.exposeMJUtilities(page, options, dataErrors, debug);
125
+ if (debug) {
126
+ console.log('šŸ“¤ NODE: About to call page.evaluate with:');
127
+ console.log(' - spec.name:', options.componentSpec.name);
128
+ console.log(' - spec.code length:', options.componentSpec.code?.length || 0);
129
+ console.log(' - props:', JSON.stringify(options.props || {}, null, 2));
130
+ console.log(' - componentLibraries count:', allLibraries?.length || 0);
131
+ if (allLibraries && allLibraries.length > 0) {
132
+ console.log(' - First few libraries:', allLibraries.slice(0, 3).map(lib => ({
133
+ Name: lib.Name,
134
+ GlobalVariable: lib.GlobalVariable
135
+ })));
136
+ }
137
+ }
138
+ // Execute the component using the real React runtime with timeout (Recommendation #1)
139
+ const executionPromise = page.evaluate(async ({ spec, props, debug, componentLibraries }) => {
140
+ if (debug) {
141
+ console.log('šŸŽÆ Starting component execution');
142
+ console.log('šŸ“š BROWSER: Received componentLibraries:', componentLibraries?.length || 0);
143
+ if (componentLibraries?.length > 0) {
144
+ console.log(' First library:', componentLibraries[0]);
145
+ }
146
+ }
147
+ // Declare renderCheckInterval at the top scope for cleanup
148
+ let renderCheckInterval;
149
+ try {
150
+ // Access the real runtime classes
151
+ const MJRuntime = window.MJReactRuntime;
152
+ if (!MJRuntime) {
153
+ throw new Error('React runtime not loaded');
154
+ }
155
+ const { ComponentCompiler, ComponentRegistry, ComponentHierarchyRegistrar, SetupStyles } = MJRuntime;
156
+ if (debug) {
157
+ console.log('šŸš€ Starting component execution with real runtime');
158
+ console.log('Available runtime classes:', Object.keys(MJRuntime));
159
+ }
160
+ // Initialize LibraryRegistry if needed
161
+ // Note: In test environment, we may not have full database access
162
+ // so libraries are handled by the runtime internally
163
+ // Build libraries object from loaded libraries
164
+ const loadedLibraries = {};
165
+ if (spec.libraries && componentLibraries) {
166
+ for (const specLib of spec.libraries) {
167
+ // Find the library definition
168
+ const libDef = componentLibraries.find(l => l.Name.toLowerCase() === specLib.name.toLowerCase());
169
+ if (libDef && libDef.GlobalVariable) {
170
+ // Check if the library is available as a global
171
+ const libraryValue = window[libDef.GlobalVariable];
172
+ if (libraryValue) {
173
+ loadedLibraries[libDef.GlobalVariable] = libraryValue;
174
+ if (debug) {
175
+ console.log(`šŸ“¦ Added ${libDef.Name} to runtime context as ${libDef.GlobalVariable}`);
176
+ }
177
+ }
178
+ else {
179
+ console.warn(`āš ļø Library ${libDef.Name} global variable ${libDef.GlobalVariable} not found on window`);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ // Create runtime context with loaded libraries
185
+ const runtimeContext = {
186
+ React: window.React,
187
+ ReactDOM: window.ReactDOM,
188
+ libraries: loadedLibraries
189
+ };
190
+ // Create instances
191
+ const compiler = new ComponentCompiler();
192
+ compiler.setBabelInstance(window.Babel);
193
+ // IMPORTANT: Configure the LibraryRegistry in the browser context
194
+ // This is needed for the compiler to know about approved libraries
195
+ if (window.MJReactRuntime && window.MJReactRuntime.LibraryRegistry) {
196
+ const { LibraryRegistry } = window.MJReactRuntime;
197
+ // Configure the registry with the component libraries
198
+ // Note: LibraryRegistry.Config expects ComponentLibraryEntity[]
199
+ await LibraryRegistry.Config(false, componentLibraries || []);
200
+ if (debug) {
201
+ console.log('āš™ļø Configured LibraryRegistry with', componentLibraries?.length || 0, 'libraries');
202
+ }
203
+ }
204
+ const registry = new ComponentRegistry();
205
+ const registrar = new ComponentHierarchyRegistrar(compiler, registry, runtimeContext);
206
+ // Use the utilities we already created with mock metadata
207
+ // Don't call createRuntimeUtilities() as it tries to create new Metadata() which fails
208
+ const utilities = window.__mjUtilities;
209
+ if (!utilities) {
210
+ throw new Error('Utilities not found - exposeMJUtilities may have failed');
211
+ }
212
+ const styles = SetupStyles();
213
+ if (debug) {
214
+ console.log('šŸ“¦ Registering component hierarchy...');
215
+ }
216
+ // CRITICAL: Ensure spec.libraries have globalVariable set from componentLibraries
217
+ // The spec might not have globalVariable, but we need it for the runtime to work
218
+ if (spec.libraries && componentLibraries) {
219
+ for (const specLib of spec.libraries) {
220
+ if (!specLib.globalVariable) {
221
+ const libDef = componentLibraries.find(l => l.Name.toLowerCase() === specLib.name.toLowerCase());
222
+ if (libDef && libDef.GlobalVariable) {
223
+ specLib.globalVariable = libDef.GlobalVariable;
224
+ if (debug) {
225
+ console.log(` šŸ“ Enhanced spec library ${specLib.name} with globalVariable: ${libDef.GlobalVariable}`);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ if (debug) {
231
+ console.log('šŸ” Spec libraries after enhancement:', spec.libraries.map((l) => ({
232
+ name: l.name,
233
+ globalVariable: l.globalVariable
234
+ })));
235
+ }
236
+ }
237
+ // Register the component hierarchy with error capture
238
+ // IMPORTANT: Pass component libraries for library loading to work
239
+ if (debug) {
240
+ console.log('šŸ“š Registering component with', componentLibraries?.length || 0, 'libraries');
241
+ if (componentLibraries?.length > 0) {
242
+ console.log(' Passing libraries to registrar:', componentLibraries.slice(0, 2).map((l) => l.Name));
243
+ }
244
+ }
245
+ let registrationResult;
246
+ try {
247
+ registrationResult = await registrar.registerHierarchy(spec, {
248
+ styles,
249
+ namespace: 'Global',
250
+ version: 'v1', // Use v1 to match the registry defaults
251
+ allLibraries: componentLibraries || [] // Pass the component libraries for LibraryRegistry
252
+ });
253
+ }
254
+ catch (registrationError) {
255
+ // Capture the actual error before it gets obscured
256
+ console.error('šŸ”“ Component registration error:', registrationError);
257
+ window.__testHarnessRuntimeErrors = window.__testHarnessRuntimeErrors || [];
258
+ window.__testHarnessRuntimeErrors.push({
259
+ message: `Component registration failed: ${registrationError.message || registrationError}`,
260
+ stack: registrationError.stack,
261
+ type: 'registration-error',
262
+ phase: 'component-compilation'
263
+ });
264
+ window.__testHarnessTestFailed = true;
265
+ throw registrationError; // Re-throw for outer handler
266
+ }
267
+ if (debug && !registrationResult.success) {
268
+ console.log('āŒ Registration failed:', registrationResult.errors);
269
+ }
270
+ if (!registrationResult.success) {
271
+ throw new Error('Component registration failed: ' + JSON.stringify(registrationResult.errors));
272
+ }
273
+ if (debug) {
274
+ console.log('šŸ“ Registered components:', registrationResult.registeredComponents);
275
+ // Note: ComponentRegistry doesn't expose internal components Map directly
276
+ // We can see what was registered through the registrationResult
277
+ }
278
+ // Get the root component - explicitly pass namespace and version
279
+ const RootComponent = registry.get(spec.name, 'Global', 'v1');
280
+ if (!RootComponent) {
281
+ // Enhanced error message with debugging info
282
+ console.error('Failed to find component:', spec.name);
283
+ console.error('Registry keys:', Array.from(registry.components.keys()));
284
+ throw new Error('Root component not found: ' + spec.name);
285
+ }
286
+ // Get all registered components for prop passing
287
+ const components = registry.getAll('Global', 'v1');
288
+ // Add all loaded library exports to the components object
289
+ // This allows generated code to use components.PieChart, components.ResponsiveContainer, etc.
290
+ // for libraries that export components (like Recharts)
291
+ for (const [globalVar, libraryValue] of Object.entries(loadedLibraries)) {
292
+ if (typeof libraryValue === 'object' && libraryValue !== null) {
293
+ // If the library exports an object with multiple components, spread them
294
+ Object.assign(components, libraryValue);
295
+ if (debug) {
296
+ console.log(`šŸ“¦ Added ${globalVar} exports to components object`);
297
+ }
298
+ }
299
+ }
300
+ // Render the component
301
+ const rootElement = document.getElementById('root');
302
+ if (!rootElement) {
303
+ throw new Error('Root element not found');
304
+ }
305
+ const root = window.ReactDOM.createRoot(rootElement);
306
+ // Set up render count protection
307
+ // This is for detecting infinite loops during execution
308
+ // Note: counts createElement calls, not re-renders
309
+ const MAX_RENDERS_ALLOWED = 10000; // Complex dashboards can have many createElement calls
310
+ if (typeof window !== 'undefined') {
311
+ renderCheckInterval = setInterval(() => {
312
+ const currentRenderCount = window.__testHarnessRenderCount || 0;
313
+ if (currentRenderCount > MAX_RENDERS_ALLOWED) {
314
+ clearInterval(renderCheckInterval);
315
+ // Mark test as failed due to excessive renders
316
+ window.__testHarnessTestFailed = true;
317
+ window.__testHarnessRuntimeErrors = window.__testHarnessRuntimeErrors || [];
318
+ window.__testHarnessRuntimeErrors.push({
319
+ message: `Likely infinite render loop: ${currentRenderCount} createElement calls (max: ${MAX_RENDERS_ALLOWED})`,
320
+ type: 'render-loop'
321
+ });
322
+ // Try to unmount to stop the madness
323
+ try {
324
+ root.unmount();
325
+ }
326
+ catch (e) {
327
+ console.error('Failed to unmount after render loop:', e);
328
+ }
329
+ throw new Error(`Likely infinite render loop: ${currentRenderCount} createElement calls detected`);
330
+ }
331
+ }, 100); // Check every 100ms
332
+ }
333
+ // Build complete props
334
+ const componentProps = {
335
+ ...props,
336
+ utilities,
337
+ styles,
338
+ components,
339
+ savedUserSettings: {},
340
+ onSaveUserSettings: (settings) => {
341
+ console.log('User settings saved:', settings);
342
+ }
343
+ };
344
+ if (debug) {
345
+ console.log('šŸŽØ Rendering component with props:', Object.keys(componentProps));
346
+ }
347
+ // Create error boundary wrapper
348
+ const ErrorBoundary = class extends window.React.Component {
349
+ constructor(props) {
350
+ super(props);
351
+ this.state = { hasError: false, error: null };
352
+ }
353
+ static getDerivedStateFromError(error) {
354
+ // Capture the actual error message IMMEDIATELY
355
+ window.__testHarnessRuntimeErrors = window.__testHarnessRuntimeErrors || [];
356
+ window.__testHarnessRuntimeErrors.push({
357
+ message: error.message || error.toString(),
358
+ stack: error.stack,
359
+ type: 'react-render-error',
360
+ phase: 'component-render'
361
+ });
362
+ window.__testHarnessTestFailed = true;
363
+ return { hasError: true, error };
364
+ }
365
+ componentDidCatch(error, errorInfo) {
366
+ console.error('React Error Boundary caught:', error, errorInfo);
367
+ // Update the last error with component stack info
368
+ const errors = window.__testHarnessRuntimeErrors || [];
369
+ if (errors.length > 0) {
370
+ const lastError = errors[errors.length - 1];
371
+ if (lastError.type === 'react-render-error') {
372
+ lastError.componentStack = errorInfo.componentStack;
373
+ }
374
+ }
375
+ }
376
+ render() {
377
+ if (this.state.hasError) {
378
+ // DON'T re-throw - this causes "Script error"
379
+ // Instead, render an error message
380
+ return window.React.createElement('div', { style: { color: 'red', padding: '20px' } }, 'Component failed to render: ' + (this.state.error?.message || 'Unknown error'));
381
+ }
382
+ return this.props.children;
383
+ }
384
+ };
385
+ // Render with error boundary
386
+ root.render(window.React.createElement(ErrorBoundary, null, window.React.createElement(RootComponent, componentProps)));
387
+ // Clear the render check interval since we succeeded
388
+ if (renderCheckInterval) {
389
+ clearInterval(renderCheckInterval);
390
+ }
391
+ if (debug) {
392
+ console.log('āœ… Component rendered successfully');
393
+ }
394
+ return {
395
+ success: true,
396
+ componentCount: registrationResult.registeredComponents.length
397
+ };
398
+ }
399
+ catch (error) {
400
+ // Clean up render check interval if it exists
401
+ if (typeof renderCheckInterval !== 'undefined' && renderCheckInterval) {
402
+ clearInterval(renderCheckInterval);
403
+ }
404
+ console.error('šŸ”“ BROWSER: Component execution failed:', error);
405
+ console.error('šŸ”“ BROWSER: Error stack:', error.stack);
406
+ console.error('šŸ”“ BROWSER: Error type:', typeof error);
407
+ console.error('šŸ”“ BROWSER: Error stringified:', JSON.stringify(error, null, 2));
408
+ window.__testHarnessRuntimeErrors = window.__testHarnessRuntimeErrors || [];
409
+ window.__testHarnessRuntimeErrors.push({
410
+ message: error.message || String(error),
411
+ stack: error.stack,
412
+ type: 'execution-error'
413
+ });
414
+ window.__testHarnessTestFailed = true;
415
+ return {
416
+ success: false,
417
+ error: error.message || String(error)
418
+ };
419
+ }
420
+ }, {
421
+ spec: options.componentSpec,
422
+ props: options.props,
423
+ debug,
424
+ componentLibraries: allLibraries || []
425
+ });
426
+ // Create timeout promise (Recommendation #1)
427
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Component execution timeout after ${globalTimeout}ms`)), globalTimeout));
428
+ // Race between execution and timeout
429
+ let executionResult;
430
+ try {
431
+ executionResult = await Promise.race([executionPromise, timeoutPromise]);
432
+ }
433
+ catch (timeoutError) {
434
+ // Handle timeout gracefully
435
+ errors.push(`Component execution timed out after ${globalTimeout}ms`);
436
+ executionResult = {
437
+ success: false,
438
+ error: timeoutError instanceof Error ? timeoutError.message : 'Execution timeout'
439
+ };
440
+ }
441
+ if (debug) {
442
+ console.log('Execution result:', executionResult);
443
+ }
444
+ // Wait for render completion
445
+ const renderWaitTime = options.renderWaitTime || 500;
446
+ await page.waitForTimeout(renderWaitTime);
56
447
  // Get render count
57
- renderCount = await this.getRenderCount(page);
58
- // Collect runtime errors
448
+ renderCount = await page.evaluate(() => window.__testHarnessRenderCount || 0);
449
+ // Collect all errors
59
450
  const runtimeErrors = await this.collectRuntimeErrors(page);
60
451
  errors.push(...runtimeErrors);
61
- // Capture async errors only if wait time is specified
62
- if (options.asyncErrorWaitTime && options.asyncErrorWaitTime > 0) {
63
- const asyncErrors = await this.captureAsyncErrors(page, options.asyncErrorWaitTime);
64
- errors.push(...asyncErrors);
65
- }
66
- // Perform deep render validation
67
- const deepRenderErrors = await this.validateDeepRender(page);
68
- errors.push(...deepRenderErrors);
452
+ // Collect warnings (separate from errors)
453
+ const collectedWarnings = await this.collectWarnings(page);
454
+ warnings.push(...collectedWarnings);
455
+ // Capture async errors
456
+ const asyncWaitTime = options.asyncErrorWaitTime || 1000;
457
+ await page.waitForTimeout(asyncWaitTime);
458
+ const asyncErrors = await this.collectRuntimeErrors(page);
459
+ // Only add new errors
460
+ asyncErrors.forEach(err => {
461
+ if (!errors.includes(err)) {
462
+ errors.push(err);
463
+ }
464
+ });
465
+ // Also check for new warnings
466
+ const asyncWarnings = await this.collectWarnings(page);
467
+ asyncWarnings.forEach(warn => {
468
+ if (!warnings.includes(warn)) {
469
+ warnings.push(warn);
470
+ }
471
+ });
69
472
  // Get the rendered HTML
70
473
  const html = await page.content();
71
- // Take screenshot if needed
474
+ // Take screenshot
72
475
  const screenshot = await page.screenshot();
73
- // Determine success and collect any additional errors
74
- const { success, additionalErrors } = this.determineSuccess(errors, criticalWarnings, renderCount, !renderSuccess);
75
- // Add any additional errors
76
- errors.push(...additionalErrors);
476
+ // Determine success
477
+ const success = errors.length === 0 &&
478
+ criticalWarnings.length === 0 &&
479
+ renderCount <= ComponentRunner.MAX_RENDER_COUNT &&
480
+ executionResult.success;
481
+ if (renderCount > ComponentRunner.MAX_RENDER_COUNT) {
482
+ errors.push(`Possible render loop: ${renderCount} createElement calls detected (likely infinite loop)`);
483
+ }
484
+ // Combine runtime errors with data errors
485
+ const allErrors = [...errors, ...dataErrors];
77
486
  const result = {
78
- success,
487
+ success: success && dataErrors.length === 0, // Fail if we have data errors
79
488
  html,
80
- errors: errors.map(e => {
81
- return {
82
- message: e,
83
- severity: 'critical'
84
- }; // Ensure Violation type
85
- }),
86
- warnings: warnings.map(w => {
87
- return {
88
- message: w,
89
- severity: 'low'
90
- }; // Ensure Violation type
91
- }),
489
+ errors: allErrors.map(e => ({
490
+ message: e,
491
+ severity: 'critical',
492
+ rule: 'runtime-error',
493
+ line: 0,
494
+ column: 0
495
+ })),
496
+ warnings: warnings.map(w => ({
497
+ message: w,
498
+ severity: 'low',
499
+ rule: 'warning',
500
+ line: 0,
501
+ column: 0
502
+ })),
92
503
  criticalWarnings,
93
504
  console: consoleLogs,
94
505
  screenshot,
@@ -102,21 +513,25 @@ class ComponentRunner {
102
513
  }
103
514
  catch (error) {
104
515
  errors.push(error instanceof Error ? error.message : String(error));
516
+ // Combine runtime errors with data errors
517
+ const allErrors = [...errors, ...dataErrors];
105
518
  const result = {
106
519
  success: false,
107
520
  html: '',
108
- errors: errors.map(e => {
109
- return {
110
- message: e,
111
- severity: 'critical'
112
- }; // Ensure Violation type
113
- }),
114
- warnings: warnings.map(w => {
115
- return {
116
- message: w,
117
- severity: 'low'
118
- }; // Ensure Violation type
119
- }),
521
+ errors: allErrors.map(e => ({
522
+ message: e,
523
+ severity: 'critical',
524
+ rule: 'runtime-error',
525
+ line: 0,
526
+ column: 0
527
+ })),
528
+ warnings: warnings.map(w => ({
529
+ message: w,
530
+ severity: 'low',
531
+ rule: 'warning',
532
+ line: 0,
533
+ column: 0
534
+ })),
120
535
  criticalWarnings,
121
536
  console: consoleLogs,
122
537
  executionTime: Date.now() - startTime,
@@ -128,1011 +543,571 @@ class ComponentRunner {
128
543
  }
129
544
  return result;
130
545
  }
546
+ finally {
547
+ // Clean up: close the page after each test execution
548
+ // This is important because getPage() now creates a new page each time
549
+ // Closing the page ensures clean isolation between test runs
550
+ try {
551
+ await page.close();
552
+ }
553
+ catch (closeError) {
554
+ // Ignore errors when closing the page
555
+ if (debug) {
556
+ console.log('Note: Error closing page (this is usually harmless):', closeError);
557
+ }
558
+ }
559
+ }
131
560
  }
132
561
  /**
133
- * Dumps debug information to console for easier troubleshooting
562
+ * Load runtime libraries into the page
134
563
  */
135
- dumpDebugInfo(result) {
136
- console.log('\nšŸ“Š === Component Execution Results ===');
137
- console.log('Success:', result.success ? 'āœ…' : 'āŒ');
138
- console.log('Execution time:', result.executionTime + 'ms');
139
- console.log('Render count:', result.renderCount);
140
- if (result.console && result.console.length > 0) {
141
- console.log('\nšŸ“ Console Output:');
142
- result.console.forEach(log => {
143
- const icon = log.type === 'error' ? 'āŒ' :
144
- log.type === 'warning' ? 'āš ļø' :
145
- log.type === 'log' ? 'šŸ“' : 'šŸ’¬';
146
- console.log(` ${icon} [${log.type}] ${log.text}`);
147
- });
148
- }
149
- if (result.errors && result.errors.length > 0) {
150
- console.log('\nāŒ Errors:', result.errors.length);
151
- result.errors.forEach((err, i) => {
152
- const message = typeof err === 'string' ? err : err.message;
153
- console.log(` ${i + 1}. ${message}`);
154
- });
564
+ async loadRuntimeLibraries(page, componentSpec, allLibraries, debug = false) {
565
+ // Helper function to load scripts with timeout (Recommendation #3)
566
+ const loadScriptWithTimeout = async (url, timeout = 10000) => {
567
+ try {
568
+ await Promise.race([
569
+ page.addScriptTag({ url }),
570
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Script load timeout: ${url}`)), timeout))
571
+ ]);
572
+ }
573
+ catch (error) {
574
+ throw new Error(`Failed to load script ${url}: ${error instanceof Error ? error.message : String(error)}`);
575
+ }
576
+ };
577
+ // Load React and ReactDOM with timeout protection
578
+ await loadScriptWithTimeout('https://unpkg.com/react@18/umd/react.development.js');
579
+ await loadScriptWithTimeout('https://unpkg.com/react-dom@18/umd/react-dom.development.js');
580
+ // Load Babel for JSX transformation with timeout
581
+ await loadScriptWithTimeout('https://unpkg.com/@babel/standalone/babel.min.js');
582
+ // Load the real MemberJunction React Runtime UMD bundle
583
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
584
+ // Resolve the path to the UMD bundle
585
+ const runtimePath = require.resolve('@memberjunction/react-runtime/dist/runtime.umd.js');
586
+ const runtimeBundle = fs.readFileSync(runtimePath, 'utf-8');
587
+ // Inject the UMD bundle into the page
588
+ await page.addScriptTag({ content: runtimeBundle });
589
+ // The UMD bundle should have created window.MJReactRuntime
590
+ // Let's verify and potentially add any test-harness specific overrides
591
+ await page.evaluate(() => {
592
+ // Check if MJReactRuntime was loaded from the UMD bundle
593
+ if (typeof window.MJReactRuntime === 'undefined') {
594
+ throw new Error('MJReactRuntime UMD bundle did not load correctly');
595
+ }
596
+ // The real runtime is now available!
597
+ });
598
+ // Verify everything loaded
599
+ const loaded = await page.evaluate(() => {
600
+ return {
601
+ React: typeof window.React !== 'undefined',
602
+ ReactDOM: typeof window.ReactDOM !== 'undefined',
603
+ Babel: typeof window.Babel !== 'undefined',
604
+ MJRuntime: typeof window.MJReactRuntime !== 'undefined'
605
+ };
606
+ });
607
+ // All libraries loaded successfully
608
+ if (!loaded.React || !loaded.ReactDOM || !loaded.Babel || !loaded.MJRuntime) {
609
+ throw new Error('Failed to load required libraries');
155
610
  }
156
- if (result.warnings && result.warnings.length > 0) {
157
- console.log('\nāš ļø Warnings:', result.warnings.length);
158
- result.warnings.forEach((warn, i) => {
159
- const message = typeof warn === 'string' ? warn : warn.message;
160
- console.log(` ${i + 1}. ${message}`);
161
- });
611
+ // Load component-specific libraries from CDN
612
+ if (componentSpec?.libraries && allLibraries) {
613
+ await this.loadComponentLibraries(page, componentSpec.libraries, allLibraries, debug);
162
614
  }
163
- if (result.criticalWarnings && result.criticalWarnings.length > 0) {
164
- console.log('\nšŸ”“ Critical Warnings:', result.criticalWarnings.length);
165
- result.criticalWarnings.forEach((warn, i) => {
166
- console.log(` ${i + 1}. ${warn}`);
615
+ }
616
+ /**
617
+ * Load component-specific libraries from CDN
618
+ */
619
+ async loadComponentLibraries(page, specLibraries, allLibraries, debug = false) {
620
+ if (debug) {
621
+ console.log('šŸ“š Loading component libraries:', {
622
+ count: specLibraries.length,
623
+ libraries: specLibraries.map(l => l.name)
167
624
  });
168
625
  }
169
- if (result.html) {
170
- const htmlPreview = result.html.substring(0, 200);
171
- console.log('\nšŸ“„ HTML Preview:', htmlPreview + (result.html.length > 200 ? '...' : ''));
172
- }
173
- console.log('\n========================================\n');
174
- }
175
- createHTMLTemplate(options) {
176
- const propsJson = JSON.stringify(options.props || {});
177
- const specJson = JSON.stringify(options.componentSpec);
178
- // Set configuration if provided
179
- if (options.libraryConfiguration) {
180
- react_runtime_1.StandardLibraryManager.setConfiguration(options.libraryConfiguration);
626
+ // Create a map of library definitions from allLibraries
627
+ const libraryMap = new Map();
628
+ for (const lib of allLibraries) {
629
+ libraryMap.set(lib.Name.toLowerCase(), lib);
181
630
  }
182
- // Get all enabled libraries from configuration
183
- const enabledLibraries = react_runtime_1.StandardLibraryManager.getEnabledLibraries();
184
- // Separate runtime and component libraries
185
- const runtimeLibraries = enabledLibraries.filter((lib) => lib.category === 'runtime');
186
- const componentLibraries = enabledLibraries.filter((lib) => lib.category !== 'runtime');
187
- // Generate script tags for runtime libraries
188
- const runtimeScripts = runtimeLibraries
189
- .map((lib) => ` <script crossorigin src="${lib.cdnUrl}"></script>`)
190
- .join('\n');
191
- // Generate script tags for component libraries
192
- const componentScripts = componentLibraries
193
- .map((lib) => ` <script src="${lib.cdnUrl}"></script>`)
194
- .join('\n');
195
- // Generate CSS links
196
- const cssLinks = enabledLibraries
197
- .filter((lib) => lib.cdnCssUrl)
198
- .map((lib) => ` <link rel="stylesheet" href="${lib.cdnCssUrl}">`)
199
- .join('\n');
200
- // Include the ComponentCompiler class definition
201
- const componentCompilerCode = this.getComponentCompilerCode();
202
- return `
203
- <!DOCTYPE html>
204
- <html>
205
- <head>
206
- <meta charset="utf-8">
207
- <title>React Component Test</title>
208
- ${runtimeScripts}
209
- ${componentScripts}
210
- ${cssLinks}
211
- <style>
212
- body {
213
- margin: 0;
214
- padding: 20px;
215
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
216
- background-color: #f0f0f0; /* Light gray background to see contrast */
217
- }
218
- #root {
219
- min-height: 100vh;
220
- background-color: white;
221
- border: 2px solid #007bff;
222
- padding: 20px;
223
- box-shadow: 0 0 10px rgba(0,0,0,0.1);
224
- }
225
- .test-harness-header {
226
- background-color: #28a745;
227
- color: white;
228
- padding: 10px;
229
- margin-bottom: 20px;
230
- border-radius: 5px;
231
- font-weight: bold;
232
- text-align: center;
233
- }
234
- </style>
235
- <script>
236
- // Initialize error and console tracking
237
- window.__testHarnessRuntimeErrors = [];
238
- window.__testHarnessConsoleLogs = [];
239
-
240
- // Track console output for async error detection
241
- const originalConsoleError = console.error;
242
- console.error = function(...args) {
243
- window.__testHarnessConsoleLogs.push({
244
- type: 'error',
245
- text: args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')
246
- });
247
- originalConsoleError.apply(console, args);
248
- };
249
-
250
- // Global error handler
251
- window.addEventListener('error', (event) => {
252
- console.error('Runtime error:', event.error);
253
- window.__testHarnessRuntimeErrors.push({
254
- message: event.error.message,
255
- stack: event.error.stack,
256
- type: 'runtime'
257
- });
258
- });
259
-
260
- // Unhandled promise rejection handler
261
- window.addEventListener('unhandledrejection', (event) => {
262
- console.error('Unhandled promise rejection:', event.reason);
263
- window.__testHarnessRuntimeErrors.push({
264
- message: 'Unhandled Promise Rejection: ' + (event.reason?.message || event.reason || 'Unknown reason'),
265
- stack: event.reason?.stack || 'No stack trace available',
266
- type: 'promise-rejection'
267
- });
268
- // Prevent the default handling (which would log to console)
269
- event.preventDefault();
270
- });
271
-
272
- // Render tracking injection
273
- (function() {
274
- let renderCounter = 0;
275
- window.__testHarnessRenderCount = 0;
276
-
277
- // Wait for React to be available
278
- const setupRenderTracking = () => {
279
- if (window.React && window.React.createElement) {
280
- const originalCreateElement = window.React.createElement.bind(window.React);
281
-
282
- window.React.createElement = function(type, props, ...children) {
283
- renderCounter++;
284
- window.__testHarnessRenderCount = renderCounter;
285
-
286
- if (renderCounter > 1000) {
287
- console.error('Excessive renders detected: ' + renderCounter + ' renders. Possible infinite loop.');
631
+ // Load each library the component needs
632
+ for (const specLib of specLibraries) {
633
+ const libDef = libraryMap.get(specLib.name.toLowerCase());
634
+ if (!libDef) {
635
+ console.warn(`āš ļø Library ${specLib.name} not found in metadata`);
636
+ continue;
288
637
  }
289
-
290
- return originalCreateElement(type, props, ...children);
291
- };
292
- }
293
- };
294
-
295
- // Try to set up immediately
296
- setupRenderTracking();
297
-
298
- // Also try after a delay in case React loads later
299
- setTimeout(setupRenderTracking, 100);
300
- })();
301
- </script>
302
- </head>
303
- <body>
304
- <div class="test-harness-header">
305
- 🧪 React Test Harness - Component Loaded Successfully
306
- </div>
307
- <div id="root" data-testid="react-root">
308
- <!-- Component will render here -->
309
- </div>
310
- <script type="text/babel">
311
- // Immediate debug message
312
- console.log('šŸš€ Test harness script started executing');
313
- document.getElementById('root').innerHTML = '<div style="background: lime; padding: 20px; color: black; font-size: 18px;">šŸ“ Script is running...</div>';
314
-
315
- ${options.setupCode || ''}
316
-
317
- // Create runtime context with dynamic libraries
318
- const componentLibraries = ${JSON.stringify(componentLibraries.map((lib) => ({
319
- globalVariable: lib.globalVariable,
320
- displayName: lib.displayName
321
- })))};
322
-
323
- const libraries = {};
324
- componentLibraries.forEach(lib => {
325
- if (window[lib.globalVariable]) {
326
- libraries[lib.globalVariable] = window[lib.globalVariable];
327
- }
328
- });
329
-
330
- const runtimeContext = {
331
- React: React,
332
- ReactDOM: ReactDOM,
333
- libraries: libraries
334
- };
335
-
336
- // Import the ComponentCompiler implementation
337
- ${componentCompilerCode}
338
-
339
- // Create component compiler instance
340
- const compiler = new ComponentCompiler();
341
- compiler.setBabelInstance(Babel);
342
-
343
- // Create component registry
344
- class SimpleRegistry {
345
- constructor() {
346
- this.components = new Map();
347
- }
348
-
349
- register(name, component, namespace = 'Global', version = 'v1') {
350
- const key = \`\${namespace}/\${version}/\${name}\`;
351
- this.components.set(key, component);
352
- }
353
-
354
- get(name, namespace = 'Global', version = 'v1') {
355
- const key = \`\${namespace}/\${version}/\${name}\`;
356
- return this.components.get(key);
357
- }
358
-
359
- getAll(namespace = 'Global', version = 'v1') {
360
- const components = {};
361
- const prefix = \`\${namespace}/\${version}/\`;
362
- this.components.forEach((component, key) => {
363
- if (key.startsWith(prefix)) {
364
- const name = key.substring(prefix.length);
365
- components[name] = component;
366
- }
367
- });
368
- return components;
369
- }
370
- }
371
-
372
- // Create registry instance
373
- const registry = new SimpleRegistry();
374
-
375
- // Create hierarchy registrar
376
- class SimpleHierarchyRegistrar {
377
- constructor(compiler, registry, runtimeContext) {
378
- this.compiler = compiler;
379
- this.registry = registry;
380
- this.runtimeContext = runtimeContext;
381
- }
382
-
383
- async registerHierarchy(rootSpec, options = {}) {
384
- const registeredComponents = [];
385
- const errors = [];
386
- const warnings = [];
387
-
388
- // Register components recursively
389
- const registerSpec = async (spec) => {
390
- // Register children first
391
- const children = spec.dependencies || [];
392
- for (const child of children) {
393
- await registerSpec(child);
394
- }
395
-
396
- // Register this component
397
- if (spec.code) {
398
- const result = await this.compiler.compile({
399
- componentName: spec.name,
400
- componentCode: spec.code
401
- });
402
-
403
- if (result.success) {
404
- const factory = result.component.component(this.runtimeContext, {});
405
- this.registry.register(spec.name, factory.component);
406
- registeredComponents.push(spec.name);
407
- } else {
408
- errors.push({
409
- componentName: spec.name,
410
- error: result.error.message,
411
- phase: 'compilation'
412
- });
638
+ if (debug) {
639
+ console.log(`šŸ“¦ Loading ${specLib.name}:`, {
640
+ cdnUrl: libDef.CDNUrl,
641
+ globalVariable: libDef.GlobalVariable
642
+ });
643
+ }
644
+ // Load CSS if available
645
+ if (libDef.CDNCssUrl) {
646
+ const cssUrls = libDef.CDNCssUrl.split(',').map(url => url.trim());
647
+ for (const cssUrl of cssUrls) {
648
+ if (cssUrl) {
649
+ await page.addStyleTag({ url: cssUrl });
650
+ if (debug) {
651
+ console.log(` šŸŽØ Loaded CSS: ${cssUrl}`);
652
+ }
653
+ }
654
+ }
655
+ }
656
+ // Load the library script with timeout protection
657
+ if (libDef.CDNUrl) {
658
+ try {
659
+ // Add timeout for library loading (Recommendation #3)
660
+ await Promise.race([
661
+ page.addScriptTag({ url: libDef.CDNUrl }),
662
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Library load timeout: ${libDef.CDNUrl}`)), 10000))
663
+ ]);
664
+ // Verify the library loaded
665
+ const isLoaded = await page.evaluate((globalVar) => {
666
+ return typeof window[globalVar] !== 'undefined';
667
+ }, libDef.GlobalVariable);
668
+ if (isLoaded) {
669
+ if (debug) {
670
+ console.log(` šŸ“¦ Loaded ${specLib.name} (available as ${libDef.GlobalVariable})`);
671
+ }
672
+ }
673
+ else {
674
+ // Some libraries (like @mui/material) may load successfully but not attach to window
675
+ // Check if we can at least verify the script loaded
676
+ if (debug) {
677
+ console.log(` šŸ“¦ Loaded ${specLib.name} from CDN (global variable ${libDef.GlobalVariable} may not be exposed)`);
678
+ }
679
+ }
680
+ }
681
+ catch (error) {
682
+ console.error(` āŒ Error loading ${specLib.name} from ${libDef.CDNUrl}:`, error);
683
+ }
413
684
  }
414
- }
415
- };
416
-
417
- await registerSpec(rootSpec);
418
-
419
- return {
420
- success: errors.length === 0,
421
- registeredComponents,
422
- errors,
423
- warnings
424
- };
425
- }
426
- }
427
-
428
- const hierarchyRegistrar = new SimpleHierarchyRegistrar(compiler, registry, runtimeContext);
429
-
430
- // BuildUtilities function - uses real MJ utilities exposed via Playwright
431
- const BuildUtilities = () => {
432
- const utilities = {
433
- md: {
434
- entities: () => {
435
- return window.__mjGetEntities();
436
- },
437
- GetEntityObject: async (entityName) => {
438
- return await window.__mjGetEntityObject(entityName);
439
- }
440
- },
441
- rv: {
442
- RunView: async (params) => {
443
- return await window.__mjRunView(params);
444
- },
445
- RunViews: async (params) => {
446
- return await window.__mjRunViews(params);
447
- }
448
- },
449
- rq: {
450
- RunQuery: async (params) => {
451
- return await window.__mjRunQuery(params);
452
- }
453
- }
454
- };
455
- return utilities;
456
- };
457
-
458
- // SetupStyles function - copied from skip-chat implementation
459
- const SetupStyles = () => ({
460
- colors: {
461
- primary: '#5B4FE9',
462
- primaryHover: '#4940D4',
463
- primaryLight: '#E8E6FF',
464
- secondary: '#64748B',
465
- secondaryHover: '#475569',
466
- success: '#10B981',
467
- successLight: '#D1FAE5',
468
- warning: '#F59E0B',
469
- warningLight: '#FEF3C7',
470
- error: '#EF4444',
471
- errorLight: '#FEE2E2',
472
- info: '#3B82F6',
473
- infoLight: '#DBEAFE',
474
- background: '#FFFFFF',
475
- surface: '#F8FAFC',
476
- surfaceHover: '#F1F5F9',
477
- text: '#1E293B',
478
- textSecondary: '#64748B',
479
- textTertiary: '#94A3B8',
480
- textInverse: '#FFFFFF',
481
- border: '#E2E8F0',
482
- borderLight: '#F1F5F9',
483
- borderFocus: '#5B4FE9',
484
- shadow: 'rgba(0, 0, 0, 0.05)',
485
- shadowMedium: 'rgba(0, 0, 0, 0.1)',
486
- shadowLarge: 'rgba(0, 0, 0, 0.15)',
487
- },
488
- spacing: {
489
- xs: '4px',
490
- sm: '8px',
491
- md: '16px',
492
- lg: '24px',
493
- xl: '32px',
494
- xxl: '48px',
495
- xxxl: '64px',
496
- },
497
- typography: {
498
- fontFamily: '-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif',
499
- fontSize: {
500
- xs: '11px',
501
- sm: '12px',
502
- md: '14px',
503
- lg: '16px',
504
- xl: '20px',
505
- xxl: '24px',
506
- xxxl: '32px',
507
- },
508
- fontWeight: {
509
- light: '300',
510
- regular: '400',
511
- medium: '500',
512
- semibold: '600',
513
- bold: '700',
514
- },
515
- lineHeight: {
516
- tight: '1.25',
517
- normal: '1.5',
518
- relaxed: '1.75',
519
- },
520
- },
521
- borders: {
522
- radius: {
523
- sm: '6px',
524
- md: '8px',
525
- lg: '12px',
526
- xl: '16px',
527
- full: '9999px',
528
- },
529
- width: {
530
- thin: '1px',
531
- medium: '2px',
532
- thick: '3px',
533
- },
534
- },
535
- shadows: {
536
- sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
537
- md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
538
- lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
539
- xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
540
- inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
541
- },
542
- transitions: {
543
- fast: '150ms ease-in-out',
544
- normal: '250ms ease-in-out',
545
- slow: '350ms ease-in-out',
546
- },
547
- overflow: 'auto'
548
- });
549
-
550
- // React Error Boundary component
551
- const ErrorBoundary = class extends React.Component {
552
- constructor(props) {
553
- super(props);
554
- this.state = { hasError: false, error: null };
555
- }
556
-
557
- static getDerivedStateFromError(error) {
558
- return { hasError: true, error };
559
- }
560
-
561
- componentDidCatch(error, errorInfo) {
562
- console.error('React Error Boundary caught:', error, errorInfo);
563
- window.__testHarnessRuntimeErrors = window.__testHarnessRuntimeErrors || [];
564
- window.__testHarnessRuntimeErrors.push({
565
- message: error.message,
566
- stack: error.stack,
567
- componentStack: errorInfo.componentStack,
568
- type: 'react'
569
- });
570
- }
571
-
572
- render() {
573
- if (this.state.hasError) {
574
- return React.createElement('div', { style: { color: 'red', padding: '20px' } },
575
- 'Component Error: ' + this.state.error.message
576
- );
577
- }
578
- return this.props.children;
579
- }
580
- };
581
-
582
- // Load component spec and register hierarchy
583
- const componentSpec = ${specJson};
584
- const props = ${propsJson};
585
-
586
- (async () => {
587
- console.log('šŸ“¦ Starting component initialization...');
588
- console.log('React available:', typeof React !== 'undefined');
589
- console.log('ReactDOM available:', typeof ReactDOM !== 'undefined');
590
- console.log('Babel available:', typeof Babel !== 'undefined');
591
-
592
- // Update the root to show progress
593
- document.getElementById('root').innerHTML = '<div style="background: cyan; padding: 20px; color: black;">šŸ”„ Initializing components...</div>';
594
-
595
- // First, test that React is working with a simple component
596
- const TestComponent = () => React.createElement('div', {
597
- style: {
598
- background: 'yellow',
599
- padding: '10px',
600
- margin: '10px 0',
601
- border: '2px solid orange',
602
- color: 'black',
603
- fontSize: '16px',
604
- fontWeight: 'bold'
605
685
  }
606
- }, 'šŸ”§ React is working! Now loading your component...');
607
-
608
- const testRoot = ReactDOM.createRoot(document.getElementById('root'));
609
- testRoot.render(React.createElement(TestComponent));
610
-
611
- // Wait a moment to show the test component
612
- await new Promise(resolve => setTimeout(resolve, 500));
613
-
614
- // Now proceed with the actual component
615
- // Register the component hierarchy
616
- const result = await hierarchyRegistrar.registerHierarchy(componentSpec);
617
-
618
- if (!result.success) {
619
- console.error('Failed to register components:', result.errors);
620
- document.getElementById('root').innerHTML = '<div style="color: red; padding: 20px;">Failed to register components: ' + JSON.stringify(result.errors) + '</div>';
621
- return;
622
- }
623
-
624
- // Get all registered components
625
- const components = registry.getAll();
626
-
627
- // Get the root component
628
- const RootComponent = registry.get(componentSpec.name);
629
-
630
- if (!RootComponent) {
631
- console.error('Root component not found:', componentSpec.name);
632
- return;
633
- }
634
-
635
- // Simple in-memory storage for user settings
636
- let savedUserSettings = {};
637
-
638
- // Create root for rendering
639
- const root = ReactDOM.createRoot(document.getElementById('root'));
640
-
641
- // Function to render with current settings
642
- const renderWithSettings = () => {
643
- console.log('šŸŽÆ Starting component render...');
644
- console.log('Props:', props);
645
- console.log('Root component found:', !!RootComponent);
646
-
647
- const enhancedProps = {
648
- ...props,
649
- components: components,
650
- utilities: BuildUtilities(),
651
- styles: SetupStyles(),
652
- savedUserSettings: savedUserSettings,
653
- onSaveUserSettings: (newSettings) => {
654
- console.log('User settings saved:', newSettings);
655
- // Update in-memory storage
656
- savedUserSettings = { ...newSettings };
657
- // Re-render with new settings
658
- renderWithSettings();
659
- }
660
- };
661
-
662
- console.log('Enhanced props created:', Object.keys(enhancedProps));
663
-
664
- try {
665
- root.render(
666
- React.createElement(ErrorBoundary, null,
667
- React.createElement(RootComponent, enhancedProps)
668
- )
669
- );
670
- console.log('āœ… Component rendered successfully');
671
- } catch (error) {
672
- console.error('āŒ Render error:', error);
673
- document.getElementById('root').innerHTML = '<div style="color: red; padding: 20px;">Render Error: ' + error.message + '</div>';
686
+ if (debug) {
687
+ // Log all available global variables that look like libraries
688
+ // Get all the global variables we expect from the spec
689
+ const expectedGlobals = specLibraries.map(lib => {
690
+ const libDef = libraryMap.get(lib.name.toLowerCase());
691
+ return libDef?.GlobalVariable;
692
+ }).filter(Boolean);
693
+ const globals = await page.evaluate((expected) => {
694
+ const relevantGlobals = {};
695
+ // Check the expected globals from the spec
696
+ for (const key of expected) {
697
+ if (window[key]) {
698
+ relevantGlobals[key] = typeof window[key];
699
+ }
700
+ else {
701
+ relevantGlobals[key] = 'NOT FOUND';
702
+ }
703
+ }
704
+ // Also check some common library globals
705
+ const commonKeys = ['Recharts', 'chroma', '_', 'moment', 'dayjs', 'Chart', 'GSAP', 'gsap', 'lottie'];
706
+ for (const key of commonKeys) {
707
+ if (!(key in relevantGlobals) && window[key]) {
708
+ relevantGlobals[key] = typeof window[key];
709
+ }
710
+ }
711
+ return relevantGlobals;
712
+ }, expectedGlobals);
713
+ console.log('šŸŒ Available library globals:', globals);
674
714
  }
675
- };
676
-
677
- // Initial render
678
- renderWithSettings();
679
-
680
- // Add a fallback message if nothing renders after a delay
681
- setTimeout(() => {
682
- const rootElement = document.getElementById('root');
683
- if (rootElement) {
684
- const hasContent = rootElement.innerHTML.trim().length > 0;
685
- const hasVisibleChildren = rootElement.querySelector('*');
686
-
687
- console.log('Root element check:', {
688
- hasContent,
689
- innerHTML: rootElement.innerHTML.substring(0, 100),
690
- hasVisibleChildren: !!hasVisibleChildren,
691
- childCount: rootElement.childNodes.length
692
- });
693
-
694
- if (!hasContent || !hasVisibleChildren) {
695
- rootElement.innerHTML = '<div style="color: red; font-size: 18px; padding: 20px; border: 2px dashed red; background: #ffe6e6;">āš ļø Component did not render any visible content</div>';
696
- } else {
697
- // Force visibility on all children as a test
698
- const allElements = rootElement.querySelectorAll('*');
699
- allElements.forEach(el => {
700
- if (el instanceof HTMLElement) {
701
- // Make sure elements are visible
702
- if (window.getComputedStyle(el).display === 'none') {
703
- el.style.display = 'block !important';
715
+ }
716
+ /**
717
+ * Set up error tracking in the page
718
+ */
719
+ async setupErrorTracking(page) {
720
+ await page.evaluate(() => {
721
+ // Initialize error tracking
722
+ window.__testHarnessRuntimeErrors = [];
723
+ window.__testHarnessConsoleErrors = [];
724
+ window.__testHarnessConsoleWarnings = [];
725
+ window.__testHarnessTestFailed = false;
726
+ window.__testHarnessRenderCount = 0;
727
+ // Track renders
728
+ const originalCreateElement = window.React?.createElement;
729
+ if (originalCreateElement) {
730
+ window.React.createElement = function (...args) {
731
+ window.__testHarnessRenderCount++;
732
+ return originalCreateElement.apply(this, args);
733
+ };
734
+ }
735
+ // Override console.error
736
+ const originalConsoleError = console.error;
737
+ console.error = function (...args) {
738
+ const errorText = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
739
+ // Check if this is a warning rather than an error
740
+ // React warnings typically start with "Warning:" or contain warning-related text
741
+ const isWarning = errorText.includes('Warning:') ||
742
+ errorText.includes('DevTools') ||
743
+ errorText.includes('deprecated') ||
744
+ errorText.includes('has been renamed') ||
745
+ errorText.includes('will be removed') ||
746
+ errorText.includes('Consider using') ||
747
+ errorText.includes('Please update') ||
748
+ (errorText.includes('React') && errorText.includes('recognize the')) || // Prop warnings
749
+ (errorText.includes('React') && errorText.includes('Invalid'));
750
+ if (isWarning) {
751
+ // Track as warning, don't fail the test
752
+ window.__testHarnessConsoleWarnings.push(errorText);
704
753
  }
705
- if (window.getComputedStyle(el).visibility === 'hidden') {
706
- el.style.visibility = 'visible !important';
754
+ else {
755
+ // Real error - track and fail the test
756
+ window.__testHarnessConsoleErrors.push(errorText);
757
+ window.__testHarnessTestFailed = true;
707
758
  }
708
- // Add a test border to see if elements exist
709
- el.style.border = '1px dotted red';
710
- }
759
+ originalConsoleError.apply(console, args);
760
+ };
761
+ // Global error handler
762
+ window.addEventListener('error', (event) => {
763
+ window.__testHarnessRuntimeErrors.push({
764
+ message: event.error?.message || event.message,
765
+ stack: event.error?.stack,
766
+ type: 'runtime'
767
+ });
768
+ window.__testHarnessTestFailed = true;
711
769
  });
712
- console.log('Applied debug borders to', allElements.length, 'elements');
713
- }
714
- }
715
- }, 2000);
716
- })();
717
- </script>
718
- </body>
719
- </html>`;
770
+ // Unhandled promise rejection handler
771
+ window.addEventListener('unhandledrejection', (event) => {
772
+ window.__testHarnessRuntimeErrors.push({
773
+ message: 'Unhandled Promise Rejection: ' + (event.reason?.message || event.reason),
774
+ stack: event.reason?.stack,
775
+ type: 'promise-rejection'
776
+ });
777
+ window.__testHarnessTestFailed = true;
778
+ event.preventDefault();
779
+ });
780
+ });
720
781
  }
721
782
  /**
722
- * Checks if a console message is a warning
783
+ * Collect runtime errors from the page
723
784
  */
724
- isWarning(type, text) {
725
- return type === 'warning' || text.startsWith('Warning:');
785
+ async collectRuntimeErrors(page) {
786
+ const errorData = await page.evaluate(() => {
787
+ return {
788
+ runtimeErrors: window.__testHarnessRuntimeErrors || [],
789
+ consoleErrors: window.__testHarnessConsoleErrors || [],
790
+ testFailed: window.__testHarnessTestFailed || false
791
+ };
792
+ });
793
+ const errors = [];
794
+ // Only add "test failed" message if there are actual errors
795
+ if (errorData.testFailed && (errorData.runtimeErrors.length > 0 || errorData.consoleErrors.length > 0)) {
796
+ errors.push('Test marked as failed by error handlers');
797
+ }
798
+ errorData.runtimeErrors.forEach((error) => {
799
+ // Include phase information if available to help identify where the error occurred
800
+ const phase = error.phase ? ` (during ${error.phase})` : '';
801
+ const errorMsg = `${error.type} error: ${error.message}${phase}`;
802
+ if (!errors.includes(errorMsg)) {
803
+ errors.push(errorMsg);
804
+ }
805
+ });
806
+ errorData.consoleErrors.forEach((error) => {
807
+ const errorMsg = `Console error: ${error}`;
808
+ if (!errors.includes(errorMsg)) {
809
+ errors.push(errorMsg);
810
+ }
811
+ });
812
+ return errors;
726
813
  }
727
814
  /**
728
- * Checks if a warning is critical and should fail the test
815
+ * Collect warnings from the page (non-fatal issues)
729
816
  */
730
- isCriticalWarning(text) {
731
- return ComponentRunner.CRITICAL_WARNING_PATTERNS.some(pattern => pattern.test(text));
817
+ async collectWarnings(page) {
818
+ const warningData = await page.evaluate(() => {
819
+ return {
820
+ consoleWarnings: window.__testHarnessConsoleWarnings || []
821
+ };
822
+ });
823
+ const warnings = [];
824
+ warningData.consoleWarnings.forEach((warning) => {
825
+ if (!warnings.includes(warning)) {
826
+ warnings.push(warning);
827
+ }
828
+ });
829
+ return warnings;
732
830
  }
733
831
  /**
734
- * Sets up console logging with warning detection
832
+ * Set up console logging
735
833
  */
736
834
  setupConsoleLogging(page, consoleLogs, warnings, criticalWarnings) {
737
835
  page.on('console', (msg) => {
738
836
  const type = msg.type();
739
837
  const text = msg.text();
740
838
  consoleLogs.push({ type, text });
741
- if (this.isWarning(type, text)) {
742
- warnings.push(text);
743
- if (this.isCriticalWarning(text)) {
839
+ // Note: We're already handling warnings in our console.error override
840
+ // This catches any direct console.warn() calls
841
+ if (type === 'warning') {
842
+ if (!warnings.includes(text)) {
843
+ warnings.push(text);
844
+ }
845
+ // Check if it's a critical warning that should fail the test
846
+ if (ComponentRunner.CRITICAL_WARNING_PATTERNS.some(pattern => pattern.test(text))) {
744
847
  criticalWarnings.push(text);
745
848
  }
746
849
  }
747
850
  });
748
- }
749
- /**
750
- * Sets up error handling for the page
751
- */
752
- setupErrorHandling(page, errors) {
753
851
  page.on('pageerror', (error) => {
754
- errors.push(error.message);
852
+ consoleLogs.push({ type: 'error', text: error.message });
755
853
  });
756
854
  }
757
- /**
758
- * Injects render tracking code into the page
759
- */
760
- async injectRenderTracking(page) {
761
- // Instead of using evaluateOnNewDocument, we'll inject the script directly into the HTML
762
- // This avoids the Playwright-specific API issue
763
- // The actual injection will happen in createHTMLTemplate
855
+ buildLocalMJUtilities() {
856
+ console.log(" Building local MJ utilities");
857
+ const rv = new core_1.RunView();
858
+ const rq = new core_1.RunQuery();
859
+ const md = new core_1.Metadata();
860
+ return {
861
+ RunView: rv.RunView,
862
+ RunQuery: rq.RunQuery,
863
+ RunViews: rv.RunViews,
864
+ GetEntityObject: md.GetEntityObject, // return the function
865
+ Entities: md.Entities // return the function
866
+ };
764
867
  }
765
868
  /**
766
- * Waits for component to render and checks for timeouts
869
+ * Expose MJ utilities to the browser context
767
870
  */
768
- async waitForRender(page, options, errors) {
769
- const timeout = options.timeout || 10000; // 10 seconds default
770
- const renderWaitTime = options.renderWaitTime || 500; // Default 500ms for render completion
871
+ async exposeMJUtilities(page, options, dataErrors, debug = false) {
872
+ // Don't check if already exposed - we always start fresh after goto('about:blank')
873
+ // The page.exposeFunction calls need to be made for each new page instance
874
+ // Serialize contextUser to pass to the browser context
875
+ // UserInfo is a simple object that can be serialized
876
+ const serializedContextUser = JSON.parse(JSON.stringify(options.contextUser));
877
+ // utilities - favor the one passed in by the caller, or fall back to the local ones
878
+ const util = options.utilities || this.buildLocalMJUtilities();
879
+ // Create a lightweight mock metadata object with serializable data
880
+ // This avoids authentication/provider issues in the browser context
881
+ let entitiesData = [];
771
882
  try {
772
- if (options.waitForSelector) {
773
- await page.waitForSelector(options.waitForSelector, { timeout });
774
- }
775
- if (options.waitForLoadState) {
776
- await page.waitForLoadState(options.waitForLoadState);
883
+ // Try to get entities if available, otherwise use empty array
884
+ if (util.Entities) {
885
+ // Serialize the entities data (remove functions, keep data)
886
+ entitiesData = JSON.parse(JSON.stringify(util.Entities));
887
+ // Serialized entities for browser context
777
888
  }
778
889
  else {
779
- // Wait for React to finish rendering with configurable time
780
- await page.waitForTimeout(renderWaitTime);
781
- // Force React to flush all updates
782
- await page.evaluate(() => {
783
- if (window.React && window.React.flushSync) {
784
- try {
785
- window.React.flushSync(() => { });
786
- }
787
- catch (e) {
788
- console.error('flushSync error:', e);
789
- }
790
- }
791
- });
792
- // Additional small wait after flush to ensure DOM updates
793
- await page.waitForTimeout(50);
890
+ // Metadata.Entities not available, using empty array
794
891
  }
795
- return true;
796
892
  }
797
- catch (timeoutError) {
798
- errors.push(`Component rendering timeout after ${timeout}ms - possible infinite render loop`);
799
- return false;
893
+ catch (error) {
894
+ // Could not serialize entities
895
+ entitiesData = [];
800
896
  }
801
- }
802
- /**
803
- * Gets the render count from the page
804
- */
805
- async getRenderCount(page) {
806
- return await page.evaluate(() => window.__testHarnessRenderCount || 0);
807
- }
808
- /**
809
- * Collects runtime errors that were caught during component execution
810
- */
811
- async collectRuntimeErrors(page) {
812
- const runtimeErrors = await page.evaluate(() => {
813
- return window.__testHarnessRuntimeErrors || [];
897
+ // Create the mock metadata structure with entities and user
898
+ // Note: Don't include functions here as they can't be serialized
899
+ // Include common properties that Metadata.Provider might need
900
+ const mockMetadata = {
901
+ Entities: entitiesData,
902
+ CurrentUser: serializedContextUser,
903
+ Applications: [],
904
+ Queries: [],
905
+ QueryFields: [],
906
+ QueryCategories: [],
907
+ QueryPermissions: [],
908
+ Roles: [],
909
+ Libraries: [],
910
+ AuditLogTypes: [],
911
+ Authorizations: [],
912
+ VisibleExplorerNavigationItems: [],
913
+ AllExplorerNavigationItems: []
914
+ };
915
+ // Inject both the contextUser and mock metadata into the page
916
+ // Playwright only accepts a single argument, so wrap in an object
917
+ await page.evaluate((data) => {
918
+ const { ctxUser, mockMd } = data;
919
+ window.__mjContextUser = ctxUser;
920
+ // Add the EntityByName function directly in the browser context
921
+ mockMd.EntityByName = (name) => {
922
+ return mockMd.Entities.find((e) => e.Name === name) || null;
923
+ };
924
+ window.__mjMockMetadata = mockMd;
925
+ // IMPORTANT: Create global Metadata mock immediately to prevent undefined errors
926
+ // This must be available before any component code runs
927
+ if (!window.Metadata) {
928
+ window.Metadata = {
929
+ Provider: mockMd
930
+ };
931
+ // Created global Metadata mock with Provider (early)
932
+ }
933
+ // Received contextUser and mock metadata in browser
934
+ }, { ctxUser: serializedContextUser, mockMd: mockMetadata });
935
+ // Expose functions
936
+ await page.exposeFunction('__mjGetEntityObject', async (entityName) => {
937
+ try {
938
+ const entity = await util.GetEntityObject(entityName, options.contextUser);
939
+ return entity;
940
+ }
941
+ catch (error) {
942
+ console.error('Error in __mjGetEntityObject:', error);
943
+ return null;
944
+ }
814
945
  });
815
- const errors = [];
816
- runtimeErrors.forEach((error) => {
817
- errors.push(`${error.type} error: ${error.message}`);
818
- if (error.componentStack) {
819
- errors.push(`Component stack: ${error.componentStack}`);
946
+ await page.exposeFunction('__mjGetEntities', () => {
947
+ try {
948
+ // Return the entities array or empty array if not available
949
+ return entitiesData;
950
+ }
951
+ catch (error) {
952
+ console.error('Error in __mjGetEntities:', error);
953
+ return [];
820
954
  }
821
955
  });
822
- return errors;
823
- }
824
- /**
825
- * Captures async errors by waiting for asynchronous operations to complete
826
- * This catches errors from setTimeout, setInterval, Promises, and async effects
827
- */
828
- async captureAsyncErrors(page, waitTime) {
829
- const errors = [];
830
- try {
831
- // Clear any existing errors to track only new ones
832
- const initialErrorCount = await page.evaluate(() => {
833
- return window.__testHarnessRuntimeErrors?.length || 0;
834
- });
835
- // Wait for async operations to potentially fail
836
- await page.waitForTimeout(waitTime);
837
- // Collect any new errors that occurred during the wait
838
- const allErrors = await page.evaluate(() => {
839
- return window.__testHarnessRuntimeErrors || [];
840
- });
841
- // Process only new errors
842
- const newErrors = allErrors.slice(initialErrorCount);
843
- newErrors.forEach((error) => {
844
- if (error.type === 'promise-rejection') {
845
- errors.push(`Async operation failed: ${error.message}`);
846
- }
847
- else if (error.message && !errors.includes(`${error.type} error: ${error.message}`)) {
848
- errors.push(`Delayed ${error.type} error: ${error.message}`);
849
- }
850
- });
851
- // Also check if any console errors appeared
852
- const consoleErrors = await page.evaluate(() => {
853
- const logs = window.__testHarnessConsoleLogs || [];
854
- return logs.filter((log) => log.type === 'error').map((log) => log.text);
855
- });
856
- // Add unique console errors
857
- consoleErrors.forEach((error) => {
858
- if (!errors.some(e => e.includes(error))) {
859
- errors.push(`Console error during async wait: ${error}`);
860
- }
861
- });
862
- }
863
- catch (e) {
864
- errors.push(`Failed to capture async errors: ${e}`);
865
- }
866
- return errors;
867
- }
868
- /**
869
- * Performs deep render validation to catch errors that might be in the DOM
870
- */
871
- async validateDeepRender(page) {
872
- const errors = [];
873
- try {
874
- // Execute a full render cycle by forcing a state update
875
- await page.evaluate(() => {
876
- // Force React to complete all pending updates
877
- if (window.React && window.React.flushSync) {
878
- window.React.flushSync(() => { });
879
- }
880
- });
881
- // Check for render errors in the component tree
882
- const renderErrors = await page.evaluate(() => {
883
- const errors = [];
884
- // Walk the DOM and check for error boundaries or error text
885
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
886
- let node;
887
- while (node = walker.nextNode()) {
888
- const text = node.textContent || '';
889
- // Look for common error patterns
890
- if (text.includes('TypeError:') ||
891
- text.includes('ReferenceError:') ||
892
- text.includes('Cannot read properties of undefined') ||
893
- text.includes('Cannot access property') ||
894
- text.includes('is not a function') ||
895
- text.includes('Component Error:')) {
896
- // Only add if it's not already in our error list
897
- const errorMsg = text.trim();
898
- if (errorMsg.length < 500) { // Avoid huge text blocks
899
- errors.push(`Potential error in rendered content: ${errorMsg}`);
900
- }
956
+ await page.exposeFunction('__mjRunView', async (params) => {
957
+ try {
958
+ const result = await util.RunView(params, options.contextUser);
959
+ // Debug logging for successful calls
960
+ if (debug) {
961
+ const rowCount = result.Results?.length || 0;
962
+ console.log(`šŸ’¾ RunView SUCCESS: Entity="${params.EntityName}" Rows=${rowCount}`);
963
+ if (params.ExtraFilter) {
964
+ console.log(` Filter: ${params.ExtraFilter}`);
901
965
  }
902
966
  }
903
- return errors;
904
- });
905
- errors.push(...renderErrors);
906
- }
907
- catch (e) {
908
- errors.push(`Deep render validation failed: ${e}`);
909
- }
910
- return errors;
911
- }
912
- /**
913
- * Determines if the component execution was successful
914
- */
915
- determineSuccess(errors, criticalWarnings, renderCount, hasTimeout) {
916
- const additionalErrors = [];
917
- if (renderCount > ComponentRunner.MAX_RENDER_COUNT) {
918
- additionalErrors.push(`Excessive render count: ${renderCount} renders detected`);
919
- }
920
- const success = errors.length === 0 &&
921
- criticalWarnings.length === 0 &&
922
- !hasTimeout &&
923
- renderCount <= ComponentRunner.MAX_RENDER_COUNT;
924
- return { success, additionalErrors };
925
- }
926
- /**
927
- * Expose MemberJunction utilities to the browser context
928
- */
929
- async exposeMJUtilities(page, contextUser) {
930
- // Check if utilities are already exposed (they persist across navigations)
931
- const alreadyExposed = await page.evaluate(() => {
932
- return typeof window.__mjGetEntityObject === 'function';
933
- });
934
- if (alreadyExposed) {
935
- return; // Already exposed, skip
936
- }
937
- try {
938
- // Create instances in Node.js context
939
- const metadata = new core_1.Metadata();
940
- const runView = new core_1.RunView();
941
- const runQuery = new core_1.RunQuery();
942
- // Expose individual functions since we can't pass complex objects
943
- await page.exposeFunction('__mjGetEntityObject', async (entityName) => {
944
- try {
945
- const entity = await metadata.GetEntityObject(entityName, contextUser);
946
- return entity;
947
- }
948
- catch (error) {
949
- console.error('Error in __mjGetEntityObject:', error);
950
- return null;
967
+ return result;
968
+ }
969
+ catch (error) {
970
+ const errorMessage = error instanceof Error ? error.message : String(error);
971
+ // Debug logging for errors
972
+ if (debug) {
973
+ console.log(`āŒ RunView FAILED: Entity="${params.EntityName || 'unknown'}"`);
974
+ console.log(` Error: ${errorMessage}`);
951
975
  }
952
- });
953
- await page.exposeFunction('__mjGetEntities', () => {
954
- try {
955
- return metadata.Entities;
976
+ else {
977
+ console.error('Error in __mjRunView:', errorMessage);
956
978
  }
957
- catch (error) {
958
- console.error('Error in __mjGetEntities:', error);
959
- return null;
979
+ // Collect this error for the test report
980
+ dataErrors.push(`RunView error: ${errorMessage} (Entity: ${params.EntityName || 'unknown'})`);
981
+ // Return error result that won't crash the component
982
+ return { Success: false, ErrorMessage: errorMessage, Results: [] };
983
+ }
984
+ });
985
+ await page.exposeFunction('__mjRunViews', async (params) => {
986
+ try {
987
+ const results = await util.RunViews(params, options.contextUser);
988
+ // Debug logging for successful calls
989
+ if (debug) {
990
+ console.log(`šŸ’¾ RunViews SUCCESS: ${params.length} queries executed`);
991
+ params.forEach((p, i) => {
992
+ const rowCount = results[i]?.Results?.length || 0;
993
+ console.log(` [${i + 1}] Entity="${p.EntityName}" Rows=${rowCount}`);
994
+ });
960
995
  }
961
- });
962
- await page.exposeFunction('__mjRunView', async (params) => {
963
- try {
964
- return await runView.RunView(params, contextUser);
996
+ return results;
997
+ }
998
+ catch (error) {
999
+ const errorMessage = error instanceof Error ? error.message : String(error);
1000
+ const entities = params.map(p => p.EntityName || 'unknown').join(', ');
1001
+ // Debug logging for errors
1002
+ if (debug) {
1003
+ console.log(`āŒ RunViews FAILED: Entities=[${entities}]`);
1004
+ console.log(` Error: ${errorMessage}`);
965
1005
  }
966
- catch (error) {
967
- console.error('Error in __mjRunView:', error);
968
- const errorMessage = error instanceof Error ? error.message : String(error);
969
- return { Success: false, ErrorMessage: errorMessage, Results: [] };
1006
+ else {
1007
+ console.error('Error in __mjRunViews:', errorMessage);
970
1008
  }
971
- });
972
- await page.exposeFunction('__mjRunViews', async (params) => {
973
- try {
974
- return await runView.RunViews(params, contextUser);
1009
+ // Collect this error for the test report
1010
+ dataErrors.push(`RunViews error: ${errorMessage} (Entities: ${entities})`);
1011
+ // Return error results that won't crash the component
1012
+ return params.map(() => ({ Success: false, ErrorMessage: errorMessage, Results: [] }));
1013
+ }
1014
+ });
1015
+ await page.exposeFunction('__mjRunQuery', async (params) => {
1016
+ try {
1017
+ const result = await util.RunQuery(params, options.contextUser);
1018
+ // Debug logging for successful calls
1019
+ if (debug) {
1020
+ const queryIdentifier = params.QueryName || params.QueryID || 'unknown';
1021
+ const rowCount = result.Results?.length || 0;
1022
+ console.log(`šŸ’¾ RunQuery SUCCESS: Query="${queryIdentifier}" Rows=${rowCount}`);
1023
+ if (params.Parameters && Object.keys(params.Parameters).length > 0) {
1024
+ console.log(` Parameters:`, params.Parameters);
1025
+ }
975
1026
  }
976
- catch (error) {
977
- console.error('Error in __mjRunViews:', error);
978
- const errorMessage = error instanceof Error ? error.message : String(error);
979
- return params.map(() => ({ Success: false, ErrorMessage: errorMessage, Results: [] }));
1027
+ return result;
1028
+ }
1029
+ catch (error) {
1030
+ const errorMessage = error instanceof Error ? error.message : String(error);
1031
+ const queryIdentifier = params.QueryName || params.QueryID || 'unknown';
1032
+ // Debug logging for errors
1033
+ if (debug) {
1034
+ console.log(`āŒ RunQuery FAILED: Query="${queryIdentifier}"`);
1035
+ console.log(` Error: ${errorMessage}`);
980
1036
  }
981
- });
982
- await page.exposeFunction('__mjRunQuery', async (params) => {
983
- try {
984
- return await runQuery.RunQuery(params, contextUser);
1037
+ else {
1038
+ console.error('Error in __mjRunQuery:', errorMessage);
985
1039
  }
986
- catch (error) {
987
- console.error('Error in __mjRunQuery:', error);
988
- const errorMessage = error instanceof Error ? error.message : String(error);
989
- return { Success: false, ErrorMessage: errorMessage, Results: [] };
1040
+ // Collect this error for the test report
1041
+ dataErrors.push(`RunQuery error: ${errorMessage} (Query: ${queryIdentifier})`);
1042
+ // Return error result that won't crash the component
1043
+ return { Success: false, ErrorMessage: errorMessage, Results: [] };
1044
+ }
1045
+ });
1046
+ // Make them available in utilities with the mock metadata
1047
+ await page.evaluate(() => {
1048
+ // Use the mock metadata for synchronous access
1049
+ const mockMd = window.__mjMockMetadata || { Entities: [], CurrentUser: null };
1050
+ window.__mjUtilities = {
1051
+ md: {
1052
+ // Use the mock metadata's Entities directly (synchronous)
1053
+ Entities: mockMd.Entities,
1054
+ entities: mockMd.Entities, // Support both cases
1055
+ CurrentUser: mockMd.CurrentUser,
1056
+ EntityByName: (name) => {
1057
+ return mockMd.Entities.find((e) => e.Name === name) || null;
1058
+ },
1059
+ // Keep async function for GetEntityObject for compatibility
1060
+ GetEntityObject: async (entityName) => await window.__mjGetEntityObject(entityName)
1061
+ },
1062
+ rv: {
1063
+ RunView: async (params) => await window.__mjRunView(params),
1064
+ RunViews: async (params) => await window.__mjRunViews(params)
1065
+ },
1066
+ rq: {
1067
+ RunQuery: async (params) => await window.__mjRunQuery(params)
990
1068
  }
991
- });
992
- }
993
- catch (error) {
994
- console.error('Failed to expose MJ utilities to page:', error);
995
- // Log more details about the error
996
- if (error instanceof Error && error.message.includes('addBinding')) {
997
- console.error('addBinding error - this usually means the page context is invalid');
1069
+ };
1070
+ // Update or create global Metadata mock (in case it wasn't created earlier)
1071
+ if (!window.Metadata) {
1072
+ window.Metadata = {
1073
+ Provider: mockMd
1074
+ };
1075
+ // Created global Metadata mock with Provider (late)
998
1076
  }
999
- throw error; // Re-throw to be caught by the main error handler
1000
- }
1001
- }
1002
- /**
1003
- * Analyze component errors to identify failed components
1004
- * @param errors Array of error messages
1005
- * @returns Array of component names that failed
1006
- */
1007
- static analyzeComponentErrors(errors) {
1008
- return react_runtime_1.ComponentErrorAnalyzer.identifyFailedComponents(errors);
1009
- }
1010
- /**
1011
- * Get detailed error analysis
1012
- * @param errors Array of error messages
1013
- * @returns Detailed failure information
1014
- */
1015
- static getDetailedErrorAnalysis(errors) {
1016
- return react_runtime_1.ComponentErrorAnalyzer.analyzeComponentErrors(errors);
1077
+ else {
1078
+ // Update the existing one to ensure it has the latest mock data
1079
+ window.Metadata.Provider = mockMd;
1080
+ // Updated existing Metadata.Provider with mock data
1081
+ }
1082
+ });
1017
1083
  }
1018
1084
  /**
1019
- * Gets the ComponentCompiler code to inject into the browser
1020
- * This is a simplified version that works in the browser context
1085
+ * Dump debug information
1021
1086
  */
1022
- getComponentCompilerCode() {
1023
- // Return a browser-compatible version of ComponentCompiler
1024
- return `
1025
- class ComponentCompiler {
1026
- constructor() {
1027
- this.cache = new Map();
1028
- }
1029
-
1030
- setBabelInstance(babel) {
1031
- this.babelInstance = babel;
1032
- }
1033
-
1034
- async compile(options) {
1035
- const { componentName, componentCode } = options;
1036
-
1037
- try {
1038
- // Validate inputs
1039
- if (!componentName || !componentCode) {
1040
- throw new Error('componentName and componentCode are required');
1041
- }
1042
-
1043
- // Wrap component code
1044
- const wrappedCode = this.wrapComponentCode(componentCode, componentName);
1045
-
1046
- // Transform using Babel
1047
- const result = this.babelInstance.transform(wrappedCode, {
1048
- presets: ['react'],
1049
- filename: componentName + '.jsx'
1050
- });
1051
-
1052
- // Create factory
1053
- const componentFactory = this.createComponentFactory(result.code, componentName);
1054
-
1055
- return {
1056
- success: true,
1057
- component: {
1058
- component: componentFactory,
1059
- id: componentName + '_' + Date.now(),
1060
- name: componentName,
1061
- compiledAt: new Date(),
1062
- warnings: []
1063
- },
1064
- duration: 0
1065
- };
1066
- } catch (error) {
1067
- return {
1068
- success: false,
1069
- error: {
1070
- message: error.message,
1071
- componentName: componentName,
1072
- phase: 'compilation'
1073
- },
1074
- duration: 0
1075
- };
1087
+ dumpDebugInfo(result) {
1088
+ console.log('\nšŸ“Š === Component Execution Results ===');
1089
+ console.log('Success:', result.success ? 'āœ…' : 'āŒ');
1090
+ console.log('Execution time:', result.executionTime + 'ms');
1091
+ console.log('Render count:', result.renderCount);
1092
+ if (result.errors && result.errors.length > 0) {
1093
+ console.log('\nāŒ Errors:', result.errors.length);
1094
+ result.errors.forEach((err, i) => {
1095
+ console.log(` ${i + 1}. ${err.message}`);
1096
+ });
1076
1097
  }
1077
- }
1078
-
1079
- wrapComponentCode(componentCode, componentName) {
1080
- // Make component libraries available in scope
1081
- const libraryDeclarations = componentLibraries
1082
- .map(lib => \`const \${lib.globalVariable} = libraries['\${lib.globalVariable}'];\`)
1083
- .join('\\n ');
1084
-
1085
- return \`
1086
- function createComponent(
1087
- React, ReactDOM,
1088
- useState, useEffect, useCallback, useMemo, useRef, useContext, useReducer, useLayoutEffect,
1089
- libraries, styles, console
1090
- ) {
1091
- \${libraryDeclarations}
1092
-
1093
- \${componentCode}
1094
-
1095
- if (typeof \${componentName} === 'undefined') {
1096
- throw new Error('Component "\${componentName}" is not defined in the provided code');
1097
- }
1098
-
1099
- return {
1100
- component: \${componentName},
1101
- print: function() { window.print(); },
1102
- refresh: function(data) { }
1103
- };
1104
- }
1105
- \`;
1106
- }
1107
-
1108
- createComponentFactory(transpiledCode, componentName) {
1109
- const factoryCreator = new Function(
1110
- 'React', 'ReactDOM',
1111
- 'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext', 'useReducer', 'useLayoutEffect',
1112
- 'libraries', 'styles', 'console',
1113
- transpiledCode + '; return createComponent;'
1114
- );
1115
-
1116
- return (context, styles = {}) => {
1117
- const { React, ReactDOM, libraries = {} } = context;
1118
-
1119
- const createComponentFn = factoryCreator(
1120
- React, ReactDOM,
1121
- React.useState, React.useEffect, React.useCallback, React.useMemo,
1122
- React.useRef, React.useContext, React.useReducer, React.useLayoutEffect,
1123
- libraries, styles, console
1124
- );
1125
-
1126
- return createComponentFn(
1127
- React, ReactDOM,
1128
- React.useState, React.useEffect, React.useCallback, React.useMemo,
1129
- React.useRef, React.useContext, React.useReducer, React.useLayoutEffect,
1130
- libraries, styles, console
1131
- );
1132
- };
1133
- }
1134
- }
1135
- `;
1098
+ if (result.warnings && result.warnings.length > 0) {
1099
+ console.log('\nāš ļø Warnings:', result.warnings.length);
1100
+ result.warnings.forEach((warn, i) => {
1101
+ console.log(` ${i + 1}. ${warn.message}`);
1102
+ });
1103
+ }
1104
+ if (result.criticalWarnings && result.criticalWarnings.length > 0) {
1105
+ console.log('\nšŸ”“ Critical Warnings:', result.criticalWarnings.length);
1106
+ result.criticalWarnings.forEach((warn, i) => {
1107
+ console.log(` ${i + 1}. ${warn}`);
1108
+ });
1109
+ }
1110
+ console.log('\n========================================\n');
1136
1111
  }
1137
1112
  }
1138
1113
  exports.ComponentRunner = ComponentRunner;
@@ -1147,6 +1122,8 @@ ComponentRunner.CRITICAL_WARNING_PATTERNS = [
1147
1122
  /Error: Minified React error/i,
1148
1123
  /too many re-renders/i,
1149
1124
  ];
1150
- // Maximum allowed renders before considering it excessive
1151
- ComponentRunner.MAX_RENDER_COUNT = 1000;
1125
+ // Note: This counts React.createElement calls, not component re-renders
1126
+ // A complex dashboard can easily have 5000+ createElement calls on initial mount
1127
+ // Only flag if it's likely an infinite loop (10000+ is suspicious)
1128
+ ComponentRunner.MAX_RENDER_COUNT = 10000;
1152
1129
  //# sourceMappingURL=component-runner.js.map