@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.
- package/README.md +98 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/browser-context.d.ts.map +1 -1
- package/dist/lib/browser-context.js +6 -2
- package/dist/lib/browser-context.js.map +1 -1
- package/dist/lib/component-linter.d.ts +1 -0
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +2620 -663
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts +19 -49
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +925 -745
- package/dist/lib/component-runner.js.map +1 -1
- package/dist/lib/test-harness.d.ts +32 -0
- package/dist/lib/test-harness.d.ts.map +1 -1
- package/dist/lib/test-harness.js +52 -0
- package/dist/lib/test-harness.js.map +1 -1
- package/package.json +3 -3
|
@@ -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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
49
|
-
// Collect
|
|
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
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
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
|
|
57
|
-
// Take screenshot
|
|
58
|
-
const screenshot = await
|
|
59
|
-
// Determine success
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
489
|
+
// Combine runtime errors with data errors
|
|
490
|
+
const allErrors = [...errors, ...dataErrors];
|
|
491
|
+
const result = {
|
|
88
492
|
success: false,
|
|
89
493
|
html: '',
|
|
90
|
-
errors:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
*
|
|
535
|
+
* Load runtime libraries into the page
|
|
527
536
|
*/
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
*
|
|
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
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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 (
|
|
580
|
-
|
|
611
|
+
if (debug) {
|
|
612
|
+
console.log(`š¦ Loading ${specLib.name}:`, {
|
|
613
|
+
cdnUrl: libDef.CDNUrl,
|
|
614
|
+
globalVariable: libDef.GlobalVariable
|
|
615
|
+
});
|
|
581
616
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
592
|
-
|
|
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
|
-
|
|
597
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
*
|
|
686
|
+
* Set up error tracking in the page
|
|
608
687
|
*/
|
|
609
|
-
async
|
|
610
|
-
|
|
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
|
-
*
|
|
752
|
+
* Collect runtime errors from the page
|
|
614
753
|
*/
|
|
615
754
|
async collectRuntimeErrors(page) {
|
|
616
|
-
const
|
|
617
|
-
return
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
*
|
|
782
|
+
* Collect warnings from the page (non-fatal issues)
|
|
630
783
|
*/
|
|
631
|
-
async
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
*
|
|
799
|
+
* Set up console logging
|
|
674
800
|
*/
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
|
823
|
+
* Expose MJ utilities to the browser context
|
|
688
824
|
*/
|
|
689
|
-
async exposeMJUtilities(page, contextUser) {
|
|
690
|
-
//
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
909
|
+
return [];
|
|
719
910
|
}
|
|
720
911
|
});
|
|
721
912
|
await page.exposeFunction('__mjRunView', async (params) => {
|
|
722
913
|
try {
|
|
723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|