diff --git a/apps/web/modules/survey/editor/components/cta-question-form.tsx b/apps/web/modules/survey/editor/components/cta-question-form.tsx index 7b1a181d05..f8706a93c0 100644 --- a/apps/web/modules/survey/editor/components/cta-question-form.tsx +++ b/apps/web/modules/survey/editor/components/cta-question-form.tsx @@ -87,21 +87,6 @@ export const CTAQuestionForm = ({
- - {questionIdx !== 0 && ( )} +
diff --git a/apps/web/modules/survey/editor/components/question-card.test.tsx b/apps/web/modules/survey/editor/components/question-card.test.tsx index bbfb0c5008..5b8f66a0c5 100644 --- a/apps/web/modules/survey/editor/components/question-card.test.tsx +++ b/apps/web/modules/survey/editor/components/question-card.test.tsx @@ -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(); - 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(); + + 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(); + + 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(); + + 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(); + 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(); + 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(); + + // 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(); + + 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(); + + 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(); + + 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(); + }); }); diff --git a/apps/web/modules/survey/editor/components/question-card.tsx b/apps/web/modules/survey/editor/components/question-card.tsx index 5b7478950a..a8cc74349d 100644 --- a/apps/web/modules/survey/editor/components/question-card.tsx +++ b/apps/web/modules/survey/editor/components/question-card.tsx @@ -196,7 +196,9 @@ export const QuestionCard = ({ )}>
{QUESTIONS_ICON_MAP[question.type]}
- @@ -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">
{/*
{QUESTIONS_ICON_MAP[question.type]}
*/}
-

+

{recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[ selectedLanguageCode ] @@ -232,7 +235,7 @@ export const QuestionCard = ({ ] ?? "" ) : getTSurveyQuestionTypeEnumName(question.type, t)} -

+

{!open && (

{question?.required @@ -272,7 +275,7 @@ export const QuestionCard = ({ TSurveyQuestionTypeEnum.Ranking, TSurveyQuestionTypeEnum.Matrix, ].includes(question.type) ? ( - + {t("environments.surveys.edit.caution_text")} onAlertTrigger()}>{t("common.learn_more")} @@ -457,7 +460,9 @@ export const QuestionCard = ({ ) : null}

- + {openAdvanced ? ( ) : ( @@ -473,6 +478,30 @@ export const QuestionCard = ({ question.type !== TSurveyQuestionTypeEnum.Rating && question.type !== TSurveyQuestionTypeEnum.CTA ? (
+ {questionIdx !== 0 && ( + { + if (!question.backButtonLabel) return; + let translatedBackButtonLabel = { + ...question.backButtonLabel, + [selectedLanguageCode]: e.target.value, + }; + updateEmptyButtonLabels("backButtonLabel", translatedBackButtonLabel, 0); + }} + /> + )}
- {questionIdx !== 0 && ( - { - if (!question.backButtonLabel) return; - let translatedBackButtonLabel = { - ...question.backButtonLabel, - [selectedLanguageCode]: e.target.value, - }; - updateEmptyButtonLabels("backButtonLabel", translatedBackButtonLabel, 0); - }} - /> - )}
) : null} {(question.type === TSurveyQuestionTypeEnum.Rating || diff --git a/apps/web/playwright/utils/helper.ts b/apps/web/playwright/utils/helper.ts index 2b767a5d45..0479edbeeb 100644 --- a/apps/web/playwright/utils/helper.ts +++ b/apps/web/playwright/utils/helper.ts @@ -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();