From 56ce05fb949ae3ff09f61879e7b7c4c2a4fe2bb5 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:25:41 +0530 Subject: [PATCH 1/2] fix: validation in client api (#7206) Co-authored-by: pandeymangg --- .../responses/[responseId]/route.ts | 41 +++++++++++++++++- .../client/[environmentId]/responses/route.ts | 28 ++++++++++++ .../responses/[responseId]/route.ts | 6 +-- .../app/api/v1/management/responses/route.ts | 6 +-- .../client/[environmentId]/responses/route.ts | 18 ++++++++ .../responses => }/lib/validation.test.ts | 43 ++++++++++++++----- .../responses => }/lib/validation.ts | 21 ++++++--- .../responses/[responseId]/route.ts | 5 ++- .../api/v2/management/responses/route.ts | 5 ++- 9 files changed, 142 insertions(+), 31 deletions(-) rename apps/web/modules/api/{v2/management/responses => }/lib/validation.test.ts (81%) rename apps/web/modules/api/{v2/management/responses => }/lib/validation.ts (77%) diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts index c67af83fb2..122af961e3 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts @@ -1,13 +1,15 @@ import { NextRequest } from "next/server"; import { logger } from "@formbricks/logger"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; -import { ZResponseUpdateInput } from "@formbricks/types/responses"; +import { TResponse, TResponseUpdateInput, ZResponseUpdateInput } from "@formbricks/types/responses"; +import { TSurvey } from "@formbricks/types/surveys/types"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging"; import { sendToPipeline } from "@/app/lib/pipelines"; import { getResponse } from "@/lib/response/service"; import { getSurvey } from "@/lib/survey/service"; +import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation"; import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element"; import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers"; import { validateFileUploads } from "@/modules/storage/utils"; @@ -31,6 +33,38 @@ const handleDatabaseError = (error: Error, url: string, endpoint: string, respon return responses.internalServerErrorResponse("Unknown error occurred", true); }; +const validateResponse = ( + response: TResponse, + survey: TSurvey, + responseUpdateInput: TResponseUpdateInput +) => { + // Validate response data against validation rules + const mergedData = { + ...response.data, + ...responseUpdateInput.data, + }; + + const isFinished = responseUpdateInput.finished ?? false; + + const validationErrors = validateResponseData( + survey.blocks, + mergedData, + responseUpdateInput.language ?? response.language ?? "en", + isFinished, + survey.questions + ); + + if (validationErrors) { + return { + response: responses.badRequestResponse( + "Validation failed", + formatValidationErrorsForV1Api(validationErrors), + true + ), + }; + } +}; + export const PUT = withV1ApiWrapper({ handler: async ({ req, @@ -113,6 +147,11 @@ export const PUT = withV1ApiWrapper({ }; } + const validationResult = validateResponse(response, survey, inputValidation.data); + if (validationResult) { + return validationResult; + } + // update response with quota evaluation let updatedResponse; try { diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/route.ts index 4ac3f35edc..af4aaff8e6 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/route.ts @@ -6,12 +6,14 @@ import { ZEnvironmentId } from "@formbricks/types/environment"; import { InvalidInputError } from "@formbricks/types/errors"; import { TResponseWithQuotaFull } from "@formbricks/types/quota"; import { TResponseInput, ZResponseInput } from "@formbricks/types/responses"; +import { TSurvey } from "@formbricks/types/surveys/types"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging"; import { sendToPipeline } from "@/app/lib/pipelines"; import { getSurvey } from "@/lib/survey/service"; import { getClientIpFromHeaders } from "@/lib/utils/client-ip"; +import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers"; import { validateFileUploads } from "@/modules/storage/utils"; @@ -33,6 +35,27 @@ export const OPTIONS = async (): Promise => { ); }; +const validateResponse = (responseInputData: TResponseInput, survey: TSurvey) => { + // Validate response data against validation rules + const validationErrors = validateResponseData( + survey.blocks, + responseInputData.data, + responseInputData.language ?? "en", + responseInputData.finished, + survey.questions + ); + + if (validationErrors) { + return { + response: responses.badRequestResponse( + "Validation failed", + formatValidationErrorsForV1Api(validationErrors), + true + ), + }; + } +}; + export const POST = withV1ApiWrapper({ handler: async ({ req, props }: { req: NextRequest; props: Context }) => { const params = await props.params; @@ -123,6 +146,11 @@ export const POST = withV1ApiWrapper({ }; } + const validationResult = validateResponse(responseInputData, survey); + if (validationResult) { + return validationResult; + } + let response: TResponseWithQuotaFull; try { const meta: TResponseInput["meta"] = { diff --git a/apps/web/app/api/v1/management/responses/[responseId]/route.ts b/apps/web/app/api/v1/management/responses/[responseId]/route.ts index 66333e3ce0..189fdfee63 100644 --- a/apps/web/app/api/v1/management/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/management/responses/[responseId]/route.ts @@ -8,10 +8,7 @@ import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib import { sendToPipeline } from "@/app/lib/pipelines"; import { deleteResponse, getResponse } from "@/lib/response/service"; import { getSurvey } from "@/lib/survey/service"; -import { - formatValidationErrorsForV1Api, - validateResponseData, -} from "@/modules/api/v2/management/responses/lib/validation"; +import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; import { validateFileUploads } from "@/modules/storage/utils"; import { updateResponseWithQuotaEvaluation } from "./lib/response"; @@ -149,6 +146,7 @@ export const PUT = withV1ApiWrapper({ result.survey.blocks, responseUpdate.data, responseUpdate.language ?? "en", + responseUpdate.finished, result.survey.questions ); diff --git a/apps/web/app/api/v1/management/responses/route.ts b/apps/web/app/api/v1/management/responses/route.ts index 49090e33fd..8d92197543 100644 --- a/apps/web/app/api/v1/management/responses/route.ts +++ b/apps/web/app/api/v1/management/responses/route.ts @@ -7,10 +7,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging"; import { sendToPipeline } from "@/app/lib/pipelines"; import { getSurvey } from "@/lib/survey/service"; -import { - formatValidationErrorsForV1Api, - validateResponseData, -} from "@/modules/api/v2/management/responses/lib/validation"; +import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; import { validateFileUploads } from "@/modules/storage/utils"; import { @@ -158,6 +155,7 @@ export const POST = withV1ApiWrapper({ surveyResult.survey.blocks, responseInput.data, responseInput.language ?? "en", + responseInput.finished, surveyResult.survey.questions ); diff --git a/apps/web/app/api/v2/client/[environmentId]/responses/route.ts b/apps/web/app/api/v2/client/[environmentId]/responses/route.ts index b366e0417b..d4427739e0 100644 --- a/apps/web/app/api/v2/client/[environmentId]/responses/route.ts +++ b/apps/web/app/api/v2/client/[environmentId]/responses/route.ts @@ -11,6 +11,7 @@ import { sendToPipeline } from "@/app/lib/pipelines"; import { getSurvey } from "@/lib/survey/service"; import { getElementsFromBlocks } from "@/lib/survey/utils"; import { getClientIpFromHeaders } from "@/lib/utils/client-ip"; +import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation"; import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers"; @@ -106,6 +107,23 @@ export const POST = async (request: Request, context: Context): Promise { mockGetElementsFromBlocks.mockReturnValue(mockElements); mockValidateBlockResponses.mockReturnValue({}); - validateResponseData([], mockResponseData, "en", mockQuestions); + validateResponseData([], mockResponseData, "en", true, mockQuestions); expect(mockTransformQuestionsToBlocks).toHaveBeenCalledWith(mockQuestions, []); expect(mockGetElementsFromBlocks).toHaveBeenCalledWith(transformedBlocks); @@ -105,15 +105,15 @@ describe("validateResponseData", () => { mockGetElementsFromBlocks.mockReturnValue(mockElements); mockValidateBlockResponses.mockReturnValue({}); - validateResponseData(mockBlocks, mockResponseData, "en", mockQuestions); + validateResponseData(mockBlocks, mockResponseData, "en", true, mockQuestions); expect(mockTransformQuestionsToBlocks).not.toHaveBeenCalled(); }); test("should return null when both blocks and questions are empty", () => { - expect(validateResponseData([], mockResponseData, "en", [])).toBeNull(); - expect(validateResponseData(null, mockResponseData, "en", [])).toBeNull(); - expect(validateResponseData(undefined, mockResponseData, "en", null)).toBeNull(); + expect(validateResponseData([], mockResponseData, "en", true, [])).toBeNull(); + expect(validateResponseData(null, mockResponseData, "en", true, [])).toBeNull(); + expect(validateResponseData(undefined, mockResponseData, "en", true, null)).toBeNull(); }); test("should use default language code", () => { @@ -124,15 +124,36 @@ describe("validateResponseData", () => { expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, mockResponseData, "en"); }); + + test("should validate only present fields when finished is false", () => { + const partialResponseData: TResponseData = { element1: "test" }; + const partialElements = [mockElements[0]]; + mockGetElementsFromBlocks.mockReturnValue(mockElements); + mockValidateBlockResponses.mockReturnValue({}); + + validateResponseData(mockBlocks, partialResponseData, "en", false); + + expect(mockValidateBlockResponses).toHaveBeenCalledWith(partialElements, partialResponseData, "en"); + }); + + test("should validate all fields when finished is true", () => { + const partialResponseData: TResponseData = { element1: "test" }; + mockGetElementsFromBlocks.mockReturnValue(mockElements); + mockValidateBlockResponses.mockReturnValue({}); + + validateResponseData(mockBlocks, partialResponseData, "en", true); + + expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, partialResponseData, "en"); + }); }); -describe("formatValidationErrorsForApi", () => { +describe("formatValidationErrorsForV2Api", () => { test("should convert error map to V2 API format", () => { const errorMap: TValidationErrorMap = { element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length required" }], }; - const result = formatValidationErrorsForApi(errorMap); + const result = formatValidationErrorsForV2Api(errorMap); expect(result).toEqual([ { @@ -151,7 +172,7 @@ describe("formatValidationErrorsForApi", () => { ], }; - const result = formatValidationErrorsForApi(errorMap); + const result = formatValidationErrorsForV2Api(errorMap); expect(result).toHaveLength(2); expect(result[0].field).toBe("response.data.element1"); @@ -164,7 +185,7 @@ describe("formatValidationErrorsForApi", () => { element2: [{ ruleId: "maxLength", ruleType: "maxLength", message: "Max length" }], }; - const result = formatValidationErrorsForApi(errorMap); + const result = formatValidationErrorsForV2Api(errorMap); expect(result).toHaveLength(2); expect(result[0].field).toBe("response.data.element1"); diff --git a/apps/web/modules/api/v2/management/responses/lib/validation.ts b/apps/web/modules/api/lib/validation.ts similarity index 77% rename from apps/web/modules/api/v2/management/responses/lib/validation.ts rename to apps/web/modules/api/lib/validation.ts index b1057a3253..03f0fec757 100644 --- a/apps/web/modules/api/v2/management/responses/lib/validation.ts +++ b/apps/web/modules/api/lib/validation.ts @@ -10,17 +10,20 @@ import { ApiErrorDetails } from "@/modules/api/v2/types/api-error"; /** * Validates response data against survey validation rules + * Handles partial responses (in-progress) by only validating present fields when finished is false * * @param blocks - Survey blocks containing elements with validation rules (preferred) - * @param questions - Survey questions (legacy format, used as fallback if blocks are empty) * @param responseData - Response data to validate (keyed by element ID) * @param languageCode - Language code for error messages (defaults to "en") + * @param finished - Whether the response is finished (defaults to true for management APIs) + * @param questions - Survey questions (legacy format, used as fallback if blocks are empty) * @returns Validation error map keyed by element ID, or null if validation passes */ export const validateResponseData = ( blocks: TSurveyBlock[] | undefined | null, responseData: TResponseData, languageCode: string = "en", + finished: boolean = true, questions?: TSurveyQuestion[] | undefined | null ): TValidationErrorMap | null => { // Use blocks if available, otherwise transform questions to blocks @@ -37,22 +40,26 @@ export const validateResponseData = ( } // Extract elements from blocks - const elements = getElementsFromBlocks(blocksToUse); + const allElements = getElementsFromBlocks(blocksToUse); - // Validate all elements - const errorMap = validateBlockResponses(elements, responseData, languageCode); + // If response is not finished, only validate elements that are present in the response data + // This prevents "required" errors for fields the user hasn't reached yet + const elementsToValidate = finished ? allElements : allElements.filter((element) => Object.keys(responseData).includes(element.id)); + + // Validate selected elements + const errorMap = validateBlockResponses(elementsToValidate, responseData, languageCode); // Return null if no errors (validation passed), otherwise return error map return Object.keys(errorMap).length === 0 ? null : errorMap; }; /** - * Converts validation error map to API error response format (V2) + * Converts validation error map to V2 API error response format * * @param errorMap - Validation error map from validateResponseData - * @returns API error response details + * @returns V2 API error response details */ -export const formatValidationErrorsForApi = (errorMap: TValidationErrorMap) => { +export const formatValidationErrorsForV2Api = (errorMap: TValidationErrorMap) => { const details: ApiErrorDetails = []; for (const [elementId, errors] of Object.entries(errorMap)) { diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/route.ts b/apps/web/modules/api/v2/management/responses/[responseId]/route.ts index af8c3b72e2..c8bd252fd5 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/route.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/route.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { sendToPipeline } from "@/app/lib/pipelines"; +import { formatValidationErrorsForV2Api, validateResponseData } from "@/modules/api/lib/validation"; import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element"; import { responses } from "@/modules/api/v2/lib/response"; @@ -15,7 +16,6 @@ import { getSurveyQuestions } from "@/modules/api/v2/management/responses/[respo import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; import { validateFileUploads } from "@/modules/storage/utils"; -import { formatValidationErrorsForApi, validateResponseData } from "../lib/validation"; import { ZResponseIdSchema, ZResponseUpdateSchema } from "./types/responses"; export const GET = async (request: Request, props: { params: Promise<{ responseId: string }> }) => @@ -198,6 +198,7 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str questionsResponse.data.blocks, body.data, body.language ?? "en", + body.finished, questionsResponse.data.questions ); @@ -206,7 +207,7 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str request, { type: "bad_request", - details: formatValidationErrorsForApi(validationErrors), + details: formatValidationErrorsForV2Api(validationErrors), }, auditLog ); diff --git a/apps/web/modules/api/v2/management/responses/route.ts b/apps/web/modules/api/v2/management/responses/route.ts index 75de7f1e1c..438e0dff55 100644 --- a/apps/web/modules/api/v2/management/responses/route.ts +++ b/apps/web/modules/api/v2/management/responses/route.ts @@ -1,6 +1,7 @@ import { Response } from "@prisma/client"; import { NextRequest } from "next/server"; import { sendToPipeline } from "@/app/lib/pipelines"; +import { formatValidationErrorsForV2Api, validateResponseData } from "@/modules/api/lib/validation"; import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element"; import { responses } from "@/modules/api/v2/lib/response"; @@ -13,7 +14,6 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; import { validateFileUploads } from "@/modules/storage/utils"; import { createResponseWithQuotaEvaluation, getResponses } from "./lib/response"; -import { formatValidationErrorsForApi, validateResponseData } from "./lib/validation"; export const GET = async (request: NextRequest) => authenticatedApiClient({ @@ -134,6 +134,7 @@ export const POST = async (request: Request) => surveyQuestions.data.blocks, body.data, body.language ?? "en", + body.finished, surveyQuestions.data.questions ); @@ -142,7 +143,7 @@ export const POST = async (request: Request) => request, { type: "bad_request", - details: formatValidationErrorsForApi(validationErrors), + details: formatValidationErrorsForV2Api(validationErrors), }, auditLog ); From 4649a2de3e3b1915ac455c2a9540ed78b0a2dd53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Theod=C3=B3r=20T=C3=B3mas?= Date: Fri, 6 Feb 2026 18:42:35 +0800 Subject: [PATCH 2/2] fix: fixing issue with saving follow ups (#7218) Co-authored-by: Dhruwang --- .../follow-ups/components/follow-up-modal.tsx | 2 + apps/web/playwright/survey-follow-up.spec.ts | 107 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 apps/web/playwright/survey-follow-up.spec.ts diff --git a/apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx b/apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx index bc4e4d1ab3..0f44aff04b 100644 --- a/apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx +++ b/apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx @@ -188,6 +188,8 @@ export const FollowUpModal = ({ subject: defaultValues?.subject ?? t("environments.surveys.edit.follow_ups_modal_action_subject"), body: defaultValues?.body ?? getSurveyFollowUpActionDefaultBody(t), attachResponseData: defaultValues?.attachResponseData ?? false, + includeVariables: defaultValues?.includeVariables ?? false, + includeHiddenFields: defaultValues?.includeHiddenFields ?? false, }, resolver: zodResolver(ZCreateSurveyFollowUpFormSchema), mode: "onChange", diff --git a/apps/web/playwright/survey-follow-up.spec.ts b/apps/web/playwright/survey-follow-up.spec.ts new file mode 100644 index 0000000000..3b4abfb33b --- /dev/null +++ b/apps/web/playwright/survey-follow-up.spec.ts @@ -0,0 +1,107 @@ +import { expect } from "@playwright/test"; +import { test } from "./lib/fixtures"; + +test.describe("Survey Follow-Up Create & Edit", async () => { + // 3 minutes + test.setTimeout(1000 * 60 * 3); + + test("Create a follow-up without optional toggles and verify it saves", async ({ page, users }) => { + const user = await users.create(); + await user.login(); + + await page.waitForURL(/\/environments\/[^/]+\/surveys/); + + await test.step("Create a new survey", async () => { + await page.getByText("Start from scratch").click(); + await page.getByRole("button", { name: "Create survey", exact: true }).click(); + await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/edit$/); + }); + + await test.step("Navigate to Follow-ups tab", async () => { + await page.getByText("Follow-ups").click(); + // Verify the empty state is shown + await expect(page.getByText("Send automatic follow-ups")).toBeVisible(); + }); + + await test.step("Create a new follow-up without enabling optional toggles", async () => { + // Click the "New follow-up" button in the empty state + await page.getByRole("button", { name: "New follow-up" }).click(); + + // Verify the modal is open + await expect(page.getByText("Create a new follow-up")).toBeVisible(); + + // Fill in the follow-up name + await page.getByPlaceholder("Name your follow-up").fill("Test Follow-Up"); + + // Leave trigger as default ("Respondent completes survey") + // Leave "Attach response data" toggle OFF (the key scenario for the bug) + // Leave "Include variables" and "Include hidden fields" unchecked + + // Click Save + await page.getByRole("button", { name: "Save" }).click(); + + // The success toast should appear — this was the bug: previously save failed silently + const successToast = await page.waitForSelector(".formbricks__toast__success", { timeout: 5000 }); + expect(successToast).toBeTruthy(); + }); + + await test.step("Verify follow-up appears in the list", async () => { + // After creation, the modal closes and the follow-up should appear in the list + await expect(page.getByText("Test Follow-Up")).toBeVisible(); + await expect(page.getByText("Any response")).toBeVisible(); + await expect(page.getByText("Send email")).toBeVisible(); + }); + + await test.step("Edit the follow-up and verify it saves", async () => { + // Click on the follow-up to edit it + await page.getByText("Test Follow-Up").click(); + + // Verify the edit modal opens + await expect(page.getByText("Edit this follow-up")).toBeVisible(); + + // Change the name + const nameInput = page.getByPlaceholder("Name your follow-up"); + await nameInput.clear(); + await nameInput.fill("Updated Follow-Up"); + + // Save the edit + await page.getByRole("button", { name: "Save" }).click(); + + // The success toast should appear + const successToast = await page.waitForSelector(".formbricks__toast__success", { timeout: 5000 }); + expect(successToast).toBeTruthy(); + + // Verify the updated name appears in the list + await expect(page.getByText("Updated Follow-Up")).toBeVisible(); + }); + + await test.step("Create a second follow-up with optional toggles enabled", async () => { + // Click "+ New follow-up" button (now in the non-empty state header) + await page.getByRole("button", { name: /New follow-up/ }).click(); + + // Verify the modal is open + await expect(page.getByText("Create a new follow-up")).toBeVisible(); + + // Fill in the follow-up name + await page.getByPlaceholder("Name your follow-up").fill("Follow-Up With Data"); + + // Enable "Attach response data" toggle + await page.locator("#attachResponseData").click(); + + // Check both optional checkboxes + await page.locator("#includeVariables").click(); + await page.locator("#includeHiddenFields").click(); + + // Click Save + await page.getByRole("button", { name: "Save" }).click(); + + // The success toast should appear + const successToast = await page.waitForSelector(".formbricks__toast__success", { timeout: 5000 }); + expect(successToast).toBeTruthy(); + + // Verify both follow-ups appear in the list + await expect(page.getByText("Updated Follow-Up")).toBeVisible(); + await expect(page.getByText("Follow-Up With Data")).toBeVisible(); + }); + }); +});