Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/components/api/searchApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import axiosInstance from "@/config/axios-config";

type SearchCommon = {
query: string;
language?: string;
limit?: number;
skip?: number;
};
Expand Down
10 changes: 4 additions & 6 deletions src/components/auth/login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const Login = () => {
const { t } = useTranslate();
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState<string>("");
const [successMessage, setSuccessMessage] = useState<string>("");
const [showEmailReverify, setShowEmailReverify] = useState<boolean>(false);
Expand Down Expand Up @@ -69,10 +68,12 @@ const Login = () => {
},
});

const handleLogin = (e: React.FormEvent) => {
const handleLogin = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setShowEmailReverify(false);
setSuccessMessage("");
const formData = new FormData(e.currentTarget);
const password = formData.get("password") as string;
const clientPassword = createPasswordHash(email, password);
loginMutation.mutate({
email,
Expand Down Expand Up @@ -111,13 +112,10 @@ const Login = () => {
</Label>
<Input
type="password"
name="password"
placeholder={t("studio.login.placeholder.password")}
className=" placeholder:text-[#b1b1b1]"
required
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
</div>
<div className="flex mt-4 justify-center ">
Expand Down
97 changes: 92 additions & 5 deletions src/components/routes/create-plan/CreatePlan.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ describe("CreatePlan Component", () => {
}
return Promise.resolve({ data: {} });
});
vi.spyOn(axiosInstance, "get").mockResolvedValue({
data: {
id: "plan-123",
title: "Existing Plan",
description: "Existing description",
total_days: 14,
difficulty_level: "Beginner",
plan_image_url: "https://example.com/image.jpg",
image_url: "https://example.com/image.jpg",
tags: ["meditation"],
language: "en",
},
});
});
it("renders create plan form with main heading", () => {
renderWithProviders(<CreatePlan />);
Expand Down Expand Up @@ -162,7 +175,7 @@ describe("CreatePlan Component", () => {

fireEvent.change(titleInput, { target: { value: "Test Plan Title" } });

expect((titleInput as HTMLInputElement).value).toBe("Test Plan Title");
expect(titleInput).toHaveValue("Test Plan Title");
});

it("allows typing in description textarea", () => {
Expand All @@ -176,9 +189,7 @@ describe("CreatePlan Component", () => {
target: { value: "Test plan description" },
});

expect((descriptionTextarea as HTMLTextAreaElement).value).toBe(
"Test plan description",
);
expect(descriptionTextarea).toHaveValue("Test plan description");
});

it("allows typing in number of days input", () => {
Expand All @@ -190,7 +201,7 @@ describe("CreatePlan Component", () => {

fireEvent.change(daysInput, { target: { value: "30" } });

expect((daysInput as HTMLInputElement).value).toBe("30");
expect(daysInput).toHaveValue(30);
});

it("renders image upload area", () => {
Expand Down Expand Up @@ -382,4 +393,80 @@ describe("CreatePlan Component", () => {
fireEvent.click(confirmButton);
expect(mockBlocker.proceed).toHaveBeenCalled();
});

it("fetches and populates form in edit mode", async () => {
vi.mocked(useParams).mockReturnValue({ plan_id: "plan-123" });

renderWithProviders(<CreatePlan />);

await waitFor(() => {
expect(
screen.getByPlaceholderText("studio.plan.form.placeholder.title"),
).toHaveValue("Existing Plan");
});

expect(
screen.getByPlaceholderText("studio.plan.form.placeholder.description"),
).toHaveValue("Existing description");

expect(
screen.getByPlaceholderText(
"studio.plan.form.placeholder.number_of_days",
),
).toHaveValue(14);

expect(screen.getByAltText("Cover preview")).toBeInTheDocument();
});

it("handles navigation cancellation", async () => {
const mockBlocker = {
state: "blocked" as const,
proceed: vi.fn(),
reset: vi.fn(),
location: {} as any,
};
vi.mocked(useBlocker).mockReturnValue(mockBlocker);
renderWithProviders(<CreatePlan />);
await waitFor(() => {
expect(
screen.getByText("studio.plan.navigation.confirm_title"),
).toBeInTheDocument();
});
const cancelButtons = screen.getAllByText("common.button.cancel");
const dialogCancelButton = cancelButtons.find((btn) =>
btn.closest('[role="dialog"]'),
);
fireEvent.click(dialogCancelButton!);
expect(mockBlocker.reset).toHaveBeenCalled();
});

it("submits update in edit mode", async () => {
vi.mocked(useParams).mockReturnValue({ plan_id: "plan-123" });
vi.spyOn(axiosInstance, "put").mockResolvedValue({ data: {} });
renderWithProviders(<CreatePlan />);

await waitFor(() => {
expect(
screen.getByPlaceholderText("studio.plan.form.placeholder.title"),
).toHaveValue("Existing Plan");
});

fireEvent.change(
screen.getByPlaceholderText("studio.plan.form.placeholder.title"),
{ target: { value: "Updated Plan" } },
);

const submitButton = screen.getByText("studio.plan.update_button");
await act(async () => {
fireEvent.click(submitButton);
});

await waitFor(() => {
expect(axiosInstance.put).toHaveBeenCalledWith(
"/api/v1/cms/plans/plan-123",
expect.any(Object),
expect.any(Object),
);
});
});
});
40 changes: 40 additions & 0 deletions src/components/routes/dashboard/Dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,44 @@ describe("Dashboard Component", () => {
);
});
});

it("handles toggle featured on a plan", async () => {
vi.spyOn(axiosInstance, "get").mockResolvedValue({
data: {
plans: [
{
id: "plan-1",
image_url: "",
title: "Test Plan",
description: "Test description",
total_days: "7",
subscription_count: "10",
status: "PUBLISHED",
featured: false,
language: "EN",
},
],
total: 1,
},
});
axiosInstance.patch = vi.fn().mockResolvedValue({ data: {} });

renderWithProviders(<Dashboard />);

await waitFor(() => {
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
});

expect(screen.getByText("Test Plan")).toBeInTheDocument();

const featuredButton = screen.getByText("Not Featured");
fireEvent.click(featuredButton);

await waitFor(() => {
expect(axiosInstance.patch).toHaveBeenCalledWith(
"/api/v1/cms/plans/plan-1/featured",
expect.any(Object),
);
});
});
});
146 changes: 144 additions & 2 deletions src/components/routes/task/PlanDetailsPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { vi } from "vitest";
import { BrowserRouter } from "react-router-dom";
Expand Down Expand Up @@ -93,15 +94,18 @@ Object.defineProperty(window, "sessionStorage", {
writable: true,
});

