Merge branch 'refactor/foritify_tests' of https://git.ramvignesh.dev/me/pi-ku into feature/home_revamp
CI / Generate Certificates (pull_request) Successful in 25s
CI / Frontend CI (pull_request) Successful in 1m5s
CI / Backend CI (pull_request) Failing after 30s
CI / E2E Tests (pull_request) Failing after 15s

This commit is contained in:
me
2026-05-08 05:22:51 +05:30
parent aeead98570
commit b29218b6e6
12 changed files with 392 additions and 403 deletions
+18 -18
View File
@@ -9,8 +9,8 @@ function renderGuard(ui: React.ReactNode, mountPath: "/protected" | "/public") {
return render(
<MemoryRouter initialEntries={[mountPath]}>
<Routes>
<Route path="/login" element={<div>Login Page</div>} />
<Route path="/drawer" element={<div>Drawer Page</div>} />
<Route path="/login" element={<div data-testid="login-page">Login Page</div>} />
<Route path="/drawer" element={<div data-testid="drawer-page">Drawer Page</div>} />
<Route path="/protected" element={ui} />
<Route path="/public" element={ui} />
</Routes>
@@ -35,13 +35,13 @@ describe("ProtectedRoute", () => {
});
renderGuard(
<ProtectedRoute>
<div>Secret</div>
<div data-testid="secret-page">Secret</div>
</ProtectedRoute>,
"/protected",
);
expect(screen.getByText(/Unsealing/i)).toBeInTheDocument();
expect(screen.queryByText("Secret")).not.toBeInTheDocument();
expect(screen.getByTestId("splash-screen")).toBeInTheDocument();
expect(screen.queryByTestId("secret-page")).not.toBeInTheDocument();
});
it("should redirect unauthenticated users to /login", () => {
@@ -52,12 +52,12 @@ describe("ProtectedRoute", () => {
});
renderGuard(
<ProtectedRoute>
<div>Secret</div>
<div data-testid="secret-page">Secret</div>
</ProtectedRoute>,
"/protected",
);
expect(screen.getByText("Login Page")).toBeInTheDocument();
expect(screen.queryByText("Secret")).not.toBeInTheDocument();
expect(screen.getByTestId("login-page")).toBeInTheDocument();
expect(screen.queryByTestId("secret-page")).not.toBeInTheDocument();
});
it("should render page for authenticated users", () => {
@@ -68,12 +68,12 @@ describe("ProtectedRoute", () => {
});
renderGuard(
<ProtectedRoute>
<div>Secret</div>
<div data-testid="secret-page">Secret</div>
</ProtectedRoute>,
"/protected",
);
expect(screen.getByText("Secret")).toBeInTheDocument();
expect(screen.getByTestId("secret-page")).toBeInTheDocument();
});
});
@@ -86,12 +86,12 @@ describe("PublicRoute", () => {
});
renderGuard(
<AutoRedirectRoute>
<div>Login Page</div>
<div data-testid="mock-login-page">Login Page</div>
</AutoRedirectRoute>,
"/public",
);
expect(screen.getByText(/Unsealing/i)).toBeInTheDocument();
expect(screen.queryByText("Login Page")).not.toBeInTheDocument();
expect(screen.getByTestId("splash-screen")).toBeInTheDocument();
expect(screen.queryByTestId("mock-login-page")).not.toBeInTheDocument();
});
it("should redirect authenticated users to /drawer", () => {
@@ -102,12 +102,12 @@ describe("PublicRoute", () => {
});
renderGuard(
<AutoRedirectRoute>
<div>Login Form</div>
<div data-testid="login-form">Login Form</div>
</AutoRedirectRoute>,
"/public",
);
expect(screen.getByText("Drawer Page")).toBeInTheDocument();
expect(screen.queryByText("Login Form")).not.toBeInTheDocument();
expect(screen.getByTestId("drawer-page")).toBeInTheDocument();
expect(screen.queryByTestId("login-form")).not.toBeInTheDocument();
});
it("should render page for unauthenticated users", () => {
@@ -118,10 +118,10 @@ describe("PublicRoute", () => {
});
renderGuard(
<AutoRedirectRoute>
<div>Login Form</div>
<div data-testid="login-form">Login Form</div>
</AutoRedirectRoute>,
"/public",
);
expect(screen.getByText("Login Form")).toBeInTheDocument();
expect(screen.getByTestId("login-form")).toBeInTheDocument();
});
});
+1 -1
View File
@@ -3,7 +3,7 @@ import Logo from "./Logo";
export default function SplashScreen() {
return (
<div className="fixed w-screen h-screen inset-0 flex flex-col items-center justify-center z-9999 before:absolute before:top-0 before:left-0 before:w-full before:h-full before:content-[''] before:opacity-[0.03] before:z-10 before:pointer-events-none before:bg-[url('assets/noise.gif')">
<div data-testid="splash-screen" className="fixed w-screen h-screen inset-0 flex flex-col items-center justify-center z-9999 before:absolute before:top-0 before:left-0 before:w-full before:h-full before:content-[''] before:opacity-[0.03] before:z-10 before:pointer-events-none before:bg-[url('assets/noise.gif')">
<div className="flex flex-col items-center gap-6 animate-pulse">
<Logo />
@@ -1,97 +1,95 @@
import { GearFineIcon } from "@phosphor-icons/react";
interface DrawerSectionProps {
id: string;
title: string;
count: number;
subtext: string;
isOpen: boolean;
onClick: () => void;
children: React.ReactNode;
icon: React.ReactNode;
id: string;
title: string;
count: number;
subtext: string;
isOpen: boolean;
onClick: () => void;
children: React.ReactNode;
icon: React.ReactNode;
}
export function DrawerSection({
id,
title,
count,
subtext,
isOpen,
onClick,
children,
icon,
id,
title,
count,
subtext,
isOpen,
onClick,
children,
icon,
}: DrawerSectionProps) {
return (
<div
id={id}
className={`join-item group flex flex-col transition-colors duration-3000 ease-in-out ${isOpen ? "bg-base-300/30" : ""}`}
>
<div
className={`bg-neutral/10 transition-all duration-1000 ease-in-out overflow-visible ${isOpen ? "max-h-125" : "max-h-0 pointer-events-none"}`}
>
return (
<div
className={`transition-opacity ease-in-out ${
isOpen
? "opacity-100 py-3 border-b border-base-content/5 duration-700 delay-500"
: "opacity-0 duration-100"
}`}
id={id}
className={`join-item group flex flex-col transition-colors duration-3000 ease-in-out ${isOpen ? "bg-base-300/30" : ""}`}
>
{children}
{count === 0 && (
<div className="text-sm text-center text-neutral">
This drawer remains silent...
<div
className={`bg-neutral/10 transition-all duration-1000 ease-in-out overflow-visible ${isOpen ? "max-h-125" : "max-h-0 pointer-events-none"}`}
>
<div
className={`transition-opacity ease-in-out ${isOpen
? "opacity-100 py-3 border-b border-base-content/5 duration-700 delay-500"
: "opacity-0 duration-100"
}`}
>
{children}
{count === 0 && (
<p data-testid={`empty-drawer-message-${id}`} className="text-center text-base-content/20 mt-4">
This drawer remains silent
</p>
)}
</div>
</div>
)}
</div>
</div>
<button
type="button"
onClick={onClick}
data-testid={`drawer-section-${id}`}
className="w-full relative p-[24px_28px] cursor-pointer flex items-center gap-5 transition-all duration-2000 ease-in-out outline-none focus-visible:ring-2 overflow-hidden focus-visible:ring-primary/50 border border-base-content/10 text-left bg-linear-to-r from-transparent to-base-100/40"
>
<div className="flex-1">
<div
className={`font-sans text-xs tracking-widester uppercase transition-colors duration-800 ${
isOpen
? "text-base-content"
: "text-base-content/40 group-hover:text-base-content/80"
}`}
>
{title}
</div>
<div className="font-sans text-xs text-base-content/20 mt-1">
<span className="font-mono text-xs md:text-base -mt-1 absolute text-primary/30">
{count}
</span>{" "}
<span className="ml-3">{subtext}</span>
</div>
<div className="absolute right-5 -translate-y-15 text-base-content/4">
{icon}
</div>
</div>
<button
type="button"
onClick={onClick}
data-testid={`drawer-section-${id}`}
className="w-full relative p-[24px_28px] cursor-pointer flex items-center gap-5 transition-all duration-2000 ease-in-out outline-none focus-visible:ring-2 overflow-hidden focus-visible:ring-primary/50 border border-base-content/10 text-left bg-linear-to-r from-transparent to-base-100/40"
>
<div className="flex-1">
<div
data-testid="drawer-section-title"
className={`font-sans text-xs tracking-widester uppercase transition-colors duration-800 ${isOpen
? "text-base-content"
: "text-base-content/40 group-hover:text-base-content/80"
}`}
>
{title}
</div>
<div className="font-sans text-xs text-base-content/20 mt-1">
<span className="font-mono text-xs md:text-base -mt-1 absolute text-primary/30">
{count}
</span>{" "}
<span className="ml-3">{subtext}</span>
</div>
<div className="absolute right-5 -translate-y-15 text-base-content/4">
{icon}
</div>
</div>
{id === "vault" ? (
<GearFineIcon
className={
"-mt-3 group-hover:animate-[spin_8s_ease-in-out_1] group-hover:text-neutral-content text-neutral"
}
weight={"duotone"}
size={30}
/>
) : (
<div
className={`w-8 h-1 rounded-sm transition-all duration-300 bg-neutral ${
isOpen
? "bg-primary/80! opacity-80 scale-110"
: "group-hover:bg-primary"
}`}
>
<div className="absolute -top-1 left-1.75 w-5 h-px bg-base-content/5" />
</div>
)}
</button>
</div>
);
{id === "vault" ? (
<GearFineIcon
className={
"-mt-3 group-hover:animate-[spin_8s_ease-in-out_1] group-hover:text-neutral-content text-neutral"
}
weight={"duotone"}
size={30}
/>
) : (
<div
className={`w-8 h-1 rounded-sm transition-all duration-300 bg-neutral ${isOpen
? "bg-primary/80! opacity-80 scale-110"
: "group-hover:bg-primary"
}`}
>
<div className="absolute -top-1 left-1.75 w-5 h-px bg-base-content/5" />
</div>
)}
</button>
</div>
);
}
@@ -12,7 +12,7 @@ export function PasskeyModal() {
className="text-primary mx-auto mb-8 animate-pulse"
weight="duotone"
/>
<h3 className="font-bold text-lg font-display text-primary">
<h3 data-testid="passkey-modal-title" className="font-bold text-lg font-display text-primary">
You've been away a while.
</h3>
<p className="py-4 font-sans">
@@ -123,7 +123,7 @@ export function EnvelopeReveal({
<span className={"text-neutral-content/60 font-xs font-display"}>
to
</span>
<h1 className="text-3xl font-bold text-base-content">{recipient}</h1>
<h1 data-testid="envelope-recipient" className="text-3xl font-bold text-base-content">{recipient}</h1>
<p className="text-base-content/60 font-display mt-8">{date}</p>
<img
src={stamp}
+1 -1
View File
@@ -24,7 +24,7 @@ export const LogModal = ({
{status === "WARN" && (
<WarningIcon className="text-warning" size={16} weight="duotone" />
)}
{message}
<span data-testid="log-modal-message">{message}</span>
{log && (
<>
<div className="divider text-primary-content text-xs uppercase tracking-widest">
+94 -94
View File
@@ -9,116 +9,116 @@ import Drawer from "./Drawer";
vi.mock("../hooks/useLetters");
vi.mock("../components/drawer/WelcomeLetterOverlay", () => ({
WelcomeLetterOverlay: ({ onComplete }: WelcomeLetterOverlayProps) => (
<div data-testid="welcome-letter-overlay">
<button
type="button"
data-testid="overlay-exit-button"
onClick={onComplete}
>
I'll see you
</button>
</div>
),
WelcomeLetterOverlay: ({ onComplete }: WelcomeLetterOverlayProps) => (
<div data-testid="welcome-letter-overlay">
<button
type="button"
data-testid="overlay-exit-button"
onClick={onComplete}
>
I'll see you
</button>
</div>
),
}));
describe("Drawer Page", () => {
beforeEach(() => {
// Setup authenticated state for the test
useAuthStore.setState({
user: mockUser,
accessToken: "fake-token",
isInitializing: false,
beforeEach(() => {
// Setup authenticated state for the test
useAuthStore.setState({
user: mockUser,
accessToken: "fake-token",
isInitializing: false,
});
vi.mocked(useLetters).mockReturnValue({
drafts: [],
kept: [],
sent: [],
vault: [],
loading: false,
isAuthRequired: false,
});
});
vi.mocked(useLetters).mockReturnValue({
drafts: [],
kept: [],
sent: [],
vault: [],
loading: false,
isAuthRequired: false,
});
});
it("renders the drawer sections and empty state message", () => {
render(
<MemoryRouter>
<Drawer />
</MemoryRouter>,
);
it("renders the cabinet sections and empty state message", () => {
render(
<MemoryRouter>
<Drawer />
</MemoryRouter>,
);
expect(screen.getByText(/Drafts/i)).toBeInTheDocument();
expect(screen.getAllByText(/Kept/i).length).toBeGreaterThanOrEqual(1);
expect(screen.getByText(/Vault/i)).toBeInTheDocument();
expect(screen.getByText(/This drawer remains silent/i)).toBeInTheDocument();
});
it("renders the loading state", () => {
vi.mocked(useLetters).mockReturnValue({
drafts: [],
kept: [],
sent: [],
vault: [],
loading: true,
isAuthRequired: false,
expect(screen.getByTestId("drawer-section-drafts")).toBeInTheDocument();
expect(screen.getAllByTestId("drawer-section-title").length).toBeGreaterThanOrEqual(1);
expect(screen.getByTestId("drawer-section-vault")).toBeInTheDocument();
expect(screen.getByTestId("empty-drawer-message-drafts")).toBeInTheDocument();
});
render(
<MemoryRouter>
<Drawer />
</MemoryRouter>,
);
it("renders the loading state", () => {
vi.mocked(useLetters).mockReturnValue({
drafts: [],
kept: [],
sent: [],
vault: [],
loading: true,
isAuthRequired: false,
});
expect(screen.getByText(/Opening your cabinet/i)).toBeInTheDocument();
});
render(
<MemoryRouter>
<Drawer />
</MemoryRouter>,
);
it("renders the authentication required modal when api requires auth", () => {
vi.mocked(useLetters).mockReturnValue({
drafts: [],
kept: [],
sent: [],
vault: [],
loading: false,
isAuthRequired: true,
expect(screen.getByTestId("drawer-loading-state")).toBeInTheDocument();
});
render(
<MemoryRouter>
<Drawer />
</MemoryRouter>,
);
it("renders the authentication required modal when api requires auth", () => {
vi.mocked(useLetters).mockReturnValue({
drafts: [],
kept: [],
sent: [],
vault: [],
loading: false,
isAuthRequired: true,
});
expect(screen.getByText(/You've been away a while./i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument();
});
render(
<MemoryRouter>
<Drawer />
</MemoryRouter>,
);
it("renders the welcome letter when firstTime state is present", () => {
render(
<MemoryRouter
initialEntries={[{ pathname: "/drawer", state: { firstTime: true } }]}
>
<Drawer />
</MemoryRouter>,
);
expect(screen.getByTestId("passkey-modal-title")).toBeInTheDocument();
expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument();
});
expect(screen.getByTestId("welcome-letter-overlay")).toBeInTheDocument();
});
it("renders the welcome letter when firstTime state is present", () => {
render(
<MemoryRouter
initialEntries={[{ pathname: "/drawer", state: { firstTime: true } }]}
>
<Drawer />
</MemoryRouter>,
);
it("renders the drawer content when the letter is closed", () => {
render(
<MemoryRouter
initialEntries={[{ pathname: "/drawer", state: { firstTime: true } }]}
>
<Drawer />
</MemoryRouter>,
);
expect(screen.getByTestId("welcome-letter-overlay")).toBeInTheDocument();
});
const completeButton = screen.getByTestId("overlay-exit-button");
fireEvent.click(completeButton);
it("renders the drawer content when the letter is closed", () => {
render(
<MemoryRouter
initialEntries={[{ pathname: "/drawer", state: { firstTime: true } }]}
>
<Drawer />
</MemoryRouter>,
);
expect(
screen.queryByTestId("welcome-letter-overlay"),
).not.toBeInTheDocument();
});
const completeButton = screen.getByTestId("overlay-exit-button");
fireEvent.click(completeButton);
expect(
screen.queryByTestId("welcome-letter-overlay"),
).not.toBeInTheDocument();
});
});
+179 -179
View File
@@ -1,9 +1,9 @@
import {
ArchiveIcon,
FeatherIcon,
FileDashedIcon,
PaperPlaneTiltIcon,
VaultIcon,
ArchiveIcon,
FeatherIcon,
FileDashedIcon,
PaperPlaneTiltIcon,
VaultIcon,
} from "@phosphor-icons/react";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
@@ -17,188 +17,188 @@ import { PATHS } from "../config/routes";
import { useAuth } from "../hooks/useAuth";
import { useLetters } from "../hooks/useLetters";
import {
formatRelativeDate,
formatRelativeDateWithoutTime,
formatRelativeDate,
formatRelativeDateWithoutTime,
} from "../utils/dateFormat.ts";
export default function Drawer() {
const { user, logout } = useAuth();
const { user, logout } = useAuth();
const [openSection, setOpenSection] = useState<string | null>(null);
const navigate = useNavigate();
const location = useLocation();
const [showWelcomeLetter, setShowWelcomeLetter] = useState(
!!location.state?.firstTime,
);
const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters();
const [openSection, setOpenSection] = useState<string | null>(null);
const navigate = useNavigate();
const location = useLocation();
const [showWelcomeLetter, setShowWelcomeLetter] = useState(
!!location.state?.firstTime,
);
const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters();
if (!user) return null;
if (!user) return null;
const toggleSection = (id: string) =>
setOpenSection(openSection === id ? null : id);
const toggleSection = (id: string) =>
setOpenSection(openSection === id ? null : id);
return (
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
<div className="fixed inset-0 bg-vig pointer-events-none z-0" />
return (
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
<div className="fixed inset-0 bg-vig pointer-events-none z-0" />
{showWelcomeLetter && (
<WelcomeLetterOverlay
userName={user.full_name}
onComplete={() => {
setShowWelcomeLetter(false);
navigate(location.pathname, { replace: true, state: {} });
}}
/>
)}
{showWelcomeLetter && (
<WelcomeLetterOverlay
userName={user.full_name}
onComplete={() => {
setShowWelcomeLetter(false);
navigate(location.pathname, { replace: true, state: {} });
}}
/>
)}
{isAuthRequired && <PasskeyModal />}
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-500">
<Logo type="logo" />
<div className="font-sans text-xs tracking-widester uppercase text-base-content/40 mt-2">
Personal Archive
{isAuthRequired && <PasskeyModal />}
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-500">
<Logo />
<div className="font-sans text-xs tracking-widester uppercase text-base-content/40 mt-2">
Personal Archive
</div>
<div className="mt-6 font-sans text-sm text-base-content flex items-center justify-center gap-2 opacity-60 hover:opacity-100 transition-opacity">
Welcome Back{" "}
<span className="font-semibold text-primary">{user.full_name}</span>
<button
type="button"
onClick={logout}
className="ml-3 cursor-pointer underline underline-offset-4 text-xs hover:text-primary transition-colors"
>
Sign Out
</button>
</div>
</header>
<div className="join join-vertical w-full max-w-120 bg-base-200 border border-base-content/10 shadow-2xl z-10 rounded-sm duration-500 delay-200 min-h-64 flex flex-col">
{loading ? (
<div className="flex-1 flex flex-col items-center justify-center p-12 gap-4">
<span className="loading loading-ring loading-lg text-primary opacity-20"></span>
<span data-testid="drawer-loading-state" className="text-xxs uppercase tracking-widester font-sans text-base-content/20 animate-pulse">
Opening your cabinet...
</span>
</div>
) : (
<>
<DrawerSection
id="drafts"
title="Drafts"
count={drafts.length}
subtext="unfinished whispers"
isOpen={openSection === "drafts"}
onClick={() => toggleSection("drafts")}
icon={<FileDashedIcon weight="thin" size={128} />}
>
{drafts.map((draft) => (
<LetterItem
id={draft.public_id}
status={draft.status}
key={draft.public_id}
preview={draft.metadata?.recipient || "Untitled Draft"}
timestamp={formatRelativeDate(draft.updated_at)}
/>
))}
</DrawerSection>
<DrawerSection
id="kept"
title="Kept"
count={kept.length}
subtext="private letters"
isOpen={openSection === "kept"}
onClick={() => toggleSection("kept")}
icon={<ArchiveIcon weight="thin" size={128} />}
>
{kept.map((letter) => (
<LetterItem
id={letter.public_id}
status={letter.status}
key={letter.public_id}
preview={letter.metadata?.recipient || "Someone dear..."}
timestamp={formatRelativeDate(letter.updated_at)}
/>
))}
</DrawerSection>
<DrawerSection
id="sent"
title="Sent"
count={sent.length}
subtext="shared truths"
isOpen={openSection === "sent"}
onClick={() => toggleSection("sent")}
icon={<PaperPlaneTiltIcon weight="thin" size={128} />}
>
{sent.map((letter) => (
<LetterItem
key={letter.public_id}
status={letter.status}
id={letter.public_id}
preview={letter.metadata?.recipient || "Someone dear..."}
timestamp={formatRelativeDate(letter.updated_at)}
/>
))}
</DrawerSection>
<DrawerSection
id="vault"
title="Vault"
count={vault.length}
subtext="things locked—not lost—in time"
isOpen={openSection === "vault"}
onClick={() => toggleSection("vault")}
icon={<VaultIcon weight="thin" size={128} />}
>
{vault.map((letter) => (
<LetterItem
key={letter.public_id}
status={letter.status}
id={letter.public_id}
preview={letter.metadata?.recipient || "Future Self"}
timestamp={formatRelativeDate(letter.updated_at)}
unlock_at={formatRelativeDateWithoutTime(
letter.unlock_at || "",
)}
isLocked={letter.unlock_at > new Date().toISOString()}
/>
))}
</DrawerSection>
</>
)}
</div>
<button
type="button"
id="write-letter-btn"
data-testid="write-letter-btn"
className="group mt-15 z-10 bg-transparent border border-dashed border-base-content/10 px-8 py-4 text-base-content/40 italic cursor-pointer transition-all hover:border-primary/40 hover:text-base-content/60 hover:bg-primary/5 hover:-translate-y-0.5 flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-primary/50 duration-500"
onClick={() => navigate(PATHS.write(""))}
>
<FeatherIcon
size={18}
weight="duotone"
className="text-primary/30 transition-all duration-300 group-hover:text-primary"
/>
Write something{" "}
<span className="relative inline-flex">
<span className="transition-opacity duration-500 opacity-80 group-hover:opacity-0">
. . . . . .
</span>
<span className="absolute inset-0 text-primary transition-opacity duration-300 opacity-0 group-hover:opacity-100">
unsaid
</span>
</span>
</button>
<footer className="mt-25 font-sans text-[0.6rem] tracking-widester uppercase text-base-content/10 z-10">
For your unsaid.
</footer>
{!showWelcomeLetter && (
<div className="absolute bottom-0 z-50 font-sans">
<Saajan
message={`Good to see you again, ${user.full_name}.\nWhat's on your mind today?`}
position="top"
/>
</div>
)}
</div>
<div className="mt-6 font-sans text-sm text-base-content flex items-center justify-center gap-2 opacity-60 hover:opacity-100 transition-opacity">
Welcome Back{" "}
<span className="font-semibold text-primary">{user.full_name}</span>
<button
type="button"
onClick={logout}
className="ml-3 cursor-pointer underline underline-offset-4 text-xs hover:text-primary transition-colors"
>
Sign Out
</button>
</div>
</header>
<div className="join join-vertical w-full max-w-120 bg-base-200 border border-base-content/10 shadow-2xl z-10 rounded-sm duration-500 delay-200 min-h-64 flex flex-col">
{loading ? (
<div className="flex-1 flex flex-col items-center justify-center p-12 gap-4">
<span className="loading loading-ring loading-lg text-primary opacity-20"></span>
<span className="text-xxs uppercase tracking-widester font-sans text-base-content/20 animate-pulse">
Opening your cabinet...
</span>
</div>
) : (
<>
<DrawerSection
id="drafts"
title="Drafts"
count={drafts.length}
subtext="unfinished whispers"
isOpen={openSection === "drafts"}
onClick={() => toggleSection("drafts")}
icon={<FileDashedIcon weight="thin" size={128} />}
>
{drafts.map((draft) => (
<LetterItem
id={draft.public_id}
status={draft.status}
key={draft.public_id}
preview={draft.metadata?.recipient || "Untitled Draft"}
timestamp={formatRelativeDate(draft.updated_at)}
/>
))}
</DrawerSection>
<DrawerSection
id="kept"
title="Kept"
count={kept.length}
subtext="private letters"
isOpen={openSection === "kept"}
onClick={() => toggleSection("kept")}
icon={<ArchiveIcon weight="thin" size={128} />}
>
{kept.map((letter) => (
<LetterItem
id={letter.public_id}
status={letter.status}
key={letter.public_id}
preview={letter.metadata?.recipient || "Someone dear..."}
timestamp={formatRelativeDate(letter.updated_at)}
/>
))}
</DrawerSection>
<DrawerSection
id="sent"
title="Sent"
count={sent.length}
subtext="shared truths"
isOpen={openSection === "sent"}
onClick={() => toggleSection("sent")}
icon={<PaperPlaneTiltIcon weight="thin" size={128} />}
>
{sent.map((letter) => (
<LetterItem
key={letter.public_id}
status={letter.status}
id={letter.public_id}
preview={letter.metadata?.recipient || "Someone dear..."}
timestamp={formatRelativeDate(letter.updated_at)}
/>
))}
</DrawerSection>
<DrawerSection
id="vault"
title="Vault"
count={vault.length}
subtext="things locked—not lost—in time"
isOpen={openSection === "vault"}
onClick={() => toggleSection("vault")}
icon={<VaultIcon weight="thin" size={128} />}
>
{vault.map((letter) => (
<LetterItem
key={letter.public_id}
status={letter.status}
id={letter.public_id}
preview={letter.metadata?.recipient || "Future Self"}
timestamp={formatRelativeDate(letter.updated_at)}
unlock_at={formatRelativeDateWithoutTime(
letter.unlock_at || "",
)}
isLocked={letter.unlock_at > new Date().toISOString()}
/>
))}
</DrawerSection>
</>
)}
</div>
<button
type="button"
id="write-letter-btn"
data-testid="write-letter-btn"
className="group mt-15 z-10 bg-transparent border border-dashed border-base-content/10 px-8 py-4 text-base-content/40 italic cursor-pointer transition-all hover:border-primary/40 hover:text-base-content/60 hover:bg-primary/5 hover:-translate-y-0.5 flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-primary/50 duration-500"
onClick={() => navigate(PATHS.write(""))}
>
<FeatherIcon
size={18}
weight="duotone"
className="text-primary/30 transition-all duration-300 group-hover:text-primary"
/>
Write something{" "}
<span className="relative inline-flex">
<span className="transition-opacity duration-500 opacity-80 group-hover:opacity-0">
. . . . . .
</span>
<span className="absolute inset-0 text-primary transition-opacity duration-300 opacity-0 group-hover:opacity-100">
unsaid
</span>
</span>
</button>
<footer className="mt-25 font-sans text-xs tracking-widester uppercase text-base-content/10 z-10">
For your unsaid.
</footer>
{!showWelcomeLetter && (
<div className="absolute bottom-0 z-50 font-sans">
<Saajan
message={`Good to see you again, ${user.full_name}.\nWhat's on your mind today?`}
position="top"
/>
</div>
)}
</div>
);
);
}
+5 -13
View File
@@ -1,4 +1,4 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-library/react";
import { HttpResponse, http } from "msw";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -79,9 +79,7 @@ describe("Editor Page", () => {
);
// Wait for initial load to complete
await waitFor(() => {
expect(screen.queryByText(/Opening your draft/i)).not.toBeInTheDocument();
});
await waitForElementToBeRemoved(() => screen.queryByTestId("opening-draft-overlay"));
const canvas = screen.getByTestId("canvas");
expect(canvas.getAttribute("data-readonly")).toBe("false");
@@ -107,9 +105,7 @@ describe("Editor Page", () => {
fireEvent.click(confirmVaultBtn);
// Wait for save to complete and check readOnly
await waitFor(() => {
expect(screen.getByText(/Your letter is saved/i)).toBeInTheDocument();
});
expect(await screen.findByTestId("save-success-toast")).toBeInTheDocument();
expect(canvas.getAttribute("data-readonly")).toBe("true");
expect(screen.getByLabelText(/recipient/i)).toBeDisabled();
@@ -140,9 +136,7 @@ describe("Editor Page", () => {
</MemoryRouter>,
);
await waitFor(() => {
expect(screen.queryByText(/Opening your draft/i)).not.toBeInTheDocument();
});
await waitForElementToBeRemoved(() => screen.queryByTestId("opening-draft-overlay"));
const canvas = screen.getByTestId("canvas");
@@ -156,9 +150,7 @@ describe("Editor Page", () => {
if (!secondarySealBtn) throw new Error("Secondary seal button not found");
fireEvent.click(secondarySealBtn);
await waitFor(() => {
expect(screen.getByText(/Your letter is saved/i)).toBeInTheDocument();
});
expect(await screen.findByTestId("save-success-toast")).toBeInTheDocument();
expect(canvas.getAttribute("data-readonly")).toBe("true");
expect(screen.getByLabelText(/recipient/i)).toBeDisabled();
+5 -4
View File
@@ -31,7 +31,7 @@ describe("Login Page", () => {
await userEvent.type(screen.getByLabelText(/password/i), "password123");
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
expect(await screen.findByText(/technical issues/i)).toBeInTheDocument();
expect(await screen.findByTestId("login-error-message")).toHaveTextContent(/technical issues/i);
});
it.each([
@@ -73,8 +73,8 @@ describe("Login Page", () => {
>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/drawer" element={<div>Drawer</div>} />
<Route path="/read/:publicId" element={<div>Reader</div>} />
<Route path="/drawer" element={<div data-testid="drawer-page">Drawer</div>} />
<Route path="/read/:publicId" element={<div data-testid="reader-page">Reader</div>} />
</Routes>
</MemoryRouter>,
);
@@ -83,6 +83,7 @@ describe("Login Page", () => {
await userEvent.type(screen.getByLabelText(/password/i), "password123");
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
expect(await screen.findByText(nextRoute)).toBeInTheDocument();
const expectedTestId = nextRoute.toLowerCase() === "drawer" ? "drawer-page" : "reader-page";
expect(await screen.findByTestId(expectedTestId)).toBeInTheDocument();
});
});
+1 -1
View File
@@ -89,7 +89,7 @@ export default function Login() {
{apiError && (
<div className="alert alert-error text-xs py-2 rounded-md">
<span>{apiError}</span>
<span data-testid="login-error-message">{apiError}</span>
</div>
)}
+4 -6
View File
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import { HttpResponse, http } from "msw";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -76,9 +76,7 @@ describe("Reader Page", () => {
</Routes>
</MemoryRouter>,
);
await waitFor(() => {
expect(screen.getByText(/Guest/i)).toBeInTheDocument();
});
expect(await screen.findByTestId("envelope-recipient")).toHaveTextContent(/Guest/i);
});
it("should display an error message if the server request fails", async () => {
@@ -100,8 +98,8 @@ describe("Reader Page", () => {
);
expect(
await screen.findByText(/Failed to load letter/i),
).toBeInTheDocument();
await screen.findByTestId("log-modal-message"),
).toHaveTextContent(/Failed to load letter/i);
});
it("should navigate to the login page with redirect url when the letter has no sharing key and the user is not logged in", async () => {