Compare commits

...

3 Commits

Author SHA1 Message Date
Victor Santos
b894587a20 Added finally block and tests 2025-06-24 06:58:14 -03:00
Victor Santos
8fbff5cdc5 moved logic to survey-list 2025-06-23 10:30:03 -03:00
Varun Singh
d13037cb50 fix: Empty survey list state after deleting the last survey. 2025-06-21 00:38:02 +05:30
10 changed files with 290 additions and 11 deletions

View File

@@ -309,7 +309,6 @@
"project_not_found": "Projekt nicht gefunden",
"project_permission_not_found": "Projekt-Berechtigung nicht gefunden",
"projects": "Projekte",
"projects_limit_reached": "Projektlimit erreicht",
"question": "Frage",
"question_id": "Frage-ID",
"questions": "Fragen",

View File

@@ -309,7 +309,6 @@
"project_not_found": "Project not found",
"project_permission_not_found": "Project permission not found",
"projects": "Projects",
"projects_limit_reached": "Projects limit reached",
"question": "Question",
"question_id": "Question ID",
"questions": "Questions",

View File

@@ -309,7 +309,6 @@
"project_not_found": "Projet non trouvé",
"project_permission_not_found": "Autorisation de projet non trouvée",
"projects": "Projets",
"projects_limit_reached": "Limite de projets atteinte",
"question": "Question",
"question_id": "ID de la question",
"questions": "Questions",

View File

@@ -309,7 +309,6 @@
"project_not_found": "Projeto não encontrado",
"project_permission_not_found": "Permissão do projeto não encontrada",
"projects": "Projetos",
"projects_limit_reached": "Limites de projetos atingidos",
"question": "Pergunta",
"question_id": "ID da Pergunta",
"questions": "Perguntas",

View File

@@ -309,7 +309,6 @@
"project_not_found": "Projeto não encontrado",
"project_permission_not_found": "Permissão do projeto não encontrada",
"projects": "Projetos",
"projects_limit_reached": "Limite de projetos atingido",
"question": "Pergunta",
"question_id": "ID da pergunta",
"questions": "Perguntas",

View File

@@ -309,7 +309,6 @@
"project_not_found": "找不到專案",
"project_permission_not_found": "找不到專案權限",
"projects": "專案",
"projects_limit_reached": "已達到專案上限",
"question": "問題",
"question_id": "問題 ID",
"questions": "問題",

View File

