diff --git a/canvas/src/components/__tests__/Spinner.test.tsx b/canvas/src/components/__tests__/Spinner.test.tsx
new file mode 100644
index 00000000..610f3a03
--- /dev/null
+++ b/canvas/src/components/__tests__/Spinner.test.tsx
@@ -0,0 +1,58 @@
+// @vitest-environment jsdom
+/**
+ * Tests for Spinner component.
+ *
+ * Covers: sm/md/lg size classes, aria-hidden, motion-safe animate-spin class.
+ */
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import { Spinner } from "../Spinner";
+
+describe("Spinner — size variants", () => {
+ it("renders with sm size class", () => {
+ const { container } = render();
+ const svg = container.querySelector("svg");
+ expect(svg).toBeTruthy();
+ expect(svg?.className).toContain("w-3");
+ expect(svg?.className).toContain("h-3");
+ });
+
+ it("renders with md size class (default)", () => {
+ const { container } = render();
+ const svg = container.querySelector("svg");
+ expect(svg?.className).toContain("w-4");
+ expect(svg?.className).toContain("h-4");
+ });
+
+ it("renders with lg size class", () => {
+ const { container } = render();
+ const svg = container.querySelector("svg");
+ expect(svg?.className).toContain("w-5");
+ expect(svg?.className).toContain("h-5");
+ });
+
+ it("defaults to md size when no size prop given", () => {
+ const { container } = render();
+ const svg = container.querySelector("svg");
+ expect(svg?.className).toContain("w-4");
+ expect(svg?.className).toContain("h-4");
+ });
+
+ it("has aria-hidden=true so screen readers skip it", () => {
+ const { container } = render();
+ const svg = container.querySelector("svg");
+ expect(svg?.getAttribute("aria-hidden")).toBe("true");
+ });
+
+ it("includes the motion-safe:animate-spin class for CSS animation", () => {
+ const { container } = render();
+ const svg = container.querySelector("svg");
+ expect(svg?.className).toContain("motion-safe:animate-spin");
+ });
+
+ it("renders exactly one SVG element", () => {
+ const { container } = render();
+ expect(container.querySelectorAll("svg").length).toBe(1);
+ });
+});
diff --git a/canvas/src/components/__tests__/StatusBadge.test.tsx b/canvas/src/components/__tests__/StatusBadge.test.tsx
new file mode 100644
index 00000000..4a8ccddf
--- /dev/null
+++ b/canvas/src/components/__tests__/StatusBadge.test.tsx
@@ -0,0 +1,57 @@
+// @vitest-environment jsdom
+/**
+ * Tests for StatusBadge component.
+ *
+ * Covers: renders all three status variants, aria-label, role=status,
+ * icon presence, className variants, no render when passed invalid status.
+ */
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import { StatusBadge } from "../ui/StatusBadge";
+
+describe("StatusBadge — render", () => {
+ it("renders verified status with ✓ icon", () => {
+ render();
+ const badge = screen.getByRole("status");
+ expect(badge.textContent).toBe("✓");
+ expect(badge.getAttribute("aria-label")).toBe("Connection status: verified");
+ });
+
+ it("renders invalid status with ✗ icon", () => {
+ render();
+ const badge = screen.getByRole("status");
+ expect(badge.textContent).toBe("✗");
+ expect(badge.getAttribute("aria-label")).toBe("Connection status: invalid");
+ });
+
+ it("renders unverified status with ○ icon", () => {
+ render();
+ const badge = screen.getByRole("status");
+ expect(badge.textContent).toBe("○");
+ expect(badge.getAttribute("aria-label")).toBe("Connection status: unverified");
+ });
+
+ it("has role=status on the badge element", () => {
+ render();
+ expect(screen.getByRole("status")).toBeTruthy();
+ });
+
+ it("includes the config className on the rendered element", () => {
+ render();
+ const badge = screen.getByRole("status");
+ expect(badge.className).toContain("status-badge--valid");
+ });
+
+ it("includes status-badge--invalid class for invalid status", () => {
+ render();
+ const badge = screen.getByRole("status");
+ expect(badge.className).toContain("status-badge--invalid");
+ });
+
+ it("includes status-badge--unverified class for unverified status", () => {
+ render();
+ const badge = screen.getByRole("status");
+ expect(badge.className).toContain("status-badge--unverified");
+ });
+});
diff --git a/canvas/src/components/__tests__/ValidationHint.test.tsx b/canvas/src/components/__tests__/ValidationHint.test.tsx
new file mode 100644
index 00000000..1b2fc015
--- /dev/null
+++ b/canvas/src/components/__tests__/ValidationHint.test.tsx
@@ -0,0 +1,77 @@
+// @vitest-environment jsdom
+/**
+ * Tests for ValidationHint component.
+ *
+ * Covers: error state, valid state, neutral/hidden state,
+ * aria-live for error, icon rendering.
+ */
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import { ValidationHint } from "../ui/ValidationHint";
+
+describe("ValidationHint — error state", () => {
+ it("renders error message when error is a non-null string", () => {
+ render();
+ expect(screen.getByRole("alert")).toBeTruthy();
+ expect(screen.getByText("Invalid email address")).toBeTruthy();
+ });
+
+ it("includes the warning icon in error state", () => {
+ render();
+ expect(screen.getByText(/⚠/)).toBeTruthy();
+ });
+
+ it("uses the error class on the paragraph element", () => {
+ render();
+ const el = screen.getByRole("alert");
+ expect(el.className).toContain("validation-hint--error");
+ });
+
+ it("renders error even when showValid is true", () => {
+ render();
+ expect(screen.getByRole("alert")).toBeTruthy();
+ expect(screen.queryByText(/✓/)).toBeNull();
+ });
+});
+
+describe("ValidationHint — valid state", () => {
+ it("renders valid message when error is null and showValid is true", () => {
+ render();
+ expect(screen.getByText("Valid format")).toBeTruthy();
+ });
+
+ it("includes the checkmark icon in valid state", () => {
+ render();
+ expect(screen.getByText(/✓ Valid format/)).toBeTruthy();
+ });
+
+ it("uses the valid class on the paragraph element", () => {
+ render();
+ const el = document.body.querySelector(".validation-hint--valid");
+ expect(el).toBeTruthy();
+ });
+
+ it("renders nothing when error is null and showValid is false (default)", () => {
+ const { container } = render();
+ expect(container.textContent).toBe("");
+ });
+
+ it("renders nothing when error is empty string", () => {
+ const { container } = render();
+ expect(container.textContent).toBe("");
+ });
+});
+
+describe("ValidationHint — neutral / not-yet-validated", () => {
+ it("renders nothing when error is null and showValid defaults to false", () => {
+ const { container } = render();
+ expect(container.textContent).toBe("");
+ });
+
+ it("renders nothing when error is undefined", () => {
+ // @ts-expect-error — testing runtime behavior with undefined
+ const { container } = render();
+ expect(container.textContent).toBe("");
+ });
+});