import { TextDocument } from 'vscode-languageserver-textdocument'; import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver/node'; import { validateTextDocument } from '../../server/src/providers/diagnostics'; /** Extract message string from Diagnostic (vscode-languageserver 10.x: message is string | MarkupContent) */ function msg(d: Diagnostic): string { return typeof d.message === 'string' ? d.message : d.message.value; } function createDocument(content: string): TextDocument { return TextDocument.create('file:///test.html', 'html', 1, content); } // Mock connection that captures diagnostics function createMockConnection() { let lastDiagnostics: Diagnostic[] = []; return { sendDiagnostics: (params: { uri: string; diagnostics: Diagnostic[] }) => { lastDiagnostics = params.diagnostics; }, getDiagnostics: () => lastDiagnostics, }; } describe('DiagnosticsProvider', () => { // ─── Validation disabled ───────────────────────────────────────────── describe('Validation disabled', () => { it('sends empty diagnostics when validation is disabled', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any, { validationEnabled: false }); const diagnostics = conn.getDiagnostics(); expect(diagnostics.length).toBe(0); }); it('runs validation when validationEnabled is true', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any, { validationEnabled: true }); const diagnostics = conn.getDiagnostics(); expect(diagnostics.length).toBeGreaterThan(0); }); it('runs validation when options are undefined', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); expect(diagnostics.length).toBeGreaterThan(0); }); }); // ─── Rule 1: Unknown directive warning ─────────────────────────────── describe('Unknown directive warning', () => { it('does not warn about attribute that is a typo but fails the couldBeNoJsAttribute gate', async () => { // "bnd" is close to "bind" (levenshtein distance <= 2) but the unknown // directive check first requires the attribute to pass couldBeNoJsAttribute // (exact directive, companion, or pattern prefix match). Since "bnd" is // none of those, it never reaches the looksLikeNoJsDirective check. const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const unknown = diagnostics.find(d => msg(d).includes('Unknown directive')); expect(unknown).toBeUndefined(); }); it('does not warn about standard HTML attributes', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const unknown = diagnostics.find(d => msg(d).includes('Unknown directive')); expect(unknown).toBeUndefined(); }); it('does not warn about known directives', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const unknown = diagnostics.find(d => msg(d).includes('Unknown directive')); expect(unknown).toBeUndefined(); }); it('does not warn about attributes far from any directive name', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const unknown = diagnostics.find(d => msg(d).includes('Unknown directive')); expect(unknown).toBeUndefined(); }); }); // ─── Rule 2: Required values ───────────────────────────────────────── describe('Required values', () => { it('reports error for directive missing required value', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const getError = diagnostics.find(d => msg(d).includes('"get" requires a value')); expect(getError).toBeDefined(); expect(getError!.severity).toBe(DiagnosticSeverity.Error); }); it('reports error for directive with empty value', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const getError = diagnostics.find(d => msg(d).includes('"get" requires a value')); expect(getError).toBeDefined(); }); it('reports error for directive with whitespace-only value', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const getError = diagnostics.find(d => msg(d).includes('"get" requires a value')); expect(getError).toBeDefined(); }); it('does not report error for directive with value', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const getError = diagnostics.find(d => msg(d).includes('"get" requires')); expect(getError).toBeUndefined(); }); it('does not report error for i18n-ns without value', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const nsError = diagnostics.find(d => msg(d).includes('"i18n-ns" requires')); expect(nsError).toBeUndefined(); }); it('reports error for multiple directives missing required values', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const bindError = diagnostics.find(d => msg(d).includes('"bind" requires a value')); expect(bindError).toBeDefined(); }); }); // ─── Rule 3: Orphaned else/else-if ─────────────────────────────────── describe('Orphaned else/else-if', () => { it('reports error for else without preceding if', async () => { const content = 'No items found
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const elseError = diagnostics.find(d => msg(d).includes('"else" must be preceded')); expect(elseError).toBeUndefined(); }); it('does not report for else companion using #id template reference', async () => { const content = 'No items found
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const elseError = diagnostics.find(d => msg(d).includes('"else" must be preceded')); const tplError = diagnostics.find(d => msg(d).includes('referenced but not defined')); expect(elseError).toBeUndefined(); expect(tplError).toBeUndefined(); }); it('does not report for else after else-if sibling (conditional chains unchanged)', async () => { const content = 'Hidden
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const elseError = diagnostics.find(d => msg(d).includes('"else" must be preceded')); expect(elseError).toBeUndefined(); }); it('reports missing template for #id else companion using the bare id', async () => { const content = 'Content
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const tplWarn = diagnostics.find(d => msg(d).includes('referenced but not defined')); expect(tplWarn).toBeUndefined(); }); it('checks then, loading, error, empty, success, error-boundary attributes', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const tplWarns = diagnostics.filter(d => msg(d).includes('referenced but not defined')); expect(tplWarns.length).toBe(4); }); it('strips # prefix from template references', async () => { const content = 'Content
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const tplWarn = diagnostics.find(d => msg(d).includes('referenced but not defined')); expect(tplWarn).toBeUndefined(); }); it('does not check expressions as template IDs', async () => { // Values with spaces or complex syntax are not simple identifiers const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const tplWarn = diagnostics.find(d => msg(d).includes('referenced but not defined')); expect(tplWarn).toBeUndefined(); }); }); // ─── Rule 12: Duplicate wildcard routes ────────────────────────────── describe('Duplicate wildcard routes', () => { it('warns about duplicate wildcard routes for the same outlet', async () => { const content = '404 A
404 B
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const wildcardWarns = diagnostics.filter(d => msg(d).includes('Duplicate wildcard route')); expect(wildcardWarns.length).toBeGreaterThanOrEqual(2); }); it('does not warn for a single wildcard route', async () => { const content = '404
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const wildcardWarn = diagnostics.find(d => msg(d).includes('Duplicate wildcard route')); expect(wildcardWarn).toBeUndefined(); }); it('does not warn for wildcard routes on different outlets', async () => { const content = '404
No sidebar
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const wildcardWarn = diagnostics.find(d => msg(d).includes('Duplicate wildcard route')); expect(wildcardWarn).toBeUndefined(); }); it('only tracks wildcard routes on template elements', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const wildcardWarn = diagnostics.find(d => msg(d).includes('Duplicate wildcard route')); expect(wildcardWarn).toBeUndefined(); }); }); // ─── Rule 13: Missing companion as for HTTP directives ─────────────── describe('Missing as companion', () => { it('warns when get directive lacks as attribute', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const asWarn = diagnostics.find(d => msg(d).includes('missing the "as" companion')); expect(asWarn).toBeDefined(); }); it('does not warn when as attribute is present', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const asWarn = diagnostics.find(d => msg(d).includes('missing the "as" companion')); expect(asWarn).toBeUndefined(); }); it('warns for post, put, patch, delete without as', async () => { for (const method of ['post', 'put', 'patch', 'delete']) { const content = ``; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const asWarn = diagnostics.find(d => msg(d).includes('missing the "as" companion')); expect(asWarn).toBeDefined(); } }); }); // ─── Rule 14: Expression syntax validation ─────────────────────────── describe('Expression syntax validation', () => { it('reports hint for unbalanced brackets in expression', async () => { // The expression validator checks bracket/paren/brace balance and // unterminated strings, not operator validity. Use an unbalanced // bracket to trigger the syntax hint. const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const syntaxHint = diagnostics.find(d => msg(d).includes('Possible syntax error')); expect(syntaxHint).toBeDefined(); expect(syntaxHint!.severity).toBe(DiagnosticSeverity.Hint); }); it('does not flag valid expressions', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const syntaxHint = diagnostics.find(d => msg(d).includes('Possible syntax error')); expect(syntaxHint).toBeUndefined(); }); it('skips syntax checking for validate attribute', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const syntaxHint = diagnostics.find(d => msg(d).includes('Possible syntax error')); expect(syntaxHint).toBeUndefined(); }); it('skips syntax checking for on: event handlers', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const syntaxHint = diagnostics.find(d => msg(d).includes('Possible syntax error')); expect(syntaxHint).toBeUndefined(); }); it('skips syntax checking for HTTP directives', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const syntaxHint = diagnostics.find(d => msg(d).includes('Possible syntax error')); expect(syntaxHint).toBeUndefined(); }); it('skips syntax checking for ref, store, t, i18n-ns, route, use, trigger, error-boundary, drag-handle', async () => { for (const attr of ['ref', 'store', 't', 'i18n-ns', 'route', 'use', 'trigger', 'error-boundary']) { const content = ``; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const syntaxHint = diagnostics.find(d => msg(d).includes('Possible syntax error')); expect(syntaxHint).toBeUndefined(); } }); }); // ─── Rule 15: Deprecated transition CSS ────────────────────────────── describe('Deprecated transition CSS', () => { it('detects deprecated class-based transition CSS patterns', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const cssWarn = diagnostics.find(d => msg(d).includes('deprecated')); expect(cssWarn).toBeDefined(); expect(cssWarn!.severity).toBe(DiagnosticSeverity.Information); }); it('detects fade-leave pattern', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const cssWarn = diagnostics.find(d => msg(d).includes('.fade-leave')); expect(cssWarn).toBeDefined(); }); it('detects all transition class variants', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const cssWarns = diagnostics.filter(d => msg(d).includes('deprecated')); expect(cssWarns.length).toBe(2); }); it('does not flag non-transition CSS classes', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const cssWarn = diagnostics.find(d => msg(d).includes('deprecated')); expect(cssWarn).toBeUndefined(); }); it('does not flag when there are no style blocks', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const cssWarn = diagnostics.find(d => msg(d).includes('deprecated')); expect(cssWarn).toBeUndefined(); }); }); // ─── No false positives ────────────────────────────────────────────── describe('No false positives', () => { it('does not report diagnostics for plain HTML', async () => { const content = 'Hello World