diff --git a/apps/web/modules/survey/editor/components/survey-variables-card-item.test.tsx b/apps/web/modules/survey/editor/components/survey-variables-card-item.test.tsx
index 0cecba84db..fb21931fa1 100644
--- a/apps/web/modules/survey/editor/components/survey-variables-card-item.test.tsx
+++ b/apps/web/modules/survey/editor/components/survey-variables-card-item.test.tsx
@@ -1,14 +1,37 @@
+import * as utils from "@/modules/survey/editor/lib/utils";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { FormProvider, useForm } from "react-hook-form";
+import toast from "react-hot-toast";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyVariable } from "@formbricks/types/surveys/types";
import { SurveyVariablesCardItem } from "./survey-variables-card-item";
+vi.mock("@/modules/survey/editor/lib/utils", () => {
+ return {
+ findVariableUsedInLogic: vi.fn(),
+ getVariableTypeFromValue: vi.fn().mockImplementation((value) => {
+ if (typeof value === "number") return "number";
+ if (typeof value === "boolean") return "boolean";
+ return "text";
+ }),
+ translateOptions: vi.fn().mockReturnValue([]),
+ validateLogic: vi.fn(),
+ };
+});
+
+vi.mock("react-hot-toast", () => ({
+ default: {
+ error: vi.fn(),
+ success: vi.fn(),
+ },
+}));
+
describe("SurveyVariablesCardItem", () => {
afterEach(() => {
cleanup();
+ vi.resetAllMocks();
});
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
@@ -88,6 +111,62 @@ describe("SurveyVariablesCardItem", () => {
);
});
+ test("should not create a new survey variable when mode is 'create' and the form input is invalid", async () => {
+ const mockSetLocalSurvey = vi.fn();
+ const initialSurvey = {
+ id: "survey123",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ name: "Test Survey",
+ status: "draft",
+ environmentId: "env123",
+ type: "app",
+ welcomeCard: {
+ enabled: true,
+ timeToFinish: false,
+ headline: { default: "Welcome" },
+ buttonLabel: { default: "Start" },
+ showResponseCount: false,
+ },
+ autoClose: null,
+ closeOnDate: null,
+ delay: 0,
+ displayOption: "displayOnce",
+ recontactDays: null,
+ displayLimit: null,
+ runOnDate: null,
+ questions: [],
+ endings: [],
+ hiddenFields: {
+ enabled: true,
+ fieldIds: ["field1", "field2"],
+ },
+ variables: [],
+ } as unknown as TSurvey;
+
+ render(
+
+
+
+ );
+
+ const nameInput = screen.getByPlaceholderText("environments.surveys.edit.field_name_eg_score_price");
+ const valueInput = screen.getByPlaceholderText("environments.surveys.edit.initial_value");
+ const addButton = screen.getByRole("button", { name: "environments.surveys.edit.add_variable" });
+
+ await userEvent.type(nameInput, "1invalidvariablename");
+ await userEvent.type(valueInput, "10");
+ await userEvent.click(addButton);
+
+ const errorMessage = screen.getByText("environments.surveys.edit.variable_name_must_start_with_a_letter");
+ expect(errorMessage).toBeVisible();
+ expect(mockSetLocalSurvey).not.toHaveBeenCalled();
+ });
+
test("should display an error message when the variable name is invalid", async () => {
const mockSetLocalSurvey = vi.fn();
const initialSurvey = {
@@ -244,4 +323,142 @@ describe("SurveyVariablesCardItem", () => {
screen.getByText("environments.surveys.edit.variable_name_is_already_taken_please_choose_another")
).toBeVisible();
});
+
+ test("should show error toast if trying to delete a variable used in logic and not call setLocalSurvey", async () => {
+ const variableUsedInLogic = {
+ id: "logicVarId",
+ name: "logic_variable",
+ type: "text",
+ value: "test_value",
+ } as TSurveyVariable;
+
+ const mockSetLocalSurvey = vi.fn();
+
+ // Mock findVariableUsedInLogic to return 2, indicating the variable is used in logic
+ const findVariableUsedInLogicMock = vi.fn().mockReturnValue(2);
+ vi.spyOn(utils, "findVariableUsedInLogic").mockImplementation(findVariableUsedInLogicMock);
+
+ const initialSurvey = {
+ id: "survey123",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ name: "Test Survey",
+ status: "draft",
+ environmentId: "env123",
+ type: "app",
+ welcomeCard: {
+ enabled: true,
+ timeToFinish: false,
+ headline: { default: "Welcome" },
+ buttonLabel: { default: "Start" },
+ showResponseCount: false,
+ },
+ autoClose: null,
+ closeOnDate: null,
+ delay: 0,
+ displayOption: "displayOnce",
+ recontactDays: null,
+ displayLimit: null,
+ runOnDate: null,
+ questions: [
+ {
+ id: "q1WithLogic",
+ type: "openText",
+ headline: { default: "Question with logic" },
+ required: false,
+ logic: [{ condition: "equals", value: "logicVarId", destination: "q2" }],
+ },
+ { id: "q2", type: "openText", headline: { default: "Q2" }, required: false },
+ ],
+ endings: [],
+ hiddenFields: {
+ enabled: true,
+ fieldIds: ["field1", "field2"],
+ },
+ variables: [variableUsedInLogic],
+ } as unknown as TSurvey;
+
+ render(
+
+ );
+
+ const deleteButton = screen.getByRole("button");
+ await userEvent.click(deleteButton);
+
+ expect(utils.findVariableUsedInLogic).toHaveBeenCalledWith(initialSurvey, variableUsedInLogic.id);
+ expect(mockSetLocalSurvey).not.toHaveBeenCalled();
+ });
+
+ test("should delete variable when it's not used in logic", async () => {
+ const variableToDelete = {
+ id: "recallVarId",
+ name: "recall_variable",
+ type: "text",
+ value: "recall_value",
+ } as TSurveyVariable;
+
+ const mockSetLocalSurvey = vi.fn();
+
+ const findVariableUsedInLogicMock = vi.fn().mockReturnValue(-1);
+ vi.spyOn(utils, "findVariableUsedInLogic").mockImplementation(findVariableUsedInLogicMock);
+
+ const initialSurvey = {
+ id: "survey123",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ name: "Test Survey",
+ status: "draft",
+ environmentId: "env123",
+ type: "app",
+ welcomeCard: {
+ enabled: true,
+ timeToFinish: false,
+ headline: { default: "Welcome" },
+ buttonLabel: { default: "Start" },
+ showResponseCount: false,
+ },
+ autoClose: null,
+ closeOnDate: null,
+ delay: 0,
+ displayOption: "displayOnce",
+ recontactDays: null,
+ displayLimit: null,
+ runOnDate: null,
+ questions: [
+ {
+ id: "q1",
+ type: "openText",
+ headline: { default: "Question with recall:recallVarId in it" },
+ required: false,
+ },
+ ],
+ endings: [],
+ hiddenFields: {
+ enabled: true,
+ fieldIds: ["field1", "field2"],
+ },
+ variables: [variableToDelete],
+ } as unknown as TSurvey;
+
+ render(
+
+ );
+
+ const deleteButton = screen.getByRole("button");
+ await userEvent.click(deleteButton);
+
+ expect(utils.findVariableUsedInLogic).toHaveBeenCalledWith(initialSurvey, variableToDelete.id);
+ expect(mockSetLocalSurvey).toHaveBeenCalledTimes(1);
+ expect(mockSetLocalSurvey).toHaveBeenCalledWith(expect.any(Function));
+ });
});
diff --git a/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx b/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx
index 39fa44ad20..26ef9e7563 100644
--- a/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx
+++ b/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx
@@ -16,7 +16,7 @@ import {
import { createId } from "@paralleldrive/cuid2";
import { useTranslate } from "@tolgee/react";
import { TrashIcon } from "lucide-react";
-import React, { useCallback, useEffect } from "react";
+import React, { useCallback } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { TSurvey, TSurveyVariable } from "@formbricks/types/surveys/types";
@@ -64,7 +64,6 @@ export const SurveyVariablesCardItem = ({
...localSurvey,
variables: [...localSurvey.variables, data],
});
-
form.reset({
id: createId(),
name: "",
@@ -73,26 +72,18 @@ export const SurveyVariablesCardItem = ({
});
};
- useEffect(() => {
- if (mode === "create") {
- return;
- }
+ // Removed auto-submit effect
- const subscription = form.watch(() => form.handleSubmit(editSurveyVariable)());
- return () => subscription.unsubscribe();
- }, [form, mode, editSurveyVariable]);
-
- const onVariableDelete = (variable: TSurveyVariable) => {
+ const onVariableDelete = (variableToDelete: TSurveyVariable) => {
const questions = [...localSurvey.questions];
-
- const quesIdx = findVariableUsedInLogic(localSurvey, variable.id);
+ const quesIdx = findVariableUsedInLogic(localSurvey, variableToDelete.id);
if (quesIdx !== -1) {
toast.error(
t(
"environments.surveys.edit.variable_is_used_in_logic_of_question_please_remove_it_from_logic_first",
{
- variable: variable.name,
+ variable: variableToDelete.name,
questionIndex: quesIdx + 1,
}
)
@@ -100,10 +91,10 @@ export const SurveyVariablesCardItem = ({
return;
}
- // find if this variable is used in any question's recall and remove it for every language
+ // remove recall references
questions.forEach((question) => {
for (const [languageCode, headline] of Object.entries(question.headline)) {
- if (headline.includes(`recall:${variable.id}`)) {
+ if (headline.includes(`recall:${variableToDelete.id}`)) {
const recallInfo = extractRecallInfo(headline);
if (recallInfo) {
question.headline[languageCode] = headline.replace(recallInfo, "");
@@ -113,7 +104,7 @@ export const SurveyVariablesCardItem = ({
});
setLocalSurvey((prevSurvey) => {
- const updatedVariables = prevSurvey.variables.filter((v) => v.id !== variable.id);
+ const updatedVariables = prevSurvey.variables.filter((v) => v.id !== variableToDelete.id);
return { ...prevSurvey, variables: updatedVariables, questions };
});
};
@@ -139,6 +130,7 @@ export const SurveyVariablesCardItem = ({
)}