147 lines
4.7 KiB
TypeScript
147 lines
4.7 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import {
|
|
clearHtmlRegionCache,
|
|
isInsideHtml,
|
|
viewRegions,
|
|
virtualHtmlDocument,
|
|
} from "../src/html-regions.ts";
|
|
|
|
function doc(text: string): { uri: string; text: string; version?: number } {
|
|
return { uri: `file:///${Math.random()}.wrn`, text };
|
|
}
|
|
|
|
const PAGE = `page Home {
|
|
view {
|
|
<div class="card">hello</div>
|
|
}
|
|
}
|
|
`;
|
|
|
|
test("the virtual document preserves length and newline offsets", () => {
|
|
// This is what makes position mapping unnecessary. If it breaks, every
|
|
// feature reports positions off by some amount instead of failing loudly.
|
|
const source = doc(PAGE);
|
|
const virtual = virtualHtmlDocument(source);
|
|
|
|
expect(virtual.text.length).toBe(source.text.length);
|
|
expect(virtual.languageId).toBe("html");
|
|
for (let i = 0; i < source.text.length; i += 1) {
|
|
if (source.text[i] === "\n") expect(virtual.text[i]).toBe("\n");
|
|
}
|
|
});
|
|
|
|
test("markup survives into the virtual document and everything else is blanked", () => {
|
|
const virtual = virtualHtmlDocument(doc(PAGE));
|
|
expect(virtual.text).toContain('<div class="card">hello</div>');
|
|
expect(virtual.text).not.toContain("page Home");
|
|
expect(virtual.text).not.toContain("view");
|
|
});
|
|
|
|
test("an apostrophe in text content does not swallow later regions", () => {
|
|
// A scanner treating ' as a string delimiter anywhere considers the rest of
|
|
// the file one open string and loses every later region.
|
|
const source = `page A {
|
|
view {
|
|
<p>it's fine</p>
|
|
}
|
|
}
|
|
component B {
|
|
view {
|
|
<span>second</span>
|
|
}
|
|
}
|
|
`;
|
|
expect(viewRegions(source)).toHaveLength(2);
|
|
expect(virtualHtmlDocument(doc(source)).text).toContain("<span>second</span>");
|
|
});
|
|
|
|
test("interpolation braces nest without ending the region early", () => {
|
|
const source = `page A {
|
|
view {
|
|
<div class={cond ? "a" : "b"} data-x={{ a: 1 }}>after</div>
|
|
}
|
|
}
|
|
`;
|
|
const regions = viewRegions(source);
|
|
expect(regions).toHaveLength(1);
|
|
expect(virtualHtmlDocument(doc(source)).text).toContain("after</div>");
|
|
});
|
|
|
|
test("unparseable mid-edit markup still yields a region", () => {
|
|
// Completion fires exactly when the document does not parse.
|
|
const source = `page A {
|
|
view {
|
|
<div class="
|
|
}
|
|
}
|
|
`;
|
|
expect(viewRegions(source).length).toBe(1);
|
|
});
|
|
|
|
test("a file with no view block yields no regions and a fully blank document", () => {
|
|
const source = `page A {
|
|
functions {
|
|
function go() {}
|
|
}
|
|
}
|
|
`;
|
|
expect(viewRegions(source)).toEqual([]);
|
|
expect(virtualHtmlDocument(doc(source)).text.trim()).toBe("");
|
|
});
|
|
|
|
test("regions are cached per document version", () => {
|
|
// One keystroke fans out into completion, hover, and tag-close requests.
|
|
const first = doc(PAGE);
|
|
first.version = 1;
|
|
expect(isInsideHtml(first, PAGE.indexOf("<div"))).toBe(true);
|
|
|
|
// Same version, mutated text: the cached regions are reused, proving the
|
|
// scan did not run again.
|
|
first.text = "page A { }";
|
|
expect(isInsideHtml(first, PAGE.indexOf("<div"))).toBe(true);
|
|
|
|
first.version = 2;
|
|
expect(isInsideHtml(first, PAGE.indexOf("<div"))).toBe(false);
|
|
});
|
|
|
|
test("clearHtmlRegionCache drops a closed document's cached regions", () => {
|
|
// A reopened document commonly restarts at version 1. Without clearing the
|
|
// cache on close, that version would match the stale entry from the prior
|
|
// session and serve regions scanned from the old text.
|
|
const first = doc(PAGE);
|
|
first.version = 1;
|
|
expect(isInsideHtml(first, PAGE.indexOf("<div"))).toBe(true);
|
|
|
|
clearHtmlRegionCache(first.uri);
|
|
|
|
// Same uri, same version 1, but a document with no view block at all: if
|
|
// the cache had survived, this would still report true from the old scan.
|
|
const reopened = { uri: first.uri, text: "page A { }", version: 1 };
|
|
expect(isInsideHtml(reopened, PAGE.indexOf("<div"))).toBe(false);
|
|
});
|
|
|
|
test("isInsideHtml distinguishes markup from surrounding code", () => {
|
|
const source = doc(PAGE);
|
|
const markupOffset = PAGE.indexOf("<div");
|
|
const keywordOffset = PAGE.indexOf("page");
|
|
|
|
expect(isInsideHtml(source, markupOffset)).toBe(true);
|
|
expect(isInsideHtml(source, keywordOffset)).toBe(false);
|
|
});
|
|
|
|
test("documents without a version field scan every time and detect mutations", () => {
|
|
// Without a version, the cache has no key to validate freshness. Mutations
|
|
// must be detected on every call, even when uri and document are reused.
|
|
const source = doc(PAGE);
|
|
// Explicitly verify version is undefined (not set by doc() helper).
|
|
expect(source.version).toBeUndefined();
|
|
|
|
const markupOffset = PAGE.indexOf("<div");
|
|
expect(isInsideHtml(source, markupOffset)).toBe(true);
|
|
|
|
// Mutate the text: remove the view block.
|
|
source.text = "page A { }";
|
|
// Same uri, same missing version, but different text: mutation must be detected.
|
|
expect(isInsideHtml(source, markupOffset)).toBe(false);
|
|
});
|