mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-01 18:58:46 -06:00
Compare commits
2 Commits
empty_list
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a59dc8c109 | ||
|
|
975a3a2157 |
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: Guideline for writing end-user facing documentation in the apps/docs folder
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
Follow these instructions and guidelines when asked to write documentation in the apps/docs folder
|
||||
|
||||
Follow this structure to write the title, describtion and pick a matching icon and insert it at the top of the MDX file:
|
||||
|
||||
---
|
||||
title: "FEATURE NAME"
|
||||
description: "1 concise sentence to describe WHEN the feature is being used and FOR WHAT BENEFIT."
|
||||
icon: "link"
|
||||
---
|
||||
|
||||
- Description: 1 concise sentence to describe WHEN the feature is being used and FOR WHAT BENEFIT.
|
||||
- Make ample use of the Mintlify components you can find here https://mintlify.com/docs/llms.txt
|
||||
- In all Headlines, only capitalize the current feature and nothing else, to Camel Case
|
||||
- If a feature is part of the Enterprise Edition, use this note:
|
||||
|
||||
<Note>
|
||||
FEATURE NAME is part of the @Enterprise Edition.
|
||||
</Note>
|
||||
@@ -41,7 +41,7 @@ describe("Survey Builder", () => {
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
shuffleOption: "none",
|
||||
required: false,
|
||||
required: true,
|
||||
});
|
||||
expect(question.choices.length).toBe(3);
|
||||
expect(question.id).toBeDefined();
|
||||
@@ -141,7 +141,7 @@ describe("Survey Builder", () => {
|
||||
inputType: "text",
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
required: false,
|
||||
required: true,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
@@ -204,7 +204,7 @@ describe("Survey Builder", () => {
|
||||
range: 5,
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
required: false,
|
||||
required: true,
|
||||
isColorCodingEnabled: false,
|
||||
});
|
||||
expect(question.id).toBeDefined();
|
||||
@@ -265,7 +265,7 @@ describe("Survey Builder", () => {
|
||||
headline: { default: "NPS Question" },
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
required: false,
|
||||
required: true,
|
||||
isColorCodingEnabled: false,
|
||||
});
|
||||
expect(question.id).toBeDefined();
|
||||
@@ -324,7 +324,7 @@ describe("Survey Builder", () => {
|
||||
label: { default: "I agree to terms" },
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
required: false,
|
||||
required: true,
|
||||
});
|
||||
expect(question.id).toBeDefined();
|
||||
});
|
||||
@@ -377,7 +377,7 @@ describe("Survey Builder", () => {
|
||||
headline: { default: "CTA Question" },
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
required: false,
|
||||
required: true,
|
||||
buttonExternal: false,
|
||||
});
|
||||
expect(question.id).toBeDefined();
|
||||
|
||||
@@ -66,7 +66,7 @@ export const buildMultipleChoiceQuestion = ({
|
||||
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
|
||||
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
|
||||
shuffleOption: shuffleOption || "none",
|
||||
required: required ?? false,
|
||||
required: required ?? true,
|
||||
logic,
|
||||
};
|
||||
};
|
||||
@@ -105,7 +105,7 @@ export const buildOpenTextQuestion = ({
|
||||
headline: createI18nString(headline, []),
|
||||
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
|
||||
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
|
||||
required: required ?? false,
|
||||
required: required ?? true,
|
||||
longAnswer,
|
||||
logic,
|
||||
charLimit: {
|
||||
@@ -153,7 +153,7 @@ export const buildRatingQuestion = ({
|
||||
range,
|
||||
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
|
||||
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
|
||||
required: required ?? false,
|
||||
required: required ?? true,
|
||||
isColorCodingEnabled,
|
||||
lowerLabel: lowerLabel ? createI18nString(lowerLabel, []) : undefined,
|
||||
upperLabel: upperLabel ? createI18nString(upperLabel, []) : undefined,
|
||||
@@ -194,7 +194,7 @@ export const buildNPSQuestion = ({
|
||||
headline: createI18nString(headline, []),
|
||||
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
|
||||
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
|
||||
required: required ?? false,
|
||||
required: required ?? true,
|
||||
isColorCodingEnabled,
|
||||
lowerLabel: lowerLabel ? createI18nString(lowerLabel, []) : undefined,
|
||||
upperLabel: upperLabel ? createI18nString(upperLabel, []) : undefined,
|
||||
@@ -230,7 +230,7 @@ export const buildConsentQuestion = ({
|
||||
headline: createI18nString(headline, []),
|
||||
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
|
||||
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
|
||||
required: required ?? false,
|
||||
required: required ?? true,
|
||||
label: createI18nString(label, []),
|
||||
logic,
|
||||
};
|
||||
@@ -269,7 +269,7 @@ export const buildCTAQuestion = ({
|
||||
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
|
||||
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
|
||||
dismissButtonLabel: dismissButtonLabel ? createI18nString(dismissButtonLabel, []) : undefined,
|
||||
required: required ?? false,
|
||||
required: required ?? true,
|
||||
buttonExternal,
|
||||
buttonUrl,
|
||||
logic,
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"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",
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"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",
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"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",
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"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",
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"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",
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"project_not_found": "找不到專案",
|
||||
"project_permission_not_found": "找不到專案權限",
|
||||
"projects": "專案",
|
||||
"projects_limit_reached": "已達到專案上限",
|
||||
"question": "問題",
|
||||
"question_id": "問題 ID",
|
||||
"questions": "問題",
|
||||
|
||||
@@ -336,7 +336,7 @@ export const getCXQuestionNameMap = (t: TFnType) =>
|
||||
) as Record<TSurveyQuestionTypeEnum, string>;
|
||||
|
||||
export const universalQuestionPresets = {
|
||||
required: false,
|
||||
required: true,
|
||||
};
|
||||
|
||||
export const getQuestionDefaults = (id: string, project: any, t: TFnType) => {
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
// 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 toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { afterEach, 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 }),
|
||||
@@ -50,24 +43,6 @@ 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", () => {
|
||||
@@ -265,245 +240,4 @@ 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"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,13 +71,13 @@ export const SurveyDropDownMenu = ({
|
||||
try {
|
||||
await deleteSurveyAction({ surveyId });
|
||||
deleteSurvey(surveyId);
|
||||
toast.success(t("environments.surveys.survey_deleted_successfully"));
|
||||
router.refresh();
|
||||
setDeleteDialogOpen(false);
|
||||
toast.success(t("environments.surveys.survey_deleted_successfully"));
|
||||
} catch (error) {
|
||||
toast.error(t("environments.surveys.error_deleting_survey"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleCopyLink = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -242,7 +242,6 @@ export const SurveyDropDownMenu = ({
|
||||
setOpen={setDeleteDialogOpen}
|
||||
onDelete={() => handleDeleteSurvey(survey.id)}
|
||||
text={t("environments.surveys.delete_survey_and_responses_warning")}
|
||||
isDeleting={loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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";
|
||||
|
||||
@@ -323,24 +324,6 @@ 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] });
|
||||
|
||||
@@ -123,7 +123,6 @@ 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) => {
|
||||
|
||||
@@ -103,7 +103,7 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
page.locator("#questionCard-3").getByText(surveys.createAndSubmit.ratingQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.locator("path").nth(3).click();
|
||||
|
||||
@@ -115,7 +115,7 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
await expect(
|
||||
page.locator("#questionCard-4").getByText(surveys.createAndSubmit.npsQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
|
||||
for (let i = 0; i < 11; i++) {
|
||||
@@ -135,7 +135,7 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
await expect(page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel)).toBeVisible();
|
||||
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByLabel(surveys.createAndSubmit.consentQuestion.checkboxLabel).check();
|
||||
await page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel).check();
|
||||
await page.locator("#questionCard-6").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Picture Select Question
|
||||
@@ -760,7 +760,7 @@ test.describe("Testing Survey with advanced logic", async () => {
|
||||
page.locator("#questionCard-4").getByText(surveys.createWithLogicAndSubmit.ratingQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByRole("group", { name: "Choices" }).locator("path").nth(3).click();
|
||||
|
||||
@@ -772,7 +772,7 @@ test.describe("Testing Survey with advanced logic", async () => {
|
||||
await expect(
|
||||
page.locator("#questionCard-5").getByText(surveys.createWithLogicAndSubmit.npsQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
await expect(page.locator("#questionCard-5").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-5").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-5").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
|
||||
for (let i = 0; i < 11; i++) {
|
||||
@@ -831,7 +831,7 @@ test.describe("Testing Survey with advanced logic", async () => {
|
||||
).toBeVisible();
|
||||
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByLabel(surveys.createWithLogicAndSubmit.consentQuestion.checkboxLabel).check();
|
||||
await page.getByText(surveys.createWithLogicAndSubmit.consentQuestion.checkboxLabel).check();
|
||||
await page.locator("#questionCard-9").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// File Upload Question
|
||||
|
||||
@@ -418,6 +418,7 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
||||
await page.getByPlaceholder("Option 1").fill(params.singleSelectQuestion.options[0]);
|
||||
await page.getByPlaceholder("Option 2").fill(params.singleSelectQuestion.options[1]);
|
||||
await page.getByRole("button", { name: 'Add "Other"', exact: true }).click();
|
||||
await page.getByLabel("Required").click();
|
||||
|
||||
// Multi Select Question
|
||||
await page
|
||||
@@ -462,6 +463,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
||||
},
|
||||
]);
|
||||
|
||||
await page.getByLabel("Required").click();
|
||||
|
||||
// Rating Question
|
||||
await page
|
||||
.locator("div")
|
||||
@@ -507,6 +510,7 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
||||
await page.getByRole("button", { name: "Add option" }).click();
|
||||
await page.getByPlaceholder("Option 5").click();
|
||||
await page.getByPlaceholder("Option 5").fill(params.ranking.choices[4]);
|
||||
await page.getByLabel("Required").click();
|
||||
|
||||
// Matrix Question
|
||||
await page
|
||||
@@ -545,6 +549,7 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
||||
await page.getByRole("button", { name: "Statement (Call to Action)" }).click();
|
||||
await page.getByPlaceholder("Your question here. Recall").fill(params.ctaQuestion.question);
|
||||
await page.getByPlaceholder("Finish").fill(params.ctaQuestion.buttonLabel);
|
||||
await page.getByLabel("Required").click();
|
||||
|
||||
// Consent Question
|
||||
await page
|
||||
@@ -573,6 +578,7 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Date" }).click();
|
||||
await page.getByLabel("Question*").fill(params.date.question);
|
||||
await page.getByLabel("Required").click();
|
||||
|
||||
// Cal Question
|
||||
await page
|
||||
@@ -582,6 +588,7 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Schedule a meeting" }).click();
|
||||
await page.getByLabel("Question*").fill(params.cal.question);
|
||||
await page.getByLabel("Required").click();
|
||||
|
||||
// Fill Address Question
|
||||
await page
|
||||
@@ -626,8 +633,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
||||
await page.getByRole("option", { name: "secret" }).click();
|
||||
await page.locator("#action-2-operator").click();
|
||||
await page.getByRole("option", { name: "Assign =" }).click();
|
||||
await page.locator("#action-2-value-input").click();
|
||||
await page.locator("#action-2-value-input").fill("1");
|
||||
await page.getByRole("textbox", { name: "Value" }).click();
|
||||
await page.getByRole("textbox", { name: "Value" }).fill("This ");
|
||||
|
||||
// Single Select Question
|
||||
await page.getByRole("heading", { name: params.singleSelectQuestion.question }).click();
|
||||
|
||||
@@ -132,9 +132,10 @@ externalSecret:
|
||||
ingress:
|
||||
annotations:
|
||||
alb.ingress.kubernetes.io/certificate-arn: {{ requiredEnv "FORMBRICKS_INGRESS_CERT_ARN" }}
|
||||
alb.ingress.kubernetes.io/group.name: internal
|
||||
alb.ingress.kubernetes.io/group.name: formbricks-stage
|
||||
alb.ingress.kubernetes.io/healthcheck-path: /health
|
||||
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
|
||||
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||
alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-Res-2021-06
|
||||
alb.ingress.kubernetes.io/ssl-redirect: "443"
|
||||
alb.ingress.kubernetes.io/target-type: ip
|
||||
|
||||
@@ -43,7 +43,7 @@ export function AddressQuestion({
|
||||
currentQuestionId,
|
||||
autoFocusEnabled,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<AddressQuestionProps>) {
|
||||
}: AddressQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
@@ -40,7 +40,7 @@ export function CalQuestion({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<CalQuestionProps>) {
|
||||
}: CalQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
@@ -40,7 +40,7 @@ export function ConsentQuestion({
|
||||
currentQuestionId,
|
||||
autoFocusEnabled,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<ConsentQuestionProps>) {
|
||||
}: ConsentQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
|
||||
@@ -43,7 +43,7 @@ export function ContactInfoQuestion({
|
||||
currentQuestionId,
|
||||
autoFocusEnabled,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<ContactInfoQuestionProps>) {
|
||||
}: ContactInfoQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
@@ -41,7 +41,7 @@ export function CTAQuestion({
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
onOpenExternalURL,
|
||||
}: Readonly<CTAQuestionProps>) {
|
||||
}: CTAQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
|
||||
@@ -94,7 +94,7 @@ export function DateQuestion({
|
||||
ttc,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<DateQuestionProps>) {
|
||||
}: DateQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
@@ -14,21 +14,21 @@ import { FileInput } from "../general/file-input";
|
||||
import { Subheader } from "../general/subheader";
|
||||
|
||||
interface FileUploadQuestionProps {
|
||||
question: TSurveyFileUploadQuestion;
|
||||
value: string[];
|
||||
onChange: (responseData: TResponseData) => void;
|
||||
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
|
||||
onBack: () => void;
|
||||
onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise<string>;
|
||||
isFirstQuestion: boolean;
|
||||
isLastQuestion: boolean;
|
||||
surveyId: string;
|
||||
languageCode: string;
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
autoFocusEnabled: boolean;
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
isBackButtonHidden: boolean;
|
||||
readonly question: TSurveyFileUploadQuestion;
|
||||
readonly value: string[];
|
||||
readonly onChange: (responseData: TResponseData) => void;
|
||||
readonly onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
|
||||
readonly onBack: () => void;
|
||||
readonly onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise<string>;
|
||||
readonly isFirstQuestion: boolean;
|
||||
readonly isLastQuestion: boolean;
|
||||
readonly surveyId: string;
|
||||
readonly languageCode: string;
|
||||
readonly ttc: TResponseTtc;
|
||||
readonly setTtc: (ttc: TResponseTtc) => void;
|
||||
readonly autoFocusEnabled: boolean;
|
||||
readonly currentQuestionId: TSurveyQuestionId;
|
||||
readonly isBackButtonHidden: boolean;
|
||||
}
|
||||
|
||||
export function FileUploadQuestion({
|
||||
@@ -46,7 +46,7 @@ export function FileUploadQuestion({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<FileUploadQuestionProps>) {
|
||||
}: FileUploadQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
@@ -40,7 +40,7 @@ export function MatrixQuestion({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<MatrixQuestionProps>) {
|
||||
}: MatrixQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
@@ -41,7 +41,7 @@ export function MultipleChoiceMultiQuestion({
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<MultipleChoiceMultiProps>) {
|
||||
}: MultipleChoiceMultiProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
@@ -41,7 +41,7 @@ export function MultipleChoiceSingleQuestion({
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<MultipleChoiceSingleProps>) {
|
||||
}: MultipleChoiceSingleProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [otherSelected, setOtherSelected] = useState(false);
|
||||
const otherSpecify = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
@@ -40,7 +40,7 @@ export function NPSQuestion({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<NPSQuestionProps>) {
|
||||
}: NPSQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
@@ -43,7 +43,7 @@ export function OpenTextQuestion({
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<OpenTextQuestionProps>) {
|
||||
}: OpenTextQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [currentLength, setCurrentLength] = useState(value.length || 0);
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
@@ -41,7 +41,7 @@ export function PictureSelectionQuestion({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<PictureSelectionProps>) {
|
||||
}: PictureSelectionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
|
||||
@@ -46,7 +46,7 @@ export function RankingQuestion({
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
}: Readonly<RankingQuestionProps>) {
|
||||
}: RankingQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
const shuffledChoicesIds = useMemo(() => {
|
||||
@@ -189,7 +189,7 @@ export function RankingQuestion({
|
||||
handleItemClick(item);
|
||||
}}
|
||||
type="button"
|
||||
aria-label={`Select ${getLocalizedValue(item.label, languageCode)} for ranking`}
|
||||
aria-label={`Select ${getLocalizedValue(item.label,languageCode)} for ranking`}
|
||||
className="fb-flex fb-gap-x-4 fb-px-4 fb-items-center fb-grow fb-h-full group text-left focus:outline-none">
|
||||
<span
|
||||
className={cn(
|
||||
@@ -207,13 +207,13 @@ export function RankingQuestion({
|
||||
{isSorted ? (
|
||||
<div className="fb-flex fb-flex-col fb-h-full fb-grow-0 fb-border-l fb-border-border">
|
||||
<button
|
||||
tabIndex={isFirst ? -1 : 0}
|
||||
tabIndex={isFirst?-1:0}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleMove(item.id, "up");
|
||||
}}
|
||||
aria-label={`Move ${getLocalizedValue(item.label, languageCode)} up`}
|
||||
aria-label={`Move ${getLocalizedValue(item.label,languageCode)} up`}
|
||||
className={cn(
|
||||
"fb-px-2 fb-flex fb-flex-1 fb-items-center fb-justify-center",
|
||||
isFirst
|
||||
@@ -236,7 +236,7 @@ export function RankingQuestion({
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
tabIndex={isLast ? -1 : 0}
|
||||
tabIndex={isLast?-1:0}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -248,7 +248,7 @@ export function RankingQuestion({
|
||||
? "fb-opacity-30 fb-cursor-not-allowed"
|
||||
: "hover:fb-bg-black/5 fb-rounded-br-custom fb-transition-colors"
|
||||
)}
|
||||
aria-label={`Move ${getLocalizedValue(item.label, languageCode)} down`}
|
||||
aria-label={`Move ${getLocalizedValue(item.label,languageCode)} down`}
|
||||
disabled={isLast}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -6,7 +6,7 @@ interface ScrollableContainerProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export function ScrollableContainer({ children }: Readonly<ScrollableContainerProps>) {
|
||||
export function ScrollableContainer({ children }: ScrollableContainerProps) {
|
||||
const [isAtBottom, setIsAtBottom] = useState(false);
|
||||
const [isAtTop, setIsAtTop] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -30,7 +30,7 @@ export function StackedCardsContainer({
|
||||
setQuestionId,
|
||||
shouldResetQuestionId = true,
|
||||
fullSizeCards = false,
|
||||
}: Readonly<StackedCardsContainerProps>) {
|
||||
}: StackedCardsContainerProps) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const highlightBorderColor = survey.styling?.overwriteThemeStyling
|
||||
? survey.styling?.highlightBorderColor?.light
|
||||
|
||||
@@ -20,7 +20,7 @@ export function SurveyContainer({
|
||||
onClose,
|
||||
clickOutside,
|
||||
isOpen = true,
|
||||
}: Readonly<SurveyContainerProps>) {
|
||||
}: SurveyContainerProps) {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const isCenter = placement === "center";
|
||||
const isModal = mode === "modal";
|
||||
|
||||
Reference in New Issue
Block a user