@@ -1,9 +1,16 @@
// Import the actions to access mocked functions
import { deleteSurveyAction } from "@/modules/survey/list/actions";
import { TSurvey } from "@/modules/survey/list/types/surveys";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import toast from "react-hot-toast";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { SurveyDropDownMenu } from "./survey-dropdown-menu";
// Cast to mocked functions
const mockDeleteSurveyAction = vi.mocked(deleteSurveyAction);
const mockToast = vi.mocked(toast);
// Mock translation
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({ t: (key: string) => key }),
@@ -43,6 +50,24 @@ vi.mock("@/modules/survey/list/actions", () => ({
getSurveyAction: vi.fn(() =>
Promise.resolve({ data: { id: "duplicatedSurveyId", name: "Duplicated Survey" } })
),
deleteSurveyAction: vi.fn(),
}));
// Mock next/navigation
const mockRouterRefresh = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({
refresh: mockRouterRefresh,
push: vi.fn(),
}),
}));
// Mock react-hot-toast
vi.mock("react-hot-toast", () => ({
default: {
success: vi.fn(),
error: vi.fn(),
},
}));
describe("SurveyDropDownMenu", () => {
@@ -240,4 +265,245 @@ describe("SurveyDropDownMenu", () => {
expect(mockDuplicateSurvey).toHaveBeenCalled();
});
});
describe("handleDeleteSurvey", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("successfully deletes survey - calls all expected functions and shows success toast", async () => {
const mockDeleteSurvey = vi.fn();
mockDeleteSurveyAction.mockResolvedValueOnce({ data: true });
render(
<SurveyDropDownMenu
environmentId="env123"
survey={fakeSurvey}
publicDomain="http://survey.test"
refreshSingleUseId={vi.fn()}
duplicateSurvey={vi.fn()}
deleteSurvey={mockDeleteSurvey}
/>
);
// Open dropdown and click delete
const menuWrapper = screen.getByTestId("survey-dropdown-menu");
const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement;
await userEvent.click(triggerElement);
const deleteButton = screen.getByText("common.delete");
await userEvent.click(deleteButton);
// Confirm deletion in dialog
const confirmDeleteButton = screen.getByText("common.delete");
await userEvent.click(confirmDeleteButton);
await waitFor(() => {
expect(mockDeleteSurveyAction).toHaveBeenCalledWith({ surveyId: "testSurvey" });
expect(mockDeleteSurvey).toHaveBeenCalledWith("testSurvey");
expect(mockToast.success).toHaveBeenCalledWith("environments.surveys.survey_deleted_successfully");
expect(mockRouterRefresh).toHaveBeenCalled();
});
});
test("handles deletion error - shows error toast and resets loading state", async () => {
const mockDeleteSurvey = vi.fn();
const deletionError = new Error("Deletion failed");
mockDeleteSurveyAction.mockRejectedValueOnce(deletionError);
render(
<SurveyDropDownMenu
environmentId="env123"
survey={fakeSurvey}
publicDomain="http://survey.test"
refreshSingleUseId={vi.fn()}
duplicateSurvey={vi.fn()}
deleteSurvey={mockDeleteSurvey}
/>
);
// Open dropdown and click delete
const menuWrapper = screen.getByTestId("survey-dropdown-menu");
const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement;
await userEvent.click(triggerElement);
const deleteButton = screen.getByText("common.delete");
await userEvent.click(deleteButton);
// Confirm deletion in dialog
const confirmDeleteButton = screen.getByText("common.delete");
await userEvent.click(confirmDeleteButton);
await waitFor(() => {
expect(mockDeleteSurveyAction).toHaveBeenCalledWith({ surveyId: "testSurvey" });
expect(mockDeleteSurvey).not.toHaveBeenCalled();
expect(mockToast.error).toHaveBeenCalledWith("environments.surveys.error_deleting_survey");
expect(mockRouterRefresh).not.toHaveBeenCalled();
});
});
test("manages loading state correctly during successful deletion", async () => {
const mockDeleteSurvey = vi.fn();
mockDeleteSurveyAction.mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve({ data: true }), 100))
);
render(
<SurveyDropDownMenu
environmentId="env123"
survey={fakeSurvey}
publicDomain="http://survey.test"
refreshSingleUseId={vi.fn()}
duplicateSurvey={vi.fn()}
deleteSurvey={mockDeleteSurvey}
/>
);
// Open dropdown and click delete
const menuWrapper = screen.getByTestId("survey-dropdown-menu");
const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement;
await userEvent.click(triggerElement);
const deleteButton = screen.getByText("common.delete");
await userEvent.click(deleteButton);
// Confirm deletion in dialog using a more reliable selector
const confirmDeleteButton = screen.getByText("common.delete");
await userEvent.click(confirmDeleteButton);
// Wait for the deletion process to complete
await waitFor(() => {
expect(mockDeleteSurveyAction).toHaveBeenCalled();
expect(mockDeleteSurvey).toHaveBeenCalled();
expect(mockToast.success).toHaveBeenCalled();
});
});
test("manages loading state correctly during failed deletion", async () => {
const mockDeleteSurvey = vi.fn();
mockDeleteSurveyAction.mockImplementation(
() => new Promise((_, reject) => setTimeout(() => reject(new Error("Network error")), 100))
);
render(
<SurveyDropDownMenu
environmentId="env123"
survey={fakeSurvey}
publicDomain="http://survey.test"
refreshSingleUseId={vi.fn()}
duplicateSurvey={vi.fn()}
deleteSurvey={mockDeleteSurvey}
/>
);
// Open dropdown and click delete
const menuWrapper = screen.getByTestId("survey-dropdown-menu");
const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement;
await userEvent.click(triggerElement);
const deleteButton = screen.getByText("common.delete");
await userEvent.click(deleteButton);
// Confirm deletion in dialog using a more reliable selector
const confirmDeleteButton = screen.getByText("common.delete");
await userEvent.click(confirmDeleteButton);
// Wait for the error to occur
await waitFor(() => {
expect(mockDeleteSurveyAction).toHaveBeenCalled();
expect(mockToast.error).toHaveBeenCalledWith("environments.surveys.error_deleting_survey");
});
// Verify that deleteSurvey callback was not called due to error
expect(mockDeleteSurvey).not.toHaveBeenCalled();
expect(mockRouterRefresh).not.toHaveBeenCalled();
});
test("does not call router.refresh or success toast when deleteSurveyAction throws", async () => {
const mockDeleteSurvey = vi.fn();
mockDeleteSurveyAction.mockRejectedValueOnce(new Error("API Error"));
render(
<SurveyDropDownMenu
environmentId="env123"
survey={fakeSurvey}
publicDomain="http://survey.test"
refreshSingleUseId={vi.fn()}
duplicateSurvey={vi.fn()}
deleteSurvey={mockDeleteSurvey}
/>
);
// Open dropdown and click delete
const menuWrapper = screen.getByTestId("survey-dropdown-menu");
const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement;
await userEvent.click(triggerElement);
const deleteButton = screen.getByText("common.delete");
await userEvent.click(deleteButton);
// Confirm deletion in dialog
const confirmDeleteButton = screen.getByText("common.delete");
await userEvent.click(confirmDeleteButton);
await waitFor(() => {
expect(mockDeleteSurveyAction).toHaveBeenCalled();
expect(mockToast.error).toHaveBeenCalled();
});
// Verify success-path functions are not called
expect(mockDeleteSurvey).not.toHaveBeenCalled();
expect(mockToast.success).not.toHaveBeenCalled();
expect(mockRouterRefresh).not.toHaveBeenCalled();
});
test("calls functions in correct order during successful deletion", async () => {
const mockDeleteSurvey = vi.fn();
const callOrder: string[] = [];
mockDeleteSurveyAction.mockImplementation(async () => {
callOrder.push("deleteSurveyAction");
return { data: true };
});
mockDeleteSurvey.mockImplementation(() => {
callOrder.push("deleteSurvey");
});
(mockToast.success as any).mockImplementation(() => {
callOrder.push("toast.success");
});
mockRouterRefresh.mockImplementation(() => {
callOrder.push("router.refresh");
});
render(
<SurveyDropDownMenu
environmentId="env123"
survey={fakeSurvey}
publicDomain="http://survey.test"
refreshSingleUseId={vi.fn()}
duplicateSurvey={vi.fn()}
deleteSurvey={mockDeleteSurvey}
/>
);
// Open dropdown and click delete
const menuWrapper = screen.getByTestId("survey-dropdown-menu");
const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement;
await userEvent.click(triggerElement);
const deleteButton = screen.getByText("common.delete");
await userEvent.click(deleteButton);
// Confirm deletion in dialog
const confirmDeleteButton = screen.getByText("common.delete");
await userEvent.click(confirmDeleteButton);
await waitFor(() => {
expect(callOrder).toEqual(["deleteSurveyAction", "deleteSurvey", "toast.success", "router.refresh"]);
});
});
});
});

