feat: add auth pages (login, signup, password reset, onboarding)

- Create stub auth API (lib/auth.ts) with simulated delay
- Add PasswordInput component with visibility toggle
- Add SocialAuthButtons component (Google/Apple placeholders)
- Add AuthLayout with split-panel layout and rotating testimonial
- Implement login page with email/password validation and remember me
- Implement signup page with password strength indicator and ToS checkbox
- Implement forgot-password page with email submission and success state
- Implement reset-password page with token validation from query params
- Implement 4-step onboarding flow (plan selection, watchlist, invites, success)
- Add ToastProvider to root app
- Write 28 tests for all auth components and form validation
This commit is contained in:
2026-05-25 15:20:01 -04:00
parent 6acbb6ca37
commit 25da0cd687
13 changed files with 1723 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render } from "solid-js/web";
import type { JSX } from "solid-js";
import PasswordInput from "./PasswordInput";
import SocialAuthButtons from "./SocialAuthButtons";
import AuthLayout from "./AuthLayout";
import { Input, Button } from "~/components/ui";
function mount(comp: () => JSX.Element): HTMLDivElement {
const container = document.createElement("div");
document.body.appendChild(container);
render(() => comp(), container);
return container;
}
beforeEach(() => {
document.body.innerHTML = "";
if (!globalThis.crypto) {
Object.defineProperty(globalThis, "crypto", { value: {} });
}
(globalThis.crypto as unknown as Record<string, unknown>).randomUUID = vi.fn(
() => "test-uuid-1234",
);
});
afterEach(() => {
document.body.innerHTML = "";
});
describe("PasswordInput", () => {
it("renders with label", () => {
mount(() => <PasswordInput label="Password" />);
expect(document.body.textContent).toContain("Password");
expect(document.querySelector("label")).toBeTruthy();
});
it("renders password type by default", () => {
mount(() => <PasswordInput label="Password" />);
const input = document.querySelector("input")!;
expect(input.getAttribute("type")).toBe("password");
});
it("toggles visibility when eye icon is clicked", () => {
mount(() => <PasswordInput label="Password" />);
const input = document.querySelector("input")!;
const toggle = document.querySelector("button[aria-label]")!;
expect(input.getAttribute("type")).toBe("password");
expect(toggle.getAttribute("aria-label")).toBe("Show password");
toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(input.getAttribute("type")).toBe("text");
expect(toggle.getAttribute("aria-label")).toBe("Hide password");
toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(input.getAttribute("type")).toBe("password");
});
it("shows error message", () => {
mount(() => (
<PasswordInput label="Password" error="Password is required" />
));
expect(document.body.textContent).toContain("Password is required");
});
it("shows helper text when no error", () => {
mount(() => (
<PasswordInput label="Password" helperText="At least 8 characters" />
));
expect(document.body.textContent).toContain("At least 8 characters");
});
it("hides helper text when error is present", () => {
mount(() => (
<PasswordInput
label="Password"
error="Required"
helperText="Helper text"
/>
));
expect(document.body.textContent).toContain("Required");
expect(document.body.textContent).not.toContain("Helper text");
});
it("forwards onInput handler", () => {
const onInput = vi.fn();
mount(() => <PasswordInput onInput={onInput} />);
const input = document.querySelector("input")!;
input.dispatchEvent(new InputEvent("input", { bubbles: true }));
expect(onInput).toHaveBeenCalled();
});
});
describe("SocialAuthButtons", () => {
it("renders Google and Apple buttons", () => {
mount(() => <SocialAuthButtons />);
const buttons = document.querySelectorAll("button");
expect(buttons.length).toBe(2);
expect(buttons[0].textContent).toContain("Google");
expect(buttons[1].textContent).toContain("Apple");
});
it("renders SVG icons in each button", () => {
mount(() => <SocialAuthButtons />);
const buttons = document.querySelectorAll("button");
expect(buttons[0].querySelector("svg")).toBeTruthy();
expect(buttons[1].querySelector("svg")).toBeTruthy();
});
});
describe("AuthLayout", () => {
it("renders children inside the form card", () => {
mount(() => (
<AuthLayout>
<p>Form content</p>
</AuthLayout>
));
expect(document.body.textContent).toContain("Form content");
});
it("renders ShieldAI branding", () => {
mount(() => (
<AuthLayout>
<p>Content</p>
</AuthLayout>
));
expect(document.body.textContent).toContain("ShieldAI");
});
it("renders gradient-card wrapper", () => {
mount(() => (
<AuthLayout>
<p>Content</p>
</AuthLayout>
));
expect(document.querySelector(".gradient-card")).toBeTruthy();
});
it("renders testimonial text", () => {
mount(() => (
<AuthLayout>
<p>Content</p>
</AuthLayout>
));
expect(document.body.textContent).toContain("ShieldAI");
expect(document.body.textContent).toContain("AI-Powered Identity Protection");
});
});