const renderWithProviders = (component: React.ReactElement, isDraft = true) => {
const renderWithProviders = (
component: React.ReactElement,
isEditable = true,
) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
queryClient.setQueryData(["planDetails", "test-plan-id"], {
...mockPlanData,
status: isDraft ? "DRAFT" : "ARCHIVED",
status: isEditable ? "DRAFT" : "PUBLISHED",
});
return render(
<QueryClientProvider client={queryClient}>
Expand Down Expand Up @@ -230,4 +234,142 @@ describe("PlanDetailsPanel Component", () => {
expect(screen.getByText("Task")).toBeInTheDocument();
});
});

it("fetches plan details via queryFn when cache is empty", async () => {
const { default: axiosInstance } = await import("@/config/axios-config");
const mockAxios = axiosInstance as any;
mockAxios.get.mockResolvedValue({
data: { ...mockPlanData, status: "DRAFT" },
});

const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});

render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<PlanDetailsPage />
</BrowserRouter>
</QueryClientProvider>,
);

await waitFor(() => {
expect(mockAxios.get).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/cms/plans/test-plan-id"),
expect.any(Object),
);
});
await waitFor(() => {
expect(screen.getByText(mockPlanData.title)).toBeInTheDocument();
});
});

it("clears selected task when that task is deleted from sidebar", async () => {
const user = userEvent.setup();
const { default: axiosInstance } = await import("@/config/axios-config");
const mockAxios = axiosInstance as any;
mockAxios.get.mockImplementation((url: string) => {
if (url.includes("/tasks/task1")) {
return Promise.resolve({
data: {
id: "task1",
title: "Morning Intention Setting",
display_order: 1,
estimated_time: 30,
subtasks: [],
},
});
}
return Promise.resolve({ data: { ...mockPlanData, status: "DRAFT" } });
});
mockAxios.delete.mockResolvedValue({ data: {} });

renderWithProviders(<PlanDetailsPage />, true);
await waitFor(() => {
expect(screen.getByText(mockPlanData.title)).toBeInTheDocument();
});

fireEvent.click(screen.getByText("Morning Intention Setting"));
await waitFor(() => {
expect(screen.getByText("Task")).toBeInTheDocument();
});

const taskElements = screen.getAllByText("Morning Intention Setting");
const sidebarSpan = taskElements.find(
(el) => el.tagName === "SPAN" && el.classList.contains("cursor-pointer"),
);
const taskRow = sidebarSpan!.parentElement!;
const dropdownTrigger = taskRow.querySelector(
'[data-slot="dropdown-menu-trigger"]',
) as HTMLElement;
await user.click(dropdownTrigger);

const deleteText = await screen.findByText("Delete");
await user.click(deleteText);

const deleteTaskBtn = await screen.findByText("Delete Task");
await user.click(deleteTaskBtn);

await waitFor(() => {
expect(screen.getByText("Add Task")).toBeInTheDocument();
});
});

it("clears editing task when that task is deleted from sidebar", async () => {
const user = userEvent.setup();
const { default: axiosInstance } = await import("@/config/axios-config");
const mockAxios = axiosInstance as any;
mockAxios.get.mockImplementation((url: string) => {
if (url.includes("/tasks/task1")) {
return Promise.resolve({
data: {
id: "task1",
title: "Morning Intention Setting",
display_order: 1,
estimated_time: 30,
subtasks: [],
},
});
}
return Promise.resolve({ data: { ...mockPlanData, status: "DRAFT" } });
});
mockAxios.delete.mockResolvedValue({ data: {} });

renderWithProviders(<PlanDetailsPage />, true);
await waitFor(() => {
expect(screen.getByText(mockPlanData.title)).toBeInTheDocument();
});

fireEvent.click(screen.getByText("Morning Intention Setting"));
await waitFor(() => {
expect(screen.getByText("Task")).toBeInTheDocument();
});

const editBtn = await screen.findByRole("button", { name: /edit/i });
fireEvent.click(editBtn);
await waitFor(() => {
expect(screen.getByText("Edit Task")).toBeInTheDocument();
});

const taskElements = screen.getAllByText("Morning Intention Setting");
const sidebarSpan = taskElements.find(
(el) => el.tagName === "SPAN" && el.classList.contains("cursor-pointer"),
);
const taskRow = sidebarSpan!.parentElement!;
const dropdownTrigger = taskRow.querySelector(
'[data-slot="dropdown-menu-trigger"]',
) as HTMLElement;
await user.click(dropdownTrigger);

const deleteText = await screen.findByText("Delete");
await user.click(deleteText);

const deleteTaskBtn = await screen.findByText("Delete Task");
await user.click(deleteTaskBtn);

await waitFor(() => {
expect(screen.getByText("Add Task")).toBeInTheDocument();
});
});
});
Loading
Loading