@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
277 lines (234 loc) • 9.32 kB
text/typescript
/**
* @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();
});
});