UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

336 lines 12.1 kB
/** * @fileoverview Reactive Testing Utilities */ /** * Utilities for testing reactive behavior */ export class ReactiveTestUtils { domUpdateRecords = []; stateChangeListeners = new Map(); /** * Execute a reactive action on a component instance */ async executeAction(componentInstance, action) { const timestamp = Date.now(); switch (action.type) { case 'set': if (action.variable && action.value !== undefined) { const oldValue = componentInstance.getState?.()?.[action.variable]; componentInstance.setState?.(action.variable, action.value); this.recordDOMUpdate({ timestamp, type: 'set', target: action.variable, oldValue, newValue: action.value, batched: false }); } break; case 'increment': if (action.variable) { const currentValue = componentInstance.getState?.()?.[action.variable] || 0; const newValue = currentValue + (action.value || 1); componentInstance.setState?.(action.variable, newValue); this.recordDOMUpdate({ timestamp, type: 'increment', target: action.variable, oldValue: currentValue, newValue, batched: false }); } break; case 'decrement': if (action.variable) { const currentValue = componentInstance.getState?.()?.[action.variable] || 0; const newValue = currentValue - (action.value || 1); componentInstance.setState?.(action.variable, newValue); this.recordDOMUpdate({ timestamp, type: 'decrement', target: action.variable, oldValue: currentValue, newValue, batched: false }); } break; case 'call': if (action.function) { const result = await componentInstance[action.function]?.(...(action.args || [])); this.recordDOMUpdate({ timestamp, type: 'call', target: action.function, oldValue: undefined, newValue: result, batched: false }); } break; case 'event': if (action.event) { await this.simulateEvent(componentInstance, action.event); } break; default: throw new Error(`Unknown action type: ${action.type}`); } // Allow for async state updates await this.waitForStateUpdates(); } /** * Simulate DOM event */ async simulateEvent(componentInstance, event) { const timestamp = Date.now(); // Find the target element const element = document.querySelector(event.target); if (!element) { throw new Error(`Element not found: ${event.target}`); } // Create and dispatch event const domEvent = new Event(event.type, { bubbles: true, cancelable: true }); if (event.data) { Object.assign(domEvent, event.data); } element.dispatchEvent(domEvent); this.recordDOMUpdate({ timestamp, type: 'event', target: event.target, oldValue: undefined, newValue: event.type, batched: false }); } /** * Wait for async state updates to complete */ async waitForStateUpdates(timeout = 100) { return new Promise(resolve => { setTimeout(resolve, timeout); }); } /** * Record DOM update for testing */ recordDOMUpdate(update) { this.domUpdateRecords.push(update); } /** * Compare two state objects */ compareStates(actual, expected) { const actualKeys = Object.keys(actual).sort(); const expectedKeys = Object.keys(expected).sort(); // Check if keys match if (actualKeys.length !== expectedKeys.length) { return false; } for (let i = 0; i < actualKeys.length; i++) { if (actualKeys[i] !== expectedKeys[i]) { return false; } } // Check if values match for (const key of actualKeys) { if (!this.deepEqual(actual[key], expected[key])) { return false; } } return true; } /** * Deep equality check */ deepEqual(a, b) { if (a === b) return true; if (a == null || b == null) return a === b; if (typeof a !== typeof b) return false; if (typeof a === 'object') { if (Array.isArray(a) !== Array.isArray(b)) return false; if (Array.isArray(a)) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!this.deepEqual(a[i], b[i])) return false; } return true; } const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!keysB.includes(key)) return false; if (!this.deepEqual(a[key], b[key])) return false; } return true; } return false; } /** * Verify DOM updates match expectations */ verifyDOMUpdates(expectations) { for (const expectation of expectations) { const element = document.querySelector(expectation.selector); if (!element) { console.error(`Element not found: ${expectation.selector}`); return false; } let actualValue; switch (expectation.updateType) { case 'textContent': actualValue = element.textContent; break; case 'attribute': actualValue = element.getAttribute(expectation.property); break; case 'style': actualValue = element.style.getPropertyValue(expectation.property); break; case 'class': actualValue = element.classList.contains(expectation.expectedValue); break; default: console.error(`Unknown update type: ${expectation.updateType}`); return false; } if (!this.deepEqual(actualValue, expectation.expectedValue)) { console.error(`DOM update mismatch for ${expectation.selector}.${expectation.property}. Expected: ${expectation.expectedValue}, Got: ${actualValue}`); return false; } } return true; } /** * Get recorded DOM updates */ getDOMUpdateRecords() { return [...this.domUpdateRecords]; } /** * Clear DOM update records */ clearDOMUpdateRecords() { this.domUpdateRecords = []; } /** * Create a reactive test case */ createTestCase(name, initialState, actions, expectedState, expectedDOMUpdates) { return { name, initialState, actions, expectedState, expectedDOMUpdates }; } /** * Run a reactive test case */ async runTestCase(componentInstance, testCase) { const errors = []; try { // Reset component state first if (componentInstance._state) { componentInstance._state = {}; } // Set initial state for (const [key, value] of Object.entries(testCase.initialState)) { componentInstance.setState?.(key, value); } // Execute actions for (const action of testCase.actions) { await this.executeAction(componentInstance, action); } // Verify final state const finalState = componentInstance.getState?.() || {}; // Only compare the keys that are expected const filteredFinalState = {}; for (const key of Object.keys(testCase.expectedState)) { filteredFinalState[key] = finalState[key]; } if (!this.compareStates(filteredFinalState, testCase.expectedState)) { errors.push(`State mismatch. Expected: ${JSON.stringify(testCase.expectedState)}, Got: ${JSON.stringify(filteredFinalState)}`); } // Verify DOM updates if specified if (testCase.expectedDOMUpdates) { if (!this.verifyDOMUpdates(testCase.expectedDOMUpdates)) { errors.push('DOM updates did not match expectations'); } } return { success: errors.length === 0, errors }; } catch (error) { errors.push(error instanceof Error ? error.message : String(error)); return { success: false, errors }; } } /** * Batch multiple reactive actions */ async executeBatchedActions(componentInstance, actions) { const timestamp = Date.now(); // Start batch componentInstance.startBatch?.(); try { for (const action of actions) { await this.executeAction(componentInstance, action); } } finally { // End batch componentInstance.endBatch?.(); // Record batched update this.recordDOMUpdate({ timestamp, type: 'batch', target: 'multiple', oldValue: undefined, newValue: actions.length, batched: true }); } } /** * Test reactive dependency tracking */ testDependencyTracking(componentInstance, variable, expectedDependents) { const dependents = componentInstance.getDependents?.(variable) || []; const dependentNames = dependents.map((dep) => dep.name || dep); return expectedDependents.every(expected => dependentNames.includes(expected)); } /** * Test circular dependency detection */ testCircularDependencyDetection(componentInstance, expectedCircularDeps) { const circularDeps = componentInstance.getCircularDependencies?.() || []; if (circularDeps.length !== expectedCircularDeps.length) { return false; } return expectedCircularDeps.every(expectedCycle => circularDeps.some((actualCycle) => { if (expectedCycle.length !== actualCycle.length) return false; // Check if all elements in expected cycle are in actual cycle // This handles cycles that might be in different order (e.g., [a,b,c,a] vs [b,c,a,b]) const expectedSet = new Set(expectedCycle); const actualSet = new Set(actualCycle); return expectedSet.size === actualSet.size && [...expectedSet].every(dep => actualSet.has(dep)); })); } } //# sourceMappingURL=reactive-utils.js.map