UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

358 lines (356 loc) 13.2 kB
/** * @fileoverview Component Testing Utilities */ /** * Utilities for testing OrdoJS components */ export class ComponentTestUtils { componentInstances = new Map(); /** * Create a component instance from generated code */ async createComponentInstance(generatedCode, props = {}, initialState = {}) { try { // Create a safe execution context const context = this.createExecutionContext(); // Execute the generated client code const componentFactory = this.executeGeneratedCode(generatedCode.client, context); // Create component instance const instance = new componentFactory(props); // Set initial state if provided if (Object.keys(initialState).length > 0) { for (const [key, value] of Object.entries(initialState)) { instance.setState?.(key, value); } } // Store instance for cleanup const instanceId = this.generateInstanceId(); this.componentInstances.set(instanceId, instance); // Add testing utilities to instance this.addTestingUtilities(instance, instanceId); return instance; } catch (error) { throw new Error(`Failed to create component instance: ${error instanceof Error ? error.message : String(error)}`); } } /** * Create execution context for generated code */ createExecutionContext() { return { // DOM APIs document: global.document, window: global.window, console: global.console, // Utility functions that might be used in generated code createElement: (tagName) => document.createElement(tagName), createTextNode: (text) => document.createTextNode(text), querySelector: (selector) => document.querySelector(selector), querySelectorAll: (selector) => document.querySelectorAll(selector), // Event handling addEventListener: (element, event, handler) => { element.addEventListener(event, handler); }, removeEventListener: (element, event, handler) => { element.removeEventListener(event, handler); }, // Reactive system utilities createReactiveVariable: (initialValue) => { return { value: initialValue, subscribers: new Set(), get: function () { return this.value; }, set: function (newValue) { if (this.value !== newValue) { this.value = newValue; this.subscribers.forEach((callback) => callback(newValue)); } }, subscribe: function (callback) { this.subscribers.add(callback); return () => this.subscribers.delete(callback); } }; }, // Batch updates batchUpdates: (fn) => { // Simple batching implementation const updates = []; const originalSetState = this.setState; this.setState = (key, value) => { updates.push(() => originalSetState.call(this, key, value)); }; fn(); // Execute all updates updates.forEach(update => update()); this.setState = originalSetState; } }; } /** * Execute generated code in a safe context */ executeGeneratedCode(code, context) { try { // Extract component name from the code const componentMatch = code.match(/function\s+(\w+)\s*\(/); const componentName = componentMatch ? componentMatch[1] : 'Component'; // Create a safer execution approach const wrappedCode = ` (function(context) { const { document, window, console, createElement, createTextNode } = context; ${code} // Return the component constructor return ${componentName}; }) `; const factory = eval(wrappedCode); return factory(context); } catch (error) { // If execution fails, return a simple mock component return function MockComponent(props = {}) { return { mount: function (target) { target.innerHTML = '<div>Mock Component</div>'; }, unmount: function () { }, getState: function () { return {}; }, setState: function (key, value) { } }; }; } } /** * Add testing utilities to component instance */ addTestingUtilities(instance, instanceId) { // Add state management utilities instance._testingState = {}; instance._testingInstanceId = instanceId; // Add getState method if not present if (!instance.getState) { instance.getState = () => ({ ...instance._testingState }); } // Add setState method if not present if (!instance.setState) { instance.setState = (key, value) => { instance._testingState[key] = value; // Trigger reactivity if available if (instance._reactiveVariables && instance._reactiveVariables[key]) { instance._reactiveVariables[key].set(value); } }; } // Add batch update utilities instance.startBatch = () => { instance._batchMode = true; instance._batchedUpdates = []; }; instance.endBatch = () => { if (instance._batchMode && instance._batchedUpdates) { instance._batchedUpdates.forEach((update) => update()); instance._batchedUpdates = []; instance._batchMode = false; } }; // Add dependency tracking utilities instance.getDependents = (variable) => { return instance._dependencyGraph?.[variable] || []; }; instance.getCircularDependencies = () => { return instance._circularDependencies || []; }; // Add DOM utilities instance.getElement = () => { return document.querySelector(`[data-component-id="${instanceId}"]`); }; instance.getAllElements = () => { return document.querySelectorAll(`[data-component-id="${instanceId}"] *`); }; // Add event simulation utilities instance.simulateEvent = (selector, eventType, eventData = {}) => { const element = document.querySelector(selector); if (element) { const event = new Event(eventType, { bubbles: true, cancelable: true }); Object.assign(event, eventData); element.dispatchEvent(event); } }; // Add cleanup utilities instance.cleanup = () => { this.componentInstances.delete(instanceId); // Remove from DOM if mounted const element = instance.getElement(); if (element) { element.remove(); } }; } /** * Render component to HTML string */ async renderComponent(instance, props = {}) { try { // Create a container element const container = document.createElement('div'); container.setAttribute('data-component-id', instance._testingInstanceId); // Mount component to container if (instance.mount) { await instance.mount(container, props); } else if (instance.render) { const rendered = await instance.render(props); if (typeof rendered === 'string') { container.innerHTML = rendered; } else if (rendered instanceof Element) { container.appendChild(rendered); } } return container.outerHTML; } catch (error) { throw new Error(`Failed to render component: ${error instanceof Error ? error.message : String(error)}`); } } /** * Mount component to DOM element */ async mountComponent(instance, target, props = {}) { try { target.setAttribute('data-component-id', instance._testingInstanceId); if (instance.mount) { await instance.mount(target, props); } else if (instance.render) { const rendered = await instance.render(props); if (typeof rendered === 'string') { target.innerHTML = rendered; } else if (rendered instanceof Element) { target.appendChild(rendered); } } else { throw new Error('Component has no mount or render method'); } } catch (error) { throw new Error(`Failed to mount component: ${error instanceof Error ? error.message : String(error)}`); } } /** * Unmount component from DOM */ async unmountComponent(instance) { try { if (instance.unmount) { await instance.unmount(); } // Remove from DOM const element = instance.getElement(); if (element) { element.remove(); } // Cleanup instance instance.cleanup?.(); } catch (error) { throw new Error(`Failed to unmount component: ${error instanceof Error ? error.message : String(error)}`); } } /** * Generate unique instance ID */ generateInstanceId() { return `component_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; } /** * Get component instance by ID */ getComponentInstance(instanceId) { return this.componentInstances.get(instanceId); } /** * Get all component instances */ getAllComponentInstances() { return Array.from(this.componentInstances.values()); } /** * Cleanup all component instances */ cleanupAllInstances() { for (const instance of this.componentInstances.values()) { try { instance.cleanup?.(); } catch (error) { console.warn('Error during instance cleanup:', error); } } this.componentInstances.clear(); } /** * Wait for component to be ready */ async waitForComponent(instance, timeout = 5000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { if (instance.isReady?.() || instance.getElement()) { return; } await new Promise(resolve => setTimeout(resolve, 10)); } throw new Error('Component did not become ready within timeout'); } /** * Wait for DOM updates to complete */ async waitForDOMUpdates(timeout = 100) { return new Promise(resolve => { setTimeout(resolve, timeout); }); } /** * Create a test component with minimal setup */ createTestComponent(name, template, state = {}) { const instanceId = this.generateInstanceId(); const component = { name, _testingInstanceId: instanceId, _testingState: { ...state }, getState: function () { return { ...this._testingState }; }, setState: function (key, value) { this._testingState[key] = value; }, render: function (props = {}) { // Simple template rendering let html = template; // Replace state variables for (const [key, value] of Object.entries(this._testingState)) { html = html.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value)); } // Replace props for (const [key, value] of Object.entries(props)) { html = html.replace(new RegExp(`\\{props\\.${key}\\}`, 'g'), String(value)); } return html; }, mount: function (target, props = {}) { const html = this.render(props); target.innerHTML = html; target.setAttribute('data-component-id', this._testingInstanceId); } }; this.addTestingUtilities(component, instanceId); this.componentInstances.set(instanceId, component); return component; } } //# sourceMappingURL=component-utils.js.map