Skip to main content
Glama
html-file-output-integration.test.ts16.8 kB
/** * Integration tests for HTML visualization tools with file output system */ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals'; import { promises as fs } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { createPaletteHtmlTool } from '../../src/tools/create-palette-html'; import { createColorWheelHtmlTool } from '../../src/tools/create-color-wheel-html'; import { createGradientHtmlTool } from '../../src/tools/create-gradient-html'; import { createThemePreviewHtmlTool } from '../../src/tools/create-theme-preview-html'; import { fileOutputManager } from '../../src/utils/file-output-manager'; import { environmentConfig } from '../../src/utils/environment-config'; import { ToolResponse } from '../../src/types/index'; // Helper type for file-based visualization responses interface FileBasedVisualizationResponse extends ToolResponse { visualizations: any; // Use any to avoid type conflicts with union types data: { file_path?: string; file_name?: string; file_size?: number; background_controls_enabled?: boolean; accessibility_features?: string[]; color_count?: number; [key: string]: unknown; }; } describe('HTML File Output Integration Tests', () => { let testDir: string; let originalEnvVar: string | undefined; beforeEach(async () => { // Create temporary test directory testDir = join(tmpdir(), `mcp-color-test-${Date.now()}`); await fs.mkdir(testDir, { recursive: true }); // Set environment variable for testing originalEnvVar = process.env['COLOR_MCP_VISUALIZATIONS_DIR']; process.env['COLOR_MCP_VISUALIZATIONS_DIR'] = testDir; // Reload configuration environmentConfig.reloadConfiguration(); // Initialize file output manager await fileOutputManager.initialize(); }); afterEach(async () => { // Restore original environment variable if (originalEnvVar !== undefined) { process.env['COLOR_MCP_VISUALIZATIONS_DIR'] = originalEnvVar; } else { delete process.env['COLOR_MCP_VISUALIZATIONS_DIR']; } // Clean up test directory try { await fs.rm(testDir, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } // Destroy file output manager fileOutputManager.destroy(); }); describe('create_palette_html with file output', () => { test('should save HTML file and return file path', async () => { const params = { palette: ['#ff0000', '#00ff00', '#0000ff'], layout: 'horizontal', show_values: true, interactive: true, enable_background_controls: true, }; const result = (await createPaletteHtmlTool.handler( params )) as FileBasedVisualizationResponse; expect(result.success).toBe(true); if (result.success) { expect(result.data).toHaveProperty('file_path'); expect(result.data).toHaveProperty('file_name'); expect(result.data).toHaveProperty('file_size'); expect(result.data).toHaveProperty('background_controls_enabled', true); expect(result.data).toHaveProperty('accessibility_features'); // Check that visualizations contains both HTML content and file info expect(result.visualizations).toHaveProperty('html'); expect(result.visualizations).toHaveProperty('html_file'); if (result.success) { const viz = (result as any).visualizations; if (viz?.html_file) { const filePath = viz.html_file.file_path; // Verify file exists const fileExists = await fs .access(filePath) .then(() => true) .catch(() => false); expect(fileExists).toBe(true); // Verify file content const content = await fs.readFile(filePath, 'utf8'); expect(content).toContain('<!DOCTYPE html>'); expect(content).toContain('background-controls'); expect(content).toContain('#ff0000'); expect(content).toContain('#00ff00'); expect(content).toContain('#0000ff'); expect(content).toContain('BackgroundController'); } } } }); test('should handle file saving errors gracefully', async () => { const params = { palette: ['#ff0000', '#00ff00'], layout: 'grid', }; const result = (await createPaletteHtmlTool.handler( params )) as FileBasedVisualizationResponse; expect(result.success).toBe(true); if (result.success) { // Should either have html_file (successful save) or html (fallback) const hasHtmlFile = Object.prototype.hasOwnProperty.call( result.visualizations, 'html_file' ); const hasHtml = Object.prototype.hasOwnProperty.call( result.visualizations, 'html' ); expect(hasHtmlFile || hasHtml).toBe(true); if (hasHtmlFile) { // File was saved successfully expect(result.visualizations).toHaveProperty('html_file'); } else { // Fallback to HTML content expect(result.visualizations).toHaveProperty('html'); expect(result.visualizations?.html).toContain('<!DOCTYPE html>'); expect(result.visualizations?.html).toContain('#ff0000'); } } }); test('should include all accessibility features', async () => { const params = { palette: ['#2563eb', '#dc2626', '#059669'], accessibility_info: true, enable_accessibility_testing: true, include_keyboard_help: true, }; const result = (await createPaletteHtmlTool.handler( params )) as FileBasedVisualizationResponse; expect(result.success).toBe(true); if (result.success) { expect(result.data.accessibility_features).toContain( 'Background theme toggle' ); expect(result.data.accessibility_features).toContain( 'Custom background color picker' ); expect(result.data.accessibility_features).toContain( 'Keyboard navigation' ); expect(result.data.accessibility_features).toContain( 'Screen reader support' ); if (result.success) { const viz = (result as any).visualizations; if (viz?.html_file) { const content = await fs.readFile(viz.html_file.file_path, 'utf8'); expect(content).toContain('accessibility-features'); expect(content).toContain('keyboard-shortcuts'); expect(content).toContain('Alt+T'); expect(content).toContain('Alt+C'); } } } }); }); describe('create_color_wheel_html with file output', () => { test('should save color wheel HTML file', async () => { const params = { type: 'hsl', size: 400, interactive: true, highlight_colors: ['#ff0000', '#00ff00'], enable_background_controls: true, }; const result = await createColorWheelHtmlTool.handler(params); expect(result.success).toBe(true); if (result.success) { expect(result.visualizations).toHaveProperty('html_file'); if (result.success) { const viz = (result as any).visualizations; if (viz?.html_file) { const content = await fs.readFile(viz.html_file.file_path, 'utf8'); expect(content).toContain('color-wheel'); expect(content).toContain('background-controls'); expect(content).toContain('BackgroundController'); } } } }); test('should handle harmony visualization', async () => { const params = { type: 'hsv', show_harmony: true, harmony_type: 'complementary', highlight_colors: ['#2563eb'], }; const result = await createColorWheelHtmlTool.handler(params); expect(result.success).toBe(true); if (result.success) { const viz = (result as any).visualizations; if (viz?.html_file) { const content = await fs.readFile(viz.html_file.file_path, 'utf8'); expect(content).toContain('harmony'); expect(content).toContain('#2563eb'); } } }); }); describe('create_gradient_html with file output', () => { test('should save gradient HTML file', async () => { const params = { gradient_css: 'linear-gradient(45deg, #ff0000, #0000ff)', preview_shapes: ['rectangle', 'circle'], show_css_code: true, enable_background_controls: true, }; const result = await createGradientHtmlTool.handler(params); expect(result.success).toBe(true); if (result.success) { expect(result.visualizations).toHaveProperty('html_file'); const viz = (result as any).visualizations; if (viz?.html_file) { const content = await fs.readFile(viz.html_file.file_path, 'utf8'); expect(content).toContain('gradient-preview'); expect(content).toContain('linear-gradient(45deg, #ff0000, #0000ff)'); expect(content).toContain('background-controls'); } } }); test('should validate gradient CSS syntax', async () => { const params = { gradient_css: 'invalid-gradient-syntax', }; const result = await createGradientHtmlTool.handler(params); expect(result.success).toBe(false); if (!result.success) { expect(result.error.code).toBe('INVALID_GRADIENT_CSS'); } }); }); describe('create_theme_preview_html with file output', () => { test('should save theme preview HTML file', async () => { const params = { theme_colors: { primary: '#2563eb', secondary: '#64748b', background: '#ffffff', text: '#1e293b', success: '#059669', warning: '#d97706', error: '#dc2626', }, preview_type: 'website', components: ['header', 'content', 'buttons'], enable_background_controls: true, }; const result = await createThemePreviewHtmlTool.handler(params); expect(result.success).toBe(true); if (result.success) { expect(result.visualizations).toHaveProperty('html_file'); expect((result.data as any).color_count).toBe(7); const viz = (result as any).visualizations; if (viz?.html_file) { const content = await fs.readFile(viz.html_file.file_path, 'utf8'); expect(content).toContain('theme-preview'); expect(content).toContain('#2563eb'); expect(content).toContain('background-controls'); } } }); test('should validate theme colors', async () => { const params = { theme_colors: { primary: 'invalid-color', }, preview_type: 'dashboard', }; const result = await createThemePreviewHtmlTool.handler(params); expect(result.success).toBe(false); if (!result.success) { expect(result.error.code).toBe('INVALID_COLOR_FORMAT'); } }); }); describe('File system operations', () => { test('should create files with proper naming convention', async () => { const params = { palette: ['#ff0000'], }; const result = await createPaletteHtmlTool.handler(params); expect(result.success).toBe(true); if (result.success) { const viz = (result as any).visualizations; if (viz?.html_file) { const fileName = viz.html_file.filename; expect(fileName).toMatch( /mcp-color-html-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[a-f0-9]{8}\.html/ ); expect(fileName).toContain('.html'); } } }); test('should handle concurrent file operations', async () => { const promises = Array.from({ length: 5 }, (_, i) => createPaletteHtmlTool.handler({ palette: [`#${i.toString(16).padStart(6, '0')}`], }) ); const results = await Promise.all(promises); results.forEach(result => { expect(result.success).toBe(true); }); // Verify all files have unique names const fileNames = results .filter(r => r.success && (r as any).visualizations?.html_file) .map(r => (r as any).visualizations.html_file.filename); const uniqueNames = new Set(fileNames); expect(uniqueNames.size).toBe(fileNames.length); }); test('should include proper file metadata', async () => { const params = { palette: ['#ff0000', '#00ff00', '#0000ff'], }; const result = await createPaletteHtmlTool.handler(params); expect(result.success).toBe(true); if (result.success) { const viz = (result as any).visualizations; if (viz?.html_file) { const fileInfo = viz.html_file; expect(fileInfo.type).toBe('html'); expect(fileInfo.size).toBeGreaterThan(0); expect(fileInfo.created_at).toBeDefined(); expect(fileInfo.description).toContain( 'Enhanced palette visualization' ); expect(fileInfo.description).toContain('3 colors'); } } }); }); describe('Background controls integration', () => { test('should include background controls in all HTML files', async () => { const testCases = [ { tool: createPaletteHtmlTool, params: { palette: ['#ff0000'] }, }, { tool: createColorWheelHtmlTool, params: { type: 'hsl' }, }, { tool: createGradientHtmlTool, params: { gradient_css: 'linear-gradient(45deg, #ff0000, #0000ff)' }, }, { tool: createThemePreviewHtmlTool, params: { theme_colors: { primary: '#2563eb' } }, }, ]; for (const testCase of testCases) { const result = await testCase.tool.handler(testCase.params); expect(result.success).toBe(true); if (result.success) { const viz = (result as any).visualizations; if (viz?.html_file) { const content = await fs.readFile(viz.html_file.file_path, 'utf8'); // Check for background controls elements expect(content).toContain('background-controls'); expect(content).toContain('background-toggle-btn'); expect(content).toContain('color-picker-toggle'); expect(content).toContain('BackgroundController'); // Check for accessibility features expect(content).toContain('<kbd>Alt</kbd> + <kbd>T</kbd>'); expect(content).toContain('<kbd>Alt</kbd> + <kbd>C</kbd>'); expect(content).toContain('aria-label'); expect(content).toContain('role='); } } } }); test('should allow disabling background controls', async () => { const params = { palette: ['#ff0000'], enable_background_controls: false, }; const result = await createPaletteHtmlTool.handler(params); expect(result.success).toBe(true); if (result.success) { expect((result.data as any).background_controls_enabled).toBe(false); } }); }); describe('Error handling and recovery', () => { test('should handle invalid file paths gracefully', async () => { // Mock file output manager to simulate failure const originalSaveFile = fileOutputManager.saveFile; fileOutputManager.saveFile = jest .fn() .mockRejectedValue(new Error('File system error')); const params = { palette: ['#ff0000'], }; const result = await createPaletteHtmlTool.handler(params); expect(result.success).toBe(true); if (result.success) { // Should fall back to HTML content expect(result.visualizations).toHaveProperty('html'); expect(result.visualizations?.html).toContain('<!DOCTYPE html>'); } // Restore original method fileOutputManager.saveFile = originalSaveFile; }); test('should validate file permissions', async () => { // Create read-only directory const readOnlyDir = join(testDir, 'readonly'); await fs.mkdir(readOnlyDir); await fs.chmod(readOnlyDir, 0o444); process.env['COLOR_MCP_VISUALIZATIONS_DIR'] = readOnlyDir; environmentConfig.reloadConfiguration(); const params = { palette: ['#ff0000'], }; const result = await createPaletteHtmlTool.handler(params); // Should still succeed with fallback expect(result.success).toBe(true); }); }); });

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/keyurgolani/ColorMcp'

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