Files
Clintchiz fdd0c9f847
Quality / quality (ubuntu-latest) (push) Failing after 10m13s
Quality / quality (windows-latest) (push) Canceled after 0s
chore: complete HTML editing verification
2026-08-18 22:49:07 +05:30

199 lines
8.0 KiB
TypeScript

import { expect, test } from "bun:test";
import { fileURLToPath } from "node:url";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { positionAt } from "../src/index.ts";
// End-to-end verification (Task 8): drives the real language server, over
// real LSP stdio framing, against a realistic WRN page fixture. The page's
// text is read from disk rather than duplicated here, so the fixture remains
// reusable and independently inspectable.
const pagePath = join(
fileURLToPath(new URL(".", import.meta.url)),
"fixtures/html-editing-check.wrn",
);
const pageText = readFileSync(pagePath, "utf8");
function packet(value: unknown): Uint8Array {
const body = JSON.stringify(value);
return new TextEncoder().encode(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
}
async function withServer<T>(
run: (
send: (msg: unknown) => Promise<void>,
readUntil: (marker: string) => Promise<string>,
) => Promise<T>,
): Promise<T> {
const process = Bun.spawn(
["bun", "run", fileURLToPath(new URL("../src/server.ts", import.meta.url))],
{ stdin: "pipe", stdout: "pipe", stderr: "pipe" },
);
const reader = process.stdout.getReader();
let output = "";
async function readUntil(marker: string): Promise<string> {
while (!output.includes(marker)) {
const chunk = await reader.read();
if (chunk.done) break;
output += new TextDecoder().decode(chunk.value);
}
return output.slice(output.indexOf(marker));
}
async function send(message: unknown): Promise<void> {
process.stdin.write(packet(message));
await process.stdin.flush();
}
try {
return await run(send, readUntil);
} finally {
process.kill();
await process.exited;
}
}
test("HTML editor support works end-to-end against the real scratch page", async () => {
const uri = "file:///html-editing-check.wrn";
// Positions derived from the real file content, not hardcoded line/column
// literals, so the test tracks the page if it changes.
const viewOpenBrace = pageText.indexOf("view {");
const viewCloseBrace = pageText.indexOf("}", pageText.indexOf("</main>"));
const h1TagOffset = pageText.indexOf("<h1>") + 1; // inside "h1"
const mainOpenNameOffset = pageText.indexOf("<main>") + 1; // inside "main" opening tag name
const mainOpenTagEndOffset = pageText.indexOf("<main>") + "<main>".length; // just after '>'
const seoTitleOffset = pageText.indexOf('title = "HTML editing check"'); // inside the seo block
// Simulate typing "<" inside the seo {} block: insert the character into a
// copy of the real page text (the checked-in file itself is never
// mutated) and ask for completion right after it. This is the scenario
// that actually exercises the view-region guard: if the guard were
// defeated, this exact position is where the HTML language service would
// offer tag completions, because it is positioned directly after an
// unclosed "<".
const seoInjectedText = pageText.slice(0, seoTitleOffset) + "<" + pageText.slice(seoTitleOffset);
const seoInjectedPosition = positionAt(seoInjectedText, seoTitleOffset + 1);
const viewPosition = positionAt(pageText, h1TagOffset);
const linkedEditingPosition = positionAt(pageText, mainOpenNameOffset);
const tagCompletePosition = positionAt(pageText, mainOpenTagEndOffset);
const viewStartLine = positionAt(pageText, viewOpenBrace).line;
const viewEndLine = positionAt(pageText, viewCloseBrace).line;
await withServer(async (send, readUntil) => {
await send({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
await readUntil('"id":1');
await send({
jsonrpc: "2.0",
method: "textDocument/didOpen",
params: { textDocument: { uri, version: 1, text: pageText } },
});
// --- Completion inside view {} : HTML entries present, WRNexus sorts first ---
await send({
jsonrpc: "2.0",
id: 2,
method: "textDocument/completion",
params: { textDocument: { uri }, position: viewPosition },
});
const completionReply = await readUntil('"id":2');
// An HTML tag entry is present, tagged with the "1" (below-wrnexus) sortText prefix.
expect(completionReply).toMatch(/"label":"div"[^}]*"sortText":"1/);
// A WRNexus keyword entry is present, tagged with the "0" (above-html) sortText prefix.
expect(completionReply).toMatch(/"label":"page"[^}]*"sortText":"0/);
// No html-prefixed sortText is lexicographically smaller than any wrnexus one:
// every "0..." sortText must precede every "1..." sortText, which is exactly
// what makes WRNexus entries render above HTML ones in an editor.
expect(completionReply).not.toMatch(/"sortText":"1[^"]*"[\s\S]*"sortText":"0/);
// --- Negative case: completion right after "<" typed inside seo {} must NOT include HTML entries ---
const seoUri = "file:///html-editing-check-seo-inject.wrn";
await send({
jsonrpc: "2.0",
method: "textDocument/didOpen",
params: { textDocument: { uri: seoUri, version: 1, text: seoInjectedText } },
});
await send({
jsonrpc: "2.0",
id: 3,
method: "textDocument/completion",
params: { textDocument: { uri: seoUri }, position: seoInjectedPosition },
});
const seoReply = await readUntil('"id":3');
expect(seoReply).not.toContain('"label":"div"');
expect(seoReply).not.toContain('"label":"span"');
expect(seoReply).not.toContain('"label":"h1"');
expect(seoReply).not.toMatch(/"sortText":"1/);
// --- Hover over a tag inside the view block returns documentation ---
await send({
jsonrpc: "2.0",
id: 4,
method: "textDocument/hover",
params: { textDocument: { uri }, position: viewPosition },
});
const hoverReply = await readUntil('"id":4');
expect(hoverReply).toContain('"contents"');
expect(hoverReply).not.toMatch(/"result":\s*null/);
// --- Folding ranges are returned and all lie within the view block ---
await send({
jsonrpc: "2.0",
id: 5,
method: "textDocument/foldingRange",
params: { textDocument: { uri } },
});
const foldReply = await readUntil('"id":5');
expect(foldReply).toContain('"result":[');
const foldBody = foldReply.slice(foldReply.indexOf('"result":['));
const foldRanges = [...foldBody.matchAll(/"startLine":(\d+),"endLine":(\d+)/g)];
expect(foldRanges.length).toBeGreaterThan(0);
for (const [, start, end] of foldRanges) {
expect(Number(start)).toBeGreaterThanOrEqual(viewStartLine);
expect(Number(end)).toBeLessThanOrEqual(viewEndLine);
}
// --- Linked editing at the <main> opening tag name returns two ranges ---
await send({
jsonrpc: "2.0",
id: 6,
method: "textDocument/linkedEditingRange",
params: { textDocument: { uri }, position: linkedEditingPosition },
});
const linkedReply = await readUntil('"id":6');
expect(linkedReply).toContain('"ranges"');
const rangeCount = (linkedReply.match(/"start":\{/g) ?? []).length;
expect(rangeCount).toBe(2);
// --- wrn/tagComplete after <main> returns the closing snippet ---
await send({
jsonrpc: "2.0",
id: 7,
method: "wrn/tagComplete",
params: { textDocument: { uri }, position: tagCompletePosition },
});
const tagCompleteReply = await readUntil('"id":7');
expect(tagCompleteReply).toContain('"result":"$0</main>"');
// --- wrn/tagComplete for a void element (<br>) returns null ---
const voidText = `page A {\n view {\n <br>\n }\n}\n`;
const voidUri = "file:///html-editing-check-void.wrn";
await send({
jsonrpc: "2.0",
method: "textDocument/didOpen",
params: { textDocument: { uri: voidUri, version: 1, text: voidText } },
});
const brOffset = voidText.indexOf("<br>") + "<br>".length;
const brPosition = positionAt(voidText, brOffset);
await send({
jsonrpc: "2.0",
id: 8,
method: "wrn/tagComplete",
params: { textDocument: { uri: voidUri }, position: brPosition },
});
const voidReply = await readUntil('"id":8');
expect(voidReply).toContain('"result":null');
});
});