languageServer.test.ts•10.7 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { LanguageServerManager } from '../../src/languageServerManager'
import { LanguageServerService } from '../../src/languageServerService'
import type { LanguageServerConfig, MonorepoProject } from '../../src/types/languageServer'
import * as fs from 'fs/promises'
import { existsSync } from 'fs'
// Mock the fs module
vi.mock('fs/promises')
vi.mock('fs', () => ({
existsSync: vi.fn(),
}))
describe('LanguageServerManager', () => {
let manager: LanguageServerManager
let mockConfig: LanguageServerConfig
beforeEach(() => {
vi.clearAllMocks()
mockConfig = {
monorepoRoot: '/test/monorepo',
projects: [],
}
manager = new LanguageServerManager(mockConfig)
})
describe('discoverProjects', () => {
it('should discover TypeScript projects in monorepo', async () => {
// Mock file system structure
vi.mocked(fs.readdir).mockImplementation((dir, _options) => {
if (dir === '/test/monorepo') {
return Promise.resolve([
{ name: 'package-a', isDirectory: () => true },
{ name: 'package-b', isDirectory: () => true },
{ name: 'node_modules', isDirectory: () => true },
{ name: '.git', isDirectory: () => true },
] as any)
} else if (dir === '/test/monorepo/package-a') {
return Promise.resolve([
{ name: 'tsconfig.json', isDirectory: () => false },
{ name: 'package.json', isDirectory: () => false },
] as any)
} else if (dir === '/test/monorepo/package-b') {
return Promise.resolve([
{ name: 'tsconfig.json', isDirectory: () => false },
{ name: 'package.json', isDirectory: () => false },
] as any)
}
return Promise.resolve([])
})
vi.mocked(fs.readFile).mockImplementation((filePath) => {
const pathStr = filePath as string
if (pathStr.includes('package-a/package.json')) {
return Promise.resolve(
JSON.stringify({
name: 'package-a',
dependencies: {},
})
)
} else if (pathStr.includes('package-b/package.json')) {
return Promise.resolve(
JSON.stringify({
name: 'package-b',
devDependencies: {
'@sveltejs/kit': '^1.0.0',
},
})
)
}
return Promise.resolve('{}')
})
const projects = await manager.discoverProjects()
expect(projects).toHaveLength(2)
expect(projects[0]).toEqual({
name: 'package-a',
path: 'package-a',
type: 'node',
tsconfigPath: 'tsconfig.json',
rootDir: '/test/monorepo/package-a',
})
expect(projects[1]).toEqual({
name: 'package-b',
path: 'package-b',
type: 'sveltekit',
tsconfigPath: 'tsconfig.json',
rootDir: '/test/monorepo/package-b',
})
})
it('should skip hidden directories and node_modules', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
{ name: '.hidden', isDirectory: () => true },
{ name: 'node_modules', isDirectory: () => true },
{ name: 'valid-package', isDirectory: () => true },
] as any)
await manager.discoverProjects()
// Should only process valid-package
expect(fs.readdir).toHaveBeenCalledWith('/test/monorepo', { withFileTypes: true })
expect(fs.readdir).not.toHaveBeenCalledWith(
'/test/monorepo/.hidden',
expect.any(Object)
)
expect(fs.readdir).not.toHaveBeenCalledWith(
'/test/monorepo/node_modules',
expect.any(Object)
)
})
})
describe('getProjectForFile', () => {
it('should return undefined for files outside any project', () => {
const result = manager.getProjectForFile('/outside/file.ts')
expect(result).toBeUndefined()
})
})
})
describe('LanguageServerService', () => {
let service: LanguageServerService
beforeEach(() => {
vi.clearAllMocks()
service = new LanguageServerService()
})
describe('initialize', () => {
it('should initialize with auto-discovery', async () => {
vi.mocked(fs.readdir).mockImplementation((dir, _options) => {
if (dir === '/test/monorepo') {
return Promise.resolve([{ name: 'project1', isDirectory: () => true }] as any)
} else if (dir === '/test/monorepo/project1') {
return Promise.resolve([
{ name: 'tsconfig.json', isDirectory: () => false },
{ name: 'package.json', isDirectory: () => false },
] as any)
}
return Promise.resolve([])
})
vi.mocked(fs.readFile).mockImplementation((filePath) => {
const pathStr = filePath as string
if (pathStr.includes('package.json')) {
return Promise.resolve(JSON.stringify({ name: 'project1' }))
} else if (pathStr.includes('tsconfig.json')) {
return Promise.resolve(
JSON.stringify({
compilerOptions: {
target: 'es2020',
module: 'commonjs',
skipLibCheck: true,
noLib: true,
types: [],
typeRoots: [],
noResolve: true,
isolatedModules: true,
},
exclude: ['node_modules', '**/*.spec.ts', '**/*.test.ts'],
})
)
}
return Promise.resolve('{}')
})
vi.mocked(existsSync).mockReturnValue(true)
const result = await service.initialize('/test/monorepo', true)
expect(result.projects).toHaveLength(1)
expect(result.projects[0]?.name).toBe('project1')
})
it('should initialize without auto-discovery', async () => {
const result = await service.initialize('/test/monorepo', false)
expect(result.projects).toHaveLength(0)
})
})
describe('configureProject', () => {
it('should add a new project configuration', async () => {
vi.mocked(fs.readFile).mockResolvedValue(
JSON.stringify({
compilerOptions: {
target: 'es2020',
module: 'commonjs',
skipLibCheck: true,
noLib: true,
types: [],
typeRoots: [],
noResolve: true,
isolatedModules: true,
},
exclude: ['node_modules', '**/*.spec.ts', '**/*.test.ts'],
})
)
vi.mocked(existsSync).mockReturnValue(true)
await service.initialize('/test/monorepo', false)
const project: MonorepoProject = {
name: 'test-project',
path: 'packages/test',
type: 'node',
tsconfigPath: 'tsconfig.json',
rootDir: '/test/monorepo/packages/test',
}
await service.configureProject(project)
// Project should be added successfully
expect(service.isInitialized()).toBe(true)
})
})
describe('getDiagnostics', () => {
it('should return empty diagnostics when not initialized', () => {
const result = service.getDiagnostics({})
expect(result).toEqual([])
})
it('should filter diagnostics by severity', async () => {
vi.mocked(fs.readFile).mockResolvedValue('{}')
vi.mocked(existsSync).mockReturnValue(true)
await service.initialize('/test/monorepo', false)
// Since we're mocking, we can't test actual TypeScript diagnostics
// but we can test the filtering logic
const result = service.getDiagnostics({ severity: 'error' })
expect(result).toBeDefined()
})
})
describe('getCompletions', () => {
it('should return empty completions when not initialized', () => {
const result = service.getCompletions('/test/file.ts', 1, 1)
expect(result).toEqual({ completions: [] })
})
})
describe('getHover', () => {
it('should return undefined when not initialized', () => {
const result = service.getHover('/test/file.ts', 1, 1)
expect(result).toBeUndefined()
})
})
describe('getDefinition', () => {
it('should return undefined when not initialized', () => {
const result = service.getDefinition('/test/file.ts', 1, 1)
expect(result).toBeUndefined()
})
})
describe('findReferences', () => {
it('should return empty references when not initialized', () => {
const result = service.findReferences('/test/file.ts', 1, 1)
expect(result).toEqual({ references: [] })
})
})
describe('renameSymbol', () => {
it('should return empty changes when not initialized', () => {
const result = service.renameSymbol('/test/file.ts', 1, 1, 'newName')
expect(result).toEqual({ changes: {} })
})
})
describe('getTypeInfo', () => {
it('should return undefined when not initialized', () => {
const result = service.getTypeInfo('/test/file.ts', 1, 1)
expect(result).toBeUndefined()
})
})
})