import { afterEach, describe, it, expect, vi } from "vitest";
import { lintHyperframeHtml, lintMediaUrls } from "./hyperframeLinter.js";
afterEach(() => vi.unstubAllGlobals());
describe("lintHyperframeHtml — orchestrator", () => {
const validComposition = `
`;
it("reports no errors for a valid composition", async () => {
const result = await lintHyperframeHtml(validComposition);
expect(result.ok).toBe(true);
expect(result.errorCount).toBe(0);
});
it("attaches filePath to findings when option is set", async () => {
const html = "";
const result = await lintHyperframeHtml(html, { filePath: "test.html" });
for (const finding of result.findings) {
expect(finding.file).toBe("test.html");
}
});
it("deduplicates identical findings", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const codes = result.findings.map((f) => `${f.code}|${f.message}`);
const uniqueCodes = [...new Set(codes)];
expect(codes.length).toBe(uniqueCodes.length);
});
it("strips wrapper before linting composition files", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html, { filePath: "compositions/my-comp.html" });
const missing = result.findings.filter(
(f) => f.code === "missing-composition-id" || f.code === "missing-dimensions",
);
expect(missing).toHaveLength(0);
});
it("ignores comments that mention template tags before the real template", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html, { filePath: "compositions/my-comp.html" });
const rootFindings = result.findings.filter(
(f) => f.code === "root_missing_composition_id" || f.code === "root_missing_dimensions",
);
expect(rootFindings).toHaveLength(0);
});
it("strips comments whose markers re-form after one pass (no decoy template survives)", async () => {
// Adjacent comment markers: removing the inner `` in a single pass
// re-joins `<` + `!-- … -->` into a fresh, complete `` that a lone
// global replace leaves behind — surfacing a decoy with no
// composition-id. A fixpoint strip removes it; this guards that behavior.
const html = `
<!-- -->
`;
const result = await lintHyperframeHtml(html, { filePath: "compositions/my-comp.html" });
const rootFindings = result.findings.filter(
(f) => f.code === "root_missing_composition_id" || f.code === "root_missing_dimensions",
);
expect(rootFindings).toHaveLength(0);
});
});
describe("lintMediaUrls", () => {
it("checks top-level remote media elements", async () => {
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
vi.stubGlobal("fetch", fetchMock);
await lintMediaUrls('
');
expect(fetchMock).toHaveBeenCalledWith(
"https://example.com/hero.png",
expect.objectContaining({ method: "HEAD" }),
);
});
it("ignores remote media embedded inside an iframe srcdoc attribute", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
await lintMediaUrls(``);
expect(fetchMock).not.toHaveBeenCalled();
});
});