@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
336 lines • 12.1 kB
JavaScript
/**
* @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