@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
358 lines (356 loc) • 13.2 kB
JavaScript
/**
* @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