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