From 548b34fb5127c49d929abd782ef5c27ea716e9d3 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 6 May 2026 22:48:06 +0530 Subject: [PATCH] refactor: migrate e2e tests and components to use data-testid attributes --- frontend/e2e/letter.spec.ts | 76 ++--- frontend/e2e/utils/auth.ts | 53 +--- frontend/e2e/utils/envelope.ts | 38 +++ .../drawer/WelcomeLetterOverlay.tsx | 123 ++++---- .../src/components/editor/PostSealModal.tsx | 2 + frontend/src/components/editor/ToolBar.tsx | 3 + .../src/components/login/WelcomeModal.tsx | 1 + .../src/components/reader/EnvelopeReveal.tsx | 3 + frontend/src/components/ui/FormField.tsx | 3 + frontend/src/pages/Activate.tsx | 1 + frontend/src/pages/Drawer.test.tsx | 189 ++++++------ frontend/src/pages/Drawer.tsx | 1 + frontend/src/pages/Editor.tsx | 1 + frontend/src/pages/Login.tsx | 3 + frontend/src/pages/Reader.tsx | 2 + frontend/src/pages/Register.tsx | 283 +++++++++--------- 16 files changed, 395 insertions(+), 387 deletions(-) create mode 100644 frontend/e2e/utils/envelope.ts diff --git a/frontend/e2e/letter.spec.ts b/frontend/e2e/letter.spec.ts index 3b90fce..6ef1a3a 100644 --- a/frontend/e2e/letter.spec.ts +++ b/frontend/e2e/letter.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from "@playwright/test"; import pino from "pino"; import { AuthHelper } from "./utils/auth"; +import { revealEnvelope } from "./utils/envelope"; const logger = pino({ transport: { @@ -22,12 +23,12 @@ test.describe("Letter Drafting (Real Backend)", () => { await AuthHelper.registerAndLogin(page, email, name, password); logger.info(">> [Draft] Navigating to Editor via UI..."); - await page.getByRole("button", { name: /write something/i }).click(); + await page.getByTestId("write-letter-btn").click(); logger.info(`>> [Draft] Current URL after click: ${page.url()}`); // Wait for the recipient input to be present in the DOM - const recipientInput = page.locator("#recipient"); + const recipientInput = page.getByTestId("recipient-input"); await recipientInput.waitFor({ state: "visible", timeout: 20000 }); const recipientName = "Dear Friend"; @@ -46,7 +47,7 @@ test.describe("Letter Drafting (Real Backend)", () => { await page.keyboard.press("Enter"); await page.keyboard.type("It should persist."); logger.info(">> [Draft] Clicking Draft..."); - await page.getByRole("button", { name: /draft/i }).click(); + await page.getByTestId("draft-btn").click(); // Verify Success Modal/Alert await expect(page.getByText(/your letter is saved/i)).toBeVisible(); @@ -70,7 +71,7 @@ test.describe("Letter Drafting (Real Backend)", () => { }); // Check recipient - await expect(page.locator("#recipient")).toHaveValue(recipientName); + await expect(page.getByTestId("recipient-input")).toHaveValue(recipientName); // Check canvas content // We wait for the content to appear in the textarea. @@ -92,9 +93,9 @@ test.describe("Letter Drafting (Real Backend)", () => { await AuthHelper.registerAndLogin(page, email, name, password); logger.info(">> [Seal] Navigating to Editor via UI..."); - await page.locator("#write-letter-btn").click(); + await page.getByTestId("write-letter-btn").click(); - const recipientInput = page.locator("#recipient"); + const recipientInput = page.getByTestId("recipient-input"); await recipientInput.waitFor({ state: "visible", timeout: 10000 }); await recipientInput.fill("A Secret Guest"); @@ -104,14 +105,8 @@ test.describe("Letter Drafting (Real Backend)", () => { // Click Seal (open menu, then confirm) logger.info(">> [Seal] Clicking Seal..."); - await page - .getByRole("button", { name: /seal/i }) - .filter({ visible: true }) - .click(); - await page - .getByRole("button", { name: /seal/i }) - .filter({ visible: true }) - .click(); + await page.getByTestId("seal-trigger-btn").click(); + await page.getByTestId("seal-confirm-btn").click(); // Should show sealed confirmation modal logger.info(">> [Seal] Verifying sealed modal..."); @@ -120,7 +115,7 @@ test.describe("Letter Drafting (Real Backend)", () => { }); // Navigate to Reader via "View letter" - await page.getByRole("button", { name: /view letter/i }).click(); + await page.getByTestId("view-letter-btn").click(); // Should be on Reader URL await expect(page).toHaveURL(/\/read\/[a-f0-9-]{36}$/, { timeout: 15000 }); @@ -129,18 +124,13 @@ test.describe("Letter Drafting (Real Backend)", () => { await expect(page.getByText(/breaking the seal/i)).toBeHidden({ timeout: 10000, }); - // Flip the envelope to show the seal - await page.locator("#env-front").click(); - await page.waitForTimeout(2500); // Wait for flip transition - - await page.getByAltText("Seal").click(); - await page.waitForTimeout(1500); - await page.locator("#letter").click({ position: { x: 30, y: 15 } }); - await expect(page.locator("#letter")).toBeHidden({ timeout: 20000 }); + // Flip the envelope to show the seal and reveal the letter + await revealEnvelope(page); + await expect(page.getByTestId("envelope-letter")).toBeHidden({ timeout: 20000 }); // Share on demand logger.info(">> [Seal] Clicking Share button in Reader..."); - await page.locator("#share-letter-btn").click(); + await page.getByTestId("share-letter-btn").click(); // Verify share modal with a valid link await expect(page.getByText(/send this letter/i)).toBeVisible(); @@ -151,6 +141,7 @@ test.describe("Letter Drafting (Real Backend)", () => { logger.info(`>> [Seal] Sharing link: ${linkValue}`); await expect(page.getByRole("button", { name: /copy/i })).toBeVisible(); + // Assuming Close button in ShareModal might need a testid too, but for now let's use text if unique or add testid await page.getByRole("button", { name: /close/i }).click(); await expect(page.getByText(/send this letter/i)).toBeHidden(); }); @@ -167,9 +158,9 @@ test.describe("Letter Drafting (Real Backend)", () => { await AuthHelper.registerAndLogin(page, email, name, password); logger.info(">> [Drawer] Creating and sealing a letter..."); - await page.getByRole("button", { name: /write something/i }).click(); + await page.getByTestId("write-letter-btn").click(); - const recipientInput = page.locator("#recipient"); + const recipientInput = page.getByTestId("recipient-input"); await recipientInput.waitFor({ state: "visible" }); await recipientInput.fill(recipientName); @@ -178,20 +169,14 @@ test.describe("Letter Drafting (Real Backend)", () => { await canvasInput.fill(letterContent); // Click Seal (open menu, then confirm) - await page - .getByRole("button", { name: /seal/i }) - .filter({ visible: true }) - .click(); - await page - .getByRole("button", { name: /seal/i }) - .filter({ visible: true }) - .click(); + await page.getByTestId("seal-trigger-btn").click(); + await page.getByTestId("seal-confirm-btn").click(); // Sealed modal should appear — click "Keep it" to go to Drawer await expect(page.getByText(/your letter is sealed/i)).toBeVisible({ timeout: 10000, }); - await page.getByRole("button", { name: /keep it to myself/i }).click(); + await page.getByTestId("keep-it-btn").click(); // Open "Kept" section - search for the section with id='kept' and click its toggle button logger.info(">> [Drawer] Opening Kept section..."); @@ -213,24 +198,9 @@ test.describe("Letter Drafting (Real Backend)", () => { await expect(page.getByText(/breaking the seal/i)).toBeHidden({ timeout: 10000, }); - // Check recipient on the front of the envelope - await expect(page.getByText(new RegExp(recipientName, "i"))).toBeVisible(); - - // Flip the envelope to the back - await page.getByText(new RegExp(recipientName, "i")).click(); - // Wait for flip transition (2s) - await page.waitForTimeout(2500); - - // Reveal the letter: click seal then click letter - await page.getByAltText("Seal").click(); - // Wait for flap transition - await page.waitForTimeout(1500); - - // Click the letter to pull it out - await page.locator("#letter").click({ position: { x: 30, y: 15 } }); - - // Wait for reveal transition - await expect(page.locator("#letter")).toBeHidden({ timeout: 20000 }); + // Flip the envelope and reveal the letter + await revealEnvelope(page); + await expect(page.getByTestId("envelope-letter")).toBeHidden({ timeout: 20000 }); // Also check if we are redirected to the Reader if we manually go to the Editor URL const readerUrl = page.url(); diff --git a/frontend/e2e/utils/auth.ts b/frontend/e2e/utils/auth.ts index e0b255e..66b86e8 100644 --- a/frontend/e2e/utils/auth.ts +++ b/frontend/e2e/utils/auth.ts @@ -1,6 +1,7 @@ import { expect, type Page } from "@playwright/test"; import pino from "pino"; import { MailpitHelper } from "./mailpit"; +import { handleWelcomeLetter } from "./envelope"; const logger = pino({ transport: { @@ -23,11 +24,11 @@ async function registerAndLogin( // Register the User logger.info(`[Auth] Registering user: ${email}`); await page.goto("/onboard"); - await page.getByLabel(/pen name/i).fill(fullName); - await page.getByLabel("Email", { exact: true }).fill(email); - await page.getByLabel("Password", { exact: true }).fill(password); - await page.getByLabel(/confirm password/i).fill(password); - await page.getByRole("button", { name: /^register$/i }).click(); + await page.getByTestId("pen-name-input").fill(fullName); + await page.getByTestId("email-input").fill(email); + await page.getByTestId("password-input").fill(password); + await page.getByTestId("confirm-password-input").fill(password); + await page.getByTestId("register-submit-btn").click(); await expect(page).toHaveURL(/\/verify-email/); @@ -38,50 +39,22 @@ async function registerAndLogin( await page.goto(activationLink); await expect(page.getByText(/account activated/i)).toBeVisible(); - await page.getByRole("button", { name: /start writing/i }).click(); + await page.getByTestId("start-writing-btn").click(); // Dismiss the Welcom Modal and Perform Login logger.info(`[Auth] Logging in...`); await expect(page).toHaveURL(/\/login/); - const welcomeButton = page.getByRole("button", { name: /I'll remember/i }); - await welcomeButton.waitFor({ state: "visible", timeout: 10000 }); - await welcomeButton.click(); - await expect(welcomeButton).toBeHidden(); + await page.getByTestId("welcome-dismiss-btn").click(); + await expect(page.getByTestId("welcome-dismiss-btn")).toBeHidden(); - await page.getByLabel("Email", { exact: true }).fill(email); - await page.getByLabel("Password", { exact: true }).fill(password); - await page.getByRole("button", { name: /sign in/i }).click(); + await page.getByTestId("email-input").fill(email); + await page.getByTestId("password-input").fill(password); + await page.getByTestId("login-submit-btn").click(); await expect(page).toHaveURL(/\/drawer/); await handleWelcomeLetter(page); logger.info(`[Auth] Successfully authenticated ${email}`); } -/** - * Handles and dismisses the first welocme letter - */ -async function handleWelcomeLetter(page: Page) { - logger.info("[Auth] Handling Welcome Letter..."); - // Click envelope to flip - const envelope = page.locator("#env-front"); - await envelope.waitFor({ state: "visible", timeout: 10000 }); - await envelope.click(); - - // Click seal to open flap - const seal = page.getByAltText("Seal"); - await seal.waitFor({ state: "visible" }); - await seal.click(); - - // Click letter to reveal - await page.locator("#letter").click({ position: { x: 30, y: 15 } }); - - // Click "I'll see you" button - const completeButton = page.getByRole("button", { name: /I'll see you/i }); - await completeButton.waitFor({ state: "visible", timeout: 10000 }); - await completeButton.click(); - - await expect(completeButton).toBeHidden(); -} - -export const AuthHelper = { registerAndLogin, handleWelcomeLetter }; +export const AuthHelper = { registerAndLogin }; diff --git a/frontend/e2e/utils/envelope.ts b/frontend/e2e/utils/envelope.ts new file mode 100644 index 0000000..85aae9c --- /dev/null +++ b/frontend/e2e/utils/envelope.ts @@ -0,0 +1,38 @@ +import { type Page, expect } from "@playwright/test"; +import pino from "pino"; + +const logger = pino({ + transport: { + target: "pino-pretty", + options: { + colorize: true, + }, + }, +}); + +/** + * Reveal a letter from an envelope. + */ +export async function revealEnvelope(page: Page) { + logger.info("[Envelope] Revealing envelope..."); + // Click envelope to flip + await page.getByTestId("envelope-front").click(); + + // Click seal to open flap + await page.getByTestId("wax-seal").click(); + + // Click letter to reveal + await page.getByTestId("envelope-letter").click({ position: { x: 30, y: 15 } }); +} + +/** + * Handles and dismisses the first welcome letter + */ +export async function handleWelcomeLetter(page: Page) { + logger.info("[Envelope] Handling Welcome Letter..."); + await revealEnvelope(page); + + // Click "I'll see you" button + await page.getByTestId("dismiss-welcome-letter-btn").click(); + await expect(page.getByTestId("dismiss-welcome-letter-btn")).toBeHidden(); +} diff --git a/frontend/src/components/drawer/WelcomeLetterOverlay.tsx b/frontend/src/components/drawer/WelcomeLetterOverlay.tsx index 5b59420..8422e08 100644 --- a/frontend/src/components/drawer/WelcomeLetterOverlay.tsx +++ b/frontend/src/components/drawer/WelcomeLetterOverlay.tsx @@ -1,77 +1,78 @@ -import { AnimatePresence, motion } from "framer-motion"; +import { AnimatePresence, motion } from "motion/react"; import { useEffect, useRef, useState } from "react"; import { getWelcomeLetterContent } from "../../config/welcomeLetter"; import { formatDate } from "../../utils/dateFormat"; import { type CanvasTools, ComposeCanvas } from "../editor/ComposeCanvas"; import { EnvelopeReveal } from "../reader/EnvelopeReveal"; -interface WelcomeLetterOverlayProps { - onComplete: () => void; - userName: string; +export interface WelcomeLetterOverlayProps { + onComplete: () => void; + userName: string; } export function WelcomeLetterOverlay({ - onComplete, - userName, + onComplete, + userName, }: WelcomeLetterOverlayProps) { - const [revealState, setRevealState] = useState<"SEALED" | "REVEALED">( - "SEALED", - ); - const canvasRef = useRef(null); + const [revealState, setRevealState] = useState<"SEALED" | "REVEALED">( + "SEALED", + ); + const canvasRef = useRef(null); - useEffect(() => { - if (revealState === "REVEALED" && canvasRef.current) { - const welcomeContent = getWelcomeLetterContent(userName); - canvasRef.current.loadData(welcomeContent); - } - }, [revealState, userName]); + useEffect(() => { + if (revealState === "REVEALED" && canvasRef.current) { + const welcomeContent = getWelcomeLetterContent(userName); + canvasRef.current.loadData(welcomeContent); + } + }, [revealState, userName]); - return ( -
-
+ return ( +
+
-
- - {revealState === "SEALED" && ( - - setRevealState("REVEALED")} - ignite={false} - /> - - )} - -
-
-
- -
+
+ + {revealState === "SEALED" && ( + + setRevealState("REVEALED")} + ignite={false} + /> + + )} + +
+
+
+ +
-
- -
+
+ +
+
+
-
-
- ); + ); } diff --git a/frontend/src/components/editor/PostSealModal.tsx b/frontend/src/components/editor/PostSealModal.tsx index 6bb8c51..bccd2b8 100644 --- a/frontend/src/components/editor/PostSealModal.tsx +++ b/frontend/src/components/editor/PostSealModal.tsx @@ -53,6 +53,7 @@ export function PostSealModal({ <>
@@ -112,6 +114,7 @@ export function EnvelopeReveal({ -
- ), + 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 cabinet 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.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(); }); - 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.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.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.getByText(/You've been away a while./i)).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 b21f941..c0e65ac 100644 --- a/frontend/src/pages/Drawer.tsx +++ b/frontend/src/pages/Drawer.tsx @@ -159,6 +159,7 @@ export default function Drawer() { +
+
- )} - - - setSaajanMessage("Hello friend. What should I call you?") - } - /> - - - setSaajanMessage( - "Where should I send your letters?\nNo empty lunchboxes, please.", - ) - } - /> - - - setSaajanMessage( - "Something only you know.\nI have one of those too.", - ) - } - /> - - - setSaajanMessage( - "Just once? Trust me, \nsome things are worth repeating twice.", - ) - } - /> - -
- -

- Choose a password you won't forget.
- Just like life,{" "} - there is no reset{" "} - here. If you lose it, your letters cannot be recovered. -

-
- -
- -
- -
- - ); + + ); }