From 2ba5d6964f53b4c56f5ce739a830d92b58591132 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 8 May 2026 21:21:30 +0530 Subject: [PATCH] test: replace role texts by testids --- .../src/components/drawer/PasskeyModal.tsx | 7 +- frontend/src/components/editor/ToolBar.tsx | 3 + frontend/src/components/ui/FormField.tsx | 71 ++-- frontend/src/components/ui/Modal.tsx | 68 ++-- frontend/src/pages/Drawer.test.tsx | 2 +- frontend/src/pages/Editor.test.tsx | 21 +- frontend/src/pages/Login.test.tsx | 12 +- frontend/src/pages/Login.tsx | 233 ++++++------- frontend/src/pages/Register.tsx | 313 +++++++++--------- 9 files changed, 368 insertions(+), 362 deletions(-) diff --git a/frontend/src/components/drawer/PasskeyModal.tsx b/frontend/src/components/drawer/PasskeyModal.tsx index e33a38b..fea647f 100644 --- a/frontend/src/components/drawer/PasskeyModal.tsx +++ b/frontend/src/components/drawer/PasskeyModal.tsx @@ -41,10 +41,15 @@ export function PasskeyModal() { required type="password" placeholder="password" + data-testid="passkey-input" className="font-sans validator input input-bordered rounded-r-none" />
- diff --git a/frontend/src/components/editor/ToolBar.tsx b/frontend/src/components/editor/ToolBar.tsx index ef16a4a..e0969da 100644 --- a/frontend/src/components/editor/ToolBar.tsx +++ b/frontend/src/components/editor/ToolBar.tsx @@ -194,6 +194,7 @@ export function ToolBar({ - )} - {children} - - , - mainContainer, - ); + if (!isOpen) return null; + // render the modal top of all elements and position them to document viewport (/ the main wrapper). + // NOTE: this is recommended approach for modals as it shouldn't be bound to the parent box. + const mainContainer = document.querySelector("main") || document.body; + return createPortal( +
+
+ {onClose && ( + + )} + {children} +
+
, + mainContainer, + ); } diff --git a/frontend/src/pages/Drawer.test.tsx b/frontend/src/pages/Drawer.test.tsx index 3001514..b8acdcd 100644 --- a/frontend/src/pages/Drawer.test.tsx +++ b/frontend/src/pages/Drawer.test.tsx @@ -94,7 +94,7 @@ describe("Drawer Page", () => { ); expect(screen.getByTestId("passkey-modal-title")).toBeInTheDocument(); - expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument(); + expect(screen.getByTestId("passkey-input")).toBeInTheDocument(); }); it("renders the welcome letter when firstTime state is present", () => { diff --git a/frontend/src/pages/Editor.test.tsx b/frontend/src/pages/Editor.test.tsx index 7b8ad34..98b20f7 100644 --- a/frontend/src/pages/Editor.test.tsx +++ b/frontend/src/pages/Editor.test.tsx @@ -97,25 +97,22 @@ describe("Editor Page", () => { fireEvent.click(sealBtn); // Click Vault to show confirm modal - const vaultBtn = screen.getByRole("button", { name: /vault/i }); + const vaultBtn = screen.getByTestId("vault-trigger-btn"); fireEvent.click(vaultBtn); // Set date and submit vault form - const dateInput = container.querySelector('input[name="vault-date"]'); + const dateInput = document.body.querySelector('input[name="vault-date"]'); if (!dateInput) throw new Error("Date input not found"); fireEvent.change(dateInput, { target: { value: "2026-12-31" } }); - const confirmVaultBtn = container.querySelector( - 'button[form="vault-form"]', - ); - if (!confirmVaultBtn) throw new Error("Confirm vault button not found"); + const confirmVaultBtn = screen.getByTestId("vault-confirm-btn"); fireEvent.click(confirmVaultBtn); // Wait for save to complete and check readOnly expect(await screen.findByTestId("save-success-toast")).toBeInTheDocument(); expect(canvas.getAttribute("data-readonly")).toBe("true"); - expect(screen.getByLabelText(/recipient/i)).toBeDisabled(); + expect(screen.getByTestId("recipient-input")).toBeDisabled(); }); it("should set canvas to readOnly when status is SEALED", async () => { @@ -135,7 +132,7 @@ describe("Editor Page", () => { }), ); - const { container } = render( + render( } /> @@ -149,19 +146,17 @@ describe("Editor Page", () => { const canvas = screen.getByTestId("canvas"); - const toolbar = container.querySelector("#writer-toolbar"); - const sealBtn = toolbar?.querySelector(".btn-primary"); - if (!sealBtn) throw new Error("Seal button not found"); + const sealBtn = screen.getByTestId("seal-trigger-btn"); fireEvent.click(sealBtn); // The secondary seal button appears (it has btn-accent class) - const secondarySealBtn = container.querySelector(".btn-accent"); + const secondarySealBtn = screen.getByTestId("seal-confirm-btn"); if (!secondarySealBtn) throw new Error("Secondary seal button not found"); fireEvent.click(secondarySealBtn); expect(await screen.findByTestId("save-success-toast")).toBeInTheDocument(); expect(canvas.getAttribute("data-readonly")).toBe("true"); - expect(screen.getByLabelText(/recipient/i)).toBeDisabled(); + expect(screen.getByTestId("recipient-input")).toBeDisabled(); }); }); diff --git a/frontend/src/pages/Login.test.tsx b/frontend/src/pages/Login.test.tsx index 3b77068..a7909d7 100644 --- a/frontend/src/pages/Login.test.tsx +++ b/frontend/src/pages/Login.test.tsx @@ -27,9 +27,9 @@ describe("Login Page", () => { , ); - await userEvent.type(screen.getByLabelText(/email/i), "test@example.com"); - await userEvent.type(screen.getByLabelText(/password/i), "password123"); - await userEvent.click(screen.getByRole("button", { name: /sign in/i })); + await userEvent.type(screen.getByTestId("email-input"), "test@example.com"); + await userEvent.type(screen.getByTestId("password-input"), "password123"); + await userEvent.click(screen.getByTestId("login-submit-btn")); expect(await screen.findByTestId("login-error-message")).toHaveTextContent( /technical issues/i, @@ -87,9 +87,9 @@ describe("Login Page", () => { , ); - await userEvent.type(screen.getByLabelText(/email/i), "test@example.com"); - await userEvent.type(screen.getByLabelText(/password/i), "password123"); - await userEvent.click(screen.getByRole("button", { name: /sign in/i })); + await userEvent.type(screen.getByTestId("email-input"), "test@example.com"); + await userEvent.type(screen.getByTestId("password-input"), "password123"); + await userEvent.click(screen.getByTestId("login-submit-btn")); const expectedTestId = nextRoute.toLowerCase() === "drawer" ? "drawer-page" : "reader-page"; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 9aca27f..0621949 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -16,134 +16,135 @@ import { useAuth } from "../hooks/useAuth"; import { CryptoUtils } from "../utils/crypto"; const loginSchema = z.object({ - email: z.email("Please enter a valid email"), - password: z.string().min(1, "Password is required"), + email: z.email("Please enter a valid email"), + password: z.string().min(1, "Password is required"), }); type LoginInputs = z.infer; export default function Login() { - const navigate = useNavigate(); - const location = useLocation(); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(null); - const { setAuthStore } = useAuth(); - const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); - const [saajanMessage, setSaajanMessage] = useState( - "I was wondering when you'd return.", - ); - const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER; + const navigate = useNavigate(); + const location = useLocation(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const { setAuthStore } = useAuth(); + const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); + const [saajanMessage, setSaajanMessage] = useState( + "I was wondering when you'd return.", + ); + const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER; - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(loginSchema), - }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + }); - const onSubmit = async (data: LoginInputs) => { - setIsLoading(true); - setApiError(null); - try { - // client side key derivation for e2e encryption - const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( - data.password, - data.email, - ); + const onSubmit = async (data: LoginInputs) => { + setIsLoading(true); + setApiError(null); + try { + // client side key derivation for e2e encryption + const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( + data.password, + data.email, + ); - // send just the authHash as the password to the server - const { data: authData } = await publicApi.post(endpoints.LOGIN, { - email: data.email, - password: authHash, - }); + // send just the authHash as the password to the server + const { data: authData } = await publicApi.post(endpoints.LOGIN, { + email: data.email, + password: authHash, + }); - const { data: userData } = await api.get(endpoints.ME, { - headers: { Authorization: `Bearer ${authData.access}` }, - }); + const { data: userData } = await api.get(endpoints.ME, { + headers: { Authorization: `Bearer ${authData.access}` }, + }); - await setAuthStore(authData.access, userData, masterKey); + await setAuthStore(authData.access, userData, masterKey); - navigate(nextRoute, { replace: true, state: location.state }); - } catch (err) { - let message = - "Sorry, we're experiencing technical issues.\nPlease try again later."; - if (axios.isAxiosError(err) && err.response?.status !== 500) { - message = err.response?.data?.detail || err.response?.data?.message; - } - setApiError(message); - } finally { - setIsLoading(false); - } - }; + navigate(nextRoute, { replace: true, state: location.state }); + } catch (err) { + let message = + "Sorry, we're experiencing technical issues.\nPlease try again later."; + if (axios.isAxiosError(err) && err.response?.status !== 500) { + message = err.response?.data?.detail || err.response?.data?.message; + } + setApiError(message); + } finally { + setIsLoading(false); + } + }; - return ( -
- {!showWelcome && } - {showWelcome && } -
-
-

-   Enter Archive -

+ return ( +
+ {!showWelcome && } + {showWelcome && } +
+ +

+   Enter Archive +

- {apiError && ( -
- {apiError} -
- )} - - setSaajanMessage("I remember you.")} - /> - - - setSaajanMessage("The one thing I cannot know for you.") - } - /> - -
- -
-
or
-
- New to ?{" "} - . -
- + {apiError && ( +
+ {apiError}
-
- ); + )} + + setSaajanMessage("I remember you.")} + /> + + + setSaajanMessage("The one thing I cannot know for you.") + } + /> + +
+ +
+
or
+
+ New to ?{" "} + + . +
+ +
+
+ ); } diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index aa711dd..feb75c7 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -14,170 +14,171 @@ import { ROUTES } from "../config/routes"; import { CryptoUtils } from "../utils/crypto"; const registerSchema = z - .object({ - full_name: z.string().min(2, "Name must be at least 2 characters"), - email: z.email("Please enter a valid email"), - password: z.string().min(8, "Password must be at least 8 characters"), - confirm_password: z.string(), - }) - .refine((data) => data.password === data.confirm_password, { - message: "Passwords don't match", - path: ["confirm_password"], - }); + .object({ + full_name: z.string().min(2, "Name must be at least 2 characters"), + email: z.email("Please enter a valid email"), + password: z.string().min(8, "Password must be at least 8 characters"), + confirm_password: z.string(), + }) + .refine((data) => data.password === data.confirm_password, { + message: "Passwords don't match", + path: ["confirm_password"], + }); type RegisterInputs = z.infer; export default function Register() { - const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(null); - const [saajanMessage, setSaajanMessage] = useState( - "I didn't think I'd be here either.\nAnd yet, here we are.", - ); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const [saajanMessage, setSaajanMessage] = useState( + "I didn't think I'd be here either.\nAnd yet, here we are.", + ); - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(registerSchema), - }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + }); - const onSubmit = async (data: RegisterInputs) => { - setSaajanMessage("Good. I'll remember that."); - setIsLoading(true); - setApiError(null); - try { - // we generate the key bundle here to get the authHash (password) to be haSHed and stored in the db. - const { authHash } = await CryptoUtils.deriveKeyBundle( - data.password, - data.email, - ); + const onSubmit = async (data: RegisterInputs) => { + setSaajanMessage("Good. I'll remember that."); + setIsLoading(true); + setApiError(null); + try { + // we generate the key bundle here to get the authHash (password) to be haSHed and stored in the db. + const { authHash } = await CryptoUtils.deriveKeyBundle( + data.password, + data.email, + ); - await publicApi.post(endpoints.REGISTER, { - full_name: data.full_name, - email: data.email, - password: authHash, - }); - navigate(ROUTES.VERIFY_EMAIL, { replace: true }); - } catch (err) { - let message = "Registration failed. Please try again."; - if (axios.isAxiosError(err)) { - message = err.response?.data?.message || message; - } - setApiError(message); - } finally { - setIsLoading(false); - } - }; + await publicApi.post(endpoints.REGISTER, { + full_name: data.full_name, + email: data.email, + password: authHash, + }); + navigate(ROUTES.VERIFY_EMAIL, { replace: true }); + } catch (err) { + let message = "Registration failed. Please try again."; + if (axios.isAxiosError(err)) { + message = err.response?.data?.message || message; + } + setApiError(message); + } finally { + setIsLoading(false); + } + }; - return ( -
- -
-
-
- Create a Account -
+ return ( +
+ +
+ +
+ Create a Account +
- {apiError && ( -
- {apiError} -
- )} - - - 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. -

-
- -
- -
-
or
-
- Been here before?{" "} - . -
- + {apiError && ( +
+ {apiError}
-
- ); + )} + + + 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. +

+
+ +
+ +
+
or
+
+ Been here before?{" "} + + . +
+ +
+
+ ); }