import { TextDocument } from 'vscode-languageserver-textdocument'; import { CompletionItem, CompletionItemKind } from 'vscode-languageserver/node'; import { onCompletion, CompletionSettings } from '../../server/src/providers/completion'; import { invalidateCache } from '../../server/src/workspace-scanner'; // Mock TextDocuments function createMockDocuments(content: string) { const doc = TextDocument.create('file:///test.html', 'html', 1, content); return { get: (uri: string) => uri === doc.uri ? doc : undefined, all: () => [doc], doc, }; } function getCompletions(content: string, offset: number, settings?: Partial): Promise { invalidateCache(); // ensure fresh workspace data per test const mock = createMockDocuments(content); const mergedSettings: CompletionSettings = { filtersEnabled: true, customFilters: [], customValidators: [], ...settings, }; const handler = onCompletion(mock as any, async () => mergedSettings); const position = mock.doc.positionAt(offset); return handler({ textDocument: { uri: mock.doc.uri }, position, context: undefined as any, }); } describe('CompletionProvider', () => { describe('Directive name completions', () => { it('suggests directives when typing in attribute position', async () => { const content = '
'; const items = await getCompletions(content, 5); // after space, before > expect(items.length).toBeGreaterThan(0); const labels = items.map(i => i.label); expect(labels).toContain('state'); expect(labels).toContain('if'); expect(labels).toContain('each'); expect(labels).toContain('for'); }); it('filters directives by partial input', async () => { const content = '
'; const items = await getCompletions(content, 6); // after "st" const labels = items.map(i => i.label); expect(labels).toContain('state'); expect(labels).toContain('store'); }); it('does not suggest already-present directives', async () => { const content = '
'; const items = await getCompletions(content, 22); // after space const labels = items.map(i => i.label); expect(labels).not.toContain('state'); }); }); describe('Pattern-based completions', () => { it('suggests on: events', async () => { const content = ''; const items = await getCompletions(content, 10); // after "on" const labels = items.map(i => i.label); const onItems = labels.filter(l => l.startsWith('on:')); expect(onItems.length).toBeGreaterThan(0); }); it('suggests bind- targets', async () => { const content = ''; const items = await getCompletions(content, 7); // after "bind" const labels = items.map(i => i.label); const bindItems = labels.filter(l => l.startsWith('bind-')); expect(bindItems.length).toBeGreaterThan(0); }); }); describe('Companion completions', () => { it('suggests companion attributes when directive is present', async () => { const content = '
'; const items = await getCompletions(content, 22); // after space const labels = items.map(i => i.label); expect(labels).toContain('as'); }); it('suggests foreach companion attributes', async () => { const content = '
  • '; const items = await getCompletions(content, 28); // after space const labels = items.map(i => i.label); expect(labels).not.toContain('from'); expect(labels).toContain('filter'); expect(labels).toContain('sort'); expect(labels).toContain('limit'); expect(labels).toContain('offset'); expect(labels).toContain('template'); }); it('suggests each companion attributes including filter/sort/limit/offset', async () => { const content = '
  • '; const items = await getCompletions(content, 25); // after space const labels = items.map(i => i.label); expect(labels).toContain('filter'); expect(labels).toContain('sort'); expect(labels).toContain('limit'); expect(labels).toContain('offset'); expect(labels).toContain('template'); }); it('suggests for companion attributes', async () => { const content = '
  • '; const items = await getCompletions(content, 24); // after space const labels = items.map(i => i.label); expect(labels).toContain('filter'); expect(labels).toContain('sort'); expect(labels).toContain('limit'); expect(labels).toContain('offset'); expect(labels).toContain('template'); }); }); describe('Filter completions', () => { it('suggests filters after pipe in attribute value', async () => { const content = ''; const items = await getCompletions(content, 19); // after "| " const labels = items.map(i => i.label); expect(labels).toContain('uppercase'); expect(labels).toContain('lowercase'); expect(labels).toContain('currency'); }); }); describe('Animation completions', () => { it('suggests animation names for animate attribute', async () => { const content = '
    '; const items = await getCompletions(content, 14); // inside quotes const labels = items.map(i => i.label); expect(labels).toContain('fadeIn'); }); }); describe('Validator completions', () => { it('suggests validators for validate attribute', async () => { const content = ''; const items = await getCompletions(content, 17); // inside quotes const labels = items.map(i => i.label); expect(labels).toContain('required'); expect(labels).toContain('email'); }); }); describe('Event modifier completions', () => { it('suggests modifiers after . on on: attributes', async () => { const content = ''; const items = await getCompletions(content, 17); // after "on:click." const labels = items.map(i => i.label); const modifierLabels = labels.filter(l => l.startsWith('on:click.')); expect(modifierLabels.length).toBeGreaterThan(0); expect(modifierLabels.some(l => l.includes('prevent'))).toBe(true); expect(modifierLabels.some(l => l.includes('stop'))).toBe(true); }); }); describe('Filter argument hints', () => { it('suggests argument info after filter name and colon', async () => { const content = ''; const items = await getCompletions(content, 28); // after "truncate:" expect(items.length).toBeGreaterThan(0); const argItem = items[0]; expect(argItem.detail).toContain('truncate'); }); }); describe('Configuration: filters disabled', () => { it('does not suggest filters when filtersEnabled is false', async () => { const content = ''; const items = await getCompletions(content, 19, { filtersEnabled: false }); const labels = items.map(i => i.label); expect(labels).not.toContain('uppercase'); }); }); describe('Configuration: custom filters', () => { it('includes custom filter names in completions', async () => { const content = ''; const items = await getCompletions(content, 19, { customFilters: ['myCustomFilter'] }); const labels = items.map(i => i.label); expect(labels).toContain('myCustomFilter'); }); }); describe('Configuration: custom validators', () => { it('includes custom validator names in completions', async () => { const content = ''; const items = await getCompletions(content, 17, { customValidators: ['myRule'] }); const labels = items.map(i => i.label); expect(labels).toContain('myRule'); }); }); describe('Phase 4: Workspace-aware completions', () => { it('suggests custom directive names from workspace', async () => { // The mock doesn't have real workspace files, but we can test that // the custom directive code path works by checking inline scripts const content = `\n
    `; const offset = content.indexOf('>'); const items = await getCompletions(content, offset); const labels = items.map(i => i.label); expect(labels).toContain('tooltip'); }); it('suggests var-* attributes when use="templateId" is present', async () => { const content = `\n
    `; const offset = content.indexOf('>', content.indexOf('use=')); const items = await getCompletions(content, offset); const labels = items.map(i => i.label); expect(labels).toContain('var-item'); }); it('suggests store property paths with $store.', async () => { const content = '
    \n'; const offset = content.indexOf('">'); const items = await getCompletions(content, offset); const labels = items.map(i => i.label); expect(labels).toContain('$store.user.name'); expect(labels).toContain('$store.user.role'); }); it('suggests store names after $store.', async () => { const content = '
    \n'; const offset = content.indexOf('">'); const items = await getCompletions(content, offset); const labels = items.map(i => i.label); expect(labels).toContain('$store.cart'); }); }); describe('Plugin-requirement note (derived from .plugin field)', () => { function docValue(item: CompletionItem | undefined): string { if (!item || !item.documentation) return ''; return typeof item.documentation === 'string' ? item.documentation : item.documentation.value; } it('includes the requirement note in a gated directive completion (validate)', async () => { const content = '
    '; const items = await getCompletions(content, 6); // after space const validate = items.find(i => i.label === 'validate'); expect(validate).toBeDefined(); expect(docValue(validate)).toContain('Requires the `@erickxavier/nojs-elements` plugin'); }); it('includes the requirement note in a companion completion derived from its parent', async () => { const content = '
    '; const items = await getCompletions(content, 17); // after space, drag present const handle = items.find(i => i.label === 'drag-handle'); expect(handle).toBeDefined(); expect(docValue(handle)).toContain('Requires the `@erickxavier/nojs-elements` plugin'); }); it('omits the requirement note for a non-gated directive (if)', async () => { const content = '
    '; const items = await getCompletions(content, 5); const ifItem = items.find(i => i.label === 'if'); expect(ifItem).toBeDefined(); expect(docValue(ifItem)).not.toContain('Requires the `@erickxavier/nojs-elements` plugin'); }); }); });