Skip to main content
Glama
deleonio
by deleonio
server.test.mjs17.8 kB
import assert from 'node:assert/strict'; import { existsSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; import { credentials, loadPackageDefinition } from '@grpc/grpc-js'; import { load } from '@grpc/proto-loader'; import { createHydrateServer, hydrateProtoPath, memoryMonitor } from '../../dist/index.mjs'; const require = createRequire(import.meta.url); const FALLBACK_CUSTOM_ELEMENTS = { tags: [ { name: 'kol-badge', attributes: [ { name: '_label', required: true, type: 'string', }, ], slots: [{ name: '' }], }, { name: 'kol-button', attributes: [ { name: '_label', required: true, type: 'string', }, ], slots: [{ name: '' }], }, { name: 'kol-heading', attributes: [ { name: '_label', required: true, type: 'string', }, { name: '_level', required: false, type: 'number', }, ], slots: [{ name: '' }], }, ], }; const loadCustomElements = () => { const candidatePaths = ['../../../../components/custom-elements.json', '../../../../components/dist/custom-elements.json']; for (const candidate of candidatePaths) { const url = new URL(candidate, import.meta.url); const path = fileURLToPath(url); if (existsSync(path)) { try { return JSON.parse(readFileSync(path, 'utf-8')); } catch (error) { console.warn(`⚠️ Failed to read custom-elements metadata from ${path}:`, error); } } } console.warn('⚠️ Using fallback custom-elements metadata for hydrate-server tests.'); return FALLBACK_CUSTOM_ELEMENTS; }; const loadRenderer = async () => { const extractRenderer = (module) => { if (!module) { return null; } const directExport = module.renderToString; if (typeof directExport === 'function') { return directExport; } const defaultExport = module.default; if (typeof defaultExport === 'function') { return defaultExport; } if (defaultExport && typeof defaultExport === 'object' && typeof defaultExport.renderToString === 'function') { return defaultExport.renderToString; } return null; }; const loadAttempts = [async () => await import('@public-ui/hydrate'), async () => require('@public-ui/hydrate')]; for (const attempt of loadAttempts) { try { const module = await attempt(); const renderer = extractRenderer(module); if (renderer) { return renderer; } } catch (error) { const code = error?.code; const isResolutionError = code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED'; if (!isResolutionError) { throw error; } } } console.warn('⚠️ Falling back to stub renderer because @public-ui/hydrate could not be resolved.'); return async (html) => ({ html, components: [], hydratedCount: 0, diagnostics: [] }); }; const rendererPromise = loadRenderer(); const customElements = loadCustomElements(); // Components that cause severe memory issues in server environment // These should be excluded until SSR memory leaks are fixed const PROBLEMATIC_COMPONENTS = new Set([ // Input-Komponenten ohne disconnectedCallback() - alle verwenden debounced Timer (500ms) 'kol-input-checkbox', 'kol-input-color', // 78MB heap, 461MB RSS 'kol-input-date', // 432MB heap 'kol-input-email', // 582MB heap, 1917MB RSS 'kol-input-file', // Timeout/hang issues 'kol-input-number', // Server crashes 'kol-input-password', // Server crashes 'kol-input-radio', // Server crashes 'kol-input-range', // Timeout issues 'kol-input-text', // Timeout issues 'kol-textarea', // Uses debounced timer + setTimeout ohne cleanup // Komponenten mit Timer-Problemen ohne ordnungsgemäße Cleanup 'kol-progress', // setInterval(5000ms) without cleanup 'kol-tooltip', // Multiple setTimeout + @floating-ui/dom autoUpdate ohne proper cleanup 'kol-table-stateless', // Map<HTMLElement, setTimeout> + ResizeObserver 'kol-table-stateful', // Multiple setTimeout calls 'kol-form', // setTimeout in controller 'kol-details', // toggleTimeout 'kol-pagination', // Multiple setTimeout 'kol-pagination-wc', // Same component, actual tag name 'kol-toaster', // Multiple setTimeout 'kol-popover-button', // setTimeout + @floating-ui/dom autoUpdate 'kol-popover-button-wc', // Same component, actual tag name 'kol-popover', // addEventListener zu document.body ohne disconnectedCallback 'kol-accordion', // setTimeout ohne disconnectedCallback 'kol-single-select', // setTimeout ohne disconnectedCallback 'kol-combobox', // setTimeout ohne disconnectedCallback 'kol-quote', // Unknown memory issue (316MB heap) 'kol-select', // Memory explosion (219MB heap) 'kol-skip-nav', // Memory explosion (291MB heap, 1GB RSS) 'kol-spin', // Memory explosion (582MB heap, 1.2GB RSS) 'kol-split-button', // Server crashes 'kol-tabs', // Server crashes 'kol-toast-container', // Server crashes 'kol-toolbar', // Server crashes 'kol-tree', // Server crashes 'kol-tree-item', // Server crashes 'kol-tree-item-wc', // Server crashes ]); // Test components with enhanced resource management and timeout protection // Use TEST_COMPONENT_LIMIT env var to limit components for faster testing (e.g., TEST_COMPONENT_LIMIT=10) // Use INCLUDE_PROBLEMATIC=1 to test problematic components (warning: may cause memory issues) const maxComponentsToTest = process.env.TEST_COMPONENT_LIMIT ? parseInt(process.env.TEST_COMPONENT_LIMIT, 10) : customElements.tags.length; const includeProblematic = process.env.INCLUDE_PROBLEMATIC === '1'; /** * Generate realistic test HTML for components with proper attributes */ function generateTestComponents() { const testComponents = []; // Filter available components and take a representative sample const availableComponents = customElements.tags .map((tag) => tag.name) .filter((name) => includeProblematic || !PROBLEMATIC_COMPONENTS.has(name)) // Exclude problematic components unless explicitly included .sort(); // Test components with enhanced resource management and timeout protection const componentsToTest = maxComponentsToTest < availableComponents.length ? availableComponents.slice(0, maxComponentsToTest) : availableComponents; const excludedCount = customElements.tags.length - availableComponents.length; const exclusionInfo = excludedCount > 0 ? ` (${excludedCount} excluded due to memory issues${includeProblematic ? ' - OVERRIDDEN' : ''})` : ''; console.log(`📋 Found ${customElements.tags.length} total components, ${availableComponents.length} suitable for testing${exclusionInfo}`); const testingAll = maxComponentsToTest >= availableComponents.length; console.log( `🎯 Testing ${testingAll ? 'ALL' : 'first'} ${componentsToTest.length} components with timeout protection and resource management${testingAll ? '' : ' (use TEST_COMPONENT_LIMIT=N to limit)'}`, ); for (const componentName of componentsToTest) { const componentMeta = customElements.tags.find((tag) => tag.name === componentName); if (!componentMeta) { console.log(`⚠️ Component ${componentName} not found in custom-elements.json`); continue; } const { name, attributes = [], slots = [] } = componentMeta; // Generate only one simple test case per component to avoid resource accumulation const testCase = generateSingleTestCase(componentMeta, 'common'); if (testCase) { testComponents.push({ name: name, componentName: name, html: testCase.html, category: getComponentCategory(name), }); } } return testComponents; } /** * Generate a single test case with specific attribute strategy */ function generateSingleTestCase(componentMeta, strategy) { const { name, attributes = [], slots = [] } = componentMeta; // Keep it simple - only required + very common attributes const attrs = attributes.filter((attr) => attr.required || attr.name === '_label' || attr.name === '_heading'); const attrString = attrs .map((attr) => { let value = ''; if (attr.type.includes('string')) { if (attr.name === '_label') value = `Test ${name}`; else if (attr.name === '_heading') value = 'Test Heading'; else value = 'Test value'; } else if (attr.type.includes('boolean')) { value = attr.defaultValue === 'true' ? 'true' : 'false'; } else if (attr.type.includes('number')) { value = '1'; } else if (attr.type.includes('object') || attr.type.includes('array')) { // Skip complex types return null; } return `${attr.name}="${value}"`; }) .filter(Boolean) .join(' '); // Simple content for slots const hasDefaultSlot = slots.some((slot) => slot.name === ''); const content = hasDefaultSlot ? 'Test content' : ''; const html = `<${name}${attrString ? ' ' + attrString : ''}>${content}</${name}>`; return { html }; } /** * Get the category of a component based on its name patterns */ function getComponentCategory(componentName) { if (componentName.startsWith('kol-input-') || componentName === 'kol-textarea' || componentName === 'kol-select') { return 'inputs'; } if (['kol-button', 'kol-badge', 'kol-heading', 'kol-link', 'kol-image', 'kol-icon'].includes(componentName)) { return 'basic'; } if (['kol-card', 'kol-accordion', 'kol-modal', 'kol-drawer', 'kol-details'].includes(componentName)) { return 'layout'; } if (['kol-alert', 'kol-progress', 'kol-spin', 'kol-toast-container'].includes(componentName)) { return 'feedback'; } if (['kol-breadcrumb', 'kol-nav', 'kol-tabs', 'kol-skip-nav'].includes(componentName)) { return 'navigation'; } return 'other'; } const testComponents = generateTestComponents(); // Fast test - only REST API (default) test('REST endpoint returns hydrated markup', { timeout: 60000 }, async (t) => { const renderer = await rendererPromise; // Use the real hydrate renderer instead of a stub const server = await createHydrateServer({ restHost: '127.0.0.1', restPort: 0, grpcHost: '127.0.0.1', grpcPort: 0, logger: false, // Use real renderer from @public-ui/hydrate renderer, }); t.after(async () => { await server.stop(); }); await server.start(); const restUrl = server.getRestUrl(); assert.ok(restUrl, 'REST URL should be available after startup'); console.log(`\n🧪 Testing ${testComponents.length} component cases via REST API...`); const results = { total: testComponents.length, passed: 0, categories: {}, }; // Test multiple components with realistic HTML and proper error handling for (const component of testComponents) { try { const startTime = Date.now(); // Monitor memory before component rendering const memoryBefore = memoryMonitor.beforeRender(); // Create a timeout for individual component rendering const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); // 15s timeout per component const restResponse = await fetch(restUrl, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ html: component.html }), signal: controller.signal, }); clearTimeout(timeoutId); const endTime = Date.now(); const duration = endTime - startTime; // Monitor memory after component rendering memoryMonitor.afterRender(component.name, memoryBefore, startTime); if (restResponse.status !== 200) { const errorText = await restResponse.text(); console.warn(` ⚠️ ${component.name} failed with status ${restResponse.status}: ${errorText}`); continue; } const restPayload = await restResponse.json(); // Verify response structure assert.ok(typeof restPayload.html === 'string', `${component.name} should return HTML string`); assert.ok(restPayload.html.length > 0, `${component.name} should return non-empty HTML`); assert.ok(Array.isArray(restPayload.components), `${component.name} should return components array`); assert.ok(Array.isArray(restPayload.diagnostics), `${component.name} should return diagnostics array`); assert.ok(typeof restPayload.hydratedCount === 'number', `${component.name} should return hydrated count`); // The HTML should contain the component assert.ok(restPayload.html.includes(component.componentName), `${component.name} should be present in hydrated HTML`); // Track results by category const category = component.category; if (!results.categories[category]) { results.categories[category] = { total: 0, passed: 0, failed: 0 }; } results.categories[category].total++; results.categories[category].passed++; results.passed++; // Log with timing info const timingInfo = duration > 1000 ? ` (${duration}ms - SLOW)` : duration > 500 ? ` (${duration}ms)` : ''; console.log(` ✅ ${component.name} (${category})${timingInfo}`); // Warn about potential resource issues if (restPayload.diagnostics.length > 0) { console.log(` 📋 Diagnostics: ${restPayload.diagnostics.length} items`); } // Small delay to prevent resource accumulation and check memory if (results.passed % 10 === 0) { await new Promise((resolve) => setTimeout(resolve, 100)); // Check for dangerous memory usage if (memoryMonitor.isMemoryUsageDangerous()) { console.warn(`🚨 MEMORY DANGER: ${memoryMonitor.getCurrentMemoryInfo()}`); console.warn('Consider stopping test run to prevent system crash'); } } // More frequent memory logging for high-usage components if (results.passed % 5 === 0) { console.log(` 📊 ${memoryMonitor.getCurrentMemoryInfo()}`); } } catch (error) { const isTimeout = error.name === 'AbortError' || error.message.includes('timeout'); const errorType = isTimeout ? 'TIMEOUT' : 'ERROR'; console.error(` ❌ ${component.name} failed (${errorType}):`, error.message); // Track failed component const category = component.category; if (!results.categories[category]) { results.categories[category] = { total: 0, passed: 0, failed: 0 }; } results.categories[category].total++; results.categories[category].failed++; // If it's a timeout, we might want to skip similar components if (isTimeout) { console.warn(` ⚠️ ${component.name} timed out - this component may cause hanging`); } } } // Print summary const totalFailed = results.total - results.passed; console.log(`\n📊 REST API Test Summary:`); console.log(` Total: ${results.passed}/${results.total} passed${totalFailed > 0 ? ` (${totalFailed} failed)` : ''}`); for (const [category, stats] of Object.entries(results.categories)) { const categoryFailed = stats.failed || 0; const categoryTotal = stats.total || 0; const categoryPassed = stats.passed || 0; console.log(` ${category}: ${categoryPassed}/${categoryTotal} components${categoryFailed > 0 ? ` (${categoryFailed} failed)` : ''}`); } // Print detailed memory report console.log(memoryMonitor.generateReport()); }); // Slow test - gRPC API (optional, run with TEST_GRPC=true) test('gRPC endpoint returns hydrated markup', { timeout: 600000, skip: !process.env.TEST_GRPC }, async (t) => { const renderer = await rendererPromise; const server = await createHydrateServer({ restHost: '127.0.0.1', restPort: 0, grpcHost: '127.0.0.1', grpcPort: 0, logger: false, renderer, }); t.after(async () => { await server.stop(); }); await server.start(); const grpcEndpoint = server.getGrpcEndpoint(); assert.ok(grpcEndpoint, 'gRPC endpoint should be available after startup'); const packageDefinition = await load(hydrateProtoPath, { keepCase: false, longs: String, enums: String, defaults: true, oneofs: true, }); const proto = loadPackageDefinition(packageDefinition); const hydratePackage = proto.publicui?.hydrate; assert.ok(hydratePackage?.HydrateRenderer, 'HydrateRenderer service should be defined'); const client = new hydratePackage.HydrateRenderer(grpcEndpoint, credentials.createInsecure()); // Test only first 3 components via gRPC to keep it fast (gRPC is much slower than REST) const grpcTestLimit = 3; const grpcTestComponents = testComponents.slice(0, grpcTestLimit); const categoriesTested = new Set(grpcTestComponents.map((c) => c.category)); console.log(`\n🌐 Testing first ${grpcTestComponents.length} components via gRPC API...`); for (const component of grpcTestComponents) { const grpcResponse = await new Promise((resolve, reject) => { client.renderHtml({ html: component.html }, (error, response) => { if (error) { reject(error); return; } resolve(response); }); }); assert.ok(grpcResponse, `gRPC response for ${component.name} should not be null`); assert.ok(typeof grpcResponse.html === 'string', `${component.name} gRPC should return HTML string`); assert.ok(grpcResponse.html.length > 0, `${component.name} gRPC should return non-empty HTML`); assert.ok(Array.isArray(grpcResponse.components), `${component.name} gRPC should return components array`); assert.ok(typeof grpcResponse.hydratedCount === 'number', `${component.name} gRPC should return hydrated count`); // Parse diagnostics JSON const diagnostics = JSON.parse(grpcResponse.diagnosticsJson || '[]'); assert.ok(Array.isArray(diagnostics), `${component.name} gRPC should return valid diagnostics`); // The HTML should contain the component assert.ok(grpcResponse.html.includes(component.componentName), `${component.name} should be present in gRPC hydrated HTML`); console.log(` ✅ ${component.name} (${component.category})`); } console.log(`\n📡 gRPC API Test Summary: ${grpcTestComponents.length} components from ${categoriesTested.size} categories`); console.log(` Categories tested: ${Array.from(categoriesTested).join(', ')}\n`); client.close(); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/deleonio/public-ui-kolibri'

If you have feedback or need assistance with the MCP directory API, please join our Discord server