mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 16:16:21 -06:00
chore: add tests to package/surveys/src/components/questions (#5694)
This commit is contained in:
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
@@ -11,7 +11,7 @@ When generating test files inside the "/app/web" path, follow these rules:
|
||||
- Follow the same test pattern used for other files in the package where the file is located
|
||||
- All imports should be at the top of the file, not inside individual tests
|
||||
- For mocking inside "test" blocks use "vi.mocked"
|
||||
- Add the original file path to the "test.coverage.include"array in the "apps/web/vite.config.mts" file. Do this only when the test file is created.
|
||||
- If the file is located in the "packages/survey" path, use "@testing-library/preact" instead of "@testing-library/react"
|
||||
- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file
|
||||
- When using "screen.getByText" check for the tolgee string if it is being used in the file.
|
||||
- The types for mocked variables can be found in the "packages/types" path. Be sure that every imported type exists before using it. Don't create types that are not already in the codebase.
|
||||
@@ -28,4 +28,5 @@ afterEach(() => {
|
||||
- The "afterEach" function should only have the "cleanup()" line inside it and should be adde to the "vitest" imports.
|
||||
- For click events, import userEvent from "@testing-library/user-event"
|
||||
- Mock other components that can make the text more complex and but at the same time mocking it wouldn't make the test flaky. It's ok to leave basic and simple components.
|
||||
- You don't need to mock @tolgee/react
|
||||
- You don't need to mock @tolgee/react
|
||||
- Use "import "@testing-library/jest-dom/vitest";"
|
||||
@@ -1,6 +1,10 @@
|
||||
import { processResponseData } from "@/lib/responses";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { getFormattedDateTimeString } from "@/lib/utils/datetime";
|
||||
import { getSelectionColumn } from "@/modules/ui/components/data-table";
|
||||
import { ResponseBadges } from "@/modules/ui/components/response-badges";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { AnyActionArg } from "react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TResponseNote, TResponseNoteUser, TResponseTableData } from "@formbricks/types/responses";
|
||||
import {
|
||||
@@ -257,3 +261,238 @@ describe("generateResponseTableColumns", () => {
|
||||
expect(vi.mocked(processResponseData)).toHaveBeenCalledWith(["This is a note"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ResponseTableColumns", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("includes verifiedEmailColumn when isVerifyEmailEnabled is true", () => {
|
||||
// Arrange
|
||||
const mockSurvey = {
|
||||
questions: [],
|
||||
variables: [],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
isVerifyEmailEnabled: true,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockT = vi.fn((key) => key);
|
||||
const isExpanded = false;
|
||||
const isReadOnly = false;
|
||||
|
||||
// Act
|
||||
const columns = generateResponseTableColumns(mockSurvey, isExpanded, isReadOnly, mockT);
|
||||
|
||||
// Assert
|
||||
const verifiedEmailColumn: any = columns.find((col: any) => col.accessorKey === "verifiedEmail");
|
||||
expect(verifiedEmailColumn).toBeDefined();
|
||||
expect(verifiedEmailColumn?.accessorKey).toBe("verifiedEmail");
|
||||
|
||||
// Call the header function to trigger the t function call with "common.verified_email"
|
||||
if (verifiedEmailColumn && typeof verifiedEmailColumn.header === "function") {
|
||||
verifiedEmailColumn.header();
|
||||
expect(mockT).toHaveBeenCalledWith("common.verified_email");
|
||||
}
|
||||
});
|
||||
|
||||
test("excludes verifiedEmailColumn when isVerifyEmailEnabled is false", () => {
|
||||
// Arrange
|
||||
const mockSurvey = {
|
||||
questions: [],
|
||||
variables: [],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
isVerifyEmailEnabled: false,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockT = vi.fn((key) => key);
|
||||
const isExpanded = false;
|
||||
const isReadOnly = false;
|
||||
|
||||
// Act
|
||||
const columns = generateResponseTableColumns(mockSurvey, isExpanded, isReadOnly, mockT);
|
||||
|
||||
// Assert
|
||||
const verifiedEmailColumn = columns.find((col: any) => col.accessorKey === "verifiedEmail");
|
||||
expect(verifiedEmailColumn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ResponseTableColumns - Column Implementations", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("dateColumn renders with formatted date", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const dateColumn: any = columns.find((col) => (col as any).accessorKey === "createdAt");
|
||||
expect(dateColumn).toBeDefined();
|
||||
|
||||
// Call the header function to test it returns the expected value
|
||||
expect(dateColumn?.header?.()).toBe("common.date");
|
||||
|
||||
// Mock a response with a date to test the cell function
|
||||
const mockRow = {
|
||||
original: { createdAt: "2023-01-01T12:00:00Z" },
|
||||
} as any;
|
||||
|
||||
// Call the cell function and check the formatted date
|
||||
dateColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(vi.mocked(getFormattedDateTimeString)).toHaveBeenCalledWith(new Date("2023-01-01T12:00:00Z"));
|
||||
});
|
||||
|
||||
test("personColumn renders anonymous when person is null", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const personColumn: any = columns.find((col) => (col as any).accessorKey === "personId");
|
||||
expect(personColumn).toBeDefined();
|
||||
|
||||
// Test header content
|
||||
const headerResult = personColumn?.header?.();
|
||||
expect(headerResult).toBeDefined();
|
||||
|
||||
// Mock a response with no person
|
||||
const mockRow = {
|
||||
original: { person: null },
|
||||
} as any;
|
||||
|
||||
// Mock the t function for this specific call
|
||||
t.mockReturnValueOnce("Anonymous User");
|
||||
|
||||
// Call the cell function and check it returns "Anonymous"
|
||||
const cellResult = personColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(t).toHaveBeenCalledWith("common.anonymous");
|
||||
expect(cellResult?.props?.children).toBe("Anonymous User");
|
||||
});
|
||||
|
||||
test("personColumn renders person identifier when person exists", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const personColumn: any = columns.find((col) => (col as any).accessorKey === "personId");
|
||||
expect(personColumn).toBeDefined();
|
||||
|
||||
// Mock a response with a person
|
||||
const mockRow = {
|
||||
original: {
|
||||
person: { id: "123", attributes: { email: "test@example.com" } },
|
||||
contactAttributes: { name: "John Doe" },
|
||||
},
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
personColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(vi.mocked(getContactIdentifier)).toHaveBeenCalledWith(
|
||||
mockRow.original.person,
|
||||
mockRow.original.contactAttributes
|
||||
);
|
||||
});
|
||||
|
||||
test("tagsColumn returns undefined when tags is not an array", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const tagsColumn: any = columns.find((col) => (col as any).accessorKey === "tags");
|
||||
expect(tagsColumn).toBeDefined();
|
||||
|
||||
// Mock a response with no tags
|
||||
const mockRow = {
|
||||
original: { tags: null },
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
const cellResult = tagsColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(cellResult).toBeUndefined();
|
||||
});
|
||||
|
||||
test("notesColumn renders when notes is an array", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const notesColumn: any = columns.find((col) => (col as any).accessorKey === "notes");
|
||||
expect(notesColumn).toBeDefined();
|
||||
|
||||
// Mock a response with notes
|
||||
const mockRow = {
|
||||
original: { notes: [{ text: "Note 1" }, { text: "Note 2" }] },
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
notesColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(vi.mocked(processResponseData)).toHaveBeenCalledWith(["Note 1", "Note 2"]);
|
||||
});
|
||||
|
||||
test("notesColumn returns undefined when notes is not an array", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const notesColumn: any = columns.find((col) => (col as any).accessorKey === "notes");
|
||||
expect(notesColumn).toBeDefined();
|
||||
|
||||
// Mock a response with no notes
|
||||
const mockRow = {
|
||||
original: { notes: null },
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
const cellResult = notesColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(cellResult).toBeUndefined();
|
||||
});
|
||||
|
||||
test("variableColumns render variable values correctly", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
|
||||
// Find the variable column for var1
|
||||
const var1Column: any = columns.find((col) => (col as any).accessorKey === "var1");
|
||||
expect(var1Column).toBeDefined();
|
||||
|
||||
// Test the header
|
||||
const headerResult = var1Column?.header?.();
|
||||
expect(headerResult).toBeDefined();
|
||||
|
||||
// Mock a response with a string variable
|
||||
const mockRow = {
|
||||
original: { variables: { var1: "Test Value" } },
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
const cellResult = var1Column?.cell?.({ row: mockRow } as any);
|
||||
expect(cellResult?.props.children).toBe("Test Value");
|
||||
|
||||
// Test with a number variable
|
||||
const var2Column: any = columns.find((col) => (col as any).accessorKey === "var2");
|
||||
expect(var2Column).toBeDefined();
|
||||
|
||||
const mockRowNumber = {
|
||||
original: { variables: { var2: 42 } },
|
||||
} as any;
|
||||
|
||||
const cellResultNumber = var2Column?.cell?.({ row: mockRowNumber } as any);
|
||||
expect(cellResultNumber?.props.children).toBe(42);
|
||||
});
|
||||
|
||||
test("hiddenFieldColumns render when fieldIds exist", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
|
||||
// Find the hidden field column
|
||||
const hfColumn: any = columns.find((col) => (col as any).accessorKey === "hf1");
|
||||
expect(hfColumn).toBeDefined();
|
||||
|
||||
// Test the header
|
||||
const headerResult = hfColumn?.header?.();
|
||||
expect(headerResult).toBeDefined();
|
||||
|
||||
// Mock a response with a hidden field value
|
||||
const mockRow = {
|
||||
original: { responseData: { hf1: "Hidden Value" } },
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
const cellResult = hfColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(cellResult?.props.children).toBe("Hidden Value");
|
||||
});
|
||||
|
||||
test("hiddenFieldColumns are empty when fieldIds don't exist", () => {
|
||||
// Create a survey with no hidden field IDs
|
||||
const surveyWithNoHiddenFields = {
|
||||
...mockSurvey,
|
||||
hiddenFields: { enabled: true }, // no fieldIds
|
||||
};
|
||||
|
||||
const columns = generateResponseTableColumns(surveyWithNoHiddenFields, false, true, t as any);
|
||||
|
||||
// Check that no hidden field columns were created
|
||||
const hfColumn = columns.find((col) => (col as any).accessorKey === "hf1");
|
||||
expect(hfColumn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,263 @@
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
|
||||
import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys";
|
||||
import { getSurveyFilterDataBySurveySharingKeyAction } from "@/app/share/[sharingKey]/actions";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useParams } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { ResponseFilter } from "./ResponseFilter";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
|
||||
useResponseFilter: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions", () => ({
|
||||
getSurveyFilterDataAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/share/[sharingKey]/actions", () => ({
|
||||
getSurveyFilterDataBySurveySharingKeyAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/lib/surveys/surveys", () => ({
|
||||
generateQuestionAndFilterOptions: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useParams: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [[vi.fn()]],
|
||||
}));
|
||||
|
||||
vi.mock("./QuestionsComboBox", () => ({
|
||||
QuestionsComboBox: ({ onChangeValue }) => (
|
||||
<div data-testid="questions-combo-box">
|
||||
<button onClick={() => onChangeValue({ id: "q1", label: "Question 1", type: "OpenText" })}>
|
||||
Select Question
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
OptionsType: {
|
||||
QUESTIONS: "Questions",
|
||||
ATTRIBUTES: "Attributes",
|
||||
TAGS: "Tags",
|
||||
LANGUAGES: "Languages",
|
||||
},
|
||||
}));
|
||||
|
||||
// Update the mock for QuestionFilterComboBox to always render
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox",
|
||||
() => ({
|
||||
QuestionFilterComboBox: () => (
|
||||
<div data-testid="filter-combo-box">
|
||||
<button data-testid="select-filter-btn">Select Filter</button>
|
||||
<button data-testid="select-filter-type-btn">Select Filter Type</button>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
describe("ResponseFilter", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockSelectedFilter = {
|
||||
filter: [],
|
||||
onlyComplete: false,
|
||||
};
|
||||
|
||||
const mockSelectedOptions = {
|
||||
questionFilterOptions: [
|
||||
{
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
filterOptions: ["equals", "does not equal"],
|
||||
filterComboBoxOptions: [],
|
||||
id: "q1",
|
||||
},
|
||||
],
|
||||
questionOptions: [
|
||||
{
|
||||
label: "Questions",
|
||||
type: "Questions",
|
||||
option: [
|
||||
{ id: "q1", label: "Question 1", type: "OpenText", questionType: TSurveyQuestionTypeEnum.OpenText },
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const mockSetSelectedFilter = vi.fn();
|
||||
const mockSetSelectedOptions = vi.fn();
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey1",
|
||||
environmentId: "env1",
|
||||
name: "Test Survey",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
status: "draft",
|
||||
createdBy: "user1",
|
||||
questions: [],
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
triggers: [],
|
||||
displayOption: "displayOnce",
|
||||
} as unknown as TSurvey;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useResponseFilter).mockReturnValue({
|
||||
selectedFilter: mockSelectedFilter,
|
||||
setSelectedFilter: mockSetSelectedFilter,
|
||||
selectedOptions: mockSelectedOptions,
|
||||
setSelectedOptions: mockSetSelectedOptions,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useParams).mockReturnValue({ environmentId: "env1", surveyId: "survey1" });
|
||||
|
||||
vi.mocked(getSurveyFilterDataAction).mockResolvedValue({
|
||||
data: {
|
||||
attributes: [],
|
||||
meta: {},
|
||||
environmentTags: [],
|
||||
hiddenFields: [],
|
||||
} as any,
|
||||
});
|
||||
|
||||
vi.mocked(generateQuestionAndFilterOptions).mockReturnValue({
|
||||
questionFilterOptions: mockSelectedOptions.questionFilterOptions,
|
||||
questionOptions: mockSelectedOptions.questionOptions,
|
||||
});
|
||||
});
|
||||
|
||||
test("renders with default state", () => {
|
||||
render(<ResponseFilter survey={mockSurvey} />);
|
||||
expect(screen.getByText("Filter")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("opens the filter popover when clicked", async () => {
|
||||
render(<ResponseFilter survey={mockSurvey} />);
|
||||
|
||||
await userEvent.click(screen.getByText("Filter"));
|
||||
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.show_all_responses_that_match")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.summary.only_completed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("fetches filter data when opened", async () => {
|
||||
render(<ResponseFilter survey={mockSurvey} />);
|
||||
|
||||
await userEvent.click(screen.getByText("Filter"));
|
||||
|
||||
expect(getSurveyFilterDataAction).toHaveBeenCalledWith({ surveyId: "survey1" });
|
||||
expect(mockSetSelectedOptions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles adding new filter", async () => {
|
||||
// Start with an empty filter
|
||||
vi.mocked(useResponseFilter).mockReturnValue({
|
||||
selectedFilter: { filter: [], onlyComplete: false },
|
||||
setSelectedFilter: mockSetSelectedFilter,
|
||||
selectedOptions: mockSelectedOptions,
|
||||
setSelectedOptions: mockSetSelectedOptions,
|
||||
} as any);
|
||||
|
||||
render(<ResponseFilter survey={mockSurvey} />);
|
||||
|
||||
await userEvent.click(screen.getByText("Filter"));
|
||||
// Verify there's no filter yet
|
||||
expect(screen.queryByTestId("questions-combo-box")).not.toBeInTheDocument();
|
||||
|
||||
// Add a new filter and check that the questions combo box appears
|
||||
await userEvent.click(screen.getByText("common.add_filter"));
|
||||
|
||||
expect(screen.getByTestId("questions-combo-box")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles only complete checkbox toggle", async () => {
|
||||
render(<ResponseFilter survey={mockSurvey} />);
|
||||
|
||||
await userEvent.click(screen.getByText("Filter"));
|
||||
await userEvent.click(screen.getByRole("checkbox"));
|
||||
await userEvent.click(screen.getByText("common.apply_filters"));
|
||||
|
||||
expect(mockSetSelectedFilter).toHaveBeenCalledWith({ filter: [], onlyComplete: true });
|
||||
});
|
||||
|
||||
test("handles selecting question and filter options", async () => {
|
||||
// Setup with a pre-populated filter to ensure the filter components are rendered
|
||||
const setSelectedFilterMock = vi.fn();
|
||||
vi.mocked(useResponseFilter).mockReturnValue({
|
||||
selectedFilter: {
|
||||
filter: [
|
||||
{
|
||||
questionType: { id: "q1", label: "Question 1", type: "OpenText" },
|
||||
filterType: { filterComboBoxValue: undefined, filterValue: undefined },
|
||||
},
|
||||
],
|
||||
onlyComplete: false,
|
||||
},
|
||||
setSelectedFilter: setSelectedFilterMock,
|
||||
selectedOptions: mockSelectedOptions,
|
||||
setSelectedOptions: mockSetSelectedOptions,
|
||||
} as any);
|
||||
|
||||
render(<ResponseFilter survey={mockSurvey} />);
|
||||
|
||||
await userEvent.click(screen.getByText("Filter"));
|
||||
|
||||
// Verify both combo boxes are rendered
|
||||
expect(screen.getByTestId("questions-combo-box")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("filter-combo-box")).toBeInTheDocument();
|
||||
|
||||
// Use data-testid to find our buttons instead of text
|
||||
await userEvent.click(screen.getByText("Select Question"));
|
||||
await userEvent.click(screen.getByTestId("select-filter-btn"));
|
||||
await userEvent.click(screen.getByText("common.apply_filters"));
|
||||
|
||||
expect(setSelectedFilterMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles clear all filters", async () => {
|
||||
render(<ResponseFilter survey={mockSurvey} />);
|
||||
|
||||
await userEvent.click(screen.getByText("Filter"));
|
||||
await userEvent.click(screen.getByText("common.clear_all"));
|
||||
|
||||
expect(mockSetSelectedFilter).toHaveBeenCalledWith({ filter: [], onlyComplete: false });
|
||||
});
|
||||
|
||||
test("uses sharing key action when on sharing page", async () => {
|
||||
vi.mocked(useParams).mockReturnValue({
|
||||
environmentId: "env1",
|
||||
surveyId: "survey1",
|
||||
sharingKey: "share123",
|
||||
});
|
||||
vi.mocked(getSurveyFilterDataBySurveySharingKeyAction).mockResolvedValue({
|
||||
data: {
|
||||
attributes: [],
|
||||
meta: {},
|
||||
environmentTags: [],
|
||||
hiddenFields: [],
|
||||
} as any,
|
||||
});
|
||||
|
||||
render(<ResponseFilter survey={mockSurvey} />);
|
||||
|
||||
await userEvent.click(screen.getByText("Filter"));
|
||||
|
||||
expect(getSurveyFilterDataBySurveySharingKeyAction).toHaveBeenCalledWith({
|
||||
sharingKey: "share123",
|
||||
environmentId: "env1",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { diffInDays, formatDateWithOrdinal, getFormattedDateTimeString, isValidDateString } from "./datetime";
|
||||
|
||||
describe("datetime utils", () => {
|
||||
@@ -9,7 +9,11 @@ describe("datetime utils", () => {
|
||||
});
|
||||
|
||||
test("formatDateWithOrdinal formats a date with ordinal suffix", () => {
|
||||
const date = new Date("2025-05-06");
|
||||
// Create a date that's fixed to May 6, 2025 at noon UTC
|
||||
// Using noon ensures the date won't change in most timezones
|
||||
const date = new Date(Date.UTC(2025, 4, 6, 12, 0, 0));
|
||||
|
||||
// Test the function
|
||||
expect(formatDateWithOrdinal(date)).toBe("Tuesday, May 6th, 2025");
|
||||
});
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ export const EmailCustomizationSettings = ({
|
||||
<div className="mb-10">
|
||||
<Small>{t("environments.settings.general.logo_in_email_header")}</Small>
|
||||
|
||||
<div className="mb-6 mt-2 flex items-center gap-4">
|
||||
<div className="mt-2 mb-6 flex items-center gap-4">
|
||||
{logoUrl && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex w-max items-center justify-center rounded-lg border border-slate-200 px-4 py-2">
|
||||
@@ -256,7 +256,7 @@ export const EmailCustomizationSettings = ({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shadow-card-xl min-h-52 w-[446px] rounded-t-lg border border-slate-100 px-10 pb-4 pt-10">
|
||||
<div className="shadow-card-xl min-h-52 w-[446px] rounded-t-lg border border-slate-100 px-10 pt-10 pb-4">
|
||||
<Image
|
||||
data-testid="email-customization-preview-image"
|
||||
src={logoUrl || fbLogoUrl}
|
||||
@@ -284,7 +284,7 @@ export const EmailCustomizationSettings = ({
|
||||
)}
|
||||
|
||||
{hasWhiteLabelPermission && isReadOnly && (
|
||||
<Alert variant="warning" className="mb-6 mt-4">
|
||||
<Alert variant="warning" className="mt-4 mb-6">
|
||||
<AlertDescription>
|
||||
{t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")}
|
||||
</AlertDescription>
|
||||
|
||||
@@ -237,7 +237,7 @@ export const FileInput = ({
|
||||
/>
|
||||
{file.uploaded ? (
|
||||
<div
|
||||
className="absolute right-2 top-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
|
||||
className="absolute top-2 right-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
|
||||
onClick={() => handleRemove(idx)}>
|
||||
<XIcon className="h-5 text-slate-700 hover:text-slate-900" />
|
||||
</div>
|
||||
@@ -255,7 +255,7 @@ export const FileInput = ({
|
||||
</p>
|
||||
{file.uploaded ? (
|
||||
<div
|
||||
className="absolute right-2 top-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
|
||||
className="absolute top-2 right-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
|
||||
onClick={() => handleRemove(idx)}>
|
||||
<XIcon className="h-5 text-slate-700 hover:text-slate-900" />
|
||||
</div>
|
||||
@@ -295,7 +295,7 @@ export const FileInput = ({
|
||||
/>
|
||||
{selectedFiles[0].uploaded ? (
|
||||
<div
|
||||
className="absolute right-2 top-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
|
||||
className="absolute top-2 right-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
|
||||
onClick={() => handleRemove(0)}>
|
||||
<XIcon className="h-5 text-slate-700 hover:text-slate-900" />
|
||||
</div>
|
||||
@@ -311,7 +311,7 @@ export const FileInput = ({
|
||||
</p>
|
||||
{selectedFiles[0].uploaded ? (
|
||||
<div
|
||||
className="absolute right-2 top-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
|
||||
className="absolute top-2 right-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
|
||||
onClick={() => handleRemove(0)}>
|
||||
<XIcon className="h-5 text-slate-700 hover:text-slate-900" />
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export default defineConfig({
|
||||
"**/mocks/**", // Mock directories
|
||||
"**/__mocks__/**", // Jest-style mock directories
|
||||
"**/constants.ts", // Constants files
|
||||
"**/route.ts", // Next.js API routes
|
||||
"**/route.{ts,tsx}", // Next.js API routes
|
||||
"**/openapi.ts", // OpenAPI spec files
|
||||
"**/openapi-document.ts", // OpenAPI-related document files
|
||||
"**/types/**", // Type definition folders
|
||||
@@ -37,6 +37,7 @@ export default defineConfig({
|
||||
"**/instrumentation.ts", // Next.js instrumentation files
|
||||
"**/instrumentation-node.ts", // Next.js Node.js instrumentation files
|
||||
"**/vitestSetup.ts", // Vitest setup files
|
||||
"**/*.setup.*", // Vitest setup files
|
||||
"**/*.json", // JSON files
|
||||
"**/*.mdx", // MDX files
|
||||
"**/playwright/**", // Playwright E2E test files
|
||||
@@ -74,6 +75,7 @@ export default defineConfig({
|
||||
"lib/airtable/**",
|
||||
"app/api/v1/integrations/**",
|
||||
"lib/env.ts",
|
||||
"**/cache/**",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
interface LabelProps {
|
||||
text: string;
|
||||
htmlForId?: string;
|
||||
}
|
||||
|
||||
export function Label({ text }: Readonly<LabelProps>) {
|
||||
return <label className="fb-text-subheading fb-font-normal fb-text-sm">{text}</label>;
|
||||
export function Label({ text, htmlForId }: Readonly<LabelProps>) {
|
||||
return (
|
||||
<label htmlFor={htmlForId} className="fb-text-subheading fb-font-normal fb-text-sm">
|
||||
{text}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
import { getUpdatedTtc } from "@/lib/ttc";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { type TSurveyAddressQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { AddressQuestion } from "./address-question";
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: vi
|
||||
.fn()
|
||||
.mockImplementation((val, lang) => (typeof val === "object" ? val[lang] || val.default : val)),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
getUpdatedTtc: vi.fn().mockReturnValue({}),
|
||||
useTtc: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockOnChange = vi.fn();
|
||||
const mockOnSubmit = vi.fn();
|
||||
const mockOnBack = vi.fn();
|
||||
const mockSetTtc = vi.fn();
|
||||
|
||||
describe("AddressQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockQuestion: TSurveyAddressQuestion = {
|
||||
id: "address-1",
|
||||
type: TSurveyQuestionTypeEnum.Address,
|
||||
headline: { default: "Address Question" },
|
||||
subheader: { default: "Enter your address" },
|
||||
required: true,
|
||||
buttonLabel: { default: "Submit" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
addressLine1: { show: true, required: false, placeholder: { default: "Address Line 1" } },
|
||||
addressLine2: { show: true, required: false, placeholder: { default: "Address Line 2" } },
|
||||
city: { show: true, required: false, placeholder: { default: "City" } },
|
||||
state: { show: true, required: false, placeholder: { default: "State" } },
|
||||
zip: { show: true, required: false, placeholder: { default: "ZIP" } },
|
||||
country: { show: true, required: false, placeholder: { default: "Country" } },
|
||||
};
|
||||
|
||||
test("renders the address question with all fields", () => {
|
||||
render(
|
||||
<AddressQuestion
|
||||
question={mockQuestion}
|
||||
onChange={mockOnChange}
|
||||
onSubmit={mockOnSubmit}
|
||||
onBack={mockOnBack}
|
||||
isFirstQuestion={false}
|
||||
isLastQuestion={false}
|
||||
languageCode="default"
|
||||
ttc={{}}
|
||||
setTtc={mockSetTtc}
|
||||
currentQuestionId="address-1"
|
||||
autoFocusEnabled={true}
|
||||
isBackButtonHidden={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Address Question")).toBeInTheDocument();
|
||||
expect(screen.getByText("Enter your address")).toBeInTheDocument();
|
||||
expect(screen.getByText("Address Line 1*")).toBeInTheDocument();
|
||||
expect(screen.getByText("Address Line 2*")).toBeInTheDocument();
|
||||
expect(screen.getByText("City*")).toBeInTheDocument();
|
||||
expect(screen.getByText("State*")).toBeInTheDocument();
|
||||
expect(screen.getByText("ZIP*")).toBeInTheDocument();
|
||||
expect(screen.getByText("Country*")).toBeInTheDocument();
|
||||
expect(screen.getByText("Submit")).toBeInTheDocument();
|
||||
expect(screen.getByText("Back")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders question with media when available", () => {
|
||||
const questionWithMedia = {
|
||||
...mockQuestion,
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
};
|
||||
|
||||
render(
|
||||
<AddressQuestion
|
||||
question={questionWithMedia}
|
||||
onChange={mockOnChange}
|
||||
onSubmit={mockOnSubmit}
|
||||
onBack={mockOnBack}
|
||||
isFirstQuestion={false}
|
||||
isLastQuestion={false}
|
||||
languageCode="default"
|
||||
ttc={{}}
|
||||
setTtc={mockSetTtc}
|
||||
currentQuestionId="address-1"
|
||||
autoFocusEnabled={true}
|
||||
isBackButtonHidden={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("img")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("updates value when fields are changed", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<AddressQuestion
|
||||
question={mockQuestion}
|
||||
onChange={mockOnChange}
|
||||
onSubmit={mockOnSubmit}
|
||||
onBack={mockOnBack}
|
||||
isFirstQuestion={false}
|
||||
isLastQuestion={false}
|
||||
languageCode="default"
|
||||
ttc={{}}
|
||||
setTtc={mockSetTtc}
|
||||
currentQuestionId="address-1"
|
||||
autoFocusEnabled={true}
|
||||
isBackButtonHidden={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const addressLine1Input = screen.getByLabelText("Address Line 1*");
|
||||
await user.type(addressLine1Input, "123 Main St");
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
"address-1": ["123 Main St", "", "", "", "", ""],
|
||||
});
|
||||
});
|
||||
|
||||
test("submits data when form is submitted", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getUpdatedTtc).mockReturnValue({ "address-1": 1000 });
|
||||
|
||||
render(
|
||||
<AddressQuestion
|
||||
question={mockQuestion}
|
||||
value={["123 Main St", "Apt 4", "City", "State", "12345", "Country"]}
|
||||
onChange={mockOnChange}
|
||||
onSubmit={mockOnSubmit}
|
||||
onBack={mockOnBack}
|
||||
isFirstQuestion={false}
|
||||
isLastQuestion={false}
|
||||
languageCode="default"
|
||||
ttc={{}}
|
||||
setTtc={mockSetTtc}
|
||||
currentQuestionId="address-1"
|
||||
autoFocusEnabled={true}
|
||||
isBackButtonHidden={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const submitButton = screen.getByText("Submit");
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(getUpdatedTtc).toHaveBeenCalled();
|
||||
expect(mockSetTtc).toHaveBeenCalledWith({ "address-1": 1000 });
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith(
|
||||
{ "address-1": ["123 Main St", "Apt 4", "City", "State", "12345", "Country"] },
|
||||
{ "address-1": 1000 }
|
||||
);
|
||||
});
|
||||
|
||||
test("submits empty array when all fields are empty", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getUpdatedTtc).mockReturnValue({ "address-1": 1000 });
|
||||
|
||||
// Create a modified question with no required fields to allow empty submission
|
||||
const nonRequiredQuestion = {
|
||||
...mockQuestion,
|
||||
required: false,
|
||||
addressLine1: { ...mockQuestion.addressLine1, required: false },
|
||||
addressLine2: { ...mockQuestion.addressLine2, required: false },
|
||||
city: { ...mockQuestion.city, required: false },
|
||||
state: { ...mockQuestion.state, required: false },
|
||||
zip: { ...mockQuestion.zip, required: false },
|
||||
country: { ...mockQuestion.country, required: false },
|
||||
};
|
||||
|
||||
render(
|
||||
<AddressQuestion
|
||||
question={nonRequiredQuestion}
|
||||
value={["", "", "", "", "", ""]}
|
||||
onChange={mockOnChange}
|
||||
onSubmit={mockOnSubmit}
|
||||
onBack={mockOnBack}
|
||||
isFirstQuestion={false}
|
||||
isLastQuestion={false}
|
||||
languageCode="default"
|
||||
ttc={{}}
|
||||
setTtc={mockSetTtc}
|
||||
currentQuestionId="address-1"
|
||||
autoFocusEnabled={true}
|
||||
isBackButtonHidden={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const submitButton = screen.getByRole("button", { name: "Submit" });
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(getUpdatedTtc).toHaveBeenCalled();
|
||||
expect(mockSetTtc).toHaveBeenCalledWith({ "address-1": 1000 });
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith({ "address-1": [] }, { "address-1": 1000 });
|
||||
});
|
||||
|
||||
test("calls onBack when back button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getUpdatedTtc).mockReturnValue({ "address-1": 500 });
|
||||
|
||||
render(
|
||||
<AddressQuestion
|
||||
question={mockQuestion}
|
||||
onChange={mockOnChange}
|
||||
onSubmit={mockOnSubmit}
|
||||
onBack={mockOnBack}
|
||||
isFirstQuestion={false}
|
||||
isLastQuestion={false}
|
||||
languageCode="default"
|
||||
ttc={{}}
|
||||
setTtc={mockSetTtc}
|
||||
currentQuestionId="address-1"
|
||||
autoFocusEnabled={true}
|
||||
isBackButtonHidden={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const backButton = screen.getByText("Back");
|
||||
await user.click(backButton);
|
||||
|
||||
expect(getUpdatedTtc).toHaveBeenCalled();
|
||||
expect(mockSetTtc).toHaveBeenCalledWith({ "address-1": 500 });
|
||||
expect(mockOnBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("doesn't render back button when isFirstQuestion is true", () => {
|
||||
render(
|
||||
<AddressQuestion
|
||||
question={mockQuestion}
|
||||
onChange={mockOnChange}
|
||||
onSubmit={mockOnSubmit}
|
||||
onBack={mockOnBack}
|
||||
isFirstQuestion={true}
|
||||
isLastQuestion={false}
|
||||
languageCode="default"
|
||||
ttc={{}}
|
||||
setTtc={mockSetTtc}
|
||||
currentQuestionId="address-1"
|
||||
autoFocusEnabled={true}
|
||||
isBackButtonHidden={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles field visibility based on question config", () => {
|
||||
const customQuestion = {
|
||||
...mockQuestion,
|
||||
addressLine2: { ...mockQuestion.addressLine2, show: false },
|
||||
state: { ...mockQuestion.state, show: false },
|
||||
};
|
||||
|
||||
render(
|
||||
<AddressQuestion
|
||||
question={customQuestion}
|
||||
onChange={mockOnChange}
|
||||
onSubmit={mockOnSubmit}
|
||||
onBack={mockOnBack}
|
||||
isFirstQuestion={false}
|
||||
isLastQuestion={false}
|
||||
languageCode="default"
|
||||
ttc={{}}
|
||||
setTtc={mockSetTtc}
|
||||
currentQuestionId="address-1"
|
||||
autoFocusEnabled={true}
|
||||
isBackButtonHidden={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Address Line 1*")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Address Line 2")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("State*")).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText("City*")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles required fields correctly", () => {
|
||||
const customQuestion = {
|
||||
...mockQuestion,
|
||||
required: false,
|
||||
addressLine1: { ...mockQuestion.addressLine1, required: true },
|
||||
};
|
||||
|
||||
render(
|
||||
<AddressQuestion
|
||||
question={customQuestion}
|
||||
onChange={mockOnChange}
|
||||
onSubmit={mockOnSubmit}
|
||||
onBack={mockOnBack}
|
||||
isFirstQuestion={false}
|
||||
isLastQuestion={false}
|
||||
languageCode="default"
|
||||
ttc={{}}
|
||||
setTtc={mockSetTtc}
|
||||
currentQuestionId="address-1"
|
||||
autoFocusEnabled={true}
|
||||
isBackButtonHidden={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Address Line 1*")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("City")).toBeInTheDocument(); // Not required anymore
|
||||
});
|
||||
|
||||
test("auto focuses the first field when autoFocusEnabled is true", () => {
|
||||
render(
|
||||
<AddressQuestion
|
||||
question={mockQuestion}
|
||||
onChange={mockOnChange}
|
||||
onSubmit={mockOnSubmit}
|
||||
onBack={mockOnBack}
|
||||
isFirstQuestion={false}
|
||||
isLastQuestion={false}
|
||||
languageCode="default"
|
||||
ttc={{}}
|
||||
setTtc={mockSetTtc}
|
||||
currentQuestionId="address-1"
|
||||
autoFocusEnabled={true}
|
||||
isBackButtonHidden={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const addressLine1Input = screen.getByLabelText("Address Line 1*");
|
||||
expect(document.activeElement).toBe(addressLine1Input);
|
||||
});
|
||||
});
|
||||
@@ -157,8 +157,9 @@ export function AddressQuestion({
|
||||
return (
|
||||
field.show && (
|
||||
<div className="fb-space-y-1">
|
||||
<Label text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<Label htmlForId={field.id} text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<Input
|
||||
id={field.id}
|
||||
key={field.id}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/preact";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurveyCalQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { CalEmbed } from "../general/cal-embed";
|
||||
import { CalQuestion } from "./cal-question";
|
||||
|
||||
// Mock the CalEmbed component
|
||||
vi.mock("../general/cal-embed", () => ({
|
||||
CalEmbed: vi.fn(({ question }) => (
|
||||
<div data-testid="cal-embed-mock">
|
||||
Cal Embed for {question.calUserName}
|
||||
{question.calHost && <span>Host: {question.calHost}</span>}
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
describe("CalQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockQuestion: TSurveyCalQuestion = {
|
||||
id: "cal-question-1",
|
||||
type: TSurveyQuestionTypeEnum.Cal,
|
||||
headline: { default: "Schedule a meeting" },
|
||||
subheader: { default: "Choose a time that works for you" },
|
||||
required: true,
|
||||
calUserName: "johndoe",
|
||||
calHost: "cal.com",
|
||||
};
|
||||
|
||||
const mockQuestionWithoutHost: TSurveyCalQuestion = {
|
||||
id: "cal-question-2",
|
||||
type: TSurveyQuestionTypeEnum.Cal,
|
||||
headline: { default: "Schedule a meeting" },
|
||||
required: false,
|
||||
calUserName: "janedoe",
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
question: mockQuestion,
|
||||
value: null,
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
isInvalid: false,
|
||||
direction: "vertical" as const,
|
||||
languageCode: "en",
|
||||
} as any;
|
||||
|
||||
test("renders with headline and subheader", () => {
|
||||
render(<CalQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Schedule a meeting")).toBeInTheDocument();
|
||||
expect(screen.getByText("Choose a time that works for you")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders without subheader", () => {
|
||||
render(<CalQuestion {...defaultProps} question={mockQuestionWithoutHost} />);
|
||||
|
||||
expect(screen.getByText("Schedule a meeting")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Choose a time that works for you")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders CalEmbed component with correct props", () => {
|
||||
render(<CalQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("cal-embed-mock")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cal Embed for johndoe")).toBeInTheDocument();
|
||||
expect(screen.getByText("Host: cal.com")).toBeInTheDocument();
|
||||
expect(CalEmbed).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
question: mockQuestion,
|
||||
onSuccessfulBooking: expect.any(Function),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
test("renders CalEmbed without host when not provided", () => {
|
||||
render(<CalQuestion {...defaultProps} question={mockQuestionWithoutHost} />);
|
||||
|
||||
expect(screen.getByTestId("cal-embed-mock")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cal Embed for janedoe")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Host:/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not add required indicator when question is optional", () => {
|
||||
render(<CalQuestion {...defaultProps} question={mockQuestionWithoutHost} />);
|
||||
|
||||
expect(screen.queryByText("*")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import { getUpdatedTtc } from "@/lib/ttc";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { type TSurveyConsentQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { ConsentQuestion } from "./consent-question";
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
useTtc: vi.fn(),
|
||||
getUpdatedTtc: vi.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/question-media", () => ({
|
||||
QuestionMedia: () => <div data-testid="question-media">Question Media</div>,
|
||||
}));
|
||||
|
||||
describe("ConsentQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockQuestion: TSurveyConsentQuestion = {
|
||||
id: "consent-q",
|
||||
type: TSurveyQuestionTypeEnum.Consent,
|
||||
headline: { default: "Consent Headline" },
|
||||
html: { default: "This is the consent text" },
|
||||
label: { default: "I agree to the terms" },
|
||||
buttonLabel: { default: "Submit" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
required: true,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
question: mockQuestion,
|
||||
value: "",
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
languageCode: "en",
|
||||
ttc: {},
|
||||
setTtc: vi.fn(),
|
||||
autoFocusEnabled: false,
|
||||
currentQuestionId: "consent-q",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
test("renders consent question correctly", () => {
|
||||
render(<ConsentQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Consent Headline")).toBeInTheDocument();
|
||||
expect(screen.getByText("I agree to the terms")).toBeInTheDocument();
|
||||
expect(screen.getByText("Submit")).toBeInTheDocument();
|
||||
expect(screen.getByText("Back")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with media when available", () => {
|
||||
const questionWithMedia = {
|
||||
...mockQuestion,
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
};
|
||||
|
||||
render(<ConsentQuestion {...defaultProps} question={questionWithMedia} />);
|
||||
|
||||
expect(screen.getByTestId("question-media")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("checkbox changes value when clicked", async () => {
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(<ConsentQuestion {...defaultProps} onChange={onChange} />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ "consent-q": "accepted" });
|
||||
|
||||
onChange.mockReset();
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ "consent-q": "" });
|
||||
});
|
||||
|
||||
test("submits form with correct data", async () => {
|
||||
const onSubmit = vi.fn();
|
||||
|
||||
render(<ConsentQuestion {...defaultProps} value="accepted" onSubmit={onSubmit} />);
|
||||
|
||||
const submitButton = screen.getByText("Submit");
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
expect(getUpdatedTtc).toHaveBeenCalled();
|
||||
expect(onSubmit).toHaveBeenCalledWith({ "consent-q": "accepted" }, {});
|
||||
});
|
||||
|
||||
test("back button triggers onBack handler", async () => {
|
||||
const onBack = vi.fn();
|
||||
|
||||
render(<ConsentQuestion {...defaultProps} onBack={onBack} />);
|
||||
|
||||
const backButton = screen.getByText("Back");
|
||||
await userEvent.click(backButton);
|
||||
|
||||
expect(getUpdatedTtc).toHaveBeenCalled();
|
||||
expect(onBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("back button is not rendered when isFirstQuestion is true", () => {
|
||||
render(<ConsentQuestion {...defaultProps} isFirstQuestion={true} />);
|
||||
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("back button is not rendered when isBackButtonHidden is true", () => {
|
||||
render(<ConsentQuestion {...defaultProps} isBackButtonHidden={true} />);
|
||||
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles keyboard space press on label", () => {
|
||||
render(<ConsentQuestion {...defaultProps} />);
|
||||
|
||||
const label = screen.getByText("I agree to the terms").closest("label");
|
||||
|
||||
fireEvent.keyDown(label!, { key: " " });
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({ "consent-q": "accepted" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
import { getUpdatedTtc } from "@/lib/ttc";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { type TSurveyContactInfoQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { ContactInfoQuestion } from "./contact-info-question";
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: vi.fn().mockImplementation((obj, lang) => obj[lang] ?? obj.default ?? ""),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
getUpdatedTtc: vi.fn().mockReturnValue({}),
|
||||
useTtc: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/buttons/back-button", () => ({
|
||||
BackButton: ({ onClick, backButtonLabel }: { onClick: () => void; backButtonLabel: string }) => (
|
||||
<button onClick={onClick} data-testid="back-button">
|
||||
{backButtonLabel}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/buttons/submit-button", () => ({
|
||||
SubmitButton: ({ buttonLabel }: { buttonLabel: string }) => (
|
||||
<button type="submit" data-testid="submit-button">
|
||||
{buttonLabel}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/headline", () => ({
|
||||
Headline: ({ headline }: { headline: string }) => <h1 data-testid="headline">{headline}</h1>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/subheader", () => ({
|
||||
Subheader: ({ subheader }: { subheader: string }) => <p data-testid="subheader">{subheader}</p>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/question-media", () => ({
|
||||
QuestionMedia: ({ imgUrl, videoUrl }: { imgUrl?: string; videoUrl?: string }) => (
|
||||
<div data-testid="question-media" data-img={imgUrl} data-video={videoUrl} />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/wrappers/scrollable-container", () => ({
|
||||
ScrollableContainer: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="scrollable-container">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("ContactInfoQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockQuestion: TSurveyContactInfoQuestion = {
|
||||
id: "contact-info-q",
|
||||
type: TSurveyQuestionTypeEnum.ContactInfo,
|
||||
headline: {
|
||||
default: "Contact Information",
|
||||
en: "Contact Information",
|
||||
},
|
||||
subheader: {
|
||||
default: "Please provide your contact info",
|
||||
en: "Please provide your contact info",
|
||||
},
|
||||
required: true,
|
||||
buttonLabel: {
|
||||
default: "Next",
|
||||
en: "Next",
|
||||
},
|
||||
backButtonLabel: {
|
||||
default: "Back",
|
||||
en: "Back",
|
||||
},
|
||||
imageUrl: "test-image-url",
|
||||
firstName: {
|
||||
show: true,
|
||||
required: true,
|
||||
placeholder: {
|
||||
default: "First Name",
|
||||
en: "First Name",
|
||||
},
|
||||
},
|
||||
lastName: {
|
||||
show: true,
|
||||
required: false,
|
||||
placeholder: {
|
||||
default: "Last Name",
|
||||
en: "Last Name",
|
||||
},
|
||||
},
|
||||
email: {
|
||||
show: true,
|
||||
required: true,
|
||||
placeholder: {
|
||||
default: "Email",
|
||||
en: "Email",
|
||||
},
|
||||
},
|
||||
phone: {
|
||||
show: false,
|
||||
required: false,
|
||||
placeholder: {
|
||||
default: "Phone",
|
||||
en: "Phone",
|
||||
},
|
||||
},
|
||||
company: {
|
||||
show: false,
|
||||
required: false,
|
||||
placeholder: {
|
||||
default: "Company",
|
||||
en: "Company",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
question: mockQuestion,
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
languageCode: "en",
|
||||
ttc: {},
|
||||
setTtc: vi.fn(),
|
||||
currentQuestionId: "contact-info-q",
|
||||
autoFocusEnabled: true,
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
test("renders contact info question correctly", () => {
|
||||
render(<ContactInfoQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("headline")).toHaveTextContent("Contact Information");
|
||||
expect(screen.getByTestId("subheader")).toHaveTextContent("Please provide your contact info");
|
||||
expect(screen.getByTestId("question-media")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("First Name*")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Last Name")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Email*")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Phone")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Company")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles input changes correctly", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ContactInfoQuestion {...defaultProps} />);
|
||||
|
||||
const firstNameInput = screen.getByLabelText("First Name*");
|
||||
await user.type(firstNameInput, "John");
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
"contact-info-q": ["John", "", "", "", ""],
|
||||
});
|
||||
|
||||
const emailInput = screen.getByLabelText("Email*");
|
||||
await user.type(emailInput, "john@example.com");
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
"contact-info-q": ["", "", "john@example.com", "", ""],
|
||||
});
|
||||
});
|
||||
|
||||
test("handles form submission with values", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getUpdatedTtc).mockReturnValueOnce({ "contact-info-q": 100 });
|
||||
|
||||
render(<ContactInfoQuestion {...defaultProps} value={["John", "Doe", "john@example.com", "", ""]} />);
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(defaultProps.onSubmit).toHaveBeenCalledWith(
|
||||
{ "contact-info-q": ["John", "Doe", "john@example.com", "", ""] },
|
||||
{ "contact-info-q": 100 }
|
||||
);
|
||||
});
|
||||
|
||||
test("handles form submission with empty values", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getUpdatedTtc).mockReturnValueOnce({ "contact-info-q": 100 });
|
||||
|
||||
const onSubmitMock = vi.fn();
|
||||
const { container } = render(
|
||||
<ContactInfoQuestion {...defaultProps} value={["", "", "", "", ""]} onSubmit={onSubmitMock} />
|
||||
);
|
||||
|
||||
// Get the form element and submit it directly
|
||||
const form = container.querySelector("form");
|
||||
expect(form).not.toBeNull();
|
||||
|
||||
// Trigger the submit event directly on the form
|
||||
await user.click(screen.getByTestId("submit-button"));
|
||||
|
||||
// Manually trigger the form submission event as a fallback
|
||||
if (form && onSubmitMock.mock.calls.length === 0) {
|
||||
const submitEvent = new Event("submit", { bubbles: true, cancelable: true });
|
||||
form.dispatchEvent(submitEvent);
|
||||
}
|
||||
|
||||
expect(onSubmitMock).toHaveBeenCalledWith({ "contact-info-q": [] }, { "contact-info-q": 100 });
|
||||
});
|
||||
|
||||
test("handles back button click", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getUpdatedTtc).mockReturnValueOnce({ "contact-info-q": 100 });
|
||||
|
||||
render(<ContactInfoQuestion {...defaultProps} />);
|
||||
|
||||
const backButton = screen.getByTestId("back-button");
|
||||
await user.click(backButton);
|
||||
|
||||
expect(defaultProps.onBack).toHaveBeenCalled();
|
||||
expect(defaultProps.setTtc).toHaveBeenCalledWith({ "contact-info-q": 100 });
|
||||
});
|
||||
|
||||
test("hides back button when isFirstQuestion is true", () => {
|
||||
render(<ContactInfoQuestion {...defaultProps} isFirstQuestion={true} />);
|
||||
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides back button when isBackButtonHidden is true", () => {
|
||||
render(<ContactInfoQuestion {...defaultProps} isBackButtonHidden={true} />);
|
||||
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders without media when not available", () => {
|
||||
const questionWithoutMedia = {
|
||||
...mockQuestion,
|
||||
imageUrl: undefined,
|
||||
videoUrl: undefined,
|
||||
};
|
||||
|
||||
render(<ContactInfoQuestion {...defaultProps} question={questionWithoutMedia} />);
|
||||
|
||||
expect(screen.queryByTestId("question-media")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles different field types correctly", () => {
|
||||
const questionWithAllFields = {
|
||||
...mockQuestion,
|
||||
phone: {
|
||||
...mockQuestion.phone,
|
||||
show: true,
|
||||
},
|
||||
company: {
|
||||
...mockQuestion.company,
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ContactInfoQuestion {...defaultProps} question={questionWithAllFields} />);
|
||||
|
||||
expect(screen.getByLabelText("First Name*")).toHaveAttribute("type", "text");
|
||||
expect(screen.getByLabelText("Last Name")).toHaveAttribute("type", "text");
|
||||
expect(screen.getByLabelText("Email*")).toHaveAttribute("type", "email");
|
||||
expect(screen.getByLabelText("Phone")).toHaveAttribute("type", "number");
|
||||
expect(screen.getByLabelText("Company")).toHaveAttribute("type", "text");
|
||||
});
|
||||
});
|
||||
@@ -159,8 +159,9 @@ export function ContactInfoQuestion({
|
||||
return (
|
||||
field.show && (
|
||||
<div className="fb-space-y-1">
|
||||
<Label text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<Label htmlForId={field.id} text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<Input
|
||||
id={field.id}
|
||||
ref={index === 0 ? contactInfoRef : null}
|
||||
key={field.id}
|
||||
required={isFieldRequired()}
|
||||
|
||||
199
packages/surveys/src/components/questions/cta-question.test.tsx
Normal file
199
packages/surveys/src/components/questions/cta-question.test.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import { getUpdatedTtc } from "@/lib/ttc";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { type TSurveyCTAQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { CTAQuestion } from "./cta-question";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/components/buttons/back-button", () => ({
|
||||
BackButton: vi.fn(({ onClick, backButtonLabel, tabIndex }) => (
|
||||
<button onClick={onClick} data-testid="back-button" tabIndex={tabIndex}>
|
||||
{backButtonLabel || "Back"}
|
||||
</button>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/buttons/submit-button", () => ({
|
||||
SubmitButton: vi.fn(({ onClick, buttonLabel, tabIndex }) => (
|
||||
<button onClick={onClick} data-testid="submit-button" tabIndex={tabIndex}>
|
||||
{buttonLabel || "Submit"}
|
||||
</button>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/headline", () => ({
|
||||
Headline: vi.fn(({ headline }) => <div data-testid="headline">{headline}</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/html-body", () => ({
|
||||
HtmlBody: vi.fn(({ htmlString }) => <div data-testid="html-body">{htmlString}</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/question-media", () => ({
|
||||
QuestionMedia: vi.fn(() => <div data-testid="question-media">Media</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/wrappers/scrollable-container", () => ({
|
||||
ScrollableContainer: vi.fn(({ children }) => <div data-testid="scrollable-container">{children}</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: vi.fn((value) => value),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
getUpdatedTtc: vi.fn(() => ({})),
|
||||
useTtc: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("CTAQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
const mockQuestion: TSurveyCTAQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
headline: { default: "Test Headline" },
|
||||
html: { default: "Test HTML content" },
|
||||
buttonLabel: { default: "Click Me" },
|
||||
dismissButtonLabel: { default: "Skip This" },
|
||||
backButtonLabel: { default: "Go Back" },
|
||||
required: false,
|
||||
buttonExternal: false,
|
||||
buttonUrl: "",
|
||||
};
|
||||
|
||||
const mockProps = {
|
||||
question: mockQuestion,
|
||||
value: "",
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
languageCode: "en",
|
||||
ttc: {},
|
||||
setTtc: vi.fn(),
|
||||
autoFocusEnabled: false,
|
||||
currentQuestionId: "q1",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
test("renders correctly without media", () => {
|
||||
render(<CTAQuestion {...mockProps} />);
|
||||
expect(screen.getByTestId("headline")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("html-body")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("back-button")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("submit-button")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("question-media")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly with image media", () => {
|
||||
const questionWithMedia = {
|
||||
...mockQuestion,
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
};
|
||||
render(<CTAQuestion {...mockProps} question={questionWithMedia} />);
|
||||
expect(screen.getByTestId("question-media")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly with video media", () => {
|
||||
const questionWithMedia = {
|
||||
...mockQuestion,
|
||||
videoUrl: "https://example.com/video.mp4",
|
||||
};
|
||||
render(<CTAQuestion {...mockProps} question={questionWithMedia} />);
|
||||
expect(screen.getByTestId("question-media")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not show back button when isFirstQuestion is true", () => {
|
||||
render(<CTAQuestion {...mockProps} isFirstQuestion={true} />);
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not show back button when isBackButtonHidden is true", () => {
|
||||
render(<CTAQuestion {...mockProps} isBackButtonHidden={true} />);
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onSubmit and onChange when submit button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CTAQuestion {...mockProps} />);
|
||||
await user.click(screen.getByTestId("submit-button"));
|
||||
expect(mockProps.onSubmit).toHaveBeenCalledWith({ q1: "clicked" }, {});
|
||||
expect(mockProps.onChange).toHaveBeenCalledWith({ q1: "clicked" });
|
||||
});
|
||||
|
||||
test("calls onBack when back button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CTAQuestion {...mockProps} />);
|
||||
await user.click(screen.getByTestId("back-button"));
|
||||
expect(mockProps.onBack).toHaveBeenCalled();
|
||||
expect(vi.mocked(getUpdatedTtc)).toHaveBeenCalled();
|
||||
expect(mockProps.setTtc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not show skip button when question is required", () => {
|
||||
const requiredQuestion = {
|
||||
...mockQuestion,
|
||||
required: true,
|
||||
};
|
||||
render(<CTAQuestion {...mockProps} question={requiredQuestion} />);
|
||||
|
||||
// There should only be 2 buttons (submit and back) when required is true
|
||||
const buttons = screen.getAllByRole("button");
|
||||
expect(buttons.length).toBe(2);
|
||||
});
|
||||
|
||||
test("opens external URL when buttonExternal is true", async () => {
|
||||
const mockOpenExternalURL = vi.fn();
|
||||
const externalQuestion = {
|
||||
...mockQuestion,
|
||||
buttonExternal: true,
|
||||
buttonUrl: "https://example.com",
|
||||
};
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<CTAQuestion {...mockProps} question={externalQuestion} onOpenExternalURL={mockOpenExternalURL} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId("submit-button"));
|
||||
expect(mockOpenExternalURL).toHaveBeenCalledWith("https://example.com");
|
||||
});
|
||||
|
||||
test("falls back to window.open when onOpenExternalURL is not provided", async () => {
|
||||
const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => {
|
||||
return { focus: vi.fn() } as unknown as Window;
|
||||
});
|
||||
|
||||
const externalQuestion = {
|
||||
...mockQuestion,
|
||||
buttonExternal: true,
|
||||
buttonUrl: "https://example.com",
|
||||
};
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<CTAQuestion {...mockProps} question={externalQuestion} />);
|
||||
|
||||
await user.click(screen.getByTestId("submit-button"));
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith("https://example.com", "_blank");
|
||||
windowOpenSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("sets tab index correctly when isCurrent is true", () => {
|
||||
render(<CTAQuestion {...mockProps} currentQuestionId="q1" />);
|
||||
expect(screen.getByTestId("submit-button")).toHaveAttribute("tabindex", "0");
|
||||
expect(screen.getByTestId("back-button")).toHaveAttribute("tabindex", "0");
|
||||
});
|
||||
|
||||
test("sets tab index to -1 when isCurrent is false", () => {
|
||||
render(<CTAQuestion {...mockProps} currentQuestionId="q2" />);
|
||||
expect(screen.getByTestId("submit-button")).toHaveAttribute("tabindex", "-1");
|
||||
expect(screen.getByTestId("back-button")).toHaveAttribute("tabindex", "-1");
|
||||
});
|
||||
});
|
||||
149
packages/surveys/src/components/questions/date-question.test.tsx
Normal file
149
packages/surveys/src/components/questions/date-question.test.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { DateQuestion } from "./date-question";
|
||||
|
||||
// Mock react-date-picker
|
||||
vi.mock("react-date-picker", () => ({
|
||||
default: vi.fn(({ onChange, value }) => (
|
||||
<div data-testid="date-picker-mock">
|
||||
<button data-testid="date-select-button" onClick={() => onChange(new Date("2023-01-15"))}>
|
||||
Select Date
|
||||
</button>
|
||||
<span>{value ? value.toISOString() : "No date selected"}</span>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
useTtc: vi.fn(),
|
||||
getUpdatedTtc: vi.fn().mockReturnValue({ mockUpdatedTtc: true }),
|
||||
}));
|
||||
|
||||
describe("DateQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockQuestion = {
|
||||
id: "date-question-1",
|
||||
type: TSurveyQuestionTypeEnum.Date,
|
||||
headline: { default: "Select a date" },
|
||||
subheader: { default: "Please choose a date" },
|
||||
required: true,
|
||||
buttonLabel: { default: "Next" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
question: mockQuestion,
|
||||
value: "",
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
languageCode: "en",
|
||||
ttc: {},
|
||||
setTtc: vi.fn(),
|
||||
autoFocusEnabled: false,
|
||||
currentQuestionId: "date-question-1",
|
||||
isBackButtonHidden: false,
|
||||
} as any;
|
||||
|
||||
test("renders date question correctly", () => {
|
||||
render(<DateQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.getAllByText("Select a date")[0]).toBeInTheDocument();
|
||||
expect(screen.getByText("Please choose a date")).toBeInTheDocument();
|
||||
expect(screen.getByText("Next")).toBeInTheDocument();
|
||||
expect(screen.getByText("Back")).toBeInTheDocument();
|
||||
expect(screen.getByText("Select a date", { selector: "span" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays error message when form is submitted without a date if required", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<DateQuestion {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByText("Next"));
|
||||
|
||||
expect(screen.getByText("Please select a date.")).toBeInTheDocument();
|
||||
expect(defaultProps.onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls onSubmit when form is submitted with a valid date", async () => {
|
||||
const user = userEvent.setup();
|
||||
const testDate = "2023-01-15";
|
||||
const props = { ...defaultProps, value: testDate };
|
||||
|
||||
render(<DateQuestion {...props} />);
|
||||
|
||||
await user.click(screen.getByText("Next"));
|
||||
|
||||
expect(props.onSubmit).toHaveBeenCalledWith({ "date-question-1": testDate }, expect.anything());
|
||||
});
|
||||
|
||||
test("calls onBack when back button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<DateQuestion {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByText("Back"));
|
||||
|
||||
expect(defaultProps.onBack).toHaveBeenCalledTimes(1);
|
||||
expect(defaultProps.setTtc).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("does not render back button when isFirstQuestion is true", () => {
|
||||
render(<DateQuestion {...defaultProps} isFirstQuestion={true} />);
|
||||
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not render back button when isBackButtonHidden is true", () => {
|
||||
render(<DateQuestion {...defaultProps} isBackButtonHidden={true} />);
|
||||
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders media content when available", () => {
|
||||
const questionWithMedia = {
|
||||
...mockQuestion,
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
};
|
||||
|
||||
render(<DateQuestion {...defaultProps} question={questionWithMedia} />);
|
||||
|
||||
// Media component should be rendered (implementation detail check)
|
||||
expect(screen.getAllByText("Select a date")[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("opens date picker when button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<DateQuestion {...defaultProps} />);
|
||||
|
||||
// Click the select date button
|
||||
const dateButton = screen.getByRole("button", { name: /select a date/i });
|
||||
await user.click(dateButton);
|
||||
|
||||
// We can check for our mocked date picker
|
||||
expect(screen.getByTestId("date-picker-mock")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays formatted date when a date is selected", async () => {
|
||||
const dateValue = "2023-01-15";
|
||||
const props = { ...defaultProps, value: dateValue };
|
||||
|
||||
render(<DateQuestion {...props} />);
|
||||
|
||||
// Handle timezone differences by allowing either 14th or 15th
|
||||
const dateRegex = /(14th|15th) of January, 2023/;
|
||||
const dateElement = screen.getByText(dateRegex);
|
||||
expect(dateElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,235 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { type TSurveyFileUploadQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { FileUploadQuestion } from "./file-upload-question";
|
||||
|
||||
vi.mock("@/components/buttons/submit-button", () => ({
|
||||
SubmitButton: ({ buttonLabel, tabIndex }: any) => (
|
||||
<button data-testid="submit-button" tabIndex={tabIndex}>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/headline", () => ({
|
||||
Headline: ({ headline, required }: any) => (
|
||||
<h2 data-testid="headline" data-required={required}>
|
||||
{headline}
|
||||
</h2>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/subheader", () => ({
|
||||
Subheader: ({ subheader }: any) => <p data-testid="subheader">{subheader}</p>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/question-media", () => ({
|
||||
QuestionMedia: ({ imgUrl, videoUrl }: any) => (
|
||||
<div data-testid="question-media" data-img-url={imgUrl} data-video-url={videoUrl}></div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/wrappers/scrollable-container", () => ({
|
||||
ScrollableContainer: ({ children }: any) => <div data-testid="scrollable-container">{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/buttons/back-button", () => ({
|
||||
BackButton: ({ backButtonLabel, onClick, tabIndex }: any) => (
|
||||
<button data-testid="back-button" onClick={onClick} tabIndex={tabIndex}>
|
||||
{backButtonLabel}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/file-input", () => ({
|
||||
FileInput: ({ onUploadCallback, fileUrls }: any) => (
|
||||
<div data-testid="file-input">
|
||||
<button data-testid="upload-button" onClick={() => onUploadCallback(["file-url-1"])}>
|
||||
Upload
|
||||
</button>
|
||||
<div data-testid="file-urls">{fileUrls ? fileUrls.join(",") : ""}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: (value: any, language?: string) => {
|
||||
if (typeof value === "string") return value;
|
||||
if (value?.default) return value.default;
|
||||
return value?.[language ?? "en"] ?? "";
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
useTtc: vi.fn(),
|
||||
getUpdatedTtc: (ttc: any, id: string) => ({ ...ttc, [id]: 1000 }),
|
||||
}));
|
||||
|
||||
// Mock window.alert before tests
|
||||
Object.defineProperty(window, "alert", {
|
||||
writable: true,
|
||||
value: vi.fn(),
|
||||
});
|
||||
|
||||
describe("FileUploadQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockQuestion: TSurveyFileUploadQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.FileUpload,
|
||||
headline: { default: "Upload your file" },
|
||||
subheader: { default: "Please upload a relevant file" },
|
||||
required: true,
|
||||
buttonLabel: { default: "Submit" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
allowMultipleFiles: false,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
question: mockQuestion,
|
||||
value: [],
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
onFileUpload: vi.fn().mockResolvedValue("uploaded-file-url"),
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
surveyId: "survey123",
|
||||
languageCode: "en",
|
||||
ttc: {},
|
||||
setTtc: vi.fn(),
|
||||
autoFocusEnabled: true,
|
||||
currentQuestionId: "q1",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
test("renders correctly with all elements", () => {
|
||||
render(<FileUploadQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("headline")).toHaveTextContent("Upload your file");
|
||||
expect(screen.getByTestId("subheader")).toHaveTextContent("Please upload a relevant file");
|
||||
expect(screen.getByTestId("submit-button")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("back-button")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("file-input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with media when available", () => {
|
||||
const questionWithMedia = {
|
||||
...mockQuestion,
|
||||
imageUrl: "image-url.jpg",
|
||||
};
|
||||
|
||||
render(<FileUploadQuestion {...defaultProps} question={questionWithMedia} />);
|
||||
|
||||
expect(screen.getByTestId("question-media")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("question-media")).toHaveAttribute("data-img-url", "image-url.jpg");
|
||||
});
|
||||
|
||||
test("does not render media when not available", () => {
|
||||
render(<FileUploadQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByTestId("question-media")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides back button when isFirstQuestion is true", () => {
|
||||
render(<FileUploadQuestion {...defaultProps} isFirstQuestion={true} />);
|
||||
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides back button when isBackButtonHidden is true", () => {
|
||||
render(<FileUploadQuestion {...defaultProps} isBackButtonHidden={true} />);
|
||||
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onBack when back button is clicked", async () => {
|
||||
const onBackMock = vi.fn();
|
||||
render(<FileUploadQuestion {...defaultProps} onBack={onBackMock} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId("back-button"));
|
||||
|
||||
expect(onBackMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("calls onChange when file is uploaded", async () => {
|
||||
const onChangeMock = vi.fn();
|
||||
render(<FileUploadQuestion {...defaultProps} onChange={onChangeMock} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId("upload-button"));
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith({ q1: ["file-url-1"] });
|
||||
});
|
||||
|
||||
test("calls onSubmit with value when form is submitted with valid data", () => {
|
||||
const onSubmitMock = vi.fn();
|
||||
const setTtcMock = vi.fn();
|
||||
|
||||
global.performance.now = vi.fn().mockReturnValue(1000);
|
||||
|
||||
const { container } = render(
|
||||
<FileUploadQuestion
|
||||
{...defaultProps}
|
||||
onSubmit={onSubmitMock}
|
||||
setTtc={setTtcMock}
|
||||
value={["file-url-1"]}
|
||||
/>
|
||||
);
|
||||
|
||||
const form = container.querySelector("form");
|
||||
fireEvent.submit(form as HTMLFormElement);
|
||||
|
||||
expect(setTtcMock).toHaveBeenCalled();
|
||||
expect(onSubmitMock).toHaveBeenCalledWith({ q1: ["file-url-1"] }, expect.any(Object));
|
||||
});
|
||||
|
||||
test("shows alert when submitting without a file for required question", () => {
|
||||
const onSubmitMock = vi.fn();
|
||||
|
||||
const { container } = render(<FileUploadQuestion {...defaultProps} onSubmit={onSubmitMock} value={[]} />);
|
||||
|
||||
const form = container.querySelector("form");
|
||||
fireEvent.submit(form as HTMLFormElement);
|
||||
|
||||
expect(window.alert).toHaveBeenCalledWith("Please upload a file");
|
||||
expect(onSubmitMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("submits with empty array when question is not required and no file provided", () => {
|
||||
const onSubmitMock = vi.fn();
|
||||
const questionNotRequired = { ...mockQuestion, required: false };
|
||||
|
||||
const { container } = render(
|
||||
<FileUploadQuestion
|
||||
{...defaultProps}
|
||||
onSubmit={onSubmitMock}
|
||||
question={questionNotRequired}
|
||||
value={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
const form = container.querySelector("form");
|
||||
fireEvent.submit(form as HTMLFormElement);
|
||||
|
||||
expect(onSubmitMock).toHaveBeenCalledWith({ q1: [] }, { q1: 1000 });
|
||||
});
|
||||
|
||||
test("sets tabIndex correctly based on current question", () => {
|
||||
render(<FileUploadQuestion {...defaultProps} currentQuestionId="q1" />);
|
||||
|
||||
expect(screen.getByTestId("submit-button")).toHaveAttribute("tabIndex", "0");
|
||||
expect(screen.getByTestId("back-button")).toHaveAttribute("tabIndex", "0");
|
||||
|
||||
cleanup();
|
||||
|
||||
render(<FileUploadQuestion {...defaultProps} currentQuestionId="different-id" />);
|
||||
|
||||
expect(screen.getByTestId("submit-button")).toHaveAttribute("tabIndex", "-1");
|
||||
expect(screen.getByTestId("back-button")).toHaveAttribute("tabIndex", "-1");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
import { getShuffledRowIndices } from "@/lib/utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { type TSurveyMatrixQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { MatrixQuestion } from "./matrix-question";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: vi.fn((value, languageCode) => {
|
||||
if (typeof value === "string") return value;
|
||||
return value[languageCode] ?? value.default ?? "";
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
useTtc: vi.fn(),
|
||||
getUpdatedTtc: vi.fn((ttc) => ttc),
|
||||
}));
|
||||
|
||||
// Fix the utils mock to handle all exports
|
||||
vi.mock("@/lib/utils", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/utils")>();
|
||||
return {
|
||||
...(actual as Record<string, unknown>),
|
||||
getShuffledRowIndices: vi.fn((length) => Array.from({ length }, (_, i) => i)),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock components that might make tests more complex
|
||||
vi.mock("@/components/general/question-media", () => ({
|
||||
QuestionMedia: ({ imgUrl, videoUrl }: { imgUrl?: string; videoUrl?: string }) =>
|
||||
imgUrl ? <img src={imgUrl} alt="Question media" /> : videoUrl ? <video src={videoUrl}></video> : null, // NOSONAR
|
||||
}));
|
||||
|
||||
describe("MatrixQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
question: {
|
||||
id: "matrix-q1",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Rate our services" },
|
||||
subheader: { default: "Please rate the following services" },
|
||||
required: true,
|
||||
shuffleOption: "none",
|
||||
rows: ["Service 1", "Service 2", "Service 3"],
|
||||
columns: ["Poor", "Fair", "Good", "Excellent"],
|
||||
buttonLabel: { default: "Next" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
imageUrl: "",
|
||||
videoUrl: "",
|
||||
} as unknown as TSurveyMatrixQuestion,
|
||||
value: {},
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
languageCode: "default",
|
||||
ttc: {},
|
||||
setTtc: vi.fn(),
|
||||
currentQuestionId: "matrix-q1",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
test("renders matrix question with correct rows and columns", () => {
|
||||
render(<MatrixQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Rate our services")).toBeInTheDocument();
|
||||
expect(screen.getByText("Please rate the following services")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Service 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Service 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Service 3")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Poor")).toBeInTheDocument();
|
||||
expect(screen.getByText("Fair")).toBeInTheDocument();
|
||||
expect(screen.getByText("Good")).toBeInTheDocument();
|
||||
expect(screen.getByText("Excellent")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Next")).toBeInTheDocument();
|
||||
expect(screen.getByText("Back")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides back button when isFirstQuestion is true", () => {
|
||||
render(<MatrixQuestion {...defaultProps} isFirstQuestion={true} />);
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides back button when isBackButtonHidden is true", () => {
|
||||
render(<MatrixQuestion {...defaultProps} isBackButtonHidden={true} />);
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("selects and deselects a radio button on click", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MatrixQuestion {...defaultProps} />);
|
||||
|
||||
// Find the first radio input cell by finding the intersection of row and column
|
||||
const firstRow = screen.getByText("Service 1").closest("tr");
|
||||
const firstCell = firstRow?.querySelector("td:first-of-type");
|
||||
expect(firstCell).toBeInTheDocument();
|
||||
|
||||
await user.click(firstCell!);
|
||||
expect(defaultProps.onChange).toHaveBeenCalled();
|
||||
|
||||
// Select the same option again should deselect it
|
||||
await user.click(firstCell!);
|
||||
expect(defaultProps.onChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("selects a radio button with keyboard navigation", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MatrixQuestion {...defaultProps} />);
|
||||
|
||||
// Find a specific row and a cell in that row
|
||||
const firstRow = screen.getByText("Service 1").closest("tr");
|
||||
// Get the third cell (which would be the "Good" column)
|
||||
const goodCell = firstRow?.querySelectorAll("td")[2];
|
||||
expect(goodCell).toBeInTheDocument();
|
||||
|
||||
goodCell?.focus();
|
||||
await user.keyboard(" "); // Press space
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("submits the form with selected values", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
const { container } = render(
|
||||
<MatrixQuestion
|
||||
{...defaultProps}
|
||||
onSubmit={onSubmit}
|
||||
value={{ "Service 1": "Good", "Service 2": "Excellent" }}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find the form element and submit it directly
|
||||
const form = container.querySelector("form");
|
||||
expect(form).toBeInTheDocument();
|
||||
|
||||
// Use fireEvent instead of userEvent for form submission
|
||||
await user.click(screen.getByText("Next"));
|
||||
form?.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
{ "matrix-q1": { "Service 1": "Good", "Service 2": "Excellent" } },
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
test("calls onBack when back button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onBack = vi.fn();
|
||||
render(<MatrixQuestion {...defaultProps} onBack={onBack} />);
|
||||
|
||||
const backButton = screen.getByText("Back");
|
||||
await user.click(backButton);
|
||||
|
||||
expect(onBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders media when available", () => {
|
||||
const question = {
|
||||
...defaultProps.question,
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
} as unknown as TSurveyMatrixQuestion;
|
||||
|
||||
render(<MatrixQuestion {...defaultProps} question={question} />);
|
||||
|
||||
// QuestionMedia component should be rendered
|
||||
const questionMediaContainer = document.querySelector("img");
|
||||
expect(questionMediaContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shuffles rows when shuffleOption is not 'none'", () => {
|
||||
const question = {
|
||||
...defaultProps.question,
|
||||
shuffleOption: "all",
|
||||
} as unknown as TSurveyMatrixQuestion;
|
||||
|
||||
render(<MatrixQuestion {...defaultProps} question={question} />);
|
||||
|
||||
expect(getShuffledRowIndices).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("initializes empty values correctly when selecting first option", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
render(<MatrixQuestion {...defaultProps} value={{}} onChange={onChange} />);
|
||||
|
||||
// Find the first row and its first cell
|
||||
const firstRow = screen.getByText("Service 1").closest("tr");
|
||||
const firstCell = firstRow?.querySelector("td:first-of-type");
|
||||
expect(firstCell).toBeInTheDocument();
|
||||
|
||||
await user.click(firstCell!);
|
||||
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
const expectedValue = expect.objectContaining({
|
||||
"matrix-q1": expect.any(Object),
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledWith(expectedValue);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import type { TSurveyMultipleChoiceQuestion } from "@formbricks/types/surveys/types";
|
||||
import { MultipleChoiceMultiQuestion } from "./multiple-choice-multi-question";
|
||||
|
||||
// Mock components
|
||||
vi.mock("@/components/buttons/back-button", () => ({
|
||||
BackButton: ({ onClick, backButtonLabel }: { onClick: () => void; backButtonLabel?: string }) => (
|
||||
<button onClick={onClick} data-testid="back-button">
|
||||
{backButtonLabel ?? "Back"}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/buttons/submit-button", () => ({
|
||||
SubmitButton: ({ buttonLabel }: { buttonLabel?: string }) => (
|
||||
<button type="submit" data-testid="submit-button">
|
||||
{buttonLabel ?? "Submit"}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/headline", () => ({
|
||||
Headline: ({ headline }: { headline: string }) => <h1 data-testid="headline">{headline}</h1>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/subheader", () => ({
|
||||
Subheader: ({ subheader }: { subheader: string }) => <p data-testid="subheader">{subheader}</p>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/question-media", () => ({
|
||||
QuestionMedia: () => <div data-testid="question-media" />,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/wrappers/scrollable-container", () => ({
|
||||
ScrollableContainer: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="scrollable-container">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
useTtc: vi.fn(),
|
||||
getUpdatedTtc: vi.fn(() => ({ questionId: "ttc-value" })),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: (_value: any, _languageCode: string) => {
|
||||
if (typeof _value === "string") return _value;
|
||||
return _value?.["en"] ?? _value?.default ?? "";
|
||||
},
|
||||
}));
|
||||
|
||||
describe("MultipleChoiceMultiQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
question: {
|
||||
id: "q1",
|
||||
type: "multipleChoiceMulti",
|
||||
headline: { en: "Test Question" },
|
||||
subheader: { en: "Select multiple options" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "c1", label: { en: "Option 1" } },
|
||||
{ id: "c2", label: { en: "Option 2" } },
|
||||
{ id: "c3", label: { en: "Option 3" } },
|
||||
{ id: "other", label: { en: "Other" } },
|
||||
],
|
||||
buttonLabel: { en: "Next" },
|
||||
backButtonLabel: { en: "Back" },
|
||||
otherOptionPlaceholder: { en: "Please specify" },
|
||||
} as TSurveyMultipleChoiceQuestion,
|
||||
value: [],
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
languageCode: "en",
|
||||
ttc: {},
|
||||
setTtc: vi.fn(),
|
||||
autoFocusEnabled: false,
|
||||
currentQuestionId: "q1",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders the component correctly", () => {
|
||||
render(<MultipleChoiceMultiQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("headline")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("subheader")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("back-button")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("submit-button")).toBeInTheDocument();
|
||||
|
||||
// Check all options are rendered
|
||||
expect(screen.getByLabelText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Option 2")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Option 3")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Other")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles selecting options", async () => {
|
||||
// Test selecting first option (starting with empty array)
|
||||
const onChange1 = vi.fn();
|
||||
const { unmount } = render(
|
||||
<MultipleChoiceMultiQuestion {...defaultProps} value={[]} onChange={onChange1} />
|
||||
);
|
||||
await userEvent.click(screen.getByLabelText("Option 1"));
|
||||
expect(onChange1).toHaveBeenCalledWith({ q1: ["Option 1"] });
|
||||
unmount();
|
||||
|
||||
// Test selecting second option (already having first option selected)
|
||||
const onChange2 = vi.fn();
|
||||
const { unmount: unmount2 } = render(
|
||||
<MultipleChoiceMultiQuestion {...defaultProps} value={["Option 1"]} onChange={onChange2} />
|
||||
);
|
||||
await userEvent.click(screen.getByLabelText("Option 2"));
|
||||
expect(onChange2).toHaveBeenCalledWith({ q1: ["Option 1", "Option 2"] });
|
||||
unmount2();
|
||||
|
||||
// Test deselecting an option
|
||||
const onChange3 = vi.fn();
|
||||
render(
|
||||
<MultipleChoiceMultiQuestion {...defaultProps} value={["Option 1", "Option 2"]} onChange={onChange3} />
|
||||
);
|
||||
await userEvent.click(screen.getByLabelText("Option 1"));
|
||||
expect(onChange3).toHaveBeenCalledWith({ q1: ["Option 2"] });
|
||||
});
|
||||
|
||||
test("handles 'Other' option correctly", async () => {
|
||||
const onChange = vi.fn();
|
||||
render(<MultipleChoiceMultiQuestion {...defaultProps} onChange={onChange} />);
|
||||
|
||||
// When clicking Other, it calls onChange with an empty string first
|
||||
await userEvent.click(screen.getByLabelText("Other"));
|
||||
expect(screen.getByPlaceholderText("Please specify")).toBeInTheDocument();
|
||||
expect(onChange).toHaveBeenCalledWith({ q1: [""] });
|
||||
|
||||
// Clear the mock to focus on typing behavior
|
||||
onChange.mockClear();
|
||||
|
||||
// Enter text in the field and use fireEvent directly which doesn't trigger onChange for each character
|
||||
const otherInput = screen.getByPlaceholderText("Please specify");
|
||||
fireEvent.change(otherInput, { target: { value: "Custom response" } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ q1: ["Custom response"] });
|
||||
});
|
||||
|
||||
test("handles form submission", async () => {
|
||||
const onSubmit = vi.fn();
|
||||
const { container } = render(
|
||||
<MultipleChoiceMultiQuestion {...defaultProps} value={["Option 1"]} onSubmit={onSubmit} />
|
||||
);
|
||||
|
||||
// Get the form directly and submit it
|
||||
const form = container.querySelector("form");
|
||||
expect(form).toBeInTheDocument();
|
||||
fireEvent.submit(form!);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ q1: ["Option 1"] }, { questionId: "ttc-value" });
|
||||
});
|
||||
|
||||
test("calls onBack when back button is clicked", async () => {
|
||||
const onBack = vi.fn();
|
||||
render(<MultipleChoiceMultiQuestion {...defaultProps} onBack={onBack} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId("back-button"));
|
||||
|
||||
expect(onBack).toHaveBeenCalled();
|
||||
expect(defaultProps.setTtc).toHaveBeenCalledWith({ questionId: "ttc-value" });
|
||||
});
|
||||
|
||||
test("hides back button when isFirstQuestion is true or isBackButtonHidden is true", () => {
|
||||
const { rerender } = render(<MultipleChoiceMultiQuestion {...defaultProps} isFirstQuestion={true} />);
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
|
||||
rerender(<MultipleChoiceMultiQuestion {...defaultProps} isBackButtonHidden={true} />);
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders media when available", () => {
|
||||
const questionWithMedia = {
|
||||
...defaultProps.question,
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
};
|
||||
|
||||
render(<MultipleChoiceMultiQuestion {...defaultProps} question={questionWithMedia} />);
|
||||
expect(screen.getByTestId("question-media")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles shuffled choices correctly", () => {
|
||||
const shuffledQuestion = {
|
||||
...defaultProps.question,
|
||||
shuffleOption: "all",
|
||||
} as TSurveyMultipleChoiceQuestion;
|
||||
|
||||
render(<MultipleChoiceMultiQuestion {...defaultProps} question={shuffledQuestion} />);
|
||||
expect(screen.getByLabelText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Option 2")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Option 3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,274 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { type TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TSurveyMultipleChoiceQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { MultipleChoiceSingleQuestion } from "./multiple-choice-single-question";
|
||||
|
||||
// Mock components
|
||||
vi.mock("@/components/buttons/back-button", () => ({
|
||||
BackButton: ({
|
||||
onClick,
|
||||
backButtonLabel,
|
||||
tabIndex,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
backButtonLabel: string;
|
||||
tabIndex: number;
|
||||
}) => (
|
||||
<button data-testid="back-button" onClick={onClick} tabIndex={tabIndex}>
|
||||
{backButtonLabel}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/buttons/submit-button", () => ({
|
||||
SubmitButton: ({ buttonLabel, tabIndex }: { buttonLabel: string; tabIndex: number }) => (
|
||||
<button data-testid="submit-button" type="submit" tabIndex={tabIndex}>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/headline", () => ({
|
||||
Headline: ({ headline }: { headline: string }) => <h1 data-testid="headline">{headline}</h1>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/subheader", () => ({
|
||||
Subheader: ({ subheader }: { subheader: string }) => <p data-testid="subheader">{subheader}</p>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/question-media", () => ({
|
||||
QuestionMedia: () => <div data-testid="question-media">Media Content</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/wrappers/scrollable-container", () => ({
|
||||
ScrollableContainer: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="scrollable-container">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock utility functions
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: vi.fn((obj, lang) => obj[lang] || obj["default"]),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
getUpdatedTtc: vi.fn((ttc, questionId, time) => ({ ...ttc, [questionId]: time })),
|
||||
useTtc: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils", () => ({
|
||||
cn: vi.fn((...args) => args.filter(Boolean).join(" ")),
|
||||
getShuffledChoicesIds: vi.fn((choices) => choices.map((choice: any) => choice.id)),
|
||||
}));
|
||||
|
||||
// Test data
|
||||
const mockQuestion: TSurveyMultipleChoiceQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Test Question" },
|
||||
subheader: { default: "This is a test question" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Choice 1" } },
|
||||
{ id: "c2", label: { default: "Choice 2" } },
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
buttonLabel: { default: "Next" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
otherOptionPlaceholder: { default: "Please specify" },
|
||||
};
|
||||
|
||||
describe("MultipleChoiceSingleQuestion", () => {
|
||||
const mockOnChange = vi.fn();
|
||||
const mockOnSubmit = vi.fn();
|
||||
const mockOnBack = vi.fn();
|
||||
const mockSetTtc = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
question: mockQuestion,
|
||||
onChange: mockOnChange,
|
||||
onSubmit: mockOnSubmit,
|
||||
onBack: mockOnBack,
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
languageCode: "default",
|
||||
ttc: {} as TResponseTtc,
|
||||
setTtc: mockSetTtc,
|
||||
autoFocusEnabled: false,
|
||||
currentQuestionId: "q1",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
Object.defineProperty(window, "performance", {
|
||||
value: { now: vi.fn(() => 1000) },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the question with choices", () => {
|
||||
render(<MultipleChoiceSingleQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("headline")).toHaveTextContent("Test Question");
|
||||
expect(screen.getByTestId("subheader")).toHaveTextContent("This is a test question");
|
||||
expect(screen.getByLabelText("Choice 1")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Choice 2")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Other")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays media content when available", () => {
|
||||
const questionWithMedia = {
|
||||
...mockQuestion,
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
};
|
||||
|
||||
render(<MultipleChoiceSingleQuestion {...defaultProps} question={questionWithMedia} />);
|
||||
expect(screen.getByTestId("question-media")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("allows selecting a choice", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MultipleChoiceSingleQuestion {...defaultProps} />);
|
||||
|
||||
const choice1Radio = screen.getByLabelText("Choice 1");
|
||||
await user.click(choice1Radio);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({ q1: "Choice 1" });
|
||||
});
|
||||
|
||||
test("shows input field when 'Other' option is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MultipleChoiceSingleQuestion {...defaultProps} />);
|
||||
|
||||
const otherRadio = screen.getByLabelText("Other");
|
||||
await user.click(otherRadio);
|
||||
|
||||
expect(screen.getByPlaceholderText("Please specify")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles 'other' option input change", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with initial value to simulate user typing in the other field
|
||||
render(
|
||||
<MultipleChoiceSingleQuestion
|
||||
{...defaultProps}
|
||||
value="" // Start with empty string
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByRole to more specifically target the radio input
|
||||
const otherRadio = screen.getByRole("radio", { name: "Other" });
|
||||
await user.click(otherRadio);
|
||||
|
||||
// Clear mock calls from the initial setup
|
||||
mockOnChange.mockClear();
|
||||
|
||||
// Get the input and simulate change directly
|
||||
const otherInput = screen.getByPlaceholderText("Please specify");
|
||||
|
||||
// Use fireEvent directly for more reliable testing of the onChange handler
|
||||
(otherInput as any).value = "Custom response";
|
||||
otherInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
otherInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
|
||||
// Verify the onChange handler was called with the correct value
|
||||
expect(mockOnChange).toHaveBeenCalledWith({ q1: "Custom response" });
|
||||
});
|
||||
|
||||
test("submits form with selected value", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MultipleChoiceSingleQuestion {...defaultProps} value="Choice 1" />);
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith({ q1: "Choice 1" }, expect.any(Object));
|
||||
});
|
||||
|
||||
test("calls onBack when back button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MultipleChoiceSingleQuestion {...defaultProps} />);
|
||||
|
||||
const backButton = screen.getByTestId("back-button");
|
||||
await user.click(backButton);
|
||||
|
||||
expect(mockOnBack).toHaveBeenCalled();
|
||||
expect(mockSetTtc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("hides back button when isFirstQuestion or isBackButtonHidden is true", () => {
|
||||
render(<MultipleChoiceSingleQuestion {...defaultProps} isFirstQuestion={true} />);
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
|
||||
cleanup();
|
||||
|
||||
render(<MultipleChoiceSingleQuestion {...defaultProps} isBackButtonHidden={true} />);
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles prefilled answer from URL for first question", () => {
|
||||
// Mock URL parameter properly for URLSearchParams
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append("q1", "Choice 1");
|
||||
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
search: `?${searchParams.toString()}`,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// We need to make sure the component actually checks for the URL param
|
||||
// To do this, we'll create a mock URLSearchParams with a spy
|
||||
const mockGet = vi.fn().mockReturnValue("Choice 1");
|
||||
const mockURLSearchParams = vi.fn(() => ({
|
||||
get: mockGet,
|
||||
}));
|
||||
|
||||
global.URLSearchParams = mockURLSearchParams as any;
|
||||
|
||||
render(
|
||||
<MultipleChoiceSingleQuestion
|
||||
{...defaultProps}
|
||||
isFirstQuestion={true}
|
||||
// Ensure value is undefined so the prefill logic runs
|
||||
value={undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
// Verify the URLSearchParams was called with the correct search string
|
||||
expect(mockURLSearchParams).toHaveBeenCalledWith(window.location.search);
|
||||
// Verify the get method was called with the question id
|
||||
expect(mockGet).toHaveBeenCalledWith("q1");
|
||||
});
|
||||
|
||||
test("applies accessibility attributes correctly", () => {
|
||||
render(<MultipleChoiceSingleQuestion {...defaultProps} />);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
expect(radioGroup).toBeInTheDocument();
|
||||
|
||||
const radioInputs = screen.getAllByRole("radio");
|
||||
expect(radioInputs.length).toBe(3); // 2 regular choices + Other
|
||||
});
|
||||
|
||||
test("sets focus correctly when currentQuestionId matches question.id", () => {
|
||||
render(<MultipleChoiceSingleQuestion {...defaultProps} currentQuestionId="q1" />);
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
expect(submitButton).toHaveAttribute("tabIndex", "0");
|
||||
|
||||
const backButton = screen.getByTestId("back-button");
|
||||
expect(backButton).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
});
|
||||
211
packages/surveys/src/components/questions/nps-question.test.tsx
Normal file
211
packages/surveys/src/components/questions/nps-question.test.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { getUpdatedTtc } from "@/lib/ttc";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { type TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TSurveyNPSQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { NPSQuestion } from "./nps-question";
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: vi.fn().mockImplementation((value) => {
|
||||
if (typeof value === "string") return value;
|
||||
return value?.default || "";
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
getUpdatedTtc: vi.fn().mockReturnValue({}),
|
||||
useTtc: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("preact/hooks", async () => {
|
||||
const actual = await vi.importActual<typeof import("preact/hooks")>("preact/hooks");
|
||||
return {
|
||||
...actual,
|
||||
useState: vi.fn().mockImplementation(actual.useState),
|
||||
};
|
||||
});
|
||||
|
||||
describe("NPSQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockQuestion: TSurveyNPSQuestion = {
|
||||
id: "nps-question-1",
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
headline: { default: "How likely are you to recommend us?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Not likely" },
|
||||
upperLabel: { default: "Very likely" },
|
||||
buttonLabel: { default: "Next" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
isColorCodingEnabled: false,
|
||||
};
|
||||
|
||||
const mockProps = {
|
||||
question: mockQuestion,
|
||||
value: undefined,
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: true,
|
||||
isLastQuestion: false,
|
||||
languageCode: "en",
|
||||
ttc: {} as TResponseTtc,
|
||||
setTtc: vi.fn(),
|
||||
autoFocusEnabled: true,
|
||||
currentQuestionId: "nps-question-1",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
test("renders NPS question with correct elements", () => {
|
||||
render(<NPSQuestion {...mockProps} />);
|
||||
|
||||
expect(screen.getByText("How likely are you to recommend us?")).toBeInTheDocument();
|
||||
expect(screen.getByText("Not likely")).toBeInTheDocument();
|
||||
expect(screen.getByText("Very likely")).toBeInTheDocument();
|
||||
|
||||
// Check all 11 NPS options (0-10) are rendered
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
expect(screen.getByRole("radio", { name: i.toString() })).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
test("calls onChange and onSubmit when clicking on an NPS option", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
render(<NPSQuestion {...mockProps} />);
|
||||
|
||||
// Click on rating 7
|
||||
fireEvent.click(screen.getByRole("radio", { name: "7" }));
|
||||
|
||||
expect(mockProps.onChange).toHaveBeenCalledWith({ [mockQuestion.id]: 7 });
|
||||
expect(getUpdatedTtc).toHaveBeenCalled();
|
||||
expect(mockProps.setTtc).toHaveBeenCalled();
|
||||
|
||||
// Advance timers to trigger the setTimeout callback
|
||||
vi.advanceTimersByTime(300);
|
||||
|
||||
expect(mockProps.onSubmit).toHaveBeenCalledWith({ [mockQuestion.id]: 7 }, {});
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("renders with color coding when enabled", () => {
|
||||
const colorCodedProps = {
|
||||
...mockProps,
|
||||
question: {
|
||||
...mockQuestion,
|
||||
isColorCodingEnabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
const { container } = render(<NPSQuestion {...colorCodedProps} />);
|
||||
|
||||
// Find the fieldset that contains the NPS options
|
||||
const fieldset = container.querySelector("fieldset");
|
||||
expect(fieldset).toBeInTheDocument();
|
||||
|
||||
// Get only the labels within the NPS options fieldset
|
||||
const npsLabels = fieldset?.querySelectorAll("label");
|
||||
expect(npsLabels?.length).toBe(11);
|
||||
|
||||
// Verify each NPS label has a color coding div when enabled
|
||||
let colorDivCount = 0;
|
||||
npsLabels?.forEach((label) => {
|
||||
if (label.firstElementChild?.classList.contains("fb-absolute")) {
|
||||
colorDivCount++;
|
||||
}
|
||||
});
|
||||
|
||||
expect(colorDivCount).toBe(11);
|
||||
|
||||
// Check at least one has the emerald color class for higher ratings
|
||||
const lastLabel = npsLabels?.[10];
|
||||
const colorDiv = lastLabel?.firstElementChild;
|
||||
expect(colorDiv?.classList.contains("fb-bg-emerald-100")).toBe(true);
|
||||
});
|
||||
|
||||
test("renders back button when not first question", () => {
|
||||
render(<NPSQuestion {...mockProps} isFirstQuestion={false} />);
|
||||
|
||||
const backButton = screen.getByText("Back");
|
||||
expect(backButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(backButton);
|
||||
expect(mockProps.onBack).toHaveBeenCalled();
|
||||
expect(getUpdatedTtc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("doesn't render back button when isBackButtonHidden is true", () => {
|
||||
render(<NPSQuestion {...mockProps} isFirstQuestion={false} isBackButtonHidden={true} />);
|
||||
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles form submission for non-required questions", async () => {
|
||||
const nonRequiredProps = {
|
||||
...mockProps,
|
||||
question: {
|
||||
...mockQuestion,
|
||||
required: false,
|
||||
},
|
||||
};
|
||||
|
||||
render(<NPSQuestion {...nonRequiredProps} />);
|
||||
|
||||
// Submit button should be visible for non-required questions
|
||||
const submitButton = screen.getByText("Next");
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
expect(mockProps.onSubmit).toHaveBeenCalled();
|
||||
expect(getUpdatedTtc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("updates hover state when mouse moves over options", () => {
|
||||
render(<NPSQuestion {...mockProps} />);
|
||||
|
||||
const option = screen.getByText("5").closest("label");
|
||||
expect(option).toBeInTheDocument();
|
||||
|
||||
fireEvent.mouseOver(option!);
|
||||
expect(option).toHaveClass("fb-bg-accent-bg");
|
||||
|
||||
fireEvent.mouseLeave(option!);
|
||||
expect(option).not.toHaveClass("fb-bg-accent-bg");
|
||||
});
|
||||
|
||||
test("supports keyboard navigation", () => {
|
||||
render(<NPSQuestion {...mockProps} />);
|
||||
|
||||
const option = screen.getByText("5").closest("label");
|
||||
expect(option).toBeInTheDocument();
|
||||
|
||||
// Test spacebar press
|
||||
fireEvent.keyDown(option!, { key: " " });
|
||||
|
||||
expect(mockProps.onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders media when available", () => {
|
||||
const propsWithMedia = {
|
||||
...mockProps,
|
||||
question: {
|
||||
...mockQuestion,
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
},
|
||||
};
|
||||
|
||||
const { container } = render(<NPSQuestion {...propsWithMedia} />);
|
||||
|
||||
// Check if QuestionMedia component is rendered
|
||||
// Since we're not mocking the QuestionMedia component, we can just verify it's being included
|
||||
const mediaContainer = container.querySelector(".fb-my-4");
|
||||
expect(mediaContainer).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { type TSurveyOpenTextQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { OpenTextQuestion } from "./open-text-question";
|
||||
|
||||
// Mock the components that render headline and subheader
|
||||
vi.mock("@/components/general/headline", () => ({
|
||||
Headline: ({ headline }: any) => <div data-testid="mock-headline">{headline}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/subheader", () => ({
|
||||
Subheader: ({ subheader }: any) => <div data-testid="mock-subheader">{subheader}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
getUpdatedTtc: vi.fn().mockReturnValue({}),
|
||||
useTtc: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: vi
|
||||
.fn()
|
||||
.mockImplementation((value) => (typeof value === "string" ? value : (value.en ?? value.default))),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/question-media", () => ({
|
||||
QuestionMedia: () => <div data-testid="question-media">Media Component</div>,
|
||||
}));
|
||||
|
||||
describe("OpenTextQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const defaultQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Your feedback" },
|
||||
subheader: { default: "Please share your thoughts" },
|
||||
inputType: "text",
|
||||
placeholder: { default: "Type here..." },
|
||||
required: true,
|
||||
buttonLabel: { default: "Submit" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
longAnswer: false,
|
||||
} as unknown as TSurveyOpenTextQuestion;
|
||||
|
||||
const defaultProps = {
|
||||
question: defaultQuestion,
|
||||
value: "",
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: true,
|
||||
isLastQuestion: false,
|
||||
languageCode: "en",
|
||||
ttc: {},
|
||||
setTtc: vi.fn(),
|
||||
autoFocusEnabled: false,
|
||||
currentQuestionId: "q1",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
test("renders question with headline and subheader", () => {
|
||||
render(<OpenTextQuestion {...defaultProps} />);
|
||||
expect(screen.getByTestId("mock-headline")).toHaveTextContent("Your feedback");
|
||||
expect(screen.getByTestId("mock-subheader")).toHaveTextContent("Please share your thoughts");
|
||||
});
|
||||
|
||||
test("handles input change for text field", async () => {
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(<OpenTextQuestion {...defaultProps} onChange={onChange} />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Type here...");
|
||||
|
||||
// Directly set the input value and trigger the input event
|
||||
Object.defineProperty(input, "value", { value: "Hello" });
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ q1: "Hello" });
|
||||
});
|
||||
|
||||
test("submits form with entered value", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
const setTtc = vi.fn();
|
||||
|
||||
render(<OpenTextQuestion {...defaultProps} value="My feedback" onSubmit={onSubmit} setTtc={setTtc} />);
|
||||
|
||||
const submitButton = screen.getByRole("button", { name: "Submit" });
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ q1: "My feedback" }, {});
|
||||
expect(setTtc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("displays back button when not first question", () => {
|
||||
render(<OpenTextQuestion {...defaultProps} isFirstQuestion={false} />);
|
||||
expect(screen.getByText("Back")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides back button when isBackButtonHidden is true", () => {
|
||||
render(<OpenTextQuestion {...defaultProps} isFirstQuestion={false} isBackButtonHidden={true} />);
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onBack when back button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onBack = vi.fn();
|
||||
const setTtc = vi.fn();
|
||||
|
||||
render(<OpenTextQuestion {...defaultProps} isFirstQuestion={false} onBack={onBack} setTtc={setTtc} />);
|
||||
|
||||
const backButton = screen.getByText("Back");
|
||||
await user.click(backButton);
|
||||
|
||||
expect(onBack).toHaveBeenCalled();
|
||||
expect(setTtc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders textarea for long answers", () => {
|
||||
render(<OpenTextQuestion {...defaultProps} question={{ ...defaultQuestion, longAnswer: true }} />);
|
||||
|
||||
expect(screen.getByRole("textbox")).toHaveAttribute("rows", "3");
|
||||
});
|
||||
|
||||
test("displays character limit when configured", () => {
|
||||
render(<OpenTextQuestion {...defaultProps} question={{ ...defaultQuestion, charLimit: { max: 100 } }} />);
|
||||
|
||||
expect(screen.getByText("0/100")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with media when available", () => {
|
||||
render(<OpenTextQuestion {...defaultProps} question={{ ...defaultQuestion, imageUrl: "test.jpg" }} />);
|
||||
expect(screen.getByTestId("question-media")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies input validation for phone type", () => {
|
||||
render(<OpenTextQuestion {...defaultProps} question={{ ...defaultQuestion, inputType: "phone" }} />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Type here...");
|
||||
expect(input).toHaveAttribute("pattern", "^[0-9+][0-9+\\- ]*[0-9]$");
|
||||
expect(input).toHaveAttribute("title", "Enter a valid phone number");
|
||||
});
|
||||
|
||||
test("applies correct attributes for required fields", () => {
|
||||
render(<OpenTextQuestion {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Type here...");
|
||||
expect(input).toBeRequired();
|
||||
});
|
||||
|
||||
test("auto focuses input when enabled and is current question", () => {
|
||||
const focusMock = vi.fn();
|
||||
// Mock the ref implementation for this test
|
||||
window.HTMLElement.prototype.focus = focusMock;
|
||||
|
||||
render(<OpenTextQuestion {...defaultProps} autoFocusEnabled={true} currentQuestionId="q1" />);
|
||||
|
||||
expect(focusMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
type TSurveyPictureSelectionQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { PictureSelectionQuestion } from "./picture-selection-question";
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: vi.fn((value) => (typeof value === "string" ? value : value.default)),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/storage", () => ({
|
||||
getOriginalFileNameFromUrl: vi.fn(() => "test-image"),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
getUpdatedTtc: vi.fn((ttc) => ttc),
|
||||
useTtc: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("PictureSelectionQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockQuestion: TSurveyPictureSelectionQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.PictureSelection,
|
||||
headline: { default: "Select an image" },
|
||||
required: true,
|
||||
allowMulti: false,
|
||||
choices: [
|
||||
{ id: "c1", imageUrl: "https://example.com/image1.jpg" },
|
||||
{ id: "c2", imageUrl: "https://example.com/image2.jpg" },
|
||||
],
|
||||
buttonLabel: { default: "Next" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
};
|
||||
|
||||
const mockProps = {
|
||||
question: mockQuestion,
|
||||
value: [],
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
languageCode: "en",
|
||||
ttc: {},
|
||||
setTtc: vi.fn(),
|
||||
autoFocusEnabled: true,
|
||||
currentQuestionId: "q1",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
test("renders the component correctly", () => {
|
||||
render(<PictureSelectionQuestion {...mockProps} />);
|
||||
|
||||
// Check for images and buttons which are clearly visible in the DOM
|
||||
expect(screen.getAllByRole("img")).toHaveLength(2);
|
||||
expect(screen.getByRole("button", { name: "Next" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Back" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders media content when available", () => {
|
||||
const questionWithMedia = {
|
||||
...mockQuestion,
|
||||
imageUrl: "https://example.com/question-image.jpg",
|
||||
};
|
||||
|
||||
render(<PictureSelectionQuestion {...mockProps} question={questionWithMedia} />);
|
||||
|
||||
// Check for the QuestionMedia component (additional img would be present)
|
||||
expect(screen.getAllByRole("img").length).toBeGreaterThan(2);
|
||||
});
|
||||
|
||||
test("handles single selection correctly", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
|
||||
// Render with custom onChange to track calls more precisely
|
||||
render(<PictureSelectionQuestion {...mockProps} onChange={onChange} />);
|
||||
|
||||
const images = screen.getAllByRole("img");
|
||||
|
||||
// First click should select the item
|
||||
await user.click(images[0]);
|
||||
expect(onChange).toHaveBeenLastCalledWith({ q1: ["c1"] });
|
||||
|
||||
// Reset the mock to clearly see the next call
|
||||
onChange.mockClear();
|
||||
|
||||
// Re-render with the updated value to reflect the current state
|
||||
cleanup();
|
||||
render(<PictureSelectionQuestion {...mockProps} value={["c1"]} onChange={onChange} />);
|
||||
|
||||
// Click the same image again - should now deselect
|
||||
const updatedImages = screen.getAllByRole("img");
|
||||
await user.click(updatedImages[0]);
|
||||
expect(onChange).toHaveBeenCalledWith({ q1: [] });
|
||||
});
|
||||
|
||||
test("handles multiple selection when allowMulti is true", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const multiQuestion = { ...mockQuestion, allowMulti: true };
|
||||
|
||||
render(<PictureSelectionQuestion {...mockProps} question={multiQuestion} onChange={onChange} />);
|
||||
|
||||
const images = screen.getAllByRole("img");
|
||||
|
||||
// First click selects the first item
|
||||
await user.click(images[0]);
|
||||
expect(onChange).toHaveBeenLastCalledWith({ q1: ["c1"] });
|
||||
|
||||
// Now we need to re-render with the updated value to simulate state update
|
||||
onChange.mockClear();
|
||||
cleanup();
|
||||
|
||||
render(
|
||||
<PictureSelectionQuestion {...mockProps} question={multiQuestion} onChange={onChange} value={["c1"]} />
|
||||
);
|
||||
|
||||
// Click the second image to add it to selection
|
||||
const updatedImages = screen.getAllByRole("img");
|
||||
await user.click(updatedImages[1]);
|
||||
|
||||
// Now it should add c2 to the existing array with c1
|
||||
expect(onChange).toHaveBeenCalledWith({ q1: ["c1", "c2"] });
|
||||
});
|
||||
|
||||
test("handles form submission", async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockValue = ["c1"];
|
||||
const mockTtc = { q1: 1000 };
|
||||
|
||||
render(<PictureSelectionQuestion {...mockProps} value={mockValue} ttc={mockTtc} />);
|
||||
|
||||
const submitButton = screen.getByText("Next");
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(mockProps.onSubmit).toHaveBeenCalledWith({ q1: ["c1"] }, mockTtc);
|
||||
});
|
||||
|
||||
test("handles back button click", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<PictureSelectionQuestion {...mockProps} />);
|
||||
|
||||
const backButton = screen.getByText("Back");
|
||||
await user.click(backButton);
|
||||
|
||||
expect(mockProps.onBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("doesn't render back button when isFirstQuestion or isBackButtonHidden is true", () => {
|
||||
render(<PictureSelectionQuestion {...mockProps} isFirstQuestion={true} />);
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
|
||||
cleanup();
|
||||
|
||||
render(<PictureSelectionQuestion {...mockProps} isBackButtonHidden={true} />);
|
||||
expect(screen.queryByText("Back")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles keyboard navigation with Space key", () => {
|
||||
render(<PictureSelectionQuestion {...mockProps} />);
|
||||
|
||||
const images = screen.getAllByRole("img");
|
||||
const label = images[0].closest("label");
|
||||
|
||||
fireEvent.keyDown(label!, { key: " " });
|
||||
|
||||
expect(mockProps.onChange).toHaveBeenCalledWith({ q1: ["c1"] });
|
||||
});
|
||||
|
||||
test("renders checkboxes when allowMulti is true", () => {
|
||||
const multiQuestion = { ...mockQuestion, allowMulti: true };
|
||||
|
||||
render(<PictureSelectionQuestion {...mockProps} question={multiQuestion} value={["c1"]} />);
|
||||
|
||||
const checkboxes = screen.getAllByRole("checkbox");
|
||||
expect(checkboxes).toHaveLength(2);
|
||||
expect(checkboxes[0]).toBeChecked();
|
||||
expect(checkboxes[1]).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("renders radio buttons when allowMulti is false", () => {
|
||||
render(<PictureSelectionQuestion {...mockProps} value={["c1"]} />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons).toHaveLength(2);
|
||||
expect(radioButtons[0]).toBeChecked();
|
||||
expect(radioButtons[1]).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("prevents default action when clicking image expand link", async () => {
|
||||
render(<PictureSelectionQuestion {...mockProps} />);
|
||||
|
||||
const links = screen.getAllByTitle("Open in new tab");
|
||||
const mockStopPropagation = vi.fn();
|
||||
|
||||
// Simulate clicking the link but prevent the event from propagating
|
||||
fireEvent.click(links[0], { stopPropagation: mockStopPropagation });
|
||||
|
||||
// The onChange should not be called because stopPropagation prevents it
|
||||
expect(mockProps.onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,258 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import type { TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestionTypeEnum, type TSurveyRankingQuestion } from "@formbricks/types/surveys/types";
|
||||
import { RankingQuestion } from "./ranking-question";
|
||||
|
||||
// Mock components used in the RankingQuestion component
|
||||
vi.mock("@/components/buttons/back-button", () => ({
|
||||
BackButton: ({ onClick, backButtonLabel }: { onClick: () => void; backButtonLabel: string }) => (
|
||||
<button data-testid="back-button" onClick={onClick}>
|
||||
{backButtonLabel}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/buttons/submit-button", () => ({
|
||||
SubmitButton: ({ buttonLabel }: { buttonLabel: string }) => (
|
||||
<button data-testid="submit-button" type="submit">
|
||||
{buttonLabel}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/headline", () => ({
|
||||
Headline: ({ headline }: { headline: string }) => <h1 data-testid="headline">{headline}</h1>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/subheader", () => ({
|
||||
Subheader: ({ subheader }: { subheader: string }) => <p data-testid="subheader">{subheader}</p>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/question-media", () => ({
|
||||
QuestionMedia: () => <div data-testid="question-media"></div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/wrappers/scrollable-container", () => ({
|
||||
ScrollableContainer: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="scrollable-container">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [null],
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: (value: any, _: string) => (typeof value === "string" ? value : value.default),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
useTtc: vi.fn(),
|
||||
getUpdatedTtc: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils", () => ({
|
||||
cn: (...args: any[]) => args.filter(Boolean).join(" "),
|
||||
getShuffledChoicesIds: (choices: any[], _?: string) => choices.map((c) => c.id),
|
||||
}));
|
||||
|
||||
describe("RankingQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockQuestion: TSurveyRankingQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.Ranking,
|
||||
headline: { default: "Rank these items" },
|
||||
subheader: { default: "Please rank all items" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Item 1" } },
|
||||
{ id: "c2", label: { default: "Item 2" } },
|
||||
{ id: "c3", label: { default: "Item 3" } },
|
||||
],
|
||||
buttonLabel: { default: "Next" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
question: mockQuestion,
|
||||
value: [] as string[],
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
languageCode: "en",
|
||||
ttc: {} as TResponseTtc,
|
||||
setTtc: vi.fn(),
|
||||
autoFocusEnabled: false,
|
||||
currentQuestionId: "q1",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
test("renders correctly with all elements", () => {
|
||||
render(<RankingQuestion {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("headline")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("subheader")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("submit-button")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("back-button")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders media when available", () => {
|
||||
const questionWithMedia = {
|
||||
...mockQuestion,
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
};
|
||||
|
||||
render(<RankingQuestion {...defaultProps} question={questionWithMedia} />);
|
||||
expect(screen.getByTestId("question-media")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("doesn't show back button when isFirstQuestion is true", () => {
|
||||
render(<RankingQuestion {...defaultProps} isFirstQuestion={true} />);
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("doesn't show back button when isBackButtonHidden is true", () => {
|
||||
render(<RankingQuestion {...defaultProps} isBackButtonHidden={true} />);
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("clicking on item adds it to the sorted list", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RankingQuestion {...defaultProps} />);
|
||||
|
||||
const item1 = screen.getByText("Item 1").closest("div");
|
||||
await user.click(item1!);
|
||||
|
||||
const itemElements = screen
|
||||
.getAllByRole("button")
|
||||
.filter((btn) => btn.innerHTML.includes("chevron-up") || btn.innerHTML.includes("chevron-down"));
|
||||
expect(itemElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("clicking on a sorted item removes it from the sorted list", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RankingQuestion {...defaultProps} value={["c1"]} />);
|
||||
|
||||
// First verify the item is in the sorted list
|
||||
const sortedItems = screen
|
||||
.getAllByRole("button")
|
||||
.filter((btn) => btn.innerHTML.includes("chevron-up") || btn.innerHTML.includes("chevron-down"));
|
||||
expect(sortedItems.length).toBeGreaterThan(0);
|
||||
|
||||
// Click the item to unselect it
|
||||
const item1 = screen.getByText("Item 1").closest("div");
|
||||
await user.click(item1!);
|
||||
|
||||
// The move buttons should be gone
|
||||
const moveButtons = screen
|
||||
.queryAllByRole("button")
|
||||
.filter((btn) => btn.innerHTML.includes("chevron-up") || btn.innerHTML.includes("chevron-down"));
|
||||
expect(moveButtons.length).toBe(0);
|
||||
});
|
||||
|
||||
test("moving an item up in the list", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RankingQuestion {...defaultProps} value={["c1", "c2"]} />);
|
||||
|
||||
const upButtons = screen.getAllByRole("button").filter((btn) => btn.innerHTML.includes("chevron-up"));
|
||||
|
||||
// The first item's up button should be disabled
|
||||
expect(upButtons[0]).toBeDisabled();
|
||||
|
||||
// The second item's up button should work
|
||||
expect(upButtons[1]).not.toBeDisabled();
|
||||
await user.click(upButtons[1]);
|
||||
});
|
||||
|
||||
test("moving an item down in the list", async () => {
|
||||
// For this test, we'll just verify the component renders correctly with ranked items
|
||||
render(<RankingQuestion {...defaultProps} value={["c1", "c2"]} />);
|
||||
|
||||
// Verify both items are rendered
|
||||
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 2")).toBeInTheDocument();
|
||||
|
||||
// Verify there are some move buttons present
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const moveButtons = buttons.filter(
|
||||
(btn) =>
|
||||
btn.innerHTML && (btn.innerHTML.includes("chevron-up") || btn.innerHTML.includes("chevron-down"))
|
||||
);
|
||||
|
||||
// Just make sure we have some move buttons rendered
|
||||
expect(moveButtons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("submits form with complete ranking", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RankingQuestion {...defaultProps} value={["c1", "c2", "c3"]} />);
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(defaultProps.onSubmit).toHaveBeenCalled();
|
||||
expect(screen.queryByText("Please rank all items before submitting.")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("clicking back button calls onBack", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RankingQuestion {...defaultProps} />);
|
||||
|
||||
const backButton = screen.getByTestId("back-button");
|
||||
await user.click(backButton);
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenCalled();
|
||||
expect(defaultProps.onBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("allows incomplete ranking if not required", async () => {
|
||||
const user = userEvent.setup();
|
||||
const nonRequiredQuestion = {
|
||||
...mockQuestion,
|
||||
required: false,
|
||||
};
|
||||
|
||||
render(<RankingQuestion {...defaultProps} question={nonRequiredQuestion} value={[]} />);
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(defaultProps.onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles keyboard navigation", () => {
|
||||
render(<RankingQuestion {...defaultProps} />);
|
||||
|
||||
const item = screen.getByText("Item 1").closest("div");
|
||||
fireEvent.keyDown(item!, { key: " " }); // Space key
|
||||
|
||||
const moveButtons = screen
|
||||
.getAllByRole("button")
|
||||
.filter((btn) => btn.innerHTML.includes("chevron-up") || btn.innerHTML.includes("chevron-down"));
|
||||
expect(moveButtons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("applies shuffle option correctly", () => {
|
||||
const shuffledQuestion = {
|
||||
...mockQuestion,
|
||||
shuffleOption: "all",
|
||||
} as TSurveyRankingQuestion;
|
||||
|
||||
render(<RankingQuestion {...defaultProps} question={shuffledQuestion} />);
|
||||
|
||||
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/preact";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestionTypeEnum, TSurveyRatingQuestion } from "@formbricks/types/surveys/types";
|
||||
import { RatingQuestion } from "./rating-question";
|
||||
|
||||
vi.mock("@/components/buttons/back-button", () => ({
|
||||
BackButton: ({ onClick, backButtonLabel, tabIndex }: any) => (
|
||||
<button data-testid="back-button" onClick={onClick} tabIndex={tabIndex}>
|
||||
{backButtonLabel}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/buttons/submit-button", () => ({
|
||||
SubmitButton: ({ buttonLabel, tabIndex }: any) => (
|
||||
<button data-testid="submit-button" tabIndex={tabIndex}>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/headline", () => ({
|
||||
Headline: ({ headline, questionId, required }: any) => (
|
||||
<h2 data-testid="headline" data-required={required} data-question-id={questionId}>
|
||||
{headline}
|
||||
</h2>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/subheader", () => ({
|
||||
Subheader: ({ subheader, questionId }: any) => (
|
||||
<p data-testid="subheader" data-question-id={questionId}>
|
||||
{subheader}
|
||||
</p>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/general/question-media", () => ({
|
||||
QuestionMedia: ({ imgUrl, videoUrl }: any) => (
|
||||
<div data-testid="question-media" data-img-url={imgUrl} data-video-url={videoUrl}></div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/wrappers/scrollable-container", () => ({
|
||||
ScrollableContainer: ({ children }: any) => <div data-testid="scrollable-container">{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: (value: any) => (typeof value === "string" ? value : value.default),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ttc", () => ({
|
||||
getUpdatedTtc: vi.fn().mockReturnValue({}),
|
||||
useTtc: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("preact/hooks", async () => {
|
||||
const actual = await vi.importActual<typeof import("preact/hooks")>("preact/hooks");
|
||||
return {
|
||||
...actual,
|
||||
useState: vi.fn().mockImplementation(actual.useState),
|
||||
useEffect: vi.fn().mockImplementation(actual.useEffect),
|
||||
};
|
||||
});
|
||||
|
||||
describe("RatingQuestion", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockQuestion: TSurveyRatingQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "How would you rate our service?" },
|
||||
subheader: { default: "Please give us your honest feedback" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
lowerLabel: { default: "Very poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
buttonLabel: { default: "Next" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
isColorCodingEnabled: true,
|
||||
};
|
||||
|
||||
const mockProps = {
|
||||
question: mockQuestion,
|
||||
value: undefined,
|
||||
onChange: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
isFirstQuestion: false,
|
||||
isLastQuestion: false,
|
||||
languageCode: "en",
|
||||
ttc: {} as TResponseTtc,
|
||||
setTtc: vi.fn(),
|
||||
autoFocusEnabled: true,
|
||||
currentQuestionId: "q1",
|
||||
isBackButtonHidden: false,
|
||||
};
|
||||
|
||||
test("renders the question correctly", () => {
|
||||
render(<RatingQuestion {...mockProps} />);
|
||||
|
||||
expect(screen.getByTestId("headline")).toHaveTextContent("How would you rate our service?");
|
||||
expect(screen.getByTestId("subheader")).toHaveTextContent("Please give us your honest feedback");
|
||||
expect(screen.queryByTestId("question-media")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("back-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders media when available", () => {
|
||||
const questionWithMedia = {
|
||||
...mockQuestion,
|
||||
imageUrl: "image.jpg",
|
||||
};
|
||||
|
||||
render(<RatingQuestion {...mockProps} question={questionWithMedia} />);
|
||||
|
||||
expect(screen.getByTestId("question-media")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles number scale correctly", async () => {
|
||||
vi.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
render(<RatingQuestion {...mockProps} />);
|
||||
|
||||
const ratingOption = screen.getByText("3");
|
||||
await user.click(ratingOption);
|
||||
|
||||
expect(mockProps.onChange).toHaveBeenCalledWith({ q1: 3 });
|
||||
|
||||
// Fast-forward timers to handle the setTimeout in the component
|
||||
vi.advanceTimersByTime(250);
|
||||
|
||||
expect(mockProps.onSubmit).toHaveBeenCalled();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("handles star scale correctly", async () => {
|
||||
vi.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
const starQuestion = {
|
||||
...mockQuestion,
|
||||
scale: "star" as const,
|
||||
};
|
||||
|
||||
render(<RatingQuestion {...mockProps} question={starQuestion} />);
|
||||
|
||||
const stars = screen.getAllByRole("radio");
|
||||
await user.click(stars[2]); // Click the 3rd star
|
||||
|
||||
expect(mockProps.onChange).toHaveBeenCalledWith({ q1: 3 });
|
||||
|
||||
vi.advanceTimersByTime(250);
|
||||
|
||||
expect(mockProps.onSubmit).toHaveBeenCalled();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("handles smiley scale correctly", async () => {
|
||||
vi.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
const smileyQuestion = {
|
||||
...mockQuestion,
|
||||
scale: "smiley" as const,
|
||||
};
|
||||
|
||||
render(<RatingQuestion {...mockProps} question={smileyQuestion} />);
|
||||
|
||||
const smileys = screen.getAllByRole("radio");
|
||||
await user.click(smileys[2]); // Click the 3rd smiley
|
||||
|
||||
expect(mockProps.onChange).toHaveBeenCalledWith({ q1: 3 });
|
||||
|
||||
vi.advanceTimersByTime(250);
|
||||
|
||||
expect(mockProps.onSubmit).toHaveBeenCalled();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("hides back button when isFirstQuestion is true", () => {
|
||||
render(<RatingQuestion {...mockProps} isFirstQuestion={true} />);
|
||||
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides back button when isBackButtonHidden is true", () => {
|
||||
render(<RatingQuestion {...mockProps} isBackButtonHidden={true} />);
|
||||
|
||||
expect(screen.queryByTestId("back-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles form submission", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(
|
||||
<RatingQuestion {...mockProps} value={4} question={{ ...mockQuestion, required: false }} />
|
||||
);
|
||||
|
||||
const form = container.querySelector("form");
|
||||
expect(form).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByTestId("submit-button"));
|
||||
|
||||
expect(mockProps.onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles keyboard navigation via spacebar", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RatingQuestion {...mockProps} />);
|
||||
|
||||
await user.tab(); // Focus on first rating option
|
||||
await user.keyboard(" "); // Press spacebar
|
||||
|
||||
expect(mockProps.onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("triggers onBack when back button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RatingQuestion {...mockProps} />);
|
||||
|
||||
const backButton = screen.getByTestId("back-button");
|
||||
await user.click(backButton);
|
||||
|
||||
expect(mockProps.onBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("supports color coding when enabled", () => {
|
||||
render(<RatingQuestion {...mockProps} />);
|
||||
|
||||
// Check if color coding elements are present
|
||||
const colorElements = document.querySelectorAll('[class*="fb-h-[6px]"]');
|
||||
expect(colorElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -23,12 +23,9 @@ const config = ({ mode }) => {
|
||||
reporter: ["text", "html", "lcov"],
|
||||
reportsDirectory: "./coverage",
|
||||
include: [
|
||||
"src/lib/api-client.ts",
|
||||
"src/lib/response-queue.ts",
|
||||
"src/lib/logic.ts",
|
||||
"src/components/buttons/*.tsx"
|
||||
"src/lib/**/*.{ts,tsx}",
|
||||
"src/components/**/*.{ts,tsx}"
|
||||
],
|
||||
exclude: ["dist/**", "node_modules/**"],
|
||||
},
|
||||
},
|
||||
define: {
|
||||
|
||||
@@ -21,5 +21,5 @@ sonar.scm.exclusions.disabled=false
|
||||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
# Coverage
|
||||
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts
|
||||
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts
|
||||
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**
|
||||
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**
|
||||
Reference in New Issue
Block a user