aero-vscode 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Unit tests for VS Code language providers: AeroCompletionProvider, AeroHoverProvider,
3
+ * AeroDefinitionProvider. Mocks vscode (Range, Position, workspace, languages, etc.),
4
+ * pathResolver (getResolver), and scope (isAeroDocument, getScopeMode). Asserts completion
5
+ * items, hover null for non-Aero docs, and definition locations for content globals and components.
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
9
+
10
+ const mockSet = vi.fn()
11
+ const mockCollection = {
12
+ set: mockSet,
13
+ delete: vi.fn(),
14
+ dispose: vi.fn(),
15
+ }
16
+
17
+ vi.mock('vscode', () => {
18
+ return {
19
+ Range: class {
20
+ start: any
21
+ end: any
22
+ constructor(start: any, end: any) {
23
+ this.start = start
24
+ this.end = end
25
+ }
26
+ },
27
+ Position: class {
28
+ line: any
29
+ character: any
30
+ constructor(line: any, character: any) {
31
+ this.line = line
32
+ this.character = character
33
+ }
34
+ },
35
+ Diagnostic: class {
36
+ range: any
37
+ message: any
38
+ severity: any
39
+ tags: any[]
40
+ constructor(range: any, message: any, severity: any) {
41
+ this.range = range
42
+ this.message = message
43
+ this.severity = severity
44
+ this.tags = []
45
+ }
46
+ },
47
+ DiagnosticSeverity: { Error: 0, Warning: 1, Information: 2, Hint: 3 },
48
+ DiagnosticTag: { Unnecessary: 1 },
49
+ workspace: {
50
+ onDidOpenTextDocument: vi.fn(),
51
+ onDidSaveTextDocument: vi.fn(),
52
+ onDidChangeTextDocument: vi.fn(),
53
+ onDidCloseTextDocument: vi.fn(),
54
+ textDocuments: [],
55
+ getWorkspaceFolder: vi.fn(() => ({ uri: { fsPath: '/workspace' } })),
56
+ getConfiguration: () => ({ get: () => 'always' }),
57
+ },
58
+ languages: {
59
+ createDiagnosticCollection: () => mockCollection,
60
+ registerDefinitionProvider: vi.fn(),
61
+ registerCompletionItemProvider: vi.fn(),
62
+ registerHoverProvider: vi.fn(),
63
+ },
64
+ Uri: {
65
+ parse: (s: string) => ({ toString: () => s, fsPath: s, scheme: 'file' }),
66
+ file: (s: string) => ({ fsPath: s, scheme: 'file' }),
67
+ },
68
+ CompletionItem: class {
69
+ label: string
70
+ kind: number
71
+ constructor(label: string, kind: number) {
72
+ this.label = label
73
+ this.kind = kind
74
+ }
75
+ },
76
+ CompletionItemKind: {
77
+ Property: 6,
78
+ Keyword: 13,
79
+ Class: 6,
80
+ Struct: 22,
81
+ Folder: 17,
82
+ File: 1,
83
+ Module: 8,
84
+ Variable: 5,
85
+ },
86
+ SnippetString: class {
87
+ value: string
88
+ constructor(value: string) {
89
+ this.value = value
90
+ }
91
+ },
92
+ MarkdownString: class {
93
+ value: string
94
+ constructor(value?: string) {
95
+ this.value = value || ''
96
+ }
97
+ appendMarkdown(val: string) {
98
+ this.value += val
99
+ }
100
+ appendCodeblock(val: string, lang?: string) {
101
+ this.value += '```' + (lang || '') + '\n' + val + '\n```\n'
102
+ }
103
+ },
104
+ }
105
+ })
106
+
107
+ vi.mock('../pathResolver', () => ({
108
+ getResolver: vi.fn(() => ({
109
+ root: '/workspace',
110
+ resolve: vi.fn((specifier: string) => {
111
+ if (specifier.startsWith('@components/')) {
112
+ return '/workspace/client/components/' + specifier.replace('@components/', '') + '.html'
113
+ }
114
+ if (specifier.startsWith('@layouts/')) {
115
+ return '/workspace/client/layouts/' + specifier.replace('@layouts/', '') + '.html'
116
+ }
117
+ if (specifier.startsWith('@content/')) {
118
+ return '/workspace/client/content/' + specifier.replace('@content/', '') + '.ts'
119
+ }
120
+ return '/workspace/' + specifier
121
+ }),
122
+ })),
123
+ clearResolverCache: vi.fn(),
124
+ }))
125
+
126
+ vi.mock('../scope', () => ({
127
+ isAeroDocument: vi.fn(() => true),
128
+ getScopeMode: vi.fn(() => 'auto'),
129
+ clearScopeCache: vi.fn(),
130
+ }))
131
+
132
+ import { AeroCompletionProvider } from '../completionProvider'
133
+ import { AeroHoverProvider } from '../hoverProvider'
134
+ import { AeroDefinitionProvider } from '../definitionProvider'
135
+
136
+ /** Completions: after < (tags), inside tag (attrs), inside { } (content globals), import paths. */
137
+ describe('AeroCompletionProvider', () => {
138
+ let provider: AeroCompletionProvider
139
+
140
+ beforeEach(() => {
141
+ vi.clearAllMocks()
142
+ provider = new AeroCompletionProvider()
143
+ })
144
+
145
+ it('should provide component tag completions after <', async () => {
146
+ const doc = {
147
+ uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
148
+ getText: () => '<',
149
+ lineAt: (line: number) => ({ text: '<' }),
150
+ } as any
151
+
152
+ const position = { line: 0, character: 1 } as any
153
+ const context = { triggerCharacter: '<', isIncomplete: false } as any
154
+
155
+ const result = provider.provideCompletionItems(doc, position, {} as any, context)
156
+
157
+ expect(result).not.toBeNull()
158
+ })
159
+
160
+ it('should provide attribute completions inside a tag', async () => {
161
+ const doc = {
162
+ uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
163
+ getText: () => '<div ',
164
+ lineAt: (line: number) => ({ text: '<div ' }),
165
+ } as any
166
+
167
+ const position = { line: 0, character: 5 } as any
168
+ const context = {} as any
169
+
170
+ const result = provider.provideCompletionItems(doc, position, {} as any, context)
171
+
172
+ expect(result).not.toBeNull()
173
+ })
174
+
175
+ it('should provide content global completions inside expression', async () => {
176
+ const doc = {
177
+ uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
178
+ getText: () => '<div>{',
179
+ lineAt: (line: number) => ({ text: '<div>{|' }),
180
+ } as any
181
+
182
+ const position = { line: 0, character: 6 } as any
183
+ const context = {} as any
184
+
185
+ const result = provider.provideCompletionItems(doc, position, {} as any, context)
186
+
187
+ expect(result).not.toBeNull()
188
+ const items = result as any[]
189
+ const labels = items.map((i: any) => i.label)
190
+ expect(labels).toContain('site')
191
+ expect(labels).toContain('theme')
192
+ })
193
+
194
+ it('should provide import path completions', async () => {
195
+ const doc = {
196
+ uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
197
+ getText: () => "import foo from '@components",
198
+ lineAt: (line: number) => ({ text: "import foo from '@components" }),
199
+ } as any
200
+
201
+ const position = { line: 0, character: 17 } as any // after '@components'
202
+
203
+ const result = provider.provideCompletionItems(doc, position, {} as any, {} as any)
204
+
205
+ expect(result).not.toBeNull()
206
+ })
207
+ })
208
+
209
+ /** Hover: returns null when isAeroDocument is false; otherwise hover content for symbols. */
210
+ describe('AeroHoverProvider', () => {
211
+ let provider: AeroHoverProvider
212
+
213
+ beforeEach(() => {
214
+ vi.clearAllMocks()
215
+ provider = new AeroHoverProvider()
216
+ })
217
+
218
+ it('should return null for non-Aero documents', async () => {
219
+ const { isAeroDocument } = await import('../scope')
220
+ ;(isAeroDocument as any).mockReturnValueOnce(false)
221
+
222
+ const doc = {
223
+ uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
224
+ getText: () => '<div></div>',
225
+ lineAt: (line: number) => ({ text: '<div></div>' }),
226
+ positionAt: (offset: number) => ({ line: 0, character: offset }),
227
+ } as any
228
+
229
+ const position = { line: 0, character: 2 } as any
230
+ const result = await provider.provideHover(doc, position, {} as any)
231
+
232
+ expect(result).toBeNull()
233
+ })
234
+ })
235
+
236
+ /** Go-to-definition: null for non-Aero docs; content global (site) and component tags resolve to file locations. */
237
+ describe('AeroDefinitionProvider', () => {
238
+ let provider: AeroDefinitionProvider
239
+
240
+ beforeEach(() => {
241
+ vi.clearAllMocks()
242
+ provider = new AeroDefinitionProvider()
243
+ })
244
+
245
+ it('should return null for non-Aero documents', async () => {
246
+ const { isAeroDocument } = await import('../scope')
247
+ ;(isAeroDocument as any).mockReturnValueOnce(false)
248
+
249
+ const doc = {
250
+ uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
251
+ getText: () => '<div></div>',
252
+ lineAt: (line: number) => ({ text: '<div></div>' }),
253
+ positionAt: (offset: number) => ({ line: 0, character: offset }),
254
+ } as any
255
+
256
+ const position = { line: 0, character: 2 } as any
257
+ const result = await provider.provideDefinition(doc, position, {} as any)
258
+
259
+ expect(result).toBeNull()
260
+ })
261
+
262
+ it('should provide definition for content global', async () => {
263
+ const doc = {
264
+ uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
265
+ getText: () => '<div>{site.title}</div>',
266
+ lineAt: (line: number) => ({ text: '<div>{site.title}</div>' }),
267
+ positionAt: (offset: number) => ({ line: 0, character: offset }),
268
+ offsetAt: (pos: any) => pos.character,
269
+ } as any
270
+
271
+ const position = { line: 0, character: 7 } as any // on 'site'
272
+ const result = await provider.provideDefinition(doc, position, {} as any)
273
+
274
+ expect(result).not.toBeNull()
275
+ expect(result?.length).toBeGreaterThan(0)
276
+ })
277
+
278
+ it('should provide definition for component tag', async () => {
279
+ const doc = {
280
+ uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
281
+ getText: () => '<header-component></header-component>',
282
+ lineAt: (line: number) => ({ text: '<header-component></header-component>' }),
283
+ positionAt: (offset: number) => ({ line: 0, character: offset }),
284
+ } as any
285
+
286
+ const position = { line: 0, character: 1 } as any // on 'header'
287
+ const result = await provider.provideDefinition(doc, position, {} as any)
288
+
289
+ expect(result).not.toBeNull()
290
+ expect(result?.length).toBeGreaterThan(0)
291
+ })
292
+ })
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Unit tests for scope.ts (isAeroDocument, getScopeMode) and pathResolver.ts (getResolver,
3
+ * clearResolverCache). Mocks vscode workspace/Uri and node:fs so workspace detection and
4
+ * config file presence are under test control.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
8
+
9
+ vi.mock('vscode', () => {
10
+ return {
11
+ workspace: {
12
+ getWorkspaceFolder: vi.fn((uri: any) => {
13
+ if (uri?.fsPath?.startsWith('/workspace')) {
14
+ return { uri: { fsPath: '/workspace' } }
15
+ }
16
+ return undefined
17
+ }),
18
+ getConfiguration: vi.fn(() => ({ get: vi.fn(() => 'auto') })),
19
+ },
20
+ Uri: {
21
+ file: (s: string) => ({ fsPath: s, scheme: 'file' }),
22
+ },
23
+ }
24
+ })
25
+
26
+ vi.mock('node:fs', () => ({
27
+ existsSync: vi.fn((path: string) => {
28
+ if (
29
+ path.includes('vite.config.ts') ||
30
+ path.includes('package.json') ||
31
+ path.includes('tsconfig.json')
32
+ ) {
33
+ if (path.includes('aero') || path.includes('workspace')) {
34
+ return true
35
+ }
36
+ }
37
+ return false
38
+ }),
39
+ readFileSync: vi.fn((path: string) => {
40
+ if (path.includes('vite.config.ts')) {
41
+ return "import { aero } from '@aerobuilt/core'"
42
+ }
43
+ if (path.includes('package.json')) {
44
+ return '{ "name": "test-project" }'
45
+ }
46
+ return '{}'
47
+ }),
48
+ }))
49
+
50
+ describe('scope', () => {
51
+ beforeEach(() => {
52
+ vi.clearAllMocks()
53
+ })
54
+
55
+ /** Aero documents: file scheme + HTML languageId; untitled and non-HTML are excluded. */
56
+ describe('isAeroDocument', () => {
57
+ it('should return false for non-HTML documents', async () => {
58
+ const { isAeroDocument } = await import('../scope')
59
+
60
+ const doc = {
61
+ languageId: 'javascript',
62
+ uri: { scheme: 'file', fsPath: '/test.js' },
63
+ } as any
64
+
65
+ expect(isAeroDocument(doc)).toBe(false)
66
+ })
67
+
68
+ it('should return false for non-file URIs', async () => {
69
+ const { isAeroDocument } = await import('../scope')
70
+
71
+ const doc = {
72
+ languageId: 'html',
73
+ uri: { scheme: 'untitled', fsPath: '/test.html' },
74
+ } as any
75
+
76
+ expect(isAeroDocument(doc)).toBe(false)
77
+ })
78
+ })
79
+
80
+ /** Scope mode from workspace config (e.g. 'auto', 'always'); mock returns 'auto'. */
81
+ describe('getScopeMode', () => {
82
+ it('should return default mode as auto', async () => {
83
+ const { getScopeMode } = await import('../scope')
84
+
85
+ expect(getScopeMode()).toBe('auto')
86
+ })
87
+ })
88
+ })
89
+
90
+ describe('pathResolver', () => {
91
+ beforeEach(() => {
92
+ vi.clearAllMocks()
93
+ })
94
+
95
+ /** Resolver is built from document's workspace folder; root and resolve() are used by providers. */
96
+ describe('getResolver', () => {
97
+ it('should return a resolver object with root and resolve method', async () => {
98
+ const { getResolver, clearResolverCache } = await import('../pathResolver')
99
+ clearResolverCache()
100
+
101
+ const doc = {
102
+ uri: { fsPath: '/workspace/test.html' },
103
+ } as any
104
+
105
+ const resolver = getResolver(doc)
106
+
107
+ expect(resolver).toBeDefined()
108
+ expect(resolver!.root).toBe('/workspace')
109
+ expect(typeof resolver!.resolve).toBe('function')
110
+ })
111
+ })
112
+
113
+ describe('clearResolverCache', () => {
114
+ it('should be exported function', async () => {
115
+ const { clearResolverCache } = await import('../pathResolver')
116
+
117
+ expect(typeof clearResolverCache).toBe('function')
118
+ })
119
+ })
120
+ })