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 = '
Alt
'; 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).toBeDefined(); }); it('does not report for else after if sibling', async () => { const content = '
VisibleHidden
'; 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 error for sibling else after a loop element (removed in Core v1.15)', async () => { const content = ''; 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).toBeDefined(); }); it('does not report for else template companion on a loop element', async () => { const content = ''; 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 = ''; 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 = '
ABC
'; 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('must be preceded')); expect(elseError).toBeUndefined(); }); it('reports error for orphaned else-if without the loop hint', async () => { const content = '
B
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const elseIfError = diagnostics.find(d => msg(d).includes('"else-if" must be preceded')); expect(elseIfError).toBeDefined(); expect(elseIfError!.message).not.toContain('else="templateId"'); }); it('sibling else after a loop element error includes the else="templateId" hint', async () => { const content = ''; 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).toBeDefined(); expect(elseError!.message).toContain('else="templateId"'); }); it('reports error for sibling else after foreach and for loop variants', async () => { for (const loopDir of ['foreach', 'for']) { const content = ``; 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).toBeDefined(); } }); it('does not report for else template companion on an if element', async () => { const content = '
Visible
'; 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 = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const tplError = diagnostics.find(d => msg(d).includes('referenced but not defined')); expect(tplError).toBeDefined(); expect(tplError!.message).toContain('Template "missing-tpl"'); }); }); // ─── Rule 4: Unknown filters ───────────────────────────────────────── describe('Unknown filters', () => { it('warns about unknown filter names', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const filterWarn = diagnostics.find(d => msg(d).includes('Unknown filter')); expect(filterWarn).toBeDefined(); }); it('does not warn about known filters', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const filterWarn = diagnostics.find(d => msg(d).includes('Unknown filter')); expect(filterWarn).toBeUndefined(); }); it('warns about each unknown filter in chained pipes', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const filterWarns = diagnostics.filter(d => msg(d).includes('Unknown filter')); expect(filterWarns.length).toBe(2); }); it('does not warn for non-directive attributes with pipes', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const filterWarn = diagnostics.find(d => msg(d).includes('Unknown filter')); expect(filterWarn).toBeUndefined(); }); it('warns for logical OR operand mistaken as filter name', async () => { // Expressions like "a || b" contain pipes. The diagnostics provider // splits by single | and checks each segment as a potential filter. // The segment "| b" yields filterName "b" which passes the alphanumeric // check, so it IS flagged as an unknown filter. This is a known // limitation: the naive pipe split does not distinguish || from |. const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const filterWarn = diagnostics.find(d => msg(d).includes('Unknown filter')); expect(filterWarn).toBeDefined(); }); it('does not warn for filter with colon args', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const filterWarn = diagnostics.find(d => msg(d).includes('Unknown filter')); expect(filterWarn).toBeUndefined(); }); }); // ─── Rule 5: Duplicate state declarations ──────────────────────────── describe('Duplicate state declarations', () => { it('warns about duplicate state names across elements', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const dupState = diagnostics.filter(d => msg(d).includes('Duplicate state declaration')); expect(dupState.length).toBeGreaterThanOrEqual(2); }); it('does not warn for unique state names', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const dupState = diagnostics.find(d => msg(d).includes('Duplicate state declaration')); expect(dupState).toBeUndefined(); }); it('parses simple state names (non-object form)', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const dupState = diagnostics.filter(d => msg(d).includes('Duplicate state declaration')); expect(dupState.length).toBeGreaterThanOrEqual(2); }); it('does not flag state without a value', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const dupState = diagnostics.find(d => msg(d).includes('Duplicate state declaration')); expect(dupState).toBeUndefined(); }); }); // ─── Rule 6: Invalid animation ─────────────────────────────────────── describe('Invalid animations', () => { it('warns about unknown animation name', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const animWarn = diagnostics.find(d => msg(d).includes('Unknown animation')); expect(animWarn).toBeDefined(); }); it('does not warn about valid animation', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const animWarn = diagnostics.find(d => msg(d).includes('Unknown animation')); expect(animWarn).toBeUndefined(); }); it('does not warn about custom- prefixed animations', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const animWarn = diagnostics.find(d => msg(d).includes('Unknown animation')); expect(animWarn).toBeUndefined(); }); it('does not warn for empty animation value', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const animWarn = diagnostics.find(d => msg(d).includes('Unknown animation')); expect(animWarn).toBeUndefined(); }); it('warns for animate-enter and animate-leave with unknown names', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const animWarns = diagnostics.filter(d => msg(d).includes('Unknown animation')); expect(animWarns.length).toBe(2); }); }); // ─── Rule 7: Invalid event modifier ────────────────────────────────── describe('Invalid event modifiers', () => { it('warns about unknown event modifiers', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const modWarn = diagnostics.find(d => msg(d).includes('Unknown event modifier')); expect(modWarn).toBeDefined(); expect(modWarn!.message).toContain('badmod'); }); it('does not warn about known event modifiers', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const modWarn = diagnostics.find(d => msg(d).includes('Unknown event modifier')); expect(modWarn).toBeUndefined(); }); it('does not warn for on: events without modifiers', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const modWarn = diagnostics.find(d => msg(d).includes('Unknown event modifier')); expect(modWarn).toBeUndefined(); }); it('warns only about the first unknown modifier in a chain', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const modWarns = diagnostics.filter(d => msg(d).includes('Unknown event modifier')); // The code breaks after the first unknown modifier expect(modWarns.length).toBe(1); expect(modWarns[0].message).toContain('badmod'); }); }); // ─── Rule 8: Unknown validator ─────────────────────────────────────── describe('Unknown validators', () => { it('warns about unknown validator names', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const valWarn = diagnostics.find(d => msg(d).includes('Unknown validator')); expect(valWarn).toBeDefined(); expect(valWarn!.message).toContain('notAValidator'); }); it('does not warn about known validators', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const valWarn = diagnostics.find(d => msg(d).includes('Unknown validator')); expect(valWarn).toBeUndefined(); }); it('warns for each unknown validator in pipe-separated list', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const valWarns = diagnostics.filter(d => msg(d).includes('Unknown validator')); expect(valWarns.length).toBe(2); }); it('handles validators with colon arguments', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const valWarn = diagnostics.find(d => msg(d).includes('Unknown validator')); expect(valWarn).toBeUndefined(); }); it('skips empty rules in validator list', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const valWarn = diagnostics.find(d => msg(d).includes('Unknown validator')); expect(valWarn).toBeUndefined(); }); }); // ─── Rule 9: Model on non-form element ─────────────────────────────── describe('Model on non-form element', () => { it('warns about model on a div', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const modelWarn = diagnostics.find(d => msg(d).includes('"model" is typically used')); expect(modelWarn).toBeDefined(); expect(modelWarn!.message).toContain('
'); }); it('does not warn for model on input', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const modelWarn = diagnostics.find(d => msg(d).includes('"model" is typically used')); expect(modelWarn).toBeUndefined(); }); it('does not warn for model on textarea', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const modelWarn = diagnostics.find(d => msg(d).includes('"model" is typically used')); expect(modelWarn).toBeUndefined(); }); it('does not warn for model on select', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const modelWarn = diagnostics.find(d => msg(d).includes('"model" is typically used')); expect(modelWarn).toBeUndefined(); }); it('warns about model on a span', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const modelWarn = diagnostics.find(d => msg(d).includes('"model" is typically used')); expect(modelWarn).toBeDefined(); expect(modelWarn!.message).toContain(''); }); }); // ─── Rule 10: Duplicate ref names ──────────────────────────────────── describe('Duplicate ref names', () => { it('warns about duplicate ref declarations', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const dupRef = diagnostics.filter(d => msg(d).includes('Duplicate ref')); expect(dupRef.length).toBeGreaterThanOrEqual(2); }); it('does not warn for unique ref names', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const dupRef = diagnostics.find(d => msg(d).includes('Duplicate ref')); expect(dupRef).toBeUndefined(); }); it('does not track ref without a value', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const dupRef = diagnostics.find(d => msg(d).includes('Duplicate ref')); expect(dupRef).toBeUndefined(); }); }); // ─── Rule 11: Template ID referenced but not defined ────────────────── describe('Template reference validation', () => { it('warns when a use attribute references a non-existent template', async () => { 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('Template "missingTpl" is referenced but not defined')); expect(tplWarn).toBeDefined(); }); it('does not warn when template exists', async () => { 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(); }); 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 = '
'; 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 = ''; 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 = ''; 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 = ''; 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

'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); expect(diagnostics.length).toBe(0); }); it('does not report diagnostics for standard HTML form elements', async () => { const content = '
'; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); expect(diagnostics.length).toBe(0); }); }); // ─── foreach / for directives ──────────────────────────────────────── describe('foreach directive', () => { it('does not flag foreach without template as an error', async () => { const content = '
  • '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const templateError = diagnostics.find(d => msg(d).includes('template')); expect(templateError).toBeUndefined(); }); it('reports error for foreach without a value', async () => { const content = '
  • '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const reqError = diagnostics.find(d => msg(d).includes('"foreach" requires a value')); expect(reqError).toBeDefined(); }); }); describe('for directive', () => { it('does not flag valid for directive', async () => { const content = '
  • '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const forError = diagnostics.find(d => msg(d).includes('"for"')); expect(forError).toBeUndefined(); }); it('reports error for for directive without a value', async () => { const content = '
  • '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const reqError = diagnostics.find(d => msg(d).includes('"for" requires a value')); expect(reqError).toBeDefined(); }); }); // ─── Rule 16: switch × loop incompatibility ───────────────────────── describe('switch × loop incompatibility (Finding 5)', () => { it('warns when case appears on a looped element', async () => { const content = '
    '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"case" on a looped element')); expect(warn).toBeDefined(); expect(warn!.severity).toBe(DiagnosticSeverity.Warning); }); it('warns when default appears on a looped element', async () => { const content = '
    '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"default" on a looped element')); expect(warn).toBeDefined(); }); it('does not warn for case without a loop', async () => { const content = '
    Active
    '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('on a looped element')); expect(warn).toBeUndefined(); }); it('does not warn for loop without case', async () => { const content = '
  • '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('on a looped element is incompatible')); expect(warn).toBeUndefined(); }); }); // ─── Rule 17: if + loop on same element ────────────────────────────── describe('if + loop on same element (Finding 9)', () => { it('warns when if and each are on the same element', async () => { const content = '
  • '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"if" and "each" on the same element')); expect(warn).toBeDefined(); expect(warn!.severity).toBe(DiagnosticSeverity.Warning); }); it('warns when if and foreach are on the same element', async () => { const content = '
  • '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"if" and "foreach" on the same element')); expect(warn).toBeDefined(); }); it('warns when if and for are on the same element', async () => { const content = '
  • '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"if" and "for" on the same element')); expect(warn).toBeDefined(); }); it('does not warn when if and loop are on separate elements', async () => { const content = '
  • '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"if" and "each" on the same element')); expect(warn).toBeUndefined(); }); }); // ─── Rule 18: ref on looped element ────────────────────────────────── describe('ref on looped element (Finding 10)', () => { it('warns when ref is on a looped element', async () => { const content = '
  • '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"ref" on a looped element')); expect(warn).toBeDefined(); expect(warn!.severity).toBe(DiagnosticSeverity.Warning); }); it('warns for ref on foreach loop', async () => { const content = '
    '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"ref" on a looped element')); expect(warn).toBeDefined(); }); it('does not warn for ref without a loop', async () => { const content = '
    '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"ref" on a looped element')); expect(warn).toBeUndefined(); }); }); // ─── Rule 19: bind-value + model redundancy ───────────────────────── describe('bind-value + model redundancy (Finding 13)', () => { it('warns when bind-value and model are on the same element', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"bind-value" and "model"')); expect(warn).toBeDefined(); expect(warn!.severity).toBe(DiagnosticSeverity.Warning); }); it('does not warn for model alone', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"bind-value" and "model"')); expect(warn).toBeUndefined(); }); it('does not warn for bind-value alone', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"bind-value" and "model"')); expect(warn).toBeUndefined(); }); it('does not warn for bind-src or other bind-* with model', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"bind-value" and "model"')); expect(warn).toBeUndefined(); }); }); // ─── Rule 20: watch + on:change on form control ───────────────────── describe('watch + on:change on form control (Finding 19)', () => { it('warns when watch and on:change are on an input', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"watch" and "on:change"')); expect(warn).toBeDefined(); expect(warn!.severity).toBe(DiagnosticSeverity.Warning); }); it('warns when watch and on:change are on a select', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"watch" and "on:change"')); expect(warn).toBeDefined(); }); it('warns when watch and on:change with modifiers are on an input', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"watch" and "on:change.debounce.300"')); expect(warn).toBeDefined(); }); it('does not warn on non-form elements', async () => { const content = '
    '; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"watch" and "on:change"')); expect(warn).toBeUndefined(); }); it('does not warn for watch without on:change', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"watch" and')); expect(warn).toBeUndefined(); }); }); // ─── Rule 21: t + bind double text-writer ─────────────────────────── describe('t + bind double text-writer (Finding 20)', () => { it('warns when t and bind are on the same element', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"t" and "bind"')); expect(warn).toBeDefined(); expect(warn!.severity).toBe(DiagnosticSeverity.Warning); }); it('does not warn for t without bind', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"t" and "bind"')); expect(warn).toBeUndefined(); }); it('does not warn for bind without t', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"t" and "bind"')); expect(warn).toBeUndefined(); }); it('does not warn for t with bind-html (different targets)', async () => { const content = ''; const doc = createDocument(content); const conn = createMockConnection(); await validateTextDocument(doc, conn as any); const diagnostics = conn.getDiagnostics(); const warn = diagnostics.find(d => msg(d).includes('"t" and "bind"')); expect(warn).toBeUndefined(); }); }); });