Skip to content

Commit e9c9cf7

Browse files
Copilothotlong
andcommitted
feat: Add vitest test file for vscode-objectql extension
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 69463d2 commit e9c9cf7

1 file changed

Lines changed: 321 additions & 0 deletions

File tree

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
/**
2+
* ObjectQL VSCode Extension Tests
3+
* Copyright (c) 2026-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect, vi, beforeEach } from 'vitest';
10+
import * as fs from 'fs';
11+
import * as path from 'path';
12+
13+
// Mock vscode module (unavailable outside VS Code runtime)
14+
vi.mock('vscode', () => ({
15+
commands: { registerCommand: vi.fn(), executeCommand: vi.fn() },
16+
window: {
17+
showInformationMessage: vi.fn().mockResolvedValue(undefined),
18+
showErrorMessage: vi.fn(),
19+
showWarningMessage: vi.fn(),
20+
showInputBox: vi.fn(),
21+
showTextDocument: vi.fn(),
22+
activeTextEditor: undefined,
23+
createOutputChannel: vi.fn(() => ({ appendLine: vi.fn(), show: vi.fn() })),
24+
},
25+
workspace: {
26+
workspaceFolders: [],
27+
findFiles: vi.fn().mockResolvedValue([]),
28+
getConfiguration: vi.fn(() => ({ get: vi.fn() })),
29+
openTextDocument: vi.fn(),
30+
applyEdit: vi.fn(),
31+
asRelativePath: vi.fn((uri: any) => uri.toString()),
32+
onDidChangeTextDocument: vi.fn(),
33+
createFileSystemWatcher: vi.fn(() => ({
34+
onDidCreate: vi.fn(),
35+
onDidChange: vi.fn(),
36+
onDidDelete: vi.fn(),
37+
dispose: vi.fn(),
38+
})),
39+
fs: { stat: vi.fn() },
40+
},
41+
languages: {
42+
registerDefinitionProvider: vi.fn(),
43+
registerCompletionItemProvider: vi.fn(),
44+
},
45+
Uri: {
46+
file: vi.fn((f: string) => ({ fsPath: f, toString: () => f })),
47+
parse: vi.fn((s: string) => ({ fsPath: s, toString: () => s })),
48+
joinPath: vi.fn((_base: any, ...segments: string[]) => {
49+
const joined = segments.join('/');
50+
return { fsPath: joined, toString: () => joined };
51+
}),
52+
},
53+
env: { openExternal: vi.fn() },
54+
Position: class {
55+
constructor(public line: number, public character: number) {}
56+
},
57+
Range: class {
58+
constructor(public start: any, public end: any) {}
59+
},
60+
Location: class {
61+
constructor(public uri: any, public range: any) {}
62+
},
63+
CompletionItem: class {
64+
label: string;
65+
kind?: number;
66+
detail?: string;
67+
documentation?: any;
68+
constructor(label: string, kind?: number) {
69+
this.label = label;
70+
this.kind = kind;
71+
}
72+
},
73+
CompletionItemKind: { Field: 5, Class: 7, Property: 10 },
74+
MarkdownString: class {
75+
value: string;
76+
constructor(value?: string) { this.value = value ?? ''; }
77+
},
78+
WorkspaceEdit: class {
79+
createFile: any = vi.fn();
80+
insert: any = vi.fn();
81+
},
82+
ExtensionContext: class {},
83+
Disposable: class {
84+
static from(..._args: any[]) {
85+
return { dispose: vi.fn() };
86+
}
87+
},
88+
}));
89+
90+
// Mock js-yaml for ObjectIndex
91+
vi.mock('js-yaml', () => ({
92+
load: vi.fn(),
93+
}));
94+
95+
// ─── Helpers ─────────────────────────────────────────────
96+
97+
function readPackageJson() {
98+
const pkgPath = path.resolve(__dirname, '..', 'package.json');
99+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
100+
}
101+
102+
function createMockContext(): any {
103+
return {
104+
subscriptions: [],
105+
extensionUri: { fsPath: '/mock/extension' },
106+
globalState: {
107+
get: vi.fn().mockReturnValue(true), // suppress welcome message
108+
update: vi.fn(),
109+
},
110+
};
111+
}
112+
113+
// ─── Tests ───────────────────────────────────────────────
114+
115+
describe('VSCode ObjectQL Extension', () => {
116+
beforeEach(() => {
117+
vi.clearAllMocks();
118+
});
119+
120+
// ── 1. Extension Manifest ─────────────────────────────
121+
122+
describe('Extension Manifest (package.json)', () => {
123+
const pkg = readPackageJson();
124+
125+
it('should declare contributes.commands', () => {
126+
expect(pkg.contributes).toBeDefined();
127+
expect(pkg.contributes.commands).toBeDefined();
128+
expect(Array.isArray(pkg.contributes.commands)).toBe(true);
129+
expect(pkg.contributes.commands.length).toBeGreaterThan(0);
130+
});
131+
132+
it('should have the expected command IDs', () => {
133+
const commandIds: string[] = pkg.contributes.commands.map((c: any) => c.command);
134+
135+
expect(commandIds).toContain('objectql.newObject');
136+
expect(commandIds).toContain('objectql.newValidation');
137+
expect(commandIds).toContain('objectql.newPermission');
138+
expect(commandIds).toContain('objectql.newWorkflow');
139+
expect(commandIds).toContain('objectql.validateSchema');
140+
});
141+
142+
it('should categorize all commands under "ObjectQL"', () => {
143+
for (const cmd of pkg.contributes.commands) {
144+
expect(cmd.category).toBe('ObjectQL');
145+
}
146+
});
147+
148+
it('should have activation events for ObjectQL file types', () => {
149+
expect(pkg.activationEvents).toBeDefined();
150+
expect(pkg.activationEvents).toContain('workspaceContains:**/*.object.yml');
151+
expect(pkg.activationEvents).toContain('workspaceContains:**/*.validation.yml');
152+
expect(pkg.activationEvents).toContain('workspaceContains:**/*.permission.yml');
153+
expect(pkg.activationEvents).toContain('workspaceContains:**/*.workflow.yml');
154+
});
155+
156+
it('should depend on the YAML extension', () => {
157+
expect(pkg.extensionDependencies).toContain('redhat.vscode-yaml');
158+
});
159+
160+
it('should declare language contributions for all metadata file types', () => {
161+
const langIds: string[] = pkg.contributes.languages.map((l: any) => l.id);
162+
expect(langIds).toContain('objectql-object');
163+
expect(langIds).toContain('objectql-validation');
164+
expect(langIds).toContain('objectql-permission');
165+
expect(langIds).toContain('objectql-workflow');
166+
});
167+
});
168+
169+
// ── 2. Command Registration in activate() ────────────
170+
171+
describe('activate() command registration', () => {
172+
it('should register all five commands declared in the manifest', async () => {
173+
const vscode = await import('vscode');
174+
const { activate } = await import('./extension');
175+
176+
const ctx = createMockContext();
177+
activate(ctx);
178+
179+
const registeredIds = (vscode.commands.registerCommand as ReturnType<typeof vi.fn>).mock.calls.map(
180+
(call: any[]) => call[0]
181+
);
182+
183+
const pkg = readPackageJson();
184+
const manifestIds: string[] = pkg.contributes.commands.map((c: any) => c.command);
185+
186+
for (const id of manifestIds) {
187+
expect(registeredIds).toContain(id);
188+
}
189+
});
190+
191+
it('should register a DefinitionProvider and CompletionItemProvider', async () => {
192+
const vscode = await import('vscode');
193+
const { activate } = await import('./extension');
194+
195+
const ctx = createMockContext();
196+
activate(ctx);
197+
198+
expect(vscode.languages.registerDefinitionProvider).toHaveBeenCalledTimes(1);
199+
expect(vscode.languages.registerCompletionItemProvider).toHaveBeenCalledTimes(1);
200+
});
201+
202+
it('should push disposables into context.subscriptions', async () => {
203+
const { activate } = await import('./extension');
204+
const ctx = createMockContext();
205+
activate(ctx);
206+
207+
// 5 commands + 2 providers + 1 ObjectIndex dispose = 8
208+
expect(ctx.subscriptions.length).toBe(8);
209+
});
210+
});
211+
212+
// ── 3. deactivate() ──────────────────────────────────
213+
214+
describe('deactivate()', () => {
215+
it('should not throw when called', async () => {
216+
const { deactivate } = await import('./extension');
217+
expect(() => deactivate()).not.toThrow();
218+
});
219+
});
220+
221+
// ── 4. Utility Constants ─────────────────────────────
222+
223+
describe('utils/constants', () => {
224+
it('should export SCHEMES.FILE as "file"', async () => {
225+
const { SCHEMES } = await import('./utils/constants');
226+
expect(SCHEMES.FILE).toBe('file');
227+
});
228+
229+
it('should export SCHEMES.UNTITLED as "untitled"', async () => {
230+
const { SCHEMES } = await import('./utils/constants');
231+
expect(SCHEMES.UNTITLED).toBe('untitled');
232+
});
233+
234+
it('should export LANGUAGES.YAML as "yaml"', async () => {
235+
const { LANGUAGES } = await import('./utils/constants');
236+
expect(LANGUAGES.YAML).toBe('yaml');
237+
});
238+
});
239+
240+
// ── 5. ObjectIndex service initialisation ─────────────
241+
242+
describe('ObjectIndex service', () => {
243+
it('should be constructible and exposable', async () => {
244+
const { ObjectIndex } = await import('./services/ObjectIndex');
245+
const index = new ObjectIndex();
246+
expect(index).toBeDefined();
247+
expect(typeof index.getObject).toBe('function');
248+
expect(typeof index.getAllObjects).toBe('function');
249+
expect(typeof index.dispose).toBe('function');
250+
});
251+
252+
it('should return undefined for unknown objects', async () => {
253+
const { ObjectIndex } = await import('./services/ObjectIndex');
254+
const index = new ObjectIndex();
255+
expect(index.getObject('nonexistent')).toBeUndefined();
256+
});
257+
258+
it('should return an empty array from getAllObjects initially', async () => {
259+
const { ObjectIndex } = await import('./services/ObjectIndex');
260+
const index = new ObjectIndex();
261+
expect(index.getAllObjects()).toEqual([]);
262+
});
263+
});
264+
265+
// ── 6. Provider instantiation ─────────────────────────
266+
267+
describe('Providers', () => {
268+
it('should instantiate ObjectDefinitionProvider', async () => {
269+
const { ObjectIndex } = await import('./services/ObjectIndex');
270+
const { ObjectDefinitionProvider } = await import('./providers/ObjectDefinitionProvider');
271+
const index = new ObjectIndex();
272+
const provider = new ObjectDefinitionProvider(index);
273+
expect(provider).toBeDefined();
274+
expect(typeof provider.provideDefinition).toBe('function');
275+
});
276+
277+
it('should instantiate ObjectCompletionProvider', async () => {
278+
const { ObjectIndex } = await import('./services/ObjectIndex');
279+
const { ObjectCompletionProvider } = await import('./providers/ObjectCompletionProvider');
280+
const index = new ObjectIndex();
281+
const provider = new ObjectCompletionProvider(index);
282+
expect(provider).toBeDefined();
283+
expect(typeof provider.provideCompletionItems).toBe('function');
284+
});
285+
});
286+
287+
// ── 7. Manifest ↔ Code consistency ────────────────────
288+
289+
describe('Manifest / Code consistency', () => {
290+
it('should have no commands in code that are missing from the manifest', async () => {
291+
const vscode = await import('vscode');
292+
const { activate } = await import('./extension');
293+
const ctx = createMockContext();
294+
activate(ctx);
295+
296+
const registeredIds: string[] = (vscode.commands.registerCommand as ReturnType<typeof vi.fn>).mock.calls.map(
297+
(call: any[]) => call[0]
298+
);
299+
300+
const pkg = readPackageJson();
301+
const manifestIds: string[] = pkg.contributes.commands.map((c: any) => c.command);
302+
303+
for (const id of registeredIds) {
304+
expect(manifestIds).toContain(id);
305+
}
306+
});
307+
308+
it('should register the providers with the yaml/file selector', async () => {
309+
const vscode = await import('vscode');
310+
const { activate } = await import('./extension');
311+
const ctx = createMockContext();
312+
activate(ctx);
313+
314+
const defCall = (vscode.languages.registerDefinitionProvider as ReturnType<typeof vi.fn>).mock.calls[0];
315+
expect(defCall[0]).toEqual({ language: 'yaml', scheme: 'file' });
316+
317+
const compCall = (vscode.languages.registerCompletionItemProvider as ReturnType<typeof vi.fn>).mock.calls[0];
318+
expect(compCall[0]).toEqual({ language: 'yaml', scheme: 'file' });
319+
});
320+
});
321+
});

0 commit comments

Comments
 (0)