import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/preact";
import { FormField } from "./FormField.js";
describe("FormField", () => {
it("renders label text", () => {
const { getByText } = render(
);
expect(getByText("Name")).toBeTruthy();
});
it("renders children (input element)", () => {
const { container } = render(
);
expect(container.querySelector("input#name")).toBeTruthy();
});
it("shows hint text when provided", () => {
const { getByText } = render(
);
expect(getByText("Enter your full name")).toBeTruthy();
});
it("shows error text when provided", () => {
const { getByText } = render(
);
expect(getByText("Name is required")).toBeTruthy();
});
it("shows required indicator when required", () => {
const { container } = render(
);
const indicator = container.querySelector(".strand-form-field__required");
expect(indicator).toBeTruthy();
expect(indicator!.textContent).toBe("*");
});
it("error replaces hint when both are present", () => {
const { queryByText } = render(
);
expect(queryByText("Name is required")).toBeTruthy();
expect(queryByText("Enter your full name")).toBeNull();
});
it("label has htmlFor attribute", () => {
const { container } = render(
);
const label = container.querySelector("label");
expect(label).toHaveAttribute("for", "email");
});
it("applies error class when error is present", () => {
const { container } = render(
);
expect(
container.querySelector(".strand-form-field--error")
).toBeTruthy();
});
// ── Success: the third message state ──
it("confirms a checked value without shouting it", () => {
// Polite, not assertive: this arrives while the member is still typing, so
// an alert region would interrupt a screen reader to deliver good news.
const { container } = render(
);
const el = container.querySelector(".strand-form-field__success");
expect(el?.textContent).toBe("Available.");
expect(el?.getAttribute("role")).toBe("status");
});
it("shows the problem rather than the confirmation when both are set", () => {
// A field reading "taken" above "Available." argues with itself.
const { container } = render(
);
expect(container.querySelector(".strand-form-field__error")?.textContent).toBe("Taken");
expect(container.querySelector(".strand-form-field__success")).toBeNull();
expect(container.querySelector(".strand-form-field__hint")).toBeNull();
});
it("replaces the hint, so one input never carries two instructions", () => {
const { container } = render(
);
expect(container.querySelector(".strand-form-field__success")).toBeTruthy();
expect(container.querySelector(".strand-form-field__hint")).toBeNull();
});
// ── The control is actually described ──
it("points the control at whichever message is showing", () => {
// Every message this component has ever rendered was announced by nothing
// on focus: the ids existed and no control referenced them.
const hint = render(
);
expect(
hint.container.querySelector("#name")?.getAttribute("aria-describedby")
).toBe("name-hint");
const err = render(
);
expect(
err.container.querySelector("#name")?.getAttribute("aria-describedby")
).toBe("name-error");
});
it("keeps a description the caller set themselves", () => {
const { container } = render(
);
expect(
container.querySelector("#name")?.getAttribute("aria-describedby")
).toBe("name-extra name-hint");
});
it("describes nothing when there is no message", () => {
const { container } = render(
);
expect(container.querySelector("#name")?.getAttribute("aria-describedby")).toBeNull();
});
});
import { snapshotFixtures } from "../../test/snapshot.js";
import { snapshotStylesheet } from "../../test/stylesheet.js";
import { fixtures } from "./FormField.fixtures.js";
snapshotFixtures(FormField, fixtures);
snapshotStylesheet(resolve(__dirname, "./FormField.css"));