Skip to main content
Glama
bundlerServiceFactory.test.ts8.93 kB
/** * BundlerServiceFactory Tests * * TESTING PATTERNS: * - Unit tests with mocked dependencies * - Test factory pattern, caching, custom service loading * - Cover success cases, edge cases, and error handling * * CODING STANDARDS: * - Use descriptive test names (should...) * - Arrange-Act-Assert pattern * - Mock external dependencies * - Test behavior, not implementation */ import type { Mock } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { BaseBundlerService, DevServerResult, PrerenderResult, ServeComponentResult, } from '../../../src/services/BundlerService'; /** * Creates a mock bundler service with all required methods properly typed */ function createMockBundlerService(overrides: Partial<{ bundlerId: string; frameworkId: string; }>= {}): BaseBundlerService { const { bundlerId = 'mock', frameworkId = 'mock' } = overrides; return { config: {}, getBundlerId: vi.fn(() => bundlerId), getFrameworkId: vi.fn(() => frameworkId), startDevServer: vi.fn((): Promise<DevServerResult> => Promise.resolve({ url: 'http://localhost:3000', port: 3000 })), serveComponent: vi.fn((): Promise<ServeComponentResult> => Promise.resolve({ url: 'http://localhost:3000/component' })), prerenderComponent: vi.fn((): Promise<PrerenderResult> => Promise.resolve({ htmlFilePath: '/tmp/component.html' })), isServerRunning: vi.fn(() => false), getServerUrl: vi.fn(() => null), getServerPort: vi.fn(() => null), getCurrentAppPath: vi.fn(() => null), cleanup: vi.fn((): Promise<void> => Promise.resolve()), } as BaseBundlerService; } /** * Mock structure for @agiflowai/aicode-utils module */ interface MockAicodeUtils { log: { info: Mock; warn: Mock; error: Mock; debug: Mock; }; TemplatesManagerService: { getWorkspaceRootSync: Mock<[], string>; }; } /** * Mock structure for config module */ interface MockConfig { getBundlerConfig: Mock; } // Mock dependencies before importing vi.mock('@agiflowai/aicode-utils', (): MockAicodeUtils => ({ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }, TemplatesManagerService: { getWorkspaceRootSync: vi.fn((): string => '/mock/workspace'), }, })); vi.mock('../../../src/config', (): MockConfig => ({ getBundlerConfig: vi.fn(), })); // Mock ViteReactBundlerService const mockViteService = { getBundlerId: vi.fn(() => 'vite'), getFrameworkId: vi.fn(() => 'react'), startDevServer: vi.fn(), serveComponent: vi.fn(), prerenderComponent: vi.fn(), isServerRunning: vi.fn(() => false), getServerUrl: vi.fn(() => null), getServerPort: vi.fn(() => null), getCurrentAppPath: vi.fn(() => null), cleanup: vi.fn(), }; vi.mock('../../../src/services/BundlerService/ViteReactBundlerService', () => ({ ViteReactBundlerService: { getInstance: vi.fn(() => mockViteService), }, })); import { getBundlerConfig } from '../../../src/config'; import { bundlerRegistry, createDefaultBundlerService, getBundlerService, getBundlerServiceFromConfig, registerBundlerService, resetBundlerServiceCache, ViteReactBundlerService, } from '../../../src/services/BundlerService'; describe('BundlerServiceFactory', () => { beforeEach(() => { vi.clearAllMocks(); resetBundlerServiceCache(); // Clear any custom registry entries except vite-react for (const key of bundlerRegistry.keys()) { if (key !== 'vite-react') { bundlerRegistry.delete(key); } } }); describe('createDefaultBundlerService', () => { it('should return ViteReactBundlerService singleton instance', () => { const result = createDefaultBundlerService(); expect(result).toBe(mockViteService); expect(ViteReactBundlerService.getInstance).toHaveBeenCalledTimes(1); }); }); describe('bundlerRegistry', () => { it('should have vite-react registered by default', () => { expect(bundlerRegistry.has('vite-react')).toBe(true); }); }); describe('getBundlerService', () => { it('should return registered bundler service by key', () => { const result = getBundlerService('vite-react'); expect(result).toBe(mockViteService); }); it('should fall back to default bundler for unknown key', () => { const result = getBundlerService('unknown-bundler'); expect(result).toBe(mockViteService); }); }); describe('registerBundlerService', () => { it('should register a custom bundler service factory', () => { const customService = createMockBundlerService({ bundlerId: 'custom', frameworkId: 'custom' }); const customFactory = vi.fn(() => customService); registerBundlerService('custom-bundler', customFactory); const result = getBundlerService('custom-bundler'); expect(result).toBe(customService); expect(customFactory).toHaveBeenCalled(); }); it('should allow overriding existing bundler', () => { const customService = createMockBundlerService({ bundlerId: 'override', frameworkId: 'override' }); const customFactory = vi.fn(() => customService); registerBundlerService('vite-react', customFactory); const result = getBundlerService('vite-react'); expect(result).toBe(customService); }); }); describe('getBundlerServiceFromConfig', () => { it('should return cached service on subsequent calls', async () => { vi.mocked(getBundlerConfig).mockResolvedValue(undefined); const result1 = await getBundlerServiceFromConfig(); const result2 = await getBundlerServiceFromConfig(); expect(result1).toBe(result2); expect(getBundlerConfig).toHaveBeenCalledTimes(1); }); it('should return default service when no custom service is configured', async () => { vi.mocked(getBundlerConfig).mockResolvedValue(undefined); const result = await getBundlerServiceFromConfig(); expect(result).toBe(mockViteService); }); it('should return default service when bundler config has no customService', async () => { vi.mocked(getBundlerConfig).mockResolvedValue({}); const result = await getBundlerServiceFromConfig(); expect(result).toBe(mockViteService); }); it('should fall back to default service when custom service import fails', async () => { vi.mocked(getBundlerConfig).mockResolvedValue({ customService: 'path/to/nonexistent-service.ts', }); const result = await getBundlerServiceFromConfig(); expect(result).toBe(mockViteService); }); it('should reset cache correctly', async () => { vi.mocked(getBundlerConfig).mockResolvedValue(undefined); await getBundlerServiceFromConfig(); resetBundlerServiceCache(); // After reset, getBundlerConfig should be called again await getBundlerServiceFromConfig(); expect(getBundlerConfig).toHaveBeenCalledTimes(2); }); }); describe('resetBundlerServiceCache', () => { it('should clear cached bundler service', async () => { vi.mocked(getBundlerConfig).mockResolvedValue(undefined); await getBundlerServiceFromConfig(); expect(getBundlerConfig).toHaveBeenCalledTimes(1); resetBundlerServiceCache(); await getBundlerServiceFromConfig(); expect(getBundlerConfig).toHaveBeenCalledTimes(2); }); }); describe('edge cases', () => { it('should handle empty string bundler key by falling back to default', () => { const result = getBundlerService(''); expect(result).toBe(mockViteService); }); it('should handle registering bundler with empty string key', () => { const customService = createMockBundlerService({ bundlerId: 'empty-key', frameworkId: 'test' }); const customFactory = vi.fn(() => customService); registerBundlerService('', customFactory); const result = getBundlerService(''); expect(result).toBe(customService); }); it('should handle factory that throws error by propagating the error', () => { const throwingFactory = vi.fn(() => { throw new Error('Factory error'); }); registerBundlerService('throwing-bundler', throwingFactory); expect(() => getBundlerService('throwing-bundler')).toThrow('Factory error'); }); it('should handle concurrent calls to getBundlerServiceFromConfig', async () => { vi.mocked(getBundlerConfig).mockResolvedValue(undefined); const [result1, result2, result3] = await Promise.all([ getBundlerServiceFromConfig(), getBundlerServiceFromConfig(), getBundlerServiceFromConfig(), ]); expect(result1).toBe(result2); expect(result2).toBe(result3); }); it('should handle whitespace-only bundler key', () => { const result = getBundlerService(' '); expect(result).toBe(mockViteService); }); }); });

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/AgiFlow/aicode-toolkit'

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