From 548b34fb5127c49d929abd782ef5c27ea716e9d3 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 6 May 2026 22:48:06 +0530 Subject: [PATCH 1/3] 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. -

-
- -
- -
- -
- - ); + + ); } -- 2.52.0 From e8d589d06ddc52c1f918c23e698cef77c34bf90d Mon Sep 17 00:00:00 2001 From: me Date: Wed, 6 May 2026 22:48:31 +0530 Subject: [PATCH 2/3] chore: update test webServer to use preview mode --- frontend/package.json | 136 +++++++++++++++++----------------- frontend/playwright.config.ts | 3 +- frontend/vite.config.ts | 5 ++ 3 files changed, 75 insertions(+), 69 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index f2c0f19..5471727 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,70 +1,70 @@ { - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b & vite build", - "build:prod": "vite build --mode production", - "lint": "biome lint --write ./src", - "format": "biome format --write ./src", - "check": "biome check --write ./src", - "check-all": "biome check --write .", - "preview": "vite preview", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui --ui-host=0.0.0.0 --ui-port=43008" - }, - "dependencies": { - "@fontsource-variable/jost": "^5.2.8", - "@fontsource-variable/playfair-display": "^5.2.8", - "@fontsource-variable/playwrite-hr-lijeva": "^5.2.7", - "@fontsource/architects-daughter": "^5.2.7", - "@fontsource/cutive-mono": "^5.2.8", - "@fontsource/kavivanar": "^5.2.8", - "@fontsource/knewave": "^5.2.7", - "@fontsource/redacted-script": "^5.2.8", - "@fontsource/space-mono": "^5.2.9", - "@hookform/resolvers": "^5.2.2", - "@phosphor-icons/react": "^2.1.10", - "@tailwindcss/vite": "^4.2.2", - "axios": "^1.15.0", - "daisyui": "^5.5.19", - "fabric": "^7.2.0", - "idb": "^8.0.3", - "lenis": "^1.3.23", - "motion": "^12.38.0", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "react-hook-form": "^7.72.1", - "react-router-dom": "^7.14.0", - "tailwindcss": "^4.2.2", - "zod": "^4.3.6", - "zustand": "^5.0.12" - }, - "devDependencies": { - "@biomejs/biome": "^2.4.11", - "@playwright/test": "^1.59.1", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.1", - "@types/node": "^25.6.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-basic-ssl": "^2.3.0", - "@vitejs/plugin-react": "^6.0.1", - "@vitest/coverage-v8": "^4.1.4", - "dotenv": "^17.4.2", - "fake-indexeddb": "^6.2.5", - "jsdom": "^29.0.2", - "msw": "^2.13.2", - "pino": "^10.3.1", - "pino-pretty": "^13.1.3", - "typescript": "~6.0.2", - "vite": "^8.0.4", - "vitest": "^4.1.4" - } + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b & vite build", + "build:prod": "vite build --mode production", + "lint": "biome lint --write ./src", + "format": "biome format --write ./src", + "check": "biome check --write ./src", + "check-all": "biome check --write .", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui --ui-host=0.0.0.0 --ui-port=43008" + }, + "dependencies": { + "@fontsource-variable/jost": "^5.2.8", + "@fontsource-variable/playfair-display": "^5.2.8", + "@fontsource-variable/playwrite-hr-lijeva": "^5.2.7", + "@fontsource/architects-daughter": "^5.2.7", + "@fontsource/cutive-mono": "^5.2.8", + "@fontsource/kavivanar": "^5.2.8", + "@fontsource/knewave": "^5.2.7", + "@fontsource/redacted-script": "^5.2.8", + "@fontsource/space-mono": "^5.2.9", + "@hookform/resolvers": "^5.2.2", + "@phosphor-icons/react": "^2.1.10", + "@tailwindcss/vite": "^4.2.2", + "axios": "^1.15.0", + "daisyui": "^5.5.19", + "fabric": "^7.2.0", + "idb": "^8.0.3", + "lenis": "^1.3.23", + "motion": "^12.38.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-hook-form": "^7.72.1", + "react-router-dom": "^7.14.0", + "tailwindcss": "^4.2.2", + "zod": "^4.3.6", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.11", + "@playwright/test": "^1.59.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.6.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-basic-ssl": "^2.3.0", + "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "^4.1.4", + "dotenv": "^17.4.2", + "fake-indexeddb": "^6.2.5", + "jsdom": "^29.0.2", + "msw": "^2.13.2", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", + "typescript": "~6.0.2", + "vite": "^8.0.4", + "vitest": "^4.1.4" + } } diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 8a81b4f..82e9662 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -60,7 +60,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: "npm run dev -- --mode e2e", + // NOTE: using npm here for docker compat mainly + command: "npm run build -- --mode e2e && npm run preview -- --mode e2e", url: getBaseUrl( process.env.SSL_ENABLED === "true", process.env.FRONTEND_DOMAIN, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 0193b1d..e6cfa73 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -46,5 +46,10 @@ export default defineConfig(({ mode }) => { host: env.FRONTEND_DOMAIN, https: isSslEnabled ? sslCerts : undefined, }, + preview: { + port: Number(env.FRONTEND_PORT), + host: env.FRONTEND_DOMAIN, + https: isSslEnabled ? sslCerts : undefined, + }, }; }); -- 2.52.0 From ddf62cd565e49a35a3613ff773d8ff4ea421f938 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 6 May 2026 23:20:04 +0530 Subject: [PATCH 3/3] refactor: standardize test IDs in E2E testing --- frontend/e2e/letter.spec.ts | 59 ++---- frontend/e2e/utils/auth.ts | 2 +- .../src/components/drawer/DrawerSection.tsx | 1 + frontend/src/components/drawer/LetterItem.tsx | 1 + .../src/components/editor/PostSealModal.tsx | 2 +- frontend/src/components/reader/ShareModal.tsx | 7 +- frontend/src/components/ui/Modal.tsx | 14 +- frontend/src/pages/Activate.tsx | 174 +++++++++--------- frontend/src/pages/Editor.tsx | 6 +- frontend/src/pages/Reader.tsx | 5 +- 10 files changed, 138 insertions(+), 133 deletions(-) diff --git a/frontend/e2e/letter.spec.ts b/frontend/e2e/letter.spec.ts index 6ef1a3a..dc1e3e5 100644 --- a/frontend/e2e/letter.spec.ts +++ b/frontend/e2e/letter.spec.ts @@ -27,16 +27,15 @@ test.describe("Letter Drafting (Real Backend)", () => { logger.info(`>> [Draft] Current URL after click: ${page.url()}`); - // Wait for the recipient input to be present in the DOM + // Editor page + await expect(page.getByTestId("recipient-input")).toBeVisible(); const recipientInput = page.getByTestId("recipient-input"); - await recipientInput.waitFor({ state: "visible", timeout: 20000 }); const recipientName = "Dear Friend"; await recipientInput.fill(recipientName); // Initial load: verify textarea value (populated by Fabric when focused) const canvasInput = page.locator("textarea"); - await canvasInput.waitFor({ state: "attached" }); await canvasInput.focus(); await expect(canvasInput).toHaveValue(/Take a deep breath/i); @@ -50,7 +49,7 @@ test.describe("Letter Drafting (Real Backend)", () => { await page.getByTestId("draft-btn").click(); // Verify Success Modal/Alert - await expect(page.getByText(/your letter is saved/i)).toBeVisible(); + await expect(page.getByTestId("save-success-toast")).toBeVisible(); // Verify URL updated with a UUID await expect(page).toHaveURL(/\/quill\/[0-9a-f-]{36}/); @@ -62,13 +61,7 @@ test.describe("Letter Drafting (Real Backend)", () => { await page.goto(savedUrl); // Wait for initial load overlay to appear and then definitely disappear - await page - .getByText(/opening your draft/i) - .waitFor({ state: "visible", timeout: 2000 }) - .catch(() => {}); - await expect(page.getByText(/opening your draft/i)).toBeHidden({ - timeout: 10000, - }); + await expect(page.getByTestId("opening-draft-overlay")).toBeHidden(); // Check recipient await expect(page.getByTestId("recipient-input")).toHaveValue(recipientName); @@ -77,9 +70,7 @@ test.describe("Letter Drafting (Real Backend)", () => { // We wait for the content to appear in the textarea. // toHaveValue will poll until it matches or timeouts. await canvasInput.focus(); - await expect(canvasInput).toHaveValue(/This is a secret draft/i, { - timeout: 10000, - }); + await expect(canvasInput).toHaveValue(/This is a secret draft/i); await expect(canvasInput).toHaveValue(/It should persist/i); }); @@ -96,7 +87,6 @@ test.describe("Letter Drafting (Real Backend)", () => { await page.getByTestId("write-letter-btn").click(); const recipientInput = page.getByTestId("recipient-input"); - await recipientInput.waitFor({ state: "visible", timeout: 10000 }); await recipientInput.fill("A Secret Guest"); const canvasInput = page.locator("textarea"); @@ -110,40 +100,36 @@ test.describe("Letter Drafting (Real Backend)", () => { // Should show sealed confirmation modal logger.info(">> [Seal] Verifying sealed modal..."); - await expect(page.getByText(/your letter is sealed/i)).toBeVisible({ - timeout: 10000, - }); + await expect(page.getByTestId("post-seal-modal")).toBeVisible(); // Navigate to Reader via "View letter" await page.getByTestId("view-letter-btn").click(); // Should be on Reader URL - await expect(page).toHaveURL(/\/read\/[a-f0-9-]{36}$/, { timeout: 15000 }); + await expect(page).toHaveURL(/\/read\/[a-f0-9-]{36}$/); // Open the envelope to reveal the letter - await expect(page.getByText(/breaking the seal/i)).toBeHidden({ - timeout: 10000, - }); + await expect(page.getByTestId("decryption-overlay")).toBeHidden(); // Flip the envelope to show the seal and reveal the letter await revealEnvelope(page); - await expect(page.getByTestId("envelope-letter")).toBeHidden({ timeout: 20000 }); + await expect(page.getByTestId("envelope-letter")).toBeHidden(); // Share on demand logger.info(">> [Seal] Clicking Share button in Reader..."); await page.getByTestId("share-letter-btn").click(); // Verify share modal with a valid link - await expect(page.getByText(/send this letter/i)).toBeVisible(); + await expect(page.getByTestId("share-letter-modal")).toBeVisible(); const linkInput = page.locator("#share-link-input"); const linkValue = await linkInput.inputValue(); expect(linkValue).toContain("/read/"); expect(linkValue).toContain("#"); logger.info(`>> [Seal] Sharing link: ${linkValue}`); - await expect(page.getByRole("button", { name: /copy/i })).toBeVisible(); + await expect(page.getByTestId("copy-link-btn")).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(); + await page.getByTestId("modal-close-btn").click(); + await expect(page.getByTestId("share-letter-modal")).toBeHidden(); }); test("should allow author to access sealed letter from drawer without sharing key", async ({ @@ -161,7 +147,6 @@ test.describe("Letter Drafting (Real Backend)", () => { await page.getByTestId("write-letter-btn").click(); const recipientInput = page.getByTestId("recipient-input"); - await recipientInput.waitFor({ state: "visible" }); await recipientInput.fill(recipientName); const canvasInput = page.locator("textarea"); @@ -173,34 +158,30 @@ test.describe("Letter Drafting (Real Backend)", () => { 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 expect(page.getByTestId("post-seal-modal")).toBeVisible(); 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..."); - const keptSection = page.locator("#kept"); - await keptSection.getByRole("button", { name: /kept/i }).click(); + await page.getByTestId("drawer-section-kept").click(); // Find the sealed letter in the drawer by recipient name and click it logger.info(">> [Drawer] Clicking sealed letter in drawer..."); const sealedItem = page - .getByRole("button", { name: new RegExp(recipientName, "i") }) + .getByTestId(/^letter-item-/) + .filter({ hasText: recipientName }) .first(); await sealedItem.click(); // Verify it opens the Reader without a hash logger.info(">> [Drawer] Verifying Reader page..."); // Give it a bit more time for decryption - await expect(page).toHaveURL(/\/read\/[a-f0-9-]{36}$/, { timeout: 15000 }); + await expect(page).toHaveURL(/\/read\/[a-f0-9-]{36}$/); // Reveal and check decrypted content in Reader - await expect(page.getByText(/breaking the seal/i)).toBeHidden({ - timeout: 10000, - }); + await expect(page.getByTestId("decryption-overlay")).toBeHidden(); // Flip the envelope and reveal the letter await revealEnvelope(page); - await expect(page.getByTestId("envelope-letter")).toBeHidden({ timeout: 20000 }); + await expect(page.getByTestId("envelope-letter")).toBeHidden(); // 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 66b86e8..3e92f04 100644 --- a/frontend/e2e/utils/auth.ts +++ b/frontend/e2e/utils/auth.ts @@ -38,7 +38,7 @@ async function registerAndLogin( await page.goto(activationLink); - await expect(page.getByText(/account activated/i)).toBeVisible(); + await expect(page.getByTestId("activation-success-header")).toBeVisible(); await page.getByTestId("start-writing-btn").click(); // Dismiss the Welcom Modal and Perform Login diff --git a/frontend/src/components/drawer/DrawerSection.tsx b/frontend/src/components/drawer/DrawerSection.tsx index 0865d9b..f189bf3 100644 --- a/frontend/src/components/drawer/DrawerSection.tsx +++ b/frontend/src/components/drawer/DrawerSection.tsx @@ -35,6 +35,7 @@ export function DrawerSection({ - - )} + }; - {status === "error" && ( -
-
- -
-

Activation Failed

-

- The link might be expired or already used. Please try registering - again. -

-
- + activateAccount(); + }, [uidb64, token]); + + return ( +
+ {status === "loading" && ( +
+ +

Activating your account...

+
+ )} + + {status === "success" && ( +
+
+ +
+

+ You're in. +

+

+ Welcome to +
+ Just one more step and you can start writing timeless letters. +

+
+ +
+ )} + + {status === "error" && ( +
+
+ +
+

Activation Failed

+

+ The link might be expired or already used. Please try registering + again. +

+
+ +
+ )}
- )} -
- ); + ); } diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 418811f..912ac1e 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -376,7 +376,10 @@ export default function Editor() { weight="bold" className="animate-spin text-primary" /> -

+

Opening your draft...

@@ -406,6 +409,7 @@ export default function Editor() { {saveOverlay === "SAVED" && (
-

+

Breaking the seal...

-- 2.52.0