View File

@@ -71,13 +71,13 @@ export const SurveyDropDownMenu = ({
try {
await deleteSurveyAction({ surveyId });
deleteSurvey(surveyId);
router.refresh();
setDeleteDialogOpen(false);
toast.success(t("environments.surveys.survey_deleted_successfully"));
router.refresh();
} catch (error) {
toast.error(t("environments.surveys.error_deleting_survey"));
} finally {
setLoading(false);
}
setLoading(false);
};
const handleCopyLink = async (e: React.MouseEvent<HTMLButtonElement>) => {
@@ -242,6 +242,7 @@ export const SurveyDropDownMenu = ({
setOpen={setDeleteDialogOpen}
onDelete={() => handleDeleteSurvey(survey.id)}
text={t("environments.surveys.delete_survey_and_responses_warning")}
isDeleting={loading}
/>
)}

View File

@@ -10,7 +10,6 @@ import { TProjectConfigChannel } from "@formbricks/types/project";
import { TSurveyFilters } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { SurveyCard } from "./survey-card";
import { SurveyFilters } from "./survey-filters";
import { SurveysList, initialFilters as surveyFiltersInitialFiltersFromModule } from "./survey-list";
import { SurveyLoading } from "./survey-loading";
@@ -324,6 +323,24 @@ describe("SurveysList", () => {
expect(screen.getByText("Survey Two")).toBeInTheDocument();
});
test("handleDeleteSurvey shows loading state when the last survey is deleted", async () => {
const surveysData = [{ ...surveyMock, id: "s1", name: "Last Survey" }];
vi.mocked(getSurveysAction).mockResolvedValueOnce({ data: surveysData });
const user = userEvent.setup();
render(<SurveysList {...defaultProps} />);
await waitFor(() => expect(screen.getByText("Last Survey")).toBeInTheDocument());
expect(screen.queryByTestId("survey-loading")).not.toBeInTheDocument();
const deleteButtonS1 = screen.getByTestId("delete-s1");
await user.click(deleteButtonS1);
await waitFor(() => {
expect(screen.queryByText("Last Survey")).not.toBeInTheDocument();
expect(screen.getByTestId("survey-loading")).toBeInTheDocument();
});
});
test("handleDuplicateSurvey adds the duplicated survey to the beginning of the list", async () => {
const initialSurvey = { ...surveyMock, id: "s1", name: "Original Survey" };
vi.mocked(getSurveysAction).mockResolvedValueOnce({ data: [initialSurvey] });

View File

@@ -123,6 +123,7 @@ export const SurveysList = ({
const handleDeleteSurvey = async (surveyId: string) => {
const newSurveys = surveys.filter((survey) => survey.id !== surveyId);
setSurveys(newSurveys);
if (newSurveys.length === 0) setIsFetching(true);
};
const handleDuplicateSurvey = async (survey: TSurvey) => {