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:
149
web/src/components/auth/auth.test.tsx
Normal file
149
web/src/components/auth/auth.test.tsx
Normal 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user