diff --git a/frontend/src/components/RouteGuards.test.tsx b/frontend/src/components/RouteGuards.test.tsx index 5cb34a3..7eea6d4 100644 --- a/frontend/src/components/RouteGuards.test.tsx +++ b/frontend/src/components/RouteGuards.test.tsx @@ -9,8 +9,8 @@ function renderGuard(ui: React.ReactNode, mountPath: "/protected" | "/public") { return render( - Login Page} /> - Drawer Page} /> + Login Page} /> + Drawer Page} /> @@ -35,13 +35,13 @@ describe("ProtectedRoute", () => { }); renderGuard( -
Secret
+
Secret
, "/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( -
Secret
+
Secret
, "/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( -
Secret
+
Secret
, "/protected", ); - expect(screen.getByText("Secret")).toBeInTheDocument(); + expect(screen.getByTestId("secret-page")).toBeInTheDocument(); }); }); @@ -86,12 +86,12 @@ describe("PublicRoute", () => { }); renderGuard( -
Login Page
+
Login Page
, "/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( -
Login Form
+
Login Form
, "/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( -
Login Form
+
Login Form
, "/public", ); - expect(screen.getByText("Login Form")).toBeInTheDocument(); + expect(screen.getByTestId("login-form")).toBeInTheDocument(); }); }); diff --git a/frontend/src/components/SplashScreen.tsx b/frontend/src/components/SplashScreen.tsx index ca6ce1a..7c4297e 100644 --- a/frontend/src/components/SplashScreen.tsx +++ b/frontend/src/components/SplashScreen.tsx @@ -3,7 +3,7 @@ import Logo from "./Logo"; export default function SplashScreen() { return ( -
+
diff --git a/frontend/src/components/drawer/DrawerSection.tsx b/frontend/src/components/drawer/DrawerSection.tsx index cdea2c1..7fe6ff0 100644 --- a/frontend/src/components/drawer/DrawerSection.tsx +++ b/frontend/src/components/drawer/DrawerSection.tsx @@ -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 ( -
-
+ return (
- {children} - {count === 0 && ( -
- This drawer remains silent... +
+
+ {children} + {count === 0 && ( +

+ This drawer remains silent +

+ )} +
- )} -
-
- -
- ); + {id === "vault" ? ( + + ) : ( +
+
+
+ )} + +
+ ); } diff --git a/frontend/src/components/drawer/PasskeyModal.tsx b/frontend/src/components/drawer/PasskeyModal.tsx index bf1b5ad..04ba0ed 100644 --- a/frontend/src/components/drawer/PasskeyModal.tsx +++ b/frontend/src/components/drawer/PasskeyModal.tsx @@ -12,7 +12,7 @@ export function PasskeyModal() { className="text-primary mx-auto mb-8 animate-pulse" weight="duotone" /> -

+

You've been away a while.

diff --git a/frontend/src/components/reader/EnvelopeReveal.tsx b/frontend/src/components/reader/EnvelopeReveal.tsx index c76cf32..a830307 100644 --- a/frontend/src/components/reader/EnvelopeReveal.tsx +++ b/frontend/src/components/reader/EnvelopeReveal.tsx @@ -123,7 +123,7 @@ export function EnvelopeReveal({ to -

{recipient}

+

{recipient}

{date}

)} - {message} + {message} {log && ( <>
diff --git a/frontend/src/pages/Drawer.test.tsx b/frontend/src/pages/Drawer.test.tsx index b79aed3..ae4ddd6 100644 --- a/frontend/src/pages/Drawer.test.tsx +++ b/frontend/src/pages/Drawer.test.tsx @@ -9,116 +9,116 @@ import Drawer from "./Drawer"; vi.mock("../hooks/useLetters"); vi.mock("../components/drawer/WelcomeLetterOverlay", () => ({ - WelcomeLetterOverlay: ({ onComplete }: WelcomeLetterOverlayProps) => ( -
- -
- ), + WelcomeLetterOverlay: ({ onComplete }: WelcomeLetterOverlayProps) => ( +
+ +
+ ), })); 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( + + + , + ); - it("renders the cabinet sections and empty state message", () => { - render( - - - , - ); - - 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( - - - , - ); + 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( + + + , + ); - 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( - - - , - ); + 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( + + + , + ); - it("renders the welcome letter when firstTime state is present", () => { - render( - - - , - ); + 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( + + + , + ); - it("renders the drawer content when the letter is closed", () => { - render( - - - , - ); + 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( + + + , + ); - 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(); + }); }); diff --git a/frontend/src/pages/Drawer.tsx b/frontend/src/pages/Drawer.tsx index 284d626..425956a 100644 --- a/frontend/src/pages/Drawer.tsx +++ b/frontend/src/pages/Drawer.tsx @@ -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(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(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 ( -
-
+ return ( +
+
- {showWelcomeLetter && ( - { - setShowWelcomeLetter(false); - navigate(location.pathname, { replace: true, state: {} }); - }} - /> - )} + {showWelcomeLetter && ( + { + setShowWelcomeLetter(false); + navigate(location.pathname, { replace: true, state: {} }); + }} + /> + )} - {isAuthRequired && } -
- -
- Personal Archive + {isAuthRequired && } +
+ +
+ Personal Archive +
+
+ Welcome Back{" "} + {user.full_name} + +
+
+ +
+ {loading ? ( +
+ + + Opening your cabinet... + +
+ ) : ( + <> + toggleSection("drafts")} + icon={} + > + {drafts.map((draft) => ( + + ))} + + + toggleSection("kept")} + icon={} + > + {kept.map((letter) => ( + + ))} + + toggleSection("sent")} + icon={} + > + {sent.map((letter) => ( + + ))} + + toggleSection("vault")} + icon={} + > + {vault.map((letter) => ( + new Date().toISOString()} + /> + ))} + + + )} +
+ + + +
+ For your unsaid. +
+ {!showWelcomeLetter && ( +
+ +
+ )}
-
- Welcome Back{" "} - {user.full_name} - -
-
- -
- {loading ? ( -
- - - Opening your cabinet... - -
- ) : ( - <> - toggleSection("drafts")} - icon={} - > - {drafts.map((draft) => ( - - ))} - - - toggleSection("kept")} - icon={} - > - {kept.map((letter) => ( - - ))} - - toggleSection("sent")} - icon={} - > - {sent.map((letter) => ( - - ))} - - toggleSection("vault")} - icon={} - > - {vault.map((letter) => ( - new Date().toISOString()} - /> - ))} - - - )} -
- - - -
- For your unsaid. -
- {!showWelcomeLetter && ( -
- -
- )} -
- ); + ); } diff --git a/frontend/src/pages/Editor.test.tsx b/frontend/src/pages/Editor.test.tsx index d1e63c4..6b32945 100644 --- a/frontend/src/pages/Editor.test.tsx +++ b/frontend/src/pages/Editor.test.tsx @@ -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", () => { , ); - 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(); diff --git a/frontend/src/pages/Login.test.tsx b/frontend/src/pages/Login.test.tsx index c6f4af3..7f58b75 100644 --- a/frontend/src/pages/Login.test.tsx +++ b/frontend/src/pages/Login.test.tsx @@ -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", () => { > } /> - Drawer
} /> - Reader
} /> + Drawer
} /> + Reader
} /> , ); @@ -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(); }); }); diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index c5de9c9..4ed7089 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -89,7 +89,7 @@ export default function Login() { {apiError && (
- {apiError} + {apiError}
)} diff --git a/frontend/src/pages/Reader.test.tsx b/frontend/src/pages/Reader.test.tsx index 8078e1b..50164b1 100644 --- a/frontend/src/pages/Reader.test.tsx +++ b/frontend/src/pages/Reader.test.tsx @@ -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", () => { , ); - 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 () => {