UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

277 lines (234 loc) 9.32 kB
/** * @fileoverview Integration tests for SSR and Hydration */ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { OrdoJSRuntime } from '../runtime/index.js'; import { OrdoJSSSR } from './ssr-engine.js'; // Mock DOM environment const mockDocument = { readyState: 'complete', addEventListener: vi.fn(), getElementById: vi.fn(), querySelector: vi.fn(), querySelectorAll: vi.fn() }; Object.defineProperty(global, 'document', { value: mockDocument, writable: true }); describe('SSR and Hydration Integration', () => { let ssr: OrdoJSSSR; let runtime: OrdoJSRuntime; // Mock component AST const mockAST: any = { component: { name: 'CounterComponent', props: [ { name: 'initialCount', isRequired: false, defaultValue: { expressionType: 'LITERAL', value: 0 } } ], clientBlock: { reactiveVariables: [ { name: 'count', initialValue: { expressionType: 'LITERAL', value: 0 }, dataType: { name: 'number', isArray: false, isOptional: false, genericTypes: [] } } ] }, serverBlock: { functions: [ { name: 'getServerSideProps', parameters: [], body: [], returnType: { name: 'object', isArray: false, isOptional: false, genericTypes: [] }, isAsync: true, isPublic: true, middleware: [], permissions: [] } ] }, markupBlock: { elements: [], textNodes: [], interpolations: [] } }, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; beforeEach(() => { // Reset singleton instance (OrdoJSRuntime as any).instance = undefined; ssr = new OrdoJSSSR({ includeHydrationMarkers: true, includeHydrationData: true }); runtime = OrdoJSRuntime.getInstance(); // Reset mocks vi.clearAllMocks(); }); it('should render component on server and prepare for hydration', async () => { // Register component for SSR ssr.registerComponent(mockAST); // Render component with initial props const serverHtml = await ssr.renderComponent('CounterComponent', { initialCount: 5 }); // Verify server-rendered HTML contains hydration markers expect(serverHtml).toContain('data-ordojs-component="CounterComponent"'); expect(serverHtml).toContain('data-component-id'); expect(serverHtml).toContain('<script type="application/json" id="ordojs-hydration-data">'); // Parse hydration data from HTML const hydrationDataMatch = serverHtml.match(/<script type="application\/json" id="ordojs-hydration-data">\s*([\s\S]*?)\s*<\/script>/); expect(hydrationDataMatch).toBeTruthy(); if (hydrationDataMatch) { const hydrationData = JSON.parse(hydrationDataMatch[1]); expect(hydrationData.componentName).toBe('CounterComponent'); expect(hydrationData.props.initialCount).toBe(5); expect(hydrationData.initialState.count).toBe(0); } }); it('should hydrate server-rendered component on client', async () => { // Register component for SSR ssr.registerComponent(mockAST); // Render component on server const serverHtml = await ssr.renderComponent('CounterComponent', { initialCount: 10 }); // Extract hydration data const hydrationDataMatch = serverHtml.match(/<script type="application\/json" id="ordojs-hydration-data">\s*([\s\S]*?)\s*<\/script>/); const hydrationData = JSON.parse(hydrationDataMatch![1]); // Mock DOM elements for client-side hydration const mockHydrationScript = { textContent: JSON.stringify(hydrationData) }; const mockComponentElement = { getAttribute: vi.fn((attr) => { if (attr === 'data-ordojs-component') return 'CounterComponent'; if (attr === 'data-component-id') return hydrationData.componentId; return null; }), setAttribute: vi.fn(), querySelectorAll: vi.fn().mockReturnValue([]) }; mockDocument.getElementById.mockReturnValue(mockHydrationScript); mockDocument.querySelector.mockReturnValue(mockComponentElement); mockDocument.querySelectorAll.mockReturnValue([]); // Mock component constructor for client-side const mockConstructor = vi.fn().mockReturnValue({ id: hydrationData.componentId, name: 'CounterComponent', element: mockComponentElement, props: hydrationData.props, state: {}, eventListeners: new Map(), update: vi.fn(), unmount: vi.fn() }); // Register component constructor for hydration runtime.registerComponent('CounterComponent', mockConstructor); // Perform client-side hydration runtime.autoHydrate(); // Verify component was hydrated with correct props expect(mockConstructor).toHaveBeenCalledWith({ initialCount: 10 }); expect(mockComponentElement.setAttribute).toHaveBeenCalledWith('data-ordojs-hydrated', 'true'); // Verify component is registered in runtime const hydratedComponent = runtime.getComponent(hydrationData.componentId); expect(hydratedComponent).toBeTruthy(); expect(hydratedComponent?.name).toBe('CounterComponent'); }); it('should handle route-based SSR with hydration', async () => { // Configure SSR with routes const ssrWithRoutes = new OrdoJSSSR({ includeHydrationMarkers: true, includeHydrationData: true, routes: [ { path: '/counter/:initialValue', component: 'CounterComponent', dataFetcher: async (params) => { return { initialCount: parseInt(params.initialValue, 10), serverTime: new Date().toISOString() }; } } ] }); // Register component ssrWithRoutes.registerComponent(mockAST); // Render route const routeHtml = await ssrWithRoutes.renderRoute('/counter/42'); // Verify route data was included expect(routeHtml).toContain('data-ordojs-component="CounterComponent"'); // Parse hydration data const hydrationDataMatch = routeHtml.match(/<script type="application\/json" id="ordojs-hydration-data">\s*([\s\S]*?)\s*<\/script>/); const hydrationData = JSON.parse(hydrationDataMatch![1]); expect(hydrationData.props.initialCount).toBe(42); expect(hydrationData.props.routeParams.initialValue).toBe('42'); expect(hydrationData.props.serverTime).toBeTruthy(); }); it('should generate complete HTML document with hydration', () => { const componentHtml = '<div data-ordojs-component="CounterComponent">Counter: 0</div>'; const document = ssr.generateDocument( componentHtml, 'Counter App', ['/js/counter.js'], ['/css/counter.css'] ); // Verify complete HTML structure expect(document).toContain('<!DOCTYPE html>'); expect(document).toContain('<title>Counter App</title>'); expect(document).toContain('<div id="app">'); expect(document).toContain('data-ordojs-component="CounterComponent"'); expect(document).toContain('<script src="/js/counter.js" defer></script>'); expect(document).toContain('<link rel="stylesheet" href="/css/counter.css">'); }); it('should handle component unmounting after hydration', async () => { // Register and render component ssr.registerComponent(mockAST); const serverHtml = await ssr.renderComponent('CounterComponent'); // Extract hydration data and set up client-side mocks const hydrationDataMatch = serverHtml.match(/<script type="application\/json" id="ordojs-hydration-data">\s*([\s\S]*?)\s*<\/script>/); const hydrationData = JSON.parse(hydrationDataMatch![1]); const mockComponentElement = { getAttribute: vi.fn((attr) => { if (attr === 'data-ordojs-component') return 'CounterComponent'; if (attr === 'data-component-id') return hydrationData.componentId; return null; }), setAttribute: vi.fn(), removeAttribute: vi.fn(), querySelectorAll: vi.fn().mockReturnValue([]) }; mockDocument.getElementById.mockReturnValue({ textContent: JSON.stringify(hydrationData) }); mockDocument.querySelector.mockReturnValue(mockComponentElement); mockDocument.querySelectorAll.mockReturnValue([]); const mockUnmount = vi.fn(); const mockConstructor = vi.fn().mockReturnValue({ id: hydrationData.componentId, name: 'CounterComponent', element: mockComponentElement, props: {}, state: {}, eventListeners: new Map(), update: vi.fn(), unmount: mockUnmount }); runtime.registerComponent('CounterComponent', mockConstructor); runtime.autoHydrate(); // Verify component was hydrated expect(runtime.getComponent(hydrationData.componentId)).toBeTruthy(); // Unmount component runtime.unmountComponent(hydrationData.componentId); // Verify component was properly unmounted expect(mockUnmount).toHaveBeenCalled(); expect(mockComponentElement.removeAttribute).toHaveBeenCalledWith('data-ordojs-hydrated'); expect(runtime.getComponent(hydrationData.componentId)).toBeUndefined(); }); });