Skip to main content
Glama
server.test.js29.5 kB
import { describe, it, before, after } from 'node:test'; import assert from 'node:assert'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * BasecoatMCPServer Test Suite * * Tests all available MCP tools: * 1. get_component - Get HTML code for a specific component * 2. list_components - List all components by category * 3. get_usage - Get usage documentation for a component * 4. get_setup - Get Basecoat CSS setup code * 5. get_theme_script - Get theme switcher script * 6. search_components - Search components by name or category * 7. get_category - Get all components in a category */ // Helper class that mirrors the server's internal methods for testing class TestableBasecoatServer { constructor() { this.componentsDir = path.join(__dirname, 'components'); this.usageDir = path.join(__dirname, 'usage'); this.scriptsDir = path.join(__dirname, 'scripts'); } async getComponentsList() { const categories = ['forms', 'navigation', 'feedback', 'interactive', 'layout']; const components = {}; for (const category of categories) { const categoryPath = path.join(this.componentsDir, category); try { const files = await fs.readdir(categoryPath); const htmlFiles = files .filter(file => file.endsWith('.html')) .map(file => ({ name: file.replace('.html', ''), category: category, file: file })); if (htmlFiles.length > 0) { components[category] = htmlFiles; } } catch (error) { // Category doesn't exist } } return components; } async getComponent(componentName) { const components = await this.getComponentsList(); for (const [category, categoryComponents] of Object.entries(components)) { const component = categoryComponents.find(comp => comp.name === componentName); if (component) { const filePath = path.join(this.componentsDir, category, component.file); try { const content = await fs.readFile(filePath, 'utf-8'); return { name: componentName, category: category, html: content.trim(), file: component.file }; } catch (error) { throw new Error(`Failed to read component file: ${error.message}`); } } } throw new Error(`Component '${componentName}' not found`); } async getUsageDocumentation(componentName) { const usageCategories = ['forms', 'navigation', 'feedback', 'interactive', 'layout']; for (const category of usageCategories) { const usageFile = `${componentName}-usage.md`; const usagePath = path.join(this.usageDir, category, usageFile); try { const content = await fs.readFile(usagePath, 'utf-8'); return { component: componentName, category: category, documentation: content.trim() }; } catch (error) { continue; } } throw new Error(`Usage documentation for '${componentName}' not found`); } async getSetupCode() { const setupPath = path.join(this.scriptsDir, 'setup.html'); try { const content = await fs.readFile(setupPath, 'utf-8'); return content.trim(); } catch (error) { throw new Error(`Failed to read setup code: ${error.message}`); } } async getThemeScript() { const themePath = path.join(this.scriptsDir, 'theme-script.html'); try { const content = await fs.readFile(themePath, 'utf-8'); return content.trim(); } catch (error) { throw new Error(`Failed to read theme script: ${error.message}`); } } async searchComponents(query) { const components = await this.getComponentsList(); const results = []; const queryLower = query.toLowerCase(); for (const [category, categoryComponents] of Object.entries(components)) { for (const component of categoryComponents) { if (component.name.toLowerCase().includes(queryLower) || category.toLowerCase().includes(queryLower)) { results.push({ name: component.name, category: category, file: component.file, match: component.name.toLowerCase().includes(queryLower) ? 'name' : 'category' }); } } } return results; } async getComponentsByCategory(category) { const components = await this.getComponentsList(); if (!components[category]) { throw new Error(`Category '${category}' not found. Available categories: ${Object.keys(components).join(', ')}`); } return { category: category, components: components[category] }; } } // Test Suite describe('Basecoat MCP Server Tools', () => { let server; before(() => { server = new TestableBasecoatServer(); }); // ============================================ // Tool 1: list_components // ============================================ describe('list_components', () => { it('should return components organized by category', async () => { const components = await server.getComponentsList(); assert.ok(typeof components === 'object', 'Should return an object'); assert.ok(Object.keys(components).length > 0, 'Should have at least one category'); }); it('should include all expected categories', async () => { const components = await server.getComponentsList(); const expectedCategories = ['forms', 'navigation', 'feedback', 'interactive', 'layout']; for (const category of expectedCategories) { assert.ok(components[category], `Should include category: ${category}`); } }); it('should have components in each category', async () => { const components = await server.getComponentsList(); for (const [category, categoryComponents] of Object.entries(components)) { assert.ok(Array.isArray(categoryComponents), `${category} should be an array`); assert.ok(categoryComponents.length > 0, `${category} should have components`); } }); it('should have correct component structure', async () => { const components = await server.getComponentsList(); for (const [category, categoryComponents] of Object.entries(components)) { for (const component of categoryComponents) { assert.ok(component.name, 'Component should have a name'); assert.ok(component.category, 'Component should have a category'); assert.ok(component.file, 'Component should have a file'); assert.ok(component.file.endsWith('.html'), 'File should be an HTML file'); } } }); it('should include newly added components', async () => { const components = await server.getComponentsList(); // Check for new components added during enrichment const layoutComponents = components.layout.map(c => c.name); assert.ok(layoutComponents.includes('empty'), 'Should include empty component'); assert.ok(layoutComponents.includes('spinner'), 'Should include spinner component'); assert.ok(layoutComponents.includes('kbd'), 'Should include kbd component'); assert.ok(layoutComponents.includes('item'), 'Should include item component'); const formsComponents = components.forms.map(c => c.name); assert.ok(formsComponents.includes('button-group'), 'Should include button-group component'); assert.ok(formsComponents.includes('input-group'), 'Should include input-group component'); const feedbackComponents = components.feedback.map(c => c.name); assert.ok(feedbackComponents.includes('progress'), 'Should include progress component'); const navComponents = components.navigation.map(c => c.name); assert.ok(navComponents.includes('command'), 'Should include command component'); }); }); // ============================================ // Tool 2: get_component // ============================================ describe('get_component', () => { it('should return a valid component by name', async () => { const component = await server.getComponent('button-primary'); assert.ok(component, 'Should return a component'); assert.strictEqual(component.name, 'button-primary'); assert.strictEqual(component.category, 'forms'); assert.ok(component.html, 'Should have HTML content'); assert.ok(component.html.includes('<button'), 'HTML should contain button element'); }); it('should throw error for non-existent component', async () => { await assert.rejects( async () => await server.getComponent('non-existent-component'), /Component 'non-existent-component' not found/ ); }); it('should return correct category for component', async () => { const formsComponent = await server.getComponent('input'); assert.strictEqual(formsComponent.category, 'forms'); const layoutComponent = await server.getComponent('card'); assert.strictEqual(layoutComponent.category, 'layout'); const feedbackComponent = await server.getComponent('alert'); assert.strictEqual(feedbackComponent.category, 'feedback'); }); it('should return HTML content for new components', async () => { const progress = await server.getComponent('progress'); assert.ok(progress.html.includes('progress'), 'Progress should have progress element'); const spinner = await server.getComponent('spinner'); assert.ok(spinner.html.includes('animate-spin'), 'Spinner should have animation class'); const kbd = await server.getComponent('kbd'); assert.ok(kbd.html.includes('<kbd'), 'Kbd should have kbd element'); const empty = await server.getComponent('empty'); assert.ok(empty.html.includes('empty') || empty.html.includes('Empty'), 'Empty state should have empty content'); }); it('should handle components with dashes in name', async () => { const component = await server.getComponent('button-outline'); assert.strictEqual(component.name, 'button-outline'); assert.ok(component.html, 'Should have HTML content'); }); }); // ============================================ // Tool 3: get_usage // ============================================ describe('get_usage', () => { it('should return usage documentation for button', async () => { const usage = await server.getUsageDocumentation('button'); assert.ok(usage, 'Should return usage documentation'); assert.strictEqual(usage.component, 'button'); assert.strictEqual(usage.category, 'forms'); assert.ok(usage.documentation, 'Should have documentation content'); assert.ok(usage.documentation.includes('#'), 'Should have markdown headers'); }); it('should throw error for non-existent usage documentation', async () => { await assert.rejects( async () => await server.getUsageDocumentation('non-existent'), /Usage documentation for 'non-existent' not found/ ); }); it('should return usage docs from correct categories', async () => { const buttonUsage = await server.getUsageDocumentation('button'); assert.strictEqual(buttonUsage.category, 'forms'); const cardUsage = await server.getUsageDocumentation('card'); assert.strictEqual(cardUsage.category, 'layout'); const alertUsage = await server.getUsageDocumentation('alert'); assert.strictEqual(alertUsage.category, 'feedback'); const tabsUsage = await server.getUsageDocumentation('tabs'); assert.strictEqual(tabsUsage.category, 'navigation'); }); it('should return usage documentation for new components', async () => { // Forms const checkboxUsage = await server.getUsageDocumentation('checkbox'); assert.ok(checkboxUsage.documentation.length > 0, 'Checkbox usage should have content'); const switchUsage = await server.getUsageDocumentation('switch'); assert.ok(switchUsage.documentation.length > 0, 'Switch usage should have content'); const radioUsage = await server.getUsageDocumentation('radio'); assert.ok(radioUsage.documentation.length > 0, 'Radio usage should have content'); const sliderUsage = await server.getUsageDocumentation('slider'); assert.ok(sliderUsage.documentation.length > 0, 'Slider usage should have content'); // Navigation const accordionUsage = await server.getUsageDocumentation('accordion'); assert.ok(accordionUsage.documentation.length > 0, 'Accordion usage should have content'); const sidebarUsage = await server.getUsageDocumentation('sidebar'); assert.ok(sidebarUsage.documentation.length > 0, 'Sidebar usage should have content'); // Interactive const tooltipUsage = await server.getUsageDocumentation('tooltip'); assert.ok(tooltipUsage.documentation.length > 0, 'Tooltip usage should have content'); const comboboxUsage = await server.getUsageDocumentation('combobox'); assert.ok(comboboxUsage.documentation.length > 0, 'Combobox usage should have content'); // Layout const avatarUsage = await server.getUsageDocumentation('avatar'); assert.ok(avatarUsage.documentation.length > 0, 'Avatar usage should have content'); const tableUsage = await server.getUsageDocumentation('table'); assert.ok(tableUsage.documentation.length > 0, 'Table usage should have content'); }); it('should include JavaScript requirements in documentation', async () => { const tooltipUsage = await server.getUsageDocumentation('tooltip'); assert.ok( tooltipUsage.documentation.includes('JavaScript') || tooltipUsage.documentation.includes('script'), 'Tooltip usage should mention JavaScript requirements' ); }); }); // ============================================ // Tool 4: get_setup // ============================================ describe('get_setup', () => { it('should return setup code', async () => { const setupCode = await server.getSetupCode(); assert.ok(setupCode, 'Should return setup code'); assert.ok(typeof setupCode === 'string', 'Should be a string'); }); it('should include CDN links', async () => { const setupCode = await server.getSetupCode(); assert.ok( setupCode.includes('cdn') || setupCode.includes('CDN') || setupCode.includes('http'), 'Should include CDN references' ); }); it('should include Tailwind or Basecoat references', async () => { const setupCode = await server.getSetupCode(); assert.ok( setupCode.includes('tailwind') || setupCode.includes('Tailwind') || setupCode.includes('basecoat') || setupCode.includes('Basecoat'), 'Should include Tailwind or Basecoat references' ); }); it('should be valid HTML', async () => { const setupCode = await server.getSetupCode(); // Basic HTML validation - should have script or link tags assert.ok( setupCode.includes('<script') || setupCode.includes('<link'), 'Should contain script or link tags' ); }); }); // ============================================ // Tool 5: get_theme_script // ============================================ describe('get_theme_script', () => { it('should return theme script', async () => { const themeScript = await server.getThemeScript(); assert.ok(themeScript, 'Should return theme script'); assert.ok(typeof themeScript === 'string', 'Should be a string'); }); it('should include script tag', async () => { const themeScript = await server.getThemeScript(); assert.ok(themeScript.includes('<script'), 'Should include script tag'); }); it('should include theme-related code', async () => { const themeScript = await server.getThemeScript(); assert.ok( themeScript.includes('theme') || themeScript.includes('dark') || themeScript.includes('light'), 'Should include theme-related code' ); }); it('should include localStorage or class manipulation', async () => { const themeScript = await server.getThemeScript(); assert.ok( themeScript.includes('localStorage') || themeScript.includes('classList') || themeScript.includes('class'), 'Should include storage or class manipulation' ); }); }); // ============================================ // Tool 6: search_components // ============================================ describe('search_components', () => { it('should find components by name', async () => { const results = await server.searchComponents('button'); assert.ok(Array.isArray(results), 'Should return an array'); assert.ok(results.length > 0, 'Should find at least one button component'); const buttonResults = results.filter(r => r.name.includes('button')); assert.ok(buttonResults.length > 0, 'Should find button components'); }); it('should find components by category', async () => { const results = await server.searchComponents('forms'); assert.ok(results.length > 0, 'Should find form components'); assert.ok(results.every(r => r.category === 'forms'), 'All results should be from forms category'); }); it('should return empty array for no matches', async () => { const results = await server.searchComponents('xyznonexistent'); assert.ok(Array.isArray(results), 'Should return an array'); assert.strictEqual(results.length, 0, 'Should return empty array'); }); it('should be case-insensitive', async () => { const lowerResults = await server.searchComponents('button'); const upperResults = await server.searchComponents('BUTTON'); const mixedResults = await server.searchComponents('BuTtOn'); assert.strictEqual(lowerResults.length, upperResults.length, 'Should find same results regardless of case'); assert.strictEqual(lowerResults.length, mixedResults.length, 'Should find same results regardless of case'); }); it('should include match type in results', async () => { const nameResults = await server.searchComponents('button'); const categoryResults = await server.searchComponents('layout'); const nameMatch = nameResults.find(r => r.match === 'name'); assert.ok(nameMatch, 'Should have name matches'); const categoryMatch = categoryResults.find(r => r.match === 'category'); assert.ok(categoryMatch, 'Should have category matches'); }); it('should find new components', async () => { const spinnerResults = await server.searchComponents('spinner'); assert.ok(spinnerResults.length > 0, 'Should find spinner component'); const progressResults = await server.searchComponents('progress'); assert.ok(progressResults.length > 0, 'Should find progress component'); const commandResults = await server.searchComponents('command'); assert.ok(commandResults.length > 0, 'Should find command component'); }); it('should find partial matches', async () => { const results = await server.searchComponents('btn'); // Should find nothing since components are named 'button-*' not 'btn-*' // This tests that partial matching works correctly assert.ok(Array.isArray(results), 'Should return an array'); }); }); // ============================================ // Tool 7: get_category // ============================================ describe('get_category', () => { it('should return components for forms category', async () => { const result = await server.getComponentsByCategory('forms'); assert.strictEqual(result.category, 'forms'); assert.ok(Array.isArray(result.components), 'Should have components array'); assert.ok(result.components.length > 0, 'Should have components'); }); it('should return components for all valid categories', async () => { const categories = ['forms', 'navigation', 'feedback', 'interactive', 'layout']; for (const category of categories) { const result = await server.getComponentsByCategory(category); assert.strictEqual(result.category, category); assert.ok(result.components.length > 0, `${category} should have components`); } }); it('should throw error for invalid category', async () => { await assert.rejects( async () => await server.getComponentsByCategory('invalid-category'), /Category 'invalid-category' not found/ ); }); it('should include available categories in error message', async () => { try { await server.getComponentsByCategory('invalid'); assert.fail('Should have thrown an error'); } catch (error) { assert.ok(error.message.includes('forms'), 'Error should mention forms'); assert.ok(error.message.includes('layout'), 'Error should mention layout'); } }); it('should return correct structure for each component', async () => { const result = await server.getComponentsByCategory('layout'); for (const component of result.components) { assert.ok(component.name, 'Component should have name'); assert.ok(component.category, 'Component should have category'); assert.ok(component.file, 'Component should have file'); assert.strictEqual(component.category, 'layout', 'Category should match'); } }); it('should include newly added components in categories', async () => { const layoutResult = await server.getComponentsByCategory('layout'); const layoutNames = layoutResult.components.map(c => c.name); assert.ok(layoutNames.includes('spinner'), 'Layout should include spinner'); assert.ok(layoutNames.includes('empty'), 'Layout should include empty'); assert.ok(layoutNames.includes('kbd'), 'Layout should include kbd'); const formsResult = await server.getComponentsByCategory('forms'); const formsNames = formsResult.components.map(c => c.name); assert.ok(formsNames.includes('button-group'), 'Forms should include button-group'); const feedbackResult = await server.getComponentsByCategory('feedback'); const feedbackNames = feedbackResult.components.map(c => c.name); assert.ok(feedbackNames.includes('progress'), 'Feedback should include progress'); }); }); // ============================================ // Integration Tests // ============================================ describe('Integration Tests', () => { it('should be able to list, search, and get a component', async () => { // List all components const allComponents = await server.getComponentsList(); assert.ok(Object.keys(allComponents).length > 0); // Search for button const searchResults = await server.searchComponents('button'); assert.ok(searchResults.length > 0); // Get the first button component const firstButton = searchResults[0]; const component = await server.getComponent(firstButton.name); assert.ok(component.html); }); it('should have consistent component data across methods', async () => { // Get component via get_component const directComponent = await server.getComponent('button-primary'); // Get component via get_category const categoryResult = await server.getComponentsByCategory('forms'); const categoryComponent = categoryResult.components.find(c => c.name === 'button-primary'); // Get component via search const searchResults = await server.searchComponents('button-primary'); const searchComponent = searchResults.find(c => c.name === 'button-primary'); // All should have consistent data assert.strictEqual(directComponent.name, categoryComponent.name); assert.strictEqual(directComponent.category, categoryComponent.category); assert.strictEqual(directComponent.name, searchComponent.name); }); it('should have usage docs for components that need them', async () => { const componentsWithUsage = [ 'button', 'input', 'card', 'tabs', 'select', 'dialog', 'toast', 'alert', 'checkbox', 'switch', 'radio', 'slider', 'accordion', 'sidebar', 'breadcrumb', 'badge', 'tooltip', 'popover', 'dropdown', 'combobox', 'avatar', 'skeleton', 'table' ]; for (const componentName of componentsWithUsage) { try { const usage = await server.getUsageDocumentation(componentName); assert.ok(usage.documentation, `${componentName} should have usage documentation`); } catch (error) { assert.fail(`${componentName} should have usage documentation: ${error.message}`); } } }); }); // ============================================ // Edge Cases // ============================================ describe('Edge Cases', () => { it('should handle empty search query', async () => { const results = await server.searchComponents(''); // Empty string should match everything by category assert.ok(Array.isArray(results), 'Should return an array'); }); it('should handle special characters in search', async () => { const results = await server.searchComponents('button-'); assert.ok(Array.isArray(results), 'Should handle dashes'); const results2 = await server.searchComponents('alert_'); assert.ok(Array.isArray(results2), 'Should handle underscores'); }); it('should handle whitespace in search', async () => { const results = await server.searchComponents(' button '); // Should still work but may not find exact matches due to trimming assert.ok(Array.isArray(results), 'Should handle whitespace'); }); it('should handle component names with multiple dashes', async () => { const component = await server.getComponent('checkbox-with-label'); assert.ok(component.html, 'Should handle multi-dash names'); }); }); // ============================================ // Performance Tests // ============================================ describe('Performance', () => { it('should list components quickly', async () => { const start = Date.now(); await server.getComponentsList(); const duration = Date.now() - start; assert.ok(duration < 1000, `Should complete in under 1 second, took ${duration}ms`); }); it('should search components quickly', async () => { const start = Date.now(); await server.searchComponents('button'); const duration = Date.now() - start; assert.ok(duration < 500, `Should complete in under 500ms, took ${duration}ms`); }); it('should get component quickly', async () => { const start = Date.now(); await server.getComponent('button-primary'); const duration = Date.now() - start; assert.ok(duration < 500, `Should complete in under 500ms, took ${duration}ms`); }); }); // ============================================ // File Integrity Tests // ============================================ describe('File Integrity', () => { it('should have valid HTML in all component files', async () => { const components = await server.getComponentsList(); for (const [category, categoryComponents] of Object.entries(components)) { for (const comp of categoryComponents) { const component = await server.getComponent(comp.name); // Basic HTML validation - should have at least one HTML tag assert.ok( component.html.includes('<') && component.html.includes('>'), `${comp.name} should contain valid HTML` ); } } }); it('should have valid markdown in usage documentation', async () => { const usageFiles = ['button', 'input', 'card', 'alert', 'tabs']; for (const file of usageFiles) { try { const usage = await server.getUsageDocumentation(file); // Basic markdown validation - should have headers assert.ok( usage.documentation.includes('#'), `${file} usage should have markdown headers` ); } catch (error) { // Skip if usage doc doesn't exist } } }); it('should have setup.html file', async () => { const setupCode = await server.getSetupCode(); assert.ok(setupCode.length > 0, 'Setup file should not be empty'); }); it('should have theme-script.html file', async () => { const themeScript = await server.getThemeScript(); assert.ok(themeScript.length > 0, 'Theme script file should not be empty'); }); }); }); // Run summary describe('Test Summary', () => { it('should have tested all 7 MCP tools', async () => { const tools = [ 'list_components', 'get_component', 'get_usage', 'get_setup', 'get_theme_script', 'search_components', 'get_category' ]; // This test just confirms we have coverage for all tools assert.strictEqual(tools.length, 7, 'Should test all 7 tools'); }); });

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/GustavoGomezPG/basecoat-mcp'

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