175 lines
4.2 KiB
TypeScript
175 lines
4.2 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import {
|
|
htmlCompletions,
|
|
htmlFoldingRanges,
|
|
htmlHover,
|
|
htmlTagComplete,
|
|
} from "../src/html-service.ts";
|
|
|
|
function doc(text: string) {
|
|
return { uri: "file:///Page.wrn", text };
|
|
}
|
|
|
|
function positionOf(text: string, needle: string) {
|
|
const offset = text.indexOf(needle) + needle.length;
|
|
const before = text.slice(0, offset);
|
|
const lines = before.split("\n");
|
|
return { line: lines.length - 1, character: lines[lines.length - 1]!.length };
|
|
}
|
|
|
|
test("suggests HTML tags inside a view block", () => {
|
|
const text = `page A {
|
|
view {
|
|
<
|
|
}
|
|
}
|
|
`;
|
|
const items = htmlCompletions(doc(text), positionOf(text, " <"));
|
|
expect(items.some((item) => item.label === "div")).toBe(true);
|
|
expect(items.every((item) => item.sortText?.startsWith("1"))).toBe(true);
|
|
});
|
|
|
|
test("suggests attributes inside a tag", () => {
|
|
const text = `page A {
|
|
view {
|
|
<input
|
|
}
|
|
}
|
|
`;
|
|
const items = htmlCompletions(doc(text), positionOf(text, "<input "));
|
|
expect(items.some((item) => item.label === "type")).toBe(true);
|
|
});
|
|
|
|
test("returns nothing outside a view block", () => {
|
|
const text = `page A {
|
|
functions {
|
|
function go() { }
|
|
}
|
|
}
|
|
`;
|
|
expect(htmlCompletions(doc(text), positionOf(text, "function go() "))).toEqual([]);
|
|
});
|
|
|
|
test("hovers a tag inside a view block and nothing outside one", () => {
|
|
const text = `page A {
|
|
view {
|
|
<div>x</div>
|
|
}
|
|
}
|
|
`;
|
|
expect(htmlHover(doc(text), positionOf(text, "<di"))).not.toBeNull();
|
|
|
|
const code = `page A {
|
|
functions {
|
|
function go() { }
|
|
}
|
|
}
|
|
`;
|
|
expect(htmlHover(doc(code), positionOf(code, "func"))).toBeNull();
|
|
});
|
|
|
|
test("closes an open tag and leaves void elements alone", () => {
|
|
const open = `page A {
|
|
view {
|
|
<div>
|
|
}
|
|
}
|
|
`;
|
|
expect(htmlTagComplete(doc(open), positionOf(open, "<div>"))).toContain("</div>");
|
|
|
|
const void_ = `page A {
|
|
view {
|
|
<br>
|
|
}
|
|
}
|
|
`;
|
|
expect(htmlTagComplete(doc(void_), positionOf(void_, "<br>"))).toBeNull();
|
|
});
|
|
|
|
test("completes a self-closing component tag", () => {
|
|
const text = `page A {
|
|
view {
|
|
<Card /
|
|
}
|
|
}
|
|
`;
|
|
expect(htmlTagComplete(doc(text), positionOf(text, "<Card /"))).toBe(">");
|
|
});
|
|
|
|
test("does not misfire inside a quoted attribute value containing a slash", () => {
|
|
const doubleQuoted = `page A {
|
|
view {
|
|
<a href="https:/
|
|
}
|
|
}
|
|
`;
|
|
expect(htmlTagComplete(doc(doubleQuoted), positionOf(doubleQuoted, `href="https:/`))).toBeNull();
|
|
|
|
const singleQuoted = `page A {
|
|
view {
|
|
<img src='/assets/
|
|
}
|
|
}
|
|
`;
|
|
expect(htmlTagComplete(doc(singleQuoted), positionOf(singleQuoted, `src='/assets/`))).toBeNull();
|
|
});
|
|
|
|
test("still completes a self-close after a preceding attribute", () => {
|
|
const text = `page A {
|
|
view {
|
|
<Card title="x" /
|
|
}
|
|
}
|
|
`;
|
|
expect(htmlTagComplete(doc(text), positionOf(text, `title="x" /`))).toBe(">");
|
|
});
|
|
|
|
test("returns no tag completion outside a view block", () => {
|
|
const text = `page A {
|
|
functions {
|
|
function go() { }
|
|
}
|
|
}
|
|
`;
|
|
expect(htmlTagComplete(doc(text), positionOf(text, "function go() "))).toBeNull();
|
|
});
|
|
|
|
test("folding ranges stay inside view regions", () => {
|
|
const text = `page A {
|
|
view {
|
|
<ul>
|
|
<li>one</li>
|
|
</ul>
|
|
}
|
|
}
|
|
`;
|
|
const ranges = htmlFoldingRanges(doc(text));
|
|
expect(ranges.length).toBeGreaterThan(0);
|
|
|
|
const viewStartLine = text.slice(0, text.indexOf("view {")).split("\n").length - 1;
|
|
for (const range of ranges) expect(range.startLine).toBeGreaterThan(viewStartLine - 1);
|
|
});
|
|
|
|
import { mergeCompletions } from "../src/html-service.ts";
|
|
|
|
test("merging ranks WRNexus entries above HTML and drops exact collisions", () => {
|
|
const merged = mergeCompletions(
|
|
[
|
|
{ label: "Card", kind: 7 },
|
|
{ label: "table", kind: 7 },
|
|
],
|
|
[
|
|
{ label: "div", kind: 10, sortText: "1div" },
|
|
{ label: "table", kind: 10, sortText: "1table" },
|
|
],
|
|
);
|
|
|
|
const labels = merged.map((item) => item.label);
|
|
expect(labels.filter((label) => label === "table")).toHaveLength(1);
|
|
expect(merged.find((item) => item.label === "Card")?.sortText?.startsWith("0")).toBe(true);
|
|
expect(merged.find((item) => item.label === "div")?.sortText?.startsWith("1")).toBe(true);
|
|
|
|
const sorted = [...merged].sort((a, b) => (a.sortText ?? "").localeCompare(b.sortText ?? ""));
|
|
expect(sorted[0]!.label).toBe("Card");
|
|
});
|