Skip to content

Commit 2ab5b5f

Browse files
committed
Refactor server testing infra
This builds out completely new APIs for testing language servers as well as stubs itself into the existing infra so the existing tests can be migrated incrementally
1 parent 00dd24f commit 2ab5b5f

File tree

8 files changed

+1038
-269
lines changed

8 files changed

+1038
-269
lines changed

packages/tailwindcss-language-server/src/testing/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { afterAll, onTestFinished, test, TestOptions } from 'vitest'
1+
import { onTestFinished, test, TestOptions } from 'vitest'
22
import * as fs from 'node:fs/promises'
33
import * as path from 'node:path'
44
import * as proc from 'node:child_process'
@@ -16,7 +16,7 @@ export interface Storage {
1616

1717
export interface TestConfig<Extras extends {}> {
1818
name: string
19-
fs: Storage
19+
fs?: Storage
2020
prepare?(utils: TestUtils): Promise<Extras>
2121
handle(utils: TestUtils & Extras): void | Promise<void>
2222

@@ -43,8 +43,10 @@ async function setup<T>(config: TestConfig<T>): Promise<TestUtils> {
4343

4444
await fs.mkdir(baseDir, { recursive: true })
4545

46-
await prepareFileSystem(baseDir, config.fs)
47-
await installDependencies(baseDir, config.fs)
46+
if (config.fs) {
47+
await prepareFileSystem(baseDir, config.fs)
48+
await installDependencies(baseDir, config.fs)
49+
}
4850

4951
onTestFinished(async (result) => {
5052
// Once done, move all the files to a new location

packages/tailwindcss-language-server/tests/common.ts

Lines changed: 48 additions & 221 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,21 @@
11
import * as path from 'node:path'
22
import { beforeAll, describe } from 'vitest'
3-
import { connect, launch } from './connection'
4-
import {
5-
CompletionRequest,
6-
ConfigurationRequest,
7-
DidChangeConfigurationNotification,
8-
DidChangeTextDocumentNotification,
9-
DidOpenTextDocumentNotification,
10-
InitializeRequest,
11-
InitializedNotification,
12-
RegistrationRequest,
13-
InitializeParams,
14-
DidOpenTextDocumentParams,
15-
MessageType,
16-
} from 'vscode-languageserver-protocol'
17-
import type { ClientCapabilities, ProtocolConnection } from 'vscode-languageclient'
3+
import { DidChangeTextDocumentNotification } from 'vscode-languageserver'
4+
import type { ProtocolConnection } from 'vscode-languageclient'
185
import type { Feature } from '@tailwindcss/language-service/src/features'
19-
import { clearLanguageBoundariesCache } from '@tailwindcss/language-service/src/util/getLanguageBoundaries'
20-
import { CacheMap } from '../src/cache-map'
6+
import { URI } from 'vscode-uri'
7+
import { Client, createClient } from './utils/client'
218

229
type Settings = any
2310

2411
interface FixtureContext
2512
extends Pick<ProtocolConnection, 'sendRequest' | 'sendNotification' | 'onNotification'> {
26-
client: ProtocolConnection
13+
client: Client
2714
openDocument: (params: {
2815
text: string
2916
lang?: string
3017
dir?: string
18+
name?: string | null
3119
settings?: Settings
3220
}) => Promise<{ uri: string; updateSettings: (settings: Settings) => Promise<void> }>
3321
updateSettings: (settings: Settings) => Promise<void>
@@ -57,117 +45,22 @@ export interface InitOptions {
5745
* Extra initialization options to pass to the LSP
5846
*/
5947
options?: Record<string, any>
48+
49+
/**
50+
* Settings to provide the server immediately when it starts
51+
*/
52+
settings?: Settings
6053
}
6154

6255
export async function init(
6356
fixture: string | string[],
6457
opts: InitOptions = {},
6558
): Promise<FixtureContext> {
66-
let settings = {}
67-
let docSettings = new Map<string, Settings>()
68-
69-
const { client } = opts?.mode === 'spawn' ? await launch() : await connect()
70-
71-
if (opts?.mode === 'spawn') {
72-
client.onNotification('window/logMessage', ({ message, type }) => {
73-
if (type === MessageType.Error) {
74-
console.error(message)
75-
} else if (type === MessageType.Warning) {
76-
console.warn(message)
77-
} else if (type === MessageType.Info) {
78-
console.info(message)
79-
} else if (type === MessageType.Log) {
80-
console.log(message)
81-
} else if (type === MessageType.Debug) {
82-
console.debug(message)
83-
}
84-
})
85-
}
86-
87-
const capabilities: ClientCapabilities = {
88-
textDocument: {
89-
codeAction: { dynamicRegistration: true },
90-
codeLens: { dynamicRegistration: true },
91-
colorProvider: { dynamicRegistration: true },
92-
completion: {
93-
completionItem: {
94-
commitCharactersSupport: true,
95-
documentationFormat: ['markdown', 'plaintext'],
96-
snippetSupport: true,
97-
},
98-
completionItemKind: {
99-
valueSet: [
100-
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
101-
25,
102-
],
103-
},
104-
contextSupport: true,
105-
dynamicRegistration: true,
106-
},
107-
definition: { dynamicRegistration: true },
108-
documentHighlight: { dynamicRegistration: true },
109-
documentLink: { dynamicRegistration: true },
110-
documentSymbol: {
111-
dynamicRegistration: true,
112-
symbolKind: {
113-
valueSet: [
114-
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
115-
25, 26,
116-
],
117-
},
118-
},
119-
formatting: { dynamicRegistration: true },
120-
hover: {
121-
contentFormat: ['markdown', 'plaintext'],
122-
dynamicRegistration: true,
123-
},
124-
implementation: { dynamicRegistration: true },
125-
onTypeFormatting: { dynamicRegistration: true },
126-
publishDiagnostics: { relatedInformation: true },
127-
rangeFormatting: { dynamicRegistration: true },
128-
references: { dynamicRegistration: true },
129-
rename: { dynamicRegistration: true },
130-
signatureHelp: {
131-
dynamicRegistration: true,
132-
signatureInformation: { documentationFormat: ['markdown', 'plaintext'] },
133-
},
134-
synchronization: {
135-
didSave: true,
136-
dynamicRegistration: true,
137-
willSave: true,
138-
willSaveWaitUntil: true,
139-
},
140-
typeDefinition: { dynamicRegistration: true },
141-
},
142-
workspace: {
143-
applyEdit: true,
144-
configuration: true,
145-
didChangeConfiguration: { dynamicRegistration: true },
146-
didChangeWatchedFiles: { dynamicRegistration: true },
147-
executeCommand: { dynamicRegistration: true },
148-
symbol: {
149-
dynamicRegistration: true,
150-
symbolKind: {
151-
valueSet: [
152-
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
153-
25, 26,
154-
],
155-
},
156-
},
157-
workspaceEdit: { documentChanges: true },
158-
workspaceFolders: true,
159-
},
160-
experimental: {
161-
tailwind: {
162-
projectDetails: true,
163-
},
164-
},
165-
}
166-
167-
const fixtures = Array.isArray(fixture) ? fixture : [fixture]
59+
let workspaces: Record<string, string> = {}
60+
let fixtures = Array.isArray(fixture) ? fixture : [fixture]
16861

169-
function fixtureUri(fixture: string) {
170-
return `file://${path.resolve('./tests/fixtures', fixture)}`
62+
function fixturePath(fixture: string) {
63+
return path.resolve('./tests/fixtures', fixture)
17164
}
17265

17366
function resolveUri(...parts: string[]) {
@@ -176,86 +69,44 @@ export async function init(
17669
? path.resolve('./tests/fixtures', ...parts)
17770
: path.resolve('./tests/fixtures', fixtures[0], ...parts)
17871

179-
return `file://${filepath}`
72+
return URI.file(filepath).toString()
18073
}
18174

182-
const workspaceFolders = fixtures.map((fixture) => ({
183-
name: `Fixture ${fixture}`,
184-
uri: fixtureUri(fixture),
185-
}))
186-
187-
const rootUri = fixtures.length > 1 ? null : workspaceFolders[0].uri
188-
189-
await client.sendRequest(InitializeRequest.type, {
190-
processId: -1,
191-
rootUri,
192-
capabilities,
193-
trace: 'off',
194-
workspaceFolders,
195-
initializationOptions: {
196-
testMode: true,
197-
...(opts.options ?? {}),
198-
},
199-
} as InitializeParams)
200-
201-
await client.sendNotification(InitializedNotification.type)
202-
203-
client.onRequest(ConfigurationRequest.type, (params) => {
204-
return params.items.map((item) => {
205-
if (docSettings.has(item.scopeUri!)) {
206-
return docSettings.get(item.scopeUri!)[item.section!] ?? {}
207-
}
208-
return settings[item.section!] ?? {}
209-
})
210-
})
211-
212-
let initPromise = new Promise<void>((resolve) => {
213-
client.onRequest(RegistrationRequest.type, ({ registrations }) => {
214-
if (registrations.some((r) => r.method === CompletionRequest.method)) {
215-
resolve()
216-
}
75+
for (let [idx, fixture] of fixtures.entries()) {
76+
workspaces[`Fixture ${idx}`] = fixturePath(fixture)
77+
}
21778

218-
return null
219-
})
79+
let client = await createClient({
80+
server: 'tailwindcss',
81+
mode: opts.mode,
82+
options: opts.options,
83+
root: workspaces,
84+
settings: opts.settings,
22085
})
22186

222-
interface PromiseWithResolvers<T> extends Promise<T> {
223-
resolve: (value?: T | PromiseLike<T>) => void
224-
reject: (reason?: any) => void
225-
}
226-
227-
let openingDocuments = new CacheMap<string, PromiseWithResolvers<void>>()
87+
let counter = 0
22888
let projectDetails: any = null
22989

230-
client.onNotification('@/tailwindCSS/projectDetails', (params) => {
231-
console.log('[TEST] Project detailed changed')
232-
projectDetails = params
90+
client.project().then((project) => {
91+
projectDetails = project
23392
})
23493

235-
client.onNotification('@/tailwindCSS/documentReady', (params) => {
236-
console.log('[TEST] Document ready', params.uri)
237-
openingDocuments.get(params.uri)?.resolve()
238-
})
239-
240-
// This is a global cache that must be reset between tests for accurate results
241-
clearLanguageBoundariesCache()
242-
243-
let counter = 0
244-
24594
return {
24695
client,
247-
fixtureUri,
96+
fixtureUri(fixture: string) {
97+
return URI.file(fixturePath(fixture)).toString()
98+
},
24899
get project() {
249100
return projectDetails
250101
},
251102
sendRequest(type: any, params: any) {
252-
return client.sendRequest(type, params)
103+
return client.conn.sendRequest(type, params)
253104
},
254105
sendNotification(type: any, params?: any) {
255-
return client.sendNotification(type, params)
106+
return client.conn.sendNotification(type, params)
256107
},
257108
onNotification(type: any, callback: any) {
258-
return client.onNotification(type, callback)
109+
return client.conn.onNotification(type, callback)
259110
},
260111
async openDocument({
261112
text,
@@ -267,59 +118,35 @@ export async function init(
267118
text: string
268119
lang?: string
269120
dir?: string
270-
name?: string
121+
name?: string | null
271122
settings?: Settings
272123
}) {
273124
let uri = resolveUri(dir, name ?? `file-${counter++}`)
274-
docSettings.set(uri, settings)
275125

276-
let openPromise = openingDocuments.remember(uri, () => {
277-
let resolve = () => {}
278-
let reject = () => {}
279-
280-
let p = new Promise<void>((_resolve, _reject) => {
281-
resolve = _resolve
282-
reject = _reject
283-
})
284-
285-
return Object.assign(p, {
286-
resolve,
287-
reject,
288-
})
126+
let doc = await client.open({
127+
lang,
128+
text,
129+
uri,
130+
settings,
289131
})
290132

291-
await client.sendNotification(DidOpenTextDocumentNotification.type, {
292-
textDocument: {
293-
uri,
294-
languageId: lang,
295-
version: 1,
296-
text,
297-
},
298-
} as DidOpenTextDocumentParams)
299-
300-
// If opening a document stalls then it's probably because this promise is not being resolved
301-
// This can happen if a document is not covered by one of the selectors because of it's URI
302-
await initPromise
303-
await openPromise
304-
305133
return {
306-
uri,
134+
get uri() {
135+
return doc.uri.toString()
136+
},
307137
async updateSettings(settings: Settings) {
308-
docSettings.set(uri, settings)
309-
await client.sendNotification(DidChangeConfigurationNotification.type)
138+
await doc.update({ settings })
310139
},
311140
}
312141
},
313142

314143
async updateSettings(newSettings: Settings) {
315-
settings = newSettings
316-
await client.sendNotification(DidChangeConfigurationNotification.type)
144+
await client.updateSettings(newSettings)
317145
},
318146

319147
async updateFile(file: string, text: string) {
320148
let uri = resolveUri(file)
321-
322-
await client.sendNotification(DidChangeTextDocumentNotification.type, {
149+
await client.conn.sendNotification(DidChangeTextDocumentNotification.type, {
323150
textDocument: { uri, version: counter++ },
324151
contentChanges: [{ text }],
325152
})
@@ -337,7 +164,7 @@ export function withFixture(fixture: string, callback: (c: FixtureContext) => vo
337164
// to the connection object without having to resort to using a Proxy
338165
Object.setPrototypeOf(c, await init(fixture))
339166

340-
return () => c.client.dispose()
167+
return () => c.client.conn.dispose()
341168
})
342169

343170
callback(c)
@@ -360,7 +187,7 @@ export function withWorkspace({
360187
// to the connection object without having to resort to using a Proxy
361188
Object.setPrototypeOf(c, await init(fixtures))
362189

363-
return () => c.client.dispose()
190+
return () => c.client.conn.dispose()
364191
})
365192

366193
run(c)

0 commit comments

Comments
 (0)