mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-25 09:31:37 -05:00
Compare commits
7 Commits
v3.10.1
...
non-intera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57e7485564 | ||
|
|
42a38a6f47 | ||
|
|
34bb9c2127 | ||
|
|
6442b5e4aa | ||
|
|
dde5a55446 | ||
|
|
13e615a798 | ||
|
|
9c81961b0b |
@@ -109,7 +109,7 @@ export const MainNavigation = ({
|
||||
|
||||
useEffect(() => {
|
||||
const toggleTextOpacity = () => {
|
||||
setIsTextVisible(isCollapsed ? true : false);
|
||||
setIsTextVisible(isCollapsed);
|
||||
};
|
||||
const timeoutId = setTimeout(toggleTextOpacity, 150);
|
||||
return () => clearTimeout(timeoutId);
|
||||
@@ -170,7 +170,7 @@ export const MainNavigation = ({
|
||||
name: t("common.actions"),
|
||||
href: `/environments/${environment.id}/actions`,
|
||||
icon: MousePointerClick,
|
||||
isActive: pathname?.includes("/actions") || pathname?.includes("/actions"),
|
||||
isActive: pathname?.includes("/actions"),
|
||||
},
|
||||
{
|
||||
name: t("common.integrations"),
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { OptionsType, QuestionOption, QuestionOptions, QuestionsComboBox } from "./QuestionsComboBox";
|
||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
OptionsType,
|
||||
QuestionOption,
|
||||
QuestionOptions,
|
||||
QuestionsComboBox,
|
||||
SelectedCommandItem,
|
||||
} from "./QuestionsComboBox";
|
||||
|
||||
describe("QuestionsComboBox", () => {
|
||||
afterEach(() => {
|
||||
@@ -53,3 +60,67 @@ describe("QuestionsComboBox", () => {
|
||||
expect(screen.getByText("Q1")).toBeInTheDocument(); // Verify the selected item is now displayed
|
||||
});
|
||||
});
|
||||
|
||||
describe("SelectedCommandItem", () => {
|
||||
test("renders question icon and color for QUESTIONS with questionType", () => {
|
||||
const { container } = render(
|
||||
<SelectedCommandItem
|
||||
label="Q1"
|
||||
type={OptionsType.QUESTIONS}
|
||||
questionType={TSurveyQuestionTypeEnum.OpenText}
|
||||
/>
|
||||
);
|
||||
expect(container.querySelector(".bg-brand-dark")).toBeInTheDocument();
|
||||
expect(container.querySelector("svg")).toBeInTheDocument();
|
||||
expect(container.textContent).toContain("Q1");
|
||||
});
|
||||
|
||||
test("renders attribute icon and color for ATTRIBUTES", () => {
|
||||
const { container } = render(<SelectedCommandItem label="Attr" type={OptionsType.ATTRIBUTES} />);
|
||||
expect(container.querySelector(".bg-indigo-500")).toBeInTheDocument();
|
||||
expect(container.querySelector("svg")).toBeInTheDocument();
|
||||
expect(container.textContent).toContain("Attr");
|
||||
});
|
||||
|
||||
test("renders hidden field icon and color for HIDDEN_FIELDS", () => {
|
||||
const { container } = render(<SelectedCommandItem label="Hidden" type={OptionsType.HIDDEN_FIELDS} />);
|
||||
expect(container.querySelector(".bg-amber-500")).toBeInTheDocument();
|
||||
expect(container.querySelector("svg")).toBeInTheDocument();
|
||||
expect(container.textContent).toContain("Hidden");
|
||||
});
|
||||
|
||||
test("renders meta icon and color for META with label", () => {
|
||||
const { container } = render(<SelectedCommandItem label="device" type={OptionsType.META} />);
|
||||
expect(container.querySelector(".bg-amber-500")).toBeInTheDocument();
|
||||
expect(container.querySelector("svg")).toBeInTheDocument();
|
||||
expect(container.textContent).toContain("device");
|
||||
});
|
||||
|
||||
test("renders other icon and color for OTHERS with label", () => {
|
||||
const { container } = render(<SelectedCommandItem label="Language" type={OptionsType.OTHERS} />);
|
||||
expect(container.querySelector(".bg-amber-500")).toBeInTheDocument();
|
||||
expect(container.querySelector("svg")).toBeInTheDocument();
|
||||
expect(container.textContent).toContain("Language");
|
||||
});
|
||||
|
||||
test("renders tag icon and color for TAGS", () => {
|
||||
const { container } = render(<SelectedCommandItem label="Tag1" type={OptionsType.TAGS} />);
|
||||
expect(container.querySelector(".bg-indigo-500")).toBeInTheDocument();
|
||||
expect(container.querySelector("svg")).toBeInTheDocument();
|
||||
expect(container.textContent).toContain("Tag1");
|
||||
});
|
||||
|
||||
test("renders fallback color and no icon for unknown type", () => {
|
||||
const { container } = render(<SelectedCommandItem label="Unknown" type={"UNKNOWN"} />);
|
||||
expect(container.querySelector(".bg-amber-500")).toBeInTheDocument();
|
||||
expect(container.querySelector("svg")).not.toBeInTheDocument();
|
||||
expect(container.textContent).toContain("Unknown");
|
||||
});
|
||||
|
||||
test("renders fallback for non-string label", () => {
|
||||
const { container } = render(
|
||||
<SelectedCommandItem label={{ default: "NonString" }} type={OptionsType.QUESTIONS} />
|
||||
);
|
||||
expect(container.textContent).toContain("NonString");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,11 +18,12 @@ import {
|
||||
CheckIcon,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ContactIcon,
|
||||
EyeOff,
|
||||
GlobeIcon,
|
||||
GridIcon,
|
||||
HashIcon,
|
||||
HelpCircleIcon,
|
||||
HomeIcon,
|
||||
ImageIcon,
|
||||
LanguagesIcon,
|
||||
ListIcon,
|
||||
@@ -63,59 +64,60 @@ interface QuestionComboBoxProps {
|
||||
onChangeValue: (option: QuestionOption) => void;
|
||||
}
|
||||
|
||||
const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOption>) => {
|
||||
const getIconType = () => {
|
||||
switch (type) {
|
||||
case OptionsType.QUESTIONS:
|
||||
switch (questionType) {
|
||||
case TSurveyQuestionTypeEnum.OpenText:
|
||||
return <MessageSquareTextIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.Rating:
|
||||
return <StarIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.CTA:
|
||||
return <MousePointerClickIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.OpenText:
|
||||
return <HelpCircleIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceMulti:
|
||||
return <ListIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
|
||||
return <Rows3Icon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.NPS:
|
||||
return <NetPromoterScoreIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.Consent:
|
||||
return <CheckIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.PictureSelection:
|
||||
return <ImageIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.Matrix:
|
||||
return <GridIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
return <ListOrderedIcon width={18} height={18} className="text-white" />;
|
||||
}
|
||||
case OptionsType.ATTRIBUTES:
|
||||
return <User width={18} height={18} className="text-white" />;
|
||||
const questionIcons = {
|
||||
// questions
|
||||
[TSurveyQuestionTypeEnum.OpenText]: MessageSquareTextIcon,
|
||||
[TSurveyQuestionTypeEnum.Rating]: StarIcon,
|
||||
[TSurveyQuestionTypeEnum.CTA]: MousePointerClickIcon,
|
||||
[TSurveyQuestionTypeEnum.MultipleChoiceMulti]: ListIcon,
|
||||
[TSurveyQuestionTypeEnum.MultipleChoiceSingle]: Rows3Icon,
|
||||
[TSurveyQuestionTypeEnum.NPS]: NetPromoterScoreIcon,
|
||||
[TSurveyQuestionTypeEnum.Consent]: CheckIcon,
|
||||
[TSurveyQuestionTypeEnum.PictureSelection]: ImageIcon,
|
||||
[TSurveyQuestionTypeEnum.Matrix]: GridIcon,
|
||||
[TSurveyQuestionTypeEnum.Ranking]: ListOrderedIcon,
|
||||
[TSurveyQuestionTypeEnum.Address]: HomeIcon,
|
||||
[TSurveyQuestionTypeEnum.ContactInfo]: ContactIcon,
|
||||
|
||||
case OptionsType.HIDDEN_FIELDS:
|
||||
return <EyeOff width={18} height={18} className="text-white" />;
|
||||
case OptionsType.META:
|
||||
switch (label) {
|
||||
case "device":
|
||||
return <SmartphoneIcon width={18} height={18} className="text-white" />;
|
||||
case "os":
|
||||
return <AirplayIcon width={18} height={18} className="text-white" />;
|
||||
case "browser":
|
||||
return <GlobeIcon width={18} height={18} className="text-white" />;
|
||||
case "source":
|
||||
return <GlobeIcon width={18} height={18} className="text-white" />;
|
||||
case "action":
|
||||
return <MousePointerClickIcon width={18} height={18} className="text-white" />;
|
||||
}
|
||||
case OptionsType.OTHERS:
|
||||
switch (label) {
|
||||
case "Language":
|
||||
return <LanguagesIcon width={18} height={18} className="text-white" />;
|
||||
}
|
||||
case OptionsType.TAGS:
|
||||
return <HashIcon width={18} height={18} className="text-white" />;
|
||||
// attributes
|
||||
[OptionsType.ATTRIBUTES]: User,
|
||||
|
||||
// hidden fields
|
||||
[OptionsType.HIDDEN_FIELDS]: EyeOff,
|
||||
|
||||
// meta
|
||||
device: SmartphoneIcon,
|
||||
os: AirplayIcon,
|
||||
browser: GlobeIcon,
|
||||
source: GlobeIcon,
|
||||
action: MousePointerClickIcon,
|
||||
|
||||
// others
|
||||
Language: LanguagesIcon,
|
||||
|
||||
// tags
|
||||
[OptionsType.TAGS]: HashIcon,
|
||||
};
|
||||
|
||||
const getIcon = (type: string) => {
|
||||
const IconComponent = questionIcons[type];
|
||||
return IconComponent ? <IconComponent width={18} height={18} className="text-white" /> : null;
|
||||
};
|
||||
|
||||
export const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOption>) => {
|
||||
const getIconType = () => {
|
||||
if (type) {
|
||||
if (type === OptionsType.QUESTIONS && questionType) {
|
||||
return getIcon(questionType);
|
||||
} else if (type === OptionsType.ATTRIBUTES) {
|
||||
return getIcon(OptionsType.ATTRIBUTES);
|
||||
} else if (type === OptionsType.HIDDEN_FIELDS) {
|
||||
return getIcon(OptionsType.HIDDEN_FIELDS);
|
||||
} else if ([OptionsType.META, OptionsType.OTHERS].includes(type) && label) {
|
||||
return getIcon(label);
|
||||
} else if (type === OptionsType.TAGS) {
|
||||
return getIcon(OptionsType.TAGS);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -164,7 +166,7 @@ export const QuestionsComboBox = ({ options, selected, onChangeValue }: Question
|
||||
value={inputValue}
|
||||
onValueChange={setInputValue}
|
||||
placeholder={t("common.search") + "..."}
|
||||
className="h-5 border-none border-transparent p-0 shadow-none ring-offset-transparent outline-0 focus:border-none focus:border-transparent focus:shadow-none focus:ring-offset-transparent focus:outline-0"
|
||||
className="h-5 border-none border-transparent p-0 shadow-none outline-0 ring-offset-transparent focus:border-none focus:border-transparent focus:shadow-none focus:outline-0 focus:ring-offset-transparent"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
|
||||
@@ -254,7 +254,7 @@ describe("getEnvironmentState", () => {
|
||||
vi.mocked(getEnvironment).mockResolvedValue(mockEnvironment);
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization);
|
||||
vi.mocked(getProjectForEnvironmentState).mockResolvedValue(mockProject);
|
||||
vi.mocked(getSurveysForEnvironmentState).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getSurveysForEnvironmentState).mockResolvedValue([mockSurveys[0]]); // Only return the app, inProgress survey
|
||||
vi.mocked(getActionClassesForEnvironmentState).mockResolvedValue(mockActionClasses);
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50); // Default below limit
|
||||
});
|
||||
|
||||
@@ -99,12 +99,8 @@ export const getEnvironmentState = async (
|
||||
getActionClassesForEnvironmentState(environmentId),
|
||||
]);
|
||||
|
||||
const filteredSurveys = surveys.filter(
|
||||
(survey) => survey.type === "app" && survey.status === "inProgress"
|
||||
);
|
||||
|
||||
const data: TJsEnvironmentState["data"] = {
|
||||
surveys: !isMonthlyResponsesLimitReached ? filteredSurveys : [],
|
||||
surveys: !isMonthlyResponsesLimitReached ? surveys : [],
|
||||
actionClasses,
|
||||
project: project,
|
||||
...(IS_RECAPTCHA_CONFIGURED ? { recaptchaSiteKey: RECAPTCHA_SITE_KEY } : {}),
|
||||
|
||||
@@ -100,7 +100,11 @@ describe("getSurveysForEnvironmentState", () => {
|
||||
|
||||
expect(validateInputs).toHaveBeenCalledWith([environmentId, expect.any(Object)]);
|
||||
expect(prisma.survey.findMany).toHaveBeenCalledWith({
|
||||
where: { environmentId },
|
||||
where: {
|
||||
environmentId,
|
||||
type: "app",
|
||||
status: "inProgress",
|
||||
},
|
||||
select: expect.any(Object), // Check if select is called, specific fields are in the original code
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 30,
|
||||
@@ -116,7 +120,11 @@ describe("getSurveysForEnvironmentState", () => {
|
||||
const result = await getSurveysForEnvironmentState(environmentId);
|
||||
|
||||
expect(prisma.survey.findMany).toHaveBeenCalledWith({
|
||||
where: { environmentId },
|
||||
where: {
|
||||
environmentId,
|
||||
type: "app",
|
||||
status: "inProgress",
|
||||
},
|
||||
select: expect.any(Object),
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 30,
|
||||
|
||||
@@ -20,6 +20,8 @@ export const getSurveysForEnvironmentState = reactCache(
|
||||
const surveysPrisma = await prisma.survey.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
type: "app",
|
||||
status: "inProgress",
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import structuredClonePolyfill from "@ungap/structured-clone";
|
||||
|
||||
let structuredCloneExport: typeof structuredClonePolyfill;
|
||||
|
||||
if (typeof structuredClone === "undefined") {
|
||||
structuredCloneExport = structuredClonePolyfill;
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
structuredCloneExport = structuredClone;
|
||||
}
|
||||
const structuredCloneExport =
|
||||
typeof structuredClone === "undefined" ? structuredClonePolyfill : structuredClone;
|
||||
|
||||
export { structuredCloneExport as structuredClone };
|
||||
|
||||
@@ -71,16 +71,18 @@ export function LocalizedEditor({
|
||||
key={`${questionIdx}-${selectedLanguageCode}`}
|
||||
setFirstRender={setFirstRender}
|
||||
setText={(v: string) => {
|
||||
const translatedHtml = {
|
||||
...value,
|
||||
[selectedLanguageCode]: v,
|
||||
};
|
||||
if (questionIdx === -1) {
|
||||
// welcome card
|
||||
updateQuestion({ html: translatedHtml });
|
||||
return;
|
||||
if (localSurvey.questions[questionIdx] || questionIdx === -1) {
|
||||
const translatedHtml = {
|
||||
...value,
|
||||
[selectedLanguageCode]: v,
|
||||
};
|
||||
if (questionIdx === -1) {
|
||||
// welcome card
|
||||
updateQuestion({ html: translatedHtml });
|
||||
return;
|
||||
}
|
||||
updateQuestion(questionIdx, { html: translatedHtml });
|
||||
}
|
||||
updateQuestion(questionIdx, { html: translatedHtml });
|
||||
}}
|
||||
/>
|
||||
{localSurvey.languages.length > 1 && (
|
||||
|
||||
@@ -99,7 +99,7 @@ export const FileUploadQuestionForm = ({
|
||||
const removeExtension = (event, index: number) => {
|
||||
event.preventDefault();
|
||||
if (question.allowedFileExtensions) {
|
||||
const updatedExtensions = [...question?.allowedFileExtensions];
|
||||
const updatedExtensions = [...(question.allowedFileExtensions || [])];
|
||||
updatedExtensions.splice(index, 1);
|
||||
// Ensure array is set to undefined if empty, matching toggle behavior
|
||||
updateQuestion(questionIdx, {
|
||||
@@ -178,7 +178,7 @@ export const FileUploadQuestionForm = ({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6 mb-8 space-y-6">
|
||||
<div className="mb-8 mt-6 space-y-6">
|
||||
<AdvancedOptionToggle
|
||||
isChecked={question.allowMultipleFiles}
|
||||
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
|
||||
@@ -218,7 +218,7 @@ export const FileUploadQuestionForm = ({
|
||||
|
||||
updateQuestion(questionIdx, { maxSizeInMB: parseInt(e.target.value, 10) });
|
||||
}}
|
||||
className="mr-2 ml-2 inline w-20 bg-white text-center text-sm"
|
||||
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
|
||||
/>
|
||||
MB
|
||||
</p>
|
||||
|
||||
@@ -35,7 +35,6 @@ export const ResponseOptionsCard = ({
|
||||
const autoComplete = localSurvey.autoComplete !== null;
|
||||
const [runOnDateToggle, setRunOnDateToggle] = useState(false);
|
||||
const [closeOnDateToggle, setCloseOnDateToggle] = useState(false);
|
||||
useState;
|
||||
const [surveyClosedMessageToggle, setSurveyClosedMessageToggle] = useState(false);
|
||||
const [verifyEmailToggle, setVerifyEmailToggle] = useState(localSurvey.isVerifyEmailEnabled);
|
||||
const [recaptchaToggle, setRecaptchaToggle] = useState(localSurvey.recaptcha?.enabled ?? false);
|
||||
@@ -318,7 +317,7 @@ export const ResponseOptionsCard = ({
|
||||
)}>
|
||||
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pr-5 pl-2">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<CheckIcon
|
||||
strokeWidth={3}
|
||||
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
|
||||
@@ -356,7 +355,7 @@ export const ResponseOptionsCard = ({
|
||||
value={localSurvey.autoComplete?.toString()}
|
||||
onChange={handleInputResponse}
|
||||
onBlur={handleInputResponseBlur}
|
||||
className="mr-2 ml-2 inline w-20 bg-white text-center text-sm"
|
||||
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
|
||||
/>
|
||||
{t("environments.surveys.edit.completed_responses")}
|
||||
</p>
|
||||
@@ -451,7 +450,7 @@ export const ResponseOptionsCard = ({
|
||||
<Input
|
||||
autoFocus
|
||||
id="heading"
|
||||
className="mt-2 mb-4 bg-white"
|
||||
className="mb-4 mt-2 bg-white"
|
||||
name="heading"
|
||||
defaultValue={surveyClosedMessage.heading}
|
||||
onChange={(e) => handleClosedSurveyMessageChange({ heading: e.target.value })}
|
||||
@@ -506,7 +505,7 @@ export const ResponseOptionsCard = ({
|
||||
<Input
|
||||
autoFocus
|
||||
id="heading"
|
||||
className="mt-2 mb-4 bg-white"
|
||||
className="mb-4 mt-2 bg-white"
|
||||
name="heading"
|
||||
value={singleUseMessage.heading}
|
||||
onChange={(e) => handleSingleUseSurveyMessageChange({ heading: e.target.value })}
|
||||
@@ -514,7 +513,7 @@ export const ResponseOptionsCard = ({
|
||||
|
||||
<Label htmlFor="headline">{t("environments.surveys.edit.subheading")}</Label>
|
||||
<Input
|
||||
className="mt-2 mb-4 bg-white"
|
||||
className="mb-4 mt-2 bg-white"
|
||||
id="subheading"
|
||||
name="subheading"
|
||||
value={singleUseMessage.subheading}
|
||||
|
||||
@@ -135,25 +135,6 @@ describe("MultipleChoiceMultiQuestion", () => {
|
||||
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(
|
||||
@@ -168,6 +149,46 @@ describe("MultipleChoiceMultiQuestion", () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({ q1: ["Option 1"] }, { questionId: "ttc-value" });
|
||||
});
|
||||
|
||||
test("filters out invalid values during submission", async () => {
|
||||
const onSubmit = vi.fn();
|
||||
const { container } = render(
|
||||
<MultipleChoiceMultiQuestion
|
||||
{...defaultProps}
|
||||
// Add an invalid value that should be filtered out
|
||||
value={["Option 1", "Invalid Option"]}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
// Submit the form
|
||||
const form = container.querySelector("form");
|
||||
fireEvent.submit(form!);
|
||||
|
||||
// Check that onSubmit was called with only valid values
|
||||
expect(onSubmit).toHaveBeenCalledWith({ q1: ["Option 1"] }, { questionId: "ttc-value" });
|
||||
});
|
||||
|
||||
test("calls onChange with updated values during submission", async () => {
|
||||
const onChange = vi.fn();
|
||||
const onSubmit = vi.fn();
|
||||
const { container } = render(
|
||||
<MultipleChoiceMultiQuestion
|
||||
{...defaultProps}
|
||||
value={["Option 1", "Invalid Option"]}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
// Submit the form
|
||||
const form = container.querySelector("form");
|
||||
fireEvent.submit(form!);
|
||||
|
||||
// Check that onChange was called with filtered values
|
||||
expect(onChange).toHaveBeenCalledWith({ q1: ["Option 1"] });
|
||||
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} />);
|
||||
|
||||
@@ -64,20 +64,6 @@ export function MultipleChoiceMultiQuestion({
|
||||
const [otherSelected, setOtherSelected] = useState<boolean>(false);
|
||||
const [otherValue, setOtherValue] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setOtherSelected(
|
||||
Boolean(value) &&
|
||||
(Array.isArray(value) ? value : [value]).some((item) => {
|
||||
return !getChoicesWithoutOtherLabels().includes(item);
|
||||
})
|
||||
);
|
||||
setOtherValue(
|
||||
(Array.isArray(value) &&
|
||||
value.filter((v) => !question.choices.find((c) => c.label[languageCode] === v))[0]) ||
|
||||
""
|
||||
);
|
||||
}, [question.id, getChoicesWithoutOtherLabels, question.choices, value, languageCode]);
|
||||
|
||||
const questionChoices = useMemo(() => {
|
||||
if (!question.choices) {
|
||||
return [];
|
||||
@@ -135,6 +121,16 @@ export function MultipleChoiceMultiQuestion({
|
||||
onChange({ [question.id]: [] }); // if not array, make it an array
|
||||
};
|
||||
|
||||
const getIsRequired = () => {
|
||||
const responseValues = [...value];
|
||||
if (otherSelected && otherValue) {
|
||||
responseValues.push(otherValue);
|
||||
}
|
||||
return question.required && Array.isArray(responseValues) && responseValues.length
|
||||
? false
|
||||
: question.required;
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
key={question.id}
|
||||
@@ -143,10 +139,11 @@ export function MultipleChoiceMultiQuestion({
|
||||
const newValue = value.filter((item) => {
|
||||
return getChoicesWithoutOtherLabels().includes(item) || item === otherValue;
|
||||
}); // filter out all those values which are either in getChoicesWithoutOtherLabels() (i.e. selected by checkbox) or the latest entered otherValue
|
||||
if (otherValue && otherSelected && !newValue.includes(otherValue)) newValue.push(otherValue);
|
||||
onChange({ [question.id]: newValue });
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
onSubmit({ [question.id]: newValue }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
@@ -208,11 +205,7 @@ export function MultipleChoiceMultiQuestion({
|
||||
Array.isArray(value) &&
|
||||
value.includes(getLocalizedValue(choice.label, languageCode))
|
||||
}
|
||||
required={
|
||||
question.required && Array.isArray(value) && value.length
|
||||
? false
|
||||
: question.required
|
||||
}
|
||||
required={getIsRequired()}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
@@ -225,9 +218,7 @@ export function MultipleChoiceMultiQuestion({
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
value.includes(getLocalizedValue(otherOption.label, languageCode))
|
||||
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
|
||||
: "fb-border-border",
|
||||
otherSelected ? "fb-border-brand fb-bg-input-bg-selected fb-z-10" : "fb-border-border",
|
||||
"fb-text-heading focus-within:fb-border-brand fb-bg-input-bg focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -249,11 +240,6 @@ export function MultipleChoiceMultiQuestion({
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onChange={() => {
|
||||
setOtherSelected(!otherSelected);
|
||||
if (!value.includes(otherValue)) {
|
||||
addItem(otherValue);
|
||||
} else {
|
||||
removeItem(otherValue);
|
||||
}
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
@@ -270,9 +256,9 @@ export function MultipleChoiceMultiQuestion({
|
||||
name={question.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
value={otherValue}
|
||||
pattern=".*\S+.*"
|
||||
onChange={(e) => {
|
||||
setOtherValue(e.currentTarget.value);
|
||||
addItem(e.currentTarget.value);
|
||||
}}
|
||||
className="placeholder:fb-text-placeholder fb-border-border fb-bg-survey-bg fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-mt-3 fb-flex fb-h-10 fb-w-full fb-border fb-px-3 fb-py-2 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50"
|
||||
placeholder={
|
||||
@@ -280,7 +266,6 @@ export function MultipleChoiceMultiQuestion({
|
||||
}
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
pattern=".*\S+.*"
|
||||
/>
|
||||
) : null}
|
||||
</label>
|
||||
|
||||
@@ -225,6 +225,7 @@ export function MultipleChoiceSingleQuestion({
|
||||
id={`${otherOption.id}-label`}
|
||||
dir="auto"
|
||||
name={question.id}
|
||||
pattern=".*\S+.*"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange({ [question.id]: e.currentTarget.value });
|
||||
@@ -238,7 +239,6 @@ export function MultipleChoiceSingleQuestion({
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
maxLength={250}
|
||||
pattern=".*\S+.*"
|
||||
/>
|
||||
) : null}
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user