Compare commits

..

2 Commits

Author SHA1 Message Date
Matthias Nannt
a4811351be chore: improve js logging 2025-05-16 12:13:14 +02:00
Dhruwang Jariwala
9fcbe4e8c5 chore: swap next and back button input (#5748)
Co-authored-by: Johannes <johannes@formbricks.com>
2025-05-16 08:51:12 +00:00
6 changed files with 182 additions and 90 deletions

View File

@@ -87,21 +87,6 @@ export const CTAQuestionForm = ({
<div className="mt-2 flex justify-between gap-8">
<div className="flex w-full space-x-2">
<QuestionFormInput
id="buttonLabel"
value={question.buttonLabel}
label={t("environments.surveys.edit.next_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={lastQuestion ? t("common.finish") : t("common.next")}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
/>
{questionIdx !== 0 && (
<QuestionFormInput
id="backButtonLabel"
@@ -118,6 +103,20 @@ export const CTAQuestionForm = ({
locale={locale}
/>
)}
<QuestionFormInput
id="buttonLabel"
value={question.buttonLabel}
label={t("environments.surveys.edit.next_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={lastQuestion ? t("common.finish") : t("common.next")}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
/>
</div>
</div>

View File

@@ -1,15 +1,10 @@
import { QuestionCard } from "@/modules/survey/editor/components/question-card";
import { Project } from "@prisma/client";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
// Import waitFor
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import {
TSurvey,
TSurveyAddressQuestion,
TSurveyQuestion,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
// Import waitFor
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
// Mock child components
vi.mock("@/modules/survey/components/question-form-input", () => ({
@@ -371,8 +366,9 @@ describe("QuestionCard Component", () => {
test("applies invalid styling when isInvalid is true", () => {
render(<QuestionCard {...defaultProps} isInvalid={true} />);
const dragHandle = screen.getByRole("button", { name: "" }).parentElement; // Get the div containing the GripIcon
const dragHandle = screen.getByRole("button", { name: "Drag to reorder question" }).parentElement; // Get the div containing the GripIcon
expect(dragHandle).toHaveClass("bg-red-400");
expect(dragHandle).toHaveClass("hover:bg-red-600");
});
test("disables required toggle for Address question if all fields are optional", () => {
@@ -507,4 +503,94 @@ describe("QuestionCard Component", () => {
// First question should never have back button
expect(screen.queryByTestId("question-form-input-backButtonLabel")).not.toBeInTheDocument();
});
// Accessibility Tests
test("maintains proper focus management when toggling advanced settings", async () => {
const user = userEvent.setup();
render(<QuestionCard {...defaultProps} activeQuestionId="q1" />);
const advancedSettingsTrigger = screen.getByText("environments.surveys.edit.show_advanced_settings");
await user.click(advancedSettingsTrigger);
const closeTrigger = screen.getByText("environments.surveys.edit.hide_advanced_settings");
expect(closeTrigger).toBeInTheDocument();
});
test("ensures proper ARIA attributes for collapsible sections", () => {
render(<QuestionCard {...defaultProps} activeQuestionId="q1" />);
const collapsibleTrigger = screen.getByText("environments.surveys.edit.show_advanced_settings");
expect(collapsibleTrigger).toHaveAttribute("aria-expanded", "false");
fireEvent.click(collapsibleTrigger);
expect(collapsibleTrigger).toHaveAttribute("aria-expanded", "true");
});
test("maintains keyboard accessibility for required toggle", async () => {
const user = userEvent.setup();
render(<QuestionCard {...defaultProps} activeQuestionId="q1" />);
const requiredToggle = screen.getByRole("switch", { name: "environments.surveys.edit.required" });
await user.click(requiredToggle);
expect(mockUpdateQuestion).toHaveBeenCalledWith(0, { required: false });
});
test("provides screen reader text for drag handle", () => {
render(<QuestionCard {...defaultProps} />);
const dragHandle = screen.getByRole("button", { name: "Drag to reorder question" });
const svg = dragHandle.querySelector("svg");
expect(svg).toHaveAttribute("aria-hidden", "true");
});
test("maintains proper heading hierarchy", () => {
render(<QuestionCard {...defaultProps} activeQuestionId="q1" />);
const headline = screen.getByText("Question Headline");
expect(headline.tagName).toBe("H3");
expect(headline).toHaveClass("text-sm", "font-semibold");
});
test("ensures proper focus order for form elements", async () => {
const user = userEvent.setup();
render(<QuestionCard {...defaultProps} activeQuestionId="q1" />);
// Open advanced settings
fireEvent.click(screen.getByText("environments.surveys.edit.show_advanced_settings"));
const requiredToggle = screen.getByRole("switch", { name: "environments.surveys.edit.required" });
await user.click(requiredToggle);
expect(mockUpdateQuestion).toHaveBeenCalledWith(0, { required: false });
});
test("provides proper ARIA attributes for interactive elements", () => {
render(<QuestionCard {...defaultProps} activeQuestionId="q1" />);
const requiredToggle = screen.getByRole("switch", { name: "environments.surveys.edit.required" });
expect(requiredToggle).toHaveAttribute("aria-checked", "true");
const longAnswerToggle = screen.getByRole("switch", { name: "environments.surveys.edit.long_answer" });
expect(longAnswerToggle).toHaveAttribute("aria-checked", "false");
});
test("ensures proper role attributes for interactive elements", () => {
render(<QuestionCard {...defaultProps} activeQuestionId="q1" />);
const toggles = screen.getAllByRole("switch");
expect(toggles).toHaveLength(2); // Required and Long Answer toggles
const collapsibleTrigger = screen.getByText("environments.surveys.edit.show_advanced_settings");
expect(collapsibleTrigger).toHaveAttribute("type", "button");
});
test("maintains proper focus management when closing advanced settings", async () => {
const user = userEvent.setup();
render(<QuestionCard {...defaultProps} activeQuestionId="q1" />);
const advancedSettingsTrigger = screen.getByText("environments.surveys.edit.show_advanced_settings");
await user.click(advancedSettingsTrigger);
const closeTrigger = screen.getByText("environments.surveys.edit.hide_advanced_settings");
await user.click(closeTrigger);
expect(screen.getByText("environments.surveys.edit.show_advanced_settings")).toBeInTheDocument();
});
});

View File

@@ -196,7 +196,9 @@ export const QuestionCard = ({
)}>
<div className="mt-3 flex w-full justify-center">{QUESTIONS_ICON_MAP[question.type]}</div>
<button className="opacity-0 hover:cursor-move group-hover:opacity-100">
<button
className="opacity-0 hover:cursor-move group-hover:opacity-100"
aria-label="Drag to reorder question">
<GripIcon className="h-4 w-4" />
</button>
</div>
@@ -215,14 +217,15 @@ export const QuestionCard = ({
className={cn(
open ? "" : " ",
"flex cursor-pointer justify-between gap-4 rounded-r-lg p-4 hover:bg-slate-50"
)}>
)}
aria-label="Toggle question details">
<div>
<div className="flex grow">
{/* <div className="-ml-0.5 mr-3 h-6 min-w-[1.5rem] text-slate-400">
{QUESTIONS_ICON_MAP[question.type]}
</div> */}
<div className="flex grow flex-col justify-center" dir="auto">
<p className="text-sm font-semibold">
<h3 className="text-sm font-semibold">
{recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
selectedLanguageCode
]
@@ -232,7 +235,7 @@ export const QuestionCard = ({
] ?? ""
)
: getTSurveyQuestionTypeEnumName(question.type, t)}
</p>
</h3>
{!open && (
<p className="mt-1 truncate text-xs text-slate-500">
{question?.required
@@ -272,7 +275,7 @@ export const QuestionCard = ({
TSurveyQuestionTypeEnum.Ranking,
TSurveyQuestionTypeEnum.Matrix,
].includes(question.type) ? (
<Alert variant="warning" size="small" className="w-fill">
<Alert variant="warning" size="small" className="w-fill" role="alert">
<AlertTitle>{t("environments.surveys.edit.caution_text")}</AlertTitle>
<AlertButton onClick={() => onAlertTrigger()}>{t("common.learn_more")}</AlertButton>
</Alert>
@@ -457,7 +460,9 @@ export const QuestionCard = ({
) : null}
<div className="mt-4">
<Collapsible.Root open={openAdvanced} onOpenChange={setOpenAdvanced} className="mt-5">
<Collapsible.CollapsibleTrigger className="flex items-center text-sm text-slate-700">
<Collapsible.CollapsibleTrigger
className="flex items-center text-sm text-slate-700"
aria-label="Toggle advanced settings">
{openAdvanced ? (
<ChevronDownIcon className="mr-1 h-4 w-3" />
) : (
@@ -473,6 +478,30 @@ export const QuestionCard = ({
question.type !== TSurveyQuestionTypeEnum.Rating &&
question.type !== TSurveyQuestionTypeEnum.CTA ? (
<div className="mt-2 flex space-x-2">
{questionIdx !== 0 && (
<QuestionFormInput
id="backButtonLabel"
value={question.backButtonLabel}
label={t("environments.surveys.edit.back_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={t("common.back")}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
onBlur={(e) => {
if (!question.backButtonLabel) return;
let translatedBackButtonLabel = {
...question.backButtonLabel,
[selectedLanguageCode]: e.target.value,
};
updateEmptyButtonLabels("backButtonLabel", translatedBackButtonLabel, 0);
}}
/>
)}
<div className="w-full">
<QuestionFormInput
id="buttonLabel"
@@ -503,30 +532,6 @@ export const QuestionCard = ({
locale={locale}
/>
</div>
{questionIdx !== 0 && (
<QuestionFormInput
id="backButtonLabel"
value={question.backButtonLabel}
label={t("environments.surveys.edit.back_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={t("common.back")}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
onBlur={(e) => {
if (!question.backButtonLabel) return;
let translatedBackButtonLabel = {
...question.backButtonLabel,
[selectedLanguageCode]: e.target.value,
};
updateEmptyButtonLabels("backButtonLabel", translatedBackButtonLabel, 0);
}}
/>
)}
</div>
) : null}
{(question.type === TSurveyQuestionTypeEnum.Rating ||

View File

@@ -181,7 +181,7 @@ export const createSurvey = async (page: Page, params: CreateSurveyParams) => {
await page.locator('input[name="subheader"]').fill(params.openTextQuestion.description);
await page.getByLabel("Placeholder").fill(params.openTextQuestion.placeholder);
await page.locator("p").filter({ hasText: params.openTextQuestion.question }).click();
await page.locator("h3").filter({ hasText: params.openTextQuestion.question }).click();
// Single Select Question
await page
@@ -403,7 +403,7 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.locator('input[name="subheader"]').fill(params.openTextQuestion.description);
await page.getByLabel("Placeholder").fill(params.openTextQuestion.placeholder);
await page.locator("p").filter({ hasText: params.openTextQuestion.question }).click();
await page.locator("h3").filter({ hasText: params.openTextQuestion.question }).click();
// Single Select Question
await page
@@ -606,8 +606,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
// Adding logic
// Open Text Question
await page.locator("p", { hasText: params.openTextQuestion.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.openTextQuestion.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#condition-0-0-conditionOperator").click();
await page.getByRole("option", { name: "is submitted" }).click();
@@ -637,8 +637,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.getByRole("textbox", { name: "Value" }).fill("This ");
// Single Select Question
await page.locator("p", { hasText: params.singleSelectQuestion.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.singleSelectQuestion.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#condition-0-0-conditionOperator").click();
await page.getByRole("option", { name: "Equals one of" }).click();
@@ -665,8 +665,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.getByRole("textbox", { name: "Value" }).fill("is ");
// Multi Select Question
await page.locator("p", { hasText: params.multiSelectQuestion.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.multiSelectQuestion.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#condition-0-0-conditionOperator").click();
await page.getByRole("option", { name: "Includes all of" }).click();
@@ -706,8 +706,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.getByRole("textbox", { name: "Value" }).fill("a ");
// Picture Select Question
await page.locator("p", { hasText: params.pictureSelectQuestion.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.pictureSelectQuestion.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#condition-0-0-conditionOperator").click();
await page.getByRole("option", { name: "is submitted" }).click();
@@ -731,8 +731,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.getByRole("textbox", { name: "Value" }).fill("secret ");
// Rating Question
await page.locator("p", { hasText: params.ratingQuestion.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.ratingQuestion.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#condition-0-0-conditionOperator").click();
await page.getByRole("option", { name: ">=" }).click();
@@ -758,8 +758,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.getByRole("textbox", { name: "Value" }).fill("message ");
// NPS Question
await page.locator("p", { hasText: params.npsQuestion.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.npsQuestion.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#condition-0-0-conditionOperator").click();
await page.getByRole("option", { name: ">", exact: true }).click();
@@ -819,8 +819,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.getByRole("textbox", { name: "Value" }).fill("for ");
// Ranking Question
await page.locator("p", { hasText: params.ranking.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.ranking.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#condition-0-0-conditionOperator").click();
await page.getByRole("option", { name: "is skipped" }).click();
@@ -844,8 +844,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.getByRole("textbox", { name: "Value" }).fill("e2e ");
// Matrix Question
await page.locator("p", { hasText: params.matrix.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.matrix.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#condition-0-0-conditionOperator").click();
await page.getByRole("option", { name: "is completely submitted" }).click();
@@ -877,8 +877,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.getByRole("option", { name: params.ctaQuestion.question }).click();
// CTA Question
await page.locator("p", { hasText: params.ctaQuestion.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.ctaQuestion.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#condition-0-0-conditionOperator").click();
await page.getByRole("option", { name: "is skipped" }).click();
@@ -905,8 +905,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.locator("#action-0-value-input").fill("1");
// Consent Question
await page.locator("p", { hasText: params.consentQuestion.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.consentQuestion.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#action-0-objective").click();
await page.getByRole("option", { name: "Calculate" }).click();
@@ -918,8 +918,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.locator("#action-0-value-input").fill("2");
// File Upload Question
await page.locator("p", { hasText: params.fileUploadQuestion.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.fileUploadQuestion.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#action-0-objective").click();
await page.getByRole("option", { name: "Calculate" }).click();
@@ -936,7 +936,7 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
const tomorrow = new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().split("T")[0];
await page.getByRole("main").getByText(params.date.question).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.getByPlaceholder("Value").fill(today);
@@ -965,8 +965,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.locator("#action-0-value-input").fill("1");
// Cal Question
await page.locator("p", { hasText: params.cal.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.cal.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#condition-0-0-conditionOperator").click();
await page.getByRole("option", { name: "is skipped" }).click();
@@ -980,8 +980,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.locator("#action-0-value-input").fill("1");
// Address Question
await page.locator("p", { hasText: params.address.question }).click();
await page.getByRole("button", { name: "Show Advanced Settings" }).click();
await page.getByRole("heading", { name: params.address.question }).click();
await page.getByRole("button", { name: "Toggle advanced settings" }).click();
await page.getByRole("button", { name: "Add logic" }).click();
await page.locator("#action-0-objective").click();
await page.getByRole("option", { name: "Calculate" }).click();

View File

@@ -193,6 +193,8 @@ export const setup = async (
if (environmentStateResponse.ok) {
environmentState = environmentStateResponse.data;
const backendSurveys = environmentState.data.surveys;
logger.debug(`Fetched ${backendSurveys.length.toString()} surveys from the backend`);
} else {
logger.error(
`Error fetching environment state: ${environmentStateResponse.error.code} - ${environmentStateResponse.error.responseMessage ?? ""}`
@@ -247,6 +249,7 @@ export const setup = async (
// filter the environment state wrt the person state
const filteredSurveys = filterSurveys(environmentState, userState);
logger.debug(`${filteredSurveys.length.toString()} surveys could be shown to current user on trigger.`);
// update the appConfig with the new filtered surveys and person state
config.update({
@@ -256,9 +259,8 @@ export const setup = async (
filteredSurveys,
});
const surveyNames = filteredSurveys.map((s) => s.name);
logger.debug(`Fetched ${environmentState.data.surveys.length.toString()} surveys from the backend`);
logger.debug(`${surveyNames.length.toString()} surveys could be shown to current user on trigger: ${surveyNames.join(", ")}`);
// const surveyNames = filteredSurveys.map((s) => s.name);
// logger.debug(`Fetched ${surveyNames.length.toString()} surveys during sync: ${surveyNames.join(", ")}`);
} catch {
logger.debug("Error during sync. Please try again.");
}
@@ -284,6 +286,9 @@ export const setup = async (
let userState: TUserState = DEFAULT_USER_STATE_NO_USER_ID;
const backendSurveys = environmentStateResponse.data.data.surveys;
logger.debug(`Fetched ${backendSurveys.length.toString()} surveys from the backend`);
if ("userId" in configInput && configInput.userId) {
const updatesResponse = await sendUpdatesToBackend({
appUrl: configInput.appUrl,
@@ -305,6 +310,7 @@ export const setup = async (
const environmentState = environmentStateResponse.data;
const filteredSurveys = filterSurveys(environmentState, userState);
logger.debug(`${filteredSurveys.length.toString()} surveys could be shown to current user on trigger.`);
config.update({
appUrl: configInput.appUrl,

View File

@@ -198,10 +198,6 @@ describe("setup.ts", () => {
filteredSurveys: [{ name: "S1" }, { name: "S2" }],
})
);
// Check for the new log messages
expect(mockLogger.debug).toHaveBeenCalledWith("Fetched 0 surveys from the backend");
expect(mockLogger.debug).toHaveBeenCalledWith("0 surveys could be shown to current user on trigger: ");
});
test("resets config if no valid config found, fetches environment, sets default user", async () => {