| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; |
| import { EventEmitter } from 'events'; |
| import path from 'path'; |
| import os from 'os'; |
| import fs from 'fs/promises'; |
|
|
| |
| vi.mock('child_process', () => ({ |
| spawn: vi.fn(), |
| execSync: vi.fn(), |
| execFile: vi.fn(), |
| })); |
|
|
| |
| vi.mock('@/lib/secure-fs.js', () => ({ |
| access: vi.fn(), |
| })); |
|
|
| |
| vi.mock('net', () => ({ |
| default: { |
| createServer: vi.fn(), |
| }, |
| createServer: vi.fn(), |
| })); |
|
|
| import { spawn, execSync } from 'child_process'; |
| import * as secureFs from '@/lib/secure-fs.js'; |
| import net from 'net'; |
|
|
| describe('dev-server-service.ts', () => { |
| let testDir: string; |
| let originalHostname: string | undefined; |
|
|
| beforeEach(async () => { |
| vi.clearAllMocks(); |
| vi.resetModules(); |
|
|
| |
| originalHostname = process.env.HOSTNAME; |
| process.env.HOSTNAME = 'localhost'; |
|
|
| testDir = path.join(os.tmpdir(), `dev-server-test-${Date.now()}`); |
| await fs.mkdir(testDir, { recursive: true }); |
|
|
| |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| |
| const mockServer = new EventEmitter() as any; |
| mockServer.listen = vi.fn().mockImplementation((port: number, host: string) => { |
| process.nextTick(() => mockServer.emit('listening')); |
| }); |
| mockServer.close = vi.fn(); |
| vi.mocked(net.createServer).mockReturnValue(mockServer); |
|
|
| |
| vi.mocked(execSync).mockImplementation(() => { |
| throw new Error('No process found'); |
| }); |
| }); |
|
|
| afterEach(async () => { |
| |
| if (originalHostname === undefined) { |
| delete process.env.HOSTNAME; |
| } else { |
| process.env.HOSTNAME = originalHostname; |
| } |
|
|
| try { |
| await fs.rm(testDir, { recursive: true, force: true }); |
| } catch { |
| |
| } |
| }); |
|
|
| describe('getDevServerService', () => { |
| it('should return a singleton instance', async () => { |
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
|
|
| const instance1 = getDevServerService(); |
| const instance2 = getDevServerService(); |
|
|
| expect(instance1).toBe(instance2); |
| }); |
| }); |
|
|
| describe('startDevServer', () => { |
| it('should return error if worktree path does not exist', async () => { |
| vi.mocked(secureFs.access).mockRejectedValueOnce(new Error('File not found')); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| const result = await service.startDevServer('/project', '/nonexistent/worktree'); |
|
|
| expect(result.success).toBe(false); |
| expect(result.error).toContain('does not exist'); |
| }); |
|
|
| it('should return error if no package.json found', async () => { |
| vi.mocked(secureFs.access).mockImplementation(async (p: any) => { |
| if (typeof p === 'string' && p.includes('package.json')) { |
| throw new Error('File not found'); |
| } |
| return undefined; |
| }); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| const result = await service.startDevServer(testDir, testDir); |
|
|
| expect(result.success).toBe(false); |
| expect(result.error).toContain('No package.json found'); |
| }); |
|
|
| it('should detect npm as package manager with package-lock.json', async () => { |
| vi.mocked(secureFs.access).mockImplementation(async (p: any) => { |
| const pathStr = typeof p === 'string' ? p : ''; |
| if (pathStr.includes('bun.lockb')) throw new Error('Not found'); |
| if (pathStr.includes('pnpm-lock.yaml')) throw new Error('Not found'); |
| if (pathStr.includes('yarn.lock')) throw new Error('Not found'); |
| if (pathStr.includes('package-lock.json')) return undefined; |
| if (pathStr.includes('package.json')) return undefined; |
| return undefined; |
| }); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| expect(spawn).toHaveBeenCalledWith('npm', ['run', 'dev'], expect.any(Object)); |
| }); |
|
|
| it('should detect yarn as package manager with yarn.lock', async () => { |
| vi.mocked(secureFs.access).mockImplementation(async (p: any) => { |
| const pathStr = typeof p === 'string' ? p : ''; |
| if (pathStr.includes('bun.lockb')) throw new Error('Not found'); |
| if (pathStr.includes('pnpm-lock.yaml')) throw new Error('Not found'); |
| if (pathStr.includes('yarn.lock')) return undefined; |
| if (pathStr.includes('package.json')) return undefined; |
| return undefined; |
| }); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| expect(spawn).toHaveBeenCalledWith('yarn', ['dev'], expect.any(Object)); |
| }); |
|
|
| it('should detect pnpm as package manager with pnpm-lock.yaml', async () => { |
| vi.mocked(secureFs.access).mockImplementation(async (p: any) => { |
| const pathStr = typeof p === 'string' ? p : ''; |
| if (pathStr.includes('bun.lockb')) throw new Error('Not found'); |
| if (pathStr.includes('pnpm-lock.yaml')) return undefined; |
| if (pathStr.includes('package.json')) return undefined; |
| return undefined; |
| }); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| expect(spawn).toHaveBeenCalledWith('pnpm', ['run', 'dev'], expect.any(Object)); |
| }); |
|
|
| it('should detect bun as package manager with bun.lockb', async () => { |
| vi.mocked(secureFs.access).mockImplementation(async (p: any) => { |
| const pathStr = typeof p === 'string' ? p : ''; |
| if (pathStr.includes('bun.lockb')) return undefined; |
| if (pathStr.includes('package.json')) return undefined; |
| return undefined; |
| }); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| expect(spawn).toHaveBeenCalledWith('bun', ['run', 'dev'], expect.any(Object)); |
| }); |
|
|
| it('should return existing server info if already running', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| |
| const result1 = await service.startDevServer(testDir, testDir); |
| expect(result1.success).toBe(true); |
|
|
| |
| const result2 = await service.startDevServer(testDir, testDir); |
| expect(result2.success).toBe(true); |
| expect(result2.result?.message).toContain('already running'); |
| }); |
|
|
| it('should start dev server successfully', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| const result = await service.startDevServer(testDir, testDir); |
|
|
| expect(result.success).toBe(true); |
| expect(result.result).toBeDefined(); |
| expect(result.result?.port).toBeGreaterThanOrEqual(3001); |
| expect(result.result?.url).toContain('http://localhost:'); |
| }); |
| }); |
|
|
| describe('stopDevServer', () => { |
| it('should return success if server not found', async () => { |
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| const result = await service.stopDevServer('/nonexistent/path'); |
|
|
| expect(result.success).toBe(true); |
| expect(result.result?.message).toContain('already stopped'); |
| }); |
|
|
| it('should stop a running server', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| |
| await service.startDevServer(testDir, testDir); |
|
|
| |
| const result = await service.stopDevServer(testDir); |
|
|
| expect(result.success).toBe(true); |
| expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM'); |
| }); |
| }); |
|
|
| describe('listDevServers', () => { |
| it('should return empty list when no servers running', async () => { |
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| const result = service.listDevServers(); |
|
|
| expect(result.success).toBe(true); |
| expect(result.result.servers).toEqual([]); |
| }); |
|
|
| it('should list running servers', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| const result = service.listDevServers(); |
|
|
| expect(result.success).toBe(true); |
| expect(result.result.servers.length).toBeGreaterThanOrEqual(1); |
| expect(result.result.servers[0].worktreePath).toBe(testDir); |
| }); |
| }); |
|
|
| describe('isRunning', () => { |
| it('should return false for non-running server', async () => { |
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| expect(service.isRunning('/some/path')).toBe(false); |
| }); |
|
|
| it('should return true for running server', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| expect(service.isRunning(testDir)).toBe(true); |
| }); |
| }); |
|
|
| describe('getServerInfo', () => { |
| it('should return undefined for non-running server', async () => { |
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| expect(service.getServerInfo('/some/path')).toBeUndefined(); |
| }); |
|
|
| it('should return info for running server', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| const info = service.getServerInfo(testDir); |
| expect(info).toBeDefined(); |
| expect(info?.worktreePath).toBe(testDir); |
| expect(info?.port).toBeGreaterThanOrEqual(3001); |
| }); |
| }); |
|
|
| describe('getAllocatedPorts', () => { |
| it('should return allocated ports', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| const ports = service.getAllocatedPorts(); |
| expect(ports.length).toBeGreaterThanOrEqual(1); |
| expect(ports[0]).toBeGreaterThanOrEqual(3001); |
| }); |
| }); |
|
|
| describe('stopAll', () => { |
| it('should stop all running servers', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| await service.stopAll(); |
|
|
| expect(service.listDevServers().result.servers).toHaveLength(0); |
| }); |
| }); |
|
|
| describe('URL detection from output', () => { |
| it('should detect Vite format URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| |
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit('data', Buffer.from(' VITE v5.0.0 ready in 123 ms\n')); |
| mockProcess.stdout.emit('data', Buffer.from(' ➜ Local: http://localhost:5173/\n')); |
|
|
| |
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:5173/'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should detect Next.js format URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit( |
| 'data', |
| Buffer.from('ready - started server on 0.0.0.0:3000, url: http://localhost:3000\n') |
| ); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:3000'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should detect generic localhost URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit('data', Buffer.from('Server running at http://localhost:8080\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:8080'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should keep initial URL if no URL detected in output', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| const result = await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit('data', Buffer.from('Server starting...\n')); |
| mockProcess.stdout.emit('data', Buffer.from('Ready!\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| |
| expect(serverInfo?.url).toBe(result.result?.url); |
| expect(serverInfo?.urlDetected).toBe(false); |
| }); |
|
|
| it('should detect HTTPS URLs', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit('data', Buffer.from('Server listening at https://localhost:3443\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('https://localhost:3443'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should only detect URL once (not update after first detection)', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit('data', Buffer.from('Local: http://localhost:5173/\n')); |
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const firstUrl = service.getServerInfo(testDir)?.url; |
|
|
| |
| mockProcess.stdout.emit('data', Buffer.from('Network: http://192.168.1.1:5173/\n')); |
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| |
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe(firstUrl); |
| expect(serverInfo?.url).toBe('http://localhost:5173/'); |
| }); |
|
|
| it('should detect Astro format URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit('data', Buffer.from(' 🚀 astro v4.0.0 started in 200ms\n')); |
| mockProcess.stdout.emit('data', Buffer.from(' ┃ Local http://localhost:4321/\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| |
| expect(serverInfo?.url).toBe('http://localhost:4321/'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should detect Remix format URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| mockProcess.stdout.emit( |
| 'data', |
| Buffer.from('Remix App Server started at http://localhost:3000\n') |
| ); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:3000'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should detect Django format URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| mockProcess.stdout.emit( |
| 'data', |
| Buffer.from('Starting development server at http://127.0.0.1:8000/\n') |
| ); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://127.0.0.1:8000/'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should detect Webpack Dev Server format URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| mockProcess.stdout.emit( |
| 'data', |
| Buffer.from('<i> [webpack-dev-server] Project is running at http://localhost:8080/\n') |
| ); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:8080/'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should detect PHP built-in server format URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| mockProcess.stdout.emit( |
| 'data', |
| Buffer.from('Development Server (http://localhost:8000) started\n') |
| ); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:8000'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should detect "listening on port" format (port-only detection)', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit('data', Buffer.from('Server listening on port 4000\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:4000'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should detect "running on port" format (port-only detection)', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| mockProcess.stdout.emit('data', Buffer.from('Application running on port 9000\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:9000'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should strip ANSI escape codes before detecting URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit( |
| 'data', |
| Buffer.from( |
| ' \x1B[32m➜\x1B[0m \x1B[1mLocal:\x1B[0m \x1B[36mhttp://localhost:5173/\x1B[0m\n' |
| ) |
| ); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:5173/'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should normalize 0.0.0.0 to localhost', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| mockProcess.stdout.emit('data', Buffer.from('Server listening at http://0.0.0.0:3000\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:3000'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should normalize [::] to localhost', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| mockProcess.stdout.emit('data', Buffer.from('Local: http://[::]:4000/\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:4000/'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should update port field when detected URL has different port', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| const result = await service.startDevServer(testDir, testDir); |
| const allocatedPort = result.result?.port; |
|
|
| |
| mockProcess.stdout.emit('data', Buffer.from('Local: http://localhost:9999/\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:9999/'); |
| expect(serverInfo?.port).toBe(9999); |
| |
| if (allocatedPort !== 9999) { |
| expect(serverInfo?.port).not.toBe(allocatedPort); |
| } |
| }); |
|
|
| it('should detect URL from stderr output', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stderr.emit('data', Buffer.from('Local: http://localhost:3000/\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:3000/'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should not match URLs without a port (non-dev-server URLs)', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| const result = await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit( |
| 'data', |
| Buffer.from('Downloading from https://cdn.example.com/bundle.js\n') |
| ); |
| mockProcess.stdout.emit('data', Buffer.from('Fetching https://registry.npmjs.org/package\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| |
| expect(serverInfo?.url).toBe(result.result?.url); |
| expect(serverInfo?.urlDetected).toBe(false); |
| }); |
|
|
| it('should handle URLs with trailing punctuation', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stdout.emit('data', Buffer.from('Server started at http://localhost:3000.\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:3000'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should detect Express/Fastify format URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| mockProcess.stdout.emit('data', Buffer.from('Server listening on http://localhost:3000\n')); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:3000'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
|
|
| it('should detect Angular CLI format URL', async () => { |
| vi.mocked(secureFs.access).mockResolvedValue(undefined); |
|
|
| const mockProcess = createMockProcess(); |
| vi.mocked(spawn).mockReturnValue(mockProcess as any); |
|
|
| const { getDevServerService } = await import('@/services/dev-server-service.js'); |
| const service = getDevServerService(); |
|
|
| await service.startDevServer(testDir, testDir); |
|
|
| |
| mockProcess.stderr.emit( |
| 'data', |
| Buffer.from( |
| '** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **\n' |
| ) |
| ); |
|
|
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| const serverInfo = service.getServerInfo(testDir); |
| expect(serverInfo?.url).toBe('http://localhost:4200/'); |
| expect(serverInfo?.urlDetected).toBe(true); |
| }); |
| }); |
| }); |
|
|
| |
| function createMockProcess() { |
| const mockProcess = new EventEmitter() as any; |
| mockProcess.stdout = new EventEmitter(); |
| mockProcess.stderr = new EventEmitter(); |
| mockProcess.kill = vi.fn(); |
| mockProcess.killed = false; |
| mockProcess.pid = 12345; |
|
|
| |
| return mockProcess; |
| } |
|
|