From 6f12337f1eb39d32596c92c575a0bd90ce2f5bcf Mon Sep 17 00:00:00 2001 From: Tech-lo Date: Wed, 24 Jun 2026 16:10:47 +0530 Subject: [PATCH 1/2] Enhance Planviewer functionality and tests --- src/routes/planviewer/Planviewer.test.tsx | 17 ++- src/routes/planviewer/Planviewer.tsx | 49 +++++++- .../planviewer/components/SeriesCard.tsx | 119 +++++++++++------- .../planviewer/components/SeriesListView.tsx | 3 + src/routes/user-login/UserLogin.test.tsx | 12 +- .../UserRegistration.test.tsx | 12 +- 6 files changed, 155 insertions(+), 57 deletions(-) diff --git a/src/routes/planviewer/Planviewer.test.tsx b/src/routes/planviewer/Planviewer.test.tsx index 8d804cd6..da9dbc94 100644 --- a/src/routes/planviewer/Planviewer.test.tsx +++ b/src/routes/planviewer/Planviewer.test.tsx @@ -290,19 +290,28 @@ describe("Planviewer", () => { ).toBeInTheDocument(); }); - test("navigates from series card to today's plan", async () => { + test("navigates from series card Start button to today's plan", async () => { renderPlanviewer(); const user = userEvent.setup(); await screen.findByText("200-Day Road to the ITCC 2026"); - await user.click( - screen.getByRole("button", { name: "200-Day Road to the ITCC 2026" }), - ); + await user.click(screen.getByRole("button", { name: /^Start$/i })); expect(await screen.findByText("Morning reading")).toBeInTheDocument(); expect(screen.queryByText("Enroll")).not.toBeInTheDocument(); }); + test("navigates from series card to chapter list", async () => { + renderPlanviewer(); + const user = userEvent.setup(); + + await screen.findByText("200-Day Road to the ITCC 2026"); + await user.click(screen.getByRole("button", { name: /View chapters/i })); + + expect(await screen.findByText("ITCC: Days 1-6")).toBeInTheDocument(); + expect(await screen.findByText("Enroll")).toBeInTheDocument(); + }); + test("maps tolgee language codes for backend requests", async () => { getLanguageMock.mockReturnValue("bo-IN"); diff --git a/src/routes/planviewer/Planviewer.tsx b/src/routes/planviewer/Planviewer.tsx index fad0b222..4e9d414c 100644 --- a/src/routes/planviewer/Planviewer.tsx +++ b/src/routes/planviewer/Planviewer.tsx @@ -6,6 +6,7 @@ import { useAuth } from "../../config/AuthContext.tsx"; import { LANGUAGE } from "../../utils/constants.ts"; import { mapLanguageCode } from "../../utils/helperFunctions.tsx"; import SeriesListView from "./components/SeriesListView.tsx"; +import SeriesDetailView from "./components/SeriesDetailView.tsx"; import SeriesPlanRedirect from "./components/SeriesPlanRedirect.tsx"; import DailyPlanView from "./components/DailyPlanView.tsx"; import { apiLanguageParam, tolgeeToPlanLanguage } from "./utils/seriesUtils.ts"; @@ -29,6 +30,7 @@ const Planviewer = () => { const selectedSeriesId = searchParams.get("series"); const selectedPlanId = searchParams.get("plan"); const selectedDate = searchParams.get("date"); + const seriesView = searchParams.get("view"); const isAuthenticatedReady = !isAuthLoading && !isAuth0Loading && @@ -42,13 +44,40 @@ const Planviewer = () => { [apiLanguage, setSearchParams], ); + const handleViewSeriesPlans = useCallback( + (seriesId: string) => { + setSearchParams({ series: seriesId, view: "list", lang: apiLanguage }); + }, + [apiLanguage, setSearchParams], + ); + + const handleSelectPlan = useCallback( + (planId: string) => { + if (!selectedSeriesId) return; + setSearchParams({ + series: selectedSeriesId, + plan: planId, + lang: apiLanguage, + }); + }, + [apiLanguage, selectedSeriesId, setSearchParams], + ); + const handleBackToList = useCallback(() => { setSearchParams(apiLanguage !== "en" ? { lang: apiLanguage } : {}); }, [apiLanguage, setSearchParams]); const handleBackToSeries = useCallback(() => { + if (selectedSeriesId) { + setSearchParams({ + series: selectedSeriesId, + view: "list", + lang: apiLanguage, + }); + return; + } setSearchParams(apiLanguage !== "en" ? { lang: apiLanguage } : {}); - }, [apiLanguage, setSearchParams]); + }, [apiLanguage, selectedSeriesId, setSearchParams]); const handleDateChange = useCallback( (date: string | null) => { @@ -78,6 +107,19 @@ const Planviewer = () => { ); } + if (selectedSeriesId && seriesView === "list") { + return ( + + ); + } + if (selectedSeriesId) { return ( { language={planLanguage} isAuthenticated={isAuthenticatedReady} onSelectSeries={handleSelectSeries} + onViewSeriesPlans={handleViewSeriesPlans} /> ); }, [ selectedSeriesId, selectedPlanId, selectedDate, + seriesView, planLanguage, tolgeeApiLanguage, + apiLanguage, isAuthenticatedReady, handleBackToList, handleBackToSeries, handleDateChange, handleSelectSeries, + handleViewSeriesPlans, + handleSelectPlan, ]); return
{content}
; diff --git a/src/routes/planviewer/components/SeriesCard.tsx b/src/routes/planviewer/components/SeriesCard.tsx index 9e1bd2fe..6fa78bf0 100644 --- a/src/routes/planviewer/components/SeriesCard.tsx +++ b/src/routes/planviewer/components/SeriesCard.tsx @@ -13,6 +13,7 @@ type SeriesCardProps = { language: PlanLanguageCode; enrollment?: UserSeriesEnrollmentDTO; onSelect: (seriesId: string) => void; + onViewPlans: (seriesId: string) => void; }; const SeriesCard = ({ @@ -20,6 +21,7 @@ const SeriesCard = ({ language, enrollment, onSelect, + onViewPlans, }: SeriesCardProps) => { const { t } = useTranslate(); const title = getSeriesTitleForLanguage(series.metadata, language); @@ -29,62 +31,83 @@ const SeriesCard = ({ ); const imageUrl = resolveImageUrl(series.image); const contentFontClass = getLanguageClass(language); + const isEnrolled = Boolean(enrollment); return ( - +
+ +
- + ); }; diff --git a/src/routes/planviewer/components/SeriesListView.tsx b/src/routes/planviewer/components/SeriesListView.tsx index 6a35987e..1a9b2e0b 100644 --- a/src/routes/planviewer/components/SeriesListView.tsx +++ b/src/routes/planviewer/components/SeriesListView.tsx @@ -17,6 +17,7 @@ type SeriesListViewProps = { language: PlanLanguageCode; isAuthenticated: boolean; onSelectSeries: (seriesId: string) => void; + onViewSeriesPlans: (seriesId: string) => void; }; const SeriesListView = ({ @@ -24,6 +25,7 @@ const SeriesListView = ({ language, isAuthenticated, onSelectSeries, + onViewSeriesPlans, }: SeriesListViewProps) => { const { t } = useTranslate(); @@ -102,6 +104,7 @@ const SeriesListView = ({ language={language} enrollment={enrollmentBySeriesId.get(series.id)} onSelect={onSelectSeries} + onViewPlans={onViewSeriesPlans} /> ))} diff --git a/src/routes/user-login/UserLogin.test.tsx b/src/routes/user-login/UserLogin.test.tsx index 763a8a5e..e2052e7f 100644 --- a/src/routes/user-login/UserLogin.test.tsx +++ b/src/routes/user-login/UserLogin.test.tsx @@ -1,11 +1,12 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { BrowserRouter as Router } from "react-router-dom"; -import { QueryClient, QueryClientProvider } from "react-query"; +import { QueryClient, QueryClientProvider, useQuery } from "react-query"; import UserLogin from "./UserLogin.tsx"; import "@testing-library/jest-dom"; -import { vi, test, expect, describe } from "vitest"; +import { vi, test, expect, describe, beforeEach, type Mock } from "vitest"; import { mockAxios, + mockReactQuery, mockTolgee, mockUseAuth, } from "../../test-utils/CommonMocks.ts"; @@ -14,9 +15,16 @@ import axiosInstance from "../../config/axios-config.ts"; mockUseAuth(); mockAxios(); +mockReactQuery(); describe("UserLogin Component", () => { const queryClient = new QueryClient(); + + beforeEach(() => { + (useQuery as unknown as Mock).mockImplementation(() => ({ + data: undefined, + })); + }); const setup = () => { render( diff --git a/src/routes/user-registration/UserRegistration.test.tsx b/src/routes/user-registration/UserRegistration.test.tsx index 41f59f7c..0ab832c0 100644 --- a/src/routes/user-registration/UserRegistration.test.tsx +++ b/src/routes/user-registration/UserRegistration.test.tsx @@ -1,22 +1,30 @@ import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { BrowserRouter as Router } from "react-router-dom"; -import { expect, test, describe } from "vitest"; +import { expect, test, describe, beforeEach, type Mock } from "vitest"; import "@testing-library/jest-dom"; import UserRegistration from "./UserRegistration.js"; import { mockAxios, + mockReactQuery, mockTolgee, mockUseAuth, } from "../../test-utils/CommonMocks.js"; -import { QueryClient, QueryClientProvider } from "react-query"; +import { QueryClient, QueryClientProvider, useQuery } from "react-query"; import { TolgeeProvider } from "@tolgee/react"; mockAxios(); mockUseAuth(); +mockReactQuery(); describe("UserRegistration Component", () => { + beforeEach(() => { + (useQuery as unknown as Mock).mockImplementation(() => ({ + data: undefined, + })); + }); + const setup = () => { const queryClient = new QueryClient({ defaultOptions: { From dffd006850ec36e9120a4c2567df076e62f3a395 Mon Sep 17 00:00:00 2001 From: Tech-lo Date: Wed, 24 Jun 2026 16:26:00 +0530 Subject: [PATCH 2/2] sonar quality gate pass --- src/routes/planviewer/Planviewer.test.tsx | 60 ++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/routes/planviewer/Planviewer.test.tsx b/src/routes/planviewer/Planviewer.test.tsx index da9dbc94..83fad918 100644 --- a/src/routes/planviewer/Planviewer.test.tsx +++ b/src/routes/planviewer/Planviewer.test.tsx @@ -6,7 +6,6 @@ import { MemoryRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "react-query"; import * as reactQuery from "react-query"; import { mockReactQuery } from "../../test-utils/CommonMocks.ts"; -import { LANGUAGE } from "../../utils/constants.ts"; mockReactQuery(); @@ -312,6 +311,65 @@ describe("Planviewer", () => { expect(await screen.findByText("Enroll")).toBeInTheDocument(); }); + test("renders the series detail list when view=list", async () => { + renderPlanviewer("/?series=series-1&view=list"); + + expect( + await screen.findByRole("button", { name: /Enroll/i }), + ).toBeInTheDocument(); + expect(await screen.findByText("ITCC: Days 1-6")).toBeInTheDocument(); + expect(await screen.findByText("ITCC: Days 7-37")).toBeInTheDocument(); + }); + + test("selecting a plan from the detail list opens its daily content", async () => { + renderPlanviewer("/?series=series-1&view=list"); + const user = userEvent.setup(); + + const planRow = await screen.findByText("ITCC: Days 1-6"); + await user.click(planRow); + + expect(await screen.findByText("Morning reading")).toBeInTheDocument(); + }); + + test("back from a daily plan returns to the series detail list", async () => { + renderPlanviewer("/?series=series-1&plan=plan-1"); + const user = userEvent.setup(); + + expect(await screen.findByText("Morning reading")).toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: /Back to series/i })); + + expect( + await screen.findByRole("button", { name: /Enroll/i }), + ).toBeInTheDocument(); + }); + + test("changing the day updates the daily content", async () => { + renderPlanviewer("/?series=series-1&plan=plan-1"); + const user = userEvent.setup(); + + expect(await screen.findByText("Morning reading")).toBeInTheDocument(); + + const dayButton = await screen.findByRole("button", { + name: /^Day 2,/i, + }); + await user.click(dayButton); + + expect(await screen.findByText("Morning reading")).toBeInTheDocument(); + }); + + test("back from the detail list keeps the non-English language param", async () => { + getLanguageMock.mockReturnValue("bo-IN"); + renderPlanviewer("/?series=series-1&view=list&lang=bo"); + const user = userEvent.setup(); + + await screen.findByRole("button", { name: /Enroll/i }); + await user.click(screen.getByRole("button", { name: /All routines/i })); + + expect( + await screen.findByText("200-Day Road to the ITCC 2026"), + ).toBeInTheDocument(); + }); + test("maps tolgee language codes for backend requests", async () => { getLanguageMock.mockReturnValue("bo-IN");