import { describe, expect, it } from "vitest";
import { apply, transform } from "./testids.ts";
const run = (source: string, options?: Parameters<typeof transform>[1]) => {
const { inserts, seen } = transform(source, options);
return { out: apply(source, inserts), seen };
};
const idOf = (markup: string) => run(markup).seen[0]?.id ?? null;
describe("handlers", () => {
it("names a plain handler", () =>
expect(idOf("<button onclick={deleteIssue}>x</button>")).toBe("deleteIssue"));
it("names the call inside an arrow", () => {
expect(idOf("<button onclick={() => archive(issue)}>x</button>")).toBe("archive");
expect(idOf("<button onclick={() => void copy()}>x</button>")).toBe("copy");
expect(idOf("<button onclick={() => file && upload(file)}>x</button>")).toBe("upload");
});
it("names an action() call after the action, not .run", () =>
expect(idOf("<button onclick={() => save.run(draft)}>x</button>")).toBe("save"));
it("reads assignments as what they do", () => {
expect(idOf("<button onclick={() => (open = false)}>x</button>")).toBe("close");
expect(idOf("<button onclick={() => (drawerOpen = true)}>x</button>")).toBe("openDrawer");
expect(idOf("<button onclick={() => (menu = !menu)}>x</button>")).toBe("toggleMenu");
});
it("drops the on prefix a prop carries", () => {
expect(idOf("<button onclick={onCancel}>x</button>")).toBe("cancel");
expect(idOf("<button onclick={onconfirm}>x</button>")).toBe("confirm");
});
it("leaves a forwarded onclick for the call site", () =>
expect(idOf("<button {type} onclick={onclick}>x</button>")).toBeNull());
it("gives up on a handler with no single action", () =>
expect(idOf("<button onclick={() => { a(); b(); }}>x</button>")).toBeNull());
});
describe("links", () => {
it("names a literal href by its last word", () => {
expect(idOf('<a href="/settings/keys">x</a>')).toBe("keys");
expect(idOf('<Link href={resolve("/signup")}>x</Link>')).toBe("signup");
});
it("skips the parts only known at runtime", () => {
expect(idOf("<a href={`/${handle}/${repo}/issues`}>x</a>")).toBe("issues");
expect(idOf("<a href={`/${handle}`}>x</a>")).toBeNull();
});
it("names a variable href after the variable", () =>
expect(idOf("<a href={newRepoHref}>x</a>")).toBe("newRepoHref"));
it("ignores external links and a forwarded href", () => {
expect(idOf('<a href="https://example.com">x</a>')).toBeNull();
expect(idOf("<a {href}>x</a>")).toBeNull();
});
});
describe("fields", () => {
it("names an input after what it binds", () =>
expect(idOf("<Input bind:value={form.title} />")).toBe("formTitle"));
it("names a submit button submit", () =>
expect(idOf('<button type="submit">x</button>')).toBe("submit"));
it("prefers a handler over a name", () =>
expect(idOf('<button name="a" onclick={go}>x</button>')).toBe("go"));
});
describe("injection", () => {
it("puts the id on a component call site", () =>
expect(run("<Button onclick={save}>x</Button>").out).toBe(
'<Button data-testid="save" onclick={save}>x</Button>'
));
it("leaves a hand-written id alone", () => {
const { out, seen } = run('<button data-testid="mine" onclick={save}>x</button>');
expect(out).toBe('<button data-testid="mine" onclick={save}>x</button>');
expect(seen.map((item) => [item.id, item.source])).toEqual([["mine", "hand"]]);
});
it("does not touch elements that are not interactive", () =>
expect(run("<div><p>hi</p></div>").out).toBe("<div><p>hi</p></div>"));
it("does not touch a component it has no rule for", () =>
expect(run("<Avatar bind:value={x}>y</Avatar>").out).toBe(
"<Avatar bind:value={x}>y</Avatar>"
));
it("marks the root elements with the component, through blocks too", () => {
const { out } = run("{#if a}<section>x</section>{/if}<aside>y</aside>", {
component: "Panel"
});
expect(out).toBe(
'{#if a}<section data-component="Panel">x</section>{/if}<aside data-component="Panel">y</aside>'
);
});
it("does not mark elements nested in another", () =>
expect(run("<div><span>x</span></div>", { component: "Panel" }).out).toBe(
'<div data-component="Panel"><span>x</span></div>'
));
it("copes with typescript in the script", () =>
expect(
run(
'<script lang="ts">let n: number = $state(0);</script><button onclick={() => n++}>x</button>'
).out
).toContain("<button onclick"));
});
describe("instanced components", () => {
it("names a modal after what it binds, without the Open", () => {
expect(idOf("<ConfirmModal bind:open={deleteConfirmOpen} title={t} />")).toBe(
"deleteConfirm"
);
expect(run("<Modal bind:open={showKeys}>x</Modal>").out).toContain('testid="showKeys"');
});
it("does not name a modal after a bare open", () =>
expect(idOf("<Modal bind:open>x</Modal><Modal bind:open>y</Modal>")).toBeNull());
it("names a menu after its id literal", () =>
expect(idOf('<Dropdown id="profile-menu" group="topbar">x</Dropdown>')).toBe(
"profileMenu"
));
it("falls back to the tag only when it is alone in the file", () => {
expect(run("<MoreMenu>x</MoreMenu>").out).toBe('<MoreMenu testid="moreMenu">x</MoreMenu>');
expect(
run("<MoreMenu>x</MoreMenu><MoreMenu>y</MoreMenu>").seen.map((item) => item.id)
).toEqual([null, null]);
});
it("leaves a hand-written testid prop alone", () =>
expect(run('<Modal testid="mine" bind:open={a}>x</Modal>').out).toBe(
'<Modal testid="mine" bind:open={a}>x</Modal>'
));
it("lists a hand-written id on any element, not only the interactive ones", () =>
expect(run('<h1 data-testid="title">x</h1>').seen.map((item) => item.id)).toEqual([
"title"
]));
it("passes data-component through a root Card, which spreads rest onto its own root", () =>
expect(run('<Card padding="none">x</Card>', { component: "Panel" }).out).toBe(
'<Card data-component="Panel" padding="none">x</Card>'
));
it("does not mark a root component that cannot take it", () =>
expect(run("<ProfileTabs />", { component: "Panel" }).out).toBe("<ProfileTabs />"));
});