UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

386 lines (310 loc) 10.4 kB
/** * @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); }); });