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