@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
386 lines (310 loc) • 10.4 kB
text/typescript
/**
* @fileoverview Tests for OrdoJS Runtime and Hydration
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { OrdoJSHydrator, OrdoJSRuntime } from './index.js';
// Mock DOM environment
const mockElement = {
getAttribute: vi.fn(),
setAttribute: vi.fn(),
removeAttribute: vi.fn(),
querySelector: vi.fn(),
querySelectorAll: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
textContent: '',
id: 'test-element'
};
const mockDocument = {
readyState: 'complete',
addEventListener: vi.fn(),
getElementById: vi.fn(),
querySelector: vi.fn(),
querySelectorAll: vi.fn()
};
// Mock global document
Object.defineProperty(global, 'document', {
value: mockDocument,
writable: true
});
describe('OrdoJSRuntime', () => {
let runtime: OrdoJSRuntime;
beforeEach(() => {
// Reset singleton instance
(OrdoJSRuntime as any).instance = undefined;
runtime = OrdoJSRuntime.getInstance();
// Reset mocks
vi.clearAllMocks();
});
afterEach(() => {
runtime.unmountAll();
});
it('should be a singleton', () => {
const runtime1 = OrdoJSRuntime.getInstance();
const runtime2 = OrdoJSRuntime.getInstance();
expect(runtime1).toBe(runtime2);
});
it('should register component constructors', () => {
const mockConstructor = vi.fn();
runtime.registerComponent('TestComponent', mockConstructor);
// Access private property for testing
const constructors = (runtime as any).componentConstructors;
expect(constructors.has('TestComponent')).toBe(true);
expect(constructors.get('TestComponent')).toBe(mockConstructor);
});
it('should initialize and auto-hydrate on DOM ready', () => {
mockDocument.readyState = 'loading';
mockDocument.querySelectorAll.mockReturnValue([]);
mockDocument.getElementById.mockReturnValue(null);
runtime.init();
expect(mockDocument.addEventListener).toHaveBeenCalledWith(
'DOMContentLoaded',
expect.any(Function)
);
});
it('should auto-hydrate immediately if DOM is already ready', () => {
mockDocument.readyState = 'complete';
mockDocument.querySelectorAll.mockReturnValue([]);
mockDocument.getElementById.mockReturnValue(null);
const autoHydrateSpy = vi.spyOn(runtime as any, 'autoHydrate');
runtime.init();
expect(autoHydrateSpy).toHaveBeenCalled();
});
it('should hydrate components from hydration data', () => {
const hydrationData = {
componentName: 'TestComponent',
componentId: 'test-123',
props: { name: 'Test' },
initialState: { count: 0 },
version: '1.0'
};
const mockHydrationScript = {
textContent: JSON.stringify(hydrationData)
};
const mockComponentElement = {
...mockElement,
getAttribute: vi.fn((attr) => {
if (attr === 'data-ordojs-component') return 'TestComponent';
if (attr === 'data-component-id') return 'test-123';
return null;
}),
querySelectorAll: vi.fn().mockReturnValue([])
};
mockDocument.getElementById.mockReturnValue(mockHydrationScript);
mockDocument.querySelector.mockReturnValue(mockComponentElement);
mockDocument.querySelectorAll.mockReturnValue([]);
const mockConstructor = vi.fn().mockReturnValue({
id: '',
name: '',
element: null,
props: {},
state: {},
eventListeners: new Map(),
update: vi.fn(),
unmount: vi.fn()
});
runtime.registerComponent('TestComponent', mockConstructor);
runtime.autoHydrate();
expect(mockConstructor).toHaveBeenCalledWith({ name: 'Test' });
expect(mockComponentElement.setAttribute).toHaveBeenCalledWith('data-ordojs-hydrated', 'true');
});
it('should extract props from element attributes', () => {
const mockComponentElement = {
...mockElement,
getAttribute: vi.fn((attr) => {
if (attr === 'data-props') return '{"title":"Hello","count":42}';
return null;
})
};
const props = (runtime as any).extractProps(mockComponentElement);
expect(props).toEqual({ title: 'Hello', count: 42 });
});
it('should handle malformed props gracefully', () => {
const mockComponentElement = {
...mockElement,
getAttribute: vi.fn((attr) => {
if (attr === 'data-props') return 'invalid-json';
return null;
})
};
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const props = (runtime as any).extractProps(mockComponentElement);
expect(props).toEqual({});
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to parse props from data-props attribute:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
it('should initialize reactive state', () => {
const mockComponent = {
id: 'test-123',
name: 'TestComponent',
element: mockElement,
props: {},
state: {},
eventListeners: new Map(),
update: vi.fn(),
unmount: vi.fn()
};
const initialState = { count: 5, name: 'Test' };
(runtime as any).initializeReactiveState(mockComponent, initialState);
expect(mockComponent.state.count).toBe(5);
expect(mockComponent.state.name).toBe('Test');
// Test reactivity
mockComponent.state.count = 10;
expect(mockComponent.update).toHaveBeenCalled();
});
it('should attach event listeners', () => {
const mockEventElement = {
...mockElement,
getAttribute: vi.fn((attr) => {
if (attr === 'data-event') return 'click';
return null;
}),
addEventListener: vi.fn()
};
const mockComponent = {
id: 'test-123',
name: 'TestComponent',
element: mockElement,
props: {},
state: {},
eventListeners: new Map(),
update: vi.fn(),
unmount: vi.fn(),
handleClick: vi.fn()
};
mockElement.querySelectorAll.mockReturnValue([mockEventElement]);
(runtime as any).attachEventListeners(mockElement, mockComponent);
expect(mockEventElement.addEventListener).toHaveBeenCalledWith(
'click',
expect.any(Function)
);
});
it('should set up interpolation updates', () => {
const mockInterpolationElement = {
...mockElement,
getAttribute: vi.fn((attr) => {
if (attr === 'data-interpolation') return 'message';
return null;
}),
textContent: ''
};
const mockComponent = {
id: 'test-123',
name: 'TestComponent',
element: mockElement,
props: {},
state: { message: 'Hello' },
eventListeners: new Map(),
update: vi.fn(),
unmount: vi.fn()
};
mockElement.querySelectorAll.mockReturnValue([mockInterpolationElement]);
(runtime as any).setupInterpolationUpdates(mockElement, mockComponent);
// Trigger update
mockComponent.update();
expect(mockInterpolationElement.textContent).toBe('Hello');
});
it('should get hydrated components', () => {
const mockComponent = {
id: 'test-123',
name: 'TestComponent',
element: mockElement,
props: {},
state: {},
eventListeners: new Map(),
update: vi.fn(),
unmount: vi.fn()
};
// Access private property for testing
const components = (runtime as any).hydratedComponents;
components.set('test-123', mockComponent);
expect(runtime.getComponent('test-123')).toBe(mockComponent);
expect(runtime.getAllComponents()).toEqual([mockComponent]);
});
it('should unmount components', () => {
const mockComponent = {
id: 'test-123',
name: 'TestComponent',
element: mockElement,
props: {},
state: {},
eventListeners: new Map([['element-click', vi.fn()]]),
update: vi.fn(),
unmount: vi.fn()
};
// Access private property for testing
const components = (runtime as any).hydratedComponents;
components.set('test-123', mockComponent);
runtime.unmountComponent('test-123');
expect(mockComponent.unmount).toHaveBeenCalled();
expect(mockElement.removeAttribute).toHaveBeenCalledWith('data-ordojs-hydrated');
expect(runtime.getComponent('test-123')).toBeUndefined();
});
});
describe('OrdoJSHydrator', () => {
let hydrator: OrdoJSHydrator;
let runtime: OrdoJSRuntime;
beforeEach(() => {
// Reset singleton instance
(OrdoJSRuntime as any).instance = undefined;
runtime = OrdoJSRuntime.getInstance();
hydrator = new OrdoJSHydrator();
// Reset mocks
vi.clearAllMocks();
});
it('should hydrate a component with component data', () => {
const mockComponentElement = {
...mockElement,
getAttribute: vi.fn((attr) => {
if (attr === 'data-ordojs-component') return 'TestComponent';
if (attr === 'data-component-id') return 'test-123';
return null;
})
};
const componentData = {
props: { name: 'Test' },
state: { count: 0 }
};
const mockConstructor = vi.fn().mockReturnValue({
id: '',
name: '',
element: null,
props: {},
state: {},
eventListeners: new Map(),
update: vi.fn(),
unmount: vi.fn()
});
runtime.registerComponent('TestComponent', mockConstructor);
const result = hydrator.hydrateComponent(mockComponentElement as any, componentData);
expect(result).toBeTruthy();
expect(mockConstructor).toHaveBeenCalledWith({ name: 'Test' });
});
it('should warn when element is missing hydration attributes', () => {
const mockComponentElement = {
...mockElement,
getAttribute: vi.fn(() => null)
};
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const result = hydrator.hydrateComponent(mockComponentElement as any, { props: {} });
expect(result).toBeNull();
expect(consoleSpy).toHaveBeenCalledWith('Element missing required hydration attributes');
consoleSpy.mockRestore();
});
it('should attach event listeners manually', () => {
const mockEventElement = {
...mockElement,
addEventListener: vi.fn()
};
mockElement.querySelectorAll.mockReturnValue([mockEventElement]);
const handlers = {
click: vi.fn(),
submit: vi.fn()
};
hydrator.attachEventListeners(mockElement as any, handlers);
expect(mockEventElement.addEventListener).toHaveBeenCalledWith('click', handlers.click);
});
});