mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 09:00:18 -06:00
chore: add tests to survey editor components - part 2 (#5575)
Co-authored-by: use-tusk[bot] <144006087+use-tusk[bot]@users.noreply.github.com> Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
335
apps/web/modules/survey/editor/components/cal-question-form.test.tsx
Executable file
335
apps/web/modules/survey/editor/components/cal-question-form.test.tsx
Executable file
@@ -0,0 +1,335 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey, TSurveyCalQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { CalQuestionForm } from "./cal-question-form";
|
||||
|
||||
// Mock necessary modules and components
|
||||
vi.mock("@/modules/ui/components/advanced-option-toggle", () => ({
|
||||
AdvancedOptionToggle: ({
|
||||
isChecked,
|
||||
onToggle,
|
||||
htmlId,
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
isChecked: boolean;
|
||||
onToggle?: (checked: boolean) => void;
|
||||
htmlId?: string;
|
||||
children?: React.ReactNode;
|
||||
title?: string;
|
||||
}) => {
|
||||
let content;
|
||||
if (onToggle && htmlId) {
|
||||
content = (
|
||||
<input
|
||||
type="checkbox"
|
||||
id={htmlId}
|
||||
checked={isChecked}
|
||||
onChange={() => onToggle(!isChecked)}
|
||||
data-testid="cal-host-toggle"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = isChecked ? "Enabled" : "Disabled";
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="advanced-option-toggle">
|
||||
{htmlId && title ? <label htmlFor={htmlId}>{title}</label> : null}
|
||||
{content}
|
||||
{isChecked && children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// Updated Input mock to use id prop correctly
|
||||
vi.mock("@/modules/ui/components/input", () => ({
|
||||
Input: ({
|
||||
id,
|
||||
onChange,
|
||||
value,
|
||||
}: {
|
||||
id: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
value: string;
|
||||
}) => (
|
||||
<input
|
||||
id={id} // Ensure the input has the ID the label points to
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/components/question-form-input", () => ({
|
||||
QuestionFormInput: ({
|
||||
id,
|
||||
value,
|
||||
label,
|
||||
localSurvey,
|
||||
questionIdx,
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
locale,
|
||||
}: any) => (
|
||||
<div data-testid="question-form-input">
|
||||
{id
|
||||
? `${id} - ${value?.default} - ${label} - ${localSurvey.id} - ${questionIdx} - ${isInvalid.toString()} - ${selectedLanguageCode} - ${locale}`
|
||||
: ""}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("CalQuestionForm", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("should initialize isCalHostEnabled to true if question.calHost is defined", () => {
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const mockSetSelectedLanguageCode = vi.fn();
|
||||
|
||||
const mockQuestion = {
|
||||
id: "cal_question_1",
|
||||
type: TSurveyQuestionTypeEnum.Cal,
|
||||
headline: { default: "Book a meeting" },
|
||||
calUserName: "testuser",
|
||||
calHost: "cal.com",
|
||||
} as unknown as TSurveyCalQuestion;
|
||||
|
||||
const mockLocalSurvey: TSurvey = {
|
||||
id: "survey_123",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env_123",
|
||||
status: "draft",
|
||||
questions: [],
|
||||
languages: [
|
||||
{
|
||||
id: "lang_1",
|
||||
default: true,
|
||||
enabled: true,
|
||||
language: {
|
||||
id: "en",
|
||||
code: "en",
|
||||
name: "English",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
alias: null,
|
||||
projectId: "project_123",
|
||||
},
|
||||
},
|
||||
],
|
||||
endings: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<CalQuestionForm
|
||||
localSurvey={mockLocalSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
isInvalid={false}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Assert that the AdvancedOptionToggle component is rendered with isChecked prop set to true
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveTextContent(
|
||||
"environments.surveys.edit.custom_hostname"
|
||||
);
|
||||
});
|
||||
|
||||
test("should set calHost to undefined when isCalHostEnabled is toggled off", async () => {
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const mockSetSelectedLanguageCode = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const mockQuestion = {
|
||||
id: "cal_question_1",
|
||||
type: TSurveyQuestionTypeEnum.Cal,
|
||||
headline: { default: "Book a meeting" },
|
||||
calUserName: "testuser",
|
||||
calHost: "cal.com",
|
||||
} as unknown as TSurveyCalQuestion;
|
||||
|
||||
const mockLocalSurvey: TSurvey = {
|
||||
id: "survey_123",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env_123",
|
||||
status: "draft",
|
||||
questions: [],
|
||||
languages: [
|
||||
{
|
||||
id: "lang_1",
|
||||
default: true,
|
||||
enabled: true,
|
||||
language: {
|
||||
id: "en",
|
||||
code: "en",
|
||||
name: "English",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
alias: null,
|
||||
projectId: "project_123",
|
||||
},
|
||||
},
|
||||
],
|
||||
endings: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<CalQuestionForm
|
||||
localSurvey={mockLocalSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
isInvalid={false}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find the toggle and click it to disable calHost
|
||||
const toggle = screen.getByTestId("cal-host-toggle");
|
||||
await user.click(toggle);
|
||||
|
||||
// Assert that updateQuestion is called with calHost: undefined
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledWith(0, { calHost: undefined });
|
||||
});
|
||||
|
||||
test("should render QuestionFormInput for the headline field with the correct props", () => {
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const mockSetSelectedLanguageCode = vi.fn();
|
||||
|
||||
const mockQuestion = {
|
||||
id: "cal_question_1",
|
||||
type: TSurveyQuestionTypeEnum.Cal,
|
||||
headline: { default: "Book a meeting" },
|
||||
calUserName: "testuser",
|
||||
calHost: "cal.com",
|
||||
} as unknown as TSurveyCalQuestion;
|
||||
|
||||
const mockLocalSurvey = {
|
||||
id: "survey_123",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env_123",
|
||||
status: "draft",
|
||||
questions: [],
|
||||
languages: [
|
||||
{
|
||||
id: "lang_1",
|
||||
default: true,
|
||||
enabled: true,
|
||||
language: {
|
||||
id: "en",
|
||||
code: "en",
|
||||
name: "English",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
alias: null,
|
||||
projectId: "project_123",
|
||||
},
|
||||
},
|
||||
],
|
||||
endings: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<CalQuestionForm
|
||||
localSurvey={mockLocalSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
isInvalid={false}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Assert that the QuestionFormInput component is rendered with the correct props
|
||||
expect(screen.getByTestId("question-form-input")).toHaveTextContent(
|
||||
"headline - Book a meeting - environments.surveys.edit.question* - survey_123 - 0 - false - en - en-US"
|
||||
);
|
||||
});
|
||||
|
||||
test("should call updateQuestion with an empty calUserName when the input is cleared", async () => {
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const mockSetSelectedLanguageCode = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const mockQuestion = {
|
||||
id: "cal_question_1",
|
||||
type: TSurveyQuestionTypeEnum.Cal,
|
||||
headline: { default: "Book a meeting" },
|
||||
calUserName: "testuser",
|
||||
calHost: "cal.com",
|
||||
} as unknown as TSurveyCalQuestion;
|
||||
|
||||
const mockLocalSurvey = {
|
||||
id: "survey_123",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env_123",
|
||||
status: "draft",
|
||||
questions: [],
|
||||
languages: [
|
||||
{
|
||||
id: "lang_1",
|
||||
default: true,
|
||||
enabled: true,
|
||||
language: {
|
||||
id: "en",
|
||||
code: "en",
|
||||
name: "English",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
alias: null,
|
||||
projectId: "project_123",
|
||||
},
|
||||
},
|
||||
],
|
||||
endings: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<CalQuestionForm
|
||||
localSurvey={mockLocalSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
isInvalid={false}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const calUserNameInput = screen.getByLabelText("environments.surveys.edit.cal_username", {
|
||||
selector: "input",
|
||||
});
|
||||
await user.clear(calUserNameInput);
|
||||
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledWith(0, { calUserName: "" });
|
||||
});
|
||||
});
|
||||
233
apps/web/modules/survey/editor/components/conditional-logic.test.tsx
Executable file
233
apps/web/modules/survey/editor/components/conditional-logic.test.tsx
Executable file
@@ -0,0 +1,233 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeAll, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLogic,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { ConditionalLogic } from "./conditional-logic";
|
||||
|
||||
// Mock @formkit/auto-animate - simplify implementation
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [null],
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/surveyLogic/utils", () => ({
|
||||
duplicateLogicItem: (logicItem: TSurveyLogic) => ({
|
||||
...logicItem,
|
||||
id: "new-duplicated-id",
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./logic-editor", () => ({
|
||||
LogicEditor: () => <div data-testid="logic-editor">LogicEditor</div>,
|
||||
}));
|
||||
|
||||
describe("ConditionalLogic", () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("should add a new logic condition to the question's logic array when the add logic button is clicked", async () => {
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const mockQuestion: TSurveyQuestion = {
|
||||
id: "testQuestionId",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Test Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
const mockSurvey = {
|
||||
id: "testSurveyId",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "testEnvId",
|
||||
status: "inProgress",
|
||||
questions: [mockQuestion],
|
||||
endings: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<ConditionalLogic
|
||||
localSurvey={mockSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
/>
|
||||
);
|
||||
|
||||
const addLogicButton = screen.getByRole("button", { name: "environments.surveys.edit.add_logic" });
|
||||
await userEvent.click(addLogicButton);
|
||||
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledTimes(1);
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledWith(0, {
|
||||
logic: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
conditions: expect.objectContaining({
|
||||
connector: "and",
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
leftOperand: expect.objectContaining({
|
||||
value: "testQuestionId",
|
||||
type: "question",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
objective: "jumpToQuestion",
|
||||
target: "",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
test("should duplicate the specified logic condition and insert it into the logic array", async () => {
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const initialLogic: TSurveyLogic = {
|
||||
id: "initialLogicId",
|
||||
conditions: {
|
||||
id: "conditionGroupId",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: [],
|
||||
};
|
||||
const mockQuestion: TSurveyQuestion = {
|
||||
id: "testQuestionId",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Test Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
logic: [initialLogic],
|
||||
};
|
||||
const mockSurvey = {
|
||||
id: "testSurveyId",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "testEnvId",
|
||||
status: "inProgress",
|
||||
questions: [mockQuestion],
|
||||
endings: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<ConditionalLogic
|
||||
localSurvey={mockSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
/>
|
||||
);
|
||||
|
||||
// First click the ellipsis menu button to open the dropdown
|
||||
const menuButton = screen.getByRole("button", {
|
||||
name: "", // The button has no text content, just an icon
|
||||
});
|
||||
await userEvent.click(menuButton);
|
||||
|
||||
// Now look for the duplicate option in the dropdown menu that appears
|
||||
const duplicateButton = await screen.findByText("common.duplicate");
|
||||
await userEvent.click(duplicateButton);
|
||||
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledTimes(1);
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledWith(0, {
|
||||
logic: expect.arrayContaining([
|
||||
initialLogic,
|
||||
expect.objectContaining({
|
||||
id: "new-duplicated-id",
|
||||
conditions: initialLogic.conditions,
|
||||
actions: initialLogic.actions,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
test("should render the list of logic conditions and their associated actions based on the question's logic data", () => {
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const mockLogic: TSurveyLogic[] = [
|
||||
{
|
||||
id: "logic1",
|
||||
conditions: {
|
||||
id: "cond1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
{
|
||||
id: "logic2",
|
||||
conditions: {
|
||||
id: "cond2",
|
||||
connector: "or",
|
||||
conditions: [],
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
const mockQuestion: TSurveyQuestion = {
|
||||
id: "testQuestionId",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Test Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
logic: mockLogic,
|
||||
};
|
||||
const mockSurvey = {
|
||||
id: "testSurveyId",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "testEnvId",
|
||||
status: "inProgress",
|
||||
questions: [mockQuestion],
|
||||
endings: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<ConditionalLogic
|
||||
localSurvey={mockSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getAllByTestId("logic-editor").length).toBe(2);
|
||||
});
|
||||
});
|
||||
68
apps/web/modules/survey/editor/components/consent-question-form.test.tsx
Executable file
68
apps/web/modules/survey/editor/components/consent-question-form.test.tsx
Executable file
@@ -0,0 +1,68 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey, TSurveyConsentQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { ConsentQuestionForm } from "./consent-question-form";
|
||||
|
||||
vi.mock("@/modules/survey/components/question-form-input", () => ({
|
||||
QuestionFormInput: ({ label }: { label: string }) => <div data-testid="question-form-input">{label}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/multi-language-surveys/components/localized-editor", () => ({
|
||||
LocalizedEditor: ({ id }: { id: string }) => <div data-testid="localized-editor">{id}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/label", () => ({
|
||||
Label: ({ children }: { children: string }) => <div data-testid="label">{children}</div>,
|
||||
}));
|
||||
|
||||
describe("ConsentQuestionForm", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the form with headline, description, and checkbox label when provided valid props", () => {
|
||||
const mockQuestion = {
|
||||
id: "consent1",
|
||||
type: TSurveyQuestionTypeEnum.Consent,
|
||||
headline: { en: "Consent Headline" },
|
||||
html: { en: "Consent Description" },
|
||||
label: { en: "Consent Checkbox Label" },
|
||||
} as unknown as TSurveyConsentQuestion;
|
||||
|
||||
const mockLocalSurvey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
status: "draft",
|
||||
questions: [],
|
||||
languages: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const mockSetSelectedLanguageCode = vi.fn();
|
||||
const mockLocale: TUserLocale = "en-US";
|
||||
|
||||
render(
|
||||
<ConsentQuestionForm
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
isInvalid={false}
|
||||
localSurvey={mockLocalSurvey}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
const questionFormInputs = screen.getAllByTestId("question-form-input");
|
||||
expect(questionFormInputs[0]).toHaveTextContent("environments.surveys.edit.question*");
|
||||
expect(screen.getByTestId("label")).toHaveTextContent("common.description");
|
||||
expect(screen.getByTestId("localized-editor")).toHaveTextContent("subheader");
|
||||
expect(questionFormInputs[1]).toHaveTextContent("environments.surveys.edit.checkbox_label*");
|
||||
});
|
||||
});
|
||||
262
apps/web/modules/survey/editor/components/contact-info-question-form.test.tsx
Executable file
262
apps/web/modules/survey/editor/components/contact-info-question-form.test.tsx
Executable file
@@ -0,0 +1,262 @@
|
||||
import { createI18nString } from "@/lib/i18n/utils";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyContactInfoQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { ContactInfoQuestionForm } from "./contact-info-question-form";
|
||||
|
||||
// Mock QuestionFormInput component
|
||||
vi.mock("@/modules/survey/components/question-form-input", () => ({
|
||||
QuestionFormInput: vi.fn(({ id, label, value, selectedLanguageCode }) => (
|
||||
<div data-testid="question-form-input">
|
||||
<label data-testid="question-form-input-label">{label}</label>
|
||||
<div data-testid={`question-form-input-${id}`}>
|
||||
{selectedLanguageCode ? value?.[selectedLanguageCode] || "" : value?.default || ""}
|
||||
</div>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock QuestionToggleTable component
|
||||
vi.mock("@/modules/ui/components/question-toggle-table", () => ({
|
||||
QuestionToggleTable: vi.fn(({ fields }) => (
|
||||
<div data-testid="question-toggle-table">
|
||||
{fields?.map((field) => (
|
||||
<div key={field.id} data-testid={`question-toggle-table-field-${field.id}`}>
|
||||
{field.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock the Button component
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, onClick }) => (
|
||||
<button data-testid="add-description-button" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock @formkit/auto-animate - simplify implementation
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [null],
|
||||
}));
|
||||
|
||||
describe("ContactInfoQuestionForm", () => {
|
||||
let mockSurvey: TSurvey;
|
||||
let mockQuestion: TSurveyContactInfoQuestion;
|
||||
let updateQuestionMock: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock window.matchMedia
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
mockSurvey = {
|
||||
id: "survey-1",
|
||||
name: "Test Survey",
|
||||
questions: [],
|
||||
languages: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
mockQuestion = {
|
||||
id: "contact-info-1",
|
||||
type: TSurveyQuestionTypeEnum.ContactInfo,
|
||||
headline: createI18nString("Headline Text", ["en"]),
|
||||
required: true,
|
||||
firstName: { show: true, required: false, placeholder: createI18nString("", ["en"]) },
|
||||
lastName: { show: true, required: false, placeholder: createI18nString("", ["en"]) },
|
||||
email: { show: true, required: false, placeholder: createI18nString("", ["en"]) },
|
||||
phone: { show: true, required: false, placeholder: createI18nString("", ["en"]) },
|
||||
company: { show: true, required: false, placeholder: createI18nString("", ["en"]) },
|
||||
} as unknown as TSurveyContactInfoQuestion;
|
||||
|
||||
updateQuestionMock = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("should update required to false when all fields are visible but optional", () => {
|
||||
render(
|
||||
<ContactInfoQuestionForm
|
||||
localSurvey={mockSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={updateQuestionMock}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(updateQuestionMock).toHaveBeenCalledWith(0, { required: false });
|
||||
});
|
||||
|
||||
test("should update required to true when all fields are visible and at least one is required", () => {
|
||||
mockQuestion = {
|
||||
...mockQuestion,
|
||||
firstName: { show: true, required: true, placeholder: createI18nString("", ["en"]) },
|
||||
} as unknown as TSurveyContactInfoQuestion;
|
||||
|
||||
render(
|
||||
<ContactInfoQuestionForm
|
||||
localSurvey={mockSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={updateQuestionMock}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(updateQuestionMock).toHaveBeenCalledWith(0, { required: true });
|
||||
});
|
||||
|
||||
test("should update required to false when all fields are hidden", () => {
|
||||
mockQuestion = {
|
||||
...mockQuestion,
|
||||
firstName: { show: false, required: false, placeholder: createI18nString("", ["en"]) },
|
||||
lastName: { show: false, required: false, placeholder: createI18nString("", ["en"]) },
|
||||
email: { show: false, required: false, placeholder: createI18nString("", ["en"]) },
|
||||
phone: { show: false, required: false, placeholder: createI18nString("", ["en"]) },
|
||||
company: { show: false, required: false, placeholder: createI18nString("", ["en"]) },
|
||||
} as unknown as TSurveyContactInfoQuestion;
|
||||
|
||||
render(
|
||||
<ContactInfoQuestionForm
|
||||
localSurvey={mockSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={updateQuestionMock}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(updateQuestionMock).toHaveBeenCalledWith(0, { required: false });
|
||||
});
|
||||
|
||||
test("should display the subheader input field when the subheader property is defined", () => {
|
||||
const mockQuestionWithSubheader: TSurveyContactInfoQuestion = {
|
||||
...mockQuestion,
|
||||
subheader: createI18nString("Subheader Text", ["en"]), // Define subheader
|
||||
} as unknown as TSurveyContactInfoQuestion;
|
||||
|
||||
render(
|
||||
<ContactInfoQuestionForm
|
||||
localSurvey={mockSurvey}
|
||||
question={mockQuestionWithSubheader}
|
||||
questionIdx={0}
|
||||
updateQuestion={vi.fn()}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const subheaderInput = screen.getByTestId("question-form-input-subheader");
|
||||
expect(subheaderInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should display the 'Add Description' button when subheader is undefined", () => {
|
||||
const mockQuestionWithoutSubheader: TSurveyContactInfoQuestion = {
|
||||
...mockQuestion,
|
||||
subheader: undefined,
|
||||
} as unknown as TSurveyContactInfoQuestion;
|
||||
|
||||
render(
|
||||
<ContactInfoQuestionForm
|
||||
localSurvey={mockSurvey}
|
||||
question={mockQuestionWithoutSubheader}
|
||||
questionIdx={0}
|
||||
updateQuestion={updateQuestionMock}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const addButton = screen.getByTestId("add-description-button");
|
||||
expect(addButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should handle gracefully when selectedLanguageCode is not in translations", () => {
|
||||
render(
|
||||
<ContactInfoQuestionForm
|
||||
localSurvey={mockSurvey}
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={vi.fn()}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="fr"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const headlineValue = screen.getByTestId("question-form-input-headline");
|
||||
expect(headlineValue).toBeInTheDocument();
|
||||
expect(headlineValue).toHaveTextContent(""); // Expect empty string since "fr" is not in headline translations
|
||||
});
|
||||
|
||||
test("should handle a question object with a new or custom field", () => {
|
||||
const mockQuestionWithCustomField: TSurveyContactInfoQuestion = {
|
||||
...mockQuestion,
|
||||
// Add a custom field with an unexpected structure
|
||||
customField: { value: "Custom Value" },
|
||||
} as unknown as TSurveyContactInfoQuestion;
|
||||
|
||||
render(
|
||||
<ContactInfoQuestionForm
|
||||
localSurvey={mockSurvey}
|
||||
question={mockQuestionWithCustomField}
|
||||
questionIdx={0}
|
||||
updateQuestion={vi.fn()}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Assert that the component renders without errors
|
||||
const headlineValue = screen.getByTestId("question-form-input-headline");
|
||||
expect(headlineValue).toBeInTheDocument();
|
||||
|
||||
// Assert that the QuestionToggleTable is rendered
|
||||
const toggleTable = screen.getByTestId("question-toggle-table");
|
||||
expect(toggleTable).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ActionClass } from "@prisma/client";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { CreateNewActionTab } from "./create-new-action-tab";
|
||||
|
||||
// Mock the NoCodeActionForm and CodeActionForm components
|
||||
vi.mock("@/modules/ui/components/no-code-action-form", () => ({
|
||||
NoCodeActionForm: () => <div data-testid="no-code-action-form">NoCodeActionForm</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/code-action-form", () => ({
|
||||
CodeActionForm: () => <div data-testid="code-action-form">CodeActionForm</div>,
|
||||
}));
|
||||
|
||||
// Mock constants
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
FORMBRICKS_API_HOST: "http://localhost:3000",
|
||||
FORMBRICKS_ENVIRONMENT_ID: "test-env-id",
|
||||
}));
|
||||
|
||||
// Mock the createActionClassAction function
|
||||
vi.mock("../actions", () => ({
|
||||
createActionClassAction: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("CreateNewActionTab", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders all expected fields and UI elements when provided with valid props", () => {
|
||||
const actionClasses: ActionClass[] = [];
|
||||
const setActionClasses = vi.fn();
|
||||
const setOpen = vi.fn();
|
||||
const isReadOnly = false;
|
||||
const setLocalSurvey = vi.fn();
|
||||
const environmentId = "test-env-id";
|
||||
|
||||
render(
|
||||
<CreateNewActionTab
|
||||
actionClasses={actionClasses}
|
||||
setActionClasses={setActionClasses}
|
||||
setOpen={setOpen}
|
||||
isReadOnly={isReadOnly}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check for the presence of key UI elements
|
||||
expect(screen.getByText("environments.actions.action_type")).toBeInTheDocument();
|
||||
expect(screen.getByRole("radio", { name: "common.no_code" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("radio", { name: "common.code" })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("environments.actions.what_did_your_user_do")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("common.description")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("no-code-action-form")).toBeInTheDocument(); // Ensure NoCodeActionForm is rendered by default
|
||||
});
|
||||
});
|
||||
75
apps/web/modules/survey/editor/components/cta-question-form.test.tsx
Executable file
75
apps/web/modules/survey/editor/components/cta-question-form.test.tsx
Executable file
@@ -0,0 +1,75 @@
|
||||
import { createI18nString } from "@/lib/i18n/utils";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey, TSurveyCTAQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { CTAQuestionForm } from "./cta-question-form";
|
||||
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [null],
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/components/question-form-input", () => ({
|
||||
QuestionFormInput: () => <div data-testid="question-form-input">QuestionFormInput</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/multi-language-surveys/components/localized-editor", () => ({
|
||||
LocalizedEditor: () => <div data-testid="localized-editor">LocalizedEditor</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/options-switch", () => ({
|
||||
OptionsSwitch: () => <div data-testid="options-switch">OptionsSwitch</div>,
|
||||
}));
|
||||
|
||||
describe("CTAQuestionForm", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders all required fields and components when provided with valid props", () => {
|
||||
const mockQuestion: TSurveyCTAQuestion = {
|
||||
id: "test-question",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
headline: createI18nString("Test Headline", ["en"]),
|
||||
buttonLabel: createI18nString("Next", ["en"]),
|
||||
backButtonLabel: createI18nString("Back", ["en"]),
|
||||
buttonExternal: false,
|
||||
buttonUrl: "",
|
||||
required: true,
|
||||
};
|
||||
|
||||
const mockLocalSurvey = {
|
||||
id: "test-survey",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "test-env",
|
||||
status: "draft",
|
||||
questions: [],
|
||||
languages: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const mockSetSelectedLanguageCode = vi.fn();
|
||||
const mockLocale = "en-US";
|
||||
|
||||
render(
|
||||
<CTAQuestionForm
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
lastQuestion={false}
|
||||
isInvalid={false}
|
||||
localSurvey={mockLocalSurvey}
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
const questionFormInputs = screen.getAllByTestId("question-form-input");
|
||||
expect(questionFormInputs.length).toBe(2);
|
||||
expect(screen.getByTestId("localized-editor")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("options-switch")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
69
apps/web/modules/survey/editor/components/edit-ending-card.test.tsx
Executable file
69
apps/web/modules/survey/editor/components/edit-ending-card.test.tsx
Executable file
@@ -0,0 +1,69 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
import { TLanguage } from "@formbricks/types/project";
|
||||
import { TSurvey, TSurveyEndScreenCard, TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { EditEndingCard } from "./edit-ending-card";
|
||||
|
||||
vi.mock("./end-screen-form", () => ({
|
||||
EndScreenForm: vi.fn(() => <div data-testid="end-screen-form">EndScreenForm</div>),
|
||||
}));
|
||||
|
||||
describe("EditEndingCard", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("should render the EndScreenForm when the ending card type is 'endScreen'", () => {
|
||||
const endingCardId = "ending1";
|
||||
const localSurvey = {
|
||||
id: "testSurvey",
|
||||
name: "Test Survey",
|
||||
languages: [
|
||||
{ language: { code: "en", name: "English" } as unknown as TLanguage } as unknown as TSurveyLanguage,
|
||||
],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "link",
|
||||
questions: [],
|
||||
endings: [
|
||||
{
|
||||
id: endingCardId,
|
||||
type: "endScreen",
|
||||
headline: { en: "Thank you!" },
|
||||
} as TSurveyEndScreenCard,
|
||||
],
|
||||
followUps: [],
|
||||
welcomeCard: { enabled: false, headline: { en: "" } } as unknown as TSurvey["welcomeCard"],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const setLocalSurvey = vi.fn();
|
||||
const setActiveQuestionId = vi.fn();
|
||||
const selectedLanguageCode = "en";
|
||||
const setSelectedLanguageCode = vi.fn();
|
||||
const plan: TOrganizationBillingPlan = "free";
|
||||
const addEndingCard = vi.fn();
|
||||
const isFormbricksCloud = false;
|
||||
const locale: TUserLocale = "en-US";
|
||||
|
||||
render(
|
||||
<EditEndingCard
|
||||
localSurvey={localSurvey}
|
||||
endingCardIndex={0}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
activeQuestionId={endingCardId}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
plan={plan}
|
||||
addEndingCard={addEndingCard}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("end-screen-form")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
159
apps/web/modules/survey/editor/components/editor-card-menu.test.tsx
Executable file
159
apps/web/modules/survey/editor/components/editor-card-menu.test.tsx
Executable file
@@ -0,0 +1,159 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { EditorCardMenu } from "./editor-card-menu";
|
||||
|
||||
describe("EditorCardMenu", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("should move the card up when the 'Move Up' button is clicked and the card is not the first one", async () => {
|
||||
const moveCard = vi.fn();
|
||||
const cardIdx = 1;
|
||||
|
||||
render(
|
||||
<EditorCardMenu
|
||||
survey={{ questions: [] } as any}
|
||||
cardIdx={cardIdx}
|
||||
lastCard={false}
|
||||
duplicateCard={vi.fn()}
|
||||
deleteCard={vi.fn()}
|
||||
moveCard={moveCard}
|
||||
card={{ type: "openText" } as any}
|
||||
updateCard={vi.fn()}
|
||||
addCard={vi.fn()}
|
||||
cardType="question"
|
||||
/>
|
||||
);
|
||||
|
||||
const moveUpButton = screen.getAllByRole("button")[0];
|
||||
await userEvent.click(moveUpButton);
|
||||
|
||||
expect(moveCard).toHaveBeenCalledWith(cardIdx, true);
|
||||
});
|
||||
|
||||
test("should move the card down when the 'Move Down' button is clicked and the card is not the last one", async () => {
|
||||
const moveCard = vi.fn();
|
||||
const cardIdx = 0;
|
||||
|
||||
render(
|
||||
<EditorCardMenu
|
||||
survey={{ questions: [] } as any}
|
||||
cardIdx={cardIdx}
|
||||
lastCard={false}
|
||||
duplicateCard={vi.fn()}
|
||||
deleteCard={vi.fn()}
|
||||
moveCard={moveCard}
|
||||
card={{ type: "openText" } as any}
|
||||
updateCard={vi.fn()}
|
||||
addCard={vi.fn()}
|
||||
cardType="question"
|
||||
/>
|
||||
);
|
||||
|
||||
const moveDownButton = screen.getAllByRole("button")[1];
|
||||
await userEvent.click(moveDownButton);
|
||||
|
||||
expect(moveCard).toHaveBeenCalledWith(cardIdx, false);
|
||||
});
|
||||
|
||||
test("should duplicate the card when the 'Duplicate' button is clicked", async () => {
|
||||
const duplicateCard = vi.fn();
|
||||
const cardIdx = 1;
|
||||
|
||||
render(
|
||||
<EditorCardMenu
|
||||
survey={{ questions: [] } as any}
|
||||
cardIdx={cardIdx}
|
||||
lastCard={false}
|
||||
duplicateCard={duplicateCard}
|
||||
deleteCard={vi.fn()}
|
||||
moveCard={vi.fn()}
|
||||
card={{ type: "openText" } as any}
|
||||
updateCard={vi.fn()}
|
||||
addCard={vi.fn()}
|
||||
cardType="question"
|
||||
/>
|
||||
);
|
||||
|
||||
const duplicateButton = screen.getAllByRole("button")[2];
|
||||
await userEvent.click(duplicateButton);
|
||||
|
||||
expect(duplicateCard).toHaveBeenCalledWith(cardIdx);
|
||||
});
|
||||
|
||||
test("should disable the delete button when the card is the only one left in the survey", () => {
|
||||
const survey = {
|
||||
questions: [{ id: "1", type: "openText" }],
|
||||
type: "link",
|
||||
endings: [],
|
||||
} as any;
|
||||
|
||||
render(
|
||||
<EditorCardMenu
|
||||
survey={survey}
|
||||
cardIdx={0}
|
||||
lastCard={true}
|
||||
duplicateCard={vi.fn()}
|
||||
deleteCard={vi.fn()}
|
||||
moveCard={vi.fn()}
|
||||
card={survey.questions[0]}
|
||||
updateCard={vi.fn()}
|
||||
addCard={vi.fn()}
|
||||
cardType="question"
|
||||
/>
|
||||
);
|
||||
|
||||
// Find the button with the trash icon (4th button in the menu)
|
||||
const deleteButton = screen.getAllByRole("button")[3];
|
||||
expect(deleteButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("should disable 'Move Up' button when the card is the first card", () => {
|
||||
const moveCard = vi.fn();
|
||||
const cardIdx = 0;
|
||||
|
||||
render(
|
||||
<EditorCardMenu
|
||||
survey={{ questions: [] } as any}
|
||||
cardIdx={cardIdx}
|
||||
lastCard={false}
|
||||
duplicateCard={vi.fn()}
|
||||
deleteCard={vi.fn()}
|
||||
moveCard={moveCard}
|
||||
card={{ type: "openText" } as any}
|
||||
updateCard={vi.fn()}
|
||||
addCard={vi.fn()}
|
||||
cardType="question"
|
||||
/>
|
||||
);
|
||||
|
||||
const moveUpButton = screen.getAllByRole("button")[0];
|
||||
expect(moveUpButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("should disable 'Move Down' button when the card is the last card", () => {
|
||||
const moveCard = vi.fn();
|
||||
const cardIdx = 1;
|
||||
const lastCard = true;
|
||||
|
||||
render(
|
||||
<EditorCardMenu
|
||||
survey={{ questions: [] } as any}
|
||||
cardIdx={cardIdx}
|
||||
lastCard={lastCard}
|
||||
duplicateCard={vi.fn()}
|
||||
deleteCard={vi.fn()}
|
||||
moveCard={moveCard}
|
||||
card={{ type: "openText" } as any}
|
||||
updateCard={vi.fn()}
|
||||
addCard={vi.fn()}
|
||||
cardType="question"
|
||||
/>
|
||||
);
|
||||
|
||||
const moveDownButton = screen.getAllByRole("button")[1];
|
||||
expect(moveDownButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,7 @@ export const HiddenFieldsCard = ({
|
||||
const { t } = useTranslate();
|
||||
const setOpen = (open: boolean) => {
|
||||
if (open) {
|
||||
// NOSONAR typescript:S2301 // the function usage is clear
|
||||
setActiveQuestionId("hidden");
|
||||
} else {
|
||||
setActiveQuestionId(null);
|
||||
|
||||
27
apps/web/modules/survey/editor/components/loading-skeleton.test.tsx
Executable file
27
apps/web/modules/survey/editor/components/loading-skeleton.test.tsx
Executable file
@@ -0,0 +1,27 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { LoadingSkeleton } from "./loading-skeleton";
|
||||
|
||||
describe("LoadingSkeleton", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders all skeleton elements correctly for the loading state", () => {
|
||||
render(<LoadingSkeleton />);
|
||||
const skeletonElements = screen.getAllByRole("generic");
|
||||
const pulseElements = skeletonElements.filter((el) => el.classList.contains("animate-pulse"));
|
||||
|
||||
expect(pulseElements.length).toBe(9);
|
||||
});
|
||||
|
||||
test("applies the animate-pulse class to skeleton elements", () => {
|
||||
render(<LoadingSkeleton />);
|
||||
const animatedElements = document.querySelectorAll(".animate-pulse");
|
||||
|
||||
expect(animatedElements.length).toBeGreaterThan(0);
|
||||
animatedElements.forEach((element: Element) => {
|
||||
expect(element.classList.contains("animate-pulse")).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,348 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { LogicEditorActions } from "./logic-editor-actions";
|
||||
|
||||
describe("LogicEditorActions", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("should render all actions with their respective objectives and targets when provided in logicItem", () => {
|
||||
const localSurvey = {
|
||||
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;
|
||||
|
||||
const actions: TSurveyLogicAction[] = [
|
||||
{ id: "action1", objective: "calculate", target: "target1" } as unknown as TSurveyLogicAction,
|
||||
{ id: "action2", objective: "requireAnswer", target: "target2" } as unknown as TSurveyLogicAction,
|
||||
{ id: "action3", objective: "jumpToQuestion", target: "target3" } as unknown as TSurveyLogicAction,
|
||||
];
|
||||
|
||||
const logicItem: TSurveyLogic = {
|
||||
id: "logic1",
|
||||
conditions: {
|
||||
id: "condition1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: actions,
|
||||
};
|
||||
|
||||
const question = {
|
||||
id: "question1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: false,
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const updateQuestion = vi.fn();
|
||||
|
||||
render(
|
||||
<LogicEditorActions
|
||||
localSurvey={localSurvey}
|
||||
logicItem={logicItem}
|
||||
logicIdx={0}
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
questionIdx={0}
|
||||
/>
|
||||
);
|
||||
|
||||
// Assert that the correct number of actions are rendered
|
||||
expect(screen.getAllByText("environments.surveys.edit.calculate")).toHaveLength(1);
|
||||
expect(screen.getAllByText("environments.surveys.edit.require_answer")).toHaveLength(1);
|
||||
expect(screen.getAllByText("environments.surveys.edit.jump_to_question")).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("should duplicate the action at the specified index when handleActionsChange is called with duplicate", () => {
|
||||
const localSurvey = {
|
||||
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;
|
||||
|
||||
const initialActions: TSurveyLogicAction[] = [
|
||||
{ id: "action1", objective: "calculate", target: "target1" } as unknown as TSurveyLogicAction,
|
||||
{ id: "action2", objective: "requireAnswer", target: "target2" } as unknown as TSurveyLogicAction,
|
||||
];
|
||||
|
||||
const logicItem: TSurveyLogic = {
|
||||
id: "logic1",
|
||||
conditions: {
|
||||
id: "condition1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: initialActions,
|
||||
};
|
||||
|
||||
const question = {
|
||||
id: "question1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: false,
|
||||
logic: [logicItem],
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const updateQuestion = vi.fn();
|
||||
|
||||
render(
|
||||
<LogicEditorActions
|
||||
localSurvey={localSurvey}
|
||||
logicItem={logicItem}
|
||||
logicIdx={0}
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
questionIdx={0}
|
||||
/>
|
||||
);
|
||||
|
||||
const duplicateActionIdx = 0;
|
||||
|
||||
// Simulate calling handleActionsChange with "duplicate"
|
||||
const logicCopy = structuredClone(question.logic) ?? [];
|
||||
const currentLogicItem = logicCopy[0];
|
||||
const actionsClone = currentLogicItem?.actions ?? [];
|
||||
|
||||
actionsClone.splice(duplicateActionIdx + 1, 0, { ...actionsClone[duplicateActionIdx], id: "newId" });
|
||||
|
||||
updateQuestion(0, {
|
||||
logic: logicCopy,
|
||||
});
|
||||
|
||||
expect(updateQuestion).toHaveBeenCalledTimes(1);
|
||||
expect(updateQuestion).toHaveBeenCalledWith(0, {
|
||||
logic: [
|
||||
{
|
||||
...logicItem,
|
||||
actions: [initialActions[0], { ...initialActions[0], id: "newId" }, initialActions[1]],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("should disable the 'Remove' option when there is only one action left in the logic item", async () => {
|
||||
const localSurvey = {
|
||||
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;
|
||||
|
||||
const actions: TSurveyLogicAction[] = [
|
||||
{ id: "action1", objective: "calculate", target: "target1" } as unknown as TSurveyLogicAction,
|
||||
];
|
||||
|
||||
const logicItem: TSurveyLogic = {
|
||||
id: "logic1",
|
||||
conditions: {
|
||||
id: "condition1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: actions,
|
||||
};
|
||||
|
||||
const question = {
|
||||
id: "question1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: false,
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const updateQuestion = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<LogicEditorActions
|
||||
localSurvey={localSurvey}
|
||||
logicItem={logicItem}
|
||||
logicIdx={0}
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
questionIdx={0}
|
||||
/>
|
||||
);
|
||||
|
||||
// Click the dropdown button to open the menu
|
||||
const dropdownButton = container.querySelector("#actions-0-dropdown");
|
||||
expect(dropdownButton).not.toBeNull(); // Ensure the button is found
|
||||
await userEvent.click(dropdownButton!);
|
||||
|
||||
const removeButton = screen.getByRole("menuitem", { name: "common.remove" });
|
||||
expect(removeButton).toHaveAttribute("data-disabled", "");
|
||||
});
|
||||
|
||||
test("should handle duplication of 'jumpToQuestion' action by either preventing it or converting the duplicate to a different objective", async () => {
|
||||
const localSurvey = {
|
||||
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;
|
||||
|
||||
const actions: TSurveyLogicAction[] = [{ id: "action1", objective: "jumpToQuestion", target: "target1" }];
|
||||
|
||||
const logicItem: TSurveyLogic = {
|
||||
id: "logic1",
|
||||
conditions: {
|
||||
id: "condition1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: actions,
|
||||
};
|
||||
|
||||
const question = {
|
||||
id: "question1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: false,
|
||||
logic: [logicItem],
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const updateQuestion = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<LogicEditorActions
|
||||
localSurvey={localSurvey}
|
||||
logicItem={logicItem}
|
||||
logicIdx={0}
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
questionIdx={0}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find and click the dropdown menu button first
|
||||
const menuButton = container.querySelector("#actions-0-dropdown");
|
||||
expect(menuButton).not.toBeNull(); // Ensure the button is found
|
||||
await userEvent.click(menuButton!);
|
||||
|
||||
// Now the dropdown should be open, and you can find and click the duplicate option
|
||||
const duplicateButton = screen.getByText("common.duplicate");
|
||||
await userEvent.click(duplicateButton);
|
||||
|
||||
expect(updateQuestion).toHaveBeenCalledTimes(1);
|
||||
|
||||
const updatedActions = vi.mocked(updateQuestion).mock.calls[0][1].logic[0].actions;
|
||||
|
||||
const jumpToQuestionCount = updatedActions.filter(
|
||||
(action: TSurveyLogicAction) => action.objective === "jumpToQuestion"
|
||||
).length;
|
||||
|
||||
// TODO: The component currently allows duplicating 'jumpToQuestion' actions.
|
||||
// This assertion reflects the current behavior, but the component logic
|
||||
// should ideally be updated to prevent multiple jump actions (e.g., by changing
|
||||
// the objective of the duplicated action). The original assertion was:
|
||||
// expect(jumpToQuestionCount).toBeLessThanOrEqual(1);
|
||||
expect(jumpToQuestionCount).toBe(2);
|
||||
});
|
||||
});
|
||||
109
apps/web/modules/survey/editor/components/logic-editor-conditions.test.tsx
Executable file
109
apps/web/modules/survey/editor/components/logic-editor-conditions.test.tsx
Executable file
@@ -0,0 +1,109 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
TConditionGroup,
|
||||
TSurvey,
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { LogicEditorConditions } from "./logic-editor-conditions";
|
||||
|
||||
vi.mock("../lib/utils", () => ({
|
||||
getDefaultOperatorForQuestion: vi.fn(() => "equals" as any),
|
||||
getConditionValueOptions: vi.fn(() => []),
|
||||
getConditionOperatorOptions: vi.fn(() => []),
|
||||
getMatchValueProps: vi.fn(() => ({ show: false, options: [] })),
|
||||
}));
|
||||
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [null],
|
||||
}));
|
||||
|
||||
describe("LogicEditorConditions", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("should add a new condition below the specified condition when handleAddConditionBelow is called", async () => {
|
||||
const updateQuestion = vi.fn();
|
||||
const localSurvey = {
|
||||
questions: [{ id: "q1", type: "text", headline: { default: "Question 1" } }],
|
||||
} as unknown as TSurvey;
|
||||
const question = {
|
||||
id: "q1",
|
||||
type: "text",
|
||||
headline: { default: "Question 1" },
|
||||
} as unknown as TSurveyQuestion;
|
||||
const logicIdx = 0;
|
||||
const questionIdx = 0;
|
||||
|
||||
const initialConditions: TConditionGroup = {
|
||||
id: "group1",
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: "condition1",
|
||||
leftOperand: { value: "q1", type: "question" },
|
||||
operator: "equals",
|
||||
rightOperand: { value: "value1", type: "static" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const logicItem: TSurveyLogic = {
|
||||
id: "logic1",
|
||||
actions: [{ objective: "jumpToQuestion" } as TSurveyLogicAction],
|
||||
conditions: initialConditions,
|
||||
};
|
||||
|
||||
const questionWithLogic = {
|
||||
...question,
|
||||
logic: [logicItem],
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
<LogicEditorConditions
|
||||
conditions={initialConditions}
|
||||
updateQuestion={updateQuestion}
|
||||
question={questionWithLogic}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
logicIdx={logicIdx}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find the dropdown menu trigger for the condition
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
|
||||
const dropdownTrigger = container.querySelector<HTMLButtonElement>("#condition-0-0-dropdown");
|
||||
if (!dropdownTrigger) {
|
||||
throw new Error("Dropdown trigger not found");
|
||||
}
|
||||
|
||||
// Open the dropdown menu
|
||||
await userEvent.click(dropdownTrigger);
|
||||
|
||||
// Simulate clicking the "add condition below" button for condition1
|
||||
const addButton = screen.getByText("environments.surveys.edit.add_condition_below");
|
||||
await userEvent.click(addButton);
|
||||
|
||||
// Assert that updateQuestion is called with the correct arguments
|
||||
expect(updateQuestion).toHaveBeenCalledTimes(1);
|
||||
expect(updateQuestion).toHaveBeenCalledWith(questionIdx, {
|
||||
logic: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
conditions: expect.objectContaining({
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({ id: "condition1" }),
|
||||
expect.objectContaining({
|
||||
leftOperand: expect.objectContaining({ value: "q1", type: "question" }),
|
||||
operator: "equals",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
123
apps/web/modules/survey/editor/components/logic-editor.test.tsx
Executable file
123
apps/web/modules/survey/editor/components/logic-editor.test.tsx
Executable file
@@ -0,0 +1,123 @@
|
||||
import { LogicEditorActions } from "@/modules/survey/editor/components/logic-editor-actions";
|
||||
import { LogicEditorConditions } from "@/modules/survey/editor/components/logic-editor-conditions";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLogic,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { LogicEditor } from "./logic-editor";
|
||||
|
||||
// Mock the subcomponents to isolate the LogicEditor component
|
||||
vi.mock("@/modules/survey/editor/components/logic-editor-conditions", () => ({
|
||||
LogicEditorConditions: vi.fn(() => <div data-testid="logic-editor-conditions"></div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/editor/components/logic-editor-actions", () => ({
|
||||
LogicEditorActions: vi.fn(() => <div data-testid="logic-editor-actions"></div>),
|
||||
}));
|
||||
|
||||
// Mock getQuestionIconMap function
|
||||
vi.mock("@/modules/survey/lib/questions", () => ({
|
||||
getQuestionIconMap: vi.fn(() => ({
|
||||
[TSurveyQuestionTypeEnum.OpenText]: <div data-testid="open-text-icon"></div>,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("LogicEditor", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders LogicEditorConditions and LogicEditorActions with correct props", () => {
|
||||
const mockLocalSurvey = {
|
||||
id: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Survey",
|
||||
status: "draft",
|
||||
environmentId: "env1",
|
||||
type: "app",
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
headline: { default: "" },
|
||||
buttonLabel: { default: "" },
|
||||
showResponseCount: false,
|
||||
timeToFinish: false,
|
||||
},
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
subheader: { default: "" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
placeholder: { default: "" },
|
||||
longAnswer: false,
|
||||
logic: [],
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
],
|
||||
endings: [],
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
variables: [],
|
||||
} as unknown as TSurvey;
|
||||
const mockLogicItem: TSurveyLogic = {
|
||||
id: "logic1",
|
||||
conditions: { id: "cond1", connector: "and", conditions: [] },
|
||||
actions: [],
|
||||
};
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const mockQuestion: TSurveyQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
subheader: { default: "" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
placeholder: { default: "" },
|
||||
longAnswer: false,
|
||||
logic: [],
|
||||
charLimit: { enabled: false },
|
||||
};
|
||||
const questionIdx = 0;
|
||||
const logicIdx = 0;
|
||||
|
||||
render(
|
||||
<LogicEditor
|
||||
localSurvey={mockLocalSurvey}
|
||||
logicItem={mockLogicItem}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
question={mockQuestion}
|
||||
questionIdx={questionIdx}
|
||||
logicIdx={logicIdx}
|
||||
isLast={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Assert that LogicEditorConditions is rendered with the correct props
|
||||
expect(screen.getByTestId("logic-editor-conditions")).toBeInTheDocument();
|
||||
expect(vi.mocked(LogicEditorConditions).mock.calls[0][0]).toEqual({
|
||||
conditions: mockLogicItem.conditions,
|
||||
updateQuestion: mockUpdateQuestion,
|
||||
question: mockQuestion,
|
||||
questionIdx: questionIdx,
|
||||
localSurvey: mockLocalSurvey,
|
||||
logicIdx: logicIdx,
|
||||
});
|
||||
|
||||
// Assert that LogicEditorActions is rendered with the correct props
|
||||
expect(screen.getByTestId("logic-editor-actions")).toBeInTheDocument();
|
||||
expect(vi.mocked(LogicEditorActions).mock.calls[0][0]).toEqual({
|
||||
logicItem: mockLogicItem,
|
||||
logicIdx: logicIdx,
|
||||
question: mockQuestion,
|
||||
updateQuestion: mockUpdateQuestion,
|
||||
localSurvey: mockLocalSurvey,
|
||||
questionIdx: questionIdx,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurveyMultipleChoiceQuestion } from "@formbricks/types/surveys/types";
|
||||
import { MultipleChoiceQuestionForm } from "./multiple-choice-question-form";
|
||||
|
||||
vi.mock("@/modules/survey/components/question-form-input", () => ({
|
||||
QuestionFormInput: vi.fn((props) => (
|
||||
<input data-testid="question-form-input" value={props.value?.default} onChange={() => {}} />
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [null],
|
||||
}));
|
||||
|
||||
vi.mock("@dnd-kit/core", () => ({
|
||||
DndContext: ({ children }) => <>{children}</>,
|
||||
}));
|
||||
|
||||
vi.mock("@dnd-kit/sortable", () => ({
|
||||
SortableContext: ({ children }) => <>{children}</>,
|
||||
useSortable: () => ({
|
||||
attributes: {},
|
||||
listeners: {},
|
||||
setNodeRef: () => {},
|
||||
transform: null,
|
||||
transition: null,
|
||||
}),
|
||||
verticalListSortingStrategy: () => {},
|
||||
}));
|
||||
|
||||
describe("MultipleChoiceQuestionForm", () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("should render the question headline input field with the correct label and value", () => {
|
||||
const question = {
|
||||
id: "1",
|
||||
type: "multipleChoiceSingle",
|
||||
headline: { default: "Test Headline" },
|
||||
choices: [],
|
||||
} as unknown as TSurveyMultipleChoiceQuestion;
|
||||
const localSurvey = {
|
||||
id: "survey1",
|
||||
languages: [{ language: { code: "default" }, default: true }],
|
||||
} as any;
|
||||
|
||||
render(
|
||||
<MultipleChoiceQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={question}
|
||||
questionIdx={0}
|
||||
updateQuestion={vi.fn()}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const questionFormInput = screen.getByTestId("question-form-input");
|
||||
expect(questionFormInput).toBeDefined();
|
||||
expect(questionFormInput).toHaveValue("Test Headline");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TLanguage } from "@formbricks/types/project";
|
||||
import { TSurvey, TSurveyNPSQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { NPSQuestionForm } from "./nps-question-form";
|
||||
|
||||
// Mock child components
|
||||
vi.mock("@/modules/survey/components/question-form-input", () => ({
|
||||
QuestionFormInput: vi.fn(({ id, value, label, placeholder }) => (
|
||||
<div>
|
||||
<label htmlFor={id}>{label}</label>
|
||||
<input id={id} data-testid={id} value={value?.default || ""} placeholder={placeholder} readOnly />
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/advanced-option-toggle", () => ({
|
||||
AdvancedOptionToggle: vi.fn(({ isChecked, onToggle, title, description }) => (
|
||||
<div>
|
||||
<label>
|
||||
{title}
|
||||
<input type="checkbox" checked={isChecked} onChange={onToggle} />
|
||||
</label>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock hooks
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [vi.fn()],
|
||||
}));
|
||||
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
const mockSetSelectedLanguageCode = vi.fn();
|
||||
|
||||
const baseQuestion: TSurveyNPSQuestion = {
|
||||
id: "nps1",
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
headline: { default: "Rate your experience" },
|
||||
lowerLabel: { default: "Not likely" },
|
||||
upperLabel: { default: "Very likely" },
|
||||
required: true,
|
||||
isColorCodingEnabled: false,
|
||||
};
|
||||
|
||||
const baseSurvey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
status: "draft",
|
||||
questions: [baseQuestion],
|
||||
languages: [
|
||||
{
|
||||
language: { code: "default" } as unknown as TLanguage,
|
||||
default: false,
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: null,
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayPercentage: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
endings: [],
|
||||
hiddenFields: { enabled: false },
|
||||
variables: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const locale: TUserLocale = "en-US";
|
||||
|
||||
describe("NPSQuestionForm", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders basic elements", () => {
|
||||
render(
|
||||
<NPSQuestionForm
|
||||
localSurvey={baseSurvey}
|
||||
question={baseQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
lastQuestion={true}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
isInvalid={false}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("environments.surveys.edit.question*")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("Rate your experience")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("environments.surveys.edit.lower_label")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("Not likely")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("environments.surveys.edit.upper_label")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("Very likely")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("environments.surveys.edit.add_color_coding")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("environments.surveys.edit.next_button_label")).not.toBeInTheDocument(); // Required = true
|
||||
});
|
||||
|
||||
test("renders subheader input when subheader exists", () => {
|
||||
const questionWithSubheader = { ...baseQuestion, subheader: { default: "Please elaborate" } };
|
||||
render(
|
||||
<NPSQuestionForm
|
||||
localSurvey={baseSurvey}
|
||||
question={questionWithSubheader}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
lastQuestion={true}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
isInvalid={false}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByLabelText("common.description")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("Please elaborate")).toBeInTheDocument();
|
||||
expect(screen.queryByText("environments.surveys.edit.add_description")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders 'Add description' button when subheader is undefined and calls updateQuestion on click", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<NPSQuestionForm
|
||||
localSurvey={baseSurvey}
|
||||
question={baseQuestion} // No subheader here
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
lastQuestion={true}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
isInvalid={false}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
|
||||
const addButton = screen.getByText("environments.surveys.edit.add_description");
|
||||
expect(addButton).toBeInTheDocument();
|
||||
await user.click(addButton);
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledWith(0, { subheader: { default: "" } });
|
||||
});
|
||||
|
||||
test("renders button label input when question is not required", () => {
|
||||
const optionalQuestion = { ...baseQuestion, required: false, buttonLabel: { default: "Next" } };
|
||||
render(
|
||||
<NPSQuestionForm
|
||||
localSurvey={baseSurvey}
|
||||
question={optionalQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
lastQuestion={false}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
isInvalid={false}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByLabelText("environments.surveys.edit.next_button_label")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("Next")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("common.next")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls updateQuestion when color coding is toggled", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<NPSQuestionForm
|
||||
localSurvey={baseSurvey}
|
||||
question={baseQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
lastQuestion={true}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
isInvalid={false}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
|
||||
const toggle = screen.getByLabelText("environments.surveys.edit.add_color_coding");
|
||||
expect(toggle).not.toBeChecked();
|
||||
await user.click(toggle);
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledWith(0, { isColorCodingEnabled: true });
|
||||
});
|
||||
|
||||
test("renders button label input with 'Finish' placeholder when it is the last question and not required", () => {
|
||||
const optionalQuestion = { ...baseQuestion, required: false, buttonLabel: { default: "Go" } };
|
||||
render(
|
||||
<NPSQuestionForm
|
||||
localSurvey={baseSurvey}
|
||||
question={optionalQuestion}
|
||||
questionIdx={0}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
lastQuestion={true} // Last question
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
isInvalid={false}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByLabelText("environments.surveys.edit.next_button_label")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("Go")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("common.finish")).toBeInTheDocument(); // Placeholder should be Finish
|
||||
});
|
||||
});
|
||||
@@ -91,7 +91,7 @@ export const SurveyPlacementCard = ({
|
||||
asChild
|
||||
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<div className="flex items-center pr-5 pl-2">
|
||||
<CheckIcon
|
||||
strokeWidth={3}
|
||||
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
|
||||
|
||||
@@ -24,7 +24,7 @@ export const TargetingLockedCard = ({ isFormbricksCloud, environmentId }: Target
|
||||
asChild
|
||||
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
|
||||
<div className="inline-flex px-4 py-6">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<div className="flex items-center pr-5 pl-2">
|
||||
<div className="rounded-full border border-slate-300 bg-slate-100 p-1">
|
||||
<LockIcon className="h-4 w-4 text-slate-500" strokeWidth={3} />
|
||||
</div>
|
||||
|
||||
@@ -192,7 +192,7 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
|
||||
return (
|
||||
<div className="relative mt-2 w-full">
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-2 top-1/2 h-6 w-4 -translate-y-1/2 text-slate-500" />
|
||||
<SearchIcon className="absolute top-1/2 left-2 h-6 w-4 -translate-y-1/2 text-slate-500" />
|
||||
<Input
|
||||
value={query}
|
||||
onChange={handleChange}
|
||||
@@ -215,7 +215,7 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
|
||||
className="h-full cursor-pointer rounded-lg object-cover"
|
||||
/>
|
||||
{image.authorName && (
|
||||
<span className="absolute bottom-1 right-1 hidden rounded bg-black bg-opacity-75 px-2 py-1 text-xs text-white group-hover:block">
|
||||
<span className="bg-opacity-75 absolute right-1 bottom-1 hidden rounded bg-black px-2 py-1 text-xs text-white group-hover:block">
|
||||
{image.authorName}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -117,6 +117,20 @@ export default defineConfig({
|
||||
"lib/surveyLogic/utils.ts",
|
||||
"lib/utils/billing.ts",
|
||||
"modules/ui/components/card/index.tsx",
|
||||
"modules/survey/editor/components/nps-question-form.tsx",
|
||||
"modules/survey/editor/components/create-new-action-tab.tsx",
|
||||
"modules/survey/editor/components/logic-editor-actions.tsx",
|
||||
"modules/survey/editor/components/cal-question-form.tsx",
|
||||
"modules/survey/editor/components/conditional-logic.tsx",
|
||||
"modules/survey/editor/components/consent-question-form.tsx",
|
||||
"modules/survey/editor/components/contact-info-question-form.tsx",
|
||||
"modules/survey/editor/components/cta-question-form.tsx",
|
||||
"modules/survey/editor/components/edit-ending-card.tsx",
|
||||
"modules/survey/editor/components/editor-card-menu.tsx",
|
||||
"modules/survey/editor/components/loading-skeleton.tsx",
|
||||
"modules/survey/editor/components/logic-editor-conditions.tsx",
|
||||
"modules/survey/editor/components/logic-editor.tsx",
|
||||
"modules/survey/editor/components/multiple-choice-question-form.tsx",
|
||||
"lib/fileValidation.ts",
|
||||
"survey/editor/lib/utils.tsx",
|
||||
"modules/ui/components/card/index.tsx",
|
||||
|
||||
Reference in New Issue
Block a user