From 19389bfffcc4fd559fef7c2e2ffc1cafb74abfb0 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Wed, 22 Oct 2025 14:55:44 +0200 Subject: [PATCH] chore: exclude TSX files from unit test coverage (#6723) Co-authored-by: Johannes --- .cursor/rules/testing-patterns.mdc | 322 --- .cursor/rules/testing.mdc | 7 - .github/copilot-instructions.md | 32 - AGENTS.md | 4 +- .../components/ConnectWithFormbricks.test.tsx | 79 - .../OnboardingSetupInstructions.test.tsx | 103 - .../[environmentId]/layout.test.tsx | 147 -- .../components/XMTemplateList.test.tsx | 76 - .../components/landing-sidebar.test.tsx | 82 - .../[organizationId]/landing/layout.test.tsx | 187 -- .../[organizationId]/landing/page.test.tsx | 227 -- .../[organizationId]/layout.test.tsx | 159 -- .../projects/new/channel/page.test.tsx | 88 - .../projects/new/layout.test.tsx | 223 -- .../projects/new/mode/page.test.tsx | 72 - .../components/ProjectSettings.test.tsx | 124 -- .../projects/new/settings/page.test.tsx | 106 - .../OnboardingOptionsContainer.test.tsx | 106 - .../[environmentId]/layout.test.tsx | 114 - .../contacts/[contactId]/page.test.tsx | 43 - .../(contacts)/contacts/page.test.tsx | 15 - .../(contacts)/segments/page.test.tsx | 18 - .../components/EnvironmentLayout.test.tsx | 472 ---- .../EnvironmentStorageHandler.test.tsx | 33 - .../components/EnvironmentSwitch.test.tsx | 149 -- .../components/MainNavigation.test.tsx | 287 --- .../components/NavbarLoading.test.tsx | 21 - .../components/NavigationLink.test.tsx | 105 - .../components/PosthogIdentify.test.tsx | 145 -- .../components/ProjectNavItem.test.tsx | 40 - .../components/ResponseFilterContext.test.tsx | 140 -- .../components/WidgetStatusIndicator.test.tsx | 104 - .../environment-breadcrumb.test.tsx | 329 --- .../organization-breadcrumb.test.tsx | 560 ----- .../project-and-org-switch.test.tsx | 340 --- .../components/project-breadcrumb.test.tsx | 512 ----- .../context/environment-context.test.tsx | 157 -- .../[environmentId]/layout.test.tsx | 327 --- .../[environmentId]/page.test.tsx | 141 -- .../(setup)/app-connection/loading.test.tsx | 15 - .../(setup)/app-connection/page.test.tsx | 42 - .../project/general/loading.test.tsx | 17 - .../project/general/page.test.tsx | 42 - .../components/AddIntegrationModal.test.tsx | 466 ---- .../components/AirtableWrapper.test.tsx | 134 -- .../components/BaseSelectDropdown.test.tsx | 125 -- .../components/ManageIntegration.test.tsx | 151 -- .../integrations/airtable/page.test.tsx | 223 -- .../components/AddIntegrationModal.test.tsx | 702 ------ .../components/GoogleSheetWrapper.test.tsx | 175 -- .../components/ManageIntegration.test.tsx | 162 -- .../google-sheets/loading.test.tsx | 40 - .../integrations/google-sheets/page.test.tsx | 225 -- .../components/AddIntegrationModal.test.tsx | 630 ------ .../components/ManageIntegration.test.tsx | 93 - .../notion/components/NotionWrapper.test.tsx | 155 -- .../integrations/notion/loading.test.tsx | 50 - .../project/integrations/notion/page.test.tsx | 248 --- .../project/integrations/page.test.tsx | 241 -- .../AddChannelMappingModal.test.tsx | 761 ------- .../components/ManageIntegration.test.tsx | 158 -- .../slack/components/SlackWrapper.test.tsx | 174 -- .../project/integrations/slack/page.test.tsx | 222 -- .../integrations/webhooks/page.test.tsx | 14 - .../project/languages/loading.test.tsx | 15 - .../project/languages/page.test.tsx | 36 - .../[environmentId]/project/layout.test.tsx | 24 - .../project/look/loading.test.tsx | 17 - .../project/look/page.test.tsx | 42 - .../[environmentId]/project/page.test.tsx | 33 - .../project/tags/loading.test.tsx | 15 - .../project/tags/page.test.tsx | 36 - .../project/teams/page.test.tsx | 36 - .../components/AccountSettingsNavbar.test.tsx | 148 -- .../settings/(account)/layout.test.tsx | 98 - .../components/EditAlerts.test.tsx | 267 --- .../components/IntegrationsTip.test.tsx | 36 - .../components/NotificationSwitch.test.tsx | 410 ---- .../(account)/notifications/loading.test.tsx | 38 - .../(account)/notifications/page.test.tsx | 228 -- .../components/AccountSecurity.test.tsx | 70 - .../profile/components/DeleteAccount.test.tsx | 97 - .../EditProfileDetailsForm.test.tsx | 209 -- .../password-confirmation-modal.test.tsx | 141 -- .../(account)/profile/loading.test.tsx | 60 - .../settings/(account)/profile/page.test.tsx | 184 -- .../(organization)/api-keys/loading.test.tsx | 29 - .../(organization)/api-keys/page.test.tsx | 21 - .../(organization)/billing/loading.test.tsx | 74 - .../(organization)/billing/page.test.tsx | 21 - .../OrganizationSettingsNavbar.test.tsx | 134 -- .../enterprise/loading.test.tsx | 68 - .../(organization)/enterprise/page.test.tsx | 199 -- .../components/DeleteOrganization.test.tsx | 192 -- .../EditOrganizationNameForm.test.tsx | 149 -- .../(organization)/general/loading.test.tsx | 67 - .../(organization)/general/page.test.tsx | 405 ---- .../settings/(organization)/layout.test.tsx | 98 - .../(organization)/teams/page.test.tsx | 47 - .../settings/components/SettingsCard.test.tsx | 72 - .../components/SettingsTitle.test.tsx | 25 - .../[environmentId]/settings/page.test.tsx | 15 - .../components/EmptyInAppSurveys.test.tsx | 37 - .../SurveyAnalysisNavigation.test.tsx | 225 -- .../[surveyId]/(analysis)/layout.test.tsx | 121 - .../components/ResponseCardModal.test.tsx | 257 --- .../components/ResponseDataView.test.tsx | 383 ---- .../components/ResponsePage.test.tsx | 334 --- .../components/ResponseTable.test.tsx | 588 ----- .../components/ResponseTableCell.test.tsx | 165 -- .../components/ResponseTableColumns.test.tsx | 795 ------- .../(analysis)/responses/page.test.tsx | 274 --- .../components/AddressSummary.test.tsx | 154 -- .../summary/components/CTASummary.test.tsx | 89 - .../summary/components/CalSummary.test.tsx | 69 - .../components/ConsentSummary.test.tsx | 80 - .../components/ContactInfoSummary.test.tsx | 153 -- .../components/DateQuestionSummary.test.tsx | 192 -- .../components/FileUploadSummary.test.tsx | 231 -- .../components/HiddenFieldsSummary.test.tsx | 183 -- .../components/MatrixQuestionSummary.test.tsx | 47 - .../components/MultipleChoiceSummary.test.tsx | 405 ---- .../summary/components/NPSSummary.test.tsx | 60 - .../components/OpenTextSummary.test.tsx | 174 -- .../components/PictureChoiceSummary.test.tsx | 177 -- .../components/QuestionSummaryHeader.test.tsx | 164 -- .../components/RankingSummary.test.tsx | 213 -- .../summary/components/RatingSummary.test.tsx | 87 - .../summary/components/ScrollToTop.test.tsx | 67 - .../components/SuccessMessage.test.tsx | 182 -- .../components/SummaryDropOffs.test.tsx | 127 -- .../summary/components/SummaryList.test.tsx | 461 ---- .../components/SummaryMetadata.test.tsx | 149 -- .../summary/components/SummaryPage.test.tsx | 176 -- .../components/SurveyAnalysisCTA.test.tsx | 1043 --------- .../summary/components/base-card.test.tsx | 83 - .../components/interactive-card.test.tsx | 127 -- .../components/share-survey-modal.test.tsx | 516 ----- .../anonymous-links-tab.test.tsx | 432 ---- .../shareEmbedModal/app-tab.test.tsx | 383 ---- .../disable-link-modal.test.tsx | 95 - .../documentation-links.test.tsx | 102 - .../documentationL-links-section.test.tsx | 165 -- .../dynamic-popup-tab.test.tsx | 217 -- .../shareEmbedModal/email-tab.test.tsx | 294 --- .../link-settings-tab.test.tsx | 426 ---- .../personal-links-tab.test.tsx | 526 ----- .../shareEmbedModal/qr-code-tab.test.tsx | 284 --- .../shareEmbedModal/share-view.test.tsx | 624 ------ .../shareEmbedModal/social-media-tab.test.tsx | 138 -- .../shareEmbedModal/tab-container.test.tsx | 68 - .../website-embed-tab.test.tsx | 183 -- .../summary/components/stat-card.test.tsx | 80 - .../summary/lib/emailTemplate.test.tsx | 175 -- .../(analysis)/summary/loading.test.tsx | 39 - .../(analysis)/summary/page.test.tsx | 307 --- .../components/CustomFilter.test.tsx | 256 --- .../QuestionFilterComboBox.test.tsx | 182 -- .../components/QuestionsComboBox.test.tsx | 126 -- .../components/ResponseFilter.test.tsx | 296 --- .../components/SurveyStatusDropdown.test.tsx | 155 -- .../context/survey-context.test.tsx | 273 --- .../surveys/[surveyId]/layout.test.tsx | 192 -- .../surveys/[surveyId]/page.test.tsx | 23 - .../[environmentId]/surveys/loading.test.tsx | 15 - .../[environmentId]/surveys/page.test.tsx | 24 - apps/web/app/(app)/environments/page.test.tsx | 19 - apps/web/app/(app)/layout.test.tsx | 77 - .../(auth)/auth/forgot-password/page.test.tsx | 35 - .../page.test.tsx | 20 - apps/web/app/(auth)/layout.test.tsx | 34 - .../app/ClientEnvironmentRedirect.test.tsx | 98 - apps/web/app/error.test.tsx | 186 -- apps/web/app/global-error.test.tsx | 41 - apps/web/app/intercom/IntercomClient.test.tsx | 185 -- .../intercom/IntercomClientWrapper.test.tsx | 64 - apps/web/app/layout.test.tsx | 150 -- apps/web/app/not-found.test.tsx | 37 - apps/web/app/page.test.tsx | 442 ---- apps/web/app/sentry/SentryProvider.test.tsx | 182 -- .../hooks/useMembershipRole.test.tsx | 53 - .../DeleteAccountModal/index.test.tsx | 189 -- .../components/RatingSmiley/index.test.tsx | 117 - .../components/LanguageDropdown.test.tsx | 94 - .../components/SurveyLinkDisplay.test.tsx | 22 - .../components/ShareSurveyLink/index.test.tsx | 261 --- .../components/HiddenFields.test.tsx | 70 - .../components/QuestionSkip.test.tsx | 98 - .../components/RenderResponse.test.tsx | 517 ----- .../components/ResponseTagsWrapper.test.tsx | 252 --- .../components/ResponseVariables.test.tsx | 80 - .../SingleResponseCardBody.test.tsx | 125 -- .../SingleResponseCardHeader.test.tsx | 159 -- .../components/VerifiedEmail.test.tsx | 31 - .../SingleResponseCard/index.test.tsx | 183 -- apps/web/modules/analysis/utils.test.tsx | 75 - .../components/back-to-login-button.test.tsx | 35 - .../auth/components/form-wrapper.test.tsx | 55 - .../page.test.tsx | 61 - .../forgot-password/email-sent/page.test.tsx | 26 - .../auth/forgot-password/page.test.tsx | 27 - .../components/reset-password-form.test.tsx | 132 -- .../reset/success/page.test.tsx | 30 - .../modules/auth/hooks/use-sign-out.test.tsx | 251 --- .../invite/components/content-layout.test.tsx | 27 - apps/web/modules/auth/invite/page.test.tsx | 99 - .../page.test.tsx | 83 - .../signup/components/signup-form.test.tsx | 406 ---- apps/web/modules/auth/signup/page.test.tsx | 191 -- .../request-verification-email.test.tsx | 81 - .../auth/verification-requested/page.test.tsx | 138 -- .../components/email-change-sign-in.test.tsx | 75 - .../auth/verify-email-change/page.test.tsx | 47 - .../billing/components/pricing-table.test.tsx | 174 -- .../components/attributes-section.test.tsx | 127 -- .../components/delete-contact-button.test.tsx | 124 -- .../components/response-feed.test.tsx | 137 -- .../components/response-section.test.tsx | 190 -- .../components/response-timeline.test.tsx | 98 - .../components/add-filter-modal.test.tsx | 705 ------ .../components/attribute-tab-content.test.tsx | 72 - .../components/create-segment-modal.test.tsx | 346 --- .../components/edit-segment-modal.test.tsx | 201 -- .../components/filter-button.test.tsx | 38 - .../components/segment-activity-tab.test.tsx | 126 -- .../components/segment-editor.test.tsx | 497 ----- .../components/segment-filter.test.tsx | 1198 ---------- .../components/segment-settings.test.tsx | 516 ----- .../segment-table-data-row-container.test.tsx | 223 -- .../segment-table-data-row.test.tsx | 152 -- .../components/segment-table.test.tsx | 113 - .../components/targeting-card.test.tsx | 414 ---- .../ee/contacts/segments/loading.test.tsx | 38 - .../ee/contacts/segments/page.test.tsx | 220 -- .../components/ending-card-selector.test.tsx | 180 -- .../quota-condition-builder.test.tsx | 244 -- .../ee/quotas/components/quota-list.test.tsx | 205 -- .../ee/quotas/components/quota-modal.test.tsx | 645 ------ .../ee/quotas/components/quotas-card.test.tsx | 486 ---- .../quotas/components/quotas-summary.test.tsx | 141 -- .../single-response-card-quotas.test.tsx | 30 - .../components/add-member.test.tsx | 110 - .../components/edit-membership-role.test.tsx | 129 -- .../ee/sso/components/azure-button.test.tsx | 84 - .../ee/sso/components/github-button.test.tsx | 76 - .../ee/sso/components/google-button.test.tsx | 76 - .../ee/sso/components/open-id-button.test.tsx | 91 - .../ee/sso/components/saml-button.test.tsx | 130 -- .../ee/sso/components/sso-options.test.tsx | 137 -- .../components/access-table.test.tsx | 41 - .../components/access-view.test.tsx | 72 - .../components/manage-team.test.tsx | 46 - .../ee/teams/project-teams/loading.test.tsx | 41 - .../ee/teams/project-teams/page.test.tsx | 73 - .../components/create-team-button.test.tsx | 27 - .../components/create-team-modal.test.tsx | 97 - .../components/manage-team-button.test.tsx | 42 - .../team-settings/delete-team.test.tsx | 98 - .../team-settings-modal.test.tsx | 177 -- .../team-list/components/teams-table.test.tsx | 154 -- .../components/confirm-password-form.test.tsx | 93 - .../disable-two-factor-modal.test.tsx | 162 -- .../components/display-backup-codes.test.tsx | 80 - .../enable-two-factor-modal.test.tsx | 177 -- .../components/enter-code.test.tsx | 96 - .../components/scan-qr-code.test.tsx | 83 - .../components/two-factor-backup.test.tsx | 49 - .../components/two-factor.test.tsx | 52 - .../email-customization-settings.test.tsx | 146 -- .../email/components/email-template.test.tsx | 84 - .../email/emails/lib/tests/utils.test.tsx | 259 --- .../components/add-webhook-modal.test.tsx | 181 -- .../components/webhook-detail-modal.test.tsx | 239 -- .../CreateOrganizationModal/index.test.tsx | 139 -- .../components/add-api-key-modal.test.tsx | 292 --- .../api-keys/components/api-key-list.test.tsx | 169 -- .../components/edit-api-keys.test.tsx | 367 --- .../components/view-permission-modal.test.tsx | 161 -- .../settings/api-keys/loading.test.tsx | 43 - .../settings/api-keys/page.test.tsx | 104 - .../edit-memberships.test.tsx | 118 - .../edit-memberships/members-info.test.tsx | 203 -- .../organization-actions.test.tsx | 326 --- .../invite-member/bulk-invite-tab.test.tsx | 197 -- .../individual-invite-tab.test.tsx | 117 - .../invite-member-modal.test.tsx | 88 - .../invite-member/share-invite-modal.test.tsx | 74 - .../teams/components/members-view.test.tsx | 119 - .../organization/settings/teams/page.test.tsx | 93 - .../create-project-modal/index.test.tsx | 436 ---- .../project-limit-modal/index.test.tsx | 72 - .../(setup)/app-connection/loading.test.tsx | 59 - .../(setup)/app-connection/page.test.tsx | 164 -- .../(setup)/app-connection/utils.test.tsx | 39 - .../components/ActionActivityTab.test.tsx | 369 --- .../components/ActionClassesTable.test.tsx | 122 - .../components/ActionDetailModal.test.tsx | 258 --- .../(setup)/components/ActionRowData.test.tsx | 63 - .../components/ActionSettingsTab.test.tsx | 414 ---- .../components/ActionTableHeading.test.tsx | 26 - .../components/AddActionModal.test.tsx | 166 -- .../project-config-navigation.test.tsx | 49 - .../components/delete-project-render.test.tsx | 195 -- .../components/delete-project.test.tsx | 139 -- .../edit-project-name-form.test.tsx | 107 - .../edit-waiting-time-form.test.tsx | 114 - .../settings/general/loading.test.tsx | 53 - .../projects/settings/general/page.test.tsx | 129 -- .../modules/projects/settings/layout.test.tsx | 41 - .../look/components/edit-logo.test.tsx | 422 ---- .../components/edit-placement-form.test.tsx | 117 - .../look/components/theme-styling.test.tsx | 209 -- .../projects/settings/look/loading.test.tsx | 66 - .../projects/settings/look/page.test.tsx | 241 -- .../modules/projects/settings/page.test.tsx | 20 - .../components/edit-tags-wrapper.test.tsx | 88 - .../components/merge-tags-combobox.test.tsx | 70 - .../tags/components/single-tag.test.tsx | 150 -- .../projects/settings/tags/loading.test.tsx | 51 - .../projects/settings/tags/page.test.tsx | 80 - .../(fresh-instance)/signup/page.test.tsx | 96 - .../invite/components/invite-members.test.tsx | 169 -- .../[organizationId]/invite/page.test.tsx | 177 -- .../components/create-organization.test.tsx | 122 - .../removed-from-organization.test.tsx | 120 - .../setup/organization/create/page.test.tsx | 282 --- .../index.test.tsx | 183 -- .../components/fallback-input.test.tsx | 146 -- .../components/multi-lang-wrapper.test.tsx | 144 -- .../components/recall-item-select.test.tsx | 221 -- .../components/recall-wrapper.test.tsx | 223 -- .../question-form-input/index.test.tsx | 837 ------- .../start-from-scratch-template.test.tsx | 198 -- .../components/template-filters.test.tsx | 88 - .../components/template-tags.test.tsx | 103 - .../components/template.test.tsx | 110 - .../components/template-list/index.test.tsx | 240 -- .../components/add-action-modal.test.tsx | 234 -- .../add-ending-card-button.test.tsx | 86 - .../components/add-question-button.test.tsx | 159 -- .../components/address-question-form.test.tsx | 416 ---- .../components/advanced-settings.test.tsx | 651 ------ .../components/animated-survey-bg.test.tsx | 92 - .../components/cal-question-form.test.tsx | 339 --- .../components/color-survey-bg.test.tsx | 194 -- .../components/conditional-logic.test.tsx | 280 --- .../components/consent-question-form.test.tsx | 74 - .../contact-info-question-form.test.tsx | 269 --- .../components/create-new-action-tab.test.tsx | 314 --- .../components/cta-question-form.test.tsx | 82 - .../components/date-question-form.test.tsx | 405 ---- .../components/edit-ending-card.test.tsx | 71 - .../components/edit-welcome-card.test.tsx | 264 --- .../components/editor-card-menu.test.tsx | 159 -- .../components/end-screen-form.test.tsx | 324 --- .../file-upload-question-form.test.tsx | 316 --- .../components/form-styling-settings.test.tsx | 418 ---- .../components/hidden-fields-card.test.tsx | 513 ----- .../components/how-to-send-card.test.tsx | 447 ---- .../components/image-survey-bg.test.tsx | 233 -- .../components/loading-skeleton.test.tsx | 27 - .../components/logic-editor-actions.test.tsx | 340 --- .../logic-editor-conditions.test.tsx | 109 - .../editor/components/logic-editor.test.tsx | 123 - .../components/matrix-question-form.test.tsx | 270 --- .../multiple-choice-question-form.test.tsx | 1110 --------- .../components/nps-question-form.test.tsx | 227 -- .../components/open-question-form.test.tsx | 357 --- .../editor/components/option-ids.test.tsx | 177 -- .../picture-selection-form.test.tsx | 182 -- .../editor/components/placement.test.tsx | 121 - .../editor/components/question-card.test.tsx | 594 ----- .../question-option-choice.test.tsx | 446 ---- .../components/questions-droppable.test.tsx | 381 ---- .../editor/components/questions-view.test.tsx | 568 ----- .../components/ranking-question-form.test.tsx | 205 -- .../components/rating-question-form.test.tsx | 530 ----- .../components/rating-type-dropdown.test.tsx | 168 -- .../recontact-options-card.test.tsx | 348 --- .../components/redirect-url-form.test.tsx | 246 -- .../components/response-options-card.test.tsx | 150 -- .../components/saved-actions-tab.test.tsx | 547 ----- .../editor/components/settings-view.test.tsx | 256 --- .../editor/components/styling-view.test.tsx | 330 --- .../components/survey-editor-tabs.test.tsx | 128 -- .../editor/components/survey-editor.test.tsx | 419 ---- .../components/survey-menu-bar.test.tsx | 476 ---- .../components/survey-placement-card.test.tsx | 283 --- .../survey-variables-card-item.test.tsx | 700 ------ .../components/survey-variables-card.test.tsx | 167 -- .../components/targeting-locked-card.test.tsx | 77 - .../components/unsplash-images.test.tsx | 108 - .../components/update-question-id.test.tsx | 312 --- .../components/when-to-send-card.test.tsx | 648 ------ .../modules/survey/editor/lib/utils.test.tsx | 1603 ------------- ...ollow-up-action-multi-email-input.test.tsx | 116 - .../components/follow-up-email.test.tsx | 178 -- .../components/follow-up-item.test.tsx | 982 -------- .../components/follow-up-modal.test.tsx | 232 -- .../survey/hooks/useSingleUseId.test.tsx | 149 -- .../link/components/legal-footer.test.tsx | 92 - .../components/link-survey-wrapper.test.tsx | 187 -- .../link/components/link-survey.test.tsx | 217 -- .../link/components/pin-screen.test.tsx | 132 -- .../link/components/survey-inactive.test.tsx | 138 -- .../link/components/survey-link-used.test.tsx | 62 - .../survey-loading-animation.test.tsx | 489 ---- .../link/components/survey-renderer.test.tsx | 283 --- .../link/components/verify-email.test.tsx | 118 - .../survey/link/contact-survey/page.test.tsx | 186 -- apps/web/modules/survey/link/layout.test.tsx | 30 - apps/web/modules/survey/link/loading.test.tsx | 22 - .../modules/survey/link/not-found.test.tsx | 51 - apps/web/modules/survey/link/page.test.tsx | 421 ---- .../list/components/copy-survey-form.test.tsx | 447 ---- .../components/copy-survey-modal.test.tsx | 122 - .../list/components/sort-option.test.tsx | 73 - .../list/components/survey-card.test.tsx | 99 - .../components/survey-copy-options.test.tsx | 200 -- .../components/survey-dropdown-menu.test.tsx | 621 ------ .../survey-filter-dropdown.test.tsx | 150 -- .../list/components/survey-filters.test.tsx | 381 ---- .../list/components/survey-list.test.tsx | 367 --- .../list/components/survey-loading.test.tsx | 47 - apps/web/modules/survey/list/page.test.tsx | 365 --- .../components/template-container.test.tsx | 172 -- .../action-class-info/index.test.tsx | 368 --- .../index.test.tsx | 211 -- .../index.test.tsx | 111 - .../advanced-option-toggle/index.test.tsx | 125 -- .../ui/components/alert-dialog/index.test.tsx | 197 -- .../ui/components/alert/index.test.tsx | 199 -- .../components/array-response/index.test.tsx | 43 - .../ui/components/avatars/index.test.tsx | 49 - .../background-styling-card/index.test.tsx | 240 -- .../survey-bg-selector-tab.test.tsx | 295 --- .../ui/components/badge/index.test.tsx | 75 - .../ui/components/breadcrumb/index.test.tsx | 241 -- .../ui/components/button/index.test.tsx | 144 -- .../ui/components/calendar/index.test.tsx | 90 - .../card-arrangement-tabs/index.test.tsx | 91 - .../card-styling-settings/index.test.tsx | 223 -- .../modules/ui/components/card/index.test.tsx | 154 -- .../ui/components/checkbox/index.test.tsx | 64 - .../ui/components/client-logo/index.test.tsx | 72 - .../components/client-logout/index.test.tsx | 54 - .../code-action-form/index.test.tsx | 115 - .../ui/components/code-block/index.test.tsx | 156 -- .../components/popover-picker.test.tsx | 103 - .../ui/components/color-picker/index.test.tsx | 121 - .../ui/components/command/index.test.tsx | 171 -- .../conditions-editor/index.test.tsx | 274 --- .../ui/components/confetti/index.test.tsx | 54 - .../index.test.tsx | 222 -- .../confirmation-modal/index.test.tsx | 278 --- .../connect-integration/index.test.tsx | 126 -- .../column-settings-dropdown.test.tsx | 59 - .../components/data-table-header.test.tsx | 167 -- .../data-table-settings-modal-item.test.tsx | 137 -- .../data-table-settings-modal.test.tsx | 247 --- .../components/data-table-toolbar.test.tsx | 131 -- .../components/selected-row-settings.test.tsx | 197 -- .../components/selection-column.test.tsx | 67 - .../ui/components/date-picker/index.test.tsx | 102 - .../decrement-quotas-checkbox/index.test.tsx | 69 - .../ui/components/default-tag/index.test.tsx | 32 - .../components/delete-dialog/index.test.tsx | 206 -- .../ui/components/dialog/index.test.tsx | 218 -- .../components/dropdown-menu/index.test.tsx | 322 --- .../add-variables-dropdown.test.tsx | 97 - .../components/auto-link-plugin.test.tsx | 100 - .../editor-content-checker.test.tsx | 91 - .../editor/components/editor.test.tsx | 454 ---- .../editor/components/link-editor.test.tsx | 196 -- .../editor/components/recall-node.test.tsx | 524 ----- .../editor/components/recall-plugin.test.tsx | 481 ---- .../editor/components/toolbar-plugin.test.tsx | 422 ---- .../empty-space-filler/index.test.tsx | 169 -- .../environment-notice/index.test.tsx | 118 - .../environmentId-base-layout/index.test.tsx | 66 - .../components/error-component/index.test.tsx | 77 - .../file-input/components/uploader.test.tsx | 254 --- .../components/video-settings.test.tsx | 148 -- .../ui/components/file-input/index.test.tsx | 173 -- .../file-upload-response/index.test.tsx | 48 - .../modules/ui/components/form/index.test.tsx | 124 -- .../components/formbricks-logo/index.test.tsx | 167 -- .../components/go-back-button/index.test.tsx | 47 - .../ui/components/header/index.test.tsx | 26 - .../highlighted-text/index.test.tsx | 62 - .../ui/components/iconbar/index.test.tsx | 147 -- .../ui/components/id-badge/index.test.tsx | 196 -- .../components/input-combo-box/index.test.tsx | 274 --- .../ui/components/input/index.test.tsx | 93 - .../integration-card/index.test.tsx | 161 -- .../ui/components/label/index.test.tsx | 57 - .../limits-reached-banner/index.test.tsx | 119 - .../load-segment-modal/index.test.tsx | 314 --- .../components/loading-spinner/index.test.tsx | 64 - .../modules/ui/components/logo/index.test.tsx | 40 - .../media-background/index.test.tsx | 211 -- .../components/modal-with-tabs/index.test.tsx | 179 -- .../ui/components/multi-select/badge.test.tsx | 67 - .../ui/components/multi-select/index.test.tsx | 218 -- .../components/css-selector.test.tsx | 138 -- .../components/inner-html-selector.test.tsx | 138 -- .../components/page-url-selector.test.tsx | 529 ----- .../no-code-action-form/index.test.tsx | 157 -- .../no-mobile-overlay/index.test.tsx | 51 - .../ui/components/option-card/index.test.tsx | 81 - .../components/options-switch/index.test.tsx | 92 - .../ui/components/otp-input/index.test.tsx | 119 - .../page-content-wrapper/index.test.tsx | 20 - .../ui/components/page-header/index.test.tsx | 58 - .../components/password-input/index.test.tsx | 83 - .../pending-downgrade-banner/index.test.tsx | 118 - .../picture-selection-response/index.test.tsx | 163 -- .../ui/components/popover/index.test.tsx | 112 - .../post-hog-client/indext.test.tsx | 105 - .../components/tab-option.test.tsx | 61 - .../components/preview-survey/index.test.tsx | 356 --- .../ui/components/pro-badge/index.test.tsx | 50 - .../ui/components/progress-bar/index.test.tsx | 105 - .../question-toggle-table/index.test.tsx | 324 --- .../ui/components/radio-group/index.test.tsx | 134 -- .../ranking-response/index.test.tsx | 193 -- .../components/rating-response/index.test.tsx | 71 - .../reset-progress-button/index.test.tsx | 53 - .../components/response-badges/index.test.tsx | 149 -- .../save-as-new-segment-modal/index.test.tsx | 257 --- .../ui/components/search-bar/index.test.tsx | 45 - .../secondary-navigation/index.test.tsx | 67 - .../components/segment-title/index.test.tsx | 58 - .../ui/components/select/index.test.tsx | 85 - .../ui/components/separator/index.test.tsx | 219 -- .../ui/components/sheet/index.test.tsx | 514 ----- .../shuffle-option-select/index.test.tsx | 104 - .../ui/components/sidebar/index.test.tsx | 586 ----- .../components/skeleton-loader/index.test.tsx | 73 - .../ui/components/skeleton/index.test.tsx | 40 - .../ui/components/slider/index.test.tsx | 97 - .../stacked-cards-container/index.test.tsx | 106 - .../index.test.tsx | 20 - .../lib/utils.test.tsx | 61 - .../ui/components/styling-tabs/index.test.tsx | 109 - .../survey-status-indicator/index.test.tsx | 143 -- .../ui/components/survey/index.test.tsx | 171 -- .../ui/components/switch/index.test.tsx | 112 - .../ui/components/tab-bar/index.test.tsx | 97 - .../ui/components/tab-nav/index.test.tsx | 62 - .../ui/components/tab-toggle/index.test.tsx | 121 - .../ui/components/table/index.test.tsx | 202 -- .../modules/ui/components/tabs/index.test.tsx | 389 ---- .../modules/ui/components/tag/index.test.tsx | 40 - .../components/tags-combobox/index.test.tsx | 184 -- .../targeting-indicator/index.test.tsx | 93 - .../index.test.tsx | 302 --- .../components/toaster-client/index.test.tsx | 39 - .../ui/components/tooltip/index.test.tsx | 201 -- .../ui/components/typography/index.test.tsx | 193 -- .../components/upgrade-prompt/index.test.tsx | 132 -- apps/web/modules/ui/hooks/use-mobile.test.tsx | 258 --- apps/web/modules/ui/lib/utils.test.tsx | 64 - apps/web/vite.config.mts | 7 +- .../components/buttons/back-button.test.tsx | 58 - .../components/buttons/submit-button.test.tsx | 97 - .../general/auto-close-progress-bar.test.tsx | 48 - .../src/components/general/cal-embed.test.tsx | 113 - .../components/general/ending-card.test.tsx | 175 -- .../general/error-component.test.tsx | 76 - .../components/general/file-input.test.tsx | 443 ---- .../general/formbricks-branding.test.tsx | 17 - .../src/components/general/headline.test.tsx | 64 - .../src/components/general/input.test.tsx | 48 - .../src/components/general/label.test.tsx | 21 - .../general/language-switch.test.tsx | 126 -- .../general/loading-spinner.test.tsx | 52 - .../components/general/progress-bar.test.tsx | 48 - .../src/components/general/progress.test.tsx | 55 - .../general/question-conditional.test.tsx | 227 -- .../general/question-media.test.tsx | 136 -- .../general/recaptcha-branding.test.tsx | 49 - .../components/general/render-survey.test.tsx | 263 --- .../general/response-error-component.test.tsx | 103 - .../src/components/general/smileys.test.tsx | 76 - .../src/components/general/subheader.test.tsx | 81 - .../general/survey-close-button.test.tsx | 94 - .../src/components/general/survey.test.tsx | 1976 ----------------- .../components/general/welcome-card.test.tsx | 329 --- .../src/components/icons/close-icon.test.tsx | 35 - .../src/components/icons/expand-icon.test.tsx | 27 - .../components/icons/image-down-icon.test.tsx | 27 - .../components/icons/language-icon.test.tsx | 34 - .../questions/address-question.test.tsx | 335 --- .../questions/cal-question.test.tsx | 134 -- .../questions/consent-question.test.tsx | 131 -- .../questions/contact-info-question.test.tsx | 267 --- .../questions/cta-question.test.tsx | 199 -- .../questions/date-question.test.tsx | 149 -- .../questions/file-upload-question.test.tsx | 235 -- .../questions/matrix-question.test.tsx | 220 -- .../multiple-choice-multi-question.test.tsx | 486 ---- .../multiple-choice-single-question.test.tsx | 639 ------ .../questions/nps-question.test.tsx | 211 -- .../questions/open-text-question.test.tsx | 781 ------- .../picture-selection-question.test.tsx | 213 -- .../questions/ranking-question.test.tsx | 131 -- .../questions/rating-question.test.tsx | 241 -- .../wrappers/auto-close-wrapper.test.tsx | 335 --- .../wrappers/scrollable-container.test.tsx | 94 - .../components/wrappers/stacked-card.test.tsx | 212 -- .../wrappers/stacked-cards-container.test.tsx | 494 ----- .../wrappers/survey-container.test.tsx | 272 --- packages/surveys/vite.config.mts | 3 +- sonar-project.properties | 6 +- 615 files changed, 11 insertions(+), 115477 deletions(-) delete mode 100644 .cursor/rules/testing-patterns.mdc delete mode 100644 .cursor/rules/testing.mdc delete mode 100644 .github/copilot-instructions.md delete mode 100644 apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/environments/[environmentId]/layout.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/layout.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/layout.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/channel/page.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/layout.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/mode/page.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.test.tsx delete mode 100644 apps/web/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer.test.tsx delete mode 100644 apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/(contacts)/contacts/[contactId]/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/(contacts)/contacts/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/(contacts)/segments/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/EnvironmentStorageHandler.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/EnvironmentSwitch.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/NavbarLoading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/NavigationLink.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/PosthogIdentify.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/ProjectNavItem.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/ResponseFilterContext.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/environment-breadcrumb.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/organization-breadcrumb.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/project-and-org-switch.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/components/project-breadcrumb.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/context/environment-context.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/layout.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/general/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/general/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AddIntegrationModal.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/BaseSelectDropdown.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/ManageIntegration.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/AddIntegrationModal.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/GoogleSheetWrapper.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/ManageIntegration.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/AddIntegrationModal.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/ManageIntegration.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/NotionWrapper.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/AddChannelMappingModal.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/ManageIntegration.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/integrations/webhooks/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/languages/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/languages/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/layout.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/look/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/look/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/tags/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/tags/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/project/teams/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/IntegrationsTip.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DeleteAccount.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/password-confirmation-modal.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/DeleteOrganization.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/EditOrganizationNameForm.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/teams/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsTitle.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/AddressSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RankingSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/base-card.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/interactive-card.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentationL-links-section.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/stat-card.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/layout.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/loading.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/page.test.tsx delete mode 100644 apps/web/app/(app)/environments/page.test.tsx delete mode 100644 apps/web/app/(app)/layout.test.tsx delete mode 100644 apps/web/app/(auth)/auth/forgot-password/page.test.tsx delete mode 100644 apps/web/app/(auth)/email-change-without-verification-success/page.test.tsx delete mode 100644 apps/web/app/(auth)/layout.test.tsx delete mode 100644 apps/web/app/ClientEnvironmentRedirect.test.tsx delete mode 100644 apps/web/app/error.test.tsx delete mode 100644 apps/web/app/global-error.test.tsx delete mode 100644 apps/web/app/intercom/IntercomClient.test.tsx delete mode 100644 apps/web/app/intercom/IntercomClientWrapper.test.tsx delete mode 100644 apps/web/app/layout.test.tsx delete mode 100644 apps/web/app/not-found.test.tsx delete mode 100644 apps/web/app/page.test.tsx delete mode 100644 apps/web/app/sentry/SentryProvider.test.tsx delete mode 100644 apps/web/lib/membership/hooks/useMembershipRole.test.tsx delete mode 100644 apps/web/modules/account/components/DeleteAccountModal/index.test.tsx delete mode 100644 apps/web/modules/analysis/components/RatingSmiley/index.test.tsx delete mode 100644 apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.test.tsx delete mode 100644 apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.test.tsx delete mode 100644 apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx delete mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.test.tsx delete mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.test.tsx delete mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.test.tsx delete mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.test.tsx delete mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/ResponseVariables.test.tsx delete mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.test.tsx delete mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.test.tsx delete mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/VerifiedEmail.test.tsx delete mode 100644 apps/web/modules/analysis/components/SingleResponseCard/index.test.tsx delete mode 100644 apps/web/modules/analysis/utils.test.tsx delete mode 100644 apps/web/modules/auth/components/back-to-login-button.test.tsx delete mode 100644 apps/web/modules/auth/components/form-wrapper.test.tsx delete mode 100644 apps/web/modules/auth/email-change-without-verification-success/page.test.tsx delete mode 100644 apps/web/modules/auth/forgot-password/email-sent/page.test.tsx delete mode 100644 apps/web/modules/auth/forgot-password/page.test.tsx delete mode 100644 apps/web/modules/auth/forgot-password/reset/components/reset-password-form.test.tsx delete mode 100644 apps/web/modules/auth/forgot-password/reset/success/page.test.tsx delete mode 100644 apps/web/modules/auth/hooks/use-sign-out.test.tsx delete mode 100644 apps/web/modules/auth/invite/components/content-layout.test.tsx delete mode 100644 apps/web/modules/auth/invite/page.test.tsx delete mode 100644 apps/web/modules/auth/signup-without-verification-success/page.test.tsx delete mode 100644 apps/web/modules/auth/signup/components/signup-form.test.tsx delete mode 100644 apps/web/modules/auth/signup/page.test.tsx delete mode 100644 apps/web/modules/auth/verification-requested/components/request-verification-email.test.tsx delete mode 100644 apps/web/modules/auth/verification-requested/page.test.tsx delete mode 100644 apps/web/modules/auth/verify-email-change/components/email-change-sign-in.test.tsx delete mode 100644 apps/web/modules/auth/verify-email-change/page.test.tsx delete mode 100644 apps/web/modules/ee/billing/components/pricing-table.test.tsx delete mode 100644 apps/web/modules/ee/contacts/[contactId]/components/attributes-section.test.tsx delete mode 100644 apps/web/modules/ee/contacts/[contactId]/components/delete-contact-button.test.tsx delete mode 100644 apps/web/modules/ee/contacts/[contactId]/components/response-feed.test.tsx delete mode 100644 apps/web/modules/ee/contacts/[contactId]/components/response-section.test.tsx delete mode 100644 apps/web/modules/ee/contacts/[contactId]/components/response-timeline.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/add-filter-modal.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/attribute-tab-content.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/create-segment-modal.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/edit-segment-modal.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/filter-button.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/segment-activity-tab.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/segment-editor.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/segment-filter.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/segment-settings.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/segment-table-data-row-container.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/segment-table-data-row.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/segment-table.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/components/targeting-card.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/loading.test.tsx delete mode 100644 apps/web/modules/ee/contacts/segments/page.test.tsx delete mode 100644 apps/web/modules/ee/quotas/components/ending-card-selector.test.tsx delete mode 100644 apps/web/modules/ee/quotas/components/quota-condition-builder.test.tsx delete mode 100644 apps/web/modules/ee/quotas/components/quota-list.test.tsx delete mode 100644 apps/web/modules/ee/quotas/components/quota-modal.test.tsx delete mode 100644 apps/web/modules/ee/quotas/components/quotas-card.test.tsx delete mode 100644 apps/web/modules/ee/quotas/components/quotas-summary.test.tsx delete mode 100644 apps/web/modules/ee/quotas/components/single-response-card-quotas.test.tsx delete mode 100644 apps/web/modules/ee/role-management/components/add-member.test.tsx delete mode 100644 apps/web/modules/ee/role-management/components/edit-membership-role.test.tsx delete mode 100644 apps/web/modules/ee/sso/components/azure-button.test.tsx delete mode 100644 apps/web/modules/ee/sso/components/github-button.test.tsx delete mode 100644 apps/web/modules/ee/sso/components/google-button.test.tsx delete mode 100644 apps/web/modules/ee/sso/components/open-id-button.test.tsx delete mode 100644 apps/web/modules/ee/sso/components/saml-button.test.tsx delete mode 100644 apps/web/modules/ee/sso/components/sso-options.test.tsx delete mode 100644 apps/web/modules/ee/teams/project-teams/components/access-table.test.tsx delete mode 100644 apps/web/modules/ee/teams/project-teams/components/access-view.test.tsx delete mode 100644 apps/web/modules/ee/teams/project-teams/components/manage-team.test.tsx delete mode 100644 apps/web/modules/ee/teams/project-teams/loading.test.tsx delete mode 100644 apps/web/modules/ee/teams/project-teams/page.test.tsx delete mode 100644 apps/web/modules/ee/teams/team-list/components/create-team-button.test.tsx delete mode 100644 apps/web/modules/ee/teams/team-list/components/create-team-modal.test.tsx delete mode 100644 apps/web/modules/ee/teams/team-list/components/manage-team-button.test.tsx delete mode 100644 apps/web/modules/ee/teams/team-list/components/team-settings/delete-team.test.tsx delete mode 100644 apps/web/modules/ee/teams/team-list/components/team-settings/team-settings-modal.test.tsx delete mode 100644 apps/web/modules/ee/teams/team-list/components/teams-table.test.tsx delete mode 100644 apps/web/modules/ee/two-factor-auth/components/confirm-password-form.test.tsx delete mode 100644 apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.test.tsx delete mode 100644 apps/web/modules/ee/two-factor-auth/components/display-backup-codes.test.tsx delete mode 100644 apps/web/modules/ee/two-factor-auth/components/enable-two-factor-modal.test.tsx delete mode 100644 apps/web/modules/ee/two-factor-auth/components/enter-code.test.tsx delete mode 100644 apps/web/modules/ee/two-factor-auth/components/scan-qr-code.test.tsx delete mode 100644 apps/web/modules/ee/two-factor-auth/components/two-factor-backup.test.tsx delete mode 100644 apps/web/modules/ee/two-factor-auth/components/two-factor.test.tsx delete mode 100644 apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.test.tsx delete mode 100644 apps/web/modules/email/components/email-template.test.tsx delete mode 100644 apps/web/modules/email/emails/lib/tests/utils.test.tsx delete mode 100644 apps/web/modules/integrations/webhooks/components/add-webhook-modal.test.tsx delete mode 100644 apps/web/modules/integrations/webhooks/components/webhook-detail-modal.test.tsx delete mode 100644 apps/web/modules/organization/components/CreateOrganizationModal/index.test.tsx delete mode 100644 apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.test.tsx delete mode 100644 apps/web/modules/organization/settings/api-keys/components/api-key-list.test.tsx delete mode 100644 apps/web/modules/organization/settings/api-keys/components/edit-api-keys.test.tsx delete mode 100644 apps/web/modules/organization/settings/api-keys/components/view-permission-modal.test.tsx delete mode 100644 apps/web/modules/organization/settings/api-keys/loading.test.tsx delete mode 100644 apps/web/modules/organization/settings/api-keys/page.test.tsx delete mode 100644 apps/web/modules/organization/settings/teams/components/edit-memberships/edit-memberships.test.tsx delete mode 100644 apps/web/modules/organization/settings/teams/components/edit-memberships/members-info.test.tsx delete mode 100644 apps/web/modules/organization/settings/teams/components/edit-memberships/organization-actions.test.tsx delete mode 100644 apps/web/modules/organization/settings/teams/components/invite-member/bulk-invite-tab.test.tsx delete mode 100644 apps/web/modules/organization/settings/teams/components/invite-member/individual-invite-tab.test.tsx delete mode 100644 apps/web/modules/organization/settings/teams/components/invite-member/invite-member-modal.test.tsx delete mode 100644 apps/web/modules/organization/settings/teams/components/invite-member/share-invite-modal.test.tsx delete mode 100644 apps/web/modules/organization/settings/teams/components/members-view.test.tsx delete mode 100644 apps/web/modules/organization/settings/teams/page.test.tsx delete mode 100644 apps/web/modules/projects/components/create-project-modal/index.test.tsx delete mode 100644 apps/web/modules/projects/components/project-limit-modal/index.test.tsx delete mode 100644 apps/web/modules/projects/settings/(setup)/app-connection/loading.test.tsx delete mode 100644 apps/web/modules/projects/settings/(setup)/app-connection/page.test.tsx delete mode 100644 apps/web/modules/projects/settings/(setup)/app-connection/utils.test.tsx delete mode 100644 apps/web/modules/projects/settings/(setup)/components/ActionActivityTab.test.tsx delete mode 100644 apps/web/modules/projects/settings/(setup)/components/ActionClassesTable.test.tsx delete mode 100644 apps/web/modules/projects/settings/(setup)/components/ActionDetailModal.test.tsx delete mode 100644 apps/web/modules/projects/settings/(setup)/components/ActionRowData.test.tsx delete mode 100644 apps/web/modules/projects/settings/(setup)/components/ActionSettingsTab.test.tsx delete mode 100644 apps/web/modules/projects/settings/(setup)/components/ActionTableHeading.test.tsx delete mode 100644 apps/web/modules/projects/settings/(setup)/components/AddActionModal.test.tsx delete mode 100644 apps/web/modules/projects/settings/components/project-config-navigation.test.tsx delete mode 100644 apps/web/modules/projects/settings/general/components/delete-project-render.test.tsx delete mode 100644 apps/web/modules/projects/settings/general/components/delete-project.test.tsx delete mode 100644 apps/web/modules/projects/settings/general/components/edit-project-name-form.test.tsx delete mode 100644 apps/web/modules/projects/settings/general/components/edit-waiting-time-form.test.tsx delete mode 100644 apps/web/modules/projects/settings/general/loading.test.tsx delete mode 100644 apps/web/modules/projects/settings/general/page.test.tsx delete mode 100644 apps/web/modules/projects/settings/layout.test.tsx delete mode 100644 apps/web/modules/projects/settings/look/components/edit-logo.test.tsx delete mode 100644 apps/web/modules/projects/settings/look/components/edit-placement-form.test.tsx delete mode 100644 apps/web/modules/projects/settings/look/components/theme-styling.test.tsx delete mode 100644 apps/web/modules/projects/settings/look/loading.test.tsx delete mode 100644 apps/web/modules/projects/settings/look/page.test.tsx delete mode 100644 apps/web/modules/projects/settings/page.test.tsx delete mode 100644 apps/web/modules/projects/settings/tags/components/edit-tags-wrapper.test.tsx delete mode 100644 apps/web/modules/projects/settings/tags/components/merge-tags-combobox.test.tsx delete mode 100644 apps/web/modules/projects/settings/tags/components/single-tag.test.tsx delete mode 100644 apps/web/modules/projects/settings/tags/loading.test.tsx delete mode 100644 apps/web/modules/projects/settings/tags/page.test.tsx delete mode 100644 apps/web/modules/setup/(fresh-instance)/signup/page.test.tsx delete mode 100644 apps/web/modules/setup/organization/[organizationId]/invite/components/invite-members.test.tsx delete mode 100644 apps/web/modules/setup/organization/[organizationId]/invite/page.test.tsx delete mode 100644 apps/web/modules/setup/organization/create/components/create-organization.test.tsx delete mode 100644 apps/web/modules/setup/organization/create/components/removed-from-organization.test.tsx delete mode 100644 apps/web/modules/setup/organization/create/page.test.tsx delete mode 100644 apps/web/modules/survey/components/edit-public-survey-alert-dialog/index.test.tsx delete mode 100644 apps/web/modules/survey/components/question-form-input/components/fallback-input.test.tsx delete mode 100644 apps/web/modules/survey/components/question-form-input/components/multi-lang-wrapper.test.tsx delete mode 100644 apps/web/modules/survey/components/question-form-input/components/recall-item-select.test.tsx delete mode 100644 apps/web/modules/survey/components/question-form-input/components/recall-wrapper.test.tsx delete mode 100644 apps/web/modules/survey/components/question-form-input/index.test.tsx delete mode 100644 apps/web/modules/survey/components/template-list/components/start-from-scratch-template.test.tsx delete mode 100644 apps/web/modules/survey/components/template-list/components/template-filters.test.tsx delete mode 100644 apps/web/modules/survey/components/template-list/components/template-tags.test.tsx delete mode 100644 apps/web/modules/survey/components/template-list/components/template.test.tsx delete mode 100644 apps/web/modules/survey/components/template-list/index.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/add-action-modal.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/add-ending-card-button.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/add-question-button.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/address-question-form.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/advanced-settings.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/animated-survey-bg.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/cal-question-form.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/color-survey-bg.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/conditional-logic.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/consent-question-form.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/contact-info-question-form.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/create-new-action-tab.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/cta-question-form.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/date-question-form.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/edit-ending-card.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/edit-welcome-card.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/editor-card-menu.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/end-screen-form.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/file-upload-question-form.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/form-styling-settings.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/hidden-fields-card.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/how-to-send-card.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/image-survey-bg.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/loading-skeleton.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/logic-editor-actions.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/logic-editor-conditions.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/logic-editor.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/matrix-question-form.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/multiple-choice-question-form.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/nps-question-form.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/open-question-form.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/option-ids.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/picture-selection-form.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/placement.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/question-card.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/question-option-choice.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/questions-droppable.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/questions-view.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/ranking-question-form.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/rating-question-form.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/rating-type-dropdown.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/recontact-options-card.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/redirect-url-form.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/response-options-card.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/saved-actions-tab.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/settings-view.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/styling-view.test.tsx delete mode 100755 apps/web/modules/survey/editor/components/survey-editor-tabs.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/survey-editor.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/survey-menu-bar.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/survey-placement-card.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/survey-variables-card-item.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/survey-variables-card.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/targeting-locked-card.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/unsplash-images.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/update-question-id.test.tsx delete mode 100644 apps/web/modules/survey/editor/components/when-to-send-card.test.tsx delete mode 100644 apps/web/modules/survey/editor/lib/utils.test.tsx delete mode 100644 apps/web/modules/survey/follow-ups/components/follow-up-action-multi-email-input.test.tsx delete mode 100644 apps/web/modules/survey/follow-ups/components/follow-up-email.test.tsx delete mode 100644 apps/web/modules/survey/follow-ups/components/follow-up-item.test.tsx delete mode 100644 apps/web/modules/survey/follow-ups/components/follow-up-modal.test.tsx delete mode 100644 apps/web/modules/survey/hooks/useSingleUseId.test.tsx delete mode 100644 apps/web/modules/survey/link/components/legal-footer.test.tsx delete mode 100644 apps/web/modules/survey/link/components/link-survey-wrapper.test.tsx delete mode 100644 apps/web/modules/survey/link/components/link-survey.test.tsx delete mode 100644 apps/web/modules/survey/link/components/pin-screen.test.tsx delete mode 100644 apps/web/modules/survey/link/components/survey-inactive.test.tsx delete mode 100644 apps/web/modules/survey/link/components/survey-link-used.test.tsx delete mode 100644 apps/web/modules/survey/link/components/survey-loading-animation.test.tsx delete mode 100644 apps/web/modules/survey/link/components/survey-renderer.test.tsx delete mode 100644 apps/web/modules/survey/link/components/verify-email.test.tsx delete mode 100644 apps/web/modules/survey/link/contact-survey/page.test.tsx delete mode 100644 apps/web/modules/survey/link/layout.test.tsx delete mode 100644 apps/web/modules/survey/link/loading.test.tsx delete mode 100644 apps/web/modules/survey/link/not-found.test.tsx delete mode 100644 apps/web/modules/survey/link/page.test.tsx delete mode 100644 apps/web/modules/survey/list/components/copy-survey-form.test.tsx delete mode 100644 apps/web/modules/survey/list/components/copy-survey-modal.test.tsx delete mode 100644 apps/web/modules/survey/list/components/sort-option.test.tsx delete mode 100644 apps/web/modules/survey/list/components/survey-card.test.tsx delete mode 100644 apps/web/modules/survey/list/components/survey-copy-options.test.tsx delete mode 100644 apps/web/modules/survey/list/components/survey-dropdown-menu.test.tsx delete mode 100644 apps/web/modules/survey/list/components/survey-filter-dropdown.test.tsx delete mode 100644 apps/web/modules/survey/list/components/survey-filters.test.tsx delete mode 100644 apps/web/modules/survey/list/components/survey-list.test.tsx delete mode 100644 apps/web/modules/survey/list/components/survey-loading.test.tsx delete mode 100644 apps/web/modules/survey/list/page.test.tsx delete mode 100644 apps/web/modules/survey/templates/components/template-container.test.tsx delete mode 100644 apps/web/modules/ui/components/action-class-info/index.test.tsx delete mode 100644 apps/web/modules/ui/components/action-name-description-fields/index.test.tsx delete mode 100644 apps/web/modules/ui/components/additional-integration-settings/index.test.tsx delete mode 100644 apps/web/modules/ui/components/advanced-option-toggle/index.test.tsx delete mode 100644 apps/web/modules/ui/components/alert-dialog/index.test.tsx delete mode 100644 apps/web/modules/ui/components/alert/index.test.tsx delete mode 100644 apps/web/modules/ui/components/array-response/index.test.tsx delete mode 100644 apps/web/modules/ui/components/avatars/index.test.tsx delete mode 100644 apps/web/modules/ui/components/background-styling-card/index.test.tsx delete mode 100644 apps/web/modules/ui/components/background-styling-card/survey-bg-selector-tab.test.tsx delete mode 100644 apps/web/modules/ui/components/badge/index.test.tsx delete mode 100644 apps/web/modules/ui/components/breadcrumb/index.test.tsx delete mode 100644 apps/web/modules/ui/components/button/index.test.tsx delete mode 100644 apps/web/modules/ui/components/calendar/index.test.tsx delete mode 100644 apps/web/modules/ui/components/card-arrangement-tabs/index.test.tsx delete mode 100644 apps/web/modules/ui/components/card-styling-settings/index.test.tsx delete mode 100644 apps/web/modules/ui/components/card/index.test.tsx delete mode 100644 apps/web/modules/ui/components/checkbox/index.test.tsx delete mode 100644 apps/web/modules/ui/components/client-logo/index.test.tsx delete mode 100644 apps/web/modules/ui/components/client-logout/index.test.tsx delete mode 100644 apps/web/modules/ui/components/code-action-form/index.test.tsx delete mode 100644 apps/web/modules/ui/components/code-block/index.test.tsx delete mode 100644 apps/web/modules/ui/components/color-picker/components/popover-picker.test.tsx delete mode 100644 apps/web/modules/ui/components/color-picker/index.test.tsx delete mode 100644 apps/web/modules/ui/components/command/index.test.tsx delete mode 100644 apps/web/modules/ui/components/conditions-editor/index.test.tsx delete mode 100644 apps/web/modules/ui/components/confetti/index.test.tsx delete mode 100644 apps/web/modules/ui/components/confirm-delete-segment-modal/index.test.tsx delete mode 100644 apps/web/modules/ui/components/confirmation-modal/index.test.tsx delete mode 100644 apps/web/modules/ui/components/connect-integration/index.test.tsx delete mode 100644 apps/web/modules/ui/components/data-table/components/column-settings-dropdown.test.tsx delete mode 100644 apps/web/modules/ui/components/data-table/components/data-table-header.test.tsx delete mode 100644 apps/web/modules/ui/components/data-table/components/data-table-settings-modal-item.test.tsx delete mode 100644 apps/web/modules/ui/components/data-table/components/data-table-settings-modal.test.tsx delete mode 100644 apps/web/modules/ui/components/data-table/components/data-table-toolbar.test.tsx delete mode 100644 apps/web/modules/ui/components/data-table/components/selected-row-settings.test.tsx delete mode 100644 apps/web/modules/ui/components/data-table/components/selection-column.test.tsx delete mode 100644 apps/web/modules/ui/components/date-picker/index.test.tsx delete mode 100644 apps/web/modules/ui/components/decrement-quotas-checkbox/index.test.tsx delete mode 100644 apps/web/modules/ui/components/default-tag/index.test.tsx delete mode 100644 apps/web/modules/ui/components/delete-dialog/index.test.tsx delete mode 100644 apps/web/modules/ui/components/dialog/index.test.tsx delete mode 100644 apps/web/modules/ui/components/dropdown-menu/index.test.tsx delete mode 100644 apps/web/modules/ui/components/editor/components/add-variables-dropdown.test.tsx delete mode 100644 apps/web/modules/ui/components/editor/components/auto-link-plugin.test.tsx delete mode 100644 apps/web/modules/ui/components/editor/components/editor-content-checker.test.tsx delete mode 100644 apps/web/modules/ui/components/editor/components/editor.test.tsx delete mode 100644 apps/web/modules/ui/components/editor/components/link-editor.test.tsx delete mode 100644 apps/web/modules/ui/components/editor/components/recall-node.test.tsx delete mode 100644 apps/web/modules/ui/components/editor/components/recall-plugin.test.tsx delete mode 100644 apps/web/modules/ui/components/editor/components/toolbar-plugin.test.tsx delete mode 100644 apps/web/modules/ui/components/empty-space-filler/index.test.tsx delete mode 100644 apps/web/modules/ui/components/environment-notice/index.test.tsx delete mode 100644 apps/web/modules/ui/components/environmentId-base-layout/index.test.tsx delete mode 100644 apps/web/modules/ui/components/error-component/index.test.tsx delete mode 100644 apps/web/modules/ui/components/file-input/components/uploader.test.tsx delete mode 100644 apps/web/modules/ui/components/file-input/components/video-settings.test.tsx delete mode 100644 apps/web/modules/ui/components/file-input/index.test.tsx delete mode 100644 apps/web/modules/ui/components/file-upload-response/index.test.tsx delete mode 100644 apps/web/modules/ui/components/form/index.test.tsx delete mode 100644 apps/web/modules/ui/components/formbricks-logo/index.test.tsx delete mode 100644 apps/web/modules/ui/components/go-back-button/index.test.tsx delete mode 100644 apps/web/modules/ui/components/header/index.test.tsx delete mode 100644 apps/web/modules/ui/components/highlighted-text/index.test.tsx delete mode 100644 apps/web/modules/ui/components/iconbar/index.test.tsx delete mode 100644 apps/web/modules/ui/components/id-badge/index.test.tsx delete mode 100644 apps/web/modules/ui/components/input-combo-box/index.test.tsx delete mode 100644 apps/web/modules/ui/components/input/index.test.tsx delete mode 100644 apps/web/modules/ui/components/integration-card/index.test.tsx delete mode 100644 apps/web/modules/ui/components/label/index.test.tsx delete mode 100644 apps/web/modules/ui/components/limits-reached-banner/index.test.tsx delete mode 100644 apps/web/modules/ui/components/load-segment-modal/index.test.tsx delete mode 100644 apps/web/modules/ui/components/loading-spinner/index.test.tsx delete mode 100644 apps/web/modules/ui/components/logo/index.test.tsx delete mode 100644 apps/web/modules/ui/components/media-background/index.test.tsx delete mode 100644 apps/web/modules/ui/components/modal-with-tabs/index.test.tsx delete mode 100644 apps/web/modules/ui/components/multi-select/badge.test.tsx delete mode 100644 apps/web/modules/ui/components/multi-select/index.test.tsx delete mode 100644 apps/web/modules/ui/components/no-code-action-form/components/css-selector.test.tsx delete mode 100644 apps/web/modules/ui/components/no-code-action-form/components/inner-html-selector.test.tsx delete mode 100644 apps/web/modules/ui/components/no-code-action-form/components/page-url-selector.test.tsx delete mode 100644 apps/web/modules/ui/components/no-code-action-form/index.test.tsx delete mode 100644 apps/web/modules/ui/components/no-mobile-overlay/index.test.tsx delete mode 100644 apps/web/modules/ui/components/option-card/index.test.tsx delete mode 100644 apps/web/modules/ui/components/options-switch/index.test.tsx delete mode 100644 apps/web/modules/ui/components/otp-input/index.test.tsx delete mode 100644 apps/web/modules/ui/components/page-content-wrapper/index.test.tsx delete mode 100644 apps/web/modules/ui/components/page-header/index.test.tsx delete mode 100644 apps/web/modules/ui/components/password-input/index.test.tsx delete mode 100644 apps/web/modules/ui/components/pending-downgrade-banner/index.test.tsx delete mode 100644 apps/web/modules/ui/components/picture-selection-response/index.test.tsx delete mode 100644 apps/web/modules/ui/components/popover/index.test.tsx delete mode 100644 apps/web/modules/ui/components/post-hog-client/indext.test.tsx delete mode 100644 apps/web/modules/ui/components/preview-survey/components/tab-option.test.tsx delete mode 100644 apps/web/modules/ui/components/preview-survey/index.test.tsx delete mode 100644 apps/web/modules/ui/components/pro-badge/index.test.tsx delete mode 100644 apps/web/modules/ui/components/progress-bar/index.test.tsx delete mode 100644 apps/web/modules/ui/components/question-toggle-table/index.test.tsx delete mode 100644 apps/web/modules/ui/components/radio-group/index.test.tsx delete mode 100644 apps/web/modules/ui/components/ranking-response/index.test.tsx delete mode 100644 apps/web/modules/ui/components/rating-response/index.test.tsx delete mode 100644 apps/web/modules/ui/components/reset-progress-button/index.test.tsx delete mode 100644 apps/web/modules/ui/components/response-badges/index.test.tsx delete mode 100644 apps/web/modules/ui/components/save-as-new-segment-modal/index.test.tsx delete mode 100644 apps/web/modules/ui/components/search-bar/index.test.tsx delete mode 100644 apps/web/modules/ui/components/secondary-navigation/index.test.tsx delete mode 100644 apps/web/modules/ui/components/segment-title/index.test.tsx delete mode 100644 apps/web/modules/ui/components/select/index.test.tsx delete mode 100644 apps/web/modules/ui/components/separator/index.test.tsx delete mode 100644 apps/web/modules/ui/components/sheet/index.test.tsx delete mode 100644 apps/web/modules/ui/components/shuffle-option-select/index.test.tsx delete mode 100644 apps/web/modules/ui/components/sidebar/index.test.tsx delete mode 100644 apps/web/modules/ui/components/skeleton-loader/index.test.tsx delete mode 100644 apps/web/modules/ui/components/skeleton/index.test.tsx delete mode 100644 apps/web/modules/ui/components/slider/index.test.tsx delete mode 100644 apps/web/modules/ui/components/stacked-cards-container/index.test.tsx delete mode 100644 apps/web/modules/ui/components/storage-not-configured-toast/index.test.tsx delete mode 100644 apps/web/modules/ui/components/storage-not-configured-toast/lib/utils.test.tsx delete mode 100644 apps/web/modules/ui/components/styling-tabs/index.test.tsx delete mode 100644 apps/web/modules/ui/components/survey-status-indicator/index.test.tsx delete mode 100644 apps/web/modules/ui/components/survey/index.test.tsx delete mode 100644 apps/web/modules/ui/components/switch/index.test.tsx delete mode 100644 apps/web/modules/ui/components/tab-bar/index.test.tsx delete mode 100644 apps/web/modules/ui/components/tab-nav/index.test.tsx delete mode 100644 apps/web/modules/ui/components/tab-toggle/index.test.tsx delete mode 100644 apps/web/modules/ui/components/table/index.test.tsx delete mode 100644 apps/web/modules/ui/components/tabs/index.test.tsx delete mode 100644 apps/web/modules/ui/components/tag/index.test.tsx delete mode 100644 apps/web/modules/ui/components/tags-combobox/index.test.tsx delete mode 100644 apps/web/modules/ui/components/targeting-indicator/index.test.tsx delete mode 100644 apps/web/modules/ui/components/theme-styling-preview-survey/index.test.tsx delete mode 100644 apps/web/modules/ui/components/toaster-client/index.test.tsx delete mode 100644 apps/web/modules/ui/components/tooltip/index.test.tsx delete mode 100644 apps/web/modules/ui/components/typography/index.test.tsx delete mode 100644 apps/web/modules/ui/components/upgrade-prompt/index.test.tsx delete mode 100644 apps/web/modules/ui/hooks/use-mobile.test.tsx delete mode 100644 apps/web/modules/ui/lib/utils.test.tsx delete mode 100644 packages/surveys/src/components/buttons/back-button.test.tsx delete mode 100644 packages/surveys/src/components/buttons/submit-button.test.tsx delete mode 100644 packages/surveys/src/components/general/auto-close-progress-bar.test.tsx delete mode 100644 packages/surveys/src/components/general/cal-embed.test.tsx delete mode 100644 packages/surveys/src/components/general/ending-card.test.tsx delete mode 100644 packages/surveys/src/components/general/error-component.test.tsx delete mode 100644 packages/surveys/src/components/general/file-input.test.tsx delete mode 100644 packages/surveys/src/components/general/formbricks-branding.test.tsx delete mode 100644 packages/surveys/src/components/general/headline.test.tsx delete mode 100644 packages/surveys/src/components/general/input.test.tsx delete mode 100644 packages/surveys/src/components/general/label.test.tsx delete mode 100644 packages/surveys/src/components/general/language-switch.test.tsx delete mode 100644 packages/surveys/src/components/general/loading-spinner.test.tsx delete mode 100644 packages/surveys/src/components/general/progress-bar.test.tsx delete mode 100644 packages/surveys/src/components/general/progress.test.tsx delete mode 100644 packages/surveys/src/components/general/question-conditional.test.tsx delete mode 100644 packages/surveys/src/components/general/question-media.test.tsx delete mode 100644 packages/surveys/src/components/general/recaptcha-branding.test.tsx delete mode 100644 packages/surveys/src/components/general/render-survey.test.tsx delete mode 100644 packages/surveys/src/components/general/response-error-component.test.tsx delete mode 100644 packages/surveys/src/components/general/smileys.test.tsx delete mode 100644 packages/surveys/src/components/general/subheader.test.tsx delete mode 100644 packages/surveys/src/components/general/survey-close-button.test.tsx delete mode 100644 packages/surveys/src/components/general/survey.test.tsx delete mode 100644 packages/surveys/src/components/general/welcome-card.test.tsx delete mode 100644 packages/surveys/src/components/icons/close-icon.test.tsx delete mode 100644 packages/surveys/src/components/icons/expand-icon.test.tsx delete mode 100644 packages/surveys/src/components/icons/image-down-icon.test.tsx delete mode 100644 packages/surveys/src/components/icons/language-icon.test.tsx delete mode 100644 packages/surveys/src/components/questions/address-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/cal-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/consent-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/contact-info-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/cta-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/date-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/file-upload-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/matrix-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/multiple-choice-multi-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/multiple-choice-single-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/nps-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/open-text-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/picture-selection-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/ranking-question.test.tsx delete mode 100644 packages/surveys/src/components/questions/rating-question.test.tsx delete mode 100644 packages/surveys/src/components/wrappers/auto-close-wrapper.test.tsx delete mode 100644 packages/surveys/src/components/wrappers/scrollable-container.test.tsx delete mode 100644 packages/surveys/src/components/wrappers/stacked-card.test.tsx delete mode 100644 packages/surveys/src/components/wrappers/stacked-cards-container.test.tsx delete mode 100644 packages/surveys/src/components/wrappers/survey-container.test.tsx diff --git a/.cursor/rules/testing-patterns.mdc b/.cursor/rules/testing-patterns.mdc deleted file mode 100644 index b79a9b01da..0000000000 --- a/.cursor/rules/testing-patterns.mdc +++ /dev/null @@ -1,322 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Testing Patterns & Best Practices - -## Running Tests - -### Test Commands -From the **root directory** (formbricks/): -- `npm test` - Run all tests across all packages (recommended for CI/full testing) -- `npm run test:coverage` - Run all tests with coverage reports -- `npm run test:e2e` - Run end-to-end tests with Playwright - -From the **apps/web directory** (apps/web/): -- `npm run test` - Run only web app tests (fastest for development) -- `npm run test:coverage` - Run web app tests with coverage -- `npm run test -- ` - Run specific test files - -### Examples -```bash -# Run all tests from root (takes ~3 minutes, runs 790 test files with 5334+ tests) -npm test - -# Run specific test file from apps/web (fastest for development) -npm run test -- modules/cache/lib/service.test.ts - -# Run tests matching pattern from apps/web -npm run test -- modules/ee/license-check/lib/license.test.ts - -# Run with coverage from root -npm run test:coverage - -# Run specific test with watch mode from apps/web (for development) -npm run test -- --watch modules/cache/lib/service.test.ts - -# Run tests for a specific directory from apps/web -npm run test -- modules/cache/ -``` - -### Performance Tips -- **For development**: Use `apps/web` directory commands to run only web app tests -- **For CI/validation**: Use root directory commands to run all packages -- **For specific features**: Use file patterns to target specific test files -- **For debugging**: Use `--watch` mode for continuous testing during development - -### Test File Organization -- Place test files in the **same directory** as the source file -- Use `.test.ts` for utility/service tests (Node environment) -- Use `.test.tsx` for React component tests (jsdom environment) - -## Test File Naming & Environment - -### File Extensions -- Use `.test.tsx` for React component/hook tests (runs in jsdom environment) -- Use `.test.ts` for utility/service tests (runs in Node environment) -- The vitest config uses `environmentMatchGlobs` to automatically set jsdom for `.tsx` files - -### Test Structure -```typescript -// Import the mocked functions first -import { useHook } from "@/path/to/hook"; -import { serviceFunction } from "@/path/to/service"; -import { renderHook, waitFor } from "@testing-library/react"; -import { beforeEach, describe, expect, test, vi } from "vitest"; - -// Mock dependencies -vi.mock("@/path/to/hook", () => ({ - useHook: vi.fn(), -})); - -describe("ComponentName", () => { - beforeEach(() => { - vi.clearAllMocks(); - // Setup default mocks - }); - - test("descriptive test name", async () => { - // Test implementation - }); -}); -``` - -## React Hook Testing - -### Context Mocking -When testing hooks that use React Context: -```typescript -vi.mocked(useResponseFilter).mockReturnValue({ - selectedFilter: { - filter: [], - responseStatus: "all", - }, - setSelectedFilter: vi.fn(), - selectedOptions: { - questionOptions: [], - questionFilterOptions: [], - }, - setSelectedOptions: vi.fn(), - dateRange: { from: new Date(), to: new Date() }, - setDateRange: vi.fn(), - resetState: vi.fn(), -}); -``` - -### Testing Async Hooks -- Always use `waitFor` for async operations -- Test both loading and completed states -- Verify API calls with correct parameters - -```typescript -test("fetches data on mount", async () => { - const { result } = renderHook(() => useHook()); - - expect(result.current.isLoading).toBe(true); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(result.current.data).toBe(expectedData); - expect(vi.mocked(apiCall)).toHaveBeenCalledWith(expectedParams); -}); -``` - -### Testing Hook Dependencies -To test useEffect dependencies, ensure mocks return different values: -```typescript -// First render -mockGetFormattedFilters.mockReturnValue(mockFilters); - -// Change dependency and trigger re-render -const newMockFilters = { ...mockFilters, finished: true }; -mockGetFormattedFilters.mockReturnValue(newMockFilters); -rerender(); -``` - -## Performance Testing - -### Race Condition Testing -Test AbortController implementation: -```typescript -test("cancels previous request when new request is made", async () => { - let resolveFirst: (value: any) => void; - let resolveSecond: (value: any) => void; - - const firstPromise = new Promise((resolve) => { - resolveFirst = resolve; - }); - const secondPromise = new Promise((resolve) => { - resolveSecond = resolve; - }); - - vi.mocked(apiCall) - .mockReturnValueOnce(firstPromise as any) - .mockReturnValueOnce(secondPromise as any); - - const { result } = renderHook(() => useHook()); - - // Trigger second request - result.current.refetch(); - - // Resolve in order - first should be cancelled - resolveFirst!({ data: 100 }); - resolveSecond!({ data: 200 }); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - // Should have result from second request - expect(result.current.data).toBe(200); -}); -``` - -### Cleanup Testing -```typescript -test("cleans up on unmount", () => { - const abortSpy = vi.spyOn(AbortController.prototype, "abort"); - - const { unmount } = renderHook(() => useHook()); - unmount(); - - expect(abortSpy).toHaveBeenCalled(); - abortSpy.mockRestore(); -}); -``` - -## Error Handling Testing - -### API Error Testing -```typescript -test("handles API errors gracefully", async () => { - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - vi.mocked(apiCall).mockRejectedValue(new Error("API Error")); - - const { result } = renderHook(() => useHook()); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(consoleSpy).toHaveBeenCalledWith("Error message:", expect.any(Error)); - expect(result.current.data).toBe(fallbackValue); - - consoleSpy.mockRestore(); -}); -``` - -### Cancelled Request Testing -```typescript -test("does not update state for cancelled requests", async () => { - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - let rejectFirst: (error: any) => void; - const firstPromise = new Promise((_, reject) => { - rejectFirst = reject; - }); - - vi.mocked(apiCall) - .mockReturnValueOnce(firstPromise as any) - .mockResolvedValueOnce({ data: 42 }); - - const { result } = renderHook(() => useHook()); - result.current.refetch(); - - const abortError = new Error("Request cancelled"); - rejectFirst!(abortError); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - // Should not log error for cancelled request - expect(consoleSpy).not.toHaveBeenCalled(); - consoleSpy.mockRestore(); -}); -``` - -## Type Safety in Tests - -### Mock Type Assertions -Use type assertions for edge cases: -```typescript -vi.mocked(apiCall).mockResolvedValue({ - data: null as any, // For testing null handling -}); - -vi.mocked(apiCall).mockResolvedValue({ - data: undefined as any, // For testing undefined handling -}); -``` - -### Proper Mock Typing -Ensure mocks match the actual interface: -```typescript -const mockSurvey: TSurvey = { - id: "survey-123", - name: "Test Survey", - // ... other required properties -} as unknown as TSurvey; // Use when partial mocking is needed -``` - -## Common Test Patterns - -### Testing State Changes -```typescript -test("updates state correctly", async () => { - const { result } = renderHook(() => useHook()); - - // Initial state - expect(result.current.value).toBe(initialValue); - - // Trigger change - result.current.updateValue(newValue); - - // Verify change - expect(result.current.value).toBe(newValue); -}); -``` - -### Testing Multiple Scenarios -```typescript -test("handles different modes", async () => { - // Test regular mode - vi.mocked(useParams).mockReturnValue({ surveyId: "123" }); - const { rerender } = renderHook(() => useHook()); - - await waitFor(() => { - expect(vi.mocked(regularApi)).toHaveBeenCalled(); - }); - - rerender(); - - await waitFor(() => { - expect(vi.mocked(sharingApi)).toHaveBeenCalled(); - }); -}); -``` - -## Test Organization - -### Comprehensive Test Coverage -For hooks, ensure you test: -- ✅ Initialization (with/without initial values) -- ✅ Data fetching (success/error cases) -- ✅ State updates and refetching -- ✅ Dependency changes triggering effects -- ✅ Manual actions (refetch, reset) -- ✅ Race condition prevention -- ✅ Cleanup on unmount -- ✅ Mode switching (if applicable) -- ✅ Edge cases (null/undefined data) - -### Test Naming -Use descriptive test names that explain the scenario: -- ✅ "initializes with initial count" -- ✅ "fetches response count on mount for regular survey" -- ✅ "cancels previous request when new request is made" -- ❌ "test hook" -- ❌ "it works" diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc deleted file mode 100644 index 92ac2c1c29..0000000000 --- a/.cursor/rules/testing.mdc +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: Whenever the user asks to write or update a test file for .tsx or .ts files. -globs: -alwaysApply: false ---- -Use the rules in this file when writing tests [copilot-instructions.md](mdc:.github/copilot-instructions.md). -After writing the tests, run them and check if there's any issue with the tests and if all of them are passing. Fix the issues and rerun the tests until all pass. \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index c2edec8552..0000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,32 +0,0 @@ -# Testing Instructions - -When generating test files inside the "/app/web" path, follow these rules: - -- You are an experienced senior software engineer -- Use vitest -- Ensure 100% code coverage -- Add as few comments as possible -- The test file should be located in the same folder as the original file -- Use the `test` function instead of `it` -- Follow the same test pattern used for other files in the package where the file is located -- All imports should be at the top of the file, not inside individual tests -- For mocking inside "test" blocks use "vi.mocked" -- If the file is located in the "packages/survey" path, use "@testing-library/preact" instead of "@testing-library/react" -- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file -- When using "screen.getByText" check for the tolgee string if it is being used in the file. -- The types for mocked variables can be found in the "packages/types" path. Be sure that every imported type exists before using it. Don't create types that are not already in the codebase. -- When mocking data check if the properties added are part of the type of the object being mocked. Only specify known properties, don't use properties that are not part of the type. - -If it's a test for a ".tsx" file, follow these extra instructions: - -- Add this code inside the "describe" block and before any test: - -afterEach(() => { -cleanup(); -}); - -- The "afterEach" function should only have the "cleanup()" line inside it and should be adde to the "vitest" imports. -- For click events, import userEvent from "@testing-library/user-event" -- Mock other components that can make the text more complex and but at the same time mocking it wouldn't make the test flaky. It's ok to leave basic and simple components. -- You don't need to mock @tolgee/react -- Use "import "@testing-library/jest-dom/vitest";" diff --git a/AGENTS.md b/AGENTS.md index 1c42d197b0..c79c9a5adc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Project Structure & Module Organization -Formbricks runs as a pnpm/turbo monorepo. `apps/web` is the Next.js product surface, with feature modules under `app/` and `modules/`, assets in `public/` and `images/`, and Playwright specs in `apps/web/playwright/`. `apps/storybook` renders reusable UI pieces for review. Shared logic lives in `packages/*`: `database` (Prisma schemas/migrations), `surveys`, `js-core`, `types`, plus linting and TypeScript presets (`config-*`). Deployment collateral is kept in `docs/`, `docker/`, and `helm-chart/`. Tests generally sit next to their source as `*.test.ts(x)` or inside `__tests__`. +Formbricks runs as a pnpm/turbo monorepo. `apps/web` is the Next.js product surface, with feature modules under `app/` and `modules/`, assets in `public/` and `images/`, and Playwright specs in `apps/web/playwright/`. `apps/storybook` renders reusable UI pieces for review. Shared logic lives in `packages/*`: `database` (Prisma schemas/migrations), `surveys`, `js-core`, `types`, plus linting and TypeScript presets (`config-*`). Deployment collateral is kept in `docs/`, `docker/`, and `helm-chart/`. Unit tests sit next to their source as `*.test.ts` or inside `__tests__`. ## Build, Test & Development Commands @@ -21,7 +21,7 @@ TypeScript, React, and Prisma are the primary languages. Use the shared ESLint p ## Testing Guidelines -Prefer Vitest with Testing Library for logic in `.ts` files, keeping specs colocated with the code they exercise (`utility.test.ts`). Do not write tests for `.tsx` files. Mock network and storage boundaries through helpers from `@formbricks/*`. Run `pnpm test` before opening a PR and `pnpm test:coverage` when touching critical flows; keep coverage from regressing. End-to-end scenarios belong in `apps/web/playwright`, using descriptive filenames (`billing.spec.ts`) and tagging slow suites with `@slow` when necessary. +Prefer Vitest with Testing Library for logic in `.ts` files, keeping specs colocated with the code they exercise (`utility.test.ts`). Do not write tests for `.tsx` files—React components are covered by Playwright E2E tests instead. Mock network and storage boundaries through helpers from `@formbricks/*`. Run `pnpm test` before opening a PR and `pnpm test:coverage` when touching critical flows; keep coverage from regressing. End-to-end scenarios belong in `apps/web/playwright`, using descriptive filenames (`billing.spec.ts`) and tagging slow suites with `@slow` when necessary. ## Commit & Pull Request Guidelines diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.test.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.test.tsx deleted file mode 100644 index ea8770dbdb..0000000000 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ConnectWithFormbricks } from "./ConnectWithFormbricks"; - -// Mocks before import -const pushMock = vi.fn(); -const refreshMock = vi.fn(); -vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t: (key: string) => key }) })); -vi.mock("next/navigation", () => ({ useRouter: vi.fn(() => ({ push: pushMock, refresh: refreshMock })) })); -vi.mock("./OnboardingSetupInstructions", () => ({ - OnboardingSetupInstructions: () =>
, -})); - -afterEach(() => { - cleanup(); - vi.clearAllMocks(); -}); - -describe("ConnectWithFormbricks", () => { - const environment = { id: "env1" } as any; - const webAppUrl = "http://app"; - const channel = {} as any; - - test("renders waiting state when appSetupCompleted is false", () => { - render( - - ); - expect(screen.getByTestId("instructions")).toBeInTheDocument(); - expect(screen.getByText("environments.connect.waiting_for_your_signal")).toBeInTheDocument(); - }); - - test("renders success state when appSetupCompleted is true", () => { - render( - - ); - expect(screen.getByText("environments.connect.congrats")).toBeInTheDocument(); - expect(screen.getByText("environments.connect.connection_successful_message")).toBeInTheDocument(); - }); - - test("clicking finish button navigates to surveys", async () => { - render( - - ); - const button = screen.getByRole("button", { name: "environments.connect.finish_onboarding" }); - await userEvent.click(button); - expect(pushMock).toHaveBeenCalledWith(`/environments/${environment.id}/surveys`); - }); - - test("refresh is called on visibilitychange to visible", () => { - render( - - ); - Object.defineProperty(document, "visibilityState", { value: "visible", configurable: true }); - document.dispatchEvent(new Event("visibilitychange")); - expect(refreshMock).toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.test.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.test.tsx deleted file mode 100644 index 67b2b1460d..0000000000 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeAll, describe, expect, test, vi } from "vitest"; -import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions"; - -// Mock react-hot-toast so we can assert that a success message is shown -vi.mock("react-hot-toast", () => ({ - __esModule: true, - default: { - success: vi.fn(), - }, -})); - -// Set up a spy for navigator.clipboard.writeText so it becomes a ViTest spy. -beforeAll(() => { - Object.defineProperty(navigator, "clipboard", { - configurable: true, - writable: true, - value: { - // Using a mockResolvedValue resolves the promise as writeText is async. - writeText: vi.fn().mockResolvedValue(undefined), - }, - }); -}); - -describe("OnboardingSetupInstructions", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - // Provide some default props for testing - const defaultProps = { - environmentId: "env-123", - publicDomain: "https://example.com", - channel: "app" as const, // Assuming channel is either "app" or "website" - appSetupCompleted: false, - }; - - test("renders HTML tab content by default", () => { - render(); - - // Since the default active tab is "html", we check for a unique text - expect( - screen.getByText(/environments.connect.insert_this_code_into_the_head_tag_of_your_website/i) - ).toBeInTheDocument(); - - // The HTML snippet contains a marker comment - expect(screen.getByText("START")).toBeInTheDocument(); - - // Verify the "Copy Code" button is present - expect(screen.getByRole("button", { name: /common.copy_code/i })).toBeInTheDocument(); - }); - - test("renders NPM tab content when selected", async () => { - render(); - const user = userEvent.setup(); - - // Click on the "NPM" tab to switch views. - const npmTab = screen.getByText("NPM"); - await user.click(npmTab); - - // Check that the install commands are present - expect(screen.getByText(/npm install @formbricks\/js/)).toBeInTheDocument(); - expect(screen.getByText(/yarn add @formbricks\/js/)).toBeInTheDocument(); - - // Verify the "Read Docs" link has the correct URL (based on channel prop) - const readDocsLink = screen.getByRole("link", { name: /common.read_docs/i }); - expect(readDocsLink).toHaveAttribute("href", "https://formbricks.com/docs/app-surveys/framework-guides"); - }); - - test("copies HTML snippet to clipboard and shows success toast when Copy Code button is clicked", async () => { - render(); - const user = userEvent.setup(); - - const writeTextSpy = vi.spyOn(navigator.clipboard, "writeText"); - - // Click the "Copy Code" button - const copyButton = screen.getByRole("button", { name: /common.copy_code/i }); - await user.click(copyButton); - - // Ensure navigator.clipboard.writeText was called. - expect(writeTextSpy).toHaveBeenCalled(); - const writtenText = (navigator.clipboard.writeText as any).mock.calls[0][0] as string; - - // Check that the pasted snippet contains the expected environment values - expect(writtenText).toContain('var appUrl = "https://example.com"'); - expect(writtenText).toContain('var environmentId = "env-123"'); - - // Verify that a success toast was shown - expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard"); - }); - - test("renders step-by-step manual link with correct URL in HTML tab", () => { - render(); - const manualLink = screen.getByRole("link", { name: /common.step_by_step_manual/i }); - expect(manualLink).toHaveAttribute( - "href", - "https://formbricks.com/docs/app-surveys/framework-guides#html" - ); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/layout.test.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/layout.test.tsx deleted file mode 100644 index 155021d98b..0000000000 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/layout.test.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { getServerSession } from "next-auth"; -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; -import OnboardingLayout from "./layout"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_PRODUCTION: false, - IS_DEVELOPMENT: true, - E2E_TESTING: false, - WEBAPP_URL: "http://localhost:3000", - PUBLIC_URL: "http://localhost:3000/survey", - ENCRYPTION_KEY: "mock-encryption-key", - CRON_SECRET: "mock-cron-secret", - DEFAULT_BRAND_COLOR: "#64748b", - FB_LOGO_URL: "https://mock-logo-url.com/logo.png", - PRIVACY_URL: "http://localhost:3000/privacy", - TERMS_URL: "http://localhost:3000/terms", - IMPRINT_URL: "http://localhost:3000/imprint", - IMPRINT_ADDRESS: "Mock Address", - PASSWORD_RESET_DISABLED: false, - EMAIL_VERIFICATION_DISABLED: false, - GOOGLE_OAUTH_ENABLED: false, - GITHUB_OAUTH_ENABLED: false, - AZURE_OAUTH_ENABLED: false, - OIDC_OAUTH_ENABLED: false, - SAML_OAUTH_ENABLED: false, - SAML_XML_DIR: "./mock-saml-connection", - SIGNUP_ENABLED: true, - EMAIL_AUTH_ENABLED: true, - INVITE_DISABLED: false, - SLACK_CLIENT_SECRET: "mock-slack-secret", - SLACK_CLIENT_ID: "mock-slack-id", - SLACK_AUTH_URL: "https://mock-slack-auth-url.com", - GOOGLE_SHEETS_CLIENT_ID: "mock-google-sheets-id", - GOOGLE_SHEETS_CLIENT_SECRET: "mock-google-sheets-secret", - GOOGLE_SHEETS_REDIRECT_URL: "http://localhost:3000/google-sheets-redirect", - NOTION_OAUTH_CLIENT_ID: "mock-notion-id", - NOTION_OAUTH_CLIENT_SECRET: "mock-notion-secret", - NOTION_REDIRECT_URI: "http://localhost:3000/notion-redirect", - NOTION_AUTH_URL: "https://mock-notion-auth-url.com", - AIRTABLE_CLIENT_ID: "mock-airtable-id", - SMTP_HOST: "mock-smtp-host", - SMTP_PORT: "587", - SMTP_SECURE_ENABLED: false, - SMTP_USER: "mock-smtp-user", - SMTP_PASSWORD: "mock-smtp-password", - SMTP_AUTHENTICATED: true, - SMTP_REJECT_UNAUTHORIZED_TLS: true, - MAIL_FROM: "mock@mail.com", - MAIL_FROM_NAME: "Mock Mail", - NEXTAUTH_SECRET: "mock-nextauth-secret", - ITEMS_PER_PAGE: 30, - SURVEYS_PER_PAGE: 12, - RESPONSES_PER_PAGE: 25, - TEXT_RESPONSES_PER_PAGE: 5, - INSIGHTS_PER_PAGE: 10, - DOCUMENTS_PER_PAGE: 10, - MAX_RESPONSES_FOR_INSIGHT_GENERATION: 500, - MAX_OTHER_OPTION_LENGTH: 250, - ENTERPRISE_LICENSE_KEY: "ABC", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "mock-github-secret", - GITHUB_OAUTH_URL: "https://mock-github-auth-url.com", - AZURE_ID: "mock-azure-id", - AZUREAD_CLIENT_ID: "mock-azure-client-id", - AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", - GOOGLE_CLIENT_ID: "mock-google-client-id", - GOOGLE_CLIENT_SECRET: "mock-google-client-secret", - GOOGLE_OAUTH_URL: "https://mock-google-auth-url.com", - AZURE_OAUTH_URL: "https://mock-azure-auth-url.com", - OIDC_ID: "mock-oidc-id", - OIDC_OAUTH_URL: "https://mock-oidc-auth-url.com", - SAML_ID: "mock-saml-id", - SAML_OAUTH_URL: "https://mock-saml-auth-url.com", - SAML_METADATA_URL: "https://mock-saml-metadata-url.com", - AZUREAD_TENANT_ID: "mock-azure-tenant-id", - AZUREAD_OAUTH_URL: "https://mock-azuread-auth-url.com", - OIDC_DISPLAY_NAME: "Mock OIDC", - OIDC_CLIENT_ID: "mock-oidc-client-id", - OIDC_CLIENT_SECRET: "mock-oidc-client-secret", - OIDC_REDIRECT_URL: "http://localhost:3000/oidc-redirect", - OIDC_AUTH_URL: "https://mock-oidc-auth-url.com", - OIDC_ISSUER: "https://mock-oidc-issuer.com", - OIDC_SIGNING_ALGORITHM: "RS256", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -vi.mock("@/lib/environment/auth", () => ({ - hasUserEnvironmentAccess: vi.fn(), -})); - -describe("OnboardingLayout", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("redirects to login if session is missing", async () => { - vi.mocked(getServerSession).mockResolvedValueOnce(null); - - await OnboardingLayout({ - params: { environmentId: "env1" }, - children:
Test Content
, - }); - - expect(redirect).toHaveBeenCalledWith("/auth/login"); - }); - - test("throws AuthorizationError if user lacks access", async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user1" } }); - vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false); - - await expect( - OnboardingLayout({ - params: { environmentId: "env1" }, - children:
Test Content
, - }) - ).rejects.toThrow("User is not authorized to access this environment"); - }); - - test("renders children if user has access", async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user1" } }); - vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true); - - const result = await OnboardingLayout({ - params: { environmentId: "env1" }, - children:
Test Content
, - }); - - render(result); - - expect(screen.getByTestId("child")).toHaveTextContent("Test Content"); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.test.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.test.tsx deleted file mode 100644 index 9fa830e912..0000000000 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { createSurveyAction } from "@/modules/survey/components/template-list/actions"; -import { XMTemplateList } from "./XMTemplateList"; - -// Prepare push mock and module mocks before importing component -const pushMock = vi.fn(); -vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t: (key: string) => key }) })); -vi.mock("next/navigation", () => ({ useRouter: vi.fn(() => ({ push: pushMock })) })); -vi.mock("react-hot-toast", () => ({ default: { error: vi.fn() } })); -vi.mock("@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates", () => ({ - getXMTemplates: (t: any) => [ - { id: 1, name: "tmpl1" }, - { id: 2, name: "tmpl2" }, - ], -})); -vi.mock("@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils", () => ({ - replacePresetPlaceholders: (template: any, project: any) => ({ ...template, projectId: project.id }), -})); -vi.mock("@/modules/survey/components/template-list/actions", () => ({ createSurveyAction: vi.fn() })); -vi.mock("@/lib/utils/helper", () => ({ getFormattedErrorMessage: () => "formatted-error" })); -vi.mock("@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer", () => ({ - OnboardingOptionsContainer: ({ options }: { options: any[] }) => ( -
- {options.map((opt, idx) => ( - - ))} -
- ), -})); - -// Reset mocks between tests -afterEach(() => { - cleanup(); - vi.clearAllMocks(); -}); - -describe("XMTemplateList component", () => { - const project = { id: "proj1" } as any; - const user = { id: "user1" } as any; - const environmentId = "env1"; - - test("creates survey and navigates on success", async () => { - // Mock successful survey creation - vi.mocked(createSurveyAction).mockResolvedValue({ data: { id: "survey1" } } as any); - - render(); - - const option0 = screen.getByTestId("option-0"); - await userEvent.click(option0); - - expect(createSurveyAction).toHaveBeenCalledWith({ - environmentId, - surveyBody: expect.objectContaining({ id: 1, projectId: "proj1", type: "link", createdBy: "user1" }), - }); - expect(pushMock).toHaveBeenCalledWith(`/environments/${environmentId}/surveys/survey1/edit?mode=cx`); - }); - - test("shows error toast on failure", async () => { - // Mock failed survey creation - vi.mocked(createSurveyAction).mockResolvedValue({ error: "err" } as any); - - render(); - - const option1 = screen.getByTestId("option-1"); - await userEvent.click(option1); - - expect(createSurveyAction).toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalledWith("formatted-error"); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.test.tsx deleted file mode 100644 index 8c5e97e2a8..0000000000 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { LandingSidebar } from "./landing-sidebar"; - -// Mock constants that this test needs -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - WEBAPP_URL: "http://localhost:3000", -})); - -// Mock server actions that this test needs -vi.mock("@/modules/auth/actions/sign-out", () => ({ - logSignOutAction: vi.fn().mockResolvedValue(undefined), -})); - -// Module mocks must be declared before importing the component -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ t: (key: string) => key, isLoading: false }), -})); - -// Mock our useSignOut hook -const mockSignOut = vi.fn(); -vi.mock("@/modules/auth/hooks/use-sign-out", () => ({ - useSignOut: () => ({ - signOut: mockSignOut, - }), -})); - -vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) })); -vi.mock("@/modules/organization/components/CreateOrganizationModal", () => ({ - CreateOrganizationModal: ({ open }: { open: boolean }) => ( -
- ), -})); -vi.mock("@/modules/ui/components/avatars", () => ({ - ProfileAvatar: ({ userId }: { userId: string }) =>
{userId}
, -})); - -// Ensure mocks are reset between tests -afterEach(() => { - cleanup(); - vi.clearAllMocks(); -}); - -describe("LandingSidebar component", () => { - const user = { id: "u1", name: "Alice", email: "alice@example.com" } as any; - const organization = { id: "o1", name: "orgOne" } as any; - - test("renders logo, avatar, and initial modal closed", () => { - render(); - - // Formbricks logo - expect(screen.getByAltText("environments.formbricks_logo")).toBeInTheDocument(); - // Profile avatar - expect(screen.getByTestId("avatar")).toHaveTextContent("u1"); - // CreateOrganizationModal should be closed initially - expect(screen.getByTestId("modal-closed")).toBeInTheDocument(); - }); - - test("clicking logout triggers signOut", async () => { - render(); - - // Open user dropdown by clicking on avatar trigger - const trigger = screen.getByTestId("avatar").parentElement; - if (trigger) await userEvent.click(trigger); - - // Click logout menu item - const logoutItem = await screen.findByText("common.logout"); - await userEvent.click(logoutItem); - - expect(mockSignOut).toHaveBeenCalledWith({ - reason: "user_initiated", - redirectUrl: "/auth/login", - organizationId: "o1", - redirect: true, - callbackUrl: "/auth/login", - clearEnvironmentId: true, - }); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/layout.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/layout.test.tsx deleted file mode 100644 index 511f89637e..0000000000 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/layout.test.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup } from "@testing-library/preact"; -import { getServerSession } from "next-auth"; -import { notFound, redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getEnvironments } from "@/lib/environment/service"; -import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; -import { getUserProjects } from "@/lib/project/service"; -import LandingLayout from "./layout"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_PRODUCTION: false, - IS_DEVELOPMENT: true, - E2E_TESTING: false, - WEBAPP_URL: "http://localhost:3000", - PUBLIC_URL: "http://localhost:3000/survey", - ENCRYPTION_KEY: "mock-encryption-key", - CRON_SECRET: "mock-cron-secret", - DEFAULT_BRAND_COLOR: "#64748b", - FB_LOGO_URL: "https://mock-logo-url.com/logo.png", - PRIVACY_URL: "http://localhost:3000/privacy", - TERMS_URL: "http://localhost:3000/terms", - IMPRINT_URL: "http://localhost:3000/imprint", - IMPRINT_ADDRESS: "Mock Address", - PASSWORD_RESET_DISABLED: false, - EMAIL_VERIFICATION_DISABLED: false, - GOOGLE_OAUTH_ENABLED: false, - GITHUB_OAUTH_ENABLED: false, - AZURE_OAUTH_ENABLED: false, - OIDC_OAUTH_ENABLED: false, - SAML_OAUTH_ENABLED: false, - SAML_XML_DIR: "./mock-saml-connection", - SIGNUP_ENABLED: true, - EMAIL_AUTH_ENABLED: true, - INVITE_DISABLED: false, - SLACK_CLIENT_SECRET: "mock-slack-secret", - SLACK_CLIENT_ID: "mock-slack-id", - SLACK_AUTH_URL: "https://mock-slack-auth-url.com", - GOOGLE_SHEETS_CLIENT_ID: "mock-google-sheets-id", - GOOGLE_SHEETS_CLIENT_SECRET: "mock-google-sheets-secret", - GOOGLE_SHEETS_REDIRECT_URL: "http://localhost:3000/google-sheets-redirect", - NOTION_OAUTH_CLIENT_ID: "mock-notion-id", - NOTION_OAUTH_CLIENT_SECRET: "mock-notion-secret", - NOTION_REDIRECT_URI: "http://localhost:3000/notion-redirect", - NOTION_AUTH_URL: "https://mock-notion-auth-url.com", - AIRTABLE_CLIENT_ID: "mock-airtable-id", - SMTP_HOST: "mock-smtp-host", - SMTP_PORT: "587", - SMTP_SECURE_ENABLED: false, - SMTP_USER: "mock-smtp-user", - SMTP_PASSWORD: "mock-smtp-password", - SMTP_AUTHENTICATED: true, - SMTP_REJECT_UNAUTHORIZED_TLS: true, - MAIL_FROM: "mock@mail.com", - MAIL_FROM_NAME: "Mock Mail", - NEXTAUTH_SECRET: "mock-nextauth-secret", - ITEMS_PER_PAGE: 30, - SURVEYS_PER_PAGE: 12, - RESPONSES_PER_PAGE: 25, - TEXT_RESPONSES_PER_PAGE: 5, - INSIGHTS_PER_PAGE: 10, - DOCUMENTS_PER_PAGE: 10, - MAX_RESPONSES_FOR_INSIGHT_GENERATION: 500, - MAX_OTHER_OPTION_LENGTH: 250, - ENTERPRISE_LICENSE_KEY: "ABC", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "mock-github-secret", - GITHUB_OAUTH_URL: "https://mock-github-auth-url.com", - AZURE_ID: "mock-azure-id", - AZUREAD_CLIENT_ID: "mock-azure-client-id", - AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", - GOOGLE_CLIENT_ID: "mock-google-client-id", - GOOGLE_CLIENT_SECRET: "mock-google-client-secret", - GOOGLE_OAUTH_URL: "https://mock-google-auth-url.com", - AZURE_OAUTH_URL: "https://mock-azure-auth-url.com", - OIDC_ID: "mock-oidc-id", - OIDC_OAUTH_URL: "https://mock-oidc-auth-url.com", - SAML_ID: "mock-saml-id", - SAML_OAUTH_URL: "https://mock-saml-auth-url.com", - SAML_METADATA_URL: "https://mock-saml-metadata-url.com", - AZUREAD_TENANT_ID: "mock-azure-tenant-id", - AZUREAD_OAUTH_URL: "https://mock-azuread-auth-url.com", - OIDC_DISPLAY_NAME: "Mock OIDC", - OIDC_CLIENT_ID: "mock-oidc-client-id", - OIDC_CLIENT_SECRET: "mock-oidc-client-secret", - OIDC_REDIRECT_URL: "http://localhost:3000/oidc-redirect", - OIDC_AUTH_URL: "https://mock-oidc-auth-url.com", - OIDC_ISSUER: "https://mock-oidc-issuer.com", - OIDC_SIGNING_ALGORITHM: "RS256", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -vi.mock("@/lib/environment/service"); -vi.mock("@/lib/membership/service"); -vi.mock("@/lib/project/service"); -vi.mock("next-auth"); -vi.mock("next/navigation"); - -afterEach(() => { - cleanup(); -}); - -describe("LandingLayout", () => { - test("redirects to login if no session exists", async () => { - vi.mocked(getServerSession).mockResolvedValue(null); - - const props = { params: { organizationId: "org-123" }, children:
Child Content
}; - - await LandingLayout(props); - - expect(vi.mocked(redirect)).toHaveBeenCalledWith("/auth/login"); - }); - - test("returns notFound if no membership is found", async () => { - vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-123" } }); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null); - - const props = { params: { organizationId: "org-123" }, children:
Child Content
}; - - await LandingLayout(props); - - expect(vi.mocked(notFound)).toHaveBeenCalled(); - }); - - test("redirects to production environment if available", async () => { - vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-123" } }); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ - organizationId: "org-123", - userId: "user-123", - accepted: true, - role: "owner", - }); - vi.mocked(getUserProjects).mockResolvedValue([ - { - id: "proj-123", - organizationId: "org-123", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-02"), - name: "Project 1", - styling: { allowStyleOverwrite: true }, - recontactDays: 30, - inAppSurveyBranding: true, - linkSurveyBranding: true, - } as any, - ]); - vi.mocked(getEnvironments).mockResolvedValue([ - { - id: "env-123", - type: "production", - projectId: "proj-123", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-02"), - appSetupCompleted: true, - }, - ]); - - const props = { params: { organizationId: "org-123" }, children:
Child Content
}; - - await LandingLayout(props); - - expect(vi.mocked(redirect)).toHaveBeenCalledWith("/environments/env-123/"); - }); - - test("renders children if no projects or production environment exist", async () => { - vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-123" } }); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ - organizationId: "org-123", - userId: "user-123", - accepted: true, - role: "owner", - }); - vi.mocked(getUserProjects).mockResolvedValue([]); - - const props = { params: { organizationId: "org-123" }, children:
Child Content
}; - - const result = await LandingLayout(props); - - expect(result).toEqual( - <> -
Child Content
- - ); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.test.tsx deleted file mode 100644 index 28a797e172..0000000000 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.test.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { notFound, redirect } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; -import { getOrganizationsByUserId } from "@/lib/organization/service"; -import { getUser } from "@/lib/user/service"; -import { getOrganizationAuth } from "@/modules/organization/lib/utils"; -import { getTranslate } from "@/tolgee/server"; - -vi.mock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: true, - features: { isMultiOrgEnabled: true }, - lastChecked: new Date(), - isPendingDowngrade: false, - fallbackLevel: "live", - }), - getLicenseFeatures: vi.fn().mockResolvedValue({ isMultiOrgEnabled: true }), -})); - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_PRODUCTION: false, - IS_DEVELOPMENT: true, - E2E_TESTING: false, - WEBAPP_URL: "http://localhost:3000", - ENCRYPTION_KEY: "mock-encryption-key", - CRON_SECRET: "mock-cron-secret", - DEFAULT_BRAND_COLOR: "#64748b", - FB_LOGO_URL: "https://mock-logo-url.com/logo.png", - PRIVACY_URL: "http://localhost:3000/privacy", - TERMS_URL: "http://localhost:3000/terms", - IMPRINT_URL: "http://localhost:3000/imprint", - IMPRINT_ADDRESS: "Mock Address", - PASSWORD_RESET_DISABLED: false, - EMAIL_VERIFICATION_DISABLED: false, - GOOGLE_OAUTH_ENABLED: false, - GITHUB_OAUTH_ENABLED: false, - AZURE_OAUTH_ENABLED: false, - OIDC_OAUTH_ENABLED: false, - SAML_OAUTH_ENABLED: false, - SAML_XML_DIR: "./mock-saml-connection", - SIGNUP_ENABLED: true, - EMAIL_AUTH_ENABLED: true, - INVITE_DISABLED: false, - SLACK_CLIENT_SECRET: "mock-slack-secret", - SLACK_CLIENT_ID: "mock-slack-id", - SLACK_AUTH_URL: "https://mock-slack-auth-url.com", - GOOGLE_SHEETS_CLIENT_ID: "mock-google-sheets-id", - GOOGLE_SHEETS_CLIENT_SECRET: "mock-google-sheets-secret", - GOOGLE_SHEETS_REDIRECT_URL: "http://localhost:3000/google-sheets-redirect", - NOTION_OAUTH_CLIENT_ID: "mock-notion-id", - NOTION_OAUTH_CLIENT_SECRET: "mock-notion-secret", - NOTION_REDIRECT_URI: "http://localhost:3000/notion-redirect", - NOTION_AUTH_URL: "https://mock-notion-auth-url.com", - AIRTABLE_CLIENT_ID: "mock-airtable-id", - SMTP_HOST: "mock-smtp-host", - SMTP_PORT: "587", - SMTP_SECURE_ENABLED: false, - SMTP_USER: "mock-smtp-user", - SMTP_PASSWORD: "mock-smtp-password", - SMTP_AUTHENTICATED: true, - SMTP_REJECT_UNAUTHORIZED_TLS: true, - MAIL_FROM: "mock@mail.com", - MAIL_FROM_NAME: "Mock Mail", - NEXTAUTH_SECRET: "mock-nextauth-secret", - ITEMS_PER_PAGE: 30, - SURVEYS_PER_PAGE: 12, - RESPONSES_PER_PAGE: 25, - TEXT_RESPONSES_PER_PAGE: 5, - INSIGHTS_PER_PAGE: 10, - DOCUMENTS_PER_PAGE: 10, - MAX_RESPONSES_FOR_INSIGHT_GENERATION: 500, - MAX_OTHER_OPTION_LENGTH: 250, - ENTERPRISE_LICENSE_KEY: "ABC", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "mock-github-secret", - GITHUB_OAUTH_URL: "https://mock-github-auth-url.com", - AZURE_ID: "mock-azure-id", - AZUREAD_CLIENT_ID: "mock-azure-client-id", - AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", - GOOGLE_CLIENT_ID: "mock-google-client-id", - GOOGLE_CLIENT_SECRET: "mock-google-client-secret", - GOOGLE_OAUTH_URL: "https://mock-google-auth-url.com", - AZURE_OAUTH_URL: "https://mock-azure-auth-url.com", - OIDC_ID: "mock-oidc-id", - OIDC_OAUTH_URL: "https://mock-oidc-auth-url.com", - SAML_ID: "mock-saml-id", - SAML_OAUTH_URL: "https://mock-saml-auth-url.com", - SAML_METADATA_URL: "https://mock-saml-metadata-url.com", - AZUREAD_TENANT_ID: "mock-azure-tenant-id", - AZUREAD_OAUTH_URL: "https://mock-azuread-auth-url.com", - OIDC_DISPLAY_NAME: "Mock OIDC", - OIDC_CLIENT_ID: "mock-oidc-client-id", - OIDC_CLIENT_SECRET: "mock-oidc-client-secret", - OIDC_REDIRECT_URL: "http://localhost:3000/oidc-redirect", - OIDC_AUTH_URL: "https://mock-oidc-auth-url.com", - OIDC_ISSUER: "https://mock-oidc-issuer.com", - OIDC_SIGNING_ALGORITHM: "RS256", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -vi.mock("@/lib/getPublicUrl", () => ({ - getPublicDomain: vi.fn().mockReturnValue("http://localhost:3000"), -})); - -vi.mock("@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar", () => ({ - LandingSidebar: () =>
, -})); -vi.mock("@/app/(app)/environments/[environmentId]/components/project-and-org-switch", () => ({ - ProjectAndOrgSwitch: () =>
, -})); -vi.mock("@/modules/organization/lib/utils"); -vi.mock("@/lib/user/service"); -vi.mock("@/lib/organization/service"); -vi.mock("@/lib/membership/service"); -vi.mock("@/tolgee/server"); -vi.mock("next/navigation", () => ({ - redirect: vi.fn(() => "REDIRECT_STUB"), - notFound: vi.fn(() => "NOT_FOUND_STUB"), - usePathname: vi.fn(() => "/organizations/org1"), - useRouter: vi.fn(() => ({ - push: vi.fn(), - replace: vi.fn(), - back: vi.fn(), - forward: vi.fn(), - refresh: vi.fn(), - })), -})); - -// Mock the React cache function -vi.mock("react", async () => { - const actual = await vi.importActual("react"); - return { - ...actual, - cache: (fn: any) => fn, - }; -}); - -describe("Page component", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - vi.resetModules(); - }); - - test("redirects to login if no user session", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValue({ session: {}, organization: {} } as any); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: true, - features: { isMultiOrgEnabled: true }, - lastChecked: new Date(), - isPendingDowngrade: false, - fallbackLevel: "live", - }), - getLicenseFeatures: vi.fn().mockResolvedValue({ isMultiOrgEnabled: true }), - })); - const { default: Page } = await import("./page"); - const result = await Page({ params: { organizationId: "org1" } }); - expect(redirect).toHaveBeenCalledWith("/auth/login"); - expect(result).toBe("REDIRECT_STUB"); - }); - - test("returns notFound if user does not exist", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValue({ - session: { user: { id: "user1" } }, - organization: {}, - } as any); - vi.mocked(getUser).mockResolvedValue(null); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: true, - features: { isMultiOrgEnabled: true }, - lastChecked: new Date(), - isPendingDowngrade: false, - fallbackLevel: "live", - }), - getLicenseFeatures: vi.fn().mockResolvedValue({ isMultiOrgEnabled: true }), - })); - const { default: Page } = await import("./page"); - const result = await Page({ params: { organizationId: "org1" } }); - expect(notFound).toHaveBeenCalled(); - expect(result).toBe("NOT_FOUND_STUB"); - }); - - test("renders header and sidebar for authenticated user", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValue({ - session: { user: { id: "user1" } }, - organization: { id: "org1", billing: { plan: "free" } }, - } as any); - vi.mocked(getUser).mockResolvedValue({ id: "user1", name: "Test User" } as any); - vi.mocked(getOrganizationsByUserId).mockResolvedValue([{ id: "org1", name: "Org One" } as any]); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ - organizationId: "org1", - userId: "user1", - accepted: true, - role: "member", - } as any); - vi.mocked(getTranslate).mockResolvedValue((props: any) => - typeof props === "string" ? props : props.key || "" - ); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: true, - features: { isMultiOrgEnabled: true }, - lastChecked: new Date(), - isPendingDowngrade: false, - fallbackLevel: "live", - }), - getLicenseFeatures: vi.fn().mockResolvedValue({ isMultiOrgEnabled: true }), - })); - const { default: Page } = await import("./page"); - const element = await Page({ params: { organizationId: "org1" } }); - render(element as React.ReactElement); - expect(screen.getByTestId("landing-sidebar")).toBeInTheDocument(); - expect(screen.getByTestId("project-and-org-switch")).toBeInTheDocument(); - expect(screen.getByText("organizations.landing.no_projects_warning_title")).toBeInTheDocument(); - expect(screen.getByText("organizations.landing.no_projects_warning_subtitle")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/layout.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/layout.test.tsx deleted file mode 100644 index d2b2ce7ce4..0000000000 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/layout.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { act, cleanup, render, screen } from "@testing-library/react"; -import { getServerSession } from "next-auth"; -import { redirect } from "next/navigation"; -import React from "react"; -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TUser } from "@formbricks/types/user"; -import { canUserAccessOrganization } from "@/lib/organization/auth"; -import { getOrganization } from "@/lib/organization/service"; -import { getUser } from "@/lib/user/service"; -import ProjectOnboardingLayout from "./layout"; - -// Mock all the modules and functions that this layout uses: - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); -vi.mock("@/lib/organization/auth", () => ({ - canUserAccessOrganization: vi.fn(), -})); -vi.mock("@/lib/organization/service", () => ({ - getOrganization: vi.fn(), -})); -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(() => { - // Return a mock translator that just returns the key - return (key: string) => key; - }), -})); - -// mock the child components -vi.mock("@/app/(app)/environments/[environmentId]/components/PosthogIdentify", () => ({ - PosthogIdentify: () =>
, -})); -vi.mock("@/modules/ui/components/toaster-client", () => ({ - ToasterClient: () =>
, -})); - -describe("ProjectOnboardingLayout", () => { - beforeEach(() => { - cleanup(); - }); - - test("redirects to /auth/login if there is no session", async () => { - // Mock no session - vi.mocked(getServerSession).mockResolvedValueOnce(null); - - const layoutElement = await ProjectOnboardingLayout({ - params: { organizationId: "org-123" }, - children:
Hello!
, - }); - - expect(redirect).toHaveBeenCalledWith("/auth/login"); - // Layout returns nothing after redirect - expect(layoutElement).toBeUndefined(); - }); - - test("throws an error if user does not exist", async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ - user: { id: "user-123" }, - }); - vi.mocked(getUser).mockResolvedValueOnce(null); // no user in DB - - await expect( - ProjectOnboardingLayout({ - params: { organizationId: "org-123" }, - children:
Hello!
, - }) - ).rejects.toThrow("common.user_not_found"); - }); - - test("throws AuthorizationError if user cannot access organization", async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } }); - vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser); - vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false); - - await expect( - ProjectOnboardingLayout({ - params: { organizationId: "org-123" }, - children:
Child
, - }) - ).rejects.toThrow("common.not_authorized"); - }); - - test("throws an error if organization does not exist", async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } }); - vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser); - vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true); - vi.mocked(getOrganization).mockResolvedValueOnce(null); - - await expect( - ProjectOnboardingLayout({ - params: { organizationId: "org-123" }, - children:
Hello!
, - }) - ).rejects.toThrow("common.organization_not_found"); - }); - - test("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => { - // Provide valid data - vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } }); - vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser); - vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true); - vi.mocked(getOrganization).mockResolvedValueOnce({ - id: "org-123", - name: "Test Org", - billing: { - plan: "enterprise", - }, - } as TOrganization); - - let layoutElement: React.ReactNode; - // Because it's an async server component, do it in an act - await act(async () => { - layoutElement = await ProjectOnboardingLayout({ - params: { organizationId: "org-123" }, - children:
Hello!
, - }); - render(layoutElement); - }); - - expect(screen.getByTestId("child-content")).toHaveTextContent("Hello!"); - expect(screen.getByTestId("posthog-identify")).toBeInTheDocument(); - expect(screen.getByTestId("toaster-client")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/channel/page.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/channel/page.test.tsx deleted file mode 100644 index e5d0d06bf7..0000000000 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/channel/page.test.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getUserProjects } from "@/lib/project/service"; -import { getOrganizationAuth } from "@/modules/organization/lib/utils"; -import { getTranslate } from "@/tolgee/server"; -import Page from "./page"; - -const mockTranslate = vi.fn((key) => key); - -// Module mocks must be declared before importing the component -vi.mock("@/lib/project/service", () => ({ getUserProjects: vi.fn() })); -vi.mock("@/modules/organization/lib/utils", () => ({ getOrganizationAuth: vi.fn() })); -vi.mock("@/tolgee/server", () => ({ getTranslate: vi.fn() })); -vi.mock("next/navigation", () => ({ redirect: vi.fn(() => "REDIRECT_STUB") })); -vi.mock("@/modules/ui/components/header", () => ({ - Header: ({ title, subtitle }: { title: string; subtitle: string }) => ( -
-

{title}

-

{subtitle}

-
- ), -})); -vi.mock("@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer", () => ({ - OnboardingOptionsContainer: ({ options }: { options: any[] }) => ( -
{options.map((o) => o.title).join(",")}
- ), -})); -vi.mock("next/link", () => ({ - default: ({ href, children }: { href: string; children: React.ReactNode }) => {children}, -})); - -describe("Page component", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const params = Promise.resolve({ organizationId: "org1" }); - - test("redirects to login if no user session", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValue({ session: {} } as any); - - const result = await Page({ params }); - - expect(redirect).toHaveBeenCalledWith("/auth/login"); - expect(result).toBe("REDIRECT_STUB"); - }); - - test("renders header, options, and close button when projects exist", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValue({ session: { user: { id: "user1" } } } as any); - vi.mocked(getTranslate).mockResolvedValue(mockTranslate); - vi.mocked(getUserProjects).mockResolvedValue([{ id: 1 }] as any); - - const element = await Page({ params }); - render(element as React.ReactElement); - - // Header title and subtitle - expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent( - "organizations.projects.new.channel.channel_select_title" - ); - expect( - screen.getByText("organizations.projects.new.channel.channel_select_subtitle") - ).toBeInTheDocument(); - - // Options container with correct titles - expect(screen.getByTestId("options")).toHaveTextContent( - "organizations.projects.new.channel.link_and_email_surveys," + - "organizations.projects.new.channel.in_product_surveys" - ); - - // Close button link rendered when projects >=1 - const closeLink = screen.getByRole("link"); - expect(closeLink).toHaveAttribute("href", "/"); - }); - - test("does not render close button when no projects", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValue({ session: { user: { id: "user1" } } } as any); - vi.mocked(getTranslate).mockResolvedValue(mockTranslate); - vi.mocked(getUserProjects).mockResolvedValue([]); - - const element = await Page({ params }); - render(element as React.ReactElement); - - expect(screen.queryByRole("link")).toBeNull(); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/layout.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/layout.test.tsx deleted file mode 100644 index edb314c62c..0000000000 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/layout.test.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup } from "@testing-library/react"; -import { getServerSession } from "next-auth"; -import { notFound, redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TMembership } from "@formbricks/types/memberships"; -import { TOrganization } from "@formbricks/types/organizations"; -import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; -import { getOrganization } from "@/lib/organization/service"; -import { getOrganizationProjectsCount } from "@/lib/project/service"; -import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils"; -import OnboardingLayout from "./layout"; - -// Mock environment variables -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -// Mock dependencies -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -vi.mock("@/lib/membership/service", () => ({ - getMembershipByUserIdOrganizationId: vi.fn(), -})); - -vi.mock("@/lib/organization/service", () => ({ - getOrganization: vi.fn(), -})); - -vi.mock("@/lib/project/service", () => ({ - getOrganizationProjectsCount: vi.fn(), -})); - -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getOrganizationProjectsLimit: vi.fn(), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -describe("OnboardingLayout", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("redirects to login if no session", async () => { - vi.mocked(getServerSession).mockResolvedValue(null); - - const props = { - params: { organizationId: "test-org-id" }, - children:
Test Child
, - }; - - await OnboardingLayout(props); - expect(redirect).toHaveBeenCalledWith("/auth/login"); - }); - - test("returns not found if user is member or billing", async () => { - const mockSession = { - user: { id: "test-user-id" }, - }; - vi.mocked(getServerSession).mockResolvedValue(mockSession as any); - - const mockMembership: TMembership = { - organizationId: "test-org-id", - userId: "test-user-id", - accepted: true, - role: "member", - }; - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - - const props = { - params: { organizationId: "test-org-id" }, - children:
Test Child
, - }; - - await OnboardingLayout(props); - expect(notFound).toHaveBeenCalled(); - }); - - test("throws error if organization is not found", async () => { - const mockSession = { - user: { id: "test-user-id" }, - }; - vi.mocked(getServerSession).mockResolvedValue(mockSession as any); - - const mockMembership: TMembership = { - organizationId: "test-org-id", - userId: "test-user-id", - accepted: true, - role: "owner", - }; - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - vi.mocked(getOrganization).mockResolvedValue(null); - - const props = { - params: { organizationId: "test-org-id" }, - children:
Test Child
, - }; - - await expect(OnboardingLayout(props)).rejects.toThrow("common.organization_not_found"); - }); - - test("redirects to home if project limit is reached", async () => { - const mockSession = { - user: { id: "test-user-id" }, - }; - vi.mocked(getServerSession).mockResolvedValue(mockSession as any); - - const mockMembership: TMembership = { - organizationId: "test-org-id", - userId: "test-user-id", - accepted: true, - role: "owner", - }; - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - - const mockOrganization: TOrganization = { - id: "test-org-id", - name: "Test Org", - createdAt: new Date(), - updatedAt: new Date(), - isAIEnabled: false, - billing: { - stripeCustomerId: null, - plan: "free", - period: "monthly", - limits: { - projects: 3, - monthly: { - responses: 1500, - miu: 2000, - }, - }, - periodStart: new Date(), - }, - }; - vi.mocked(getOrganization).mockResolvedValue(mockOrganization); - vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(3); - vi.mocked(getOrganizationProjectsCount).mockResolvedValue(3); - - const props = { - params: { organizationId: "test-org-id" }, - children:
Test Child
, - }; - - await OnboardingLayout(props); - expect(redirect).toHaveBeenCalledWith("/"); - }); - - test("renders children when all conditions are met", async () => { - const mockSession = { - user: { id: "test-user-id" }, - }; - vi.mocked(getServerSession).mockResolvedValue(mockSession as any); - - const mockMembership: TMembership = { - organizationId: "test-org-id", - userId: "test-user-id", - accepted: true, - role: "owner", - }; - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - - const mockOrganization: TOrganization = { - id: "test-org-id", - name: "Test Org", - createdAt: new Date(), - updatedAt: new Date(), - isAIEnabled: false, - billing: { - stripeCustomerId: null, - plan: "free", - period: "monthly", - limits: { - projects: 3, - monthly: { - responses: 1500, - miu: 2000, - }, - }, - periodStart: new Date(), - }, - }; - vi.mocked(getOrganization).mockResolvedValue(mockOrganization); - vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(3); - vi.mocked(getOrganizationProjectsCount).mockResolvedValue(2); - - const props = { - params: { organizationId: "test-org-id" }, - children:
Test Child
, - }; - - const result = await OnboardingLayout(props); - expect(result).toEqual(<>{props.children}); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/mode/page.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/mode/page.test.tsx deleted file mode 100644 index 5a1f60e908..0000000000 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/mode/page.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getUserProjects } from "@/lib/project/service"; -import { getOrganizationAuth } from "@/modules/organization/lib/utils"; -import { getTranslate } from "@/tolgee/server"; -import Page from "./page"; - -const mockTranslate = vi.fn((key) => key); - -vi.mock("@/modules/organization/lib/utils", () => ({ getOrganizationAuth: vi.fn() })); -vi.mock("@/lib/project/service", () => ({ getUserProjects: vi.fn() })); -vi.mock("@/tolgee/server", () => ({ getTranslate: vi.fn() })); -vi.mock("next/navigation", () => ({ redirect: vi.fn() })); -vi.mock("next/link", () => ({ - __esModule: true, - default: ({ href, children }: any) => {children}, -})); -vi.mock("@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer", () => ({ - OnboardingOptionsContainer: ({ options }: any) => ( -
{options.map((o: any) => o.title).join(",")}
- ), -})); -vi.mock("@/modules/ui/components/header", () => ({ Header: ({ title }: any) =>

{title}

})); -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, ...props }: any) => , -})); - -describe("Mode Page", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const params = Promise.resolve({ organizationId: "org1" }); - - test("redirects to login if no session user", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ session: {} } as any); - await Page({ params }); - expect(redirect).toHaveBeenCalledWith("/auth/login"); - }); - - test("renders header and options without close link when no projects", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ session: { user: { id: "u1" } } } as any); - vi.mocked(getTranslate).mockResolvedValue(mockTranslate); - vi.mocked(getUserProjects).mockResolvedValueOnce([] as any); - - const element = await Page({ params }); - render(element as React.ReactElement); - - expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent( - "organizations.projects.new.mode.what_are_you_here_for" - ); - expect(screen.getByTestId("options")).toHaveTextContent( - "organizations.projects.new.mode.formbricks_surveys," + "organizations.projects.new.mode.formbricks_cx" - ); - expect(screen.queryByRole("link")).toBeNull(); - }); - - test("renders close link when projects exist", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ session: { user: { id: "u1" } } } as any); - vi.mocked(getTranslate).mockResolvedValue(mockTranslate); - vi.mocked(getUserProjects).mockResolvedValueOnce([{ id: "p1" } as any]); - - const element = await Page({ params }); - render(element as React.ReactElement); - - const link = screen.getByRole("link"); - expect(link).toHaveAttribute("href", "/"); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings.test.tsx deleted file mode 100644 index d8c8ad9850..0000000000 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings.test.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { toast } from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions"; -import { ProjectSettings } from "./ProjectSettings"; - -// Mocks before imports -const pushMock = vi.fn(); -vi.mock("next/navigation", () => ({ useRouter: () => ({ push: pushMock }) })); -vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t: (key: string) => key }) })); -vi.mock("react-hot-toast", () => ({ toast: { error: vi.fn() } })); -vi.mock("@/app/(app)/environments/[environmentId]/actions", () => ({ createProjectAction: vi.fn() })); -vi.mock("@/lib/utils/helper", () => ({ getFormattedErrorMessage: () => "formatted-error" })); -vi.mock("@/modules/ui/components/color-picker", () => ({ - ColorPicker: ({ color, onChange }: any) => ( - - ), -})); -vi.mock("@/modules/ui/components/input", () => ({ - Input: ({ value, onChange, placeholder }: any) => ( - onChange((e.target as any).value)} /> - ), -})); -vi.mock("@/modules/ui/components/multi-select", () => ({ - MultiSelect: ({ value, options, onChange }: any) => ( - - ), -})); -vi.mock("@/modules/ui/components/survey", () => ({ - SurveyInline: () =>
, -})); -vi.mock("@/lib/templates", () => ({ previewSurvey: () => ({}) })); -vi.mock("@/modules/ee/teams/team-list/components/create-team-modal", () => ({ - CreateTeamModal: ({ open }: any) =>
, -})); - -// Clean up after each test -afterEach(() => { - cleanup(); - vi.clearAllMocks(); - localStorage.clear(); -}); - -describe("ProjectSettings component", () => { - const baseProps = { - organizationId: "org1", - projectMode: "cx", - industry: "ind", - defaultBrandColor: "#fff", - organizationTeams: [], - isAccessControlAllowed: false, - userProjectsCount: 0, - } as any; - - const fillAndSubmit = async () => { - const nameInput = screen.getByPlaceholderText("e.g. Formbricks"); - await userEvent.clear(nameInput); - await userEvent.type(nameInput, "TestProject"); - const nextButton = screen.getByRole("button", { name: "common.next" }); - await userEvent.click(nextButton); - }; - - test("successful createProject for link channel navigates to surveys and clears localStorage", async () => { - (createProjectAction as any).mockResolvedValue({ - data: { environments: [{ id: "env123", type: "production" }] }, - }); - render(); - await fillAndSubmit(); - expect(createProjectAction).toHaveBeenCalledWith({ - organizationId: "org1", - data: expect.objectContaining({ teamIds: [] }), - }); - expect(pushMock).toHaveBeenCalledWith("/environments/env123/surveys"); - expect(localStorage.getItem("FORMBRICKS_SURVEYS_FILTERS_KEY_LS")).toBeNull(); - }); - - test("successful createProject for app channel navigates to connect", async () => { - (createProjectAction as any).mockResolvedValue({ - data: { environments: [{ id: "env456", type: "production" }] }, - }); - render(); - await fillAndSubmit(); - expect(pushMock).toHaveBeenCalledWith("/environments/env456/connect"); - }); - - test("successful createProject for cx mode navigates to xm-templates when channel is neither link nor app", async () => { - (createProjectAction as any).mockResolvedValue({ - data: { environments: [{ id: "env789", type: "production" }] }, - }); - render(); - await fillAndSubmit(); - expect(pushMock).toHaveBeenCalledWith("/environments/env789/xm-templates"); - }); - - test("shows error toast on createProject error response", async () => { - (createProjectAction as any).mockResolvedValue({ error: "err" }); - render(); - await fillAndSubmit(); - expect(toast.error).toHaveBeenCalledWith("formatted-error"); - }); - - test("shows error toast on exception", async () => { - (createProjectAction as any).mockImplementation(() => { - throw new Error("fail"); - }); - render(); - await fillAndSubmit(); - expect(toast.error).toHaveBeenCalledWith("organizations.projects.new.settings.project_creation_failed"); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.test.tsx deleted file mode 100644 index e56b49209f..0000000000 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding"; -import { getUserProjects } from "@/lib/project/service"; -import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils"; -import { getOrganizationAuth } from "@/modules/organization/lib/utils"; -import Page from "./page"; - -vi.mock("@/lib/constants", () => ({ DEFAULT_BRAND_COLOR: "#fff" })); -// Mocks before component import -vi.mock("@/app/(app)/(onboarding)/lib/onboarding", () => ({ getTeamsByOrganizationId: vi.fn() })); -vi.mock("@/lib/project/service", () => ({ getUserProjects: vi.fn() })); -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ getAccessControlPermission: vi.fn() })); -vi.mock("@/modules/organization/lib/utils", () => ({ getOrganizationAuth: vi.fn() })); -vi.mock("@/tolgee/server", () => ({ getTranslate: () => Promise.resolve((key: string) => key) })); -vi.mock("next/navigation", () => ({ redirect: vi.fn() })); -vi.mock("next/link", () => ({ - __esModule: true, - default: ({ href, children }: any) => {children}, -})); -vi.mock("@/modules/ui/components/header", () => ({ - Header: ({ title, subtitle }: any) => ( -
-

{title}

-

{subtitle}

-
- ), -})); -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, ...props }: any) => , -})); -vi.mock( - "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings", - () => ({ - ProjectSettings: (props: any) =>
, - }) -); - -// Cleanup after each test -afterEach(() => { - cleanup(); - vi.clearAllMocks(); -}); - -describe("ProjectSettingsPage", () => { - const params = Promise.resolve({ organizationId: "org1" }); - const searchParams = Promise.resolve({ channel: "link", industry: "other", mode: "cx" } as any); - - test("redirects to login when no session user", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ session: {} } as any); - await Page({ params, searchParams }); - expect(redirect).toHaveBeenCalledWith("/auth/login"); - }); - - test("throws when teams not found", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ - session: { user: { id: "u1" } }, - organization: { billing: { plan: "basic" } }, - } as any); - vi.mocked(getUserProjects).mockResolvedValueOnce([] as any); - vi.mocked(getTeamsByOrganizationId).mockResolvedValueOnce(null as any); - vi.mocked(getAccessControlPermission).mockResolvedValueOnce(false as any); - - await expect(Page({ params, searchParams })).rejects.toThrow("common.organization_teams_not_found"); - }); - - test("renders header, settings and close link when projects exist", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ - session: { user: { id: "u1" } }, - organization: { billing: { plan: "basic" } }, - } as any); - vi.mocked(getUserProjects).mockResolvedValueOnce([{ id: "p1" }] as any); - vi.mocked(getTeamsByOrganizationId).mockResolvedValueOnce([{ id: "t1", name: "Team1" }] as any); - vi.mocked(getAccessControlPermission).mockResolvedValueOnce(true as any); - - const element = await Page({ params, searchParams }); - render(element as React.ReactElement); - - // Header - expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent( - "organizations.projects.new.settings.project_settings_title" - ); - // ProjectSettings stub receives mode prop - expect(screen.getByTestId("project-settings")).toHaveAttribute("data-mode", "cx"); - // Close link for existing projects - const link = screen.getByRole("link"); - expect(link).toHaveAttribute("href", "/"); - }); - - test("renders without close link when no projects", async () => { - vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ - session: { user: { id: "u1" } }, - organization: { billing: { plan: "basic" } }, - } as any); - vi.mocked(getUserProjects).mockResolvedValueOnce([] as any); - vi.mocked(getTeamsByOrganizationId).mockResolvedValueOnce([{ id: "t1", name: "Team1" }] as any); - vi.mocked(getAccessControlPermission).mockResolvedValueOnce(true as any); - - const element = await Page({ params, searchParams }); - render(element as React.ReactElement); - - expect(screen.queryByRole("link")).toBeNull(); - }); -}); diff --git a/apps/web/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer.test.tsx deleted file mode 100644 index 9bea2c55d1..0000000000 --- a/apps/web/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { Home, Settings } from "lucide-react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { OnboardingOptionsContainer } from "./OnboardingOptionsContainer"; - -describe("OnboardingOptionsContainer", () => { - afterEach(() => { - cleanup(); - }); - - test("renders options with links", () => { - const options = [ - { - title: "Test Option", - description: "Test Description", - icon: Home, - href: "/test", - }, - ]; - - render(); - expect(screen.getByText("Test Option")).toBeInTheDocument(); - expect(screen.getByText("Test Description")).toBeInTheDocument(); - }); - - test("renders options with onClick handler", () => { - const onClickMock = vi.fn(); - const options = [ - { - title: "Click Option", - description: "Click Description", - icon: Home, - onClick: onClickMock, - }, - ]; - - render(); - expect(screen.getByText("Click Option")).toBeInTheDocument(); - expect(screen.getByText("Click Description")).toBeInTheDocument(); - }); - - test("renders options with iconText", () => { - const options = [ - { - title: "Icon Text Option", - description: "Icon Text Description", - icon: Home, - iconText: "Custom Icon Text", - }, - ]; - - render(); - expect(screen.getByText("Custom Icon Text")).toBeInTheDocument(); - }); - - test("renders options with loading state", () => { - const options = [ - { - title: "Loading Option", - description: "Loading Description", - icon: Home, - isLoading: true, - }, - ]; - - render(); - expect(screen.getByText("Loading Option")).toBeInTheDocument(); - }); - - test("renders multiple options", () => { - const options = [ - { - title: "First Option", - description: "First Description", - icon: Home, - }, - { - title: "Second Option", - description: "Second Description", - icon: Settings, - }, - ]; - - render(); - expect(screen.getByText("First Option")).toBeInTheDocument(); - expect(screen.getByText("Second Option")).toBeInTheDocument(); - }); - - test("calls onClick handler when clicking an option", async () => { - const onClickMock = vi.fn(); - const options = [ - { - title: "Click Option", - description: "Click Description", - icon: Home, - onClick: onClickMock, - }, - ]; - - render(); - await userEvent.click(screen.getByText("Click Option")); - expect(onClickMock).toHaveBeenCalledTimes(1); - }); -}); diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.test.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.test.tsx deleted file mode 100644 index 00a729b94a..0000000000 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.test.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { Session } from "next-auth"; -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TUser } from "@formbricks/types/user"; -import { getEnvironment } from "@/lib/environment/service"; -import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils"; -import SurveyEditorEnvironmentLayout from "./layout"; - -// Mock sub-components to render identifiable elements -vi.mock("@/modules/ui/components/environmentId-base-layout", () => ({ - EnvironmentIdBaseLayout: ({ children, environmentId }: any) => ( -
- {environmentId} - {children} -
- ), -})); - -// Mocks for dependencies -vi.mock("@/modules/environments/lib/utils", () => ({ - environmentIdLayoutChecks: vi.fn(), -})); -vi.mock("@/lib/environment/service", () => ({ - getEnvironment: vi.fn(), -})); -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -describe("SurveyEditorEnvironmentLayout", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders successfully when environment is found", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test - session: { user: { id: "user1" } } as Session, - user: { id: "user1", email: "user1@example.com" } as TUser, - organization: { id: "org1", name: "Org1", billing: {} } as TOrganization, - }); - vi.mocked(getEnvironment).mockResolvedValueOnce({ id: "env1" } as TEnvironment); - - const result = await SurveyEditorEnvironmentLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
Survey Editor Content
, - }); - - render(result); - - expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveTextContent("env1"); - expect(screen.getByTestId("child")).toHaveTextContent("Survey Editor Content"); - }); - - test("throws an error when environment is not found", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: ((key: string) => key) as any, - session: { user: { id: "user1" } } as Session, - user: { id: "user1", email: "user1@example.com" } as TUser, - organization: { id: "org1", name: "Org1", billing: {} } as TOrganization, - }); - vi.mocked(getEnvironment).mockResolvedValueOnce(null); - - await expect( - SurveyEditorEnvironmentLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
Content
, - }) - ).rejects.toThrow("common.environment_not_found"); - }); - - test("calls redirect when session is null", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: ((key: string) => key) as any, - session: undefined as unknown as Session, - user: undefined as unknown as TUser, - organization: { id: "org1", name: "Org1", billing: {} } as TOrganization, - }); - vi.mocked(redirect).mockImplementationOnce(() => { - throw new Error("Redirect called"); - }); - - await expect( - SurveyEditorEnvironmentLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
Content
, - }) - ).rejects.toThrow("Redirect called"); - }); - - test("throws error if user is null", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: ((key: string) => key) as any, - session: { user: { id: "user1" } } as Session, - user: undefined as unknown as TUser, - organization: { id: "org1", name: "Org1", billing: {} } as TOrganization, - }); - - vi.mocked(redirect).mockImplementationOnce(() => { - throw new Error("Redirect called"); - }); - - await expect( - SurveyEditorEnvironmentLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
Content
, - }) - ).rejects.toThrow("common.user_not_found"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/(contacts)/contacts/[contactId]/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/(contacts)/contacts/[contactId]/page.test.tsx deleted file mode 100644 index e5b53319b9..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/(contacts)/contacts/[contactId]/page.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { SingleContactPage } from "@/modules/ee/contacts/[contactId]/page"; -import Page from "./page"; - -// mock constants -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - ENCRYPTION_KEY: "test", - ENTERPRISE_LICENSE_KEY: "test", - GITHUB_ID: "test", - GITHUB_SECRET: "test", - GOOGLE_CLIENT_ID: "test", - GOOGLE_CLIENT_SECRET: "test", - AZUREAD_CLIENT_ID: "mock-azuread-client-id", - AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", - AZUREAD_TENANT_ID: "mock-azuread-tenant-id", - OIDC_CLIENT_ID: "mock-oidc-client-id", - OIDC_CLIENT_SECRET: "mock-oidc-client-secret", - OIDC_ISSUER: "mock-oidc-issuer", - OIDC_DISPLAY_NAME: "mock-oidc-display-name", - OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm", - WEBAPP_URL: "mock-webapp-url", - IS_PRODUCTION: true, - FB_LOGO_URL: "https://example.com/mock-logo.png", - SMTP_HOST: "mock-smtp-host", - SMTP_PORT: "mock-smtp-port", - IS_POSTHOG_CONFIGURED: true, - SESSION_MAX_AGE: 1000, - AUDIT_LOG_ENABLED: 1, - REDIS_URL: undefined, -})); - -vi.mock("@/lib/env", () => ({ - env: { - PUBLIC_URL: "https://public-domain.com", - }, -})); - -describe("Contact Page Re-export", () => { - test("should re-export SingleContactPage", () => { - expect(Page).toBe(SingleContactPage); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/(contacts)/contacts/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/(contacts)/contacts/page.test.tsx deleted file mode 100644 index c8403dce57..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/(contacts)/contacts/page.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { ContactsPage } from "@/modules/ee/contacts/page"; -import Page from "./page"; - -// Mock the actual ContactsPage component -vi.mock("@/modules/ee/contacts/page", () => ({ - ContactsPage: () =>
Mock Contacts Page
, -})); - -describe("Contacts Page Re-export", () => { - test("should re-export ContactsPage from the EE module", () => { - // Assert that the default export 'Page' is the same as the mocked 'ContactsPage' - expect(Page).toBe(ContactsPage); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/(contacts)/segments/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/(contacts)/segments/page.test.tsx deleted file mode 100644 index 97a4e0ca21..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/(contacts)/segments/page.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import SegmentsPageWrapper from "./page"; - -vi.mock("@/modules/ee/contacts/segments/page", () => ({ - SegmentsPage: vi.fn(() =>
SegmentsPageMock
), -})); - -describe("SegmentsPageWrapper", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the SegmentsPage component", () => { - render(); - expect(screen.getByText("SegmentsPageMock")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.test.tsx deleted file mode 100644 index e33fa11b36..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.test.tsx +++ /dev/null @@ -1,472 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import type { Session } from "next-auth"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TMembership } from "@formbricks/types/memberships"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TProject } from "@formbricks/types/project"; -import { TUser } from "@formbricks/types/user"; -import { getOrganizationsByUserId } from "@/app/(app)/environments/[environmentId]/lib/organization"; -import { getProjectsByUserId } from "@/app/(app)/environments/[environmentId]/lib/project"; -import { getEnvironment, getEnvironments } from "@/lib/environment/service"; -import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; -import { getAccessFlags } from "@/lib/membership/utils"; -import { - getMonthlyActiveOrganizationPeopleCount, - getMonthlyOrganizationResponseCount, - getOrganizationByEnvironmentId, -} from "@/lib/organization/service"; -import { getUser } from "@/lib/user/service"; -import { - getAccessControlPermission, - getOrganizationProjectsLimit, -} from "@/modules/ee/license-check/lib/utils"; -import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; -import { getTeamsByOrganizationId } from "@/modules/ee/teams/team-list/lib/team"; - -// Mock services and utils -vi.mock("@/lib/environment/service", () => ({ - getEnvironment: vi.fn(), - getEnvironments: vi.fn(), -})); -vi.mock("@/lib/organization/service", () => ({ - getOrganizationByEnvironmentId: vi.fn(), - getMonthlyActiveOrganizationPeopleCount: vi.fn(), - getMonthlyOrganizationResponseCount: vi.fn(), -})); -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); -vi.mock("@/lib/membership/service", () => ({ - getMembershipByUserIdOrganizationId: vi.fn(), -})); -vi.mock("@/lib/membership/utils", () => ({ - getAccessFlags: vi.fn(() => ({ isMember: true })), // Default to member for simplicity -})); -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getOrganizationProjectsLimit: vi.fn(), - getAccessControlPermission: vi.fn(), -})); -vi.mock("@/modules/ee/teams/lib/roles", () => ({ - getProjectPermissionByUserId: vi.fn(), -})); -vi.mock("@/modules/ee/teams/team-list/lib/team", () => ({ - getTeamsByOrganizationId: vi.fn(), -})); -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); -vi.mock("@/app/(app)/environments/[environmentId]/lib/organization", () => ({ - getOrganizationsByUserId: vi.fn(), -})); -vi.mock("@/app/(app)/environments/[environmentId]/lib/project", () => ({ - getProjectsByUserId: vi.fn(), -})); -vi.mock("@formbricks/database", () => ({ - prisma: { - project: { - findMany: vi.fn(), - }, - organization: { - findMany: vi.fn(), - }, - }, -})); - -let mockIsFormbricksCloud = false; -let mockIsDevelopment = false; - -vi.mock("@/lib/constants", () => ({ - get IS_FORMBRICKS_CLOUD() { - return mockIsFormbricksCloud; - }, - get IS_DEVELOPMENT() { - return mockIsDevelopment; - }, -})); - -// Mock components -vi.mock("@/app/(app)/environments/[environmentId]/components/MainNavigation", () => ({ - MainNavigation: ({ organizationTeams, isAccessControlAllowed }: any) => ( -
- MainNavigation -
{JSON.stringify(organizationTeams || [])}
-
{isAccessControlAllowed?.toString() || "false"}
-
- ), -})); -vi.mock("@/app/(app)/environments/[environmentId]/components/TopControlBar", () => ({ - TopControlBar: () =>
TopControlBar
, -})); -vi.mock("@/modules/ui/components/limits-reached-banner", () => ({ - LimitsReachedBanner: () =>
LimitsReachedBanner
, -})); -vi.mock("@/modules/ui/components/pending-downgrade-banner", () => ({ - PendingDowngradeBanner: ({ - isPendingDowngrade, - active, - }: { - isPendingDowngrade: boolean; - active: boolean; - }) => - isPendingDowngrade && active ?
PendingDowngradeBanner
: null, -})); - -const mockUser = { - id: "user-1", - name: "Test User", - email: "test@example.com", - emailVerified: new Date(), - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - notificationSettings: { alert: {} }, -} as unknown as TUser; - -const mockOrganization = { - id: "org-1", - name: "Test Org", - billing: { - plan: "free", - limits: {}, - }, -} as unknown as TOrganization; - -const mockEnvironment: TEnvironment = { - id: "env-1", - createdAt: new Date(), - updatedAt: new Date(), - type: "production", - projectId: "proj-1", - appSetupCompleted: true, -}; - -const mockProject: TProject = { - id: "proj-1", - name: "Test Project", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "org-1", - environments: [mockEnvironment], -} as unknown as TProject; - -const mockMembership: TMembership = { - organizationId: "org-1", - userId: "user-1", - accepted: true, - role: "owner", -}; - -const mockLicense = { - plan: "free", - active: false, - lastChecked: new Date(), - features: { isMultiOrgEnabled: false }, -} as any; - -const mockProjectPermission = { - userId: "user-1", - projectId: "proj-1", - role: "admin", -} as any; - -const mockOrganizationTeams = [ - { - id: "team-1", - name: "Development Team", - }, - { - id: "team-2", - name: "Marketing Team", - }, -]; - -const mockSession: Session = { - user: { - id: "user-1", - }, - expires: new Date(Date.now() + 3600 * 1000).toISOString(), -}; - -describe("EnvironmentLayout", () => { - beforeEach(() => { - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getEnvironment).mockResolvedValue(mockEnvironment); - vi.mocked(getOrganizationsByUserId).mockResolvedValue([ - { id: mockOrganization.id, name: mockOrganization.name }, - ]); - vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization); - vi.mocked(getProjectsByUserId).mockResolvedValue([{ id: mockProject.id, name: mockProject.name }]); - vi.mocked(getEnvironments).mockResolvedValue([mockEnvironment]); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - vi.mocked(getMonthlyActiveOrganizationPeopleCount).mockResolvedValue(100); - vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(500); - vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(null as any); - vi.mocked(getProjectPermissionByUserId).mockResolvedValue(mockProjectPermission); - vi.mocked(getTeamsByOrganizationId).mockResolvedValue(mockOrganizationTeams); - vi.mocked(getAccessControlPermission).mockResolvedValue(true); - mockIsDevelopment = false; - mockIsFormbricksCloud = false; - }); - - afterEach(() => { - cleanup(); - vi.resetAllMocks(); - }); - - test("renders correctly with default props", async () => { - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - render( - await EnvironmentLayout({ - environmentId: "env-1", - session: mockSession, - children:
Child Content
, - }) - ); - expect(screen.getByTestId("main-navigation")).toBeInTheDocument(); - expect(screen.getByTestId("top-control-bar")).toBeInTheDocument(); - expect(screen.getByText("Child Content")).toBeInTheDocument(); - expect(screen.queryByTestId("dev-banner")).not.toBeInTheDocument(); - expect(screen.queryByTestId("limits-banner")).not.toBeInTheDocument(); - expect(screen.queryByTestId("downgrade-banner")).not.toBeInTheDocument(); - }); - - test("renders LimitsReachedBanner in Formbricks Cloud", async () => { - mockIsFormbricksCloud = true; - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - render( - await EnvironmentLayout({ - environmentId: "env-1", - session: mockSession, - children:
Child Content
, - }) - ); - expect(screen.getByTestId("limits-banner")).toBeInTheDocument(); - expect(vi.mocked(getMonthlyActiveOrganizationPeopleCount)).toHaveBeenCalledWith(mockOrganization.id); - expect(vi.mocked(getMonthlyOrganizationResponseCount)).toHaveBeenCalledWith(mockOrganization.id); - }); - - test("renders PendingDowngradeBanner when pending downgrade", async () => { - const pendingLicense = { ...mockLicense, isPendingDowngrade: true, active: true }; - vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(null as any); - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue(pendingLicense), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - render( - await EnvironmentLayout({ - environmentId: "env-1", - session: mockSession, - children:
Child Content
, - }) - ); - expect(screen.getByTestId("downgrade-banner")).toBeInTheDocument(); - }); - - test("handles empty organizationTeams array", async () => { - vi.mocked(getTeamsByOrganizationId).mockResolvedValue([]); - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - render( - await EnvironmentLayout({ - environmentId: "env-1", - session: mockSession, - children:
Child Content
, - }) - ); - - expect(screen.getByTestId("organization-teams")).toHaveTextContent("[]"); - }); - - test("handles null organizationTeams", async () => { - vi.mocked(getTeamsByOrganizationId).mockResolvedValue(null); - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - render( - await EnvironmentLayout({ - environmentId: "env-1", - session: mockSession, - children:
Child Content
, - }) - ); - - expect(screen.getByTestId("organization-teams")).toHaveTextContent("[]"); - }); - - test("handles isAccessControlAllowed false", async () => { - vi.mocked(getAccessControlPermission).mockResolvedValue(false); - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - render( - await EnvironmentLayout({ - environmentId: "env-1", - session: mockSession, - children:
Child Content
, - }) - ); - - expect(screen.getByTestId("is-access-control-allowed")).toHaveTextContent("false"); - }); - - test("throws error if user not found", async () => { - vi.mocked(getUser).mockResolvedValue(null); - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow( - "common.user_not_found" - ); - }); - - test("throws error if organization not found", async () => { - vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null); - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow( - "common.organization_not_found" - ); - }); - - test("throws error if environment not found", async () => { - vi.mocked(getEnvironment).mockResolvedValue(null); - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow( - "common.environment_not_found" - ); - }); - - test("throws error if projects, environments or organizations not found", async () => { - vi.mocked(getProjectsByUserId).mockResolvedValue(null as any); - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow( - "environments.projects_environments_organizations_not_found" - ); - }); - - test("throws error if member has no project permission", async () => { - vi.mocked(getAccessFlags).mockReturnValue({ isMember: true } as any); - vi.mocked(getProjectPermissionByUserId).mockResolvedValue(null); - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { EnvironmentLayout } = await import( - "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout" - ); - await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow( - "common.project_permission_not_found" - ); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentStorageHandler.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentStorageHandler.test.tsx deleted file mode 100644 index bb98e789ea..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentStorageHandler.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, test, vi } from "vitest"; -import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage"; -import EnvironmentStorageHandler from "./EnvironmentStorageHandler"; - -describe("EnvironmentStorageHandler", () => { - test("sets environmentId in localStorage on mount", () => { - const setItemSpy = vi.spyOn(Storage.prototype, "setItem"); - const testEnvironmentId = "test-env-123"; - - render(); - - expect(setItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS, testEnvironmentId); - setItemSpy.mockRestore(); - }); - - test("updates environmentId in localStorage when prop changes", () => { - const setItemSpy = vi.spyOn(Storage.prototype, "setItem"); - const initialEnvironmentId = "test-env-initial"; - const updatedEnvironmentId = "test-env-updated"; - - const { rerender } = render(); - - expect(setItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS, initialEnvironmentId); - - rerender(); - - expect(setItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS, updatedEnvironmentId); - expect(setItemSpy).toHaveBeenCalledTimes(2); // Called on mount and on rerender with new prop - - setItemSpy.mockRestore(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentSwitch.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentSwitch.test.tsx deleted file mode 100644 index 6f817ea581..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentSwitch.test.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { EnvironmentSwitch } from "./EnvironmentSwitch"; - -// Mock next/navigation -const mockPush = vi.fn(); -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(() => ({ - push: mockPush, - })), -})); - -// Mock @tolgee/react -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -const mockEnvironmentDev: TEnvironment = { - id: "dev-env-id", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - projectId: "project-id", - appSetupCompleted: true, -}; - -const mockEnvironmentProd: TEnvironment = { - id: "prod-env-id", - createdAt: new Date(), - updatedAt: new Date(), - type: "production", - projectId: "project-id", - appSetupCompleted: true, -}; - -const mockEnvironments = [mockEnvironmentDev, mockEnvironmentProd]; - -describe("EnvironmentSwitch", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders checked when environment is development", () => { - render(); - const switchElement = screen.getByRole("switch"); - expect(switchElement).toBeChecked(); - expect(screen.getByText("common.dev_env")).toHaveClass("text-orange-800"); - }); - - test("renders unchecked when environment is production", () => { - render(); - const switchElement = screen.getByRole("switch"); - expect(switchElement).not.toBeChecked(); - expect(screen.getByText("common.dev_env")).not.toHaveClass("text-orange-800"); - }); - - test("calls router.push with development environment ID when toggled from production", async () => { - render(); - const switchElement = screen.getByRole("switch"); - - expect(switchElement).not.toBeChecked(); - await userEvent.click(switchElement); - - // Check loading state (switch disabled) - expect(switchElement).toBeDisabled(); - - // Check router push call - await waitFor(() => { - expect(mockPush).toHaveBeenCalledWith(`/environments/${mockEnvironmentDev.id}/`); - }); - - // Check visual state change (though state update happens before navigation) - // In a real scenario, the component would re-render with the new environment prop after navigation. - // Here, we simulate the state change directly for testing the toggle logic. - await waitFor(() => { - // Re-render or check internal state if possible, otherwise check mock calls - // Since the component manages its own state, we can check the visual state after click - expect(switchElement).toBeChecked(); // State updates immediately - }); - }); - - test("calls router.push with production environment ID when toggled from development", async () => { - render(); - const switchElement = screen.getByRole("switch"); - - expect(switchElement).toBeChecked(); - await userEvent.click(switchElement); - - // Check loading state (switch disabled) - expect(switchElement).toBeDisabled(); - - // Check router push call - await waitFor(() => { - expect(mockPush).toHaveBeenCalledWith(`/environments/${mockEnvironmentProd.id}/`); - }); - - // Check visual state change - await waitFor(() => { - expect(switchElement).not.toBeChecked(); // State updates immediately - }); - }); - - test("does not call router.push if target environment is not found", async () => { - const incompleteEnvironments = [mockEnvironmentProd]; // Only production exists - render(); - const switchElement = screen.getByRole("switch"); - - await userEvent.click(switchElement); // Try to toggle to development - - await waitFor(() => { - expect(switchElement).toBeDisabled(); // Loading state still set - }); - - // router.push should not be called because dev env is missing - expect(mockPush).not.toHaveBeenCalled(); - - // State still updates visually - await waitFor(() => { - expect(switchElement).toBeChecked(); - }); - }); - - test("toggles using the label click", async () => { - render(); - const labelElement = screen.getByText("common.dev_env"); - const switchElement = screen.getByRole("switch"); - - expect(switchElement).not.toBeChecked(); - await userEvent.click(labelElement); // Click the label - - // Check loading state (switch disabled) - expect(switchElement).toBeDisabled(); - - // Check router push call - await waitFor(() => { - expect(mockPush).toHaveBeenCalledWith(`/environments/${mockEnvironmentDev.id}/`); - }); - - // Check visual state change - await waitFor(() => { - expect(switchElement).toBeChecked(); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.test.tsx deleted file mode 100644 index 84224b7039..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.test.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { usePathname, useRouter } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TProject } from "@formbricks/types/project"; -import { TUser } from "@formbricks/types/user"; -import { useSignOut } from "@/modules/auth/hooks/use-sign-out"; -import { getLatestStableFbReleaseAction } from "@/modules/projects/settings/(setup)/app-connection/actions"; -import { MainNavigation } from "./MainNavigation"; - -// Mock constants that this test needs -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - WEBAPP_URL: "http://localhost:3000", -})); - -// Mock server actions that this test needs -vi.mock("@/modules/auth/actions/sign-out", () => ({ - logSignOutAction: vi.fn().mockResolvedValue(undefined), -})); - -// Mock dependencies -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(() => ({ push: vi.fn() })), - usePathname: vi.fn(() => "/environments/env1/surveys"), -})); -vi.mock("next-auth/react", () => ({ - signOut: vi.fn(), -})); -vi.mock("@/modules/auth/hooks/use-sign-out", () => ({ - useSignOut: vi.fn(() => ({ signOut: vi.fn() })), -})); -vi.mock("@/modules/projects/settings/(setup)/app-connection/actions", () => ({ - getLatestStableFbReleaseAction: vi.fn(), -})); -vi.mock("@/app/lib/formbricks", () => ({ - formbricksLogout: vi.fn(), -})); -vi.mock("@/lib/membership/utils", () => ({ - getAccessFlags: (role?: string) => ({ - isAdmin: role === "admin", - isOwner: role === "owner", - isManager: role === "manager", - isMember: role === "member", - isBilling: role === "billing", - }), -})); -vi.mock("@/modules/organization/components/CreateOrganizationModal", () => ({ - CreateOrganizationModal: ({ open }: { open: boolean }) => - open ?
Create Org Modal
: null, -})); -vi.mock("@/modules/ui/components/avatars", () => ({ - ProfileAvatar: () =>
Avatar
, -})); -vi.mock("next/image", () => ({ - // eslint-disable-next-line @next/next/no-img-element - default: (props: any) => test, -})); -vi.mock("../../../../../package.json", () => ({ - version: "1.0.0", -})); - -// Mock localStorage -const localStorageMock = (() => { - let store: Record = {}; - return { - getItem: (key: string) => store[key] || null, - setItem: (key: string, value: string) => { - store[key] = value.toString(); - }, - removeItem: (key: string) => { - delete store[key]; - }, - clear: () => { - store = {}; - }, - }; -})(); -Object.defineProperty(window, "localStorage", { value: localStorageMock }); - -// Mock data -const mockEnvironment: TEnvironment = { - id: "env1", - createdAt: new Date(), - updatedAt: new Date(), - type: "production", - projectId: "proj1", - appSetupCompleted: true, -}; -const mockUser = { - id: "user1", - name: "Test User", - email: "test@example.com", - emailVerified: new Date(), - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - notificationSettings: { alert: {} }, - role: "project_manager", - objective: "other", -} as unknown as TUser; - -const mockOrganization = { - id: "org1", - name: "Test Org", - createdAt: new Date(), - updatedAt: new Date(), - billing: { stripeCustomerId: null, plan: "free", limits: { monthly: { responses: null } } } as any, -} as unknown as TOrganization; - -const mockOrganizations: TOrganization[] = [ - mockOrganization, - { ...mockOrganization, id: "org2", name: "Another Org" }, -]; -const mockProject: TProject = { - id: "proj1", - name: "Test Project", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "org1", - environments: [mockEnvironment], - config: { channel: "website" }, -} as unknown as TProject; -const mockProjects: TProject[] = [mockProject]; - -const defaultProps = { - environment: mockEnvironment, - organizations: mockOrganizations, - user: mockUser, - organization: mockOrganization, - projects: mockProjects, - isMultiOrgEnabled: true, - isFormbricksCloud: false, - isDevelopment: false, - membershipRole: "owner" as const, - organizationProjectsLimit: 5, - isLicenseActive: true, - isAccessControlAllowed: true, -}; - -describe("MainNavigation", () => { - let mockRouterPush: ReturnType; - - beforeEach(() => { - mockRouterPush = vi.fn(); - vi.mocked(useRouter).mockReturnValue({ push: mockRouterPush } as any); - vi.mocked(usePathname).mockReturnValue("/environments/env1/surveys"); - vi.mocked(getLatestStableFbReleaseAction).mockResolvedValue({ data: null }); // Default: no new version - localStorage.clear(); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders expanded by default and collapses on toggle", async () => { - render(); - // Assuming the toggle button is the only one initially without an accessible name - // A more specific selector like data-testid would be better if available. - const toggleButton = screen.getByRole("button", { name: "" }); - - // Check initial state (expanded) - expect(screen.getByAltText("environments.formbricks_logo")).toBeInTheDocument(); - // Check localStorage is not set initially after clear() - expect(localStorage.getItem("isMainNavCollapsed")).toBeNull(); - - // Click to collapse - await userEvent.click(toggleButton); - - // Check state after first toggle (collapsed) - await waitFor(() => { - // Check that the attribute eventually becomes true - // Check that localStorage is updated - expect(localStorage.getItem("isMainNavCollapsed")).toBe("true"); - }); - // Check that the logo is eventually hidden - await waitFor(() => { - expect(screen.queryByAltText("environments.formbricks_logo")).not.toBeInTheDocument(); - }); - - // Click to expand - await userEvent.click(toggleButton); - - // Check state after second toggle (expanded) - await waitFor(() => { - // Check that the attribute eventually becomes false - // Check that localStorage is updated - expect(localStorage.getItem("isMainNavCollapsed")).toBe("false"); - }); - // Check that the logo is eventually visible - await waitFor(() => { - expect(screen.getByAltText("environments.formbricks_logo")).toBeInTheDocument(); - }); - }); - - test("renders user dropdown and handles logout", async () => { - const mockSignOut = vi.fn().mockResolvedValue({ url: "/auth/login" }); - vi.mocked(useSignOut).mockReturnValue({ signOut: mockSignOut }); - - // Set up localStorage spy on the mocked localStorage - - render(); - - // Find the avatar and get its parent div which acts as the trigger - const userTrigger = screen.getByTestId("profile-avatar").parentElement!; - expect(userTrigger).toBeInTheDocument(); // Ensure the trigger element is found - await userEvent.click(userTrigger); - - // Wait for the dropdown content to appear - using getAllByText to handle multiple instances - await waitFor(() => { - const accountElements = screen.getAllByText("common.account"); - expect(accountElements).toHaveLength(2); - }); - - expect(screen.getByText("common.documentation")).toBeInTheDocument(); - expect(screen.getByText("common.logout")).toBeInTheDocument(); - - const logoutButton = screen.getByText("common.logout"); - await userEvent.click(logoutButton); - - expect(mockSignOut).toHaveBeenCalledWith({ - reason: "user_initiated", - redirectUrl: "/auth/login", - organizationId: "org1", - redirect: false, - callbackUrl: "/auth/login", - clearEnvironmentId: true, - }); - - await waitFor(() => { - expect(mockRouterPush).toHaveBeenCalledWith("/auth/login"); - }); - }); - - test("hides new version banner for members or if no new version", async () => { - // Test for member - vi.mocked(getLatestStableFbReleaseAction).mockResolvedValue({ data: "v1.1.0" }); - render(); - let toggleButton = screen.getByRole("button", { name: "" }); - await userEvent.click(toggleButton); - await waitFor(() => { - expect(screen.queryByText("common.new_version_available", { exact: false })).not.toBeInTheDocument(); - }); - cleanup(); // Clean up before next render - - // Test for no new version - vi.mocked(getLatestStableFbReleaseAction).mockResolvedValue({ data: null }); - render(); - toggleButton = screen.getByRole("button", { name: "" }); - await userEvent.click(toggleButton); - await waitFor(() => { - expect(screen.queryByText("common.new_version_available", { exact: false })).not.toBeInTheDocument(); - }); - }); - - test("hides main nav and project switcher if user role is billing", () => { - render(); - expect(screen.queryByRole("link", { name: /common.surveys/ })).not.toBeInTheDocument(); - expect(screen.queryByTestId("project-switcher")).not.toBeInTheDocument(); - }); - - test("passes isAccessControlAllowed props to ProjectSwitcher", () => { - render(); - - // Test basic navigation structure is rendered (aside element with complementary role) - expect(screen.getByRole("complementary")).toBeInTheDocument(); - expect(screen.getByTestId("profile-avatar")).toBeInTheDocument(); - }); - - test("handles no organizationTeams", () => { - render(); - - // Test that navigation renders correctly with no teams - expect(screen.getByRole("complementary")).toBeInTheDocument(); - }); - - test("handles isAccessControlAllowed false", () => { - render(); - - // Test that navigation renders correctly with access control disabled - expect(screen.getByRole("complementary")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/NavbarLoading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/NavbarLoading.test.tsx deleted file mode 100644 index ecb0261618..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/NavbarLoading.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test } from "vitest"; -import { NavbarLoading } from "./NavbarLoading"; - -describe("NavbarLoading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the correct number of skeleton elements", () => { - render(); - - // Find all divs with the animate-pulse class - const skeletonElements = screen.getAllByText((content, element) => { - return element?.tagName.toLowerCase() === "div" && element.classList.contains("animate-pulse"); - }); - - // There are 8 skeleton divs in the component - expect(skeletonElements).toHaveLength(8); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/NavigationLink.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/NavigationLink.test.tsx deleted file mode 100644 index 7d17cc66e2..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/NavigationLink.test.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { cleanup, render, screen, within } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { NavigationLink } from "./NavigationLink"; - -// Mock next/link -vi.mock("next/link", () => ({ - default: ({ children, href }: { children: React.ReactNode; href: string }) => {children}, -})); - -// Mock tooltip components -vi.mock("@/modules/ui/components/tooltip", () => ({ - Tooltip: ({ children }: { children: React.ReactNode }) =>
{children}
, - TooltipContent: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - TooltipProvider: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - TooltipTrigger: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), -})); - -const defaultProps = { - href: "/test-link", - isActive: false, - isCollapsed: false, - children: , - linkText: "Test Link Text", - isTextVisible: true, -}; - -describe("NavigationLink", () => { - afterEach(() => { - cleanup(); - }); - - test("renders expanded link correctly (inactive, text visible)", () => { - render(); - const linkElement = screen.getByRole("link"); - const listItem = linkElement.closest("li"); - const textSpan = screen.getByText(defaultProps.linkText); - - expect(linkElement).toHaveAttribute("href", defaultProps.href); - expect(screen.getByTestId("icon")).toBeInTheDocument(); - expect(textSpan).toBeInTheDocument(); - expect(textSpan).toHaveClass("opacity-0"); - expect(listItem).not.toHaveClass("bg-slate-50"); // inactiveClass check - expect(listItem).toHaveClass("hover:bg-slate-50"); // inactiveClass check - expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument(); - }); - - test("renders expanded link correctly (active, text hidden)", () => { - render(); - const linkElement = screen.getByRole("link"); - const listItem = linkElement.closest("li"); - const textSpan = screen.getByText(defaultProps.linkText); - - expect(linkElement).toHaveAttribute("href", defaultProps.href); - expect(screen.getByTestId("icon")).toBeInTheDocument(); - expect(textSpan).toBeInTheDocument(); - expect(textSpan).toHaveClass("opacity-100"); - expect(listItem).toHaveClass("bg-slate-50"); // activeClass check - expect(listItem).toHaveClass("border-brand-dark"); // activeClass check - expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument(); - }); - - test("renders collapsed link correctly (inactive)", () => { - render(); - const linkElement = screen.getByRole("link"); - const listItem = linkElement.closest("li"); - - expect(linkElement).toHaveAttribute("href", defaultProps.href); - expect(screen.getByTestId("icon")).toBeInTheDocument(); - // Check text is NOT directly within the list item - expect(within(listItem!).queryByText(defaultProps.linkText)).not.toBeInTheDocument(); - expect(listItem).not.toHaveClass("bg-slate-50"); // inactiveClass check - expect(listItem).toHaveClass("hover:bg-slate-50"); // inactiveClass check - - // Check tooltip elements - expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument(); - expect(screen.getByTestId("tooltip")).toBeInTheDocument(); - expect(screen.getByTestId("tooltip-trigger")).toBeInTheDocument(); - // Check text IS within the tooltip content mock - expect(screen.getByTestId("tooltip-content")).toHaveTextContent(defaultProps.linkText); - }); - - test("renders collapsed link correctly (active)", () => { - render(); - const linkElement = screen.getByRole("link"); - const listItem = linkElement.closest("li"); - - expect(linkElement).toHaveAttribute("href", defaultProps.href); - expect(screen.getByTestId("icon")).toBeInTheDocument(); - // Check text is NOT directly within the list item - expect(within(listItem!).queryByText(defaultProps.linkText)).not.toBeInTheDocument(); - expect(listItem).toHaveClass("bg-slate-50"); // activeClass check - expect(listItem).toHaveClass("border-brand-dark"); // activeClass check - - // Check tooltip elements - expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument(); - // Check text IS within the tooltip content mock - expect(screen.getByTestId("tooltip-content")).toHaveTextContent(defaultProps.linkText); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/PosthogIdentify.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/PosthogIdentify.test.tsx deleted file mode 100644 index 02f29a314d..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/PosthogIdentify.test.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render } from "@testing-library/react"; -import { Session } from "next-auth"; -import { usePostHog } from "posthog-js/react"; -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganizationBilling } from "@formbricks/types/organizations"; -import { TUser } from "@formbricks/types/user"; -import { PosthogIdentify } from "./PosthogIdentify"; - -type PartialPostHog = Partial>; - -vi.mock("posthog-js/react", () => ({ - usePostHog: vi.fn(), -})); - -describe("PosthogIdentify", () => { - beforeEach(() => { - cleanup(); - }); - - test("identifies the user and sets groups when isPosthogEnabled is true", () => { - const mockIdentify = vi.fn(); - const mockGroup = vi.fn(); - - const mockPostHog: PartialPostHog = { - identify: mockIdentify, - group: mockGroup, - }; - - vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType); - - render( - - ); - - // verify that identify is called with the session user id + extra info - expect(mockIdentify).toHaveBeenCalledWith("user-123", { - name: "Test User", - email: "test@example.com", - }); - - // environment + organization groups - expect(mockGroup).toHaveBeenCalledTimes(2); - expect(mockGroup).toHaveBeenCalledWith("environment", "env-456", { name: "env-456" }); - expect(mockGroup).toHaveBeenCalledWith("organization", "org-789", { - name: "Test Org", - plan: "enterprise", - responseLimit: 1000, - miuLimit: 5000, - }); - }); - - test("does nothing if isPosthogEnabled is false", () => { - const mockIdentify = vi.fn(); - const mockGroup = vi.fn(); - - const mockPostHog: PartialPostHog = { - identify: mockIdentify, - group: mockGroup, - }; - - vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType); - - render( - - ); - - expect(mockIdentify).not.toHaveBeenCalled(); - expect(mockGroup).not.toHaveBeenCalled(); - }); - - test("does nothing if session user is missing", () => { - const mockIdentify = vi.fn(); - const mockGroup = vi.fn(); - - const mockPostHog: PartialPostHog = { - identify: mockIdentify, - group: mockGroup, - }; - - vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType); - - render( - - ); - - // Because there's no session.user, we skip identify - expect(mockIdentify).not.toHaveBeenCalled(); - expect(mockGroup).not.toHaveBeenCalled(); - }); - - test("identifies user but does not group if environmentId/organizationId not provided", () => { - const mockIdentify = vi.fn(); - const mockGroup = vi.fn(); - - const mockPostHog: PartialPostHog = { - identify: mockIdentify, - group: mockGroup, - }; - - vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType); - - render( - - ); - - expect(mockIdentify).toHaveBeenCalledWith("user-123", { - name: "Test User", - email: "test@example.com", - }); - // No environmentId or organizationId => no group calls - expect(mockGroup).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/ProjectNavItem.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/ProjectNavItem.test.tsx deleted file mode 100644 index d3f7548825..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/ProjectNavItem.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ProjectNavItem } from "./ProjectNavItem"; - -describe("ProjectNavItem", () => { - afterEach(() => { - cleanup(); - }); - - const defaultProps = { - href: "/test-path", - children: Test Child, - }; - - test("renders correctly when active", () => { - render(); - - const linkElement = screen.getByRole("link"); - const listItem = linkElement.closest("li"); - - expect(linkElement).toHaveAttribute("href", "/test-path"); - expect(screen.getByText("Test Child")).toBeInTheDocument(); - expect(listItem).toHaveClass("bg-slate-50"); - expect(listItem).toHaveClass("font-semibold"); - expect(listItem).not.toHaveClass("hover:bg-slate-50"); - }); - - test("renders correctly when inactive", () => { - render(); - - const linkElement = screen.getByRole("link"); - const listItem = linkElement.closest("li"); - - expect(linkElement).toHaveAttribute("href", "/test-path"); - expect(screen.getByText("Test Child")).toBeInTheDocument(); - expect(listItem).not.toHaveClass("bg-slate-50"); - expect(listItem).not.toHaveClass("font-semibold"); - expect(listItem).toHaveClass("hover:bg-slate-50"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/ResponseFilterContext.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/ResponseFilterContext.test.tsx deleted file mode 100644 index 43c6b9bce8..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/ResponseFilterContext.test.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { QuestionOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox"; -import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter"; -import { getTodayDate } from "@/app/lib/surveys/surveys"; -import { ResponseFilterProvider, useResponseFilter } from "./ResponseFilterContext"; - -// Mock the getTodayDate function -vi.mock("@/app/lib/surveys/surveys", () => ({ - getTodayDate: vi.fn(), -})); - -const mockToday = new Date("2024-01-15T00:00:00.000Z"); -const mockFromDate = new Date("2024-01-01T00:00:00.000Z"); - -// Test component to use the hook -const TestComponent = () => { - const { - selectedFilter, - setSelectedFilter, - selectedOptions, - setSelectedOptions, - dateRange, - setDateRange, - resetState, - } = useResponseFilter(); - - return ( -
-
{selectedFilter.responseStatus}
-
{selectedFilter.filter.length}
-
{selectedOptions.questionOptions.length}
-
{selectedOptions.questionFilterOptions.length}
-
{dateRange.from?.toISOString()}
-
{dateRange.to?.toISOString()}
- - - - - -
- ); -}; - -describe("ResponseFilterContext", () => { - beforeEach(() => { - vi.mocked(getTodayDate).mockReturnValue(mockToday); - }); - - afterEach(() => { - cleanup(); - vi.resetAllMocks(); - }); - - test("should provide initial state values", () => { - render( - - - - ); - - expect(screen.getByTestId("responseStatus").textContent).toBe("all"); - expect(screen.getByTestId("filterLength").textContent).toBe("0"); - expect(screen.getByTestId("questionOptionsLength").textContent).toBe("0"); - expect(screen.getByTestId("questionFilterOptionsLength").textContent).toBe("0"); - expect(screen.getByTestId("dateFrom").textContent).toBe(""); - expect(screen.getByTestId("dateTo").textContent).toBe(mockToday.toISOString()); - }); - - test("should update selectedFilter state", async () => { - render( - - - - ); - - const updateButton = screen.getByText("Update Filter"); - await userEvent.click(updateButton); - - expect(screen.getByTestId("responseStatus").textContent).toBe("complete"); - expect(screen.getByTestId("filterLength").textContent).toBe("1"); - }); - - test("should update selectedOptions state", async () => { - render( - - - - ); - - const updateButton = screen.getByText("Update Options"); - await userEvent.click(updateButton); - - expect(screen.getByTestId("questionOptionsLength").textContent).toBe("1"); - expect(screen.getByTestId("questionFilterOptionsLength").textContent).toBe("1"); - }); - - test("should update dateRange state", async () => { - render( - - - - ); - - const updateButton = screen.getByText("Update Date Range"); - await userEvent.click(updateButton); - - expect(screen.getByTestId("dateFrom").textContent).toBe(mockFromDate.toISOString()); - expect(screen.getByTestId("dateTo").textContent).toBe(mockToday.toISOString()); - }); - - test("should throw error when useResponseFilter is used outside of Provider", () => { - // Hide console error temporarily - const consoleErrorMock = vi.spyOn(console, "error").mockImplementation(() => {}); - expect(() => render()).toThrow("useFilterDate must be used within a FilterDateProvider"); - consoleErrorMock.mockRestore(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.test.tsx deleted file mode 100644 index e46a908694..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { WidgetStatusIndicator } from "./WidgetStatusIndicator"; - -// Mock next/navigation -const mockRefresh = vi.fn(); -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - refresh: mockRefresh, - }), -})); - -// Mock lucide-react icons -vi.mock("lucide-react", () => ({ - AlertTriangleIcon: () =>
AlertTriangleIcon
, - CheckIcon: () =>
CheckIcon
, - RotateCcwIcon: () =>
RotateCcwIcon
, -})); - -// Mock Button component -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, ...props }: any) => ( - - ), -})); - -const mockEnvironmentNotImplemented: TEnvironment = { - id: "env-not-implemented", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - projectId: "proj1", - appSetupCompleted: false, // Not implemented state -}; - -const mockEnvironmentRunning: TEnvironment = { - id: "env-running", - createdAt: new Date(), - updatedAt: new Date(), - type: "production", - projectId: "proj1", - appSetupCompleted: true, // Running state -}; - -describe("WidgetStatusIndicator", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders correctly for 'notImplemented' state", () => { - render(); - - // Check icon - expect(screen.getByTestId("alert-icon")).toBeInTheDocument(); - expect(screen.queryByTestId("check-icon")).not.toBeInTheDocument(); - - // Check texts - expect( - screen.getByText("environments.project.app-connection.formbricks_sdk_not_connected") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.project.app-connection.formbricks_sdk_not_connected_description") - ).toBeInTheDocument(); - - // Check button - const recheckButton = screen.getByRole("button", { name: /environments.project.app-connection.recheck/ }); - expect(recheckButton).toBeInTheDocument(); - expect(screen.getByTestId("refresh-icon")).toBeInTheDocument(); - }); - - test("renders correctly for 'running' state", () => { - render(); - - // Check icon - expect(screen.getByTestId("check-icon")).toBeInTheDocument(); - expect(screen.queryByTestId("alert-icon")).not.toBeInTheDocument(); - - // Check texts - expect(screen.getByText("environments.project.app-connection.receiving_data")).toBeInTheDocument(); - expect( - screen.getByText("environments.project.app-connection.formbricks_sdk_connected") - ).toBeInTheDocument(); - - // Check button absence - expect( - screen.queryByRole("button", { name: /environments.project.app-connection.recheck/ }) - ).not.toBeInTheDocument(); - expect(screen.queryByTestId("refresh-icon")).not.toBeInTheDocument(); - }); - - test("calls router.refresh when 'Recheck' button is clicked", async () => { - render(); - - const recheckButton = screen.getByRole("button", { name: /environments.project.app-connection.recheck/ }); - await userEvent.click(recheckButton); - - expect(mockRefresh).toHaveBeenCalledTimes(1); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/environment-breadcrumb.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/environment-breadcrumb.test.tsx deleted file mode 100644 index bc62623de2..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/environment-breadcrumb.test.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { useRouter } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { EnvironmentBreadcrumb } from "./environment-breadcrumb"; - -// Mock the dependencies -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), -})); - -// Mock the UI components -vi.mock("@/modules/ui/components/breadcrumb", () => ({ - BreadcrumbItem: ({ children, isActive, isHighlighted, ...props }: any) => ( -
  • - {children} -
  • - ), -})); - -vi.mock("@/modules/ui/components/dropdown-menu", () => ({ - DropdownMenu: ({ children, onOpenChange }: any) => ( - - ), - DropdownMenuContent: ({ children, ...props }: any) => ( -
    - {children} -
    - ), - DropdownMenuCheckboxItem: ({ children, onClick, checked, ...props }: any) => ( -
    e.key === "Enter" && onClick?.()} - role="menuitemcheckbox" - aria-checked={checked} - tabIndex={0} - {...props}> - {children} -
    - ), - DropdownMenuTrigger: ({ children, ...props }: any) => ( - - ), - DropdownMenuGroup: ({ children }: any) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/tooltip", () => ({ - TooltipProvider: ({ children }: any) =>
    {children}
    , - Tooltip: ({ children }: any) =>
    {children}
    , - TooltipTrigger: ({ children, asChild }: any) => ( -
    - {children} -
    - ), - TooltipContent: ({ children, className }: any) => ( -
    - {children} -
    - ), -})); - -// Mock Lucide React icons -vi.mock("lucide-react", () => ({ - Code2Icon: ({ className, strokeWidth }: any) => { - const isHeader = className?.includes("mr-2"); - return ( - - Code2 Icon - - ); - }, - ChevronDownIcon: ({ className, strokeWidth }: any) => ( - - ChevronDown Icon - - ), - CircleHelpIcon: ({ className }: any) => ( - - CircleHelp Icon - - ), - Loader2: ({ className }: any) => ( - - Loader2 Icon - - ), -})); - -describe("EnvironmentBreadcrumb", () => { - const mockPush = vi.fn(); - const mockRouter = { - push: mockPush, - replace: vi.fn(), - refresh: vi.fn(), - back: vi.fn(), - forward: vi.fn(), - prefetch: vi.fn(), - }; - - const mockProductionEnvironment: TEnvironment = { - id: "env-prod-1", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-01"), - type: "production", - projectId: "project-1", - appSetupCompleted: true, - }; - - const mockDevelopmentEnvironment: TEnvironment = { - id: "env-dev-1", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-01"), - type: "development", - projectId: "project-1", - appSetupCompleted: true, - }; - - const mockEnvironments: TEnvironment[] = [mockProductionEnvironment, mockDevelopmentEnvironment]; - - beforeEach(() => { - vi.mocked(useRouter).mockReturnValue(mockRouter as any); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders environment breadcrumb with production environment", () => { - render( - - ); - - expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument(); - expect(screen.getByTestId("code2-icon")).toBeInTheDocument(); - expect(screen.getAllByText("production")).toHaveLength(2); // trigger + dropdown option - }); - - test("renders environment breadcrumb with development environment and shows tooltip", () => { - render( - - ); - - expect(screen.getAllByText("development")).toHaveLength(2); // trigger + dropdown option - expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument(); - expect(screen.getByTestId("circle-help-icon")).toBeInTheDocument(); - }); - - test("highlights breadcrumb item for development environment", () => { - render( - - ); - - const breadcrumbItem = screen.getByTestId("breadcrumb-item"); - expect(breadcrumbItem).toHaveAttribute("data-highlighted", "true"); - }); - - test("does not highlight breadcrumb item for production environment", () => { - render( - - ); - - const breadcrumbItem = screen.getByTestId("breadcrumb-item"); - expect(breadcrumbItem).toHaveAttribute("data-highlighted", "false"); - }); - - test("shows chevron down icon when dropdown is open", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - await waitFor(() => { - expect(screen.getAllByTestId("chevron-down-icon")).toHaveLength(1); - }); - }); - - test("renders dropdown content with environment options", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - expect(screen.getByTestId("dropdown-content")).toBeInTheDocument(); - expect(screen.getByText("common.choose_environment")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-group")).toBeInTheDocument(); - }); - - test("renders all environment options in dropdown", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item"); - expect(checkboxItems).toHaveLength(2); - - // Check production environment option - const productionOption = checkboxItems.find((item) => item.textContent?.includes("production")); - expect(productionOption).toBeInTheDocument(); - expect(productionOption).toHaveAttribute("data-checked", "true"); - - // Check development environment option - const developmentOption = checkboxItems.find((item) => item.textContent?.includes("development")); - expect(developmentOption).toBeInTheDocument(); - expect(developmentOption).toHaveAttribute("data-checked", "false"); - }); - - test("handles environment change when clicking dropdown option", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item"); - const developmentOption = checkboxItems.find((item) => item.textContent?.includes("development")); - - expect(developmentOption).toBeInTheDocument(); - await user.click(developmentOption!); - - expect(mockPush).toHaveBeenCalledWith("/environments/env-dev-1/"); - }); - - test("capitalizes environment type in display", () => { - render( - - ); - - const environmentSpans = screen.getAllByText("production"); - const triggerSpan = environmentSpans.find((span) => span.className.includes("capitalize")); - expect(triggerSpan).toHaveClass("capitalize"); - }); - - test("tooltip shows correct content for development environment", () => { - render( - - ); - - const tooltipContent = screen.getByTestId("tooltip-content"); - expect(tooltipContent).toHaveClass("text-white bg-red-800 border-none mt-2"); - expect(tooltipContent).toHaveTextContent("common.development_environment_banner"); - }); - - test("renders without tooltip for production environment", () => { - render( - - ); - - expect(screen.queryByTestId("circle-help-icon")).not.toBeInTheDocument(); - expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument(); - }); - - test("sets breadcrumb item as active when dropdown is open", async () => { - const user = userEvent.setup(); - render( - - ); - - // Initially not active - let breadcrumbItem = screen.getByTestId("breadcrumb-item"); - expect(breadcrumbItem).toHaveAttribute("data-active", "false"); - - // Open dropdown - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - // Should be active when dropdown is open - breadcrumbItem = screen.getByTestId("breadcrumb-item"); - expect(breadcrumbItem).toHaveAttribute("data-active", "true"); - }); - - test("handles single environment scenario", () => { - const singleEnvironment = [mockProductionEnvironment]; - - render( - - ); - - expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument(); - expect(screen.getAllByText("production")).toHaveLength(2); // trigger + dropdown option - }); - - test("handles empty environments array gracefully", () => { - render(); - - expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument(); - expect(screen.getByText("production")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/organization-breadcrumb.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/organization-breadcrumb.test.tsx deleted file mode 100644 index 19c6c52d18..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/organization-breadcrumb.test.tsx +++ /dev/null @@ -1,560 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { usePathname, useRouter } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations"; -import { OrganizationBreadcrumb } from "./organization-breadcrumb"; - -// Mock the dependencies -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), - usePathname: vi.fn(), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("@/modules/organization/components/CreateOrganizationModal", () => ({ - CreateOrganizationModal: ({ open, setOpen }: any) => - open ? ( -
    - - Create Organization Modal -
    - ) : null, -})); - -// Mock the UI components -vi.mock("@/modules/ui/components/breadcrumb", () => ({ - BreadcrumbItem: ({ children, isActive, ...props }: any) => ( -
  • - {children} -
  • - ), -})); - -vi.mock("@/modules/ui/components/dropdown-menu", () => ({ - DropdownMenu: ({ children, onOpenChange }: any) => ( -
    onOpenChange?.(true)} - onKeyDown={(e: any) => e.key === "Enter" && onOpenChange?.(true)} - role="button" - tabIndex={0}> - {children} -
    - ), - DropdownMenuContent: ({ children, ...props }: any) => ( -
    - {children} -
    - ), - DropdownMenuCheckboxItem: ({ children, onClick, checked, ...props }: any) => ( -
    e.key === "Enter" && onClick?.()} - role="menuitemcheckbox" - aria-checked={checked} - tabIndex={0} - {...props}> - {children} -
    - ), - DropdownMenuTrigger: ({ children, ...props }: any) => ( - - ), - DropdownMenuGroup: ({ children }: any) =>
    {children}
    , - DropdownMenuSeparator: () =>
    , -})); - -// Mock Lucide React icons -vi.mock("lucide-react", () => ({ - BuildingIcon: ({ className, strokeWidth }: any) => { - const isHeader = className?.includes("mr-2"); - return ( - - Building Icon - - ); - }, - ChevronDownIcon: ({ className, strokeWidth }: any) => ( - - ChevronDown Icon - - ), - ChevronRightIcon: ({ className, strokeWidth }: any) => ( - - ChevronRight Icon - - ), - PlusIcon: ({ className }: any) => ( - - Plus Icon - - ), - SettingsIcon: ({ className }: any) => ( - - Settings Icon - - ), - Loader2: ({ className }: any) => ( - - Loader2 Icon - - ), -})); - -describe("OrganizationBreadcrumb", () => { - const mockPush = vi.fn(); - const mockRouter = { - push: mockPush, - replace: vi.fn(), - refresh: vi.fn(), - back: vi.fn(), - forward: vi.fn(), - prefetch: vi.fn(), - }; - - const mockOrganization1: TOrganization = { - id: "org-1", - name: "Test Organization 1", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-01"), - billing: { - plan: "free", - stripeCustomerId: null, - } as unknown as TOrganizationBilling, - isAIEnabled: false, - }; - - const mockOrganization2: TOrganization = { - id: "org-2", - name: "Test Organization 2", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-01"), - billing: { - plan: "startup", - stripeCustomerId: null, - } as unknown as TOrganizationBilling, - isAIEnabled: true, - }; - - const mockOrganizations = [mockOrganization1, mockOrganization2]; - const currentEnvironmentId = "env-123"; - - beforeEach(() => { - vi.mocked(useRouter).mockReturnValue(mockRouter as any); - vi.mocked(usePathname).mockReturnValue("/environments/env-123/"); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - describe("Single Organization Setup", () => { - test("renders organization breadcrumb without dropdown for single org", () => { - render( - - ); - - expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument(); - expect(screen.getByTestId("building-icon")).toBeInTheDocument(); - expect(screen.getByText("Test Organization 1")).toBeInTheDocument(); - }); - - test("shows organization settings without organization switcher", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - expect(screen.getByTestId("dropdown-content")).toBeInTheDocument(); - expect(screen.getByText("common.organization_settings")).toBeInTheDocument(); - expect(screen.queryByText("common.choose_organization")).not.toBeInTheDocument(); - }); - }); - - describe("Multi Organization Setup", () => { - test("renders organization breadcrumb with dropdown for multi org", () => { - render( - - ); - - expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument(); - expect(screen.getByTestId("building-icon")).toBeInTheDocument(); - expect(screen.getAllByText("Test Organization 1")).toHaveLength(2); // trigger + dropdown option - }); - - test("shows chevron icons correctly", () => { - render( - - ); - - // Should show chevron right when closed - expect(screen.getByTestId("chevron-right-icon")).toBeInTheDocument(); - expect(screen.queryByTestId("chevron-down-icon")).not.toBeInTheDocument(); - }); - - test("shows chevron down when dropdown is open", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - await waitFor(() => { - expect(screen.getByTestId("chevron-down-icon")).toBeInTheDocument(); - }); - }); - - test("renders organization selector in dropdown", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - expect(screen.getByText("common.choose_organization")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-group")).toBeInTheDocument(); - - const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item"); - expect(checkboxItems.length).toBeGreaterThanOrEqual(2); // Organizations + create new option + settings - }); - - test("handles organization change when clicking dropdown option", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item"); - const org2Option = checkboxItems.find((item) => item.textContent?.includes("Test Organization 2")); - - expect(org2Option).toBeInTheDocument(); - await user.click(org2Option!); - - expect(mockPush).toHaveBeenCalledWith("/organizations/org-2/"); - }); - - test("shows create new organization option when multi org is enabled", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const createOrgOption = screen.getByText("common.create_new_organization"); - expect(createOrgOption).toBeInTheDocument(); - expect(screen.getByTestId("plus-icon")).toBeInTheDocument(); - }); - - test("opens create organization modal when clicking create new option", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const createOrgOption = screen.getByText("common.create_new_organization"); - await user.click(createOrgOption); - - expect(screen.getByTestId("create-organization-modal")).toBeInTheDocument(); - }); - - test("hides create new organization option when multi org is disabled", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - expect(screen.queryByText("common.create_new_organization")).not.toBeInTheDocument(); - }); - }); - - describe("Organization Settings", () => { - test("renders all organization settings options", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - expect(screen.getByText("common.organization_settings")).toBeInTheDocument(); - expect(screen.getByTestId("settings-icon")).toBeInTheDocument(); - expect(screen.getByText("common.general")).toBeInTheDocument(); - expect(screen.getByText("common.teams")).toBeInTheDocument(); - expect(screen.getByText("common.api_keys")).toBeInTheDocument(); - expect(screen.getByText("common.billing")).toBeInTheDocument(); - }); - - test("handles navigation to organization settings", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const generalOption = screen.getByText("common.general"); - await user.click(generalOption); - - expect(mockPush).toHaveBeenCalledWith(`/environments/${currentEnvironmentId}/settings/general`); - }); - - test("marks current settings page as checked", async () => { - vi.mocked(usePathname).mockReturnValue("/environments/env-123/settings/teams"); - - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item"); - const teamsOption = checkboxItems.find((item) => item.textContent?.includes("common.teams")); - - expect(teamsOption).toBeInTheDocument(); - expect(teamsOption).toHaveAttribute("data-checked", "true"); - }); - }); - - describe("Edge Cases", () => { - test("handles single organization with multi org enabled", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - // Should still show organization selector since multi org is enabled - expect(screen.getByText("common.choose_organization")).toBeInTheDocument(); - expect(screen.getByText("common.create_new_organization")).toBeInTheDocument(); - }); - - test("shows separator between organization switcher and settings", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - expect(screen.getByTestId("dropdown-separator")).toBeInTheDocument(); - }); - - test("sets breadcrumb item as active when dropdown is open", async () => { - const user = userEvent.setup(); - render( - - ); - - // Initially not active - let breadcrumbItem = screen.getByTestId("breadcrumb-item"); - expect(breadcrumbItem).toHaveAttribute("data-active", "false"); - - // Open dropdown - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - // Should be active when dropdown is open - breadcrumbItem = screen.getByTestId("breadcrumb-item"); - expect(breadcrumbItem).toHaveAttribute("data-active", "true"); - }); - - test("closes create organization modal correctly", async () => { - const user = userEvent.setup(); - render( - - ); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const createOrgOption = screen.getByText("common.create_new_organization"); - await user.click(createOrgOption); - - expect(screen.getByTestId("create-organization-modal")).toBeInTheDocument(); - - const closeButton = screen.getByText("Close Modal"); - await user.click(closeButton); - - expect(screen.queryByTestId("create-organization-modal")).not.toBeInTheDocument(); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/project-and-org-switch.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/project-and-org-switch.test.tsx deleted file mode 100644 index ca0019b5ee..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/project-and-org-switch.test.tsx +++ /dev/null @@ -1,340 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { ProjectAndOrgSwitch } from "./project-and-org-switch"; - -// Mock the individual breadcrumb components -vi.mock("@/app/(app)/environments/[environmentId]/components/organization-breadcrumb", () => ({ - OrganizationBreadcrumb: ({ - currentOrganizationId, - organizations, - isMultiOrgEnabled, - currentEnvironmentId, - }: any) => { - const currentOrganization = organizations.find((org: any) => org.id === currentOrganizationId); - return ( -
    -
    Organization: {currentOrganization?.name}
    -
    Organizations Count: {organizations.length}
    -
    Multi Org: {isMultiOrgEnabled ? "Enabled" : "Disabled"}
    -
    Environment ID: {currentEnvironmentId}
    -
    - ); - }, -})); - -vi.mock("@/app/(app)/environments/[environmentId]/components/project-breadcrumb", () => ({ - ProjectBreadcrumb: ({ - currentProjectId, - projects, - isOwnerOrManager, - organizationProjectsLimit, - isFormbricksCloud, - isLicenseActive, - currentOrganizationId, - currentEnvironmentId, - isAccessControlAllowed, - }: any) => { - const currentProject = projects.find((project: any) => project.id === currentProjectId); - return ( -
    -
    Project: {currentProject?.name}
    -
    Projects Count: {projects.length}
    -
    Owner/Manager: {isOwnerOrManager ? "Yes" : "No"}
    -
    Project Limit: {organizationProjectsLimit}
    -
    Formbricks Cloud: {isFormbricksCloud ? "Yes" : "No"}
    -
    License Active: {isLicenseActive ? "Yes" : "No"}
    -
    Organization ID: {currentOrganizationId}
    -
    Environment ID: {currentEnvironmentId}
    -
    Access Control: {isAccessControlAllowed ? "Allowed" : "Not Allowed"}
    -
    - ); - }, -})); - -vi.mock("@/app/(app)/environments/[environmentId]/components/environment-breadcrumb", () => ({ - EnvironmentBreadcrumb: ({ environments, currentEnvironmentId }: any) => { - const currentEnvironment = environments.find((env: any) => env.id === currentEnvironmentId); - return ( -
    -
    Environment: {currentEnvironment?.type}
    -
    Environments Count: {environments.length}
    -
    Environment ID: {currentEnvironment?.id}
    -
    - ); - }, -})); - -// Mock the UI components -vi.mock("@/modules/ui/components/breadcrumb", () => ({ - Breadcrumb: ({ children }: any) => ( - - ), - BreadcrumbList: ({ children, className }: any) => ( -
      - {children} -
    - ), -})); - -describe("ProjectAndOrgSwitch", () => { - const mockOrganization1 = { - id: "org-1", - name: "Test Organization 1", - }; - - const mockOrganization2 = { - id: "org-2", - name: "Test Organization 2", - }; - - const mockProject1 = { - id: "proj-1", - name: "Test Project 1", - }; - - const mockProject2 = { - id: "proj-2", - name: "Test Project 2", - }; - - const mockEnvironment1: TEnvironment = { - id: "env-1", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-01"), - type: "development", - projectId: "proj-1", - appSetupCompleted: true, - }; - - const mockEnvironment2: TEnvironment = { - id: "env-2", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-01"), - type: "development", - projectId: "proj-1", - appSetupCompleted: true, - }; - - const defaultProps = { - currentOrganizationId: "org-1", - organizations: [mockOrganization1, mockOrganization2], - currentProjectId: "proj-1", - projects: [mockProject1, mockProject2], - currentEnvironmentId: "env-1", - environments: [mockEnvironment1, mockEnvironment2], - isMultiOrgEnabled: true, - organizationProjectsLimit: 5, - isFormbricksCloud: true, - isLicenseActive: false, - isOwnerOrManager: true, - isAccessControlAllowed: true, - isMember: true, - }; - - afterEach(() => { - cleanup(); - }); - - describe("Basic Rendering", () => { - test("renders main breadcrumb structure", () => { - render(); - - expect(screen.getByTestId("breadcrumb")).toBeInTheDocument(); - expect(screen.getByTestId("breadcrumb-list")).toBeInTheDocument(); - expect(screen.getByTestId("breadcrumb")).toHaveAttribute("aria-label", "breadcrumb"); - }); - - test("applies correct CSS classes to breadcrumb list", () => { - render(); - - const breadcrumbList = screen.getByTestId("breadcrumb-list"); - expect(breadcrumbList).toHaveClass("gap-0"); - }); - - test("renders all three breadcrumb components", () => { - render(); - - expect(screen.getByTestId("organization-breadcrumb")).toBeInTheDocument(); - expect(screen.getByTestId("project-breadcrumb")).toBeInTheDocument(); - }); - }); - - describe("Organization Breadcrumb Integration", () => { - test("passes correct props to organization breadcrumb", () => { - render(); - - const orgBreadcrumb = screen.getByTestId("organization-breadcrumb"); - expect(orgBreadcrumb).toHaveTextContent("Organization: Test Organization 1"); - expect(orgBreadcrumb).toHaveTextContent("Organizations Count: 2"); - expect(orgBreadcrumb).toHaveTextContent("Multi Org: Enabled"); - expect(orgBreadcrumb).toHaveTextContent("Environment ID: env-1"); - }); - - test("handles single organization setup", () => { - render( - - ); - - const orgBreadcrumb = screen.getByTestId("organization-breadcrumb"); - expect(orgBreadcrumb).toHaveTextContent("Organizations Count: 1"); - expect(orgBreadcrumb).toHaveTextContent("Multi Org: Disabled"); - }); - }); - - describe("Project Breadcrumb Integration", () => { - test("passes correct props to project breadcrumb", () => { - render(); - - const projectBreadcrumb = screen.getByTestId("project-breadcrumb"); - expect(projectBreadcrumb).toHaveTextContent("Project: Test Project 1"); - expect(projectBreadcrumb).toHaveTextContent("Projects Count: 2"); - expect(projectBreadcrumb).toHaveTextContent("Owner/Manager: Yes"); - expect(projectBreadcrumb).toHaveTextContent("Project Limit: 5"); - expect(projectBreadcrumb).toHaveTextContent("Formbricks Cloud: Yes"); - expect(projectBreadcrumb).toHaveTextContent("License Active: No"); - expect(projectBreadcrumb).toHaveTextContent("Organization ID: org-1"); - expect(projectBreadcrumb).toHaveTextContent("Environment ID: env-1"); - expect(projectBreadcrumb).toHaveTextContent("Access Control: Allowed"); - }); - - test("handles non-owner/manager user", () => { - render(); - - const projectBreadcrumb = screen.getByTestId("project-breadcrumb"); - expect(projectBreadcrumb).toHaveTextContent("Owner/Manager: No"); - }); - - test("handles self-hosted setup", () => { - render(); - - const projectBreadcrumb = screen.getByTestId("project-breadcrumb"); - expect(projectBreadcrumb).toHaveTextContent("Formbricks Cloud: No"); - expect(projectBreadcrumb).toHaveTextContent("License Active: Yes"); - }); - - test("handles access control restrictions", () => { - render(); - - const projectBreadcrumb = screen.getByTestId("project-breadcrumb"); - expect(projectBreadcrumb).toHaveTextContent("Access Control: Not Allowed"); - }); - }); - - describe("Environment Breadcrumb Integration", () => { - test("passes correct props to environment breadcrumb", () => { - render(); - - const envBreadcrumb = screen.getByTestId("environment-breadcrumb"); - expect(envBreadcrumb).toHaveTextContent("Environments Count: 2"); - }); - - test("handles single environment", () => { - render(); - - const envBreadcrumb = screen.getByTestId("environment-breadcrumb"); - expect(envBreadcrumb).toHaveTextContent("Environments Count: 1"); - }); - }); - - describe("Props Propagation", () => { - test("correctly propagates organization limits", () => { - render(); - - const projectBreadcrumb = screen.getByTestId("project-breadcrumb"); - expect(projectBreadcrumb).toHaveTextContent("Project Limit: 10"); - }); - - test("correctly propagates current organization to project breadcrumb", () => { - render(); - - const orgBreadcrumb = screen.getByTestId("organization-breadcrumb"); - const projectBreadcrumb = screen.getByTestId("project-breadcrumb"); - - expect(orgBreadcrumb).toHaveTextContent("Organization: Test Organization 2"); - expect(projectBreadcrumb).toHaveTextContent("Organization ID: org-2"); - }); - }); - - describe("Edge Cases", () => { - test("handles zero project limit", () => { - render(); - - const projectBreadcrumb = screen.getByTestId("project-breadcrumb"); - expect(projectBreadcrumb).toHaveTextContent("Project Limit: 0"); - }); - - test("handles all boolean props as false", () => { - render( - - ); - - const orgBreadcrumb = screen.getByTestId("organization-breadcrumb"); - const projectBreadcrumb = screen.getByTestId("project-breadcrumb"); - - expect(orgBreadcrumb).toHaveTextContent("Multi Org: Disabled"); - expect(projectBreadcrumb).toHaveTextContent("Owner/Manager: No"); - expect(projectBreadcrumb).toHaveTextContent("Formbricks Cloud: No"); - expect(projectBreadcrumb).toHaveTextContent("License Active: No"); - expect(projectBreadcrumb).toHaveTextContent("Access Control: Not Allowed"); - }); - - test("maintains component order in DOM", () => { - render(); - - const breadcrumbList = screen.getByTestId("breadcrumb-list"); - const children = Array.from(breadcrumbList.children); - - expect(children[0]).toHaveAttribute("data-testid", "organization-breadcrumb"); - expect(children[1]).toHaveAttribute("data-testid", "project-breadcrumb"); - expect(children[2]).toHaveAttribute("data-testid", "environment-breadcrumb"); - }); - }); - - describe("TypeScript Props Interface", () => { - test("accepts all required props without error", () => { - // This test ensures the component accepts the full interface - expect(() => { - render(); - }).not.toThrow(); - }); - - test("works with minimal valid props", () => { - const minimalProps = { - currentOrganizationId: "org-1", - organizations: [mockOrganization1], - currentProjectId: "proj-1", - projects: [mockProject1], - currentEnvironmentId: "env-1", - environments: [mockEnvironment1], - isMultiOrgEnabled: false, - organizationProjectsLimit: 1, - isFormbricksCloud: false, - isLicenseActive: false, - isOwnerOrManager: false, - isAccessControlAllowed: false, - isMember: true, - }; - - expect(() => { - render(); - }).not.toThrow(); - - expect(screen.getByTestId("breadcrumb")).toBeInTheDocument(); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/project-breadcrumb.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/project-breadcrumb.test.tsx deleted file mode 100644 index 0df76dac8a..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/components/project-breadcrumb.test.tsx +++ /dev/null @@ -1,512 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { useRouter } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations"; -import { TProject } from "@formbricks/types/project"; -import { ProjectBreadcrumb } from "./project-breadcrumb"; - -// Mock the dependencies -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), - usePathname: vi.fn(() => "/environments/env-123/project/general"), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("@/modules/projects/components/project-limit-modal", () => ({ - ProjectLimitModal: ({ open, setOpen, buttons, projectLimit }: any) => - open ? ( -
    -
    Project Limit: {projectLimit}
    - - {buttons.map((button: any) => ( - - ))} -
    - ) : null, -})); - -vi.mock("@/modules/projects/components/create-project-modal", () => ({ - CreateProjectModal: ({ open, setOpen, organizationId, isAccessControlAllowed }: any) => - open ? ( -
    -
    Organization: {organizationId}
    -
    Access Control: {isAccessControlAllowed ? "Allowed" : "Not Allowed"}
    - -
    - ) : null, -})); - -// Mock the UI components -vi.mock("@/modules/ui/components/breadcrumb", () => ({ - BreadcrumbItem: ({ children, isActive, ...props }: any) => ( -
  • - {children} -
  • - ), -})); - -vi.mock("@/modules/ui/components/dropdown-menu", () => ({ - DropdownMenu: ({ children, onOpenChange }: any) => ( - - ), - DropdownMenuContent: ({ children, ...props }: any) => ( -
    - {children} -
    - ), - DropdownMenuCheckboxItem: ({ children, onClick, checked, ...props }: any) => ( -
    e.key === "Enter" && onClick?.()} - role="menuitemcheckbox" - aria-checked={checked} - tabIndex={0} - {...props}> - {children} -
    - ), - DropdownMenuTrigger: ({ children, ...props }: any) => ( - - ), - DropdownMenuGroup: ({ children }: any) =>
    {children}
    , - DropdownMenuSeparator: () =>
    , -})); - -// Mock Lucide React icons -vi.mock("lucide-react", () => ({ - FolderOpenIcon: ({ className, strokeWidth }: any) => { - const isHeader = className?.includes("mr-2"); - return ( - - FolderOpen Icon - - ); - }, - ChevronDownIcon: ({ className, strokeWidth }: any) => ( - - ChevronDown Icon - - ), - ChevronRightIcon: ({ className, strokeWidth }: any) => ( - - ChevronRight Icon - - ), - PlusIcon: ({ className }: any) => ( - - Plus Icon - - ), - Loader2: ({ className }: any) => ( - - Loader2 Icon - - ), - CogIcon: ({ className }: any) => ( - - Cog Icon - - ), - SettingsIcon: ({ className }: any) => ( - - Settings Icon - - ), -})); - -describe("ProjectBreadcrumb", () => { - const mockPush = vi.fn(); - const mockRouter = { - push: mockPush, - replace: vi.fn(), - refresh: vi.fn(), - back: vi.fn(), - forward: vi.fn(), - prefetch: vi.fn(), - }; - - const mockProject1 = { - id: "proj-1", - name: "Test Project 1", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-01"), - organizationId: "org-1", - languages: [], - } as unknown as TProject; - - const mockProject2 = { - id: "proj-2", - name: "Test Project 2", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-01"), - organizationId: "org-1", - languages: [], - } as unknown as TProject; - - const mockProjects = [mockProject1, mockProject2]; - - const mockOrganization: TOrganization = { - id: "org-1", - name: "Test Organization", - createdAt: new Date("2023-01-01"), - updatedAt: new Date("2023-01-01"), - billing: { - plan: "free", - stripeCustomerId: null, - } as unknown as TOrganizationBilling, - isAIEnabled: false, - }; - - const defaultProps = { - currentProjectId: "proj-1", - currentOrganizationId: "org-1", - projects: mockProjects, - isOwnerOrManager: true, - organizationProjectsLimit: 3, - isFormbricksCloud: true, - isLicenseActive: false, - currentEnvironmentId: "env-123", - isAccessControlAllowed: true, - isEnvironmentBreadcrumbVisible: true, - }; - - beforeEach(() => { - vi.mocked(useRouter).mockReturnValue(mockRouter as any); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - describe("Basic Rendering", () => { - test("renders project breadcrumb correctly", () => { - render(); - - expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument(); - expect(screen.getByTestId("folder-open-icon")).toBeInTheDocument(); - expect(screen.getAllByText("Test Project 1")).toHaveLength(2); // trigger + dropdown option - }); - - test("shows chevron icons correctly", () => { - render(); - - // Should show chevron right when closed - expect(screen.getByTestId("chevron-right-icon")).toBeInTheDocument(); - expect(screen.queryByTestId("chevron-down-icon")).not.toBeInTheDocument(); - }); - - test("shows chevron down when dropdown is open", async () => { - const user = userEvent.setup(); - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - await waitFor(() => { - expect(screen.getByTestId("chevron-down-icon")).toBeInTheDocument(); - }); - }); - }); - - describe("Project Selection", () => { - test("renders dropdown content with project options", async () => { - const user = userEvent.setup(); - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - expect(screen.getByTestId("dropdown-content")).toBeInTheDocument(); - expect(screen.getByText("common.choose_project")).toBeInTheDocument(); - expect(screen.getAllByTestId("dropdown-group")).toHaveLength(2); // Projects group and settings group - }); - - test("renders all project options in dropdown", async () => { - const user = userEvent.setup(); - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item"); - - // Find project options (excluding the add new project option) - const projectOptions = checkboxItems.filter((item) => item.textContent?.includes("Test Project")); - expect(projectOptions).toHaveLength(2); - - // Check current project is marked as selected - const currentProjectOption = checkboxItems.find((item) => item.textContent?.includes("Test Project 1")); - expect(currentProjectOption).toHaveAttribute("data-checked", "true"); - - // Check other project is not selected - const otherProjectOption = checkboxItems.find((item) => item.textContent?.includes("Test Project 2")); - expect(otherProjectOption).toHaveAttribute("data-checked", "false"); - }); - - test("handles project change when clicking dropdown option", async () => { - const user = userEvent.setup(); - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item"); - const project2Option = checkboxItems.find((item) => item.textContent?.includes("Test Project 2")); - - expect(project2Option).toBeInTheDocument(); - await user.click(project2Option!); - - expect(mockPush).toHaveBeenCalledWith("/projects/proj-2/"); - }); - }); - - describe("Add New Project", () => { - test("shows add new project option when user is owner or manager", async () => { - const user = userEvent.setup(); - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - expect(screen.getByText("common.add_new_project")).toBeInTheDocument(); - expect(screen.getByTestId("plus-icon")).toBeInTheDocument(); - }); - - test("hides add new project option when user is not owner or manager", async () => { - const user = userEvent.setup(); - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - expect(screen.queryByText("common.add_new_project")).not.toBeInTheDocument(); - }); - - test("opens create project modal when within project limit", async () => { - const user = userEvent.setup(); - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const addProjectOption = screen.getByText("common.add_new_project"); - await user.click(addProjectOption); - - expect(screen.getByTestId("create-project-modal")).toBeInTheDocument(); - expect(screen.getByText("Organization: org-1")).toBeInTheDocument(); - expect(screen.getByText("Access Control: Allowed")).toBeInTheDocument(); - }); - - test("opens limit modal when exceeding project limit", async () => { - const user = userEvent.setup(); - const props = { - ...defaultProps, - projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }], - organizationProjectsLimit: 3, - }; - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const addProjectOption = screen.getByText("common.add_new_project"); - await user.click(addProjectOption); - - expect(screen.getByTestId("project-limit-modal")).toBeInTheDocument(); - expect(screen.getByText("Project Limit: 3")).toBeInTheDocument(); - }); - }); - - describe("Project Limit Modal", () => { - test("shows correct buttons for Formbricks Cloud with non-enterprise plan", async () => { - const user = userEvent.setup(); - const props = { - ...defaultProps, - projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }], - organizationProjectsLimit: 3, - isFormbricksCloud: true, - isEnvironmentBreadcrumbVisible: true, - currentOrganization: { - ...mockOrganization, - billing: { ...mockOrganization.billing, plan: "startup" } as unknown as TOrganizationBilling, - }, - }; - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const addProjectOption = screen.getByText("common.add_new_project"); - await user.click(addProjectOption); - - expect(screen.getByText("environments.settings.billing.upgrade")).toBeInTheDocument(); - expect(screen.getByText("common.cancel")).toBeInTheDocument(); - }); - - test("shows correct buttons for self-hosted with active license", async () => { - const user = userEvent.setup(); - const props = { - ...defaultProps, - projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }], - organizationProjectsLimit: 3, - isFormbricksCloud: false, - isLicenseActive: true, - isEnvironmentBreadcrumbVisible: true, - }; - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const addProjectOption = screen.getByText("common.add_new_project"); - await user.click(addProjectOption); - - expect(screen.getByText("environments.settings.billing.upgrade")).toBeInTheDocument(); - expect(screen.getByText("common.cancel")).toBeInTheDocument(); - }); - - test("closes limit modal correctly", async () => { - const user = userEvent.setup(); - const props = { - ...defaultProps, - projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }], - organizationProjectsLimit: 3, - }; - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const addProjectOption = screen.getByText("common.add_new_project"); - await user.click(addProjectOption); - - expect(screen.getByTestId("project-limit-modal")).toBeInTheDocument(); - - const closeButton = screen.getByText("Close Limit Modal"); - await user.click(closeButton); - - expect(screen.queryByTestId("project-limit-modal")).not.toBeInTheDocument(); - }); - }); - - describe("Create Project Modal", () => { - test("closes create project modal correctly", async () => { - const user = userEvent.setup(); - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const addProjectOption = screen.getByText("common.add_new_project"); - await user.click(addProjectOption); - - expect(screen.getByTestId("create-project-modal")).toBeInTheDocument(); - - const closeButton = screen.getByText("Close Create Modal"); - await user.click(closeButton); - - expect(screen.queryByTestId("create-project-modal")).not.toBeInTheDocument(); - }); - - test("passes correct props to create project modal", async () => { - const user = userEvent.setup(); - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const addProjectOption = screen.getByText("common.add_new_project"); - await user.click(addProjectOption); - - expect(screen.getByText("Access Control: Not Allowed")).toBeInTheDocument(); - }); - }); - - describe("Edge Cases", () => { - test("handles single project scenario", () => { - render(); - - expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument(); - expect(screen.getAllByText("Test Project 1")).toHaveLength(2); // trigger + dropdown option - }); - - test("sets breadcrumb item as active when dropdown is open", async () => { - const user = userEvent.setup(); - render(); - - // Initially not active - let breadcrumbItem = screen.getByTestId("breadcrumb-item"); - expect(breadcrumbItem).toHaveAttribute("data-active", "false"); - - // Open dropdown - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - // Should be active when dropdown is open - breadcrumbItem = screen.getByTestId("breadcrumb-item"); - expect(breadcrumbItem).toHaveAttribute("data-active", "true"); - }); - - test("handles project limit of zero", async () => { - const user = userEvent.setup(); - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const addProjectOption = screen.getByText("common.add_new_project"); - await user.click(addProjectOption); - - // Should show limit modal even with 0 projects when limit is 0 - expect(screen.getByTestId("project-limit-modal")).toBeInTheDocument(); - expect(screen.getByText("Project Limit: 0")).toBeInTheDocument(); - }); - - test("handles enterprise plan on Formbricks Cloud", async () => { - const user = userEvent.setup(); - const props = { - ...defaultProps, - projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }], - organizationProjectsLimit: 3, - currentOrganization: { - ...mockOrganization, - billing: { ...mockOrganization.billing, plan: "enterprise" } as unknown as TOrganizationBilling, - }, - }; - render(); - - const dropdownMenu = screen.getByTestId("dropdown-menu"); - await user.click(dropdownMenu); - - const addProjectOption = screen.getByText("common.add_new_project"); - await user.click(addProjectOption); - - // Should show self-hosted style buttons for enterprise plan - expect(screen.getByTestId("project-limit-modal")).toBeInTheDocument(); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/context/environment-context.test.tsx b/apps/web/app/(app)/environments/[environmentId]/context/environment-context.test.tsx deleted file mode 100644 index 430a414d71..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/context/environment-context.test.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TProject } from "@formbricks/types/project"; -import { EnvironmentContextWrapper, useEnvironment } from "./environment-context"; - -// Mock environment data -const mockEnvironment: TEnvironment = { - id: "test-env-id", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - projectId: "test-project-id", - appSetupCompleted: true, -}; - -// Mock project data -const mockProject = { - id: "test-project-id", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "test-org-id", - config: { - channel: "app", - industry: "saas", - }, - linkSurveyBranding: true, - styling: { - allowStyleOverwrite: true, - brandColor: { - light: "#ffffff", - dark: "#000000", - }, - questionColor: { - light: "#000000", - dark: "#ffffff", - }, - inputColor: { - light: "#000000", - dark: "#ffffff", - }, - inputBorderColor: { - light: "#cccccc", - dark: "#444444", - }, - cardBackgroundColor: { - light: "#ffffff", - dark: "#000000", - }, - cardBorderColor: { - light: "#cccccc", - dark: "#444444", - }, - isDarkModeEnabled: false, - isLogoHidden: false, - hideProgressBar: false, - roundness: 8, - cardArrangement: { - linkSurveys: "casual", - appSurveys: "casual", - }, - }, - recontactDays: 30, - inAppSurveyBranding: true, - logo: { - url: "test-logo.png", - bgColor: "#ffffff", - }, - placement: "bottomRight", - clickOutsideClose: true, -} as TProject; - -// Test component that uses the hook -const TestComponent = () => { - const { environment, project } = useEnvironment(); - return ( -
    -
    {environment.id}
    -
    {environment.type}
    -
    {project.id}
    -
    {project.organizationId}
    -
    - ); -}; - -describe("EnvironmentContext", () => { - afterEach(() => { - cleanup(); - }); - - test("provides environment and project data to child components", () => { - render( - - - - ); - - expect(screen.getByTestId("environment-id")).toHaveTextContent("test-env-id"); - expect(screen.getByTestId("environment-type")).toHaveTextContent("development"); - expect(screen.getByTestId("project-id")).toHaveTextContent("test-project-id"); - expect(screen.getByTestId("project-organization-id")).toHaveTextContent("test-org-id"); - }); - - test("throws error when useEnvironment is used outside of provider", () => { - const TestComponentWithoutProvider = () => { - useEnvironment(); - return
    Should not render
    ; - }; - - expect(() => { - render(); - }).toThrow("useEnvironment must be used within an EnvironmentProvider"); - }); - - test("updates context value when environment or project changes", () => { - const { rerender } = render( - - - - ); - - expect(screen.getByTestId("environment-type")).toHaveTextContent("development"); - - const updatedEnvironment = { - ...mockEnvironment, - type: "production" as const, - }; - - rerender( - - - - ); - - expect(screen.getByTestId("environment-type")).toHaveTextContent("production"); - }); - - test("memoizes context value correctly", () => { - const { rerender } = render( - - - - ); - - // Re-render with same props - rerender( - - - - ); - - // Should still work correctly - expect(screen.getByTestId("environment-id")).toHaveTextContent("test-env-id"); - expect(screen.getByTestId("project-id")).toHaveTextContent("test-project-id"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/layout.test.tsx b/apps/web/app/(app)/environments/[environmentId]/layout.test.tsx deleted file mode 100644 index 78a3c646af..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/layout.test.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { Session } from "next-auth"; -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TMembership } from "@formbricks/types/memberships"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TProject } from "@formbricks/types/project"; -import { TUser } from "@formbricks/types/user"; -import { getEnvironment } from "@/lib/environment/service"; -import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; -import { getProjectByEnvironmentId } from "@/lib/project/service"; -import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils"; -import EnvLayout from "./layout"; - -// Mock sub-components to render identifiable elements -vi.mock("@/app/(app)/environments/[environmentId]/components/EnvironmentLayout", () => ({ - EnvironmentLayout: ({ children, environmentId, session }: any) => ( -
    - {children} -
    - ), -})); -vi.mock("@/modules/ui/components/environmentId-base-layout", () => ({ - EnvironmentIdBaseLayout: ({ children, environmentId, session, user, organization }: any) => ( -
    - {children} -
    - ), -})); -vi.mock("@/modules/ui/components/toaster-client", () => ({ - ToasterClient: () =>
    , -})); -vi.mock("./components/EnvironmentStorageHandler", () => ({ - default: ({ environmentId }: any) => ( -
    - ), -})); -vi.mock("@/app/(app)/environments/[environmentId]/context/environment-context", () => ({ - EnvironmentContextWrapper: ({ children, environment, project }: any) => ( -
    - {children} -
    - ), -})); - -// Mock navigation -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -// Mocks for dependencies -vi.mock("@/modules/environments/lib/utils", () => ({ - environmentIdLayoutChecks: vi.fn(), -})); -vi.mock("@/lib/project/service", () => ({ - getProjectByEnvironmentId: vi.fn(), -})); -vi.mock("@/lib/environment/service", () => ({ - getEnvironment: vi.fn(), -})); -vi.mock("@/lib/membership/service", () => ({ - getMembershipByUserIdOrganizationId: vi.fn(), -})); - -describe("EnvLayout", () => { - const mockSession = { user: { id: "user1" } } as Session; - const mockUser = { id: "user1", email: "user1@example.com" } as TUser; - const mockOrganization = { id: "org1", name: "Org1", billing: {} } as TOrganization; - const mockProject = { id: "proj1", name: "Test Project" } as TProject; - const mockEnvironment = { id: "env1", type: "production" } as TEnvironment; - const mockMembership = { - id: "member1", - role: "owner", - organizationId: "org1", - userId: "user1", - accepted: true, - } as TMembership; - const mockTranslation = ((key: string) => key) as any; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders successfully when all dependencies return valid data", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: mockTranslation, - session: mockSession, - user: mockUser, - organization: mockOrganization, - }); - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject); - vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(mockMembership); - - const result = await EnvLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
    Content
    , - }); - render(result); - - // Verify main layout structure - expect(screen.getByTestId("EnvironmentIdBaseLayout")).toBeInTheDocument(); - expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-environment-id", "env1"); - expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-session", "user1"); - expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-user", "user1"); - expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-organization", "org1"); - - // Verify environment storage handler - expect(screen.getByTestId("EnvironmentStorageHandler")).toBeInTheDocument(); - expect(screen.getByTestId("EnvironmentStorageHandler")).toHaveAttribute("data-environment-id", "env1"); - - // Verify context wrapper - expect(screen.getByTestId("EnvironmentContextWrapper")).toBeInTheDocument(); - expect(screen.getByTestId("EnvironmentContextWrapper")).toHaveAttribute("data-environment-id", "env1"); - expect(screen.getByTestId("EnvironmentContextWrapper")).toHaveAttribute("data-project-id", "proj1"); - - // Verify environment layout - expect(screen.getByTestId("EnvironmentLayout")).toBeInTheDocument(); - expect(screen.getByTestId("EnvironmentLayout")).toHaveAttribute("data-environment-id", "env1"); - expect(screen.getByTestId("EnvironmentLayout")).toHaveAttribute("data-session", "user1"); - - // Verify children are rendered - expect(screen.getByTestId("child")).toHaveTextContent("Content"); - - // Verify all services were called with correct parameters - expect(environmentIdLayoutChecks).toHaveBeenCalledWith("env1"); - expect(getProjectByEnvironmentId).toHaveBeenCalledWith("env1"); - expect(getEnvironment).toHaveBeenCalledWith("env1"); - expect(getMembershipByUserIdOrganizationId).toHaveBeenCalledWith("user1", "org1"); - }); - - test("redirects when session is null", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: mockTranslation, - session: null as unknown as Session, - user: mockUser, - organization: mockOrganization, - }); - vi.mocked(redirect).mockImplementationOnce(() => { - throw new Error("Redirect called"); - }); - - await expect( - EnvLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
    Content
    , - }) - ).rejects.toThrow("Redirect called"); - - expect(redirect).toHaveBeenCalledWith("/auth/login"); - }); - - test("throws error if user is null", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: mockTranslation, - session: mockSession, - user: null as unknown as TUser, - organization: mockOrganization, - }); - - await expect( - EnvLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
    Content
    , - }) - ).rejects.toThrow("common.user_not_found"); - - // Verify redirect was not called - expect(redirect).not.toHaveBeenCalled(); - }); - - test("throws error if project is not found", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: mockTranslation, - session: mockSession, - user: mockUser, - organization: mockOrganization, - }); - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(null); - vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment); - - await expect( - EnvLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
    Content
    , - }) - ).rejects.toThrow("common.project_not_found"); - - // Verify both project and environment were called in Promise.all - expect(getProjectByEnvironmentId).toHaveBeenCalledWith("env1"); - expect(getEnvironment).toHaveBeenCalledWith("env1"); - }); - - test("throws error if environment is not found", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: mockTranslation, - session: mockSession, - user: mockUser, - organization: mockOrganization, - }); - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject); - vi.mocked(getEnvironment).mockResolvedValueOnce(null); - - await expect( - EnvLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
    Content
    , - }) - ).rejects.toThrow("common.environment_not_found"); - - // Verify both project and environment were called in Promise.all - expect(getProjectByEnvironmentId).toHaveBeenCalledWith("env1"); - expect(getEnvironment).toHaveBeenCalledWith("env1"); - }); - - test("throws error if membership is not found", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: mockTranslation, - session: mockSession, - user: mockUser, - organization: mockOrganization, - }); - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject); - vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(null); - - await expect( - EnvLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
    Content
    , - }) - ).rejects.toThrow("common.membership_not_found"); - - expect(getMembershipByUserIdOrganizationId).toHaveBeenCalledWith("user1", "org1"); - }); - - test("handles Promise.all correctly for project and environment", async () => { - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: mockTranslation, - session: mockSession, - user: mockUser, - organization: mockOrganization, - }); - - // Mock Promise.all to verify it's called correctly - const getProjectSpy = vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject); - const getEnvironmentSpy = vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(mockMembership); - - const result = await EnvLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
    Content
    , - }); - render(result); - - // Verify both calls were made - expect(getProjectSpy).toHaveBeenCalledWith("env1"); - expect(getEnvironmentSpy).toHaveBeenCalledWith("env1"); - - // Verify successful rendering - expect(screen.getByTestId("child")).toBeInTheDocument(); - }); - - test("handles different environment types correctly", async () => { - const developmentEnvironment = { id: "env1", type: "development" } as TEnvironment; - - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: mockTranslation, - session: mockSession, - user: mockUser, - organization: mockOrganization, - }); - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject); - vi.mocked(getEnvironment).mockResolvedValueOnce(developmentEnvironment); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(mockMembership); - - const result = await EnvLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
    Content
    , - }); - render(result); - - // Verify context wrapper receives the development environment - expect(screen.getByTestId("EnvironmentContextWrapper")).toHaveAttribute("data-environment-id", "env1"); - expect(screen.getByTestId("child")).toBeInTheDocument(); - }); - - test("handles different user roles correctly", async () => { - const memberMembership = { - id: "member1", - role: "member", - organizationId: "org1", - userId: "user1", - accepted: true, - } as TMembership; - - vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ - t: mockTranslation, - session: mockSession, - user: mockUser, - organization: mockOrganization, - }); - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject); - vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(memberMembership); - - const result = await EnvLayout({ - params: Promise.resolve({ environmentId: "env1" }), - children:
    Content
    , - }); - render(result); - - // Verify successful rendering with member role - expect(screen.getByTestId("child")).toBeInTheDocument(); - expect(getMembershipByUserIdOrganizationId).toHaveBeenCalledWith("user1", "org1"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/page.test.tsx deleted file mode 100644 index 841f7f1b1c..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/page.test.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TMembership } from "@formbricks/types/memberships"; -import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations"; -import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; -import { getAccessFlags } from "@/lib/membership/utils"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import EnvironmentPage from "./page"; - -vi.mock("@/lib/membership/service", () => ({ - getMembershipByUserIdOrganizationId: vi.fn(), -})); - -vi.mock("@/lib/membership/utils", () => ({ - getAccessFlags: vi.fn(), -})); - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: true, -})); - -describe("EnvironmentPage", () => { - afterEach(() => { - vi.clearAllMocks(); - }); - - const mockEnvironmentId = "test-environment-id"; - const mockUserId = "test-user-id"; - const mockOrganizationId = "test-organization-id"; - - const mockSession = { - user: { - id: mockUserId, - name: "Test User", - email: "test@example.com", - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - emailVerified: new Date(), - role: "user", - objective: "other", - }, - expires: new Date(Date.now() + 3600 * 1000).toISOString(), // 1 hour from now - } as any; - - const mockOrganization: TOrganization = { - id: mockOrganizationId, - name: "Test Organization", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - stripeCustomerId: "cus_123", - } as unknown as TOrganizationBilling, - } as unknown as TOrganization; - - test("should redirect to billing settings if isBilling is true", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: mockSession, - organization: mockOrganization, - environment: { id: mockEnvironmentId, type: "production", product: { id: "prodId" } }, - } as any); // Using 'any' for brevity as environment type is complex and not core to this test - - const mockMembership: TMembership = { - userId: mockUserId, - organizationId: mockOrganizationId, - role: "owner" as any, - accepted: true, - }; - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - vi.mocked(getAccessFlags).mockReturnValue({ isBilling: true, isOwner: true } as any); - - await EnvironmentPage({ params: { environmentId: mockEnvironmentId } }); - - expect(redirect).toHaveBeenCalledWith(`/environments/${mockEnvironmentId}/settings/billing`); - }); - - test("should redirect to surveys if isBilling is false", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: mockSession, - organization: mockOrganization, - environment: { id: mockEnvironmentId, type: "production", product: { id: "prodId" } }, - } as any); - - const mockMembership: TMembership = { - userId: mockUserId, - organizationId: mockOrganizationId, - role: "developer" as any, // Role that would result in isBilling: false - accepted: true, - }; - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - vi.mocked(getAccessFlags).mockReturnValue({ isBilling: false, isOwner: false } as any); - - await EnvironmentPage({ params: { environmentId: mockEnvironmentId } }); - - expect(redirect).toHaveBeenCalledWith(`/environments/${mockEnvironmentId}/surveys`); - }); - - test("should handle session being null", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: null, // Simulate no active session - organization: mockOrganization, - environment: { id: mockEnvironmentId, type: "production", product: { id: "prodId" } }, - } as any); - - // Membership fetch might return null or throw, depending on implementation when userId is undefined - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null); - // Access flags would likely be all false if membership is null - vi.mocked(getAccessFlags).mockReturnValue({ isBilling: false, isOwner: false } as any); - - await EnvironmentPage({ params: { environmentId: mockEnvironmentId } }); - - // Expect redirect to surveys as default when isBilling is false - expect(redirect).toHaveBeenCalledWith(`/environments/${mockEnvironmentId}/surveys`); - }); - - test("should handle currentUserMembership being null", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: mockSession, - organization: mockOrganization, - environment: { id: mockEnvironmentId, type: "production", product: { id: "prodId" } }, - } as any); - - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null); // Simulate no membership found - // Access flags would likely be all false if membership is null - vi.mocked(getAccessFlags).mockReturnValue({ isBilling: false, isOwner: false } as any); - - await EnvironmentPage({ params: { environmentId: mockEnvironmentId } }); - - // Expect redirect to surveys as default when isBilling is false - expect(redirect).toHaveBeenCalledWith(`/environments/${mockEnvironmentId}/surveys`); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/loading.test.tsx deleted file mode 100644 index e67fca7b86..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/loading.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { AppConnectionLoading as OriginalAppConnectionLoading } from "@/modules/projects/settings/(setup)/app-connection/loading"; -import AppConnectionLoading from "./loading"; - -// Mock the original component to ensure we are testing the re-export -vi.mock("@/modules/projects/settings/(setup)/app-connection/loading", () => ({ - AppConnectionLoading: () =>
    Mock AppConnectionLoading
    , -})); - -describe("AppConnectionLoading Re-export", () => { - test("should re-export AppConnectionLoading from the correct module", () => { - // Check if the re-exported component is the same as the original (mocked) component - expect(AppConnectionLoading).toBe(OriginalAppConnectionLoading); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/page.test.tsx deleted file mode 100644 index 4425d274b6..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/page.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { AppConnectionPage as OriginalAppConnectionPage } from "@/modules/projects/settings/(setup)/app-connection/page"; -import AppConnectionPage from "./page"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -vi.mock("@/lib/env", () => ({ - env: { - PUBLIC_URL: "https://example.com", - }, -})); - -describe("AppConnectionPage Re-export", () => { - test("should re-export AppConnectionPage correctly", () => { - expect(AppConnectionPage).toBe(OriginalAppConnectionPage); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/general/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/general/loading.test.tsx deleted file mode 100644 index 20636867b8..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/general/loading.test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { GeneralSettingsLoading as OriginalGeneralSettingsLoading } from "@/modules/projects/settings/general/loading"; -import GeneralSettingsLoadingPage from "./loading"; - -// Mock the original component to ensure we are testing the re-export -vi.mock("@/modules/projects/settings/general/loading", () => ({ - GeneralSettingsLoading: () => ( -
    Mock GeneralSettingsLoading
    - ), -})); - -describe("GeneralSettingsLoadingPage Re-export", () => { - test("should re-export GeneralSettingsLoading from the correct module", () => { - // Check if the re-exported component is the same as the original (mocked) component - expect(GeneralSettingsLoadingPage).toBe(OriginalGeneralSettingsLoading); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/general/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/general/page.test.tsx deleted file mode 100644 index 34801faeb2..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/general/page.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { GeneralSettingsPage } from "@/modules/projects/settings/general/page"; -import Page from "./page"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: 1, -})); - -vi.mock("@/lib/env", () => ({ - env: { - PUBLIC_URL: "https://public-domain.com", - }, -})); - -describe("GeneralSettingsPage re-export", () => { - test("should re-export GeneralSettingsPage component", () => { - expect(Page).toBe(GeneralSettingsPage); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AddIntegrationModal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AddIntegrationModal.test.tsx deleted file mode 100644 index 829a87543e..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AddIntegrationModal.test.tsx +++ /dev/null @@ -1,466 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { useRouter } from "next/navigation"; -import { toast } from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TIntegrationItem } from "@formbricks/types/integration"; -import { - TIntegrationAirtable, - TIntegrationAirtableConfigData, - TIntegrationAirtableCredential, - TIntegrationAirtableTables, -} from "@formbricks/types/integration/airtable"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions"; -import { fetchTables } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable"; -import { AddIntegrationModal } from "./AddIntegrationModal"; - -// Mock dependencies -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({ - createOrUpdateIntegrationAction: vi.fn(), -})); -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/BaseSelectDropdown", - () => ({ - BaseSelectDropdown: ({ control, airtableArray, fetchTable, defaultValue, setValue }) => ( -
    - - -
    - ), - }) -); -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable", () => ({ - fetchTables: vi.fn(), -})); -vi.mock("@/lib/i18n/utils", () => ({ - getLocalizedValue: (value, _locale) => value?.default || value || "", -})); -vi.mock("@/lib/utils/recall", () => ({ - replaceHeadlineRecall: (survey, _locale) => survey, -})); -vi.mock("@/modules/ui/components/additional-integration-settings", () => ({ - AdditionalIntegrationSettings: ({ - includeVariables, - setIncludeVariables, - includeHiddenFields, - setIncludeHiddenFields, - includeMetadata, - setIncludeMetadata, - includeCreatedAt, - setIncludeCreatedAt, - }) => ( -
    - setIncludeVariables(e.target.checked)} - /> - setIncludeHiddenFields(e.target.checked)} - /> - setIncludeMetadata(e.target.checked)} - /> - setIncludeCreatedAt(e.target.checked)} - /> -
    - ), -})); -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open, onOpenChange }: any) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children, ...props }: any) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: any) =>
    {children}
    , - DialogTitle: ({ children }: any) =>

    {children}

    , - DialogDescription: ({ children }: any) =>

    {children}

    , - DialogBody: ({ children }: any) =>
    {children}
    , - DialogFooter: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children }) =>
    {children}
    , - AlertTitle: ({ children }) =>
    {children}
    , - AlertDescription: ({ children }) =>
    {children}
    , -})); -vi.mock("next/image", () => ({ - // eslint-disable-next-line @next/next/no-img-element - default: (props) => test, -})); -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(() => ({ refresh: vi.fn() })), -})); - -// Mock the Select component used for Table and Survey selections -vi.mock("@/modules/ui/components/select", () => ({ - Select: ({ children }) => ( - // Render children, assuming Controller passes props to the Trigger/Value - // The actual select logic will be handled by the mocked Controller/field - // We need to simulate the structure expected by the Controller render prop -
    {children}
    - ), - SelectTrigger: ({ children, ...props }) =>
    {children}
    , // Mock Trigger - SelectValue: ({ placeholder }) => {placeholder || "Select..."}, // Mock Value display - SelectContent: ({ children }) =>
    {children}
    , // Mock Content wrapper - SelectItem: ({ children, value, ...props }) => ( - // Mock Item - crucial for userEvent.selectOptions if we were using a real select - // For Controller, the value change is handled by field.onChange directly -
    - {children} -
    - ), -})); - -// Mock react-hook-form Controller to render a simple select -vi.mock("react-hook-form", async () => { - const actual = await vi.importActual("react-hook-form"); - let fields = {}; - const mockReset = vi.fn((values) => { - fields = values || {}; // Reset fields, optionally with new values - }); - - return { - ...actual, - useForm: vi.fn((options) => { - fields = options?.defaultValues || {}; - const mockControlOnChange = (event) => { - if (event && event.target) { - fields[event.target.name] = event.target.value; - } - }; - return { - handleSubmit: (fn) => (e) => { - e?.preventDefault(); - fn(fields); - }, - control: { - _mockOnChange: mockControlOnChange, - // Add other necessary control properties if needed - register: vi.fn(), - unregister: vi.fn(), - getFieldState: vi.fn(() => ({ invalid: false, isDirty: false, isTouched: false, error: null })), - _names: { mount: new Set(), unMount: new Set(), array: new Set(), watch: new Set() }, - _options: {}, - _proxyFormState: { - isDirty: false, - isValidating: false, - dirtyFields: {}, - touchedFields: {}, - errors: {}, - }, - _formState: { isDirty: false, isValidating: false, dirtyFields: {}, touchedFields: {}, errors: {} }, - _updateFormState: vi.fn(), - _updateFieldArray: vi.fn(), - _executeSchema: vi.fn().mockResolvedValue({ errors: {}, values: {} }), - _getWatch: vi.fn(), - _subjects: { - watch: { subscribe: vi.fn() }, - array: { subscribe: vi.fn() }, - state: { subscribe: vi.fn() }, - }, - _getDirty: vi.fn(), - _reset: vi.fn(), - _removeUnmounted: vi.fn(), - }, - watch: (name) => fields[name], - setValue: (name, value) => { - fields[name] = value; - }, - reset: mockReset, - formState: { errors: {}, isDirty: false, isValid: true, isSubmitting: false }, - getValues: (name) => (name ? fields[name] : fields), - }; - }), - Controller: ({ name, defaultValue }) => { - // Initialize field value if not already set by reset/defaultValues - if (fields[name] === undefined && defaultValue !== undefined) { - fields[name] = defaultValue; - } - - const field = { - onChange: (valueOrEvent) => { - const value = valueOrEvent?.target ? valueOrEvent.target.value : valueOrEvent; - fields[name] = value; - // Re-render might be needed here in a real scenario, but testing library handles it - }, - onBlur: vi.fn(), - value: fields[name], - name: name, - ref: vi.fn(), - }; - - // Find the corresponding label to associate with the select - const labelId = name; // Assuming label 'for' matches field name - const labelText = - name === "table" ? "environments.integrations.airtable.table_name" : "common.select_survey"; - - // Render a simple select element instead of the complex component - // This makes interaction straightforward with userEvent.selectOptions - return ( - <> - {/* The actual label is rendered outside the Controller in the component */} - - - ); - }, - reset: mockReset, - }; -}); - -const environmentId = "test-env-id"; -const mockSurveys: TSurvey[] = [ - { - id: "survey1", - name: "Survey 1", - questions: [ - { id: "q1", headline: { default: "Question 1" } }, - { id: "q2", headline: { default: "Question 2" } }, - ], - hiddenFields: { enabled: true, fieldIds: ["hf1"] }, - variables: { enabled: true, fieldIds: ["var1"] }, - } as any, - { - id: "survey2", - name: "Survey 2", - questions: [{ id: "q3", headline: { default: "Question 3" } }], - hiddenFields: { enabled: false }, - variables: { enabled: false }, - } as any, -]; -const mockAirtableArray: TIntegrationItem[] = [ - { id: "base1", name: "Base 1" }, - { id: "base2", name: "Base 2" }, -]; -const mockAirtableIntegration: TIntegrationAirtable = { - id: "integration1", - type: "airtable", - environmentId, - config: { - key: { access_token: "abc" } as TIntegrationAirtableCredential, - email: "test@test.com", - data: [], - }, -}; -const mockTables: TIntegrationAirtableTables["tables"] = [ - { id: "table1", name: "Table 1" }, - { id: "table2", name: "Table 2" }, -]; -const mockSetOpenWithStates = vi.fn(); -const mockRouterRefresh = vi.fn(); - -describe("AddIntegrationModal", () => { - beforeEach(async () => { - vi.clearAllMocks(); - vi.mocked(useRouter).mockReturnValue({ refresh: mockRouterRefresh } as any); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders in add mode correctly", () => { - render( - - ); - - expect(screen.getByText("environments.integrations.airtable.link_airtable_table")).toBeInTheDocument(); - expect(screen.getByLabelText("Base")).toBeInTheDocument(); - // Use getByLabelText for the mocked selects - expect(screen.getByLabelText("environments.integrations.airtable.table_name")).toBeInTheDocument(); - expect(screen.getByLabelText("common.select_survey")).toBeInTheDocument(); - expect(screen.getByText("common.save")).toBeInTheDocument(); - expect(screen.getByText("common.cancel")).toBeInTheDocument(); - expect(screen.queryByText("common.delete")).not.toBeInTheDocument(); - }); - - test("shows 'No Base Found' error when airtableArray is empty", () => { - render( - - ); - expect(screen.getByTestId("alert-title")).toHaveTextContent( - "environments.integrations.airtable.no_bases_found" - ); - }); - - test("shows 'No Surveys Found' warning when surveys array is empty", () => { - render( - - ); - expect(screen.getByText("environments.integrations.create_survey_warning")).toBeInTheDocument(); - }); - - test("fetches and displays tables when a base is selected", async () => { - vi.mocked(fetchTables).mockResolvedValue({ tables: mockTables }); - render( - - ); - - const baseSelect = screen.getByLabelText("Base"); - await userEvent.selectOptions(baseSelect, "base1"); - - expect(fetchTables).toHaveBeenCalledWith(environmentId, "base1"); - await waitFor(() => { - // Use getByLabelText (mocked select) - const tableSelect = screen.getByLabelText("environments.integrations.airtable.table_name"); - expect(tableSelect).toBeEnabled(); - // Check options within the mocked select - expect(tableSelect.querySelector("option[value='table1']")).toBeInTheDocument(); - expect(tableSelect.querySelector("option[value='table2']")).toBeInTheDocument(); - }); - }); - - test("handles deletion in edit mode", async () => { - const initialData: TIntegrationAirtableConfigData = { - baseId: "base1", - tableId: "table1", - surveyId: "survey1", - questionIds: ["q1"], - questions: "common.selected_questions", - tableName: "Table 1", - surveyName: "Survey 1", - createdAt: new Date(), - includeVariables: false, - includeHiddenFields: false, - includeMetadata: false, - includeCreatedAt: true, - }; - const integrationWithData = { - ...mockAirtableIntegration, - config: { ...mockAirtableIntegration.config, data: [initialData] }, - }; - const defaultData = { ...initialData, index: 0 } as any; - - vi.mocked(fetchTables).mockResolvedValue({ tables: mockTables }); - vi.mocked(createOrUpdateIntegrationAction).mockResolvedValue({ ok: true, data: {} } as any); - - render( - - ); - - await waitFor(() => expect(fetchTables).toHaveBeenCalled()); // Wait for initial load - - // Click delete - await userEvent.click(screen.getByText("common.delete")); - - await waitFor(() => { - expect(createOrUpdateIntegrationAction).toHaveBeenCalledTimes(1); - const submittedData = vi.mocked(createOrUpdateIntegrationAction).mock.calls[0][0].integrationData; - // Expect data array to be empty after deletion - expect(submittedData.config.data).toHaveLength(0); - }); - - expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully"); - expect(mockSetOpenWithStates).toHaveBeenCalledWith(false); - expect(mockRouterRefresh).toHaveBeenCalled(); - }); - - test("handles cancel button click", async () => { - render( - - ); - - await userEvent.click(screen.getByText("common.cancel")); - expect(mockSetOpenWithStates).toHaveBeenCalledWith(false); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper.test.tsx deleted file mode 100644 index bac653c650..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper.test.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; -import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable"; -import { AirtableWrapper } from "./AirtableWrapper"; - -// Mock child components -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/ManageIntegration", - () => ({ - ManageIntegration: ({ setIsConnected }) => ( -
    - -
    - ), - }) -); -vi.mock("@/modules/ui/components/connect-integration", () => ({ - ConnectIntegration: ({ handleAuthorization, isEnabled }) => ( -
    - -
    - ), -})); - -// Mock library function -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable", () => ({ - authorize: vi.fn(), -})); - -// Mock image import -vi.mock("@/images/airtableLogo.svg", () => ({ - default: "airtable-logo-path", -})); - -// Mock window.location.replace -Object.defineProperty(window, "location", { - value: { - replace: vi.fn(), - }, - writable: true, -}); - -const environmentId = "test-env-id"; -const webAppUrl = "https://app.formbricks.com"; -const environment = { id: environmentId } as TEnvironment; -const surveys = []; -const airtableArray = []; -const locale = "en-US" as const; - -const baseProps = { - environmentId, - airtableArray, - surveys, - environment, - webAppUrl, - locale, -}; - -describe("AirtableWrapper", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders ConnectIntegration when not connected (no integration)", () => { - render(); - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled(); - }); - - test("renders ConnectIntegration when not connected (integration without key)", () => { - const integrationWithoutKey = { config: {} } as TIntegrationAirtable; - render(); - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - }); - - test("renders ConnectIntegration disabled when isEnabled is false", () => { - render(); - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Connect" })).toBeDisabled(); - }); - - test("calls authorize and redirects when Connect button is clicked", async () => { - const mockAuthorize = vi.mocked(authorize); - const redirectUrl = "https://airtable.com/auth"; - mockAuthorize.mockResolvedValue(redirectUrl); - - render(); - - const connectButton = screen.getByRole("button", { name: "Connect" }); - await userEvent.click(connectButton); - - expect(mockAuthorize).toHaveBeenCalledWith(environmentId, webAppUrl); - await vi.waitFor(() => { - expect(window.location.replace).toHaveBeenCalledWith(redirectUrl); - }); - }); - - test("renders ManageIntegration when connected", () => { - const connectedIntegration = { - id: "int-1", - config: { key: { access_token: "abc" }, email: "test@test.com", data: [] }, - } as unknown as TIntegrationAirtable; - render(); - expect(screen.getByTestId("manage-integration")).toBeInTheDocument(); - expect(screen.queryByTestId("connect-integration")).not.toBeInTheDocument(); - }); - - test("switches from ManageIntegration to ConnectIntegration when disconnected", async () => { - const connectedIntegration = { - id: "int-1", - config: { key: { access_token: "abc" }, email: "test@test.com", data: [] }, - } as unknown as TIntegrationAirtable; - render(); - - // Initially, ManageIntegration is shown - expect(screen.getByTestId("manage-integration")).toBeInTheDocument(); - - // Simulate disconnection via ManageIntegration's button - const disconnectButton = screen.getByRole("button", { name: "Disconnect" }); - await userEvent.click(disconnectButton); - - // Now, ConnectIntegration should be shown - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/BaseSelectDropdown.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/BaseSelectDropdown.test.tsx deleted file mode 100644 index c3075a0076..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/BaseSelectDropdown.test.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { useForm } from "react-hook-form"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TIntegrationItem } from "@formbricks/types/integration"; -import { IntegrationModalInputs } from "./AddIntegrationModal"; -import { BaseSelectDropdown } from "./BaseSelectDropdown"; - -// Mock UI components -vi.mock("@/modules/ui/components/label", () => ({ - Label: ({ children, htmlFor }: { children: React.ReactNode; htmlFor: string }) => ( - - ), -})); -vi.mock("@/modules/ui/components/select", () => ({ - Select: ({ children, onValueChange, disabled, defaultValue }) => ( - - ), - SelectTrigger: ({ children }) =>
    {children}
    , - SelectValue: () => SelectValueMock, - SelectContent: ({ children }) =>
    {children}
    , - SelectItem: ({ children, value }) => , -})); - -// Mock react-hook-form's Controller specifically -vi.mock("react-hook-form", async () => { - const actual = await vi.importActual("react-hook-form"); - // Keep the actual useForm - const originalUseForm = actual.useForm; - - // Mock Controller - const MockController = ({ name, _, render, defaultValue }) => { - // Minimal mock: call render with a basic field object - const field = { - onChange: vi.fn(), // Simple spy for field.onChange - onBlur: vi.fn(), - value: defaultValue, // Use defaultValue passed to Controller - name: name, - ref: vi.fn(), - }; - // The component passes the render prop result to the actual Select component - return render({ field }); - }; - - return { - ...actual, - useForm: originalUseForm, // Use the actual useForm - Controller: MockController, // Use the mocked Controller - }; -}); - -const mockAirtableArray: TIntegrationItem[] = [ - { id: "base1", name: "Base One" }, - { id: "base2", name: "Base Two" }, -]; - -const mockFetchTable = vi.fn(); - -// Use a wrapper component that utilizes the actual useForm -const renderComponent = ( - isLoading = false, - defaultValue: string | undefined = undefined, - airtableArray = mockAirtableArray -) => { - const Component = () => { - // Now uses the actual useForm because Controller is mocked separately - const { control, setValue } = useForm({ - defaultValues: { base: defaultValue }, - }); - return ( - - ); - }; - return render(); -}; - -describe("BaseSelectDropdown", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders the label and select trigger", () => { - renderComponent(); - expect(screen.getByText("environments.integrations.airtable.airtable_base")).toBeInTheDocument(); - expect(screen.getByTestId("base-select")).toBeInTheDocument(); - expect(screen.getByText("SelectValueMock")).toBeInTheDocument(); // From mocked SelectValue - }); - - test("renders options from airtableArray", () => { - renderComponent(); - const select = screen.getByTestId("base-select"); - expect(select.querySelectorAll("option")).toHaveLength(mockAirtableArray.length); - expect(screen.getByText("Base One")).toBeInTheDocument(); - expect(screen.getByText("Base Two")).toBeInTheDocument(); - }); - - test("disables the select when isLoading is true", () => { - renderComponent(true); - expect(screen.getByTestId("base-select")).toBeDisabled(); - }); - - test("enables the select when isLoading is false", () => { - renderComponent(false); - expect(screen.getByTestId("base-select")).toBeEnabled(); - }); - - test("renders correctly with empty airtableArray", () => { - renderComponent(false, undefined, []); - const select = screen.getByTestId("base-select"); - expect(select.querySelectorAll("option")).toHaveLength(0); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/ManageIntegration.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/ManageIntegration.test.tsx deleted file mode 100644 index 8191e0bab1..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/components/ManageIntegration.test.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TIntegrationAirtable, TIntegrationAirtableConfig } from "@formbricks/types/integration/airtable"; -import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions"; -import { ManageIntegration } from "./ManageIntegration"; - -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({ - deleteIntegrationAction: vi.fn(), -})); -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AddIntegrationModal", - () => ({ - AddIntegrationModal: ({ open, setOpenWithStates }) => - open ? ( -
    - -
    - ) : null, - }) -); -vi.mock("@/modules/ui/components/delete-dialog", () => ({ - DeleteDialog: ({ open, setOpen, onDelete }) => - open ? ( -
    - - -
    - ) : null, -})); -vi.mock("react-hot-toast", () => ({ toast: { success: vi.fn(), error: vi.fn() } })); - -const baseProps = { - environment: { id: "env1" } as TEnvironment, - environmentId: "env1", - setIsConnected: vi.fn(), - surveys: [], - airtableArray: [], - locale: "en-US" as const, -}; - -describe("ManageIntegration", () => { - afterEach(() => { - cleanup(); - }); - - test("empty state", () => { - render( - - ); - expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument(); - expect(screen.getByText(/link_new_table/)).toBeInTheDocument(); - }); - - test("open add modal", async () => { - render( - - ); - await userEvent.click(screen.getByText(/link_new_table/)); - expect(screen.getByTestId("add-modal")).toBeInTheDocument(); - }); - - test("list integrations and open edit modal", async () => { - const item = { - baseId: "b", - tableId: "t", - surveyId: "s", - surveyName: "S", - tableName: "T", - questions: "Q", - questionIds: ["x"], - createdAt: new Date(), - includeVariables: false, - includeHiddenFields: false, - includeMetadata: false, - includeCreatedAt: false, - }; - render( - - ); - expect(screen.getByText("S")).toBeInTheDocument(); - await userEvent.click(screen.getByText("S")); - expect(screen.getByTestId("add-modal")).toBeInTheDocument(); - }); - - test("delete integration success", async () => { - vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any); - render( - - ); - await userEvent.click(screen.getByText(/delete_integration/)); - expect(screen.getByTestId("delete-dialog")).toBeInTheDocument(); - await userEvent.click(screen.getByText("confirm")); - expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" }); - const { toast } = await import("react-hot-toast"); - expect(toast.success).toHaveBeenCalled(); - expect(baseProps.setIsConnected).toHaveBeenCalledWith(false); - }); - - test("delete integration error", async () => { - vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any); - render( - - ); - await userEvent.click(screen.getByText(/delete_integration/)); - await userEvent.click(screen.getByText("confirm")); - const { toast } = await import("react-hot-toast"); - expect(toast.error).toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/page.test.tsx deleted file mode 100644 index 3e16fded46..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/airtable/page.test.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TIntegrationItem } from "@formbricks/types/integration"; -import { TIntegrationAirtable, TIntegrationAirtableCredential } from "@formbricks/types/integration/airtable"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys"; -import { getAirtableTables } from "@/lib/airtable/service"; -import { WEBAPP_URL } from "@/lib/constants"; -import { getIntegrations } from "@/lib/integration/service"; -import { findMatchingLocale } from "@/lib/utils/locale"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; -import Page from "./page"; - -// Mock dependencies -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper", - () => ({ - AirtableWrapper: vi.fn(() =>
    AirtableWrapper Mock
    ), - }) -); -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys"); -vi.mock("@/lib/airtable/service"); - -let mockAirtableClientId: string | undefined = "test-client-id"; - -vi.mock("@/lib/constants", () => ({ - get AIRTABLE_CLIENT_ID() { - return mockAirtableClientId; - }, - WEBAPP_URL: "http://localhost:3000", - IS_PRODUCTION: true, - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - SENTRY_DSN: "mock-sentry-dsn", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -vi.mock("@/lib/integration/service"); -vi.mock("@/lib/utils/locale"); -vi.mock("@/modules/environments/lib/utils"); -vi.mock("@/modules/ui/components/go-back-button", () => ({ - GoBackButton: vi.fn(() =>
    GoBackButton Mock
    ), -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: vi.fn(({ children }) =>
    {children}
    ), -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: vi.fn(({ pageTitle }) =>

    {pageTitle}

    ), -})); -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); -vi.mock("next/navigation"); - -const mockEnvironmentId = "test-env-id"; -const mockEnvironment = { - id: mockEnvironmentId, - createdAt: new Date(), - updatedAt: new Date(), - type: "development", -} as unknown as TEnvironment; -const mockSurveys: TSurvey[] = [{ id: "survey1", name: "Survey 1" } as TSurvey]; -const mockAirtableIntegration: TIntegrationAirtable = { - type: "airtable", - config: { - key: { access_token: "test-token" } as unknown as TIntegrationAirtableCredential, - data: [], - email: "test@example.com", - }, - environmentId: mockEnvironmentId, - id: "int_airtable_123", -}; -const mockAirtableTables: TIntegrationItem[] = [{ id: "table1", name: "Table 1" } as TIntegrationItem]; -const mockLocale = "en-US"; - -const props = { - params: { - environmentId: mockEnvironmentId, - }, -}; - -describe("Airtable Integration Page", () => { - beforeEach(() => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: mockEnvironment, - isReadOnly: false, - } as unknown as TEnvironmentAuth); - vi.mocked(getSurveys).mockResolvedValue(mockSurveys); - vi.mocked(getIntegrations).mockResolvedValue([mockAirtableIntegration]); - vi.mocked(getAirtableTables).mockResolvedValue(mockAirtableTables); - vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("redirects if user is readOnly", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: mockEnvironment, - isReadOnly: true, - } as unknown as TEnvironmentAuth); - await render(await Page(props)); - expect(redirect).toHaveBeenCalledWith("./"); - }); - - test("renders correctly when integration is configured", async () => { - await render(await Page(props)); - - expect(screen.getByText("environments.integrations.airtable.airtable_integration")).toBeInTheDocument(); - expect(screen.getByText("GoBackButton Mock")).toBeInTheDocument(); - expect(screen.getByText("AirtableWrapper Mock")).toBeInTheDocument(); - - expect(vi.mocked(getEnvironmentAuth)).toHaveBeenCalledWith(mockEnvironmentId); - expect(vi.mocked(getSurveys)).toHaveBeenCalledWith(mockEnvironmentId); - expect(vi.mocked(getIntegrations)).toHaveBeenCalledWith(mockEnvironmentId); - expect(vi.mocked(getAirtableTables)).toHaveBeenCalledWith(mockEnvironmentId); - expect(vi.mocked(findMatchingLocale)).toHaveBeenCalled(); - - const AirtableWrapper = vi.mocked( - ( - await import( - "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper" - ) - ).AirtableWrapper - ); - expect(AirtableWrapper).toHaveBeenCalledWith( - { - isEnabled: true, - airtableIntegration: mockAirtableIntegration, - airtableArray: mockAirtableTables, - environmentId: mockEnvironmentId, - surveys: mockSurveys, - environment: mockEnvironment, - webAppUrl: WEBAPP_URL, - locale: mockLocale, - }, - undefined - ); - }); - - test("renders correctly when integration exists but is not configured (no key)", async () => { - const integrationWithoutKey = { - ...mockAirtableIntegration, - config: { ...mockAirtableIntegration.config, key: undefined }, - } as unknown as TIntegrationAirtable; - vi.mocked(getIntegrations).mockResolvedValue([integrationWithoutKey]); - - await render(await Page(props)); - - expect(screen.getByText("environments.integrations.airtable.airtable_integration")).toBeInTheDocument(); - expect(screen.getByText("AirtableWrapper Mock")).toBeInTheDocument(); - - expect(vi.mocked(getAirtableTables)).not.toHaveBeenCalled(); // Should not fetch tables if no key - - const AirtableWrapper = vi.mocked( - ( - await import( - "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper" - ) - ).AirtableWrapper - ); - // Update assertion to match the actual call - expect(AirtableWrapper).toHaveBeenCalledWith( - { - isEnabled: true, // isEnabled is true because AIRTABLE_CLIENT_ID is set in beforeEach - airtableIntegration: integrationWithoutKey, - airtableArray: [], // Should be empty as getAirtableTables is not called - environmentId: mockEnvironmentId, - surveys: mockSurveys, - environment: mockEnvironment, - webAppUrl: WEBAPP_URL, - locale: mockLocale, - }, - undefined // Change second argument to undefined - ); - }); - - test("renders correctly when integration is disabled (no client ID)", async () => { - mockAirtableClientId = undefined; // Simulate disabled integration - - await render(await Page(props)); - - expect(screen.getByText("environments.integrations.airtable.airtable_integration")).toBeInTheDocument(); - expect(screen.getByText("AirtableWrapper Mock")).toBeInTheDocument(); - - const AirtableWrapper = vi.mocked( - ( - await import( - "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper" - ) - ).AirtableWrapper - ); - expect(AirtableWrapper).toHaveBeenCalledWith( - expect.objectContaining({ - isEnabled: false, // Should be false - }), - undefined - ); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/AddIntegrationModal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/AddIntegrationModal.test.tsx deleted file mode 100644 index 0278551501..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/AddIntegrationModal.test.tsx +++ /dev/null @@ -1,702 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { - TIntegrationGoogleSheets, - TIntegrationGoogleSheetsConfigData, -} from "@formbricks/types/integration/google-sheet"; -import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/AddIntegrationModal"; - -// Mock actions and utilities -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({ - createOrUpdateIntegrationAction: vi.fn(), -})); -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/actions", () => ({ - getSpreadsheetNameByIdAction: vi.fn(), -})); -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/util", () => ({ - constructGoogleSheetsUrl: (id: string) => `https://docs.google.com/spreadsheets/d/${id}`, - extractSpreadsheetIdFromUrl: (url: string) => url.split("/")[5], - isValidGoogleSheetsUrl: (url: string) => url.startsWith("https://docs.google.com/spreadsheets/d/"), -})); -vi.mock("@/lib/i18n/utils", () => ({ - getLocalizedValue: (value: any, _locale: string) => value?.default || "", -})); -vi.mock("@/lib/utils/recall", () => ({ - replaceHeadlineRecall: (survey: any) => survey, -})); -vi.mock("@/modules/ui/components/additional-integration-settings", () => ({ - AdditionalIntegrationSettings: ({ - includeVariables, - setIncludeVariables, - includeHiddenFields, - setIncludeHiddenFields, - includeMetadata, - setIncludeMetadata, - includeCreatedAt, - setIncludeCreatedAt, - }: any) => ( -
    - Additional Settings - setIncludeVariables(e.target.checked)} - /> - setIncludeHiddenFields(e.target.checked)} - /> - setIncludeMetadata(e.target.checked)} - /> - setIncludeCreatedAt(e.target.checked)} - /> -
    - ), -})); -vi.mock("@/modules/ui/components/dropdown-selector", () => ({ - DropdownSelector: ({ label, items, selectedItem, setSelectedItem }: any) => ( -
    - - -
    - ), -})); -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open, onOpenChange }: any) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children, ...props }: any) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: any) =>
    {children}
    , - DialogTitle: ({ children }: any) =>

    {children}

    , - DialogDescription: ({ children }: any) =>

    {children}

    , - DialogBody: ({ children }: any) =>
    {children}
    , - DialogFooter: ({ children }: any) =>
    {children}
    , -})); -vi.mock("next/image", () => ({ - // eslint-disable-next-line @next/next/no-img-element - default: ({ src, alt }: { src: string; alt: string }) => {alt}, -})); -vi.mock("react-hook-form", () => ({ - useForm: () => ({ - handleSubmit: (callback: any) => (event: any) => { - event.preventDefault(); - callback(); - }, - }), -})); -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); -vi.mock("@tolgee/react", async () => { - const MockTolgeeProvider = ({ children }: { children: React.ReactNode }) => <>{children}; - const useTranslate = () => ({ - t: (key: string, _?: any) => { - // NOSONAR - // Simple mock translation function - if (key === "common.all_questions") return "All questions"; - if (key === "common.selected_questions") return "Selected questions"; - if (key === "environments.integrations.google_sheets.link_google_sheet") return "Link Google Sheet"; - if (key === "common.update") return "Update"; - if (key === "common.delete") return "Delete"; - if (key === "common.cancel") return "Cancel"; - if (key === "environments.integrations.google_sheets.spreadsheet_url") return "Spreadsheet URL"; - if (key === "common.select_survey") return "Select survey"; - if (key === "common.questions") return "Questions"; - if (key === "environments.integrations.google_sheets.enter_a_valid_spreadsheet_url_error") - return "Please enter a valid Google Sheet URL."; - if (key === "environments.integrations.please_select_a_survey_error") return "Please select a survey."; - if (key === "environments.integrations.select_at_least_one_question_error") - return "Please select at least one question."; - if (key === "environments.integrations.integration_updated_successfully") - return "Integration updated successfully."; - if (key === "environments.integrations.integration_added_successfully") - return "Integration added successfully."; - if (key === "environments.integrations.integration_removed_successfully") - return "Integration removed successfully."; - if (key === "environments.integrations.google_sheets.google_sheet_logo") return "Google Sheet logo"; - if (key === "environments.integrations.google_sheets.google_sheets_integration_description") - return "Sync responses with Google Sheets."; - if (key === "environments.integrations.create_survey_warning") - return "You need to create a survey first."; - return key; // Return key if no translation is found - }, - }); - return { TolgeeProvider: MockTolgeeProvider, useTranslate }; -}); - -// Mock dependencies -const createOrUpdateIntegrationAction = vi.mocked( - (await import("@/app/(app)/environments/[environmentId]/project/integrations/actions")) - .createOrUpdateIntegrationAction -); -const getSpreadsheetNameByIdAction = vi.mocked( - (await import("@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/actions")) - .getSpreadsheetNameByIdAction -); -const toast = vi.mocked((await import("react-hot-toast")).default); - -const environmentId = "test-env-id"; -const mockSetOpen = vi.fn(); - -const surveys: TSurvey[] = [ - { - id: "survey1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Survey 1", - type: "app", - environmentId: environmentId, - status: "inProgress", - questions: [ - { - id: "q1", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Question 1?" }, - required: true, - } as unknown as TSurveyQuestion, - { - id: "q2", - type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: "Question 2?" }, - required: false, - choices: [ - { id: "c1", label: { default: "Choice 1" } }, - { id: "c2", label: { default: "Choice 2" } }, - ], - }, - ], - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - autoComplete: null, - singleUse: null, - styling: null, - surveyClosedMessage: null, - segment: null, - languages: [], - variables: [], - welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"], - hiddenFields: { enabled: true, fieldIds: [] }, - pin: null, - displayLimit: null, - } as unknown as TSurvey, - { - id: "survey2", - createdAt: new Date(), - updatedAt: new Date(), - name: "Survey 2", - type: "link", - environmentId: environmentId, - status: "draft", - questions: [ - { - id: "q3", - type: TSurveyQuestionTypeEnum.Rating, - headline: { default: "Rate this?" }, - required: true, - scale: "number", - range: 5, - } as unknown as TSurveyQuestion, - ], - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - autoComplete: null, - singleUse: null, - styling: null, - surveyClosedMessage: null, - segment: null, - languages: [], - variables: [], - welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"], - hiddenFields: { enabled: true, fieldIds: [] }, - pin: null, - displayLimit: null, - } as unknown as TSurvey, -]; - -const mockGoogleSheetIntegration = { - id: "integration1", - type: "googleSheets", - config: { - key: { - access_token: "mock_access_token", - expiry_date: Date.now() + 3600000, - refresh_token: "mock_refresh_token", - scope: "mock_scope", - token_type: "Bearer", - }, - email: "test@example.com", - data: [], // Initially empty, will be populated in beforeEach - }, -} as unknown as TIntegrationGoogleSheets; - -const mockSelectedIntegration: TIntegrationGoogleSheetsConfigData & { index: number } = { - spreadsheetId: "existing-sheet-id", - spreadsheetName: "Existing Sheet", - surveyId: surveys[0].id, - surveyName: surveys[0].name, - questionIds: [surveys[0].questions[0].id], - questions: "Selected questions", - createdAt: new Date(), - includeVariables: true, - includeHiddenFields: false, - includeMetadata: true, - includeCreatedAt: false, - index: 0, -}; - -describe("AddIntegrationModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - // Reset integration data before each test if needed - mockGoogleSheetIntegration.config.data = [ - { ...mockSelectedIntegration }, // Simulate existing data for update/delete tests - ]; - }); - - test("renders correctly when open (create mode)", () => { - render( - - ); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Link Google Sheet"); - expect(screen.getByTestId("dialog-description")).toHaveTextContent("Sync responses with Google Sheets."); - // Use getByPlaceholderText for the input - expect( - screen.getByPlaceholderText("https://docs.google.com/spreadsheets/d/") - ).toBeInTheDocument(); - // Use getByTestId for the dropdown - expect(screen.getByTestId("survey-dropdown")).toBeInTheDocument(); - expect(screen.getByText("Cancel")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Link Google Sheet" })).toBeInTheDocument(); - expect(screen.queryByText("Delete")).not.toBeInTheDocument(); - expect(screen.queryByText("Questions")).not.toBeInTheDocument(); - }); - - test("renders correctly when open (update mode)", () => { - render( - - ); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Link Google Sheet"); - expect(screen.getByTestId("dialog-description")).toHaveTextContent("Sync responses with Google Sheets."); - // Use getByPlaceholderText for the input - expect( - screen.getByPlaceholderText("https://docs.google.com/spreadsheets/d/") - ).toHaveValue("https://docs.google.com/spreadsheets/d/existing-sheet-id"); - expect(screen.getByTestId("survey-dropdown")).toHaveValue(surveys[0].id); - expect(screen.getByText("Questions")).toBeInTheDocument(); - expect(screen.getByText("Delete")).toBeInTheDocument(); - expect(screen.getByText("Update")).toBeInTheDocument(); - expect(screen.queryByText("Cancel")).not.toBeInTheDocument(); - expect(screen.getByTestId("include-variables")).toBeChecked(); - expect(screen.getByTestId("include-hidden-fields")).not.toBeChecked(); - expect(screen.getByTestId("include-metadata")).toBeChecked(); - expect(screen.getByTestId("include-created-at")).not.toBeChecked(); - }); - - test("selects survey and shows questions", async () => { - render( - - ); - - const surveyDropdown = screen.getByTestId("survey-dropdown"); - await userEvent.selectOptions(surveyDropdown, surveys[1].id); - - expect(screen.getByText("Questions")).toBeInTheDocument(); - surveys[1].questions.forEach((q) => { - expect(screen.getByLabelText(q.headline.default)).toBeInTheDocument(); - // Initially all questions should be checked when a survey is selected in create mode - expect(screen.getByLabelText(q.headline.default)).toBeChecked(); - }); - }); - - test("handles question selection", async () => { - render( - - ); - - const surveyDropdown = screen.getByTestId("survey-dropdown"); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - - const firstQuestionCheckbox = screen.getByLabelText(surveys[0].questions[0].headline.default); - expect(firstQuestionCheckbox).toBeChecked(); // Initially checked - - await userEvent.click(firstQuestionCheckbox); - expect(firstQuestionCheckbox).not.toBeChecked(); // Unchecked after click - - await userEvent.click(firstQuestionCheckbox); - expect(firstQuestionCheckbox).toBeChecked(); // Checked again - }); - - test("creates integration successfully", async () => { - getSpreadsheetNameByIdAction.mockResolvedValue({ data: "Test Sheet Name" }); - createOrUpdateIntegrationAction.mockResolvedValue({ data: null as any }); // Mock successful action - - render( - - ); - - // Use getByPlaceholderText for the input - const urlInput = screen.getByPlaceholderText( - "https://docs.google.com/spreadsheets/d/" - ); - const surveyDropdown = screen.getByTestId("survey-dropdown"); - const submitButton = screen.getByRole("button", { name: "Link Google Sheet" }); - - await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/new-sheet-id"); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - - // Wait for questions to appear and potentially uncheck one - const firstQuestionCheckbox = await screen.findByLabelText(surveys[0].questions[0].headline.default); - await userEvent.click(firstQuestionCheckbox); // Uncheck first question - - // Check additional settings - await userEvent.click(screen.getByTestId("include-variables")); - await userEvent.click(screen.getByTestId("include-metadata")); - - await userEvent.click(submitButton); - - await waitFor(() => { - expect(getSpreadsheetNameByIdAction).toHaveBeenCalledWith({ - googleSheetIntegration: expect.any(Object), - environmentId, - spreadsheetId: "new-sheet-id", - }); - }); - - await waitFor(() => { - expect(createOrUpdateIntegrationAction).toHaveBeenCalledWith({ - environmentId, - integrationData: expect.objectContaining({ - type: "googleSheets", - config: expect.objectContaining({ - key: mockGoogleSheetIntegration.config.key, - email: mockGoogleSheetIntegration.config.email, - data: expect.arrayContaining([ - expect.objectContaining({ - spreadsheetId: "new-sheet-id", - spreadsheetName: "Test Sheet Name", - surveyId: surveys[0].id, - surveyName: surveys[0].name, - questionIds: surveys[0].questions.slice(1).map((q) => q.id), // Excludes the first question - questions: "Selected questions", - includeVariables: true, - includeHiddenFields: false, - includeMetadata: true, - includeCreatedAt: true, // Default - }), - ]), - }), - }), - }); - }); - - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith("Integration added successfully."); - }); - await waitFor(() => { - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - }); - - test("deletes integration successfully", async () => { - createOrUpdateIntegrationAction.mockResolvedValue({ data: null as any }); - - render( - - ); - - const deleteButton = screen.getByText("Delete"); - await userEvent.click(deleteButton); - - await waitFor(() => { - expect(createOrUpdateIntegrationAction).toHaveBeenCalledWith({ - environmentId, - integrationData: expect.objectContaining({ - config: expect.objectContaining({ - data: [], // Data array should be empty after deletion - }), - }), - }); - }); - - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith("Integration removed successfully."); - }); - await waitFor(() => { - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - }); - - test("shows validation error for invalid URL", async () => { - render( - - ); - - // Use getByPlaceholderText for the input - const urlInput = screen.getByPlaceholderText( - "https://docs.google.com/spreadsheets/d/" - ); - const surveyDropdown = screen.getByTestId("survey-dropdown"); - const submitButton = screen.getByRole("button", { name: "Link Google Sheet" }); - - await userEvent.type(urlInput, "invalid-url"); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - await userEvent.click(submitButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Please enter a valid Google Sheet URL."); - }); - expect(createOrUpdateIntegrationAction).not.toHaveBeenCalled(); - expect(mockSetOpen).not.toHaveBeenCalled(); - }); - - test("shows validation error if no survey selected", async () => { - render( - - ); - - // Use getByPlaceholderText for the input - const urlInput = screen.getByPlaceholderText( - "https://docs.google.com/spreadsheets/d/" - ); - const submitButton = screen.getByRole("button", { name: "Link Google Sheet" }); - - await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/some-id"); - // No survey selected - await userEvent.click(submitButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Please select a survey."); - }); - expect(createOrUpdateIntegrationAction).not.toHaveBeenCalled(); - expect(mockSetOpen).not.toHaveBeenCalled(); - }); - - test("shows validation error if no questions selected", async () => { - render( - - ); - - // Use getByPlaceholderText for the input - const urlInput = screen.getByPlaceholderText( - "https://docs.google.com/spreadsheets/d/" - ); - const surveyDropdown = screen.getByTestId("survey-dropdown"); - const submitButton = screen.getByRole("button", { name: "Link Google Sheet" }); - - await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/some-id"); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - - // Uncheck all questions - for (const question of surveys[0].questions) { - const checkbox = await screen.findByLabelText(question.headline.default); - await userEvent.click(checkbox); - } - - await userEvent.click(submitButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Please select at least one question."); - }); - expect(createOrUpdateIntegrationAction).not.toHaveBeenCalled(); - expect(mockSetOpen).not.toHaveBeenCalled(); - }); - - test("shows error toast if createOrUpdateIntegrationAction fails", async () => { - const errorMessage = "Failed to update integration"; - getSpreadsheetNameByIdAction.mockResolvedValue({ data: "Some Sheet Name" }); - createOrUpdateIntegrationAction.mockRejectedValue(new Error(errorMessage)); - - render( - - ); - - // Use getByPlaceholderText for the input - const urlInput = screen.getByPlaceholderText( - "https://docs.google.com/spreadsheets/d/" - ); - const surveyDropdown = screen.getByTestId("survey-dropdown"); - const submitButton = screen.getByRole("button", { name: "Link Google Sheet" }); - - await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/another-id"); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - await userEvent.click(submitButton); - - await waitFor(() => { - expect(getSpreadsheetNameByIdAction).toHaveBeenCalled(); - }); - await waitFor(() => { - expect(createOrUpdateIntegrationAction).toHaveBeenCalled(); - }); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith(errorMessage); - }); - expect(mockSetOpen).not.toHaveBeenCalled(); - }); - - test("calls setOpen(false) and resets form on cancel", async () => { - render( - - ); - - // Use getByPlaceholderText for the input - const urlInput = screen.getByPlaceholderText( - "https://docs.google.com/spreadsheets/d/" - ); - const cancelButton = screen.getByText("Cancel"); - - // Simulate some interaction - await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/temp-id"); - await userEvent.click(cancelButton); - - expect(mockSetOpen).toHaveBeenCalledWith(false); - // Re-render with open=true to check if state was reset (URL should be empty) - cleanup(); - render( - - ); - // Use getByPlaceholderText for the input check after re-render - expect( - screen.getByPlaceholderText("https://docs.google.com/spreadsheets/d/") - ).toHaveValue(""); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/GoogleSheetWrapper.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/GoogleSheetWrapper.test.tsx deleted file mode 100644 index e792081975..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/GoogleSheetWrapper.test.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { - TIntegrationGoogleSheets, - TIntegrationGoogleSheetsCredential, -} from "@formbricks/types/integration/google-sheet"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/GoogleSheetWrapper"; -import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/google"; - -// Mock child components and functions -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/ManageIntegration", - () => ({ - ManageIntegration: vi.fn(({ setOpenAddIntegrationModal }) => ( -
    - -
    - )), - }) -); - -vi.mock("@/modules/ui/components/connect-integration", () => ({ - ConnectIntegration: vi.fn(({ handleAuthorization }) => ( -
    - -
    - )), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/AddIntegrationModal", - () => ({ - AddIntegrationModal: vi.fn(({ open }) => - open ?
    Modal
    : null - ), - }) -); - -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/google", () => ({ - authorize: vi.fn(() => Promise.resolve("http://google.com/auth")), -})); - -const mockEnvironment = { - id: "test-env-id", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - appSetupCompleted: false, -} as unknown as TEnvironment; - -const mockSurveys: TSurvey[] = []; -const mockWebAppUrl = "http://localhost:3000"; -const mockLocale = "en-US"; - -const mockGoogleSheetIntegration = { - id: "test-integration-id", - type: "googleSheets", - config: { - key: { access_token: "test-token" } as unknown as TIntegrationGoogleSheetsCredential, - data: [], - email: "test@example.com", - }, -} as unknown as TIntegrationGoogleSheets; - -describe("GoogleSheetWrapper", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders ConnectIntegration when not connected", () => { - render( - - ); - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - expect(screen.queryByTestId("add-integration-modal")).not.toBeInTheDocument(); - }); - - test("renders ConnectIntegration when integration exists but has no key", () => { - const integrationWithoutKey = { - ...mockGoogleSheetIntegration, - config: { data: [], email: "test" }, - } as unknown as TIntegrationGoogleSheets; - render( - - ); - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - }); - - test("calls authorize when connect button is clicked", async () => { - const user = userEvent.setup(); - // Mock window.location.replace - const originalLocation = window.location; - // @ts-expect-error - delete window.location; - window.location = { ...originalLocation, replace: vi.fn() } as any; - - render( - - ); - - const connectButton = screen.getByRole("button", { name: "Connect" }); - await user.click(connectButton); - - expect(vi.mocked(authorize)).toHaveBeenCalledWith(mockEnvironment.id, mockWebAppUrl); - // Need to wait for the promise returned by authorize to resolve - await waitFor(() => { - expect(window.location.replace).toHaveBeenCalledWith("http://google.com/auth"); - }); - - // Restore window.location - window.location = originalLocation as any; - }); - - test("renders ManageIntegration and AddIntegrationModal when connected", () => { - render( - - ); - expect(screen.getByTestId("manage-integration")).toBeInTheDocument(); - // Modal is rendered but initially hidden - expect(screen.queryByTestId("add-integration-modal")).not.toBeInTheDocument(); - expect(screen.queryByTestId("connect-integration")).not.toBeInTheDocument(); - }); - - test("opens AddIntegrationModal when triggered from ManageIntegration", async () => { - const user = userEvent.setup(); - render( - - ); - - expect(screen.queryByTestId("add-integration-modal")).not.toBeInTheDocument(); - const openModalButton = screen.getByRole("button", { name: "Open Modal" }); // Button inside mocked ManageIntegration - await user.click(openModalButton); - expect(screen.getByTestId("add-integration-modal")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/ManageIntegration.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/ManageIntegration.test.tsx deleted file mode 100644 index 22cb56eb77..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/ManageIntegration.test.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet"; -import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions"; -import { ManageIntegration } from "./ManageIntegration"; - -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({ - deleteIntegrationAction: vi.fn(), -})); - -vi.mock("react-hot-toast", () => ({ - default: { success: vi.fn(), error: vi.fn() }, -})); - -vi.mock("@/modules/ui/components/delete-dialog", () => ({ - DeleteDialog: ({ open, setOpen, onDelete }: any) => - open ? ( -
    - - -
    - ) : null, -})); - -vi.mock("@/modules/ui/components/empty-space-filler", () => ({ - EmptySpaceFiller: ({ emptyMessage }: any) =>
    {emptyMessage}
    , -})); - -const baseProps = { - environment: { id: "env1" } as TEnvironment, - setOpenAddIntegrationModal: vi.fn(), - setIsConnected: vi.fn(), - setSelectedIntegration: vi.fn(), - locale: "en-US" as const, -} as const; - -describe("ManageIntegration (Google Sheets)", () => { - afterEach(() => { - cleanup(); - }); - - test("empty state", () => { - render( - - ); - - expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument(); - expect(screen.getByText(/link_new_sheet/)).toBeInTheDocument(); - }); - - test("click link new sheet", async () => { - render( - - ); - - await userEvent.click(screen.getByText(/link_new_sheet/)); - - expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null); - expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true); - }); - - test("list integrations and open edit", async () => { - const item = { - spreadsheetId: "sid", - spreadsheetName: "SheetName", - surveyId: "s1", - surveyName: "Survey1", - questionIds: ["q1"], - questions: "Q", - createdAt: new Date(), - }; - - render( - - ); - - expect(screen.getByText("Survey1")).toBeInTheDocument(); - - await userEvent.click(screen.getByText("Survey1")); - - expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({ - ...item, - index: 0, - }); - expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true); - }); - - test("delete integration success", async () => { - vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any); - - render( - - ); - - await userEvent.click(screen.getByText(/delete_integration/)); - expect(screen.getByTestId("delete-dialog")).toBeInTheDocument(); - - await userEvent.click(screen.getByText("confirm")); - - expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" }); - - const { default: toast } = await import("react-hot-toast"); - expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully"); - expect(baseProps.setIsConnected).toHaveBeenCalledWith(false); - }); - - test("delete integration error", async () => { - vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any); - - render( - - ); - - await userEvent.click(screen.getByText(/delete_integration/)); - await userEvent.click(screen.getByText("confirm")); - - const { default: toast } = await import("react-hot-toast"); - expect(toast.error).toHaveBeenCalledWith(expect.any(String)); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/loading.test.tsx deleted file mode 100644 index 7fd3355e78..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/loading.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Loading from "./loading"; - -// Mock the GoBackButton component -vi.mock("@/modules/ui/components/go-back-button", () => ({ - GoBackButton: () =>
    GoBackButton
    , -})); - -describe("Loading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the loading state correctly", () => { - render(); - - // Check for GoBackButton mock - expect(screen.getByText("GoBackButton")).toBeInTheDocument(); - - // Check for the disabled button text - expect(screen.getByText("environments.integrations.google_sheets.link_new_sheet")).toBeInTheDocument(); - expect( - screen.getByText("environments.integrations.google_sheets.link_new_sheet").closest("button") - ).toHaveClass("pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none"); - - // Check for table headers - expect(screen.getByText("common.survey")).toBeInTheDocument(); - expect(screen.getByText("environments.integrations.google_sheets.google_sheet_name")).toBeInTheDocument(); - expect(screen.getByText("common.questions")).toBeInTheDocument(); - expect(screen.getByText("common.updated_at")).toBeInTheDocument(); - - // Check for placeholder elements (count based on the loop) - const placeholders = screen.getAllByRole("generic", { hidden: true }); // Using generic role as divs don't have implicit roles - // Calculate expected placeholders: 3 rows * 5 placeholders per row = 15 - // Plus the button, header divs (4), and the main containers - // It's simpler to check if there are *any* pulse animations - expect(placeholders.some((el) => el.classList.contains("animate-pulse"))).toBe(true); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/page.test.tsx deleted file mode 100644 index a27156fa9e..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/google-sheets/page.test.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { - TIntegrationGoogleSheets, - TIntegrationGoogleSheetsCredential, -} from "@formbricks/types/integration/google-sheet"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import Page from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/page"; -import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys"; -import { getIntegrations } from "@/lib/integration/service"; -import { findMatchingLocale } from "@/lib/utils/locale"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; - -// Mock dependencies -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/GoogleSheetWrapper", - () => ({ - GoogleSheetWrapper: vi.fn( - ({ isEnabled, environment, surveys, googleSheetIntegration, webAppUrl, locale }) => ( -
    - Mocked GoogleSheetWrapper - {isEnabled.toString()} - {environment.id} - {surveys?.length ?? 0} - {googleSheetIntegration?.id} - {webAppUrl} - {locale} -
    - ) - ), - }) -); -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys", () => ({ - getSurveys: vi.fn(), -})); - -let mockGoogleSheetClientId: string | undefined = "test-client-id"; - -vi.mock("@/lib/constants", () => ({ - get GOOGLE_SHEETS_CLIENT_ID() { - return mockGoogleSheetClientId; - }, - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - GOOGLE_SHEETS_CLIENT_SECRET: "test-client-secret", - GOOGLE_SHEETS_REDIRECT_URL: "test-redirect-url", -})); -vi.mock("@/lib/integration/service", () => ({ - getIntegrations: vi.fn(), -})); -vi.mock("@/lib/utils/locale", () => ({ - findMatchingLocale: vi.fn(), -})); -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); -vi.mock("@/modules/ui/components/go-back-button", () => ({ - GoBackButton: vi.fn(({ url }) =>
    {url}
    ), -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: vi.fn(({ children }) =>
    {children}
    ), -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: vi.fn(({ pageTitle }) =>

    {pageTitle}

    ), -})); -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -const mockEnvironment = { - id: "test-env-id", - createdAt: new Date(), - updatedAt: new Date(), - appSetupCompleted: false, - type: "development", -} as unknown as TEnvironment; - -const mockSurveys: TSurvey[] = [ - { - id: "survey1", - name: "Survey 1", - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "test-env-id", - status: "inProgress", - type: "app", - questions: [], - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - languages: [], - pin: null, - segment: null, - singleUse: null, - styling: null, - surveyClosedMessage: null, - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - autoComplete: null, - } as unknown as TSurvey, -]; - -const mockGoogleSheetIntegration = { - id: "integration1", - type: "googleSheets", - config: { - data: [], - key: { - refresh_token: "refresh", - access_token: "access", - expiry_date: Date.now() + 3600000, - } as unknown as TIntegrationGoogleSheetsCredential, - email: "test@example.com", - }, -} as unknown as TIntegrationGoogleSheets; - -const mockProps = { - params: { environmentId: "test-env-id" }, -}; - -describe("GoogleSheetsIntegrationPage", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: mockEnvironment, - isReadOnly: false, - } as TEnvironmentAuth); - vi.mocked(getSurveys).mockResolvedValue(mockSurveys); - vi.mocked(getIntegrations).mockResolvedValue([mockGoogleSheetIntegration]); - vi.mocked(findMatchingLocale).mockResolvedValue("en-US"); - }); - - test("renders the page with GoogleSheetWrapper when enabled and not read-only", async () => { - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect( - screen.getByText("environments.integrations.google_sheets.google_sheets_integration") - ).toBeInTheDocument(); - expect(screen.getByText("Mocked GoogleSheetWrapper")).toBeInTheDocument(); - expect(screen.getByTestId("isEnabled")).toHaveTextContent("true"); - expect(screen.getByTestId("environmentId")).toHaveTextContent(mockEnvironment.id); - expect(screen.getByTestId("surveyCount")).toHaveTextContent(mockSurveys.length.toString()); - expect(screen.getByTestId("integrationId")).toHaveTextContent(mockGoogleSheetIntegration.id); - expect(screen.getByTestId("webAppUrl")).toHaveTextContent("test-webapp-url"); - expect(screen.getByTestId("locale")).toHaveTextContent("en-US"); - expect(screen.getByTestId("go-back")).toHaveTextContent( - `test-webapp-url/environments/${mockProps.params.environmentId}/project/integrations` - ); - expect(vi.mocked(redirect)).not.toHaveBeenCalled(); - }); - - test("calls redirect when user is read-only", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: mockEnvironment, - isReadOnly: true, - } as TEnvironmentAuth); - - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect(vi.mocked(redirect)).toHaveBeenCalledWith("./"); - }); - - test("passes isEnabled=false to GoogleSheetWrapper when constants are missing", async () => { - mockGoogleSheetClientId = undefined; - - const { default: PageWithMissingConstants } = (await import( - "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/page" - )) as { default: typeof Page }; - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: mockEnvironment, - isReadOnly: false, - } as TEnvironmentAuth); - vi.mocked(getSurveys).mockResolvedValue(mockSurveys); - vi.mocked(getIntegrations).mockResolvedValue([mockGoogleSheetIntegration]); - vi.mocked(findMatchingLocale).mockResolvedValue("en-US"); - - const PageComponent = await PageWithMissingConstants(mockProps); - render(PageComponent); - - expect(screen.getByTestId("isEnabled")).toHaveTextContent("false"); - }); - - test("handles case where no Google Sheet integration exists", async () => { - vi.mocked(getIntegrations).mockResolvedValue([]); // No integrations - - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect(screen.getByText("Mocked GoogleSheetWrapper")).toBeInTheDocument(); - expect(screen.getByTestId("integrationId")).toBeEmptyDOMElement(); // No integration ID passed - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/AddIntegrationModal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/AddIntegrationModal.test.tsx deleted file mode 100644 index f7b01a160d..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/AddIntegrationModal.test.tsx +++ /dev/null @@ -1,630 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { - TIntegrationNotion, - TIntegrationNotionConfigData, - TIntegrationNotionCredential, - TIntegrationNotionDatabase, -} from "@formbricks/types/integration/notion"; -import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/components/AddIntegrationModal"; - -// Mock actions and utilities -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({ - createOrUpdateIntegrationAction: vi.fn(), -})); -vi.mock("@/lib/i18n/utils", () => ({ - getLocalizedValue: (value: any, _locale: string) => value?.default || "", -})); -vi.mock("@/lib/pollyfills/structuredClone", () => ({ - structuredClone: (obj: any) => JSON.parse(JSON.stringify(obj)), -})); -vi.mock("@/lib/utils/recall", () => ({ - replaceHeadlineRecall: (survey: any) => survey, -})); -vi.mock("@/modules/survey/lib/questions", () => ({ - getQuestionTypes: () => [ - { id: TSurveyQuestionTypeEnum.OpenText, label: "Open Text" }, - { id: TSurveyQuestionTypeEnum.MultipleChoiceSingle, label: "Multiple Choice Single" }, - { id: TSurveyQuestionTypeEnum.Date, label: "Date" }, - ], -})); -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, loading, variant, type = "button" }: any) => ( - - ), -})); -vi.mock("@/modules/ui/components/dropdown-selector", () => ({ - DropdownSelector: ({ label, items, selectedItem, setSelectedItem, placeholder, disabled }: any) => { - // Ensure the selected item is always available as an option - const allOptions = [...items]; - if (selectedItem && !items.some((item: any) => item.id === selectedItem.id)) { - // Use a simple object structure consistent with how options are likely used - allOptions.push({ id: selectedItem.id, name: selectedItem.name }); - } - // Remove duplicates just in case - const uniqueOptions = Array.from(new Map(allOptions.map((item) => [item.id, item])).values()); - - return ( -
    - {label && } - -
    - ); - }, -})); -vi.mock("@/modules/ui/components/label", () => ({ - Label: ({ children }: { children: React.ReactNode }) => , -})); -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ open, children }: { open: boolean; children: React.ReactNode }) => - open ?
    {children}
    : null, - DialogContent: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogHeader: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogDescription: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -

    - {children} -

    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogBody: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogFooter: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), -})); -vi.mock("lucide-react", () => ({ - PlusIcon: () => +, - TrashIcon: () => 🗑️, -})); -vi.mock("next/image", () => ({ - // eslint-disable-next-line @next/next/no-img-element - default: ({ src, alt }: { src: string; alt: string }) => {alt}, -})); -vi.mock("react-hook-form", () => ({ - useForm: () => ({ - handleSubmit: (callback: any) => (event: any) => { - event.preventDefault(); - callback(); - }, - }), -})); -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); -vi.mock("@tolgee/react", async () => { - const MockTolgeeProvider = ({ children }: { children: React.ReactNode }) => <>{children}; - const useTranslate = () => ({ - t: (key: string, params?: any) => { - // NOSONAR - // Simple mock translation function - if (key === "common.warning") return "Warning"; - if (key === "common.metadata") return "Metadata"; - if (key === "common.created_at") return "Created at"; - if (key === "common.hidden_field") return "Hidden Field"; - if (key === "environments.integrations.notion.link_notion_database") return "Link Notion Database"; - if (key === "environments.integrations.notion.sync_responses_with_a_notion_database") - return "Sync responses with a Notion database."; - if (key === "environments.integrations.notion.select_a_database") return "Select a database"; - if (key === "common.select_survey") return "Select survey"; - if (key === "environments.integrations.notion.map_formbricks_fields_to_notion_property") - return "Map Formbricks fields to Notion property"; - if (key === "environments.integrations.notion.select_a_survey_question") - return "Select a survey question"; - if (key === "environments.integrations.notion.select_a_field_to_map") return "Select a field to map"; - if (key === "common.delete") return "Delete"; - if (key === "common.cancel") return "Cancel"; - if (key === "common.update") return "Update"; - if (key === "environments.integrations.notion.please_select_a_database") - return "Please select a database."; - if (key === "environments.integrations.please_select_a_survey_error") return "Please select a survey."; - if (key === "environments.integrations.notion.please_select_at_least_one_mapping") - return "Please select at least one mapping."; - if (key === "environments.integrations.notion.please_resolve_mapping_errors") - return "Please resolve mapping errors."; - if (key === "environments.integrations.notion.please_complete_mapping_fields_with_notion_property") - return "Please complete mapping fields."; - if (key === "environments.integrations.integration_updated_successfully") - return "Integration updated successfully."; - if (key === "environments.integrations.integration_added_successfully") - return "Integration added successfully."; - if (key === "environments.integrations.integration_removed_successfully") - return "Integration removed successfully."; - if (key === "environments.integrations.notion.notion_logo") return "Notion logo"; - if (key === "environments.integrations.create_survey_warning") - return "You need to create a survey first."; - if (key === "environments.integrations.notion.create_at_least_one_database_to_setup_this_integration") - return "Create at least one database."; - if (key === "environments.integrations.notion.duplicate_connection_warning") - return "Duplicate connection warning."; - if (key === "environments.integrations.notion.que_name_of_type_cant_be_mapped_to") - return `Question ${params.que_name} (${params.question_label}) can't be mapped to ${params.col_name} (${params.col_type}). Allowed types: ${params.mapped_type}`; - - return key; // Return key if no translation is found - }, - }); - return { TolgeeProvider: MockTolgeeProvider, useTranslate }; -}); - -// Mock dependencies -const createOrUpdateIntegrationAction = vi.mocked( - (await import("@/app/(app)/environments/[environmentId]/project/integrations/actions")) - .createOrUpdateIntegrationAction -); -const toast = vi.mocked((await import("react-hot-toast")).default); - -const environmentId = "test-env-id"; -const mockSetOpen = vi.fn(); - -const surveys: TSurvey[] = [ - { - id: "survey1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Survey 1", - type: "app", - environmentId: environmentId, - status: "inProgress", - questions: [ - { - id: "q1", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Question 1?" }, - required: true, - } as unknown as TSurveyQuestion, - { - id: "q2", - type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: "Question 2?" }, - required: false, - choices: [ - { id: "c1", label: { default: "Choice 1" } }, - { id: "c2", label: { default: "Choice 2" } }, - ], - }, - ], - variables: [{ id: "var1", name: "Variable 1" }], - hiddenFields: { enabled: true, fieldIds: ["hf1"] }, - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - autoComplete: null, - singleUse: null, - styling: null, - surveyClosedMessage: null, - segment: null, - languages: [], - welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"], - pin: null, - displayLimit: null, - } as unknown as TSurvey, - { - id: "survey2", - createdAt: new Date(), - updatedAt: new Date(), - name: "Survey 2", - type: "link", - environmentId: environmentId, - status: "draft", - questions: [ - { - id: "q3", - type: TSurveyQuestionTypeEnum.Date, - headline: { default: "Date Question?" }, - required: true, - } as unknown as TSurveyQuestion, - ], - variables: [], - hiddenFields: { enabled: false }, - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - autoComplete: null, - singleUse: null, - styling: null, - surveyClosedMessage: null, - segment: null, - languages: [], - welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"], - pin: null, - displayLimit: null, - } as unknown as TSurvey, -]; - -const databases: TIntegrationNotionDatabase[] = [ - { - id: "db1", - name: "Database 1 Title", - properties: { - prop1: { id: "p1", name: "Title Prop", type: "title" }, - prop2: { id: "p2", name: "Text Prop", type: "rich_text" }, - prop3: { id: "p3", name: "Number Prop", type: "number" }, - prop4: { id: "p4", name: "Date Prop", type: "date" }, - prop5: { id: "p5", name: "Unsupported Prop", type: "formula" }, // Unsupported - }, - }, - { - id: "db2", - name: "Database 2 Title", - properties: { - propA: { id: "pa", name: "Name", type: "title" }, - propB: { id: "pb", name: "Email", type: "email" }, - }, - }, -]; - -const mockNotionIntegration: TIntegrationNotion = { - id: "integration1", - type: "notion", - environmentId: environmentId, - config: { - key: { - access_token: "token", - bot_id: "bot", - workspace_name: "ws", - workspace_icon: "", - } as unknown as TIntegrationNotionCredential, - data: [], // Initially empty - }, -}; - -const mockSelectedIntegration: TIntegrationNotionConfigData & { index: number } = { - databaseId: databases[0].id, - databaseName: databases[0].name, - surveyId: surveys[0].id, - surveyName: surveys[0].name, - mapping: [ - { - column: { id: "p1", name: "Title Prop", type: "title" }, - question: { id: "q1", name: "Question 1?", type: TSurveyQuestionTypeEnum.OpenText }, - }, - { - column: { id: "p2", name: "Text Prop", type: "rich_text" }, - question: { id: "var1", name: "Variable 1", type: TSurveyQuestionTypeEnum.OpenText }, - }, - ], - createdAt: new Date(), - index: 0, -}; - -describe("AddIntegrationModal (Notion)", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - // Reset integration data before each test if needed - mockNotionIntegration.config.data = [ - { ...mockSelectedIntegration }, // Simulate existing data for update/delete tests - ]; - }); - - test("renders correctly when open (create mode)", () => { - render( - - ); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByText("environments.integrations.notion.link_database")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-select-a-database")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-select-survey")).toBeInTheDocument(); - expect(screen.getByText("Cancel")).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "environments.integrations.notion.link_database" }) - ).toBeInTheDocument(); - expect(screen.queryByText("Delete")).not.toBeInTheDocument(); - expect(screen.queryByText("Map Formbricks fields to Notion property")).not.toBeInTheDocument(); - }); - - test("renders correctly when open (update mode)", async () => { - render( - - ); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-select-a-database")).toHaveValue(databases[0].id); - expect(screen.getByTestId("dropdown-select-survey")).toHaveValue(surveys[0].id); - expect(screen.getByText("Map Formbricks fields to Notion property")).toBeInTheDocument(); - - // Check if mapping rows are rendered - await waitFor(() => { - const questionDropdowns = screen.getAllByTestId("dropdown-select-a-survey-question"); - const columnDropdowns = screen.getAllByTestId("dropdown-select-a-field-to-map"); - - expect(questionDropdowns).toHaveLength(2); // Expecting two rows based on mockSelectedIntegration - expect(columnDropdowns).toHaveLength(2); - - // Assert values for the first row - expect(questionDropdowns[0]).toHaveValue("q1"); - expect(columnDropdowns[0]).toHaveValue("p1"); - - // Assert values for the second row - expect(questionDropdowns[1]).toHaveValue("var1"); - expect(columnDropdowns[1]).toHaveValue("p2"); - - expect(screen.getAllByTestId("plus-icon").length).toBeGreaterThan(0); - expect(screen.getAllByTestId("trash-icon").length).toBeGreaterThan(0); - }); - - expect(screen.getByText("Delete")).toBeInTheDocument(); - expect(screen.getByText("Update")).toBeInTheDocument(); - expect(screen.queryByText("Cancel")).not.toBeInTheDocument(); - }); - - test("selects database and survey, shows mapping", async () => { - render( - - ); - - const dbDropdown = screen.getByTestId("dropdown-select-a-database"); - const surveyDropdown = screen.getByTestId("dropdown-select-survey"); - - await userEvent.selectOptions(dbDropdown, databases[0].id); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - - expect(screen.getByText("Map Formbricks fields to Notion property")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-select-a-survey-question")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-select-a-field-to-map")).toBeInTheDocument(); - }); - - test("adds and removes mapping rows", async () => { - render( - - ); - - const dbDropdown = screen.getByTestId("dropdown-select-a-database"); - const surveyDropdown = screen.getByTestId("dropdown-select-survey"); - - await userEvent.selectOptions(dbDropdown, databases[0].id); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - - expect(screen.getAllByTestId("dropdown-select-a-survey-question")).toHaveLength(1); - - const plusButton = screen.getByTestId("plus-icon"); - await userEvent.click(plusButton); - - expect(screen.getAllByTestId("dropdown-select-a-survey-question")).toHaveLength(2); - - const trashButton = screen.getAllByTestId("trash-icon")[0]; // Get the first trash button - await userEvent.click(trashButton); - - expect(screen.getAllByTestId("dropdown-select-a-survey-question")).toHaveLength(1); - }); - - test("deletes integration successfully", async () => { - createOrUpdateIntegrationAction.mockResolvedValue({ data: null as any }); - - render( - - ); - - const deleteButton = screen.getByText("Delete"); - await userEvent.click(deleteButton); - - await waitFor(() => { - expect(createOrUpdateIntegrationAction).toHaveBeenCalledWith({ - environmentId, - integrationData: expect.objectContaining({ - config: expect.objectContaining({ - data: [], // Data array should be empty after deletion - }), - }), - }); - }); - - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith("Integration removed successfully."); - }); - await waitFor(() => { - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - }); - - test("shows validation error if no database selected", async () => { - render( - - ); - await userEvent.selectOptions(screen.getByTestId("dropdown-select-survey"), surveys[0].id); - await userEvent.click( - screen.getByRole("button", { name: "environments.integrations.notion.link_database" }) - ); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Please select a database."); - }); - }); - - test("shows validation error if no survey selected", async () => { - render( - - ); - await userEvent.selectOptions(screen.getByTestId("dropdown-select-a-database"), databases[0].id); - await userEvent.click( - screen.getByRole("button", { name: "environments.integrations.notion.link_database" }) - ); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Please select a survey."); - }); - }); - - test("shows validation error if no mapping defined", async () => { - render( - - ); - await userEvent.selectOptions(screen.getByTestId("dropdown-select-a-database"), databases[0].id); - await userEvent.selectOptions(screen.getByTestId("dropdown-select-survey"), surveys[0].id); - // Default mapping row is empty - await userEvent.click( - screen.getByRole("button", { name: "environments.integrations.notion.link_database" }) - ); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Please select at least one mapping."); - }); - }); - - test("calls setOpen(false) and resets form on cancel", async () => { - render( - - ); - - const dbDropdown = screen.getByTestId("dropdown-select-a-database"); - const cancelButton = screen.getByText("Cancel"); - - await userEvent.selectOptions(dbDropdown, databases[0].id); // Simulate interaction - await userEvent.click(cancelButton); - - expect(mockSetOpen).toHaveBeenCalledWith(false); - // Re-render with open=true to check if state was reset - cleanup(); - render( - - ); - expect(screen.getByTestId("dropdown-select-a-database")).toHaveValue(""); // Should be reset - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/ManageIntegration.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/ManageIntegration.test.tsx deleted file mode 100644 index df1f5da827..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/ManageIntegration.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import type { - TIntegrationNotion, - TIntegrationNotionConfig, - TIntegrationNotionConfigData, - TIntegrationNotionCredential, -} from "@formbricks/types/integration/notion"; -import { ManageIntegration } from "./ManageIntegration"; - -vi.mock("react-hot-toast", () => ({ - default: { success: vi.fn(), error: vi.fn() }, -})); -vi.mock("@/lib/time", () => ({ timeSince: () => "ago" })); -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({ - deleteIntegrationAction: vi.fn(), -})); - -describe("ManageIntegration", () => { - afterEach(() => { - cleanup(); - }); - - const defaultProps = { - environment: {} as any, - locale: "en-US" as const, - setOpenAddIntegrationModal: vi.fn(), - setIsConnected: vi.fn(), - setSelectedIntegration: vi.fn(), - handleNotionAuthorization: vi.fn(), - }; - - test("shows empty state when no databases", () => { - render( - - ); - expect(screen.getByText("environments.integrations.notion.no_databases_found")).toBeInTheDocument(); - }); - - test("renders list and handles clicks", async () => { - const data = [ - { surveyName: "S", databaseName: "D", createdAt: new Date().toISOString(), databaseId: "db" }, - ] as unknown as TIntegrationNotionConfigData[]; - render( - - ); - expect(screen.getByText("S")).toBeInTheDocument(); - await userEvent.click(screen.getByText("S")); - expect(defaultProps.setSelectedIntegration).toHaveBeenCalledWith({ ...data[0], index: 0 }); - expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled(); - }); - - test("update and link new buttons invoke handlers", async () => { - render( - - ); - await userEvent.click(screen.getByText("environments.integrations.notion.update_connection")); - expect(defaultProps.handleNotionAuthorization).toHaveBeenCalled(); - await userEvent.click(screen.getByText("environments.integrations.notion.link_new_database")); - expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/NotionWrapper.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/NotionWrapper.test.tsx deleted file mode 100644 index a19e5f32e1..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/components/NotionWrapper.test.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TIntegrationNotion, TIntegrationNotionCredential } from "@formbricks/types/integration/notion"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/lib/notion"; -import { NotionWrapper } from "./NotionWrapper"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - GOOGLE_SHEETS_CLIENT_SECRET: "test-client-secret", - GOOGLE_SHEETS_REDIRECT_URL: "test-redirect-url", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -// Mock child components -vi.mock("@/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration", () => ({ - ManageIntegration: vi.fn(({ setIsConnected }) => ( -
    - -
    - )), -})); -vi.mock("@/modules/ui/components/connect-integration", () => ({ - ConnectIntegration: vi.fn( - ( - { handleAuthorization, isEnabled } // Reverted back to isEnabled - ) => ( -
    - -
    - ) - ), -})); - -// Mock library function -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/notion/lib/notion", () => ({ - authorize: vi.fn(), -})); - -// Mock image import -vi.mock("@/images/notion-logo.svg", () => ({ - default: "notion-logo-path", -})); - -// Mock window.location.replace -Object.defineProperty(window, "location", { - value: { - replace: vi.fn(), - }, - writable: true, -}); - -const environmentId = "test-env-id"; -const webAppUrl = "https://app.formbricks.com"; -const environment = { id: environmentId } as TEnvironment; -const surveys: TSurvey[] = []; -const databases = []; -const locale = "en-US" as const; - -const mockNotionIntegration: TIntegrationNotion = { - id: "int-notion-123", - type: "notion", - environmentId: environmentId, - config: { - key: { access_token: "test-token" } as TIntegrationNotionCredential, - data: [], - }, -}; - -const baseProps = { - environment, - surveys, - databasesArray: databases, // Renamed databases to databasesArray to match component prop - webAppUrl, - locale, -}; - -describe("NotionWrapper", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders ConnectIntegration disabled when enabled is false", () => { - // Changed description slightly - render(); // Changed isEnabled to enabled - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Connect" })).toBeDisabled(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - }); - - test("renders ConnectIntegration enabled when enabled is true and not connected (no integration)", () => { - // Changed description slightly - render(); // Changed isEnabled to enabled - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - }); - - test("renders ConnectIntegration enabled when enabled is true and not connected (integration without key)", () => { - // Changed description slightly - const integrationWithoutKey = { - ...mockNotionIntegration, - config: { data: [] }, - } as unknown as TIntegrationNotion; - render(); // Changed isEnabled to enabled - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - }); - - test("calls authorize and redirects when Connect button is clicked", async () => { - const mockAuthorize = vi.mocked(authorize); - const redirectUrl = "https://notion.com/auth"; - mockAuthorize.mockImplementation(() => Promise.resolve(redirectUrl)); - - render(); // Changed isEnabled to enabled - - const connectButton = screen.getByRole("button", { name: "Connect" }); - await userEvent.click(connectButton); - - expect(mockAuthorize).toHaveBeenCalledWith(environmentId, webAppUrl); - await waitFor(() => { - expect(window.location.replace).toHaveBeenCalledWith(redirectUrl); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/loading.test.tsx deleted file mode 100644 index f15aa69901..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/loading.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Loading from "./loading"; - -// Mock child components -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, className }: { children: React.ReactNode; className: string }) => ( - - ), -})); -vi.mock("@/modules/ui/components/go-back-button", () => ({ - GoBackButton: () =>
    Go Back
    , -})); - -// Mock @tolgee/react -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, // Simple mock translation - }), -})); - -describe("Notion Integration Loading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders loading state correctly", () => { - render(); - - // Check for GoBackButton mock - expect(screen.getByTestId("go-back-button")).toBeInTheDocument(); - - // Check for the disabled button - const linkButton = screen.getByText("environments.integrations.notion.link_database"); - expect(linkButton).toBeInTheDocument(); - expect(linkButton.closest("button")).toHaveClass( - "pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200" - ); - - // Check for table headers - expect(screen.getByText("common.survey")).toBeInTheDocument(); - expect(screen.getByText("environments.integrations.notion.database_name")).toBeInTheDocument(); - expect(screen.getByText("common.updated_at")).toBeInTheDocument(); - - // Check for placeholder elements (skeleton loaders) - // There should be 3 rows * 5 pulse divs per row = 15 pulse divs - const pulseDivs = screen.getAllByText("", { selector: "div.animate-pulse" }); - expect(pulseDivs.length).toBeGreaterThanOrEqual(15); // Check if at least 15 pulse divs are rendered - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/page.test.tsx deleted file mode 100644 index 5093713d53..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/notion/page.test.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys"; -import Page from "@/app/(app)/environments/[environmentId]/project/integrations/notion/page"; -import { getIntegrationByType } from "@/lib/integration/service"; -import { getNotionDatabases } from "@/lib/notion/service"; -import { findMatchingLocale } from "@/lib/utils/locale"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; - -// Mock dependencies -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/notion/components/NotionWrapper", - () => ({ - NotionWrapper: vi.fn( - ({ enabled, environment, surveys, notionIntegration, webAppUrl, databasesArray, locale }) => ( -
    - Mocked NotionWrapper - {enabled.toString()} - {environment.id} - {surveys?.length ?? 0} - {notionIntegration?.id} - {webAppUrl} - {databasesArray?.length ?? 0} - {locale} -
    - ) - ), - }) -); -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys", () => ({ - getSurveys: vi.fn(), -})); - -let mockNotionClientId: string | undefined = "test-client-id"; -let mockNotionClientSecret: string | undefined = "test-client-secret"; -let mockNotionAuthUrl: string | undefined = "https://notion.com/auth"; -let mockNotionRedirectUri: string | undefined = "https://app.formbricks.com/redirect"; - -vi.mock("@/lib/constants", () => ({ - get NOTION_OAUTH_CLIENT_ID() { - return mockNotionClientId; - }, - get NOTION_OAUTH_CLIENT_SECRET() { - return mockNotionClientSecret; - }, - get NOTION_AUTH_URL() { - return mockNotionAuthUrl; - }, - get NOTION_REDIRECT_URI() { - return mockNotionRedirectUri; - }, - WEBAPP_URL: "test-webapp-url", - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", -})); -vi.mock("@/lib/integration/service", () => ({ - getIntegrationByType: vi.fn(), -})); -vi.mock("@/lib/notion/service", () => ({ - getNotionDatabases: vi.fn(), -})); -vi.mock("@/lib/utils/locale", () => ({ - findMatchingLocale: vi.fn(), -})); -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); -vi.mock("@/modules/ui/components/go-back-button", () => ({ - GoBackButton: vi.fn(({ url }) =>
    {url}
    ), -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: vi.fn(({ children }) =>
    {children}
    ), -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: vi.fn(({ pageTitle }) =>

    {pageTitle}

    ), -})); -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -const mockEnvironment = { - id: "test-env-id", - createdAt: new Date(), - updatedAt: new Date(), - appSetupCompleted: false, - type: "development", -} as unknown as TEnvironment; - -const mockSurveys: TSurvey[] = [ - { - id: "survey1", - name: "Survey 1", - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "test-env-id", - status: "inProgress", - type: "app", - questions: [], - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - languages: [], - pin: null, - segment: null, - singleUse: null, - styling: null, - surveyClosedMessage: null, - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - autoComplete: null, - } as unknown as TSurvey, -]; - -const mockNotionIntegration = { - id: "integration1", - type: "notion", - config: { - data: [], - key: { bot_id: "bot-id-123" }, - email: "test@example.com", - }, -} as unknown as TIntegrationNotion; - -const mockDatabases: TIntegrationNotionDatabase[] = [ - { id: "db1", name: "Database 1", properties: {} }, - { id: "db2", name: "Database 2", properties: {} }, -]; - -const mockProps = { - params: { environmentId: "test-env-id" }, -}; - -describe("NotionIntegrationPage", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: mockEnvironment, - isReadOnly: false, - } as TEnvironmentAuth); - vi.mocked(getSurveys).mockResolvedValue(mockSurveys); - vi.mocked(getIntegrationByType).mockResolvedValue(mockNotionIntegration); - vi.mocked(getNotionDatabases).mockResolvedValue(mockDatabases); - vi.mocked(findMatchingLocale).mockResolvedValue("en-US"); - mockNotionClientId = "test-client-id"; - mockNotionClientSecret = "test-client-secret"; - mockNotionAuthUrl = "https://notion.com/auth"; - mockNotionRedirectUri = "https://app.formbricks.com/redirect"; - }); - - test("renders the page with NotionWrapper when enabled and not read-only", async () => { - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect(screen.getByText("environments.integrations.notion.notion_integration")).toBeInTheDocument(); - expect(screen.getByText("Mocked NotionWrapper")).toBeInTheDocument(); - expect(screen.getByTestId("enabled")).toHaveTextContent("true"); - expect(screen.getByTestId("environmentId")).toHaveTextContent(mockEnvironment.id); - expect(screen.getByTestId("surveyCount")).toHaveTextContent(mockSurveys.length.toString()); - expect(screen.getByTestId("integrationId")).toHaveTextContent(mockNotionIntegration.id); - expect(screen.getByTestId("webAppUrl")).toHaveTextContent("test-webapp-url"); - expect(screen.getByTestId("databaseCount")).toHaveTextContent(mockDatabases.length.toString()); - expect(screen.getByTestId("locale")).toHaveTextContent("en-US"); - expect(screen.getByTestId("go-back")).toHaveTextContent("./"); - expect(vi.mocked(redirect)).not.toHaveBeenCalled(); - expect(vi.mocked(getNotionDatabases)).toHaveBeenCalledWith(mockEnvironment.id); - }); - - test("calls redirect when user is read-only", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: mockEnvironment, - isReadOnly: true, - } as TEnvironmentAuth); - - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect(vi.mocked(redirect)).toHaveBeenCalledWith("./"); - }); - - test("passes enabled=false to NotionWrapper when constants are missing", async () => { - mockNotionClientId = undefined; // Simulate missing constant - - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect(screen.getByTestId("enabled")).toHaveTextContent("false"); - }); - - test("handles case where no Notion integration exists", async () => { - vi.mocked(getIntegrationByType).mockResolvedValue(null); // No integration - - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect(screen.getByText("Mocked NotionWrapper")).toBeInTheDocument(); - expect(screen.getByTestId("integrationId")).toBeEmptyDOMElement(); // No integration ID passed - expect(screen.getByTestId("databaseCount")).toHaveTextContent("0"); // No databases fetched - expect(vi.mocked(getNotionDatabases)).not.toHaveBeenCalled(); - }); - - test("handles case where integration exists but has no key (bot_id)", async () => { - const integrationWithoutKey = { - ...mockNotionIntegration, - config: { ...mockNotionIntegration.config, key: undefined }, - } as unknown as TIntegrationNotion; - vi.mocked(getIntegrationByType).mockResolvedValue(integrationWithoutKey); - - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect(screen.getByText("Mocked NotionWrapper")).toBeInTheDocument(); - expect(screen.getByTestId("integrationId")).toHaveTextContent(integrationWithoutKey.id); - expect(screen.getByTestId("databaseCount")).toHaveTextContent("0"); // No databases fetched - expect(vi.mocked(getNotionDatabases)).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/page.test.tsx deleted file mode 100644 index d4c7366146..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/page.test.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TIntegration } from "@formbricks/types/integration"; -import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/webhook"; -import Page from "@/app/(app)/environments/[environmentId]/project/integrations/page"; -import { getIntegrations } from "@/lib/integration/service"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; - -// Mock dependencies -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/webhook", () => ({ - getWebhookCountBySource: vi.fn(), -})); - -vi.mock("@/lib/integration/service", () => ({ - getIntegrations: vi.fn(), -})); - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -vi.mock("@/modules/ui/components/integration-card", () => ({ - Card: ({ label, description, statusText, disabled }) => ( -
    -

    {label}

    -

    {description}

    - {statusText} - {disabled && Disabled} -
    - ), -})); - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ pageTitle }) =>

    {pageTitle}

    , -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -vi.mock("next/image", () => ({ - // eslint-disable-next-line @next/next/no-img-element - default: ({ alt }) => {alt}, -})); - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -const mockEnvironment = { - id: "test-env-id", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - appSetupCompleted: true, -} as unknown as TEnvironment; - -const mockIntegrations: TIntegration[] = [ - { - id: "google-sheets-id", - type: "googleSheets", - environmentId: "test-env-id", - config: { data: [], email: "test@example.com" } as unknown as TIntegration["config"], - }, - { - id: "slack-id", - type: "slack", - environmentId: "test-env-id", - config: { data: [] } as unknown as TIntegration["config"], - }, -]; - -const mockParams = { environmentId: "test-env-id" }; -const mockProps = { params: mockParams }; - -describe("Integrations Page", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - vi.mocked(getWebhookCountBySource).mockResolvedValue(0); - vi.mocked(getIntegrations).mockResolvedValue([]); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: mockEnvironment, - isReadOnly: false, - isBilling: false, - } as unknown as TEnvironmentAuth); - }); - - test("renders the page header and integration cards", async () => { - vi.mocked(getWebhookCountBySource).mockImplementation(async (envId, source) => { - if (source === "zapier") return 1; - if (source === "user") return 2; - return 0; - }); - vi.mocked(getIntegrations).mockResolvedValue(mockIntegrations); - - const PageComponent = await Page(mockProps); - render(PageComponent); - expect(screen.getByTestId("card-Javascript SDK")).toBeInTheDocument(); - expect( - screen.getByText("environments.integrations.website_or_app_integration_description") - ).toBeInTheDocument(); - expect(screen.getAllByText("common.connected")[0]).toBeInTheDocument(); // JS SDK status - - expect(screen.getByTestId("card-Zapier")).toBeInTheDocument(); - expect(screen.getByText("environments.integrations.zapier_integration_description")).toBeInTheDocument(); - expect(screen.getByText("1 zap")).toBeInTheDocument(); // Zapier status - - expect(screen.getByTestId("card-Webhooks")).toBeInTheDocument(); - expect(screen.getByText("environments.integrations.webhook_integration_description")).toBeInTheDocument(); - expect(screen.getByText("2 webhooks")).toBeInTheDocument(); // Webhook status - - expect(screen.getByTestId("card-Google Sheets")).toBeInTheDocument(); - expect( - screen.getByText("environments.integrations.google_sheet_integration_description") - ).toBeInTheDocument(); - expect(screen.getAllByText("common.connected")[1]).toBeInTheDocument(); // Google Sheets status - - expect(screen.getByTestId("card-Airtable")).toBeInTheDocument(); - expect( - screen.getByText("environments.integrations.airtable_integration_description") - ).toBeInTheDocument(); - expect(screen.getAllByText("common.not_connected")[0]).toBeInTheDocument(); // Airtable status - - expect(screen.getByTestId("card-Slack")).toBeInTheDocument(); - expect(screen.getByText("environments.integrations.slack_integration_description")).toBeInTheDocument(); - expect(screen.getAllByText("common.connected")[2]).toBeInTheDocument(); // Slack status - - expect(screen.getByTestId("card-n8n")).toBeInTheDocument(); - expect(screen.getByText("environments.integrations.n8n_integration_description")).toBeInTheDocument(); - expect(screen.getAllByText("common.not_connected")[1]).toBeInTheDocument(); // n8n status - - expect(screen.getByTestId("card-Make.com")).toBeInTheDocument(); - expect(screen.getByText("environments.integrations.make_integration_description")).toBeInTheDocument(); - expect(screen.getAllByText("common.not_connected")[2]).toBeInTheDocument(); // Make status - - expect(screen.getByTestId("card-Notion")).toBeInTheDocument(); - expect(screen.getByText("environments.integrations.notion_integration_description")).toBeInTheDocument(); - expect(screen.getAllByText("common.not_connected")[3]).toBeInTheDocument(); // Notion status - - expect(screen.getByTestId("card-Activepieces")).toBeInTheDocument(); - expect( - screen.getByText("environments.integrations.activepieces_integration_description") - ).toBeInTheDocument(); - expect(screen.getAllByText("common.not_connected")[4]).toBeInTheDocument(); // Activepieces status - }); - - test("renders disabled cards when isReadOnly is true", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: mockEnvironment, - isReadOnly: true, - isBilling: false, - } as unknown as TEnvironmentAuth); - - const PageComponent = await Page(mockProps); - render(PageComponent); - - // JS SDK and Webhooks should not be disabled - expect(screen.getByTestId("card-Javascript SDK")).not.toHaveTextContent("Disabled"); - expect(screen.getByTestId("card-Webhooks")).not.toHaveTextContent("Disabled"); - - // Other cards should be disabled - expect(screen.getByTestId("card-Zapier")).toHaveTextContent("Disabled"); - expect(screen.getByTestId("card-Google Sheets")).toHaveTextContent("Disabled"); - expect(screen.getByTestId("card-Airtable")).toHaveTextContent("Disabled"); - expect(screen.getByTestId("card-Slack")).toHaveTextContent("Disabled"); - expect(screen.getByTestId("card-n8n")).toHaveTextContent("Disabled"); - expect(screen.getByTestId("card-Make.com")).toHaveTextContent("Disabled"); - expect(screen.getByTestId("card-Notion")).toHaveTextContent("Disabled"); - expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("Disabled"); - }); - - test("redirects when isBilling is true", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: mockEnvironment, - isReadOnly: false, - isBilling: true, - } as unknown as TEnvironmentAuth); - - await Page(mockProps); - - expect(vi.mocked(redirect)).toHaveBeenCalledWith( - `/environments/${mockParams.environmentId}/settings/billing` - ); - }); - - test("renders correct status text for single integration", async () => { - vi.mocked(getWebhookCountBySource).mockImplementation(async (envId, source) => { - if (source === "n8n") return 1; - if (source === "make") return 1; - if (source === "activepieces") return 1; - return 0; - }); - - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect(screen.getByTestId("card-n8n")).toHaveTextContent("1 common.integration"); - expect(screen.getByTestId("card-Make.com")).toHaveTextContent("1 common.integration"); - expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("1 common.integration"); - }); - - test("renders correct status text for multiple integrations", async () => { - vi.mocked(getWebhookCountBySource).mockImplementation(async (envId, source) => { - if (source === "n8n") return 3; - if (source === "make") return 4; - if (source === "activepieces") return 5; - return 0; - }); - - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect(screen.getByTestId("card-n8n")).toHaveTextContent("3 common.integrations"); - expect(screen.getByTestId("card-Make.com")).toHaveTextContent("4 common.integrations"); - expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("5 common.integrations"); - }); - - test("renders not connected status when appSetupCompleted is false", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environment: { ...mockEnvironment, appSetupCompleted: false }, - isReadOnly: false, - isBilling: false, - } as unknown as TEnvironmentAuth); - - const PageComponent = await Page(mockProps); - render(PageComponent); - - expect(screen.getByTestId("card-Javascript SDK")).toHaveTextContent("common.not_connected"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/AddChannelMappingModal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/AddChannelMappingModal.test.tsx deleted file mode 100644 index 9982773040..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/AddChannelMappingModal.test.tsx +++ /dev/null @@ -1,761 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TIntegrationItem } from "@formbricks/types/integration"; -import { - TIntegrationSlack, - TIntegrationSlackConfigData, - TIntegrationSlackCredential, -} from "@formbricks/types/integration/slack"; -import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions"; -import { AddChannelMappingModal } from "./AddChannelMappingModal"; - -// Mock dependencies -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({ - createOrUpdateIntegrationAction: vi.fn(), -})); -vi.mock("@/lib/i18n/utils", () => ({ - getLocalizedValue: (value: any, _locale: string) => value?.default || "", -})); -vi.mock("@/lib/utils/recall", () => ({ - replaceHeadlineRecall: (survey: any) => survey, -})); -vi.mock("@/modules/ui/components/additional-integration-settings", () => ({ - AdditionalIntegrationSettings: ({ - includeVariables, - setIncludeVariables, - includeHiddenFields, - setIncludeHiddenFields, - includeMetadata, - setIncludeMetadata, - includeCreatedAt, - setIncludeCreatedAt, - }: any) => ( -
    - Additional Settings - setIncludeVariables(e.target.checked)} - /> - setIncludeHiddenFields(e.target.checked)} - /> - setIncludeMetadata(e.target.checked)} - /> - setIncludeCreatedAt(e.target.checked)} - /> -
    - ), -})); -vi.mock("@/modules/ui/components/dropdown-selector", () => ({ - DropdownSelector: ({ label, items, selectedItem, setSelectedItem, disabled }: any) => ( -
    - - -
    - ), -})); -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open, onOpenChange }: any) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children, ...props }: any) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: any) =>
    {children}
    , - DialogTitle: ({ children }: any) =>

    {children}

    , - DialogDescription: ({ children }: any) =>

    {children}

    , - DialogBody: ({ children }: any) =>
    {children}
    , - DialogFooter: ({ children }: any) =>
    {children}
    , -})); -vi.mock("next/image", () => ({ - // eslint-disable-next-line @next/next/no-img-element - default: ({ src, alt }: { src: string; alt: string }) => {alt}, -})); -vi.mock("next/link", () => ({ - default: ({ href, children, ...props }: any) => ( - - {children} - - ), -})); -vi.mock("react-hook-form", () => ({ - useForm: () => ({ - handleSubmit: (callback: any) => (event: any) => { - event.preventDefault(); - callback(); - }, - }), -})); -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); -vi.mock("@tolgee/react", async () => { - const MockTolgeeProvider = ({ children }: { children: React.ReactNode }) => <>{children}; - const useTranslate = () => ({ - t: (key: string, _?: any) => { - // NOSONAR - // Simple mock translation function - if (key === "common.all_questions") return "All questions"; - if (key === "common.selected_questions") return "Selected questions"; - if (key === "environments.integrations.slack.link_slack_channel") return "Link Slack Channel"; - if (key === "environments.integrations.slack.slack_integration_description") - return "Send responses directly to Slack."; - if (key === "common.update") return "Update"; - if (key === "common.delete") return "Delete"; - if (key === "common.cancel") return "Cancel"; - if (key === "environments.integrations.slack.select_channel") return "Select channel"; - if (key === "common.select_survey") return "Select survey"; - if (key === "common.questions") return "Questions"; - if (key === "environments.integrations.slack.please_select_a_channel") - return "Please select a channel."; - if (key === "environments.integrations.please_select_a_survey_error") return "Please select a survey."; - if (key === "environments.integrations.select_at_least_one_question_error") - return "Please select at least one question."; - if (key === "environments.integrations.integration_updated_successfully") - return "Integration updated successfully."; - if (key === "environments.integrations.integration_added_successfully") - return "Integration added successfully."; - if (key === "environments.integrations.integration_removed_successfully") - return "Integration removed successfully."; - if (key === "environments.integrations.slack.dont_see_your_channel") return "Don't see your channel?"; - if (key === "common.note") return "Note"; - if (key === "environments.integrations.slack.already_connected_another_survey") - return "This channel is already connected to another survey."; - if (key === "environments.integrations.slack.create_at_least_one_channel_error") - return "Please create at least one channel in Slack first."; - if (key === "environments.integrations.create_survey_warning") - return "You need to create a survey first."; - if (key === "environments.integrations.slack.link_channel") return "Link Channel"; - return key; // Return key if no translation is found - }, - }); - return { TolgeeProvider: MockTolgeeProvider, useTranslate }; -}); -vi.mock("lucide-react", () => ({ - CircleHelpIcon: () =>
    , - Check: () =>
    , // Add the Check icon mock - Loader2: () =>
    , // Add the Loader2 icon mock -})); - -// Mock dependencies -const createOrUpdateIntegrationActionMock = vi.mocked(createOrUpdateIntegrationAction); -const toast = vi.mocked((await import("react-hot-toast")).default); - -const environmentId = "test-env-id"; -const mockSetOpen = vi.fn(); - -const surveys: TSurvey[] = [ - { - id: "survey1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Survey 1", - type: "app", - environmentId: environmentId, - status: "inProgress", - questions: [ - { - id: "q1", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Question 1?" }, - required: true, - } as unknown as TSurveyQuestion, - { - id: "q2", - type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: "Question 2?" }, - required: false, - choices: [ - { id: "c1", label: { default: "Choice 1" } }, - { id: "c2", label: { default: "Choice 2" } }, - ], - }, - ], - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - autoComplete: null, - singleUse: null, - styling: null, - surveyClosedMessage: null, - segment: null, - languages: [], - variables: [], - welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"], - hiddenFields: { enabled: true, fieldIds: [] }, - pin: null, - displayLimit: null, - } as unknown as TSurvey, - { - id: "survey2", - createdAt: new Date(), - updatedAt: new Date(), - name: "Survey 2", - type: "link", - environmentId: environmentId, - status: "draft", - questions: [ - { - id: "q3", - type: TSurveyQuestionTypeEnum.Rating, - headline: { default: "Rate this?" }, - required: true, - scale: "number", - range: 5, - } as unknown as TSurveyQuestion, - ], - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - autoComplete: null, - singleUse: null, - styling: null, - surveyClosedMessage: null, - segment: null, - languages: [], - variables: [], - welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"], - hiddenFields: { enabled: true, fieldIds: [] }, - pin: null, - displayLimit: null, - } as unknown as TSurvey, -]; - -const channels: TIntegrationItem[] = [ - { id: "channel1", name: "#general" }, - { id: "channel2", name: "#random" }, -]; - -const mockSlackIntegration: TIntegrationSlack = { - id: "integration1", - type: "slack", - environmentId: environmentId, - config: { - key: { - access_token: "xoxb-test-token", - team_name: "Test Team", - team_id: "T123", - } as unknown as TIntegrationSlackCredential, - data: [], // Initially empty - }, -}; - -const mockSelectedIntegration: TIntegrationSlackConfigData & { index: number } = { - channelId: channels[0].id, - channelName: channels[0].name, - surveyId: surveys[0].id, - surveyName: surveys[0].name, - questionIds: [surveys[0].questions[0].id], - questions: "Selected questions", - createdAt: new Date(), - includeVariables: true, - includeHiddenFields: false, - includeMetadata: true, - includeCreatedAt: false, - index: 0, -}; - -describe("AddChannelMappingModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - // Reset integration data before each test if needed - mockSlackIntegration.config.data = [ - { ...mockSelectedIntegration }, // Simulate existing data for update/delete tests - ]; - }); - - test("renders correctly when open (create mode)", () => { - render( - - ); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Link Slack Channel"); - expect(screen.getByTestId("dialog-description")).toHaveTextContent("Send responses directly to Slack."); - expect(screen.getByTestId("channel-dropdown")).toBeInTheDocument(); - expect(screen.getByTestId("survey-dropdown")).toBeInTheDocument(); - expect(screen.getByText("Cancel")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Link Channel" })).toBeInTheDocument(); - expect(screen.queryByText("Delete")).not.toBeInTheDocument(); - expect(screen.queryByText("Questions")).not.toBeInTheDocument(); - expect(screen.getByTestId("circle-help-icon")).toBeInTheDocument(); - expect(screen.getByText("Don't see your channel?")).toBeInTheDocument(); - }); - - test("renders correctly when open (update mode)", () => { - render( - - ); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Link Slack Channel"); - expect(screen.getByTestId("dialog-description")).toHaveTextContent("Send responses directly to Slack."); - expect(screen.getByTestId("channel-dropdown")).toHaveValue(channels[0].id); - expect(screen.getByTestId("survey-dropdown")).toHaveValue(surveys[0].id); - expect(screen.getByText("Questions")).toBeInTheDocument(); - expect(screen.getByText("Delete")).toBeInTheDocument(); - expect(screen.getByText("Update")).toBeInTheDocument(); - expect(screen.queryByText("Cancel")).not.toBeInTheDocument(); - expect(screen.getByTestId("include-variables")).toBeChecked(); - expect(screen.getByTestId("include-hidden-fields")).not.toBeChecked(); - expect(screen.getByTestId("include-metadata")).toBeChecked(); - expect(screen.getByTestId("include-created-at")).not.toBeChecked(); - }); - - test("selects survey and shows questions", async () => { - render( - - ); - - const surveyDropdown = screen.getByTestId("survey-dropdown"); - await userEvent.selectOptions(surveyDropdown, surveys[1].id); - - expect(screen.getByText("Questions")).toBeInTheDocument(); - surveys[1].questions.forEach((q) => { - expect(screen.getByLabelText(q.headline.default)).toBeInTheDocument(); - // Initially all questions should be checked when a survey is selected in create mode - expect(screen.getByLabelText(q.headline.default)).toBeChecked(); - }); - }); - - test("handles question selection", async () => { - render( - - ); - - const surveyDropdown = screen.getByTestId("survey-dropdown"); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - - const firstQuestionCheckbox = screen.getByLabelText(surveys[0].questions[0].headline.default); - expect(firstQuestionCheckbox).toBeChecked(); // Initially checked - - await userEvent.click(firstQuestionCheckbox); - expect(firstQuestionCheckbox).not.toBeChecked(); // Unchecked after click - - await userEvent.click(firstQuestionCheckbox); - expect(firstQuestionCheckbox).toBeChecked(); // Checked again - }); - - test("creates integration successfully", async () => { - createOrUpdateIntegrationActionMock.mockResolvedValue({ data: null as any }); // Mock successful action - - render( - - ); - - const channelDropdown = screen.getByTestId("channel-dropdown"); - const surveyDropdown = screen.getByTestId("survey-dropdown"); - const submitButton = screen.getByRole("button", { name: "Link Channel" }); - - await userEvent.selectOptions(channelDropdown, channels[1].id); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - - // Wait for questions to appear and potentially uncheck one - const firstQuestionCheckbox = await screen.findByLabelText(surveys[0].questions[0].headline.default); - await userEvent.click(firstQuestionCheckbox); // Uncheck first question - - // Check additional settings - await userEvent.click(screen.getByTestId("include-variables")); - await userEvent.click(screen.getByTestId("include-metadata")); - - await userEvent.click(submitButton); - - await waitFor(() => { - expect(createOrUpdateIntegrationActionMock).toHaveBeenCalledWith({ - environmentId, - integrationData: expect.objectContaining({ - type: "slack", - config: expect.objectContaining({ - key: mockSlackIntegration.config.key, - data: expect.arrayContaining([ - expect.objectContaining({ - channelId: channels[1].id, - channelName: channels[1].name, - surveyId: surveys[0].id, - surveyName: surveys[0].name, - questionIds: surveys[0].questions.slice(1).map((q) => q.id), // Excludes the first question - questions: "Selected questions", - includeVariables: true, - includeHiddenFields: false, - includeMetadata: true, - includeCreatedAt: true, // Default - }), - ]), - }), - }), - }); - }); - - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith("Integration added successfully."); - }); - await waitFor(() => { - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - }); - - test("deletes integration successfully", async () => { - createOrUpdateIntegrationActionMock.mockResolvedValue({ data: null as any }); - - render( - - ); - - const deleteButton = screen.getByText("Delete"); - await userEvent.click(deleteButton); - - await waitFor(() => { - expect(createOrUpdateIntegrationActionMock).toHaveBeenCalledWith({ - environmentId, - integrationData: expect.objectContaining({ - config: expect.objectContaining({ - data: [], // Data array should be empty after deletion - }), - }), - }); - }); - - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith("Integration removed successfully."); - }); - await waitFor(() => { - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - }); - - test("shows validation error if no channel selected", async () => { - render( - - ); - - const surveyDropdown = screen.getByTestId("survey-dropdown"); - const submitButton = screen.getByRole("button", { name: "Link Channel" }); - - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - // No channel selected - await userEvent.click(submitButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Please select a channel."); - }); - expect(createOrUpdateIntegrationActionMock).not.toHaveBeenCalled(); - expect(mockSetOpen).not.toHaveBeenCalled(); - }); - - test("shows validation error if no survey selected", async () => { - render( - - ); - - const channelDropdown = screen.getByTestId("channel-dropdown"); - const submitButton = screen.getByRole("button", { name: "Link Channel" }); - - await userEvent.selectOptions(channelDropdown, channels[0].id); - // No survey selected - await userEvent.click(submitButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Please select a survey."); - }); - expect(createOrUpdateIntegrationActionMock).not.toHaveBeenCalled(); - expect(mockSetOpen).not.toHaveBeenCalled(); - }); - - test("shows validation error if no questions selected", async () => { - render( - - ); - - const channelDropdown = screen.getByTestId("channel-dropdown"); - const surveyDropdown = screen.getByTestId("survey-dropdown"); - const submitButton = screen.getByRole("button", { name: "Link Channel" }); - - await userEvent.selectOptions(channelDropdown, channels[0].id); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - - // Uncheck all questions - for (const question of surveys[0].questions) { - const checkbox = await screen.findByLabelText(question.headline.default); - await userEvent.click(checkbox); - } - - await userEvent.click(submitButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Please select at least one question."); - }); - expect(createOrUpdateIntegrationActionMock).not.toHaveBeenCalled(); - expect(mockSetOpen).not.toHaveBeenCalled(); - }); - - test("shows error toast if createOrUpdateIntegrationAction fails", async () => { - const errorMessage = "Failed to update integration"; - createOrUpdateIntegrationActionMock.mockRejectedValue(new Error(errorMessage)); - - render( - - ); - - const channelDropdown = screen.getByTestId("channel-dropdown"); - const surveyDropdown = screen.getByTestId("survey-dropdown"); - const submitButton = screen.getByRole("button", { name: "Link Channel" }); - - await userEvent.selectOptions(channelDropdown, channels[0].id); - await userEvent.selectOptions(surveyDropdown, surveys[0].id); - await userEvent.click(submitButton); - - await waitFor(() => { - expect(createOrUpdateIntegrationActionMock).toHaveBeenCalled(); - }); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith(errorMessage); - }); - expect(mockSetOpen).not.toHaveBeenCalled(); - }); - - test("calls setOpen(false) and resets form on cancel", async () => { - render( - - ); - - const channelDropdown = screen.getByTestId("channel-dropdown"); - const cancelButton = screen.getByText("Cancel"); - - // Simulate some interaction - await userEvent.selectOptions(channelDropdown, channels[0].id); - await userEvent.click(cancelButton); - - expect(mockSetOpen).toHaveBeenCalledWith(false); - // Re-render with open=true to check if state was reset (channel should be unselected) - cleanup(); - render( - - ); - expect(screen.getByTestId("channel-dropdown")).toHaveValue(""); - }); - - test("shows warning when selected channel is already connected (add mode)", async () => { - // Add an existing connection for channel1 - const integrationWithExisting = { - ...mockSlackIntegration, - config: { - ...mockSlackIntegration.config, - data: [ - { - channelId: "channel1", - channelName: "#general", - surveyId: "survey-other", - surveyName: "Other Survey", - questionIds: ["q-other"], - questions: "All questions", - createdAt: new Date(), - } as TIntegrationSlackConfigData, - ], - }, - }; - - render( - - ); - - const channelDropdown = screen.getByTestId("channel-dropdown"); - await userEvent.selectOptions(channelDropdown, "channel1"); - - expect(screen.getByText("This channel is already connected to another survey.")).toBeInTheDocument(); - }); - - test("does not show warning when selected channel is the one being edited", async () => { - // Edit the existing connection for channel1 - const integrationToEdit = { - ...mockSlackIntegration, - config: { - ...mockSlackIntegration.config, - data: [ - { - channelId: "channel1", - channelName: "#general", - surveyId: "survey1", - surveyName: "Survey 1", - questionIds: ["q1"], - questions: "Selected questions", - createdAt: new Date(), - index: 0, - } as TIntegrationSlackConfigData & { index: number }, - ], - }, - }; - const selectedIntegrationForEdit = integrationToEdit.config.data[0]; - - render( - - ); - - const channelDropdown = screen.getByTestId("channel-dropdown"); - // Channel is already selected via selectedIntegration prop - expect(channelDropdown).toHaveValue("channel1"); - - expect( - screen.queryByText("This channel is already connected to another survey.") - ).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/ManageIntegration.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/ManageIntegration.test.tsx deleted file mode 100644 index f76a5e4541..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/ManageIntegration.test.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack"; -import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions"; -import { ManageIntegration } from "./ManageIntegration"; - -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({ - deleteIntegrationAction: vi.fn(), -})); -vi.mock("react-hot-toast", () => ({ default: { success: vi.fn(), error: vi.fn() } })); -vi.mock("@/modules/ui/components/delete-dialog", () => ({ - DeleteDialog: ({ open, setOpen, onDelete }: any) => - open ? ( -
    - - -
    - ) : null, -})); -vi.mock("@/modules/ui/components/empty-space-filler", () => ({ - EmptySpaceFiller: ({ emptyMessage }: any) =>
    {emptyMessage}
    , -})); - -const baseProps = { - environment: { id: "env1" } as TEnvironment, - setOpenAddIntegrationModal: vi.fn(), - setIsConnected: vi.fn(), - setSelectedIntegration: vi.fn(), - refreshChannels: vi.fn(), - handleSlackAuthorization: vi.fn(), - showReconnectButton: false, - locale: "en-US" as const, -}; - -describe("ManageIntegration (Slack)", () => { - afterEach(() => cleanup()); - - test("empty state", () => { - render( - - ); - expect(screen.getByText(/connect_your_first_slack_channel/)).toBeInTheDocument(); - expect(screen.getByText(/link_channel/)).toBeInTheDocument(); - }); - - test("link channel triggers handlers", async () => { - render( - - ); - await userEvent.click(screen.getByText(/link_channel/)); - expect(baseProps.refreshChannels).toHaveBeenCalled(); - expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null); - expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true); - }); - - test("show reconnect button and triggers authorization", async () => { - render( - - ); - expect(screen.getByText("environments.integrations.slack.slack_reconnect_button")).toBeInTheDocument(); - await userEvent.click(screen.getByText("environments.integrations.slack.slack_reconnect_button")); - expect(baseProps.handleSlackAuthorization).toHaveBeenCalled(); - }); - - test("list integrations and open edit", async () => { - const item = { - surveyName: "S", - channelName: "C", - questions: "Q", - createdAt: new Date().toISOString(), - surveyId: "s", - channelId: "c", - } as unknown as TIntegrationSlackConfigData; - render( - - ); - expect(screen.getByText("S")).toBeInTheDocument(); - await userEvent.click(screen.getByText("S")); - expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({ ...item, index: 0 }); - expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true); - }); - - test("delete integration success", async () => { - vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any); - render( - - ); - await userEvent.click(screen.getByText(/delete_integration/)); - expect(screen.getByTestId("delete-dialog")).toBeInTheDocument(); - await userEvent.click(screen.getByText("confirm")); - expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" }); - const { default: toast } = await import("react-hot-toast"); - expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully"); - expect(baseProps.setIsConnected).toHaveBeenCalledWith(false); - }); - - test("delete integration error", async () => { - vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any); - render( - - ); - await userEvent.click(screen.getByText(/delete_integration/)); - await userEvent.click(screen.getByText("confirm")); - const { default: toast } = await import("react-hot-toast"); - expect(toast.error).toHaveBeenCalledWith(expect.any(String)); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper.test.tsx deleted file mode 100644 index d67ab93c67..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper.test.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TIntegrationItem } from "@formbricks/types/integration"; -import { TIntegrationSlack, TIntegrationSlackCredential } from "@formbricks/types/integration/slack"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TUserLocale } from "@formbricks/types/user"; -import { getSlackChannelsAction } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/actions"; -import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper"; -import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/lib/slack"; - -// Mock child components and actions -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/slack/actions", () => ({ - getSlackChannelsAction: vi.fn(), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/AddChannelMappingModal", - () => ({ - AddChannelMappingModal: vi.fn(({ open }) => (open ?
    Add Modal
    : null)), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/ManageIntegration", - () => ({ - ManageIntegration: vi.fn(({ setOpenAddIntegrationModal, setIsConnected, handleSlackAuthorization }) => ( -
    - - - -
    - )), - }) -); - -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/slack/lib/slack", () => ({ - authorize: vi.fn(), -})); - -vi.mock("@/images/slacklogo.png", () => ({ - default: "slack-logo-path", -})); - -vi.mock("@/modules/ui/components/connect-integration", () => ({ - ConnectIntegration: vi.fn(({ handleAuthorization, isEnabled }) => ( -
    - -
    - )), -})); - -// Mock window.location.replace -Object.defineProperty(window, "location", { - value: { - replace: vi.fn(), - }, - writable: true, -}); - -const mockEnvironment = { id: "test-env-id" } as TEnvironment; -const mockSurveys: TSurvey[] = []; -const mockWebAppUrl = "http://localhost:3000"; -const mockLocale: TUserLocale = "en-US"; -const mockSlackChannels: TIntegrationItem[] = [{ id: "C123", name: "general" }]; - -const mockSlackIntegration: TIntegrationSlack = { - id: "slack-int-1", - type: "slack", - environmentId: "test-env-id", - config: { - key: { access_token: "xoxb-valid-token" } as unknown as TIntegrationSlackCredential, - data: [], - }, -}; - -const baseProps = { - environment: mockEnvironment, - surveys: mockSurveys, - webAppUrl: mockWebAppUrl, - locale: mockLocale, -}; - -describe("SlackWrapper", () => { - beforeEach(() => { - vi.mocked(getSlackChannelsAction).mockResolvedValue({ data: mockSlackChannels }); - vi.mocked(authorize).mockResolvedValue("https://slack.com/auth"); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders ConnectIntegration when not connected (no integration)", () => { - render(); - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled(); - }); - - test("renders ConnectIntegration when not connected (integration without key)", () => { - const integrationWithoutKey = { ...mockSlackIntegration, config: { data: [], email: "test" } } as any; - render(); - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - }); - - test("renders ConnectIntegration disabled when isEnabled is false", () => { - render(); - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Connect" })).toBeDisabled(); - }); - - test("calls authorize and redirects when Connect button is clicked", async () => { - render(); - const connectButton = screen.getByRole("button", { name: "Connect" }); - await userEvent.click(connectButton); - - expect(authorize).toHaveBeenCalledWith(mockEnvironment.id, mockWebAppUrl); - await waitFor(() => { - expect(window.location.replace).toHaveBeenCalledWith("https://slack.com/auth"); - }); - }); - - test("renders ManageIntegration and AddChannelMappingModal (hidden) when connected", () => { - render(); - expect(screen.getByTestId("manage-integration")).toBeInTheDocument(); - expect(screen.queryByTestId("connect-integration")).not.toBeInTheDocument(); - expect(screen.queryByTestId("add-modal")).not.toBeInTheDocument(); // Modal is initially hidden - }); - - test("calls getSlackChannelsAction on mount", async () => { - render(); - await waitFor(() => { - expect(getSlackChannelsAction).toHaveBeenCalledWith({ environmentId: mockEnvironment.id }); - }); - }); - - test("switches from ManageIntegration to ConnectIntegration when disconnected", async () => { - render(); - expect(screen.getByTestId("manage-integration")).toBeInTheDocument(); - - const disconnectButton = screen.getByRole("button", { name: "Disconnect" }); - await userEvent.click(disconnectButton); - - expect(screen.getByTestId("connect-integration")).toBeInTheDocument(); - expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument(); - }); - - test("opens AddChannelMappingModal when triggered from ManageIntegration", async () => { - render(); - expect(screen.queryByTestId("add-modal")).not.toBeInTheDocument(); - - const openModalButton = screen.getByRole("button", { name: "Open Modal" }); - await userEvent.click(openModalButton); - - expect(screen.getByTestId("add-modal")).toBeInTheDocument(); - }); - - test("calls handleSlackAuthorization when reconnect button is clicked in ManageIntegration", async () => { - render(); - const reconnectButton = screen.getByRole("button", { name: "Reconnect" }); - await userEvent.click(reconnectButton); - - expect(authorize).toHaveBeenCalledWith(mockEnvironment.id, mockWebAppUrl); - await waitFor(() => { - expect(window.location.replace).toHaveBeenCalledWith("https://slack.com/auth"); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/page.test.tsx deleted file mode 100644 index b5643e9e66..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/slack/page.test.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TIntegrationSlack, TIntegrationSlackCredential } from "@formbricks/types/integration/slack"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys"; -import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper"; -import Page from "@/app/(app)/environments/[environmentId]/project/integrations/slack/page"; -import { getIntegrationByType } from "@/lib/integration/service"; -import { findMatchingLocale } from "@/lib/utils/locale"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; - -// Mock dependencies -vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys", () => ({ - getSurveys: vi.fn(), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper", - () => ({ - SlackWrapper: vi.fn(({ isEnabled, environment, surveys, slackIntegration, webAppUrl, locale }) => ( -
    - Mock SlackWrapper: isEnabled={isEnabled.toString()}, envId={environment.id}, surveys= - {surveys.length}, integrationId={slackIntegration?.id}, webAppUrl={webAppUrl}, locale={locale} -
    - )), - }) -); - -vi.mock("@/lib/constants", () => ({ - IS_PRODUCTION: true, - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - SENTRY_DSN: "mock-sentry-dsn", - SLACK_CLIENT_ID: "test-slack-client-id", - SLACK_CLIENT_SECRET: "test-slack-client-secret", - WEBAPP_URL: "http://test.formbricks.com", -})); - -vi.mock("@/lib/integration/service", () => ({ - getIntegrationByType: vi.fn(), -})); - -vi.mock("@/lib/utils/locale", () => ({ - findMatchingLocale: vi.fn(), -})); - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -vi.mock("@/modules/ui/components/go-back-button", () => ({ - GoBackButton: vi.fn(({ url }) =>
    Go Back: {url}
    ), -})); - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: vi.fn(({ children }) =>
    {children}
    ), -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: vi.fn(({ pageTitle }) =>

    {pageTitle}

    ), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -// Mock data -const environmentId = "test-env-id"; -const mockEnvironment = { - id: environmentId, - createdAt: new Date(), - type: "development", -} as unknown as TEnvironment; -const mockSurveys: TSurvey[] = [ - { - id: "survey1", - name: "Survey 1", - createdAt: new Date(), - updatedAt: new Date(), - environmentId: environmentId, - status: "inProgress", - type: "link", - questions: [], - triggers: [], - recontactDays: null, - displayOption: "displayOnce", - autoClose: null, - delay: 0, - autoComplete: null, - surveyClosedMessage: null, - singleUse: null, - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - hiddenFields: { enabled: false }, - languages: [], - styling: null, - segment: null, - displayPercentage: null, - } as unknown as TSurvey, -]; -const mockSlackIntegration = { - id: "slack-int-id", - type: "slack", - config: { - data: [], - key: "test-key" as unknown as TIntegrationSlackCredential, - }, -} as unknown as TIntegrationSlack; -const mockLocale = "en-US"; -const mockParams = { params: { environmentId } }; - -describe("SlackIntegrationPage", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - vi.mocked(getSurveys).mockResolvedValue(mockSurveys); - vi.mocked(getIntegrationByType).mockResolvedValue(mockSlackIntegration); - vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale); - }); - - test("renders correctly when user is not read-only", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - isReadOnly: false, - environment: mockEnvironment, - } as unknown as TEnvironmentAuth); - - const tree = await Page(mockParams); - render(tree); - - expect(screen.getByTestId("page-header")).toHaveTextContent( - "environments.integrations.slack.slack_integration" - ); - expect(screen.getByTestId("go-back-button")).toHaveTextContent( - `Go Back: http://test.formbricks.com/environments/${environmentId}/project/integrations` - ); - expect(screen.getByTestId("slack-wrapper")).toBeInTheDocument(); - - // Check props passed to SlackWrapper - expect(vi.mocked(SlackWrapper)).toHaveBeenCalledWith( - { - isEnabled: true, // Since SLACK_CLIENT_ID and SLACK_CLIENT_SECRET are mocked - environment: mockEnvironment, - surveys: mockSurveys, - slackIntegration: mockSlackIntegration, - webAppUrl: "http://test.formbricks.com", - locale: mockLocale, - }, - undefined - ); - - expect(vi.mocked(redirect)).not.toHaveBeenCalled(); - }); - - test("redirects when user is read-only", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - isReadOnly: true, - environment: mockEnvironment, - } as unknown as TEnvironmentAuth); - - // Need to actually call the component function to trigger the redirect logic - await Page(mockParams); - - expect(vi.mocked(redirect)).toHaveBeenCalledWith("./"); - expect(vi.mocked(SlackWrapper)).not.toHaveBeenCalled(); - }); - - test("renders correctly when Slack integration is not configured", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - isReadOnly: false, - environment: mockEnvironment, - } as unknown as TEnvironmentAuth); - vi.mocked(getIntegrationByType).mockResolvedValue(null); // Simulate no integration found - - const tree = await Page(mockParams); - render(tree); - - expect(screen.getByTestId("page-header")).toHaveTextContent( - "environments.integrations.slack.slack_integration" - ); - expect(screen.getByTestId("slack-wrapper")).toBeInTheDocument(); - - // Check props passed to SlackWrapper when integration is null - expect(vi.mocked(SlackWrapper)).toHaveBeenCalledWith( - { - isEnabled: true, - environment: mockEnvironment, - surveys: mockSurveys, - slackIntegration: null, // Expecting null here - webAppUrl: "http://test.formbricks.com", - locale: mockLocale, - }, - undefined - ); - - expect(vi.mocked(redirect)).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/integrations/webhooks/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/integrations/webhooks/page.test.tsx deleted file mode 100644 index 4fc079ffad..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/integrations/webhooks/page.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, test, vi } from "vitest"; -import WebhooksPage from "./page"; - -vi.mock("@/modules/integrations/webhooks/page", () => ({ - WebhooksPage: vi.fn(() =>
    WebhooksPageMock
    ), -})); - -describe("WebhooksIntegrationPage", () => { - test("renders WebhooksPage component", () => { - render(); - expect(WebhooksPage).toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/languages/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/languages/loading.test.tsx deleted file mode 100644 index 2202a8aac6..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/languages/loading.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { LanguagesLoading as OriginalLanguagesLoading } from "@/modules/ee/languages/loading"; -import LanguagesLoading from "./loading"; - -// Mock the original component to ensure we are testing the re-export -vi.mock("@/modules/ee/languages/loading", () => ({ - LanguagesLoading: () =>
    Mock LanguagesLoading
    , -})); - -describe("LanguagesLoadingPage Re-export", () => { - test("should re-export LanguagesLoading from the correct module", () => { - // Check if the re-exported component is the same as the original (mocked) component - expect(LanguagesLoading).toBe(OriginalLanguagesLoading); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/languages/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/languages/page.test.tsx deleted file mode 100644 index 2931981bbf..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/languages/page.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { LanguagesPage } from "@/modules/ee/languages/page"; -import Page from "./page"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: 1, -})); - -describe("LanguagesPage re-export", () => { - test("should re-export LanguagesPage component", () => { - expect(Page).toBe(LanguagesPage); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/layout.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/layout.test.tsx deleted file mode 100644 index 788d7fff09..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/layout.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { cleanup, render } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import ProjectLayout, { metadata as layoutMetadata } from "./layout"; - -vi.mock("@/modules/projects/settings/layout", () => ({ - ProjectSettingsLayout: ({ children }) =>
    {children}
    , - metadata: { title: "Mocked Project Settings" }, -})); - -describe("ProjectLayout", () => { - afterEach(() => { - cleanup(); - }); - - test("renders ProjectSettingsLayout", () => { - const { getByTestId } = render(Child Content); - expect(getByTestId("project-settings-layout")).toBeInTheDocument(); - expect(getByTestId("project-settings-layout")).toHaveTextContent("Child Content"); - }); - - test("exports metadata from @/modules/projects/settings/layout", () => { - expect(layoutMetadata).toEqual({ title: "Mocked Project Settings" }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/look/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/look/loading.test.tsx deleted file mode 100644 index e20338d0de..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/look/loading.test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { ProjectLookSettingsLoading as OriginalProjectLookSettingsLoading } from "@/modules/projects/settings/look/loading"; -import ProjectLookSettingsLoading from "./loading"; - -// Mock the original component to ensure we are testing the re-export -vi.mock("@/modules/projects/settings/look/loading", () => ({ - ProjectLookSettingsLoading: () => ( -
    Mock ProjectLookSettingsLoading
    - ), -})); - -describe("ProjectLookSettingsLoadingPage Re-export", () => { - test("should re-export ProjectLookSettingsLoading from the correct module", () => { - // Check if the re-exported component is the same as the original (mocked) component - expect(ProjectLookSettingsLoading).toBe(OriginalProjectLookSettingsLoading); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/look/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/look/page.test.tsx deleted file mode 100644 index 5a3d60eff4..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/look/page.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { ProjectLookSettingsPage } from "@/modules/projects/settings/look/page"; -import Page from "./page"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: 1, -})); - -vi.mock("@/lib/env", () => ({ - env: { - PUBLIC_URL: "https://public-domain.com", - }, -})); - -describe("ProjectLookSettingsPage re-export", () => { - test("should re-export ProjectLookSettingsPage component", () => { - expect(Page).toBe(ProjectLookSettingsPage); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/page.test.tsx deleted file mode 100644 index eff3cf2a07..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/page.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { ProjectSettingsPage } from "@/modules/projects/settings/page"; -import Page from "./page"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", -})); - -describe("ProjectSettingsPage re-export", () => { - test("should re-export ProjectSettingsPage component", () => { - expect(Page).toBe(ProjectSettingsPage); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/tags/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/tags/loading.test.tsx deleted file mode 100644 index b6821578ef..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/tags/loading.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { TagsLoading as OriginalTagsLoading } from "@/modules/projects/settings/tags/loading"; -import TagsLoading from "./loading"; - -// Mock the original component to ensure we are testing the re-export -vi.mock("@/modules/projects/settings/tags/loading", () => ({ - TagsLoading: () =>
    Mock TagsLoading
    , -})); - -describe("TagsLoadingPage Re-export", () => { - test("should re-export TagsLoading from the correct module", () => { - // Check if the re-exported component is the same as the original (mocked) component - expect(TagsLoading).toBe(OriginalTagsLoading); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/tags/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/tags/page.test.tsx deleted file mode 100644 index 07d84920e9..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/tags/page.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { TagsPage } from "@/modules/projects/settings/tags/page"; -import Page from "./page"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: 1, -})); - -describe("TagsPage re-export", () => { - test("should re-export TagsPage component", () => { - expect(Page).toBe(TagsPage); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/project/teams/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/project/teams/page.test.tsx deleted file mode 100644 index a8651a90de..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/project/teams/page.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { ProjectTeams } from "@/modules/ee/teams/project-teams/page"; -import Page from "./page"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -describe("ProjectTeams re-export", () => { - test("should re-export ProjectTeams component", () => { - expect(Page).toBe(ProjectTeams); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar.test.tsx deleted file mode 100644 index 83afc6ac73..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar.test.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { cleanup, render } from "@testing-library/react"; -import { usePathname } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation"; -import { AccountSettingsNavbar } from "./AccountSettingsNavbar"; - -vi.mock("next/navigation", () => ({ - usePathname: vi.fn(), -})); - -vi.mock("@/modules/ui/components/secondary-navigation", () => ({ - SecondaryNavigation: vi.fn(() =>
    SecondaryNavigationMock
    ), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - if (key === "common.profile") return "Profile"; - if (key === "common.notifications") return "Notifications"; - return key; - }, - }), -})); - -describe("AccountSettingsNavbar", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - test("renders correctly and sets profile as current when pathname includes /profile", () => { - vi.mocked(usePathname).mockReturnValue("/environments/testEnvId/settings/profile"); - render(); - - expect(SecondaryNavigation).toHaveBeenCalledWith( - { - navigation: [ - { - id: "profile", - label: "Profile", - href: "/environments/testEnvId/settings/profile", - current: true, - }, - { - id: "notifications", - label: "Notifications", - href: "/environments/testEnvId/settings/notifications", - current: false, - }, - ], - activeId: "profile", - loading: undefined, - }, - undefined - ); - }); - - test("sets notifications as current when pathname includes /notifications", () => { - vi.mocked(usePathname).mockReturnValue("/environments/testEnvId/settings/notifications"); - render(); - - expect(SecondaryNavigation).toHaveBeenCalledWith( - expect.objectContaining({ - navigation: [ - { - id: "profile", - label: "Profile", - href: "/environments/testEnvId/settings/profile", - current: false, - }, - { - id: "notifications", - label: "Notifications", - href: "/environments/testEnvId/settings/notifications", - current: true, - }, - ], - activeId: "notifications", - }), - undefined - ); - }); - - test("passes loading prop to SecondaryNavigation", () => { - vi.mocked(usePathname).mockReturnValue("/environments/testEnvId/settings/profile"); - render(); - - expect(SecondaryNavigation).toHaveBeenCalledWith( - expect.objectContaining({ - loading: true, - }), - undefined - ); - }); - - test("handles undefined environmentId gracefully in hrefs", () => { - vi.mocked(usePathname).mockReturnValue("/environments/undefined/settings/profile"); - render(); // environmentId is undefined - - expect(SecondaryNavigation).toHaveBeenCalledWith( - expect.objectContaining({ - navigation: [ - { - id: "profile", - label: "Profile", - href: "/environments/undefined/settings/profile", - current: true, - }, - { - id: "notifications", - label: "Notifications", - href: "/environments/undefined/settings/notifications", - current: false, - }, - ], - }), - undefined - ); - }); - - test("handles null pathname gracefully", () => { - vi.mocked(usePathname).mockReturnValue(""); - render(); - - expect(SecondaryNavigation).toHaveBeenCalledWith( - expect.objectContaining({ - navigation: [ - { - id: "profile", - label: "Profile", - href: "/environments/testEnvId/settings/profile", - current: false, - }, - { - id: "notifications", - label: "Notifications", - href: "/environments/testEnvId/settings/notifications", - current: false, - }, - ], - }), - undefined - ); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.test.tsx deleted file mode 100644 index 1f9aeb0a26..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { Session, getServerSession } from "next-auth"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TProject } from "@formbricks/types/project"; -import { getOrganizationByEnvironmentId } from "@/lib/organization/service"; -import { getProjectByEnvironmentId } from "@/lib/project/service"; -import AccountSettingsLayout from "./layout"; - -// Mock dependencies -vi.mock("@/lib/organization/service"); -vi.mock("@/lib/project/service"); -vi.mock("next-auth", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - getServerSession: vi.fn(), - }; -}); - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -const mockGetOrganizationByEnvironmentId = vi.mocked(getOrganizationByEnvironmentId); -const mockGetProjectByEnvironmentId = vi.mocked(getProjectByEnvironmentId); -const mockGetServerSession = vi.mocked(getServerSession); - -const mockOrganization = { id: "org_test_id" } as unknown as TOrganization; -const mockProject = { id: "project_test_id" } as unknown as TProject; -const mockSession = { user: { id: "user_test_id" } } as unknown as Session; - -const t = (key: any) => key; -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => t, -})); - -const mockProps = { - params: { environmentId: "env_test_id" }, - children:
    Child Content
    , -}; - -describe("AccountSettingsLayout", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.resetAllMocks(); - - mockGetOrganizationByEnvironmentId.mockResolvedValue(mockOrganization); - mockGetProjectByEnvironmentId.mockResolvedValue(mockProject); - mockGetServerSession.mockResolvedValue(mockSession); - }); - - test("should render children when all data is fetched successfully", async () => { - render(await AccountSettingsLayout(mockProps)); - expect(screen.getByText("Child Content")).toBeInTheDocument(); - }); - - test("should throw error if organization is not found", async () => { - mockGetOrganizationByEnvironmentId.mockResolvedValue(null); - await expect(AccountSettingsLayout(mockProps)).rejects.toThrowError("common.organization_not_found"); - }); - - test("should throw error if project is not found", async () => { - mockGetProjectByEnvironmentId.mockResolvedValue(null); - await expect(AccountSettingsLayout(mockProps)).rejects.toThrowError("common.project_not_found"); - }); - - test("should throw error if session is not found", async () => { - mockGetServerSession.mockResolvedValue(null); - await expect(AccountSettingsLayout(mockProps)).rejects.toThrowError("common.session_not_found"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.test.tsx deleted file mode 100644 index 3f9b56d579..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.test.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TUser } from "@formbricks/types/user"; -import { Membership } from "../types"; -import { EditAlerts } from "./EditAlerts"; - -// Mock dependencies -vi.mock("@/modules/ui/components/tooltip", () => ({ - Tooltip: ({ children }: { children: React.ReactNode }) =>
    {children}
    , - TooltipContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - TooltipProvider: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - TooltipTrigger: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("lucide-react", () => ({ - HelpCircleIcon: () =>
    , - UsersIcon: () =>
    , -})); - -vi.mock("next/link", () => ({ - default: ({ children, href }: { children: React.ReactNode; href: string }) => ( - - {children} - - ), -})); - -const mockNotificationSwitch = vi.fn(); -vi.mock("./NotificationSwitch", () => ({ - NotificationSwitch: (props: any) => { - mockNotificationSwitch(props); - return ( -
    - NotificationSwitch -
    - ); - }, -})); - -const mockUser = { - id: "user1", - name: "Test User", - email: "test@example.com", - notificationSettings: { - alert: {}, - unsubscribedOrganizationIds: [], - }, - role: "project_manager", - objective: "other", - emailVerified: new Date(), - createdAt: new Date(), - updatedAt: new Date(), - identityProvider: "email", - twoFactorEnabled: false, -} as unknown as TUser; - -const mockMemberships: Membership[] = [ - { - organization: { - id: "org1", - name: "Organization 1", - projects: [ - { - id: "proj1", - name: "Project 1", - environments: [ - { - id: "env1", - surveys: [ - { id: "survey1", name: "Survey 1 Org 1 Proj 1" }, - { id: "survey2", name: "Survey 2 Org 1 Proj 1" }, - ], - }, - ], - }, - { - id: "proj2", - name: "Project 2", - environments: [ - { - id: "env2", - surveys: [{ id: "survey3", name: "Survey 3 Org 1 Proj 2" }], - }, - ], - }, - ], - }, - }, - { - organization: { - id: "org2", - name: "Organization 2", - projects: [ - { - id: "proj3", - name: "Project 3", - environments: [ - { - id: "env3", - surveys: [{ id: "survey4", name: "Survey 4 Org 2 Proj 3" }], - }, - ], - }, - ], - }, - }, - { - organization: { - id: "org3", - name: "Organization 3 No Surveys", - projects: [ - { - id: "proj4", - name: "Project 4", - environments: [ - { - id: "env4", - surveys: [], // No surveys in this environment - }, - ], - }, - ], - }, - }, -]; - -const environmentId = "test-env-id"; -const autoDisableNotificationType = "someType"; -const autoDisableNotificationElementId = "someElementId"; - -describe("EditAlerts", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders correctly with multiple memberships and surveys", () => { - render( - - ); - - // Check organization names - expect(screen.getByText("Organization 1")).toBeInTheDocument(); - expect(screen.getByText("Organization 2")).toBeInTheDocument(); - expect(screen.getByText("Organization 3 No Surveys")).toBeInTheDocument(); - - // Check survey names and project names as subtext - expect(screen.getByText("Survey 1 Org 1 Proj 1")).toBeInTheDocument(); - expect(screen.getAllByText("Project 1")[0]).toBeInTheDocument(); // Project name under survey - expect(screen.getByText("Survey 2 Org 1 Proj 1")).toBeInTheDocument(); - expect(screen.getByText("Survey 3 Org 1 Proj 2")).toBeInTheDocument(); - expect(screen.getAllByText("Project 2")[0]).toBeInTheDocument(); - expect(screen.getByText("Survey 4 Org 2 Proj 3")).toBeInTheDocument(); - expect(screen.getAllByText("Project 3")[0]).toBeInTheDocument(); - - // Check "No surveys found" message for org3 - const org3Heading = screen.getByText("Organization 3 No Surveys"); - expect(org3Heading.parentElement?.parentElement?.parentElement).toHaveTextContent( - "common.no_surveys_found" - ); - - // Check NotificationSwitch calls - // Org 1 auto-subscribe - expect(mockNotificationSwitch).toHaveBeenCalledWith( - expect.objectContaining({ - surveyOrProjectOrOrganizationId: "org1", - notificationType: "unsubscribedOrganizationIds", - autoDisableNotificationType, - autoDisableNotificationElementId, - }) - ); - // Survey 1 - expect(mockNotificationSwitch).toHaveBeenCalledWith( - expect.objectContaining({ - surveyOrProjectOrOrganizationId: "survey1", - notificationType: "alert", - autoDisableNotificationType, - autoDisableNotificationElementId, - }) - ); - // Survey 4 - expect(mockNotificationSwitch).toHaveBeenCalledWith( - expect.objectContaining({ - surveyOrProjectOrOrganizationId: "survey4", - notificationType: "alert", - autoDisableNotificationType, - autoDisableNotificationElementId, - }) - ); - - // Check tooltip - expect(screen.getAllByTestId("tooltip-provider").length).toBeGreaterThan(0); - expect(screen.getAllByTestId("tooltip").length).toBeGreaterThan(0); - expect(screen.getAllByTestId("tooltip-trigger").length).toBeGreaterThan(0); - expect(screen.getAllByTestId("tooltip-content")[0]).toHaveTextContent( - "environments.settings.notifications.every_response_tooltip" - ); - expect(screen.getAllByTestId("help-circle-icon").length).toBeGreaterThan(0); - - // Check invite link - const inviteLinks = screen.getAllByTestId("link"); - const specificInviteLink = inviteLinks.find( - (link) => link.getAttribute("href") === `/environments/${environmentId}/settings/general` - ); - expect(specificInviteLink).toBeInTheDocument(); - expect(specificInviteLink).toHaveTextContent("common.invite_them"); - - // Check UsersIcon - expect(screen.getAllByTestId("users-icon").length).toBe(mockMemberships.length); - }); - - test("renders correctly when a membership has no surveys", () => { - const singleMembershipNoSurveys: Membership[] = [ - { - organization: { - id: "org-no-survey", - name: "Org Without Surveys", - projects: [ - { - id: "proj-no-survey", - name: "Project Without Surveys", - environments: [ - { - id: "env-no-survey", - surveys: [], - }, - ], - }, - ], - }, - }, - ]; - render( - - ); - - expect(screen.getByText("Org Without Surveys")).toBeInTheDocument(); - expect(screen.getByText("common.no_surveys_found")).toBeInTheDocument(); - expect(screen.queryByText("Survey 1 Org 1 Proj 1")).not.toBeInTheDocument(); // Ensure other surveys aren't rendered - - // Check NotificationSwitch for organization auto-subscribe - expect(mockNotificationSwitch).toHaveBeenCalledWith( - expect.objectContaining({ - surveyOrProjectOrOrganizationId: "org-no-survey", - notificationType: "unsubscribedOrganizationIds", - }) - ); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/IntegrationsTip.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/IntegrationsTip.test.tsx deleted file mode 100644 index 5d5b61e7b8..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/IntegrationsTip.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { IntegrationsTip } from "./IntegrationsTip"; - -vi.mock("@/modules/ui/components/icons", () => ({ - SlackIcon: () =>
    , -})); - -const mockT = vi.fn((key) => key); -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: mockT, - }), -})); - -const environmentId = "test-env-id"; - -describe("IntegrationsTip", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders the component with correct text and link", () => { - render(); - - expect(screen.getByTestId("slack-icon")).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.notifications.need_slack_or_discord_notifications?") - ).toBeInTheDocument(); - - const linkElement = screen.getByText("environments.settings.notifications.use_the_integration"); - expect(linkElement).toBeInTheDocument(); - expect(linkElement).toHaveAttribute("href", `/environments/${environmentId}/project/integrations`); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.test.tsx deleted file mode 100644 index 7143498be2..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.test.tsx +++ /dev/null @@ -1,410 +0,0 @@ -import { act, cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TUserNotificationSettings } from "@formbricks/types/user"; -import { updateNotificationSettingsAction } from "../actions"; -import { NotificationSwitch } from "./NotificationSwitch"; - -vi.mock("@/modules/ui/components/switch", () => ({ - Switch: vi.fn(({ checked, disabled, onCheckedChange, id, "aria-label": ariaLabel }) => ( - - )), -})); - -vi.mock("../actions", () => ({ - updateNotificationSettingsAction: vi.fn(() => Promise.resolve({ data: true })), -})); - -const surveyId = "survey1"; -const projectId = "project1"; -const organizationId = "org1"; - -const baseNotificationSettings: TUserNotificationSettings = { - alert: {}, - unsubscribedOrganizationIds: [], -}; - -describe("NotificationSwitch", () => { - let user: ReturnType; - - beforeEach(() => { - user = userEvent.setup(); - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - const renderSwitch = (props: Partial>) => { - const defaultProps: React.ComponentProps = { - surveyOrProjectOrOrganizationId: surveyId, - notificationSettings: JSON.parse(JSON.stringify(baseNotificationSettings)), - notificationType: "alert", - }; - return render(); - }; - - test("renders with initial checked state for 'alert' (true)", () => { - const settings = { ...baseNotificationSettings, alert: { [surveyId]: true } }; - renderSwitch({ notificationSettings: settings, notificationType: "alert" }); - const switchInput = screen.getByLabelText("toggle notification settings for alert") as HTMLInputElement; - expect(switchInput.checked).toBe(true); - }); - - test("renders with initial checked state for 'alert' (false)", () => { - const settings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; - renderSwitch({ notificationSettings: settings, notificationType: "alert" }); - const switchInput = screen.getByLabelText("toggle notification settings for alert") as HTMLInputElement; - expect(switchInput.checked).toBe(false); - }); - - test("renders with initial checked state for 'unsubscribedOrganizationIds' (subscribed initially, so checked is true)", () => { - const settings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [] }; - renderSwitch({ - surveyOrProjectOrOrganizationId: organizationId, - notificationSettings: settings, - notificationType: "unsubscribedOrganizationIds", - }); - const switchInput = screen.getByLabelText( - "toggle notification settings for unsubscribedOrganizationIds" - ) as HTMLInputElement; - expect(switchInput.checked).toBe(true); // Not in unsubscribed list means subscribed - }); - - test("renders with initial checked state for 'unsubscribedOrganizationIds' (unsubscribed initially, so checked is false)", () => { - const settings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [organizationId] }; - renderSwitch({ - surveyOrProjectOrOrganizationId: organizationId, - notificationSettings: settings, - notificationType: "unsubscribedOrganizationIds", - }); - const switchInput = screen.getByLabelText( - "toggle notification settings for unsubscribedOrganizationIds" - ) as HTMLInputElement; - expect(switchInput.checked).toBe(false); // In unsubscribed list means unsubscribed - }); - - test("handles switch change for 'alert' type", async () => { - const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; - renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" }); - const switchInput = screen.getByLabelText("toggle notification settings for alert"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, alert: { [surveyId]: true } }, - }); - expect(toast.success).toHaveBeenCalledWith( - "environments.settings.notifications.notification_settings_updated", - { id: "notification-switch" } - ); - expect(switchInput).toBeEnabled(); // Check if not disabled after action - }); - - test("handles switch change for 'unsubscribedOrganizationIds' (subscribe)", async () => { - const initialSettings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [organizationId] }; // initially unsubscribed - renderSwitch({ - surveyOrProjectOrOrganizationId: organizationId, - notificationSettings: initialSettings, - notificationType: "unsubscribedOrganizationIds", - }); - const switchInput = screen.getByLabelText("toggle notification settings for unsubscribedOrganizationIds"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, unsubscribedOrganizationIds: [] }, // should be removed from list - }); - expect(toast.success).toHaveBeenCalledWith( - "environments.settings.notifications.notification_settings_updated", - { id: "notification-switch" } - ); - }); - - test("handles switch change for 'unsubscribedOrganizationIds' (unsubscribe)", async () => { - const initialSettings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [] }; // initially subscribed - renderSwitch({ - surveyOrProjectOrOrganizationId: organizationId, - notificationSettings: initialSettings, - notificationType: "unsubscribedOrganizationIds", - }); - const switchInput = screen.getByLabelText("toggle notification settings for unsubscribedOrganizationIds"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, unsubscribedOrganizationIds: [organizationId] }, // should be added to list - }); - expect(toast.success).toHaveBeenCalledWith( - "environments.settings.notifications.notification_settings_updated", - { id: "notification-switch" } - ); - }); - - test("useEffect: auto-disables 'alert' notification if conditions met", () => { - const settings = { ...baseNotificationSettings, alert: { [surveyId]: true } }; // Initially true - renderSwitch({ - surveyOrProjectOrOrganizationId: surveyId, - notificationSettings: settings, - notificationType: "alert", - autoDisableNotificationType: "alert", - autoDisableNotificationElementId: surveyId, - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...settings, alert: { [surveyId]: false } }, - }); - expect(toast.success).toHaveBeenCalledWith( - "environments.settings.notifications.you_will_not_receive_any_more_emails_for_responses_on_this_survey", - { id: "notification-switch" } - ); - }); - - test("useEffect: auto-disables 'unsubscribedOrganizationIds' (auto-unsubscribes) if conditions met", () => { - const settings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [] }; // Initially subscribed - renderSwitch({ - surveyOrProjectOrOrganizationId: organizationId, - notificationSettings: settings, - notificationType: "unsubscribedOrganizationIds", - autoDisableNotificationType: "someOtherType", // This prop is used to trigger the effect, not directly for type matching in this case - autoDisableNotificationElementId: organizationId, - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...settings, unsubscribedOrganizationIds: [organizationId] }, - }); - expect(toast.success).toHaveBeenCalledWith( - "environments.settings.notifications.you_will_not_be_auto_subscribed_to_this_organizations_surveys_anymore", - { id: "notification-switch" } - ); - }); - - test("useEffect: does not auto-disable if 'autoDisableNotificationElementId' does not match", () => { - const settings = { ...baseNotificationSettings, alert: { [surveyId]: true } }; - renderSwitch({ - surveyOrProjectOrOrganizationId: surveyId, - notificationSettings: settings, - notificationType: "alert", - autoDisableNotificationType: "alert", - autoDisableNotificationElementId: "otherId", // Mismatch - }); - expect(updateNotificationSettingsAction).not.toHaveBeenCalled(); - expect(toast.success).not.toHaveBeenCalledWith( - "environments.settings.notifications.you_will_not_receive_any_more_emails_for_responses_on_this_survey" - ); - }); - - test("useEffect: does not auto-disable if not checked initially for 'alert'", () => { - const settings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; // Initially false - renderSwitch({ - surveyOrProjectOrOrganizationId: surveyId, - notificationSettings: settings, - notificationType: "alert", - autoDisableNotificationType: "alert", - autoDisableNotificationElementId: surveyId, - }); - expect(updateNotificationSettingsAction).not.toHaveBeenCalled(); - }); - - test("useEffect: does not auto-disable if not checked initially for 'unsubscribedOrganizationIds' (already unsubscribed)", () => { - const settings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [organizationId] }; // Initially unsubscribed - renderSwitch({ - surveyOrProjectOrOrganizationId: organizationId, - notificationSettings: settings, - notificationType: "unsubscribedOrganizationIds", - autoDisableNotificationType: "someType", - autoDisableNotificationElementId: organizationId, - }); - expect(updateNotificationSettingsAction).not.toHaveBeenCalled(); - }); - - test("shows error toast when updateNotificationSettingsAction fails for 'alert' type", async () => { - const mockErrorResponse = { serverError: "Failed to update notification settings" }; - vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse); - - const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; - renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" }); - const switchInput = screen.getByLabelText("toggle notification settings for alert"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, alert: { [surveyId]: true } }, - }); - expect(toast.error).toHaveBeenCalledWith("Failed to update notification settings", { - id: "notification-switch", - }); - expect(toast.success).not.toHaveBeenCalled(); - }); - - test("shows error toast when updateNotificationSettingsAction fails for 'unsubscribedOrganizationIds' type", async () => { - const mockErrorResponse = { serverError: "Permission denied" }; - vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse); - - const initialSettings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [] }; - renderSwitch({ - surveyOrProjectOrOrganizationId: organizationId, - notificationSettings: initialSettings, - notificationType: "unsubscribedOrganizationIds", - }); - const switchInput = screen.getByLabelText("toggle notification settings for unsubscribedOrganizationIds"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, unsubscribedOrganizationIds: [organizationId] }, - }); - expect(toast.error).toHaveBeenCalledWith("Permission denied", { - id: "notification-switch", - }); - expect(toast.success).not.toHaveBeenCalled(); - }); - - test("shows error toast when updateNotificationSettingsAction returns null", async () => { - const mockErrorResponse = { serverError: "An error occurred" }; - vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse); - - const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; - renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" }); - const switchInput = screen.getByLabelText("toggle notification settings for alert"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, alert: { [surveyId]: true } }, - }); - expect(toast.error).toHaveBeenCalledWith("An error occurred", { - id: "notification-switch", - }); - expect(toast.success).not.toHaveBeenCalled(); - }); - - test("shows error toast when updateNotificationSettingsAction returns undefined", async () => { - const mockErrorResponse = { serverError: "An error occurred" }; - vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse); - - const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; - renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" }); - const switchInput = screen.getByLabelText("toggle notification settings for alert"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, alert: { [surveyId]: true } }, - }); - expect(toast.error).toHaveBeenCalledWith("An error occurred", { - id: "notification-switch", - }); - expect(toast.success).not.toHaveBeenCalled(); - }); - - test("shows error toast when updateNotificationSettingsAction returns response without data property", async () => { - const mockErrorResponse = { validationErrors: { _errors: ["Invalid input"] } }; - vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse); - - const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; - renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" }); - const switchInput = screen.getByLabelText("toggle notification settings for alert"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, alert: { [surveyId]: true } }, - }); - expect(toast.error).toHaveBeenCalledWith("Invalid input", { - id: "notification-switch", - }); - expect(toast.success).not.toHaveBeenCalled(); - }); - - test("shows error toast when updateNotificationSettingsAction throws an exception", async () => { - const mockErrorResponse = { serverError: "Network error" }; - vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse); - - const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; - renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" }); - const switchInput = screen.getByLabelText("toggle notification settings for alert"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, alert: { [surveyId]: true } }, - }); - expect(toast.error).toHaveBeenCalledWith("Network error", { - id: "notification-switch", - }); - expect(toast.success).not.toHaveBeenCalled(); - }); - - test("switch remains enabled after error occurs", async () => { - const mockErrorResponse = { serverError: "Failed to update" }; - vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse); - - const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; - renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" }); - const switchInput = screen.getByLabelText("toggle notification settings for alert"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(toast.error).toHaveBeenCalledWith("Failed to update", { - id: "notification-switch", - }); - expect(switchInput).toBeEnabled(); // Switch should be re-enabled after error - }); - - test("shows error toast with validation errors for specific fields", async () => { - const mockErrorResponse = { - validationErrors: { - notificationSettings: { - _errors: ["Invalid notification settings"], - }, - }, - }; - vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse); - - const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; - renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" }); - const switchInput = screen.getByLabelText("toggle notification settings for alert"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, alert: { [surveyId]: true } }, - }); - expect(toast.error).toHaveBeenCalledWith("notificationSettingsInvalid notification settings", { - id: "notification-switch", - }); - expect(toast.success).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.test.tsx deleted file mode 100644 index ea2fbf0cd0..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Loading from "./loading"; - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ pageTitle }: { pageTitle: string }) =>
    {pageTitle}
    , -})); - -describe("Loading Notifications Settings", () => { - afterEach(() => { - cleanup(); - }); - - test("renders loading state correctly", () => { - render(); - - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - const pageHeader = screen.getByTestId("page-header"); - expect(pageHeader).toBeInTheDocument(); - expect(pageHeader).toHaveTextContent("common.account_settings"); - - // Check for Alerts LoadingCard - expect(screen.getByText("environments.settings.notifications.email_alerts_surveys")).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.notifications.set_up_an_alert_to_get_an_email_on_new_responses") - ).toBeInTheDocument(); - const alertsCard = screen - .getByText("environments.settings.notifications.email_alerts_surveys") - .closest("div[class*='rounded-xl']"); // Find parent card - expect(alertsCard).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.test.tsx deleted file mode 100644 index 1da431461d..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.test.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { getServerSession } from "next-auth"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { prisma } from "@formbricks/database"; -import { TUser } from "@formbricks/types/user"; -import { getUser } from "@/lib/user/service"; -import { EditAlerts } from "./components/EditAlerts"; -import Page from "./page"; -import { Membership } from "./types"; - -// Mock external dependencies -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar", - () => ({ - AccountSettingsNavbar: ({ activeId }) =>
    AccountSettingsNavbar activeId={activeId}
    , - }) -); -vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({ - SettingsCard: ({ title, description, children }) => ( -
    -

    {title}

    -

    {description}

    - {children} -
    - ), -})); -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); -vi.mock("@/modules/auth/lib/authOptions", () => ({ - authOptions: {}, -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ pageTitle, children }) => ( -
    -

    {pageTitle}

    - {children} -
    - ), -})); -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); -vi.mock("@formbricks/database", () => ({ - prisma: { - membership: { - findMany: vi.fn(), - }, - }, -})); -vi.mock("./components/EditAlerts", () => ({ - EditAlerts: vi.fn(() =>
    EditAlertsComponent
    ), -})); - -vi.mock("./components/IntegrationsTip", () => ({ - IntegrationsTip: () =>
    IntegrationsTipComponent
    , -})); - -const mockUser: Partial = { - id: "user-1", - name: "Test User", - email: "test@example.com", - notificationSettings: { - alert: { "survey-old": true }, - unsubscribedOrganizationIds: ["org-unsubscribed"], - }, -}; - -const mockMemberships: Membership[] = [ - { - organization: { - id: "org-1", - name: "Org 1", - projects: [ - { - id: "project-1", - name: "Project 1", - environments: [ - { - id: "env-prod-1", - surveys: [ - { id: "survey-1", name: "Survey 1" }, - { id: "survey-2", name: "Survey 2" }, - ], - }, - ], - }, - ], - }, - }, -]; - -const mockSession = { - user: { - id: "user-1", - }, -} as any; - -const mockParams = { environmentId: "env-1" }; -const mockSearchParams = { - type: "alertTest", - elementId: "elementTestId", -}; - -describe("NotificationsPage", () => { - afterEach(() => { - cleanup(); - vi.resetAllMocks(); - }); - - beforeEach(() => { - vi.mocked(getServerSession).mockResolvedValue(mockSession); - vi.mocked(getUser).mockResolvedValue(mockUser as TUser); - vi.mocked(prisma.membership.findMany).mockResolvedValue(mockMemberships as any); // Prisma types can be complex - }); - - test("renders correctly with user and memberships, and processes notification settings", async () => { - const props = { params: mockParams, searchParams: mockSearchParams }; - const PageComponent = await Page(props); - render(PageComponent); - - expect(screen.getByText("common.account_settings")).toBeInTheDocument(); - expect(screen.getByText("AccountSettingsNavbar activeId=notifications")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.notifications.email_alerts_surveys")).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.notifications.set_up_an_alert_to_get_an_email_on_new_responses") - ).toBeInTheDocument(); - expect(screen.getByText("EditAlertsComponent")).toBeInTheDocument(); - expect(screen.getByText("IntegrationsTipComponent")).toBeInTheDocument(); - - // The actual `user.notificationSettings` passed to EditAlerts will be a new object - // after `setCompleteNotificationSettings` processes it. - // We verify the structure and defaults. - const editAlertsCall = vi.mocked(EditAlerts).mock.calls[0][0]; - expect(editAlertsCall.user.notificationSettings.alert["survey-1"]).toBe(false); - expect(editAlertsCall.user.notificationSettings.alert["survey-2"]).toBe(false); - // If "survey-old" was not part of any membership survey, it might be removed or kept depending on exact logic. - // The current logic only adds keys from memberships. So "survey-old" would be gone from .alert - // Let's adjust expectation based on `setCompleteNotificationSettings` - // It iterates memberships, then projects, then environments, then surveys. - // `newNotificationSettings.alert[survey.id] = notificationSettings[survey.id]?.responseFinished || (notificationSettings.alert && notificationSettings.alert[survey.id]) || false;` - // This means only survey IDs found in memberships will be in the new `alert` object. - - const finalExpectedSettings = { - alert: { - "survey-1": false, - "survey-2": false, - }, - unsubscribedOrganizationIds: ["org-unsubscribed"], - }; - - expect(editAlertsCall.user.notificationSettings).toEqual(finalExpectedSettings); - expect(editAlertsCall.memberships).toEqual(mockMemberships); - expect(editAlertsCall.environmentId).toBe(mockParams.environmentId); - expect(editAlertsCall.autoDisableNotificationType).toBe(mockSearchParams.type); - expect(editAlertsCall.autoDisableNotificationElementId).toBe(mockSearchParams.elementId); - }); - - test("throws error if session is not found", async () => { - vi.mocked(getServerSession).mockResolvedValue(null); - const props = { params: mockParams, searchParams: {} }; - await expect(Page(props)).rejects.toThrow("common.session_not_found"); - }); - - test("throws error if user is not found", async () => { - vi.mocked(getUser).mockResolvedValue(null); - const props = { params: mockParams, searchParams: {} }; - await expect(Page(props)).rejects.toThrow("common.user_not_found"); - }); - - test("renders with empty memberships and default notification settings", async () => { - vi.mocked(prisma.membership.findMany).mockResolvedValue([]); - const userWithNoSpecificSettings = { - ...mockUser, - notificationSettings: { unsubscribedOrganizationIds: [] }, // Start fresh - }; - vi.mocked(getUser).mockResolvedValue(userWithNoSpecificSettings as unknown as TUser); - - const props = { params: mockParams, searchParams: {} }; - const PageComponent = await Page(props); - render(PageComponent); - - expect(screen.getByText("EditAlertsComponent")).toBeInTheDocument(); - - const expectedEmptySettings = { - alert: {}, - unsubscribedOrganizationIds: [], - }; - - const editAlertsCall = vi.mocked(EditAlerts).mock.calls[0][0]; - expect(editAlertsCall.user.notificationSettings).toEqual(expectedEmptySettings); - expect(editAlertsCall.memberships).toEqual([]); - }); - - test("handles legacy notification settings correctly", async () => { - const userWithLegacySettings: Partial = { - id: "user-legacy", - notificationSettings: { - "survey-1": { responseFinished: true }, // Legacy alert for survey-1 - unsubscribedOrganizationIds: [], - } as any, // To allow legacy structure - }; - vi.mocked(getUser).mockResolvedValue(userWithLegacySettings as TUser); - // Memberships define survey-1 and project-1 - vi.mocked(prisma.membership.findMany).mockResolvedValue(mockMemberships as any); - - const props = { params: mockParams, searchParams: {} }; - const PageComponent = await Page(props); - render(PageComponent); - - const expectedProcessedSettings = { - alert: { - "survey-1": true, // Should be true due to legacy setting - "survey-2": false, // Default for other surveys in membership - }, - unsubscribedOrganizationIds: [], - }; - - const editAlertsCall = vi.mocked(EditAlerts).mock.calls[0][0]; - expect(editAlertsCall.user.notificationSettings).toEqual(expectedProcessedSettings); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.test.tsx deleted file mode 100644 index 835e669e5f..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TUser } from "@formbricks/types/user"; -import { AccountSecurity } from "./AccountSecurity"; - -vi.mock("@/modules/ee/two-factor-auth/components/enable-two-factor-modal", () => ({ - EnableTwoFactorModal: ({ open }) => - open ?
    EnableTwoFactorModal
    : null, -})); - -vi.mock("@/modules/ee/two-factor-auth/components/disable-two-factor-modal", () => ({ - DisableTwoFactorModal: ({ open }) => - open ?
    DisableTwoFactorModal
    : null, -})); - -const mockUser = { - id: "test-user-id", - name: "Test User", - email: "test@example.com", - notificationSettings: { - alert: {}, - - unsubscribedOrganizationIds: [], - }, - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - role: "project_manager", - objective: "other", -} as unknown as TUser; - -describe("AccountSecurity", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.resetAllMocks(); - }); - - test("renders correctly with 2FA disabled", () => { - render(); - expect(screen.getByText("environments.settings.profile.two_factor_authentication")).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.profile.two_factor_authentication_description") - ).toBeInTheDocument(); - expect(screen.getByRole("switch")).not.toBeChecked(); - }); - - test("renders correctly with 2FA enabled", () => { - render(); - expect(screen.getByRole("switch")).toBeChecked(); - }); - - test("opens EnableTwoFactorModal when switch is turned on", async () => { - render(); - const switchControl = screen.getByRole("switch"); - await userEvent.click(switchControl); - expect(screen.getByTestId("enable-2fa-modal")).toBeInTheDocument(); - }); - - test("opens DisableTwoFactorModal when switch is turned off", async () => { - render(); - const switchControl = screen.getByRole("switch"); - await userEvent.click(switchControl); - expect(screen.getByTestId("disable-2fa-modal")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DeleteAccount.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DeleteAccount.test.tsx deleted file mode 100644 index 156be7ee0c..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DeleteAccount.test.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { Session } from "next-auth"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TUser } from "@formbricks/types/user"; -import { DeleteAccount } from "./DeleteAccount"; - -vi.mock("@/modules/account/components/DeleteAccountModal", () => ({ - DeleteAccountModal: ({ open }) => - open ?
    DeleteAccountModal
    : null, -})); - -const mockUser = { - id: "user1", - name: "Test User", - email: "test@example.com", - notificationSettings: { alert: {}, unsubscribedOrganizationIds: [] }, - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - role: "project_manager", - objective: "other", -} as unknown as TUser; - -const mockSession: Session = { - user: mockUser, - expires: new Date(Date.now() + 2 * 86400).toISOString(), -}; - -const mockOrganizations: TOrganization[] = [ - { - id: "org1", - name: "Org 1", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - stripeCustomerId: "cus_123", - } as unknown as TOrganization["billing"], - } as unknown as TOrganization, -]; - -describe("DeleteAccount", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.resetAllMocks(); - }); - - test("renders correctly and opens modal on click", async () => { - render( - - ); - - expect(screen.getByText("environments.settings.profile.warning_cannot_undo")).toBeInTheDocument(); - const deleteButton = screen.getByText("environments.settings.profile.confirm_delete_my_account"); - expect(deleteButton).toBeEnabled(); - await userEvent.click(deleteButton); - expect(screen.getByTestId("delete-account-modal")).toBeInTheDocument(); - }); - - test("renders null if session is not provided", () => { - const { container } = render( - - ); - expect(container.firstChild).toBeNull(); - }); - - test("enables delete button if multi-org enabled even if user is single owner", () => { - render( - - ); - const deleteButton = screen.getByText("environments.settings.profile.confirm_delete_my_account"); - expect(deleteButton).toBeEnabled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.test.tsx deleted file mode 100644 index bbfb31327d..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.test.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TUser } from "@formbricks/types/user"; -import { resetPasswordAction, updateUserAction } from "../actions"; -import { EditProfileDetailsForm } from "./EditProfileDetailsForm"; - -const mockUser = { - id: "test-user-id", - name: "Old Name", - email: "test@example.com", - locale: "en-US", - notificationSettings: { - alert: {}, - - unsubscribedOrganizationIds: [], - }, - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - role: "project_manager", - objective: "other", -} as unknown as TUser; - -vi.mock("next-auth/react", () => ({ signOut: vi.fn() })); - -// Mock window.location.reload -const originalLocation = window.location; -beforeEach(() => { - vi.stubGlobal("location", { - ...originalLocation, - reload: vi.fn(), - }); -}); - -vi.mock("@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions", () => ({ - updateUserAction: vi.fn(), - resetPasswordAction: vi.fn(), -})); - -vi.mock("@/modules/auth/forgot-password/actions", () => ({ - forgotPasswordAction: vi.fn(), -})); - -afterEach(() => { - vi.unstubAllGlobals(); -}); - -describe("EditProfileDetailsForm", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders with initial user data and updates successfully", async () => { - vi.mocked(updateUserAction).mockResolvedValue({ ...mockUser, name: "New Name" } as any); - - render( - - ); - - const nameInput = screen.getByPlaceholderText("common.full_name"); - expect(nameInput).toHaveValue(mockUser.name); - // Check initial language (English) - expect(screen.getByText("English (US)")).toBeInTheDocument(); - - await userEvent.clear(nameInput); - await userEvent.type(nameInput, "New Name"); - - // Change language - const languageDropdownTrigger = screen.getByRole("button", { name: /English/ }); - await userEvent.click(languageDropdownTrigger); - const germanOption = await screen.findByText("German"); // Assuming 'German' is an option - await userEvent.click(germanOption); - - const updateButton = screen.getByText("common.update"); - expect(updateButton).toBeEnabled(); - await userEvent.click(updateButton); - - await waitFor(() => { - expect(updateUserAction).toHaveBeenCalledWith({ - name: "New Name", - locale: "de-DE", - email: mockUser.email, - }); - }); - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith( - "environments.settings.profile.profile_updated_successfully" - ); - }); - await waitFor(() => { - expect(window.location.reload).toHaveBeenCalled(); - }); - }); - - test("shows error toast if update fails", async () => { - const errorMessage = "Update failed"; - vi.mocked(updateUserAction).mockRejectedValue(new Error(errorMessage)); - - render( - - ); - - const nameInput = screen.getByPlaceholderText("common.full_name"); - await userEvent.clear(nameInput); - await userEvent.type(nameInput, "Another Name"); - - const updateButton = screen.getByText("common.update"); - await userEvent.click(updateButton); - - await waitFor(() => { - expect(updateUserAction).toHaveBeenCalled(); - }); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith(`common.error: ${errorMessage}`); - }); - }); - - test("update button is disabled initially and enables on change", async () => { - render( - - ); - const updateButton = screen.getByText("common.update"); - expect(updateButton).toBeDisabled(); - - const nameInput = screen.getByPlaceholderText("common.full_name"); - await userEvent.type(nameInput, " updated"); - expect(updateButton).toBeEnabled(); - }); - - test("reset password button works", async () => { - vi.mocked(resetPasswordAction).mockResolvedValue({ data: { success: true } }); - - render( - - ); - - const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" }); - await userEvent.click(resetButton); - - await waitFor(() => { - expect(resetPasswordAction).toHaveBeenCalled(); - }); - - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith("auth.forgot-password.email-sent.heading"); - }); - }); - - test("reset password button handles error correctly", async () => { - const errorMessage = "Reset failed"; - vi.mocked(resetPasswordAction).mockResolvedValue({ serverError: errorMessage }); - - render( - - ); - - const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" }); - await userEvent.click(resetButton); - - await waitFor(() => { - expect(resetPasswordAction).toHaveBeenCalled(); - }); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith(errorMessage); - }); - }); - - test("reset password button shows loading state", async () => { - vi.mocked(resetPasswordAction).mockImplementation(() => new Promise(() => {})); // Never resolves - - render( - - ); - - const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" }); - await userEvent.click(resetButton); - - expect(resetButton).toBeDisabled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/password-confirmation-modal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/password-confirmation-modal.test.tsx deleted file mode 100644 index 32a40241a0..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/password-confirmation-modal.test.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { PasswordConfirmationModal } from "./password-confirmation-modal"; - -// Mock the Dialog component -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open, onOpenChange }: any) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children, ...props }: any) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: any) =>
    {children}
    , - DialogTitle: ({ children }: any) =>

    {children}

    , - DialogDescription: ({ children }: any) =>

    {children}

    , - DialogBody: ({ children }: any) =>
    {children}
    , - DialogFooter: ({ children }: any) =>
    {children}
    , -})); - -// Mock the PasswordInput component -vi.mock("@/modules/ui/components/password-input", () => ({ - PasswordInput: ({ onChange, value, placeholder }: any) => ( - onChange(e.target.value)} - placeholder={placeholder} - data-testid="password-input" - /> - ), -})); - -// Mock the useTranslate hook -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -describe("PasswordConfirmationModal", () => { - const defaultProps = { - open: true, - setOpen: vi.fn(), - oldEmail: "old@example.com", - newEmail: "new@example.com", - onConfirm: vi.fn(), - }; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders nothing when open is false", () => { - render(); - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); - - test("renders dialog content when open is true", () => { - render(); - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toBeInTheDocument(); - }); - - test("displays old and new email addresses", () => { - render(); - expect(screen.getByText("old@example.com")).toBeInTheDocument(); - expect(screen.getByText("new@example.com")).toBeInTheDocument(); - }); - - test("shows password input field", () => { - render(); - const passwordInput = screen.getByTestId("password-input"); - expect(passwordInput).toBeInTheDocument(); - expect(passwordInput).toHaveAttribute("placeholder", "*******"); - }); - - test("disables confirm button when form is not dirty", () => { - render(); - const confirmButton = screen.getByText("common.confirm"); - expect(confirmButton).toBeDisabled(); - }); - - test("disables confirm button when old and new emails are the same", () => { - render( - - ); - const confirmButton = screen.getByText("common.confirm"); - expect(confirmButton).toBeDisabled(); - }); - - test("enables confirm button when password is entered and emails are different", async () => { - const user = userEvent.setup(); - render(); - - const passwordInput = screen.getByTestId("password-input"); - await user.type(passwordInput, "password123"); - - const confirmButton = screen.getByText("common.confirm"); - expect(confirmButton).not.toBeDisabled(); - }); - - test("shows error message when password is too short", async () => { - const user = userEvent.setup(); - render(); - - const passwordInput = screen.getByTestId("password-input"); - await user.type(passwordInput, "short"); - - const confirmButton = screen.getByText("common.confirm"); - await user.click(confirmButton); - - expect(screen.getByText("Password must be at least 8 characters long")).toBeInTheDocument(); - }); - - test("handles cancel button click and resets form", async () => { - const user = userEvent.setup(); - render(); - - const passwordInput = screen.getByTestId("password-input"); - await user.type(passwordInput, "password123"); - - const cancelButton = screen.getByText("common.cancel"); - await user.click(cancelButton); - - expect(defaultProps.setOpen).toHaveBeenCalledWith(false); - await waitFor(() => { - expect(passwordInput).toHaveValue(""); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/loading.test.tsx deleted file mode 100644 index c5a035cc6a..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/loading.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Loading from "./loading"; - -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar", - () => ({ - AccountSettingsNavbar: ({ activeId, loading }) => ( -
    - AccountSettingsNavbar - active: {activeId}, loading: {loading?.toString()} -
    - ), - }) -); - -vi.mock("@/app/(app)/components/LoadingCard", () => ({ - LoadingCard: ({ title, description }) => ( -
    -
    {title}
    -
    {description}
    -
    - ), -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ pageTitle, children }) => ( -
    -

    {pageTitle}

    - {children} -
    - ), -})); - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }) =>
    {children}
    , -})); - -describe("Loading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders loading state correctly", () => { - render(); - - expect(screen.getByText("common.account_settings")).toBeInTheDocument(); - expect(screen.getByTestId("account-settings-navbar")).toHaveTextContent( - "AccountSettingsNavbar - active: profile, loading: true" - ); - - const loadingCards = screen.getAllByTestId("loading-card"); - expect(loadingCards).toHaveLength(2); - - expect(loadingCards[0]).toHaveTextContent("environments.settings.profile.personal_information"); - expect(loadingCards[0]).toHaveTextContent("environments.settings.profile.update_personal_info"); - - expect(loadingCards[1]).toHaveTextContent("environments.settings.profile.delete_account"); - expect(loadingCards[1]).toHaveTextContent("environments.settings.profile.confirm_delete_account"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.test.tsx deleted file mode 100644 index 727e780c97..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.test.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import { Session } from "next-auth"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TUser } from "@formbricks/types/user"; -import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service"; -import { getUser } from "@/lib/user/service"; -import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; -import Page from "./page"; - -// Mock services and utils -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: 1, - PASSWORD_RESET_DISABLED: 1, - EMAIL_VERIFICATION_DISABLED: true, -})); -vi.mock("@/lib/organization/service", () => ({ - getOrganizationsWhereUserIsSingleOwner: vi.fn(), -})); -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getIsMultiOrgEnabled: vi.fn(), - getIsTwoFactorAuthEnabled: vi.fn(), -})); -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -const t = (key: any) => key; -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => t, -})); - -// Mock child components -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar", - () => ({ - AccountSettingsNavbar: ({ environmentId, activeId }) => ( -
    - AccountSettingsNavbar: {environmentId} {activeId} -
    - ), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity", - () => ({ - AccountSecurity: ({ user }) =>
    AccountSecurity: {user.id}
    , - }) -); -vi.mock("./components/DeleteAccount", () => ({ - DeleteAccount: ({ user }) =>
    DeleteAccount: {user.id}
    , -})); -vi.mock("./components/EditProfileDetailsForm", () => ({ - EditProfileDetailsForm: ({ user }) => ( -
    EditProfileDetailsForm: {user.id}
    - ), -})); -vi.mock("@/modules/ui/components/upgrade-prompt", () => ({ - UpgradePrompt: ({ title }) =>
    {title}
    , -})); - -const mockUser = { - id: "user-123", - name: "Test User", - email: "test@example.com", - twoFactorEnabled: false, - identityProvider: "email", - notificationSettings: { alert: {}, unsubscribedOrganizationIds: [] }, - createdAt: new Date(), - updatedAt: new Date(), - role: "project_manager", - objective: "other", -} as unknown as TUser; - -const mockSession: Session = { - user: mockUser, - expires: "never", -}; - -const mockOrganizations: TOrganization[] = []; - -const params = { environmentId: "env-123" }; - -describe("ProfilePage", () => { - beforeEach(() => { - vi.mocked(getOrganizationsWhereUserIsSingleOwner).mockResolvedValue(mockOrganizations); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: mockSession, - } as unknown as TEnvironmentAuth); - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); - vi.mocked(getIsTwoFactorAuthEnabled).mockResolvedValue(true); - }); - - afterEach(() => { - vi.clearAllMocks(); - cleanup(); - }); - - test("renders profile page with all sections for email user with 2FA license", async () => { - render(await Page({ params: Promise.resolve(params) })); - - await waitFor(() => { - expect(screen.getByText("common.account_settings")).toBeInTheDocument(); - expect(screen.getByTestId("account-settings-navbar")).toHaveTextContent( - "AccountSettingsNavbar: env-123 profile" - ); - expect(screen.getByTestId("edit-profile-details-form")).toBeInTheDocument(); - expect(screen.getByTestId("account-security")).toBeInTheDocument(); // Shown because 2FA license is enabled - expect(screen.queryByTestId("upgrade-prompt")).not.toBeInTheDocument(); - expect(screen.getByTestId("delete-account")).toBeInTheDocument(); - // Check for IdBadge content - expect(screen.getByText("common.profile_id")).toBeInTheDocument(); - expect(screen.getByText(mockUser.id)).toBeInTheDocument(); - }); - }); - - test("renders UpgradePrompt when 2FA license is disabled and user 2FA is off", async () => { - vi.mocked(getIsTwoFactorAuthEnabled).mockResolvedValue(false); // License disabled - const userWith2FAOff = { ...mockUser, twoFactorEnabled: false }; - vi.mocked(getUser).mockResolvedValue(userWith2FAOff); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: { ...mockSession, user: userWith2FAOff }, - } as unknown as TEnvironmentAuth); - - render(await Page({ params: Promise.resolve(params) })); - - await waitFor(() => { - expect(screen.getByTestId("upgrade-prompt")).toBeInTheDocument(); - expect(screen.getByTestId("upgrade-prompt")).toHaveTextContent( - "environments.settings.profile.unlock_two_factor_authentication" - ); - expect(screen.queryByTestId("account-security")).not.toBeInTheDocument(); - }); - }); - - test("renders AccountSecurity when 2FA license is disabled but user 2FA is on", async () => { - vi.mocked(getIsTwoFactorAuthEnabled).mockResolvedValue(false); // License disabled - const userWith2FAOn = { ...mockUser, twoFactorEnabled: true }; - vi.mocked(getUser).mockResolvedValue(userWith2FAOn); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: { ...mockSession, user: userWith2FAOn }, - } as unknown as TEnvironmentAuth); - - render(await Page({ params: Promise.resolve(params) })); - - await waitFor(() => { - expect(screen.getByTestId("account-security")).toBeInTheDocument(); - expect(screen.queryByTestId("upgrade-prompt")).not.toBeInTheDocument(); - }); - }); - - test("does not render security card if identityProvider is not email", async () => { - const nonEmailUser = { ...mockUser, identityProvider: "google" as "email" | "github" | "google" }; // type assertion - vi.mocked(getUser).mockResolvedValue(nonEmailUser); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: { ...mockSession, user: nonEmailUser }, - } as unknown as TEnvironmentAuth); - - render(await Page({ params: Promise.resolve(params) })); - - await waitFor(() => { - expect(screen.queryByTestId("account-security")).not.toBeInTheDocument(); - expect(screen.queryByTestId("upgrade-prompt")).not.toBeInTheDocument(); - expect(screen.queryByText("common.security")).not.toBeInTheDocument(); - }); - }); - - test("throws error if user is not found", async () => { - vi.mocked(getUser).mockResolvedValue(null); - // Need to catch the promise rejection for async component errors - try { - // We don't await the render directly, but the component execution - await Page({ params: Promise.resolve(params) }); - } catch (e) { - expect(e.message).toBe("common.user_not_found"); - } - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/loading.test.tsx deleted file mode 100644 index 337cc384ba..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/loading.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import LoadingPage from "./loading"; - -// Mock the IS_FORMBRICKS_CLOUD constant -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: true, -})); - -// Mock the actual Loading component that is being imported -vi.mock("@/modules/organization/settings/api-keys/loading", () => ({ - default: ({ isFormbricksCloud }: { isFormbricksCloud: boolean }) => ( -
    isFormbricksCloud: {String(isFormbricksCloud)}
    - ), -})); - -describe("LoadingPage for API Keys", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the underlying Loading component with correct isFormbricksCloud prop", () => { - render(); - const mockedLoadingComponent = screen.getByTestId("mocked-loading-component"); - expect(mockedLoadingComponent).toBeInTheDocument(); - // Check if the prop is passed correctly based on the mocked constant value - expect(mockedLoadingComponent).toHaveTextContent("isFormbricksCloud: true"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/page.test.tsx deleted file mode 100644 index 2322e618bb..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/page.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Page from "./page"; - -// Mock the APIKeysPage component -vi.mock("@/modules/organization/settings/api-keys/page", () => ({ - APIKeysPage: () =>
    APIKeysPage Content
    , -})); - -describe("APIKeys Page", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the APIKeysPage component", () => { - render(); - const apiKeysPageComponent = screen.getByTestId("mocked-api-keys-page"); - expect(apiKeysPageComponent).toBeInTheDocument(); - expect(apiKeysPageComponent).toHaveTextContent("APIKeysPage Content"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.test.tsx deleted file mode 100644 index 4986f711de..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import Loading from "./loading"; - -// Mock constants -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: true, -})); - -// Mock server-side translation -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(), -})); - -// Mock child components -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ pageTitle, children }: { pageTitle: string; children: React.ReactNode }) => ( -
    -

    {pageTitle}

    - {children} -
    - ), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar", - () => ({ - OrganizationSettingsNavbar: ({ activeId, loading }: { activeId: string; loading?: boolean }) => ( -
    - Active: {activeId}, Loading: {String(loading)} -
    - ), - }) -); - -describe("Billing Loading Page", () => { - beforeEach(async () => { - const mockTranslate = vi.fn((key) => key); - vi.mocked(await import("@/tolgee/server")).getTranslate.mockResolvedValue(mockTranslate); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders PageContentWrapper, PageHeader, and OrganizationSettingsNavbar", async () => { - render(await Loading()); - - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - const pageHeader = screen.getByTestId("page-header"); - expect(pageHeader).toBeInTheDocument(); - expect(pageHeader).toHaveTextContent("environments.settings.general.organization_settings"); - - const navbar = screen.getByTestId("org-settings-navbar"); - expect(navbar).toBeInTheDocument(); - expect(navbar).toHaveTextContent("Active: billing"); - expect(navbar).toHaveTextContent("Loading: true"); - }); - - test("renders placeholder divs", async () => { - render(await Loading()); - // Check for the presence of divs with animate-pulse, assuming they are the placeholders - const placeholders = screen.getAllByRole("generic", { hidden: true }); // Using a generic role as divs don't have implicit roles - const animatedPlaceholders = placeholders.filter((el) => el.classList.contains("animate-pulse")); - expect(animatedPlaceholders.length).toBeGreaterThanOrEqual(2); // Expecting at least two placeholder divs - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/page.test.tsx deleted file mode 100644 index 1bfd1e29da..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/page.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Page from "./page"; - -// Mock the PricingPage component -vi.mock("@/modules/ee/billing/page", () => ({ - PricingPage: () =>
    PricingPage Content
    , -})); - -describe("Billing Page", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the PricingPage component", () => { - render(); - const pricingPageComponent = screen.getByTestId("mocked-pricing-page"); - expect(pricingPageComponent).toBeInTheDocument(); - expect(pricingPageComponent).toHaveTextContent("PricingPage Content"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.test.tsx deleted file mode 100644 index 960524917b..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.test.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { usePathname } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganizationRole } from "@formbricks/types/memberships"; -import { getAccessFlags } from "@/lib/membership/utils"; -import { OrganizationSettingsNavbar } from "./OrganizationSettingsNavbar"; - -vi.mock("next/navigation", () => ({ - usePathname: vi.fn(), -})); - -vi.mock("@/lib/membership/utils", () => ({ - getAccessFlags: vi.fn(), -})); - -// Mock SecondaryNavigation to inspect its props -let mockSecondaryNavigationProps: any; -vi.mock("@/modules/ui/components/secondary-navigation", () => ({ - SecondaryNavigation: (props: any) => { - mockSecondaryNavigationProps = props; - return
    Mocked SecondaryNavigation
    ; - }, -})); - -describe("OrganizationSettingsNavbar", () => { - beforeEach(() => { - mockSecondaryNavigationProps = null; // Reset before each test - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const defaultProps = { - environmentId: "env123", - isFormbricksCloud: true, - membershipRole: "owner" as TOrganizationRole, - activeId: "general", - loading: false, - }; - - test.each([ - { - pathname: "/environments/env123/settings/general", - role: "owner", - isCloud: true, - expectedVisibility: { general: true, billing: true, teams: true, enterprise: false, "api-keys": true }, - }, - { - pathname: "/environments/env123/settings/teams", - role: "member", - isCloud: false, - expectedVisibility: { - general: true, - billing: false, - teams: true, - enterprise: false, - "api-keys": false, - }, - }, // enterprise hidden if not cloud, api-keys hidden if not owner - { - pathname: "/environments/env123/settings/api-keys", - role: "admin", - isCloud: true, - expectedVisibility: { general: true, billing: true, teams: true, enterprise: false, "api-keys": false }, - }, // api-keys hidden if not owner - { - pathname: "/environments/env123/settings/enterprise", - role: "owner", - isCloud: false, - expectedVisibility: { general: true, billing: false, teams: true, enterprise: true, "api-keys": true }, - }, // enterprise shown if not cloud and not member - ])( - "renders correct navigation items based on props and path ($pathname, $role, $isCloud)", - ({ pathname, role, isCloud, expectedVisibility }) => { - vi.mocked(usePathname).mockReturnValue(pathname); - vi.mocked(getAccessFlags).mockReturnValue({ - isOwner: role === "owner", - isMember: role === "member", - } as any); - - render( - - ); - - expect(screen.getByTestId("secondary-navigation")).toBeInTheDocument(); - expect(mockSecondaryNavigationProps).not.toBeNull(); - - const visibleNavItems = mockSecondaryNavigationProps.navigation.filter((item: any) => !item.hidden); - const visibleIds = visibleNavItems.map((item: any) => item.id); - - Object.entries(expectedVisibility).forEach(([id, shouldBeVisible]) => { - if (shouldBeVisible) { - expect(visibleIds).toContain(id); - } else { - expect(visibleIds).not.toContain(id); - } - }); - - // Check current status - mockSecondaryNavigationProps.navigation.forEach((item: any) => { - if (item.href === pathname) { - expect(item.current).toBe(true); - } - }); - } - ); - - test("passes loading prop to SecondaryNavigation", () => { - vi.mocked(usePathname).mockReturnValue("/environments/env123/settings/general"); - vi.mocked(getAccessFlags).mockReturnValue({ - isOwner: true, - isMember: false, - } as any); - render(); - expect(mockSecondaryNavigationProps.loading).toBe(true); - }); - - test("hides billing when loading is true", () => { - vi.mocked(usePathname).mockReturnValue("/environments/env123/settings/general"); - vi.mocked(getAccessFlags).mockReturnValue({ - isOwner: true, - isMember: false, - } as any); - render(); - const billingItem = mockSecondaryNavigationProps.navigation.find((item: any) => item.id === "billing"); - expect(billingItem.hidden).toBe(true); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.test.tsx deleted file mode 100644 index 74d4b55726..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.test.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Loading from "./loading"; - -// Mock constants -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, // Enterprise page is typically for self-hosted -})); - -// Mock server-side translation -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -// Mock child components -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ pageTitle, children }: { pageTitle: string; children: React.ReactNode }) => ( -
    -

    {pageTitle}

    - {children} -
    - ), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar", - () => ({ - OrganizationSettingsNavbar: ({ activeId, loading }: { activeId: string; loading?: boolean }) => ( -
    - Active: {activeId}, Loading: {String(loading)} -
    - ), - }) -); - -describe("Enterprise Loading Page", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders PageContentWrapper, PageHeader, and OrganizationSettingsNavbar", async () => { - render(await Loading()); - - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - const pageHeader = screen.getByTestId("page-header"); - expect(pageHeader).toBeInTheDocument(); - expect(pageHeader).toHaveTextContent("environments.settings.general.organization_settings"); - - const navbar = screen.getByTestId("org-settings-navbar"); - expect(navbar).toBeInTheDocument(); - expect(navbar).toHaveTextContent("Active: enterprise"); - expect(navbar).toHaveTextContent("Loading: true"); - }); - - test("renders placeholder divs", async () => { - render(await Loading()); - const placeholders = screen.getAllByRole("generic", { hidden: true }); - const animatedPlaceholders = placeholders.filter((el) => el.classList.contains("animate-pulse")); - expect(animatedPlaceholders.length).toBeGreaterThanOrEqual(2); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.test.tsx deleted file mode 100644 index ee78d2fb1b..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.test.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { getServerSession } from "next-auth"; -import { redirect } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TMembership } from "@formbricks/types/memberships"; -import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations"; -import { TUser } from "@formbricks/types/user"; -import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; -import { getAccessFlags } from "@/lib/membership/utils"; -import { getOrganizationByEnvironmentId } from "@/lib/organization/service"; -import { getUser } from "@/lib/user/service"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; - -vi.mock("@formbricks/database", () => ({ - prisma: { - membership: { - findMany: vi.fn(), - }, - environment: { - findUnique: vi.fn(), - }, - project: { - findFirst: vi.fn(), - }, - }, -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), - usePathname: vi.fn(), - notFound: vi.fn(), -})); - -vi.mock("@/lib/organization/service", () => ({ - getOrganizationByEnvironmentId: vi.fn(), -})); - -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); - -vi.mock("@/lib/membership/service", () => ({ - getMembershipByUserIdOrganizationId: vi.fn(), -})); - -vi.mock("@/lib/membership/utils", () => ({ - getAccessFlags: vi.fn(), -})); - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/ui/components/settings-card", () => ({ - SettingsCard: ({ title, description, children }: any) => ( -
    -

    {title}

    -

    {description}

    - {children} -
    - ), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -let mockIsFormbricksCloud = false; -vi.mock("@/lib/constants", async () => ({ - get IS_FORMBRICKS_CLOUD() { - return mockIsFormbricksCloud; - }, - IS_PRODUCTION: false, - FB_LOGO_URL: "https://example.com/mock-logo.png", - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "mock-github-secret", - GOOGLE_CLIENT_ID: "mock-google-client-id", - GOOGLE_CLIENT_SECRET: "mock-google-client-secret", - AZUREAD_CLIENT_ID: "mock-azuread-client-id", - AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", - AZUREAD_TENANT_ID: "mock-azuread-tenant-id", - OIDC_CLIENT_ID: "mock-oidc-client-id", - OIDC_CLIENT_SECRET: "mock-oidc-client-secret", - OIDC_ISSUER: "mock-oidc-issuer", - OIDC_DISPLAY_NAME: "mock-oidc-display-name", - OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm", - SAML_DATABASE_URL: "mock-saml-database-url", - WEBAPP_URL: "mock-webapp-url", - SMTP_HOST: "mock-smtp-host", - SMTP_PORT: "mock-smtp-port", - E2E_TESTING: "mock-e2e-testing", -})); - -const mockEnvironmentId = "c6x2k3vq00000e5twdfh8x9xg"; -const mockOrganizationId = "test-org-id"; -const mockUserId = "test-user-id"; - -const mockSession = { - user: { - id: mockUserId, - }, -}; - -const mockUser = { - id: mockUserId, - name: "Test User", - email: "test@example.com", - createdAt: new Date(), - updatedAt: new Date(), - emailVerified: new Date(), - twoFactorEnabled: false, - identityProvider: "email", - notificationSettings: { alert: {} }, - role: "project_manager", - objective: "other", -} as unknown as TUser; - -const mockOrganization = { - id: mockOrganizationId, - name: "Test Organization", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - stripeCustomerId: null, - plan: "free", - limits: { monthly: { responses: null, miu: null }, projects: null }, - features: { - isUsageBasedSubscriptionEnabled: false, - isSubscriptionUpdateDisabled: false, - }, - } as unknown as TOrganizationBilling, -} as unknown as TOrganization; - -const mockMembership: TMembership = { - organizationId: mockOrganizationId, - userId: mockUserId, - accepted: true, - role: "owner", -}; - -describe("EnterpriseSettingsPage", () => { - beforeEach(() => { - vi.resetAllMocks(); - mockIsFormbricksCloud = false; - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - environmentId: mockEnvironmentId, - organizationId: mockOrganizationId, - userId: mockUserId, - } as any); - vi.mocked(getServerSession).mockResolvedValue(mockSession as any); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - vi.mocked(getAccessFlags).mockReturnValue({ isOwner: true, isAdmin: true } as any); // Ensure isAdmin is also covered if relevant - }); - - afterEach(() => { - cleanup(); - }); - - test("renders correctly for an owner when not on Formbricks Cloud", async () => { - vi.resetModules(); - await vi.doMock("@/modules/ee/license-check/lib/license", () => ({ - getEnterpriseLicense: vi.fn().mockResolvedValue({ - active: false, - isPendingDowngrade: false, - features: { isMultiOrgEnabled: false }, - lastChecked: new Date(), - fallbackLevel: "live", - }), - })); - const { default: EnterpriseSettingsPage } = await import("./page"); - const Page = await EnterpriseSettingsPage({ params: { environmentId: mockEnvironmentId } }); - render(Page); - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.enterprise.sso")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.billing.remove_branding")).toBeInTheDocument(); - expect(redirect).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/DeleteOrganization.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/DeleteOrganization.test.tsx deleted file mode 100644 index 5ed92948e3..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/DeleteOrganization.test.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { cleanup, render, screen, waitFor, within } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { useRouter } from "next/navigation"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations"; -import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions"; -import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage"; -import { DeleteOrganization } from "./DeleteOrganization"; - -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -vi.mock("@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions", () => ({ - deleteOrganizationAction: vi.fn(), -})); - -const mockT = (key: string, params?: any) => { - if (params && typeof params === "object") { - let translation = key; - for (const p in params) { - translation = translation.replace(`{{${p}}}`, params[p]); - } - return translation; - } - return key; -}; - -const organizationMock = { - id: "org_123", - name: "Test Organization", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - stripeCustomerId: null, - plan: "free", - } as unknown as TOrganizationBilling, -} as unknown as TOrganization; - -const mockRouterPush = vi.fn(); - -const renderComponent = (props: Partial[0]> = {}) => { - const defaultProps = { - organization: organizationMock, - isDeleteDisabled: false, - isUserOwner: true, - ...props, - }; - return render(); -}; - -describe("DeleteOrganization", () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(useRouter).mockReturnValue({ push: mockRouterPush } as any); - localStorage.clear(); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders delete button and info text when delete is not disabled", () => { - renderComponent(); - expect(screen.getByText("environments.settings.general.once_its_gone_its_gone")).toBeInTheDocument(); - const deleteButton = screen.getByRole("button", { name: "common.delete" }); - expect(deleteButton).toBeInTheDocument(); - expect(deleteButton).not.toBeDisabled(); - }); - - test("renders warning and no delete button when delete is disabled and user is owner", () => { - renderComponent({ isDeleteDisabled: true, isUserOwner: true }); - expect( - screen.getByText("environments.settings.general.cannot_delete_only_organization") - ).toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "common.delete" })).not.toBeInTheDocument(); - }); - - test("renders warning and no delete button when delete is disabled and user is not owner", () => { - renderComponent({ isDeleteDisabled: true, isUserOwner: false }); - expect( - screen.getByText("environments.settings.general.only_org_owner_can_perform_action") - ).toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "common.delete" })).not.toBeInTheDocument(); - }); - - test("opens delete dialog on button click", async () => { - renderComponent(); - const deleteButton = screen.getByRole("button", { name: "common.delete" }); - await userEvent.click(deleteButton); - expect(screen.getByText("environments.settings.general.delete_organization_warning")).toBeInTheDocument(); - expect( - screen.getByText( - mockT("environments.settings.general.delete_organization_warning_3", { - organizationName: organizationMock.name, - }) - ) - ).toBeInTheDocument(); - }); - - test("delete button in modal is disabled until correct organization name is typed", async () => { - renderComponent(); - const deleteButton = screen.getByRole("button", { name: "common.delete" }); - await userEvent.click(deleteButton); - - const dialog = screen.getByRole("dialog"); - const modalDeleteButton = within(dialog).getByRole("button", { name: "common.delete" }); - expect(modalDeleteButton).toBeDisabled(); - - const inputField = screen.getByPlaceholderText(organizationMock.name); - await userEvent.type(inputField, organizationMock.name); - expect(modalDeleteButton).not.toBeDisabled(); - - await userEvent.clear(inputField); - await userEvent.type(inputField, "Wrong Name"); - expect(modalDeleteButton).toBeDisabled(); - }); - - test("calls deleteOrganizationAction on confirm, shows success, clears localStorage, and navigates", async () => { - vi.mocked(deleteOrganizationAction).mockResolvedValue({} as any); - localStorage.setItem(FORMBRICKS_ENVIRONMENT_ID_LS, "some-env-id"); - renderComponent(); - - const deleteButton = screen.getByRole("button", { name: "common.delete" }); - await userEvent.click(deleteButton); - - const inputField = screen.getByPlaceholderText(organizationMock.name); - await userEvent.type(inputField, organizationMock.name); - - const dialog = screen.getByRole("dialog"); - const modalDeleteButton = within(dialog).getByRole("button", { name: "common.delete" }); - await userEvent.click(modalDeleteButton); - - await waitFor(() => { - expect(deleteOrganizationAction).toHaveBeenCalledWith({ organizationId: organizationMock.id }); - expect(toast.success).toHaveBeenCalledWith( - "environments.settings.general.organization_deleted_successfully" - ); - expect(localStorage.getItem(FORMBRICKS_ENVIRONMENT_ID_LS)).toBeNull(); - expect(mockRouterPush).toHaveBeenCalledWith("/"); - expect( - screen.queryByText("environments.settings.general.delete_organization_warning") - ).not.toBeInTheDocument(); // Modal should close - }); - }); - - test("shows error toast on deleteOrganizationAction failure", async () => { - vi.mocked(deleteOrganizationAction).mockRejectedValue(new Error("Deletion failed")); - renderComponent(); - - const deleteButton = screen.getByRole("button", { name: "common.delete" }); - await userEvent.click(deleteButton); - - const inputField = screen.getByPlaceholderText(organizationMock.name); - await userEvent.type(inputField, organizationMock.name); - - const dialog = screen.getByRole("dialog"); - const modalDeleteButton = within(dialog).getByRole("button", { name: "common.delete" }); - await userEvent.click(modalDeleteButton); - - await waitFor(() => { - expect(deleteOrganizationAction).toHaveBeenCalledWith({ organizationId: organizationMock.id }); - expect(toast.error).toHaveBeenCalledWith( - "environments.settings.general.error_deleting_organization_please_try_again" - ); - expect( - screen.queryByText("environments.settings.general.delete_organization_warning") - ).not.toBeInTheDocument(); // Modal should close - }); - }); - - test("closes modal on cancel click", async () => { - renderComponent(); - const deleteButton = screen.getByRole("button", { name: "common.delete" }); - await userEvent.click(deleteButton); - - expect(screen.getByText("environments.settings.general.delete_organization_warning")).toBeInTheDocument(); - const cancelButton = screen.getByRole("button", { name: "common.cancel" }); - await userEvent.click(cancelButton); - - await waitFor(() => { - expect( - screen.queryByText("environments.settings.general.delete_organization_warning") - ).not.toBeInTheDocument(); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/EditOrganizationNameForm.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/EditOrganizationNameForm.test.tsx deleted file mode 100644 index cd8ded8e39..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/EditOrganizationNameForm.test.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions"; -import { EditOrganizationNameForm } from "./EditOrganizationNameForm"; - -vi.mock("@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions", () => ({ - updateOrganizationNameAction: vi.fn(), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -const organizationMock = { - id: "org_123", - name: "Old Organization Name", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - stripeCustomerId: null, - plan: "free", - } as unknown as TOrganization["billing"], -} as unknown as TOrganization; - -const renderForm = (membershipRole: "owner" | "member") => { - return render( - - ); -}; - -describe("EditOrganizationNameForm", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - vi.mocked(updateOrganizationNameAction).mockReset(); - }); - - test("renders with initial organization name and allows owner to update", async () => { - renderForm("owner"); - - const nameInput = screen.getByPlaceholderText( - "environments.settings.general.organization_name_placeholder" - ); - expect(nameInput).toHaveValue(organizationMock.name); - expect(nameInput).not.toBeDisabled(); - - const updateButton = screen.getByText("common.update"); - expect(updateButton).toBeDisabled(); // Initially disabled as form is not dirty - - await userEvent.clear(nameInput); - await userEvent.type(nameInput, "New Organization Name"); - expect(updateButton).not.toBeDisabled(); // Enabled after change - - vi.mocked(updateOrganizationNameAction).mockResolvedValueOnce({ - data: { ...organizationMock, name: "New Organization Name" }, - }); - - await userEvent.click(updateButton); - - await waitFor(() => { - expect(updateOrganizationNameAction).toHaveBeenCalledWith({ - organizationId: organizationMock.id, - data: { name: "New Organization Name" }, - }); - expect( - screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder") - ).toHaveValue("New Organization Name"); - expect(toast.success).toHaveBeenCalledWith( - "environments.settings.general.organization_name_updated_successfully" - ); - }); - expect(updateButton).toBeDisabled(); // Disabled after successful submit and reset - }); - - test("shows error toast on update failure", async () => { - renderForm("owner"); - - const nameInput = screen.getByPlaceholderText( - "environments.settings.general.organization_name_placeholder" - ); - await userEvent.clear(nameInput); - await userEvent.type(nameInput, "Another Name"); - - const updateButton = screen.getByText("common.update"); - - vi.mocked(updateOrganizationNameAction).mockResolvedValueOnce({ - data: null as any, - }); - - await userEvent.click(updateButton); - - await waitFor(() => { - expect(updateOrganizationNameAction).toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalledWith(""); - }); - expect(nameInput).toHaveValue("Another Name"); // Name should not reset on error - }); - - test("shows generic error toast on exception during update", async () => { - renderForm("owner"); - - const nameInput = screen.getByPlaceholderText( - "environments.settings.general.organization_name_placeholder" - ); - await userEvent.clear(nameInput); - await userEvent.type(nameInput, "Exception Name"); - - const updateButton = screen.getByText("common.update"); - - vi.mocked(updateOrganizationNameAction).mockRejectedValueOnce(new Error("Network error")); - - await userEvent.click(updateButton); - - await waitFor(() => { - expect(updateOrganizationNameAction).toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalledWith("Error: Network error"); - }); - }); - - test("disables input and button for non-owner roles and shows warning", async () => { - const roles: "member"[] = ["member"]; - for (const role of roles) { - renderForm(role); - - const nameInput = screen.getByPlaceholderText( - "environments.settings.general.organization_name_placeholder" - ); - expect(nameInput).toBeDisabled(); - - const updateButton = screen.getByText("common.update"); - expect(updateButton).toBeDisabled(); - - expect( - screen.getByText("environments.settings.general.only_org_owner_can_perform_action") - ).toBeInTheDocument(); - cleanup(); - } - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/loading.test.tsx deleted file mode 100644 index 4f587c32d1..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/loading.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; -import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; -import { getTranslate } from "@/tolgee/server"; -import Loading from "./loading"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar", - () => ({ - OrganizationSettingsNavbar: vi.fn(() =>
    OrganizationSettingsNavbar
    ), - }) -); - -vi.mock("@/app/(app)/components/LoadingCard", () => ({ - LoadingCard: vi.fn(({ title, description }) => ( -
    -
    {title}
    -
    {description}
    -
    - )), -})); - -describe("Loading", () => { - const mockTranslate = vi.fn((key) => key); - - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.resetAllMocks(); - vi.mocked(getTranslate).mockResolvedValue(mockTranslate); - }); - - test("renders loading state correctly", async () => { - const LoadingComponent = await Loading(); - render(LoadingComponent); - - expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument(); - expect(OrganizationSettingsNavbar).toHaveBeenCalledWith( - { - isFormbricksCloud: IS_FORMBRICKS_CLOUD, - activeId: "general", - loading: true, - }, - undefined - ); - - expect(screen.getByText("environments.settings.general.organization_name")).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.general.organization_name_description") - ).toBeInTheDocument(); - expect(screen.getByText("environments.settings.general.delete_organization")).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.general.delete_organization_description") - ).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx deleted file mode 100644 index 722d5073bd..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx +++ /dev/null @@ -1,405 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TUser } from "@formbricks/types/user"; -import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; -import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants"; -import { getUser } from "@/lib/user/service"; -import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils"; -import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; -import { IdBadge } from "@/modules/ui/components/id-badge"; -import { getTranslate } from "@/tolgee/server"; -import { DeleteOrganization } from "./components/DeleteOrganization"; -import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm"; -import Page from "./page"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_PRODUCTION: false, - IS_STORAGE_CONFIGURED: true, - FB_LOGO_URL: "https://example.com/mock-logo.png", - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "mock-github-secret", - GOOGLE_CLIENT_ID: "mock-google-client-id", - GOOGLE_CLIENT_SECRET: "mock-google-client-secret", - AZUREAD_CLIENT_ID: "mock-azuread-client-id", - AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", - AZUREAD_TENANT_ID: "mock-azuread-tenant-id", - OIDC_CLIENT_ID: "mock-oidc-client-id", - OIDC_CLIENT_SECRET: "mock-oidc-client-secret", - OIDC_ISSUER: "mock-oidc-issuer", - OIDC_DISPLAY_NAME: "mock-oidc-display-name", - OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm", - SAML_DATABASE_URL: "mock-saml-database-url", - WEBAPP_URL: "mock-webapp-url", - SMTP_HOST: "mock-smtp-host", - SMTP_PORT: "mock-smtp-port", -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(), -})); - -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getIsMultiOrgEnabled: vi.fn(), - getWhiteLabelPermission: vi.fn(), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar", - () => ({ - OrganizationSettingsNavbar: vi.fn(() =>
    OrganizationSettingsNavbar
    ), - }) -); - -vi.mock("./components/EditOrganizationNameForm", () => ({ - EditOrganizationNameForm: vi.fn(() =>
    EditOrganizationNameForm
    ), -})); - -vi.mock("@/modules/ee/whitelabel/email-customization/components/email-customization-settings", () => ({ - EmailCustomizationSettings: vi.fn(() =>
    EmailCustomizationSettings
    ), -})); - -vi.mock("./components/DeleteOrganization", () => ({ - DeleteOrganization: vi.fn(() =>
    DeleteOrganization
    ), -})); - -vi.mock("@/modules/ui/components/id-badge", () => ({ - IdBadge: vi.fn(() =>
    IdBadge
    ), -})); - -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children, variant }: any) => ( -
    - {children} -
    - ), - AlertDescription: ({ children }: any) =>
    {children}
    , -})); - -describe("Page", () => { - afterEach(() => { - cleanup(); - }); - - let mockEnvironmentAuth = { - session: { user: { id: "test-user-id" } }, - currentUserMembership: { role: "owner" }, - organization: { id: "test-organization-id", billing: { plan: "free" } }, - isOwner: true, - isManager: false, - } as unknown as TEnvironmentAuth; - - const mockUser = { id: "test-user-id" } as TUser; - const mockTranslate = vi.fn((key) => key); - const mockParams = { environmentId: "env-123" }; - - beforeEach(() => { - vi.resetAllMocks(); - vi.mocked(getTranslate).mockResolvedValue(mockTranslate); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth); - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); - vi.mocked(getWhiteLabelPermission).mockResolvedValue(true); - }); - - test("renders the page with organization settings for owner", async () => { - const props = { - params: Promise.resolve(mockParams), - }; - - const PageComponent = await Page(props); - render(PageComponent); - - expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument(); - expect(OrganizationSettingsNavbar).toHaveBeenCalledWith( - { - environmentId: mockParams.environmentId, - isFormbricksCloud: IS_FORMBRICKS_CLOUD, - membershipRole: "owner", - activeId: "general", - }, - undefined - ); - expect(screen.getByText("environments.settings.general.organization_name")).toBeInTheDocument(); - expect(EditOrganizationNameForm).toHaveBeenCalledWith( - { - organization: mockEnvironmentAuth.organization, - environmentId: mockParams.environmentId, - membershipRole: "owner", - }, - undefined - ); - expect(EmailCustomizationSettings).toHaveBeenCalledWith( - { - organization: mockEnvironmentAuth.organization, - hasWhiteLabelPermission: true, - environmentId: mockParams.environmentId, - isReadOnly: false, - isFormbricksCloud: IS_FORMBRICKS_CLOUD, - fbLogoUrl: FB_LOGO_URL, - user: mockUser, - isStorageConfigured: true, - }, - undefined - ); - expect(screen.getByText("environments.settings.general.delete_organization")).toBeInTheDocument(); - expect(DeleteOrganization).toHaveBeenCalledWith( - { - organization: mockEnvironmentAuth.organization, - isDeleteDisabled: false, - isUserOwner: true, - }, - undefined - ); - expect(IdBadge).toHaveBeenCalledWith( - { - id: mockEnvironmentAuth.organization.id, - label: "common.organization_id", - variant: "column", - }, - undefined - ); - }); - - test("renders correctly when user is manager", async () => { - const managerAuth = { - ...mockEnvironmentAuth, - currentUserMembership: { role: "manager" }, - isOwner: false, - isManager: true, - } as unknown as TEnvironmentAuth; - vi.mocked(getEnvironmentAuth).mockResolvedValue(managerAuth); - - const props = { - params: Promise.resolve(mockParams), - }; - const PageComponent = await Page(props); - render(PageComponent); - - expect(EmailCustomizationSettings).toHaveBeenCalledWith( - expect.objectContaining({ - isReadOnly: false, // owner or manager can edit - }), - undefined - ); - expect(DeleteOrganization).toHaveBeenCalledWith( - expect.objectContaining({ - isDeleteDisabled: true, // only owner can delete - isUserOwner: false, - }), - undefined - ); - }); - - test("renders correctly when multi-org is disabled", async () => { - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false); - const props = { - params: Promise.resolve(mockParams), - }; - const PageComponent = await Page(props); - render(PageComponent); - - expect(screen.queryByText("environments.settings.general.delete_organization")).not.toBeInTheDocument(); - expect(DeleteOrganization).not.toHaveBeenCalled(); - // isDeleteDisabled should be true because multiOrg is disabled, even for owner - expect(EmailCustomizationSettings).toHaveBeenCalledWith( - expect.objectContaining({ - isReadOnly: false, - }), - undefined - ); - }); - - test("renders correctly when user is not owner or manager (e.g., admin)", async () => { - const adminAuth = { - ...mockEnvironmentAuth, - currentUserMembership: { role: "admin" }, - isOwner: false, - isManager: false, - } as unknown as TEnvironmentAuth; - vi.mocked(getEnvironmentAuth).mockResolvedValue(adminAuth); - - const props = { - params: Promise.resolve(mockParams), - }; - const PageComponent = await Page(props); - render(PageComponent); - - expect(EmailCustomizationSettings).toHaveBeenCalledWith( - expect.objectContaining({ - isReadOnly: true, - }), - undefined - ); - expect(DeleteOrganization).toHaveBeenCalledWith( - expect.objectContaining({ - isDeleteDisabled: true, - isUserOwner: false, - }), - undefined - ); - }); - - test("renders if session user id empty, user is null", async () => { - const noUserSessionAuth = { - ...mockEnvironmentAuth, - session: { ...mockEnvironmentAuth.session, user: { ...mockEnvironmentAuth.session.user, id: "" } }, - }; - vi.mocked(getEnvironmentAuth).mockResolvedValue(noUserSessionAuth); - vi.mocked(getUser).mockResolvedValue(null); - - const props = { - params: Promise.resolve(mockParams), - }; - - const PageComponent = await Page(props); - render(PageComponent); - expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument(); - expect(EmailCustomizationSettings).toHaveBeenCalledWith( - expect.objectContaining({ - user: null, - }), - undefined - ); - }); - - test("handles getEnvironmentAuth error", async () => { - vi.mocked(getEnvironmentAuth).mockRejectedValue(new Error("Authentication error")); - - const props = { - params: Promise.resolve({ environmentId: "env-123" }), - }; - - await expect(Page(props)).rejects.toThrow("Authentication error"); - }); - - test("does not show storage warning when IS_STORAGE_CONFIGURED is true", async () => { - const props = { - params: Promise.resolve(mockParams), - }; - - const PageComponent = await Page(props); - render(PageComponent); - - expect(screen.queryByTestId("alert")).not.toBeInTheDocument(); - }); - - test("shows storage warning when IS_STORAGE_CONFIGURED is false", async () => { - // Mock IS_STORAGE_CONFIGURED as false - vi.doMock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_PRODUCTION: false, - IS_STORAGE_CONFIGURED: false, - FB_LOGO_URL: "https://example.com/mock-logo.png", - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "mock-github-secret", - GOOGLE_CLIENT_ID: "mock-google-client-id", - GOOGLE_CLIENT_SECRET: "mock-google-client-secret", - AZUREAD_CLIENT_ID: "mock-azuread-client-id", - AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", - AZUREAD_TENANT_ID: "mock-azuread-tenant-id", - OIDC_CLIENT_ID: "mock-oidc-client-id", - OIDC_CLIENT_SECRET: "mock-oidc-client-secret", - OIDC_ISSUER: "mock-oidc-issuer", - OIDC_DISPLAY_NAME: "mock-oidc-display-name", - OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm", - SAML_DATABASE_URL: "mock-saml-database-url", - WEBAPP_URL: "mock-webapp-url", - SMTP_HOST: "mock-smtp-host", - SMTP_PORT: "mock-smtp-port", - })); - - // Re-import the module to get the updated mock - const { default: PageWithStorageDisabled } = await import("./page"); - - const props = { - params: Promise.resolve(mockParams), - }; - - const PageComponent = await PageWithStorageDisabled(props); - render(PageComponent); - - expect(screen.getByTestId("alert")).toBeInTheDocument(); - expect(screen.getByTestId("alert")).toHaveAttribute("data-variant", "warning"); - expect(screen.getByTestId("alert-description")).toHaveTextContent("common.storage_not_configured"); - }); - - test("passes isStorageConfigured=true to EmailCustomizationSettings when storage is configured", async () => { - const props = { - params: Promise.resolve(mockParams), - }; - - const PageComponent = await Page(props); - render(PageComponent); - - expect(EmailCustomizationSettings).toHaveBeenCalledWith( - expect.objectContaining({ - isStorageConfigured: true, - }), - undefined - ); - }); - - test("passes isStorageConfigured=false to EmailCustomizationSettings when storage is not configured", async () => { - // Mock IS_STORAGE_CONFIGURED as false - vi.doMock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_PRODUCTION: false, - IS_STORAGE_CONFIGURED: false, - FB_LOGO_URL: "https://example.com/mock-logo.png", - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "mock-github-secret", - GOOGLE_CLIENT_ID: "mock-google-client-id", - GOOGLE_CLIENT_SECRET: "mock-google-client-secret", - AZUREAD_CLIENT_ID: "mock-azuread-client-id", - AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", - AZUREAD_TENANT_ID: "mock-azuread-tenant-id", - OIDC_CLIENT_ID: "mock-oidc-client-id", - OIDC_CLIENT_SECRET: "mock-oidc-client-secret", - OIDC_ISSUER: "mock-oidc-issuer", - OIDC_DISPLAY_NAME: "mock-oidc-display-name", - OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm", - SAML_DATABASE_URL: "mock-saml-database-url", - WEBAPP_URL: "mock-webapp-url", - SMTP_HOST: "mock-smtp-host", - SMTP_PORT: "mock-smtp-port", - })); - - // Re-import the module to get the updated mock - const { default: PageWithStorageDisabled } = await import("./page"); - - const props = { - params: Promise.resolve(mockParams), - }; - - const PageComponent = await PageWithStorageDisabled(props); - render(PageComponent); - - expect(EmailCustomizationSettings).toHaveBeenCalledWith( - expect.objectContaining({ - isStorageConfigured: false, - }), - undefined - ); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.test.tsx deleted file mode 100644 index 739b04d769..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { Session, getServerSession } from "next-auth"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TProject } from "@formbricks/types/project"; -import { getOrganizationByEnvironmentId } from "@/lib/organization/service"; -import { getProjectByEnvironmentId } from "@/lib/project/service"; -import OrganizationSettingsLayout from "./layout"; - -// Mock dependencies -vi.mock("@/lib/organization/service"); -vi.mock("@/lib/project/service"); -vi.mock("next-auth", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - getServerSession: vi.fn(), - }; -}); -vi.mock("@/modules/auth/lib/authOptions", () => ({ - authOptions: {}, // Mock authOptions if it's directly used or causes issues -})); - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", -})); - -const mockGetOrganizationByEnvironmentId = vi.mocked(getOrganizationByEnvironmentId); -const mockGetProjectByEnvironmentId = vi.mocked(getProjectByEnvironmentId); -const mockGetServerSession = vi.mocked(getServerSession); - -const mockOrganization = { id: "org_test_id" } as unknown as TOrganization; -const mockProject = { id: "project_test_id" } as unknown as TProject; -const mockSession = { user: { id: "user_test_id" } } as unknown as Session; - -const t = (key: string) => key; -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => t, -})); - -const mockProps = { - params: { environmentId: "env_test_id" }, - children:
    Child Content for Organization Settings
    , -}; - -describe("OrganizationSettingsLayout", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.resetAllMocks(); - - mockGetOrganizationByEnvironmentId.mockResolvedValue(mockOrganization); - mockGetProjectByEnvironmentId.mockResolvedValue(mockProject); - mockGetServerSession.mockResolvedValue(mockSession); - }); - - test("should render children when all data is fetched successfully", async () => { - render(await OrganizationSettingsLayout(mockProps)); - expect(screen.getByText("Child Content for Organization Settings")).toBeInTheDocument(); - }); - - test("should throw error if organization is not found", async () => { - mockGetOrganizationByEnvironmentId.mockResolvedValue(null); - await expect(OrganizationSettingsLayout(mockProps)).rejects.toThrowError("common.organization_not_found"); - }); - - test("should throw error if project is not found", async () => { - mockGetProjectByEnvironmentId.mockResolvedValue(null); - await expect(OrganizationSettingsLayout(mockProps)).rejects.toThrowError("common.project_not_found"); - }); - - test("should throw error if session is not found", async () => { - mockGetServerSession.mockResolvedValue(null); - await expect(OrganizationSettingsLayout(mockProps)).rejects.toThrowError("common.session_not_found"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/teams/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/teams/page.test.tsx deleted file mode 100644 index 4d0fdf77f4..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/teams/page.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { TeamsPage } from "@/modules/organization/settings/teams/page"; -import Page from "./page"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - FB_LOGO_URL: "mock-fb-logo-url", - SMTP_HOST: "mock-smtp-host", - SMTP_PORT: 587, - SMTP_USER: "mock-smtp-user", - SMTP_PASSWORD: "mock-smtp-password", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: 1, -})); - -vi.mock("@/lib/env", () => ({ - env: { - PUBLIC_URL: "https://public-domain.com", - }, -})); - -describe("TeamsPage re-export", () => { - test("should re-export TeamsPage component", () => { - expect(Page).toBe(TeamsPage); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.test.tsx deleted file mode 100644 index 15adf1c9e1..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; - -vi.mock("@/modules/ui/components/badge", () => ({ - Badge: ({ text }) =>
    {text}
    , -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key) => key, // Mock t function to return the key - }), -})); - -describe("SettingsCard", () => { - afterEach(() => { - cleanup(); - }); - - const defaultProps = { - title: "Test Title", - description: "Test Description", - children:
    Child Content
    , - }; - - test("renders title, description, and children", () => { - render(); - expect(screen.getByText(defaultProps.title)).toBeInTheDocument(); - expect(screen.getByText(defaultProps.description)).toBeInTheDocument(); - expect(screen.getByTestId("child-content")).toBeInTheDocument(); - }); - - test("renders Beta badge when beta prop is true", () => { - render(); - const badgeElement = screen.getByTestId("mock-badge"); - expect(badgeElement).toBeInTheDocument(); - expect(badgeElement).toHaveTextContent("Beta"); - }); - - test("renders Soon badge when soon prop is true", () => { - render(); - const badgeElement = screen.getByTestId("mock-badge"); - expect(badgeElement).toBeInTheDocument(); - expect(badgeElement).toHaveTextContent("environments.settings.enterprise.coming_soon"); - }); - - test("does not render badges when beta and soon props are false", () => { - render(); - expect(screen.queryByTestId("mock-badge")).not.toBeInTheDocument(); - }); - - test("applies default padding when noPadding prop is false", () => { - render(); - const childrenContainer = screen.getByTestId("child-content").parentElement; - expect(childrenContainer).toHaveClass("px-4 pt-4"); - }); - - test("applies custom className to the root element", () => { - const customClass = "my-custom-class"; - render(); - const cardElement = screen.getByText(defaultProps.title).closest("div.relative"); - expect(cardElement).toHaveClass(customClass); - }); - - test("renders with default classes", () => { - render(); - const cardElement = screen.getByText(defaultProps.title).closest("div.relative"); - expect(cardElement).toHaveClass( - "relative my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm" - ); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsTitle.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsTitle.test.tsx deleted file mode 100644 index fdcd63ff10..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsTitle.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test } from "vitest"; -import { SettingsTitle } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsTitle"; - -describe("SettingsTitle", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the title correctly", () => { - const titleText = "My Awesome Settings"; - render(); - const headingElement = screen.getByRole("heading", { name: titleText, level: 2 }); - expect(headingElement).toBeInTheDocument(); - expect(headingElement).toHaveTextContent(titleText); - expect(headingElement).toHaveClass("my-4 text-2xl font-medium leading-6 text-slate-800"); - }); - - test("renders with an empty title", () => { - render(); - const headingElement = screen.getByRole("heading", { level: 2 }); - expect(headingElement).toBeInTheDocument(); - expect(headingElement).toHaveTextContent(""); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/page.test.tsx deleted file mode 100644 index b2f786228a..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/page.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { redirect } from "next/navigation"; -import { describe, expect, test, vi } from "vitest"; -import Page from "./page"; - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -describe("Settings Page", () => { - test("should redirect to profile settings page", async () => { - const params = { environmentId: "testEnvId" }; - await Page({ params }); - expect(redirect).toHaveBeenCalledWith(`/environments/${params.environmentId}/settings/profile`); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.test.tsx deleted file mode 100644 index ec298b7eb9..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { Unplug } from "lucide-react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { EmptyAppSurveys } from "./EmptyInAppSurveys"; - -vi.mock("lucide-react", async () => { - const actual = await vi.importActual("lucide-react"); - return { - ...actual, - Unplug: vi.fn(() =>
    ), - }; -}); - -const mockEnvironment = { - id: "test-env-id", -} as unknown as TEnvironment; - -describe("EmptyAppSurveys", () => { - afterEach(() => { - cleanup(); - }); - - test("renders correctly with translated text and icon", () => { - render(); - - expect(screen.getByTestId("unplug-icon")).toBeInTheDocument(); - expect(Unplug).toHaveBeenCalled(); - - expect(screen.getByText("environments.surveys.summary.youre_not_plugged_in_yet")).toBeInTheDocument(); - expect( - screen.getByText( - "environments.surveys.summary.connect_your_website_or_app_with_formbricks_to_get_started" - ) - ).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.test.tsx deleted file mode 100644 index b45177fa49..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.test.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { act, cleanup, render, waitFor } from "@testing-library/react"; -import { useParams, usePathname, useSearchParams } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TLanguage } from "@formbricks/types/project"; -import { - TSurvey, - TSurveyLanguage, - TSurveyQuestion, - TSurveyQuestionTypeEnum, -} from "@formbricks/types/surveys/types"; -import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; -import { - getResponseCountAction, - revalidateSurveyIdPath, -} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions"; -import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation"; -import { getFormattedFilters } from "@/app/lib/surveys/surveys"; -import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - FB_LOGO_URL: "mock-fb-logo-url", - SMTP_HOST: "mock-smtp-host", - SMTP_PORT: 587, - SMTP_USER: "mock-smtp-user", - SMTP_PASSWORD: "mock-smtp-password", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -vi.mock("@/lib/env", () => ({ - env: { - PUBLIC_URL: "https://public-domain.com", - }, -})); - -vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"); -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions"); -vi.mock("@/app/lib/surveys/surveys"); -vi.mock("@/modules/ui/components/secondary-navigation", () => ({ - SecondaryNavigation: vi.fn(() =>
    ), -})); -vi.mock("next/navigation", () => ({ - usePathname: vi.fn(), - useParams: vi.fn(), - useSearchParams: vi.fn(), -})); - -const mockUsePathname = vi.mocked(usePathname); -const mockUseParams = vi.mocked(useParams); -const mockUseSearchParams = vi.mocked(useSearchParams); -const mockUseResponseFilter = vi.mocked(useResponseFilter); -const mockGetResponseCountAction = vi.mocked(getResponseCountAction); -const mockRevalidateSurveyIdPath = vi.mocked(revalidateSurveyIdPath); -const mockGetFormattedFilters = vi.mocked(getFormattedFilters); -const MockSecondaryNavigation = vi.mocked(SecondaryNavigation); - -const mockSurveyLanguages: TSurveyLanguage[] = [ - { language: { code: "en-US" } as unknown as TLanguage, default: true, enabled: true }, -]; - -const mockSurvey = { - id: "surveyId123", - name: "Test Survey", - type: "app", - environmentId: "envId123", - status: "inProgress", - questions: [ - { - id: "question1", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Question 1" }, - required: false, - logic: [], - isDraft: false, - imageUrl: "", - subheader: { default: "" }, - } as unknown as TSurveyQuestion, - ], - hiddenFields: { enabled: false, fieldIds: [] }, - displayOption: "displayOnce", - autoClose: null, - triggers: [], - createdAt: new Date(), - updatedAt: new Date(), - languages: mockSurveyLanguages, - variables: [], - singleUse: null, - styling: null, - surveyClosedMessage: null, - welcomeCard: { enabled: false, headline: { default: "" } } as unknown as TSurvey["welcomeCard"], - segment: null, - delay: 0, - autoComplete: null, - recontactDays: null, - displayPercentage: null, - createdBy: null, -} as unknown as TSurvey; - -const defaultProps = { - environmentId: "testEnvId", - survey: mockSurvey, - activeId: "summary", -}; - -describe("SurveyAnalysisNavigation", () => { - afterEach(() => { - cleanup(); - vi.resetAllMocks(); - }); - - test("calls revalidateSurveyIdPath on navigation item click", async () => { - mockUsePathname.mockReturnValue( - `/environments/${defaultProps.environmentId}/surveys/${mockSurvey.id}/summary` - ); - mockUseParams.mockReturnValue({ environmentId: defaultProps.environmentId, surveyId: mockSurvey.id }); - mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any); - mockUseResponseFilter.mockReturnValue({ selectedFilter: "all", dateRange: {} } as any); - mockGetFormattedFilters.mockReturnValue([] as any); - mockGetResponseCountAction.mockResolvedValue({ data: 5 }); - - render(); - await waitFor(() => expect(MockSecondaryNavigation).toHaveBeenCalled()); - - const lastCallArgs = MockSecondaryNavigation.mock.calls[MockSecondaryNavigation.mock.calls.length - 1][0]; - - if (!lastCallArgs.navigation || lastCallArgs.navigation.length < 2) { - throw new Error("Navigation items not found"); - } - - act(() => { - (lastCallArgs.navigation[0] as any).onClick(); - }); - expect(mockRevalidateSurveyIdPath).toHaveBeenCalledWith( - defaultProps.environmentId, - defaultProps.survey.id - ); - vi.mocked(mockRevalidateSurveyIdPath).mockClear(); - - act(() => { - (lastCallArgs.navigation[1] as any).onClick(); - }); - expect(mockRevalidateSurveyIdPath).toHaveBeenCalledWith( - defaultProps.environmentId, - defaultProps.survey.id - ); - }); - - test("displays correct response count string in label for various scenarios", async () => { - mockUsePathname.mockReturnValue( - `/environments/${defaultProps.environmentId}/surveys/${mockSurvey.id}/responses` - ); - mockUseParams.mockReturnValue({ environmentId: defaultProps.environmentId, surveyId: mockSurvey.id }); - mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any); - mockUseResponseFilter.mockReturnValue({ selectedFilter: "all", dateRange: {} } as any); - mockGetFormattedFilters.mockReturnValue([] as any); - - // Scenario 1: total = 10, filtered = null (initial state) - render(); - expect(MockSecondaryNavigation.mock.calls[0][0].navigation[1].label).toBe("common.responses"); - cleanup(); - vi.resetAllMocks(); // Reset mocks for next case - - // Scenario 2: total = 15, filtered = 15 - mockUsePathname.mockReturnValue( - `/environments/${defaultProps.environmentId}/surveys/${mockSurvey.id}/responses` - ); - mockUseParams.mockReturnValue({ environmentId: defaultProps.environmentId, surveyId: mockSurvey.id }); - mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any); - mockUseResponseFilter.mockReturnValue({ selectedFilter: "all", dateRange: {} } as any); - mockGetFormattedFilters.mockReturnValue([] as any); - mockGetResponseCountAction.mockImplementation(async (args) => { - if (args && "filterCriteria" in args) return { data: 15, error: null, success: true }; - return { data: 15, error: null, success: true }; - }); - render(); - await waitFor(() => { - const lastCallArgs = - MockSecondaryNavigation.mock.calls[MockSecondaryNavigation.mock.calls.length - 1][0]; - expect(lastCallArgs.navigation[1].label).toBe("common.responses"); - }); - cleanup(); - vi.resetAllMocks(); - - // Scenario 3: total = 10, filtered = 15 (filtered > total) - mockUsePathname.mockReturnValue( - `/environments/${defaultProps.environmentId}/surveys/${mockSurvey.id}/responses` - ); - mockUseParams.mockReturnValue({ environmentId: defaultProps.environmentId, surveyId: mockSurvey.id }); - mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any); - mockUseResponseFilter.mockReturnValue({ selectedFilter: "all", dateRange: {} } as any); - mockGetFormattedFilters.mockReturnValue([] as any); - mockGetResponseCountAction.mockImplementation(async (args) => { - if (args && "filterCriteria" in args) return { data: 15, error: null, success: true }; - return { data: 10, error: null, success: true }; - }); - render(); - await waitFor(() => { - const lastCallArgs = - MockSecondaryNavigation.mock.calls[MockSecondaryNavigation.mock.calls.length - 1][0]; - expect(lastCallArgs.navigation[1].label).toBe("common.responses"); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.test.tsx deleted file mode 100644 index 338599c246..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { getServerSession } from "next-auth"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { getResponseCountBySurveyId } from "@/lib/response/service"; -import { getSurvey } from "@/lib/survey/service"; -import { authOptions } from "@/modules/auth/lib/authOptions"; -import SurveyLayout, { generateMetadata } from "./layout"; - -vi.mock("@/lib/response/service", () => ({ - getResponseCountBySurveyId: vi.fn(), -})); - -vi.mock("@/lib/survey/service", () => ({ - getSurvey: vi.fn(), -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -vi.mock("@/modules/auth/lib/authOptions", () => ({ - authOptions: {}, -})); - -const mockSurveyId = "survey_123"; -const mockEnvironmentId = "env_456"; -const mockSurveyName = "Test Survey"; -const mockResponseCount = 10; - -const mockSurvey = { - id: mockSurveyId, - name: mockSurveyName, - questions: [], - endings: [], - status: "inProgress", - type: "app", - environmentId: mockEnvironmentId, - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - variables: [], - triggers: [], - styling: null, - languages: [], - segment: null, - autoClose: null, - delay: 0, - displayLimit: null, - displayOption: "displayOnce", - isBackButtonHidden: false, - pin: null, - recontactDays: null, - showLanguageSwitch: false, - singleUse: null, - surveyClosedMessage: null, - createdAt: new Date(), - updatedAt: new Date(), - autoComplete: null, - hiddenFields: { enabled: false, fieldIds: [] }, -} as unknown as TSurvey; - -describe("SurveyLayout", () => { - afterEach(() => { - cleanup(); - vi.resetAllMocks(); - }); - - describe("generateMetadata", () => { - test("should return correct metadata when session and survey exist", async () => { - vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user_test_id" } }); - vi.mocked(getSurvey).mockResolvedValue(mockSurvey); - vi.mocked(getResponseCountBySurveyId).mockResolvedValue(mockResponseCount); - - const metadata = await generateMetadata({ - params: Promise.resolve({ surveyId: mockSurveyId, environmentId: mockEnvironmentId }), - }); - - expect(metadata).toEqual({ - title: `${mockResponseCount} Responses | ${mockSurveyName} Results`, - }); - expect(getServerSession).toHaveBeenCalledWith(authOptions); - expect(getSurvey).toHaveBeenCalledWith(mockSurveyId); - expect(getResponseCountBySurveyId).toHaveBeenCalledWith(mockSurveyId); - }); - - test("should return correct metadata when survey is null", async () => { - vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user_test_id" } }); - vi.mocked(getSurvey).mockResolvedValue(null); - vi.mocked(getResponseCountBySurveyId).mockResolvedValue(mockResponseCount); - - const metadata = await generateMetadata({ - params: Promise.resolve({ surveyId: mockSurveyId, environmentId: mockEnvironmentId }), - }); - - expect(metadata).toEqual({ - title: `${mockResponseCount} Responses | undefined Results`, - }); - }); - - test("should return empty title when session does not exist", async () => { - vi.mocked(getServerSession).mockResolvedValue(null); - vi.mocked(getSurvey).mockResolvedValue(mockSurvey); - vi.mocked(getResponseCountBySurveyId).mockResolvedValue(mockResponseCount); - - const metadata = await generateMetadata({ - params: Promise.resolve({ surveyId: mockSurveyId, environmentId: mockEnvironmentId }), - }); - - expect(metadata).toEqual({ - title: "", - }); - }); - }); - - describe("SurveyLayout Component", () => { - test("should render children", async () => { - const childText = "Test Child Component"; - render(await SurveyLayout({ children:
    {childText}
    })); - expect(screen.getByText(childText)).toBeInTheDocument(); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal.test.tsx deleted file mode 100644 index 11c7556280..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal.test.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TResponse } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TTag } from "@formbricks/types/tags"; -import { TUser, TUserLocale } from "@formbricks/types/user"; -import { ResponseCardModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal"; -import { SingleResponseCard } from "@/modules/analysis/components/SingleResponseCard"; - -vi.mock("@/modules/analysis/components/SingleResponseCard", () => ({ - SingleResponseCard: vi.fn(() =>
    SingleResponseCard
    ), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: vi.fn(({ children, onClick, disabled, variant, className }) => ( - - )), -})); - -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: vi.fn(({ children, open, onOpenChange }) => - open ? ( -
    - {children} - -
    - ) : null - ), - DialogContent: vi.fn(({ children, hideCloseButton, width, className }) => ( -
    - {children} -
    - )), - DialogBody: vi.fn(({ children }) =>
    {children}
    ), - DialogFooter: vi.fn(({ children }) =>
    {children}
    ), - DialogTitle: vi.fn(({ children }) =>
    {children}
    ), -})); - -vi.mock("@radix-ui/react-visually-hidden", () => ({ - VisuallyHidden: vi.fn(({ children }) =>
    {children}
    ), -})); - -const mockResponses = [ - { - id: "response1", - createdAt: new Date(), - updatedAt: new Date(), - surveyId: "survey1", - finished: true, - data: {}, - meta: { - userAgent: { browser: "Chrome", os: "Mac OS", device: "Desktop" }, - url: "http://localhost:3000", - }, - tags: [], - } as unknown as TResponse, - { - id: "response2", - createdAt: new Date(), - updatedAt: new Date(), - surveyId: "survey1", - finished: true, - data: {}, - meta: { - userAgent: { browser: "Firefox", os: "Windows", device: "Desktop" }, - url: "http://localhost:3000/page2", - }, - tags: [], - } as unknown as TResponse, - { - id: "response3", - createdAt: new Date(), - updatedAt: new Date(), - surveyId: "survey1", - finished: false, - data: {}, - meta: { - userAgent: { browser: "Safari", os: "iOS", device: "Mobile" }, - url: "http://localhost:3000/page3", - }, - tags: [], - } as unknown as TResponse, -] as unknown as TResponse[]; - -const mockSurvey = { - id: "survey1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Survey", - type: "app", - environmentId: "env1", - status: "inProgress", - questions: [], - hiddenFields: { enabled: false, fieldIds: [] }, - displayOption: "displayOnce", - recontactDays: 0, - autoClose: null, - delay: 0, - autoComplete: null, - surveyClosedMessage: null, - singleUse: null, - triggers: [], - languages: [], - displayPercentage: null, - welcomeCard: { enabled: false, headline: { default: "Welcome!" } } as unknown as TSurvey["welcomeCard"], - styling: null, -} as unknown as TSurvey; - -const mockEnvironment = { - id: "env1", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - appSetupCompleted: false, -} as unknown as TEnvironment; - -const mockUser = { - id: "user1", - name: "Test User", - email: "test@example.com", - emailVerified: new Date(), - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - role: "project_manager", - objective: "increase_conversion", - notificationSettings: { alert: {}, unsubscribedOrganizationIds: [] }, -} as unknown as TUser; - -const mockEnvironmentTags: TTag[] = [ - { id: "tag1", createdAt: new Date(), updatedAt: new Date(), name: "Tag 1", environmentId: "env1" }, -]; - -const mockLocale: TUserLocale = "en-US"; - -const mockSetSelectedResponseId = vi.fn(); -const mockUpdateResponse = vi.fn(); -const mockUpdateResponseList = vi.fn(); -const mockSetOpen = vi.fn(); - -const defaultProps = { - responses: mockResponses, - selectedResponseId: mockResponses[0].id, - setSelectedResponseId: mockSetSelectedResponseId, - survey: mockSurvey, - environment: mockEnvironment, - user: mockUser, - environmentTags: mockEnvironmentTags, - updateResponse: mockUpdateResponse, - updateResponseList: mockUpdateResponseList, - isReadOnly: false, - open: true, - setOpen: mockSetOpen, - locale: mockLocale, -}; - -describe("ResponseCardModal", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - test("should not render if selectedResponseId is null", () => { - const { container } = render(); - expect(container.firstChild).toBeNull(); - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); - - test("should render the dialog when a response is selected", () => { - render(); - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("single-response-card")).toBeInTheDocument(); - }); - - test("should call setSelectedResponseId with the next response id when next button is clicked", async () => { - render(); - const buttons = screen.getAllByTestId("mock-button"); - const nextButton = buttons.find((button) => button.querySelector("svg.lucide-chevron-right")); - if (nextButton) await userEvent.click(nextButton); - expect(mockSetSelectedResponseId).toHaveBeenCalledWith(mockResponses[1].id); - }); - - test("should call setSelectedResponseId with the previous response id when back button is clicked", async () => { - render(); - const buttons = screen.getAllByTestId("mock-button"); - const backButton = buttons.find((button) => button.querySelector("svg.lucide-chevron-left")); - if (backButton) await userEvent.click(backButton); - expect(mockSetSelectedResponseId).toHaveBeenCalledWith(mockResponses[0].id); - }); - - test("should disable back button if current response is the first one", () => { - render(); - const buttons = screen.getAllByTestId("mock-button"); - const backButton = buttons.find((button) => button.querySelector("svg.lucide-chevron-left")); - expect(backButton).toBeDisabled(); - }); - - test("should disable next button if current response is the last one", () => { - render( - - ); - const buttons = screen.getAllByTestId("mock-button"); - const nextButton = buttons.find((button) => button.querySelector("svg.lucide-chevron-right")); - expect(nextButton).toBeDisabled(); - }); - - test("useEffect should set open to true and currentIndex when selectedResponseId is provided", () => { - render(); - expect(mockSetOpen).toHaveBeenCalledWith(true); - // Current index is internal state, but we can check if the correct response is displayed - // by checking the props passed to SingleResponseCard - expect(vi.mocked(SingleResponseCard).mock.calls[0][0].response).toEqual(mockResponses[1]); - }); - - test("useEffect should set open to false when selectedResponseId is null after being open", () => { - const { rerender } = render( - - ); - expect(mockSetOpen).toHaveBeenCalledWith(true); - rerender(); - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - - test("should render ChevronLeft and ChevronRight icons", () => { - render(); - expect(document.querySelector(".lucide-chevron-left")).toBeInTheDocument(); - expect(document.querySelector(".lucide-chevron-right")).toBeInTheDocument(); - }); -}); - -// Mock Lucide icons for easier querying -vi.mock("lucide-react", async () => { - const actual = await vi.importActual("lucide-react"); - return { - ...actual, - ChevronLeft: vi.fn((props) => ), - ChevronRight: vi.fn((props) => ), - XIcon: vi.fn((props) => ), - }; -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView.test.tsx deleted file mode 100644 index a0c2d78293..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TResponse, TResponseDataValue } from "@formbricks/types/responses"; -import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { TTag } from "@formbricks/types/tags"; -import { TUser, TUserLocale } from "@formbricks/types/user"; -import { ResponseTable } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable"; -import { - ResponseDataView, - extractResponseData, - formatAddressData, - formatContactInfoData, - mapResponsesToTableData, -} from "./ResponseDataView"; - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable", - () => ({ - ResponseTable: vi.fn(() =>
    ResponseTable
    ), - }) -); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: vi.fn((key) => { - if (key === "environments.surveys.responses.completed") return "Completed"; - if (key === "environments.surveys.responses.not_completed") return "Not Completed"; - return key; - }), - }), -})); - -const mockSurvey = { - id: "survey1", - name: "Test Survey", - type: "app", - status: "inProgress", - questions: [ - { - id: "q1", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Question 1" }, - required: true, - } as unknown as TSurveyQuestion, - { - id: "q2", - type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: "Question 2" }, - required: false, - choices: [{ id: "c1", label: { default: "Choice 1" } }], - }, - { - id: "matrix1", - type: TSurveyQuestionTypeEnum.Matrix, - headline: { default: "Matrix Question" }, - required: false, - rows: [{ id: "row1", label: "Row 1" }], - columns: [{ id: "col1", label: "Col 1" }], - } as unknown as TSurveyQuestion, - { - id: "address1", - type: TSurveyQuestionTypeEnum.Address, - headline: { default: "Address Question" }, - required: false, - } as unknown as TSurveyQuestion, - { - id: "contactInfo1", - type: TSurveyQuestionTypeEnum.ContactInfo, - headline: { default: "Contact Info Question" }, - required: false, - } as unknown as TSurveyQuestion, - ], - hiddenFields: { enabled: true, fieldIds: ["hidden1"] }, - variables: [{ id: "var1", name: "Variable 1", type: "text", value: "default" }], - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "env1", - autoClose: null, - delay: 0, - displayOption: "displayOnce", - recontactDays: null, - welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"], - autoComplete: null, - singleUse: null, - styling: null, - surveyClosedMessage: null, - triggers: [], - languages: [], - displayPercentage: null, -} as unknown as TSurvey; - -const mockResponses: TResponse[] = [ - { - id: "response1", - createdAt: new Date(), - updatedAt: new Date(), - surveyId: "survey1", - finished: true, - data: { - q1: "Answer 1", - q2: "Choice 1", - matrix1: { row1: "Col 1" }, - address1: ["123 Main St", "Apt 4B", "Anytown", "CA", "90210", "USA"] as TResponseDataValue, - contactInfo1: [ - "John", - "Doe", - "john.doe@example.com", - "555-1234", - "Formbricks Inc.", - ] as TResponseDataValue, - hidden1: "Hidden Value 1", - verifiedEmail: "test@example.com", - }, - meta: { userAgent: { browser: "test-agent" }, url: "http://localhost" }, - singleUseId: null, - ttc: {}, - tags: [{ id: "tag1", name: "Tag1", environmentId: "env1", createdAt: new Date(), updatedAt: new Date() }], - variables: { var1: "Response Var Value" }, - language: "en", - contact: null, - contactAttributes: null, - }, - { - id: "response2", - createdAt: new Date(), - updatedAt: new Date(), - surveyId: "survey1", - finished: false, - data: { q1: "Answer 2" }, - meta: { userAgent: { browser: "test-agent-2" }, url: "http://localhost" }, - singleUseId: null, - ttc: {}, - tags: [], - variables: {}, - language: "de", - contact: null, - contactAttributes: null, - }, -]; - -const mockUser = { - id: "user1", - name: "Test User", - email: "test@example.com", - emailVerified: new Date(), - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - role: "project_manager", - objective: "other", -} as unknown as TUser; - -const mockEnvironment = { - id: "env1", - createdAt: new Date(), - updatedAt: new Date(), - type: "production", -} as unknown as TEnvironment; - -const mockEnvironmentTags: TTag[] = [ - { id: "tag1", name: "Tag1", environmentId: "env1", createdAt: new Date(), updatedAt: new Date() }, - { id: "tag2", name: "Tag2", environmentId: "env1", createdAt: new Date(), updatedAt: new Date() }, -]; - -const mockLocale: TUserLocale = "en-US"; - -const defaultProps = { - survey: mockSurvey, - responses: mockResponses, - user: mockUser, - environment: mockEnvironment, - environmentTags: mockEnvironmentTags, - isReadOnly: false, - fetchNextPage: vi.fn(), - hasMore: true, - updateResponseList: vi.fn(), - updateResponse: vi.fn(), - isFetchingFirstPage: false, - locale: mockLocale, -}; - -describe("ResponseDataView", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - test("renders ResponseTable with correct props", () => { - render(); - expect(screen.getByTestId("response-table")).toBeInTheDocument(); - - const responseTableMock = vi.mocked(ResponseTable); - expect(responseTableMock).toHaveBeenCalledTimes(1); - - const expectedData = [ - { - responseData: { - q1: "Answer 1", - q2: "Choice 1", - row1: "Col 1", // from matrix question - addressLine1: "123 Main St", - addressLine2: "Apt 4B", - city: "Anytown", - state: "CA", - zip: "90210", - country: "USA", - firstName: "John", - lastName: "Doe", - email: "john.doe@example.com", - phone: "555-1234", - company: "Formbricks Inc.", - hidden1: "Hidden Value 1", - }, - createdAt: mockResponses[0].createdAt, - status: "Completed", - responseId: "response1", - tags: mockResponses[0].tags, - variables: { var1: "Response Var Value" }, - verifiedEmail: "test@example.com", - language: "en", - person: null, - contactAttributes: null, - meta: { - url: "http://localhost", - userAgent: { - browser: "test-agent", - }, - }, - }, - { - responseData: { - q1: "Answer 2", - }, - createdAt: mockResponses[1].createdAt, - status: "Not Completed", - responseId: "response2", - tags: [], - variables: {}, - verifiedEmail: "", - language: "de", - person: null, - contactAttributes: null, - meta: { - url: "http://localhost", - userAgent: { - browser: "test-agent-2", - }, - }, - }, - ]; - - expect(responseTableMock.mock.calls[0][0].data).toEqual(expectedData); - expect(responseTableMock.mock.calls[0][0].survey).toEqual(mockSurvey); - expect(responseTableMock.mock.calls[0][0].responses).toEqual(mockResponses); - expect(responseTableMock.mock.calls[0][0].user).toEqual(mockUser); - expect(responseTableMock.mock.calls[0][0].environmentTags).toEqual(mockEnvironmentTags); - expect(responseTableMock.mock.calls[0][0].isReadOnly).toBe(false); - expect(responseTableMock.mock.calls[0][0].environment).toEqual(mockEnvironment); - expect(responseTableMock.mock.calls[0][0].fetchNextPage).toBe(defaultProps.fetchNextPage); - expect(responseTableMock.mock.calls[0][0].hasMore).toBe(true); - expect(responseTableMock.mock.calls[0][0].updateResponseList).toBe(defaultProps.updateResponseList); - expect(responseTableMock.mock.calls[0][0].updateResponse).toBe(defaultProps.updateResponse); - expect(responseTableMock.mock.calls[0][0].isFetchingFirstPage).toBe(false); - expect(responseTableMock.mock.calls[0][0].locale).toBe(mockLocale); - }); - - test("formatAddressData correctly formats data", () => { - const addressData: TResponseDataValue = ["1 Main St", "Apt 1", "CityA", "StateA", "10001", "CountryA"]; - const formatted = formatAddressData(addressData); - expect(formatted).toEqual({ - addressLine1: "1 Main St", - addressLine2: "Apt 1", - city: "CityA", - state: "StateA", - zip: "10001", - country: "CountryA", - }); - }); - - test("formatAddressData handles undefined values", () => { - const addressData: TResponseDataValue = ["1 Main St", "", "CityA", "", "10001", ""]; // Changed undefined to empty string as per function logic - const formatted = formatAddressData(addressData); - expect(formatted).toEqual({ - addressLine1: "1 Main St", - addressLine2: "", - city: "CityA", - state: "", - zip: "10001", - country: "", - }); - }); - - test("formatAddressData returns empty object for non-array input", () => { - const formatted = formatAddressData("not an array"); - expect(formatted).toEqual({}); - }); - - test("formatContactInfoData correctly formats data", () => { - const contactData: TResponseDataValue = ["Jane", "Doe", "jane@mail.com", "123-456", "Org B"]; - const formatted = formatContactInfoData(contactData); - expect(formatted).toEqual({ - firstName: "Jane", - lastName: "Doe", - email: "jane@mail.com", - phone: "123-456", - company: "Org B", - }); - }); - - test("formatContactInfoData handles undefined values", () => { - const contactData: TResponseDataValue = ["Jane", "", "jane@mail.com", "", "Org B"]; // Changed undefined to empty string - const formatted = formatContactInfoData(contactData); - expect(formatted).toEqual({ - firstName: "Jane", - lastName: "", - email: "jane@mail.com", - phone: "", - company: "Org B", - }); - }); - - test("formatContactInfoData returns empty object for non-array input", () => { - const formatted = formatContactInfoData({}); - expect(formatted).toEqual({}); - }); - - test("extractResponseData correctly extracts and formats data", () => { - const response = mockResponses[0]; - const survey = mockSurvey; - const extracted = extractResponseData(response, survey); - expect(extracted).toEqual({ - q1: "Answer 1", - q2: "Choice 1", - row1: "Col 1", // from matrix question - addressLine1: "123 Main St", - addressLine2: "Apt 4B", - city: "Anytown", - state: "CA", - zip: "90210", - country: "USA", - firstName: "John", - lastName: "Doe", - email: "john.doe@example.com", - phone: "555-1234", - company: "Formbricks Inc.", - hidden1: "Hidden Value 1", - }); - }); - - test("extractResponseData handles missing optional data", () => { - const response: TResponse = { - ...mockResponses[1], - data: { q1: "Answer 2" }, - }; - const survey = mockSurvey; - const extracted = extractResponseData(response, survey); - expect(extracted).toEqual({ - q1: "Answer 2", - // address and contactInfo will add empty strings if the keys exist but values are not arrays - // but here, the keys 'address1' and 'contactInfo1' are not in response.data - // hidden1 is also not in response.data - }); - }); - - test("mapResponsesToTableData correctly maps responses", () => { - const tMock = vi.fn((key) => (key === "environments.surveys.responses.completed" ? "Done" : "Pending")); - const tableData = mapResponsesToTableData(mockResponses, mockSurvey, tMock); - expect(tableData.length).toBe(2); - expect(tableData[0].status).toBe("Done"); - expect(tableData[1].status).toBe("Pending"); - expect(tableData[0].responseData.q1).toBe("Answer 1"); - expect(tableData[0].responseData.hidden1).toBe("Hidden Value 1"); - expect(tableData[0].variables.var1).toBe("Response Var Value"); - expect(tableData[1].responseData.q1).toBe("Answer 2"); - expect(tableData[0].verifiedEmail).toBe("test@example.com"); - expect(tableData[1].verifiedEmail).toBe(""); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.test.tsx deleted file mode 100644 index f485213821..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.test.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TResponse } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TTag } from "@formbricks/types/tags"; -import { TUser, TUserLocale } from "@formbricks/types/user"; -import { ResponseDataView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView"; -import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage"; - -vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({ - useResponseFilter: vi.fn(), -})); - -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions", () => ({ - getResponseCountAction: vi.fn(), - getResponsesAction: vi.fn(), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView", - () => ({ - ResponseDataView: vi.fn(() =>
    ResponseDataView
    ), - }) -); - -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter", () => ({ - CustomFilter: vi.fn(() =>
    CustomFilter
    ), -})); - -vi.mock("@/app/lib/surveys/surveys", () => ({ - getFormattedFilters: vi.fn(), -})); - -vi.mock("@/lib/utils/recall", () => ({ - replaceHeadlineRecall: vi.fn((survey) => survey), -})); - -vi.mock("next/navigation", () => ({ - useParams: vi.fn(), - useSearchParams: vi.fn(), - useRouter: vi.fn(), - usePathname: vi.fn(), -})); - -const mockUseResponseFilter = vi.mocked( - (await import("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext")) - .useResponseFilter -); -const mockGetResponsesAction = vi.mocked( - (await import("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions")) - .getResponsesAction -); -const mockGetResponseCountAction = vi.mocked( - (await import("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions")) - .getResponseCountAction -); -const mockUseParams = vi.mocked((await import("next/navigation")).useParams); -const mockUseSearchParams = vi.mocked((await import("next/navigation")).useSearchParams); -const mockGetFormattedFilters = vi.mocked((await import("@/app/lib/surveys/surveys")).getFormattedFilters); - -const mockSurvey = { - id: "survey1", - name: "Test Survey", - questions: [], - hiddenFields: { enabled: true, fieldIds: [] }, - displayOption: "displayOnce", - recontactDays: 0, - autoClose: null, - triggers: [], - type: "web", - status: "inProgress", - languages: [], - styling: null, -} as unknown as TSurvey; - -const mockEnvironment = { id: "env1", name: "Test Environment" } as unknown as TEnvironment; -const mockUser = { id: "user1", name: "Test User" } as TUser; -const mockTags: TTag[] = [{ id: "tag1", name: "Tag 1", environmentId: "env1" } as TTag]; -const mockLocale: TUserLocale = "en-US"; - -const defaultProps = { - environment: mockEnvironment, - survey: mockSurvey, - surveyId: "survey1", - webAppUrl: "http://localhost:3000", - user: mockUser, - environmentTags: mockTags, - responsesPerPage: 10, - locale: mockLocale, - isReadOnly: false, -}; - -const mockResponseFilterState = { - selectedFilter: "all", - dateRange: { from: undefined, to: undefined }, - resetState: vi.fn(), -} as any; - -const mockResponses: TResponse[] = [ - { - id: "response1", - createdAt: new Date(), - updatedAt: new Date(), - surveyId: "survey1", - finished: true, - data: {}, - meta: { userAgent: {} }, - tags: [], - } as unknown as TResponse, - { - id: "response2", - createdAt: new Date(), - updatedAt: new Date(), - surveyId: "survey1", - finished: true, - data: {}, - meta: { userAgent: {} }, - tags: [], - } as unknown as TResponse, -]; - -describe("ResponsePage", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - mockUseParams.mockReturnValue({ environmentId: "env1", surveyId: "survey1" }); - mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any); - mockUseResponseFilter.mockReturnValue(mockResponseFilterState); - mockGetResponsesAction.mockResolvedValue({ data: mockResponses }); - mockGetResponseCountAction.mockResolvedValue({ data: 20 }); - mockGetFormattedFilters.mockReturnValue({}); - }); - - test("renders correctly with default props", async () => { - render(); - await waitFor(() => { - expect(screen.getByTestId("custom-filter")).toBeInTheDocument(); - expect(screen.getByTestId("response-data-view")).toBeInTheDocument(); - }); - expect(mockGetResponsesAction).toHaveBeenCalled(); - }); - - test("fetches next page of responses", async () => { - const { rerender } = render(); - await waitFor(() => { - expect(mockGetResponsesAction).toHaveBeenCalledTimes(1); - }); - - // Simulate calling fetchNextPage (e.g., via ResponseDataView prop) - // For this test, we'll directly manipulate state to simulate the effect - // In a real scenario, this would be triggered by user interaction with ResponseDataView - const responseDataViewProps = vi.mocked(ResponseDataView).mock.calls[0][0]; - - await act(async () => { - await responseDataViewProps.fetchNextPage(); - }); - - rerender(); // Rerender to reflect state changes - - await waitFor(() => { - expect(mockGetResponsesAction).toHaveBeenCalledTimes(2); // Initial fetch + next page - expect(mockGetResponsesAction).toHaveBeenLastCalledWith( - expect.objectContaining({ - offset: defaultProps.responsesPerPage, // page 2 - }) - ); - }); - }); - - test("deletes responses and updates count", async () => { - render(); - await waitFor(() => { - expect(mockGetResponsesAction).toHaveBeenCalledTimes(1); - }); - - const responseDataViewProps = vi.mocked( - ( - await import( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView" - ) - ).ResponseDataView - ).mock.calls[0][0]; - - act(() => { - responseDataViewProps.updateResponseList(["response1"]); - }); - - // Check if ResponseDataView is re-rendered with updated responses - // This requires checking the props passed to ResponseDataView after deletion - // For simplicity, we assume the state update triggers a re-render and ResponseDataView receives new props - await waitFor(async () => { - const latestCallArgs = vi - .mocked( - ( - await import( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView" - ) - ).ResponseDataView - ) - .mock.calls.pop(); - if (latestCallArgs) { - expect(latestCallArgs[0].responses).toHaveLength(mockResponses.length - 1); - } - }); - }); - - test("updates a response", async () => { - render(); - await waitFor(() => { - expect(mockGetResponsesAction).toHaveBeenCalledTimes(1); - }); - - const responseDataViewProps = vi.mocked( - ( - await import( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView" - ) - ).ResponseDataView - ).mock.calls[0][0]; - - const updatedResponseData = { ...mockResponses[0], finished: false }; - act(() => { - responseDataViewProps.updateResponse("response1", updatedResponseData); - }); - - await waitFor(async () => { - const latestCallArgs = vi - .mocked( - ( - await import( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView" - ) - ).ResponseDataView - ) - .mock.calls.pop(); - if (latestCallArgs) { - const updatedResponseInView = latestCallArgs[0].responses.find((r) => r.id === "response1"); - expect(updatedResponseInView?.finished).toBe(false); - } - }); - }); - - test("resets pagination and responses when filters change", async () => { - const { rerender } = render(); - await waitFor(() => { - expect(mockGetResponsesAction).toHaveBeenCalledTimes(1); - }); - - // Simulate filter change - const newFilterState = { ...mockResponseFilterState, selectedFilter: "completed" }; - mockUseResponseFilter.mockReturnValue(newFilterState); - mockGetFormattedFilters.mockReturnValue({ someNewFilter: "value" } as any); // Simulate new formatted filters - - rerender(); - - await waitFor(() => { - // Should fetch responses again due to filter change - expect(mockGetResponsesAction).toHaveBeenCalledTimes(2); - // Check if it fetches with offset 0 (first page) - expect(mockGetResponsesAction).toHaveBeenLastCalledWith( - expect.objectContaining({ - offset: 0, - filterCriteria: { someNewFilter: "value" }, - }) - ); - }); - }); - - test("calls resetState when referer search param is not present", () => { - mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any); - render(); - expect(mockResponseFilterState.resetState).toHaveBeenCalled(); - }); - - test("does not call resetState when referer search param is present", () => { - mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue("someReferer") } as any); - render(); - expect(mockResponseFilterState.resetState).not.toHaveBeenCalled(); - }); - - test("handles empty responses from API", async () => { - mockGetResponsesAction.mockResolvedValue({ data: [] }); - mockGetResponseCountAction.mockResolvedValue({ data: 0 }); - render(); - await waitFor(async () => { - const latestCallArgs = vi - .mocked( - ( - await import( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView" - ) - ).ResponseDataView - ) - .mock.calls.pop(); - if (latestCallArgs) { - expect(latestCallArgs[0].responses).toEqual([]); - expect(latestCallArgs[0].hasMore).toBe(false); - } - }); - }); - - test("handles API errors gracefully for getResponsesAction", async () => { - mockGetResponsesAction.mockResolvedValue({ data: null as any }); - render(); - await waitFor(async () => { - const latestCallArgs = vi - .mocked( - ( - await import( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView" - ) - ).ResponseDataView - ) - .mock.calls.pop(); - if (latestCallArgs) { - expect(latestCallArgs[0].responses).toEqual([]); // Should default to empty array - expect(latestCallArgs[0].isFetchingFirstPage).toBe(false); - } - }); - }); - - test("handles API errors gracefully for getResponseCountAction", async () => { - mockGetResponseCountAction.mockResolvedValue({ data: null as any }); - render(); - // No direct visual change, but ensure no crash and component renders - await waitFor(() => { - expect(screen.getByTestId("response-data-view")).toBeInTheDocument(); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable.test.tsx deleted file mode 100644 index 78c10a2fe9..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable.test.tsx +++ /dev/null @@ -1,588 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TResponse } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TTag } from "@formbricks/types/tags"; -import { TUserLocale } from "@formbricks/types/user"; -import { ResponseTable } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable"; -import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; - -vi.mock("@sentry/nextjs", () => ({ captureException: vi.fn() })); - -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - default: { - error: vi.fn(), - success: vi.fn(), - dismiss: vi.fn(), - }, -})); - -// Mock components -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, ...props }: any) => ( - - ), -})); - -// Mock DndContext/SortableContext -vi.mock("@dnd-kit/core", () => ({ - DndContext: ({ children }: any) =>
    {children}
    , - useSensor: vi.fn(), - useSensors: vi.fn(() => "sensors"), - closestCenter: vi.fn(), - MouseSensor: vi.fn(), - TouchSensor: vi.fn(), - KeyboardSensor: vi.fn(), -})); - -vi.mock("@dnd-kit/modifiers", () => ({ - restrictToHorizontalAxis: "restrictToHorizontalAxis", -})); - -vi.mock("@dnd-kit/sortable", () => ({ - SortableContext: ({ children }: any) => <>{children}, - horizontalListSortingStrategy: "horizontalListSortingStrategy", - arrayMove: vi.fn((arr, oldIndex, newIndex) => { - const result = [...arr]; - const [removed] = result.splice(oldIndex, 1); - result.splice(newIndex, 0, removed); - return result; - }), -})); - -// Mock AutoAnimate -vi.mock("@formkit/auto-animate/react", () => ({ - useAutoAnimate: () => [vi.fn()], -})); - -// Mock UI components -vi.mock("@/modules/ui/components/data-table", () => ({ - DataTableHeader: ({ header }: any) => {header.id}, - DataTableSettingsModal: ({ open, setOpen }: any) => - open ? ( -
    - Settings Modal -
    - ) : null, - DataTableToolbar: ({ - table, - deleteRowsAction, - downloadRowsAction, - setIsTableSettingsModalOpen, - setIsExpanded, - isExpanded, - }: any) => ( -
    - - - - - -
    - ), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal", - () => ({ - ResponseCardModal: ({ open, setOpen }: any) => - open ? ( -
    - Response Modal -
    - ) : null, - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell", - () => ({ - ResponseTableCell: ({ cell, row, setSelectedResponseId }: any) => ( - setSelectedResponseId(row.id)}> - Cell Content - - ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns", - () => ({ - generateResponseTableColumns: vi.fn(() => [ - { id: "select", accessorKey: "select", header: "Select" }, - { id: "createdAt", accessorKey: "createdAt", header: "Created At" }, - { id: "person", accessorKey: "person", header: "Person" }, - { id: "status", accessorKey: "status", header: "Status" }, - ]), - }) -); - -vi.mock("@/modules/ui/components/table", () => ({ - Table: ({ children, ...props }: any) => {children}
    , - TableBody: ({ children, ...props }: any) => {children}, - TableCell: ({ children, ...props }: any) => {children}, - TableHeader: ({ children, ...props }: any) => {children}, - TableRow: ({ children, ...props }: any) => {children}, -})); - -vi.mock("@/modules/ui/components/skeleton", () => ({ - Skeleton: ({ children }: any) =>
    {children}
    , -})); - -// Mock the actions -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions", () => ({ - getResponsesDownloadUrlAction: vi.fn(), -})); - -// Mock handleFileUpload -vi.mock("@/modules/storage/file-upload", () => ({ - handleFileUpload: vi.fn(), -})); - -vi.mock("@/modules/analysis/components/SingleResponseCard/actions", () => ({ - deleteResponseAction: vi.fn(), -})); - -// Mock helper functions -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(), -})); - -// Mock localStorage -const mockLocalStorage = (() => { - let store: Record = {}; - return { - getItem: vi.fn((key) => store[key] || null), - setItem: vi.fn((key, value) => { - store[key] = String(value); - }), - clear: vi.fn(() => { - store = {}; - }), - removeItem: vi.fn((key) => { - delete store[key]; - }), - }; -})(); -Object.defineProperty(window, "localStorage", { value: mockLocalStorage }); - -// Mock Tolgee -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -// Global mock anchor for tests -let globalMockAnchor: any; - -// Define mock data for tests -const mockProps = { - data: [ - { responseId: "resp1", createdAt: new Date().toISOString(), status: "completed", person: "Person 1" }, - { responseId: "resp2", createdAt: new Date().toISOString(), status: "completed", person: "Person 2" }, - ] as any[], - survey: { - id: "survey1", - createdAt: new Date(), - updatedAt: new Date(), - name: "name", - type: "link", - environmentId: "env-1", - createdBy: null, - status: "draft", - } as TSurvey, - responses: [ - { id: "resp1", surveyId: "survey1", data: {}, createdAt: new Date(), updatedAt: new Date() }, - { id: "resp2", surveyId: "survey1", data: {}, createdAt: new Date(), updatedAt: new Date() }, - ] as TResponse[], - environment: { id: "env1" } as TEnvironment, - environmentTags: [] as TTag[], - isReadOnly: false, - fetchNextPage: vi.fn(), - hasMore: false, - deleteResponses: vi.fn(), - updateResponse: vi.fn(), - isFetchingFirstPage: false, - locale: "en" as TUserLocale, -}; - -// Setup a container for React Testing Library before each test -beforeEach(() => { - const container = document.createElement("div"); - container.id = "test-container"; - document.body.appendChild(container); - - // Reset all toast mocks before each test - vi.mocked(toast.error).mockClear(); - vi.mocked(toast.success).mockClear(); - vi.mocked(getResponsesDownloadUrlAction).mockClear(); - - // Create a mock anchor element for download tests - globalMockAnchor = { - href: "", - click: vi.fn(), - style: {}, - download: "", - }; - - // Override the href setter to capture when it's set - Object.defineProperty(globalMockAnchor, "href", { - get() { - return this._href || ""; - }, - set(value) { - this._href = value; - }, - }); - - // Update how we mock the document methods to avoid infinite recursion - const originalCreateElement = document.createElement.bind(document); - vi.spyOn(document, "createElement").mockImplementation((tagName) => { - if (tagName === "a") return globalMockAnchor as any; - return originalCreateElement(tagName); - }); - - vi.spyOn(document.body, "appendChild").mockReturnValue(null as any); - vi.spyOn(document.body, "removeChild").mockReturnValue(null as any); - - // Mock File constructor to avoid arrayBuffer issues - vi.stubGlobal( - "File", - class MockFile { - name: string; - type: string; - size: number; - - constructor(_chunks: any[], name: string, options: any = {}) { - this.name = name; - this.type = options.type || ""; - this.size = options.size || 0; - } - - arrayBuffer() { - return Promise.resolve(new ArrayBuffer(0)); - } - } as any - ); - - // Mock atob for base64 decoding - vi.stubGlobal( - "atob", - vi.fn((_str: string) => "decoded binary string") - ); - - // Mock Uint8Array and Blob - vi.stubGlobal( - "Uint8Array", - class MockUint8Array extends Array { - constructor(data: any) { - super(); - this.length = typeof data === "number" ? data : 0; - } - - static from(source: any) { - return new MockUint8Array(source.length || 0); - } - } as any - ); - - vi.stubGlobal( - "Blob", - class MockBlob { - size: number; - type: string; - - constructor(_parts: any[], options: any = {}) { - this.size = 0; - this.type = options.type || ""; - } - } as any - ); - - vi.stubGlobal("URL", { - createObjectURL: vi.fn(), - revokeObjectURL: vi.fn(), - }); -}); - -// Cleanup after each test -afterEach(() => { - const container = document.getElementById("test-container"); - if (container) { - document.body.removeChild(container); - } - cleanup(); - vi.restoreAllMocks(); // Restore mocks after each test - vi.unstubAllGlobals(); // Restore global stubs after each test -}); - -describe("ResponseTable", () => { - afterEach(() => { - cleanup(); // Keep cleanup within describe as per instructions - }); - - test("renders the table with data", () => { - const container = document.getElementById("test-container"); - render(, { container: container! }); - expect(screen.getByRole("table")).toBeInTheDocument(); - expect(screen.getByTestId("table-toolbar")).toBeInTheDocument(); - }); - - test("renders no results message when data is empty", () => { - const container = document.getElementById("test-container"); - render(, { container: container! }); - expect(screen.getByText("common.no_results")).toBeInTheDocument(); - }); - - test("renders load more button when hasMore is true", () => { - const container = document.getElementById("test-container"); - render(, { container: container! }); - expect(screen.getByText("common.load_more")).toBeInTheDocument(); - }); - - test("calls fetchNextPage when load more button is clicked", async () => { - const container = document.getElementById("test-container"); - render(, { container: container! }); - const loadMoreButton = screen.getByText("common.load_more"); - await userEvent.click(loadMoreButton); - expect(mockProps.fetchNextPage).toHaveBeenCalledTimes(1); - }); - - test("opens settings modal when toolbar button is clicked", async () => { - const container = document.getElementById("test-container"); - render(, { container: container! }); - const openSettingsButton = screen.getByTestId("open-settings"); - await userEvent.click(openSettingsButton); - expect(screen.getByTestId("settings-modal")).toBeInTheDocument(); - }); - - test("toggles expanded state when toolbar button is clicked", async () => { - const container = document.getElementById("test-container"); - render(, { container: container! }); - const toggleExpandButton = screen.getByTestId("toggle-expand"); - - // Initially might be null, first click should set it to true - await userEvent.click(toggleExpandButton); - expect(mockLocalStorage.setItem).toHaveBeenCalledWith("survey1-rowExpand", expect.any(String)); - }); - - test("calls downloadSelectedRows with csv format when toolbar button is clicked", async () => { - vi.mocked(getResponsesDownloadUrlAction).mockResolvedValueOnce({ - data: { - fileContents: "mock,csv,content", - fileName: "survey-responses.csv", - }, - }); - - const container = document.getElementById("test-container"); - render(, { container: container! }); - // Ensure URL.createObjectURL returns a deterministic URL for assertions - (URL.createObjectURL as any).mockReturnValueOnce("https://download.url/file.csv"); - const downloadCsvButton = screen.getByTestId("download-csv"); - await userEvent.click(downloadCsvButton); - - await waitFor(() => { - expect(getResponsesDownloadUrlAction).toHaveBeenCalledWith({ - surveyId: "survey1", - format: "csv", - filterCriteria: { responseIds: [] }, - }); - - // Check if link was created and clicked - expect(document.createElement).toHaveBeenCalledWith("a"); - expect(globalMockAnchor.href).toBe("https://download.url/file.csv"); - expect(document.body.appendChild).toHaveBeenCalled(); - expect(globalMockAnchor.click).toHaveBeenCalled(); - expect(document.body.removeChild).toHaveBeenCalled(); - }); - }); - - test("calls downloadSelectedRows with xlsx format when toolbar button is clicked", async () => { - vi.mocked(getResponsesDownloadUrlAction).mockResolvedValueOnce({ - data: { - fileContents: "bW9jayB4bHN4IGNvbnRlbnQ=", // base64 encoded mock data - fileName: "survey-responses.xlsx", - }, - }); - - const container = document.getElementById("test-container"); - render(, { container: container! }); - // Ensure URL.createObjectURL returns a deterministic URL for assertions - (URL.createObjectURL as any).mockReturnValueOnce("https://download.url/file.xlsx"); - const downloadXlsxButton = screen.getByTestId("download-xlsx"); - await userEvent.click(downloadXlsxButton); - - await waitFor(() => { - expect(getResponsesDownloadUrlAction).toHaveBeenCalledWith({ - surveyId: "survey1", - format: "xlsx", - filterCriteria: { responseIds: [] }, - }); - - // Check if link was created and clicked - expect(document.createElement).toHaveBeenCalledWith("a"); - expect(globalMockAnchor.href).toBe("https://download.url/file.xlsx"); - expect(document.body.appendChild).toHaveBeenCalled(); - expect(globalMockAnchor.click).toHaveBeenCalled(); - expect(document.body.removeChild).toHaveBeenCalled(); - }); - }); - - // Test response modal - test("opens and closes response modal when a cell is clicked", async () => { - const container = document.getElementById("test-container"); - render(, { container: container! }); - const cell = screen.getByTestId("cell-resp1_select-resp1"); - await userEvent.click(cell); - expect(screen.getByTestId("response-modal")).toBeInTheDocument(); - // Close the modal - const closeButton = screen.getByText("Close"); - await userEvent.click(closeButton); - - // Modal should be closed now - expect(screen.queryByTestId("response-modal")).not.toBeInTheDocument(); - }); - - test("shows error toast when download action returns error", async () => { - const errorMsg = "Download failed"; - vi.mocked(getResponsesDownloadUrlAction).mockResolvedValueOnce({ - data: undefined, - serverError: errorMsg, - }); - vi.mocked(getFormattedErrorMessage).mockReturnValueOnce(errorMsg); - - // Reset document.createElement spy to fix the last test - vi.mocked(document.createElement).mockClear(); - - const container = document.getElementById("test-container"); - render(, { container: container! }); - const downloadCsvButton = screen.getByTestId("download-csv"); - await userEvent.click(downloadCsvButton); - - await waitFor(() => { - expect(getResponsesDownloadUrlAction).toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalledWith("environments.surveys.responses.error_downloading_responses"); - }); - }); - - test("shows default error toast when download action returns no data", async () => { - vi.mocked(getResponsesDownloadUrlAction).mockResolvedValueOnce({ - data: undefined, - }); - vi.mocked(getFormattedErrorMessage).mockReturnValueOnce(""); - - const container = document.getElementById("test-container"); - render(, { container: container! }); - const downloadCsvButton = screen.getByTestId("download-csv"); - await userEvent.click(downloadCsvButton); - - await waitFor(() => { - expect(getResponsesDownloadUrlAction).toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalledWith("environments.surveys.responses.error_downloading_responses"); - }); - }); - - test("shows error toast when download action throws exception", async () => { - vi.mocked(getResponsesDownloadUrlAction).mockRejectedValueOnce(new Error("Network error")); - - const container = document.getElementById("test-container"); - render(, { container: container! }); - const downloadCsvButton = screen.getByTestId("download-csv"); - await userEvent.click(downloadCsvButton); - - await waitFor(() => { - expect(getResponsesDownloadUrlAction).toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalledWith("environments.surveys.responses.error_downloading_responses"); - }); - }); - - test("does not create download link when download action fails", async () => { - // Clear any previous calls to document.createElement - vi.mocked(document.createElement).mockClear(); - - vi.mocked(getResponsesDownloadUrlAction).mockResolvedValueOnce({ - data: undefined, - serverError: "Download failed", - }); - - // Create a fresh spy for createElement for this test only - const createElementSpy = vi.spyOn(document, "createElement"); - - const container = document.getElementById("test-container"); - render(, { container: container! }); - const downloadCsvButton = screen.getByTestId("download-csv"); - await userEvent.click(downloadCsvButton); - - await waitFor(() => { - expect(getResponsesDownloadUrlAction).toHaveBeenCalled(); - // Check specifically for "a" element creation, not any element - expect(createElementSpy).not.toHaveBeenCalledWith("a"); - }); - }); - - test("loads saved settings from localStorage on mount", () => { - const columnOrder = ["status", "person", "createdAt", "select"]; - const columnVisibility = { status: false }; - const isExpanded = true; - - mockLocalStorage.getItem.mockImplementation((key) => { - if (key === "survey1-columnOrder") return JSON.stringify(columnOrder); - if (key === "survey1-columnVisibility") return JSON.stringify(columnVisibility); - if (key === "survey1-rowExpand") return JSON.stringify(isExpanded); - return null; - }); - - const container = document.getElementById("test-container"); - render(, { container: container! }); - - // Verify localStorage calls - expect(mockLocalStorage.getItem).toHaveBeenCalledWith("survey1-columnOrder"); - expect(mockLocalStorage.getItem).toHaveBeenCalledWith("survey1-columnVisibility"); - expect(mockLocalStorage.getItem).toHaveBeenCalledWith("survey1-rowExpand"); - - // The mock for generateResponseTableColumns returns this order: - // ["select", "createdAt", "person", "status"] - // Only visible columns should be rendered, in this order - const expectedHeaders = ["select", "createdAt", "person"]; - const headers = screen.getAllByTestId(/^header-/); - expect(headers).toHaveLength(expectedHeaders.length); - expectedHeaders.forEach((columnId, index) => { - expect(headers[index]).toHaveAttribute("data-testid", `header-${columnId}`); - }); - - // Verify column visibility is applied - const statusHeader = screen.queryByTestId("header-status"); - expect(statusHeader).not.toBeInTheDocument(); - - // Verify row expansion is applied - const toggleExpandButton = screen.getByTestId("toggle-expand"); - expect(toggleExpandButton).toHaveAttribute("aria-pressed", "true"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.test.tsx deleted file mode 100644 index 77ce5f41ca..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.test.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import type { Cell, Row } from "@tanstack/react-table"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import type { TResponse, TResponseTableData } from "@formbricks/types/responses"; -import { ResponseTableCell } from "./ResponseTableCell"; - -const makeCell = ( - id: string, - size = 100, - first = false, - last = false, - content = "CellContent" -): Cell => - ({ - column: { - id, - getSize: () => size, - getIsFirstColumn: () => first, - getIsLastColumn: () => last, - getStart: () => 0, - columnDef: { cell: () => content }, - }, - id, - getContext: () => ({}), - }) as unknown as Cell; - -const makeRow = (id: string, selected = false): Row => - ({ id, getIsSelected: () => selected }) as unknown as Row; - -describe("ResponseTableCell", () => { - afterEach(() => { - cleanup(); - }); - - test("renders cell content", () => { - const cell = makeCell("col1"); - const row = makeRow("r1"); - render( - - ); - expect(screen.getByText("CellContent")).toBeDefined(); - }); - - test("calls setSelectedResponseId on cell click when not select column", async () => { - const cell = makeCell("col1"); - const row = makeRow("r1"); - const setSel = vi.fn(); - render( - - ); - await userEvent.click(screen.getByText("CellContent")); - expect(setSel).toHaveBeenCalledWith("r1"); - }); - - test("does not call setSelectedResponseId on select column click", async () => { - const cell = makeCell("select"); - const row = makeRow("r1"); - const setSel = vi.fn(); - render( - - ); - await userEvent.click(screen.getByText("CellContent")); - expect(setSel).not.toHaveBeenCalled(); - }); - - test("renders maximize icon for createdAt column and handles click", async () => { - const cell = makeCell("createdAt", 120, false, false); - const row = makeRow("r2"); - const setSel = vi.fn(); - render( - - ); - const btn = screen.getByRole("button", { name: /expand response/i }); - expect(btn).toBeDefined(); - await userEvent.click(btn); - expect(setSel).toHaveBeenCalledWith("r2"); - }); - - test("does not apply selected style when row.getIsSelected() is false", () => { - const cell = makeCell("col1"); - const row = makeRow("r1", false); - const { container } = render( - - ); - expect(container.firstChild).not.toHaveClass("bg-slate-100"); - }); - - test("applies selected style when row.getIsSelected() is true", () => { - const cell = makeCell("col1"); - const row = makeRow("r1", true); - const { container } = render( - - ); - expect(container.firstChild).toHaveClass("bg-slate-100"); - }); - - test("renders collapsed height class when isExpanded is false", () => { - const cell = makeCell("col1"); - const row = makeRow("r1"); - const { container } = render( - - ); - const inner = container.querySelector("div > div"); - expect(inner).toHaveClass("h-10"); - }); - - test("renders expanded height class when isExpanded is true", () => { - const cell = makeCell("col1"); - const row = makeRow("r1"); - const { container } = render( - - ); - const inner = container.querySelector("div > div"); - expect(inner).toHaveClass("h-full"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.test.tsx deleted file mode 100644 index 1e12346b22..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.test.tsx +++ /dev/null @@ -1,795 +0,0 @@ -import { cleanup } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TResponseTableData } from "@formbricks/types/responses"; -import { - TSurvey, - TSurveyQuestion, - TSurveyQuestionTypeEnum, - TSurveyVariable, -} from "@formbricks/types/surveys/types"; -import { TTag } from "@formbricks/types/tags"; -import { extractChoiceIdsFromResponse } from "@/lib/response/utils"; -import { getContactIdentifier } from "@/lib/utils/contact"; -import { getFormattedDateTimeString } from "@/lib/utils/datetime"; -import { getSelectionColumn } from "@/modules/ui/components/data-table"; -import { generateResponseTableColumns } from "./ResponseTableColumns"; - -// Mock TFnType -const t = vi.fn((key: string, params?: any) => { - if (params) { - let message = key; - for (const p in params) { - message = message.replace(`{{${p}}}`, params[p]); - } - return message; - } - return key; -}); - -vi.mock("@/lib/i18n/utils", () => ({ - getLocalizedValue: vi.fn((localizedString, locale) => localizedString[locale] || localizedString.default), -})); - -vi.mock("@/lib/utils/contact", () => ({ - getContactIdentifier: vi.fn((person) => person?.attributes?.email || person?.id || "Anonymous"), -})); - -vi.mock("@/lib/utils/datetime", () => ({ - getFormattedDateTimeString: vi.fn((date) => new Date(date).toISOString()), -})); - -vi.mock("@/lib/utils/recall", () => ({ - recallToHeadline: vi.fn((headline) => headline), -})); - -vi.mock("@/modules/analysis/components/SingleResponseCard/components/RenderResponse", () => ({ - RenderResponse: vi.fn(({ responseData, isExpanded }) => ( -
    - RenderResponse: {JSON.stringify(responseData)} (Expanded: {String(isExpanded)}) -
    - )), -})); - -vi.mock("@/modules/survey/lib/questions", () => ({ - getQuestionIconMap: vi.fn(() => ({ - [TSurveyQuestionTypeEnum.OpenText]: OT, - [TSurveyQuestionTypeEnum.MultipleChoiceSingle]: MCS, - [TSurveyQuestionTypeEnum.MultipleChoiceMulti]: MCM, - [TSurveyQuestionTypeEnum.Matrix]: MX, - [TSurveyQuestionTypeEnum.Address]: AD, - [TSurveyQuestionTypeEnum.ContactInfo]: CI, - })), - VARIABLES_ICON_MAP: { - text: VarT, - number: VarN, - }, -})); - -vi.mock("@/modules/ui/components/data-table", () => ({ - getSelectionColumn: vi.fn(() => ({ - id: "select", - header: "Select", - cell: "SelectCell", - })), -})); - -vi.mock("@/modules/ui/components/response-badges", () => ({ - ResponseBadges: vi.fn(({ items, isExpanded }) => ( -
    - Badges: {items.join(", ")} (Expanded: {String(isExpanded)}) -
    - )), -})); - -vi.mock("@/modules/ui/components/tooltip", () => ({ - Tooltip: ({ children }) =>
    {children}
    , - TooltipContent: ({ children }) =>
    {children}
    , - TooltipProvider: ({ children }) =>
    {children}
    , - TooltipTrigger: ({ children }) =>
    {children}
    , -})); - -vi.mock("next/link", () => ({ - default: ({ children, href }) => {children}, -})); - -vi.mock("lucide-react", () => ({ - CircleHelpIcon: () => Help, - EyeOffIcon: () => EyeOff, - MailIcon: () => Mail, - TagIcon: () => Tag, - MousePointerClickIcon: () => MousePointerClick, - AirplayIcon: () => Airplay, - ArrowUpFromDotIcon: () => ArrowUpFromDot, - FlagIcon: () => Flag, - GlobeIcon: () => Globe, - SmartphoneIcon: () => Smartphone, -})); - -// Mock new dependencies -vi.mock("@/lib/response/utils", () => ({ - extractChoiceIdsFromResponse: vi.fn((responseValue) => { - // Mock implementation that returns choice IDs based on response value - if (Array.isArray(responseValue)) { - return responseValue.map((_, index) => `choice-${index + 1}`); - } else if (typeof responseValue === "string") { - return [`choice-single`]; - } - return []; - }), -})); - -vi.mock("@/modules/ui/components/id-badge", () => ({ - IdBadge: vi.fn(({ id }) =>
    {id}
    ), -})); - -vi.mock("@/modules/ui/lib/utils", () => ({ - cn: vi.fn((...classes) => classes.filter(Boolean).join(" ")), -})); - -const mockSurvey = { - id: "survey1", - name: "Test Survey", - type: "app", - status: "inProgress", - questions: [ - { - id: "q1open", - type: "openText", - headline: { default: "Open Text Question" }, - required: true, - } as unknown as TSurveyQuestion, - { - id: "q2matrix", - type: "matrix", - headline: { default: "Matrix Question" }, - rows: [ - { id: "row-1", label: { default: "Row1" } }, - { id: "row-2", label: { default: "Row2" } }, - ], - columns: [ - { id: "col-1", label: { default: "Col1" } }, - { id: "col-2", label: { default: "Col2" } }, - ], - required: false, - } as unknown as TSurveyQuestion, - { - id: "q3address", - type: "address", - headline: { default: "Address Question" }, - required: false, - } as unknown as TSurveyQuestion, - { - id: "q4contact", - type: "contactInfo", - headline: { default: "Contact Info Question" }, - required: false, - } as unknown as TSurveyQuestion, - { - id: "q5single", - type: "multipleChoiceSingle", - headline: { default: "Single Choice Question" }, - required: false, - choices: [ - { id: "choice-1", label: { default: "Option 1" } }, - { id: "choice-2", label: { default: "Option 2" } }, - { id: "choice-3", label: { default: "Option 3" } }, - ], - } as unknown as TSurveyQuestion, - { - id: "q6multi", - type: "multipleChoiceMulti", - headline: { default: "Multi Choice Question" }, - required: false, - choices: [ - { id: "choice-a", label: { default: "Choice A" } }, - { id: "choice-b", label: { default: "Choice B" } }, - { id: "choice-c", label: { default: "Choice C" } }, - ], - } as unknown as TSurveyQuestion, - ], - variables: [ - { id: "var1", name: "User Segment", type: "text" } as TSurveyVariable, - { id: "var2", name: "Total Spend", type: "number" } as TSurveyVariable, - ], - hiddenFields: { enabled: true, fieldIds: ["hf1", "hf2"] }, - endings: [], - triggers: [], - recontactDays: null, - displayOption: "displayOnce", - autoClose: null, - delay: 0, - autoComplete: null, - isVerifyEmailEnabled: false, - styling: null, - languages: [], - segment: null, - projectOverwrites: null, - singleUse: null, - pin: null, - surveyClosedMessage: null, - welcomeCard: { - enabled: false, - } as TSurvey["welcomeCard"], -} as unknown as TSurvey; - -const mockResponseData = { - contactAttributes: { country: "USA" }, - responseData: { - q1open: "Open text answer", - Row1: "Col1", // For matrix q2matrix - Row2: "Col2", - addressLine1: "123 Main St", - city: "Anytown", - firstName: "John", - email: "john.doe@example.com", - hf1: "Hidden Field 1 Value", - q5single: "Option 1", // Single choice response - q6multi: ["Choice A", "Choice C"], // Multi choice response - }, - variables: { - var1: "Segment A", - var2: 100, - }, - status: "completed", - tags: [{ id: "tag1", name: "Important" } as unknown as TTag], - language: "default", -} as unknown as TResponseTableData; - -describe("generateResponseTableColumns", () => { - beforeEach(() => { - vi.clearAllMocks(); - t.mockImplementation((key: string) => key); // Reset t mock for each test - }); - - afterEach(() => { - cleanup(); - }); - - test("should include selection column when not read-only", () => { - const columns = generateResponseTableColumns(mockSurvey, false, false, t as any, false); - expect(columns[0].id).toBe("select"); - expect(vi.mocked(getSelectionColumn)).toHaveBeenCalledTimes(1); - }); - - test("should not include selection column when read-only", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - expect(columns[0].id).not.toBe("select"); - expect(vi.mocked(getSelectionColumn)).not.toHaveBeenCalled(); - }); - - test("should include Verified Email column when survey.isVerifyEmailEnabled is true", () => { - const surveyWithVerifiedEmail = { ...mockSurvey, isVerifyEmailEnabled: true }; - const columns = generateResponseTableColumns(surveyWithVerifiedEmail, false, true, t as any, false); - expect(columns.some((col) => (col as any).accessorKey === "verifiedEmail")).toBe(true); - }); - - test("should not include Verified Email column when survey.isVerifyEmailEnabled is false", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - expect(columns.some((col) => (col as any).accessorKey === "verifiedEmail")).toBe(false); - }); - - test("should generate columns for variables", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const var1Col = columns.find((col) => (col as any).accessorKey === "VARIABLE_var1"); - expect(var1Col).toBeDefined(); - const var1Cell = (var1Col?.cell as any)?.({ row: { original: mockResponseData } } as any); - expect(var1Cell.props.children).toBe("Segment A"); - - const var2Col = columns.find((col) => (col as any).accessorKey === "VARIABLE_var2"); - expect(var2Col).toBeDefined(); - const var2Cell = (var2Col?.cell as any)?.({ row: { original: mockResponseData } } as any); - expect(var2Cell.props.children).toBe(100); - }); - - test("should generate columns for hidden fields if fieldIds exist", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const hf1Col = columns.find((col) => (col as any).accessorKey === "HIDDEN_FIELD_hf1"); - expect(hf1Col).toBeDefined(); - const hf1Cell = (hf1Col?.cell as any)?.({ row: { original: mockResponseData } } as any); - expect(hf1Cell.props.children).toBe("Hidden Field 1 Value"); - }); - - test("should not generate columns for hidden fields if fieldIds is undefined", () => { - const surveyWithoutHiddenFieldIds = { ...mockSurvey, hiddenFields: { enabled: true } }; - const columns = generateResponseTableColumns(surveyWithoutHiddenFieldIds, false, true, t as any, false); - const hf1Col = columns.find((col) => (col as any).accessorKey === "hf1"); - expect(hf1Col).toBeUndefined(); - }); -}); - -describe("ResponseTableColumns", () => { - afterEach(() => { - cleanup(); - }); - - test("includes verifiedEmailColumn when isVerifyEmailEnabled is true", () => { - // Arrange - const mockSurvey = { - questions: [], - variables: [], - hiddenFields: { fieldIds: [] }, - isVerifyEmailEnabled: true, - } as unknown as TSurvey; - - const mockT = vi.fn((key) => key); - const isExpanded = false; - const isReadOnly = false; - - // Act - const columns = generateResponseTableColumns(mockSurvey, isExpanded, isReadOnly, mockT, false); - - // Assert - const verifiedEmailColumn: any = columns.find((col: any) => col.accessorKey === "verifiedEmail"); - expect(verifiedEmailColumn).toBeDefined(); - expect(verifiedEmailColumn?.accessorKey).toBe("verifiedEmail"); - - // Call the header function to trigger the t function call with "common.verified_email" - if (verifiedEmailColumn && typeof verifiedEmailColumn.header === "function") { - verifiedEmailColumn.header(); - expect(mockT).toHaveBeenCalledWith("common.verified_email"); - } - }); - - test("excludes verifiedEmailColumn when isVerifyEmailEnabled is false", () => { - // Arrange - const mockSurvey = { - questions: [], - variables: [], - hiddenFields: { fieldIds: [] }, - isVerifyEmailEnabled: false, - } as unknown as TSurvey; - - const mockT = vi.fn((key) => key); - const isExpanded = false; - const isReadOnly = false; - - // Act - const columns = generateResponseTableColumns(mockSurvey, isExpanded, isReadOnly, mockT, false); - - // Assert - const verifiedEmailColumn = columns.find((col: any) => col.accessorKey === "verifiedEmail"); - expect(verifiedEmailColumn).toBeUndefined(); - }); -}); - -describe("ResponseTableColumns - Column Implementations", () => { - afterEach(() => { - cleanup(); - }); - - test("dateColumn renders with formatted date", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const dateColumn: any = columns.find((col) => (col as any).accessorKey === "createdAt"); - expect(dateColumn).toBeDefined(); - - // Call the header function to test it returns the expected value - expect(dateColumn?.header?.()).toBe("common.date"); - - // Mock a response with a date to test the cell function - const mockRow = { - original: { createdAt: "2023-01-01T12:00:00Z" }, - } as any; - - // Call the cell function and check the formatted date - dateColumn?.cell?.({ row: mockRow } as any); - expect(vi.mocked(getFormattedDateTimeString)).toHaveBeenCalledWith(new Date("2023-01-01T12:00:00Z")); - }); - - test("personColumn renders anonymous when person is null", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const personColumn: any = columns.find((col) => (col as any).accessorKey === "personId"); - expect(personColumn).toBeDefined(); - - // Test header content - const headerResult = personColumn?.header?.(); - expect(headerResult).toBeDefined(); - - // Mock a response with no person - const mockRow = { - original: { person: null }, - } as any; - - // Mock the t function for this specific call - t.mockReturnValueOnce("Anonymous User"); - - // Call the cell function and check it returns "Anonymous" - const cellResult = personColumn?.cell?.({ row: mockRow } as any); - expect(t).toHaveBeenCalledWith("common.anonymous"); - expect(cellResult?.props?.children).toBe("Anonymous User"); - }); - - test("personColumn renders person identifier when person exists", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const personColumn: any = columns.find((col) => (col as any).accessorKey === "personId"); - expect(personColumn).toBeDefined(); - - // Mock a response with a person - const mockRow = { - original: { - person: { id: "123", attributes: { email: "test@example.com" } }, - contactAttributes: { name: "John Doe" }, - }, - } as any; - - // Call the cell function - personColumn?.cell?.({ row: mockRow } as any); - expect(vi.mocked(getContactIdentifier)).toHaveBeenCalledWith( - mockRow.original.person, - mockRow.original.contactAttributes - ); - }); - - test("tagsColumn returns undefined when tags is not an array", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const tagsColumn: any = columns.find((col) => (col as any).accessorKey === "tags"); - expect(tagsColumn).toBeDefined(); - - // Mock a response with no tags - const mockRow = { - original: { tags: null }, - } as any; - - // Call the cell function - const cellResult = tagsColumn?.cell?.({ row: mockRow } as any); - expect(cellResult).toBeUndefined(); - }); - - test("variableColumns render variable values correctly", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - - // Find the variable column for var1 - const var1Column: any = columns.find((col) => (col as any).accessorKey === "VARIABLE_var1"); - expect(var1Column).toBeDefined(); - - // Test the header - const headerResult = var1Column?.header?.(); - expect(headerResult).toBeDefined(); - - // Mock a response with a string variable - const mockRow = { - original: { variables: { var1: "Test Value" } }, - } as any; - - // Call the cell function - const cellResult = var1Column?.cell?.({ row: mockRow } as any); - expect(cellResult?.props.children).toBe("Test Value"); - - // Test with a number variable - const var2Column: any = columns.find((col) => (col as any).accessorKey === "VARIABLE_var2"); - expect(var2Column).toBeDefined(); - - const mockRowNumber = { - original: { variables: { var2: 42 } }, - } as any; - - const cellResultNumber = var2Column?.cell?.({ row: mockRowNumber } as any); - expect(cellResultNumber?.props.children).toBe(42); - }); - - test("hiddenFieldColumns render when fieldIds exist", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - - // Find the hidden field column - const hfColumn: any = columns.find((col) => (col as any).accessorKey === "HIDDEN_FIELD_hf1"); - expect(hfColumn).toBeDefined(); - - // Test the header - const headerResult = hfColumn?.header?.(); - expect(headerResult).toBeDefined(); - - // Mock a response with a hidden field value - const mockRow = { - original: { responseData: { hf1: "Hidden Value" } }, - } as any; - - // Call the cell function - const cellResult = hfColumn?.cell?.({ row: mockRow } as any); - expect(cellResult?.props.children).toBe("Hidden Value"); - }); - - test("hiddenFieldColumns are empty when fieldIds don't exist", () => { - // Create a survey with no hidden field IDs - const surveyWithNoHiddenFields = { - ...mockSurvey, - hiddenFields: { enabled: true }, // no fieldIds - }; - - const columns = generateResponseTableColumns(surveyWithNoHiddenFields, false, true, t as any, false); - - // Check that no hidden field columns were created - const hfColumn = columns.find((col) => (col as any).accessorKey === "HIDDEN_FIELD_hf1"); - expect(hfColumn).toBeUndefined(); - }); -}); - -describe("ResponseTableColumns - Multiple Choice Questions", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - test("generates two columns for multipleChoiceSingle questions", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - - // Should have main response column - const mainColumn = columns.find((col) => (col as any).accessorKey === "QUESTION_q5single"); - expect(mainColumn).toBeDefined(); - - // Should have option IDs column - const optionIdsColumn = columns.find((col) => (col as any).accessorKey === "QUESTION_q5singleoptionIds"); - expect(optionIdsColumn).toBeDefined(); - }); - - test("generates two columns for multipleChoiceMulti questions", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - - // Should have main response column - const mainColumn = columns.find((col) => (col as any).accessorKey === "QUESTION_q6multi"); - expect(mainColumn).toBeDefined(); - - // Should have option IDs column - const optionIdsColumn = columns.find((col) => (col as any).accessorKey === "QUESTION_q6multioptionIds"); - expect(optionIdsColumn).toBeDefined(); - }); - - test("multipleChoiceSingle main column renders RenderResponse component", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const mainColumn: any = columns.find((col) => (col as any).accessorKey === "QUESTION_q5single"); - - const mockRow = { - original: { - responseData: { q5single: "Option 1" }, - language: "default", - }, - }; - - const cellResult = mainColumn?.cell?.({ row: mockRow } as any); - // Check that RenderResponse component is returned - expect(cellResult).toBeDefined(); - }); - - test("multipleChoiceMulti main column renders RenderResponse component", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const mainColumn: any = columns.find((col) => (col as any).accessorKey === "QUESTION_q6multi"); - - const mockRow = { - original: { - responseData: { q6multi: ["Choice A", "Choice C"] }, - language: "default", - }, - }; - - const cellResult = mainColumn?.cell?.({ row: mockRow } as any); - // Check that RenderResponse component is returned - expect(cellResult).toBeDefined(); - }); -}); - -describe("ResponseTableColumns - Choice ID Columns", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - test("option IDs column calls extractChoiceIdsFromResponse for string response", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const optionIdsColumn: any = columns.find( - (col) => (col as any).accessorKey === "QUESTION_q5singleoptionIds" - ); - - const mockRow = { - original: { - responseData: { q5single: "Option 1" }, - language: "default", - }, - }; - - optionIdsColumn?.cell?.({ row: mockRow } as any); - - expect(vi.mocked(extractChoiceIdsFromResponse)).toHaveBeenCalledWith( - "Option 1", - expect.objectContaining({ id: "q5single", type: "multipleChoiceSingle" }), - "default" - ); - }); - - test("option IDs column calls extractChoiceIdsFromResponse for array response", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const optionIdsColumn: any = columns.find( - (col) => (col as any).accessorKey === "QUESTION_q6multioptionIds" - ); - - const mockRow = { - original: { - responseData: { q6multi: ["Choice A", "Choice C"] }, - language: "default", - }, - }; - - optionIdsColumn?.cell?.({ row: mockRow } as any); - - expect(vi.mocked(extractChoiceIdsFromResponse)).toHaveBeenCalledWith( - ["Choice A", "Choice C"], - expect.objectContaining({ id: "q6multi", type: "multipleChoiceMulti" }), - "default" - ); - }); - - test("option IDs column renders IdBadge components for choice IDs", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const optionIdsColumn: any = columns.find( - (col) => (col as any).accessorKey === "QUESTION_q6multioptionIds" - ); - - const mockRow = { - original: { - responseData: { q6multi: ["Choice A", "Choice C"] }, - language: "default", - }, - }; - - // Mock extractChoiceIdsFromResponse to return specific choice IDs - vi.mocked(extractChoiceIdsFromResponse).mockReturnValueOnce(["choice-1", "choice-3"]); - - const cellResult = optionIdsColumn?.cell?.({ row: mockRow } as any); - - // Should render something for choice IDs - expect(cellResult).toBeDefined(); - // Verify that extractChoiceIdsFromResponse was called - expect(vi.mocked(extractChoiceIdsFromResponse)).toHaveBeenCalled(); - }); - - test("option IDs column returns null for non-string/array response values", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const optionIdsColumn: any = columns.find( - (col) => (col as any).accessorKey === "QUESTION_q5singleoptionIds" - ); - - const mockRow = { - original: { - responseData: { q5single: 123 }, // Invalid type - language: "default", - }, - }; - - const cellResult = optionIdsColumn?.cell?.({ row: mockRow } as any); - - expect(cellResult).toBeNull(); - expect(vi.mocked(extractChoiceIdsFromResponse)).not.toHaveBeenCalled(); - }); - - test("option IDs column returns null when no choice IDs found", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const optionIdsColumn: any = columns.find( - (col) => (col as any).accessorKey === "QUESTION_q5singleoptionIds" - ); - - const mockRow = { - original: { - responseData: { q5single: "Non-existent option" }, - language: "default", - }, - }; - - // Mock extractChoiceIdsFromResponse to return empty array - vi.mocked(extractChoiceIdsFromResponse).mockReturnValueOnce([]); - - const cellResult = optionIdsColumn?.cell?.({ row: mockRow } as any); - - expect(cellResult).toBeNull(); - }); - - test("option IDs column handles missing language gracefully", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const optionIdsColumn: any = columns.find( - (col) => (col as any).accessorKey === "QUESTION_q5singleoptionIds" - ); - - const mockRow = { - original: { - responseData: { q5single: "Option 1" }, - language: null, // No language - }, - }; - - optionIdsColumn?.cell?.({ row: mockRow } as any); - - expect(vi.mocked(extractChoiceIdsFromResponse)).toHaveBeenCalledWith( - "Option 1", - expect.objectContaining({ id: "q5single" }), - undefined - ); - }); -}); - -describe("ResponseTableColumns - Helper Functions", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - test("question headers are properly created for multiple choice questions", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const mainColumn: any = columns.find((col) => (col as any).accessorKey === "QUESTION_q5single"); - const optionIdsColumn: any = columns.find( - (col) => (col as any).accessorKey === "QUESTION_q5singleoptionIds" - ); - - // Test main column header - const mainHeader = mainColumn?.header?.(); - expect(mainHeader).toBeDefined(); - expect(mainHeader?.props?.className).toContain("flex items-center justify-between"); - - // Test option IDs column header - const optionHeader = optionIdsColumn?.header?.(); - expect(optionHeader).toBeDefined(); - expect(optionHeader?.props?.className).toContain("flex items-center justify-between"); - }); - - test("question headers include proper icons for multiple choice questions", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - const singleChoiceColumn: any = columns.find((col) => (col as any).accessorKey === "QUESTION_q5single"); - const multiChoiceColumn: any = columns.find((col) => (col as any).accessorKey === "QUESTION_q6multi"); - - // Headers should be functions that return JSX - expect(typeof singleChoiceColumn?.header).toBe("function"); - expect(typeof multiChoiceColumn?.header).toBe("function"); - - // Call headers to ensure they don't throw - expect(() => singleChoiceColumn?.header?.()).not.toThrow(); - expect(() => multiChoiceColumn?.header?.()).not.toThrow(); - }); -}); - -describe("ResponseTableColumns - Integration Tests", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - test("multiple choice questions work end-to-end with real data", () => { - const columns = generateResponseTableColumns(mockSurvey, false, true, t as any, false); - - // Find all multiple choice related columns - const singleMainCol = columns.find((col) => (col as any).accessorKey === "QUESTION_q5single"); - const singleIdsCol = columns.find((col) => (col as any).accessorKey === "QUESTION_q5singleoptionIds"); - const multiMainCol = columns.find((col) => (col as any).accessorKey === "QUESTION_q6multi"); - const multiIdsCol = columns.find((col) => (col as any).accessorKey === "QUESTION_q6multioptionIds"); - - expect(singleMainCol).toBeDefined(); - expect(singleIdsCol).toBeDefined(); - expect(multiMainCol).toBeDefined(); - expect(multiIdsCol).toBeDefined(); - - // Test with actual mock response data - const mockRow = { original: mockResponseData }; - - // Test single choice main column - const singleMainResult = (singleMainCol?.cell as any)?.({ row: mockRow }); - expect(singleMainResult).toBeDefined(); - - // Test multi choice main column - const multiMainResult = (multiMainCol?.cell as any)?.({ row: mockRow }); - expect(multiMainResult).toBeDefined(); - - // Test that choice ID columns exist and can be called - const singleIdsResult = (singleIdsCol?.cell as any)?.({ row: mockRow }); - const multiIdsResult = (multiIdsCol?.cell as any)?.({ row: mockRow }); - - // Should not error when calling the cell functions - expect(() => singleIdsResult).not.toThrow(); - expect(() => multiIdsResult).not.toThrow(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.test.tsx deleted file mode 100644 index 32431c969a..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.test.tsx +++ /dev/null @@ -1,274 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TTag } from "@formbricks/types/tags"; -import { TUser, TUserLocale } from "@formbricks/types/user"; -import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; -import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation"; -import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage"; -import Page from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page"; -import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA"; -import { getDisplayCountBySurveyId } from "@/lib/display/service"; -import { getPublicDomain } from "@/lib/getPublicUrl"; -import { getResponseCountBySurveyId } from "@/lib/response/service"; -import { getSurvey } from "@/lib/survey/service"; -import { getTagsByEnvironmentId } from "@/lib/tag/service"; -import { getUser } from "@/lib/user/service"; -import { findMatchingLocale } from "@/lib/utils/locale"; -import { getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; -import { getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization"; -import { getOrganizationBilling } from "@/modules/survey/lib/survey"; - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation", - () => ({ - SurveyAnalysisNavigation: vi.fn(() =>
    ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage", - () => ({ - ResponsePage: vi.fn(() =>
    ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA", - () => ({ - SurveyAnalysisCTA: vi.fn(() =>
    ), - }) -); - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_STORAGE_CONFIGURED: true, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - WEBAPP_URL: "http://localhost:3000", - RESPONSES_PER_PAGE: 10, - SESSION_MAX_AGE: 1000, -})); - -vi.mock("@/lib/getPublicUrl", () => ({ - getPublicDomain: vi.fn(), -})); - -vi.mock("@/lib/response/service", () => ({ - getResponseCountBySurveyId: vi.fn(), -})); - -vi.mock("@/lib/display/service", () => ({ - getDisplayCountBySurveyId: vi.fn(), -})); - -vi.mock("@/lib/survey/service", () => ({ - getSurvey: vi.fn(), -})); - -vi.mock("@/lib/tag/service", () => ({ - getTagsByEnvironmentId: vi.fn(), -})); - -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); - -vi.mock("@/lib/utils/locale", () => ({ - findMatchingLocale: vi.fn(), -})); - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: vi.fn(({ children }) =>
    {children}
    ), -})); - -vi.mock("@/modules/survey/lib/organization", () => ({ - getOrganizationIdFromEnvironmentId: vi.fn(), -})); - -vi.mock("@/modules/survey/lib/survey", () => ({ - getOrganizationBilling: vi.fn(), -})); - -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getIsQuotasEnabled: vi.fn(), - getIsContactsEnabled: vi.fn(), -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: vi.fn(({ pageTitle, children, cta }) => ( -
    -

    {pageTitle}

    - {cta} - {children} -
    - )), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -vi.mock("next/navigation", () => ({ - useParams: () => ({ - environmentId: "test-env-id", - surveyId: "test-survey-id", - }), -})); - -const mockEnvironmentId = "test-env-id"; -const mockSurveyId = "test-survey-id"; -const mockUserId = "test-user-id"; - -const mockSurvey: TSurvey = { - id: mockSurveyId, - name: "Test Survey", - environmentId: mockEnvironmentId, - status: "inProgress", - type: "web", - questions: [], - endings: [], - languages: [], - triggers: [], - recontactDays: null, - displayOption: "displayOnce", - autoClose: null, - styling: null, -} as unknown as TSurvey; - -const mockUser = { - id: mockUserId, - name: "Test User", - email: "test@example.com", - role: "project_manager", - createdAt: new Date(), - updatedAt: new Date(), - locale: "en-US", -} as unknown as TUser; - -const mockEnvironment = { - id: mockEnvironmentId, - type: "production", - createdAt: new Date(), - updatedAt: new Date(), - appSetupCompleted: true, -} as unknown as TEnvironment; - -const mockTags: TTag[] = [{ id: "tag1", name: "Tag 1", environmentId: mockEnvironmentId } as unknown as TTag]; -const mockLocale: TUserLocale = "en-US"; -const mockPublicDomain = "http://customdomain.com"; - -const mockParams = { - environmentId: mockEnvironmentId, - surveyId: mockSurveyId, -}; - -describe("ResponsesPage", () => { - beforeEach(() => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: { user: { id: mockUserId } } as any, - environment: mockEnvironment, - isReadOnly: false, - } as TEnvironmentAuth); - vi.mocked(getSurvey).mockResolvedValue(mockSurvey); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getTagsByEnvironmentId).mockResolvedValue(mockTags); - vi.mocked(getResponseCountBySurveyId).mockResolvedValue(10); - vi.mocked(getDisplayCountBySurveyId).mockResolvedValue(5); - vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale); - vi.mocked(getPublicDomain).mockReturnValue(mockPublicDomain); - }); - - afterEach(() => { - cleanup(); - vi.resetAllMocks(); - }); - - test("renders correctly with all data", async () => { - const props = { params: mockParams }; - vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValue("mock-organization-id"); - vi.mocked(getOrganizationBilling).mockResolvedValue({ plan: "scale" }); - vi.mocked(getIsQuotasEnabled).mockResolvedValue(false); - const jsx = await Page(props); - render({jsx}); - - await screen.findByTestId("page-content-wrapper"); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("page-title")).toHaveTextContent(mockSurvey.name); - expect(screen.getByTestId("survey-analysis-cta")).toBeInTheDocument(); - expect(screen.getByTestId("survey-analysis-navigation")).toBeInTheDocument(); - expect(screen.getByTestId("response-page")).toBeInTheDocument(); - - expect(vi.mocked(SurveyAnalysisCTA)).toHaveBeenCalledWith( - expect.objectContaining({ - environment: mockEnvironment, - survey: mockSurvey, - isReadOnly: false, - user: mockUser, - publicDomain: mockPublicDomain, - responseCount: 10, - displayCount: 5, - isStorageConfigured: true, - }), - undefined - ); - - expect(vi.mocked(SurveyAnalysisNavigation)).toHaveBeenCalledWith( - expect.objectContaining({ - environmentId: mockEnvironmentId, - survey: mockSurvey, - activeId: "responses", - }), - undefined - ); - - expect(vi.mocked(ResponsePage)).toHaveBeenCalledWith( - expect.objectContaining({ - environment: mockEnvironment, - survey: mockSurvey, - surveyId: mockSurveyId, - environmentTags: mockTags, - user: mockUser, - responsesPerPage: 10, - locale: mockLocale, - }), - undefined - ); - }); - - test("throws error if survey not found", async () => { - vi.mocked(getSurvey).mockResolvedValue(null); - const props = { params: mockParams }; - await expect(Page(props)).rejects.toThrow("common.survey_not_found"); - }); - - test("throws error if user not found", async () => { - vi.mocked(getUser).mockResolvedValue(null); - const props = { params: mockParams }; - await expect(Page(props)).rejects.toThrow("common.user_not_found"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/AddressSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/AddressSummary.test.tsx deleted file mode 100644 index 211228957c..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/AddressSummary.test.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionSummaryAddress } from "@formbricks/types/surveys/types"; -import { AddressSummary } from "./AddressSummary"; - -// Mock dependencies -vi.mock("@/lib/time", () => ({ - timeSince: () => "2 hours ago", -})); - -vi.mock("@/lib/utils/contact", () => ({ - getContactIdentifier: () => "contact@example.com", -})); - -vi.mock("@/modules/ui/components/avatars", () => ({ - PersonAvatar: ({ personId }: { personId: string }) =>
    {personId}
    , -})); - -vi.mock("@/modules/ui/components/array-response", () => ({ - ArrayResponse: ({ value }: { value: string[] }) => ( -
    {value.join(", ")}
    - ), -})); - -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: () =>
    , -})); - -describe("AddressSummary", () => { - afterEach(() => { - cleanup(); - }); - - const environmentId = "env-123"; - const survey = {} as TSurvey; - const locale = "en-US"; - - test("renders table headers correctly", () => { - const questionSummary = { - question: { id: "q1", headline: "Address Question" }, - samples: [], - } as unknown as TSurveyQuestionSummaryAddress; - - render( - - ); - - expect(screen.getByTestId("question-summary-header")).toBeInTheDocument(); - expect(screen.getByText("common.user")).toBeInTheDocument(); - expect(screen.getByText("common.response")).toBeInTheDocument(); - expect(screen.getByText("common.time")).toBeInTheDocument(); - }); - - test("renders contact information correctly", () => { - const questionSummary = { - question: { id: "q1", headline: "Address Question" }, - samples: [ - { - id: "response1", - value: ["123 Main St", "Apt 4", "New York", "NY", "10001"], - updatedAt: new Date().toISOString(), - contact: { id: "contact1" }, - contactAttributes: { email: "user@example.com" }, - }, - ], - } as unknown as TSurveyQuestionSummaryAddress; - - render( - - ); - - expect(screen.getByTestId("person-avatar")).toHaveTextContent("contact1"); - expect(screen.getByText("contact@example.com")).toBeInTheDocument(); - expect(screen.getByTestId("array-response")).toHaveTextContent("123 Main St, Apt 4, New York, NY, 10001"); - expect(screen.getByText("2 hours ago")).toBeInTheDocument(); - - // Check link to contact - const contactLink = screen.getByText("contact@example.com").closest("a"); - expect(contactLink).toHaveAttribute("href", `/environments/${environmentId}/contacts/contact1`); - }); - - test("renders anonymous user when no contact is provided", () => { - const questionSummary = { - question: { id: "q1", headline: "Address Question" }, - samples: [ - { - id: "response2", - value: ["456 Oak St", "London", "UK"], - updatedAt: new Date().toISOString(), - contact: null, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryAddress; - - render( - - ); - - expect(screen.getByTestId("person-avatar")).toHaveTextContent("anonymous"); - expect(screen.getByText("common.anonymous")).toBeInTheDocument(); - expect(screen.getByTestId("array-response")).toHaveTextContent("456 Oak St, London, UK"); - }); - - test("renders multiple responses correctly", () => { - const questionSummary = { - question: { id: "q1", headline: "Address Question" }, - samples: [ - { - id: "response1", - value: ["123 Main St", "New York"], - updatedAt: new Date().toISOString(), - contact: { id: "contact1" }, - contactAttributes: {}, - }, - { - id: "response2", - value: ["456 Oak St", "London"], - updatedAt: new Date().toISOString(), - contact: { id: "contact2" }, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryAddress; - - render( - - ); - - expect(screen.getAllByTestId("person-avatar")).toHaveLength(2); - expect(screen.getAllByTestId("array-response")).toHaveLength(2); - expect(screen.getAllByText("2 hours ago")).toHaveLength(2); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary.test.tsx deleted file mode 100644 index aa92690d76..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionSummaryCta } from "@formbricks/types/surveys/types"; -import { CTASummary } from "./CTASummary"; - -vi.mock("@/modules/ui/components/progress-bar", () => ({ - ProgressBar: ({ progress, barColor }: { progress: number; barColor: string }) => ( -
    {`${progress}-${barColor}`}
    - ), -})); - -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: ({ - additionalInfo, - }: { - showResponses: boolean; - additionalInfo: React.ReactNode; - }) =>
    {additionalInfo}
    , -})); - -vi.mock("lucide-react", () => ({ - InboxIcon: () =>
    , -})); - -vi.mock("../lib/utils", () => ({ - convertFloatToNDecimal: (value: number) => value.toFixed(2), -})); - -describe("CTASummary", () => { - afterEach(() => { - cleanup(); - }); - - const survey = {} as TSurvey; - - test("renders with all metrics and required question", () => { - const questionSummary = { - question: { id: "q1", headline: "CTA Question", required: true }, - impressionCount: 100, - clickCount: 25, - skipCount: 10, - ctr: { count: 25, percentage: 25 }, - } as unknown as TSurveyQuestionSummaryCta; - - render(); - - expect(screen.getByTestId("question-summary-header")).toBeInTheDocument(); - expect(screen.getByText("100 common.impressions")).toBeInTheDocument(); - // Use getAllByText instead of getByText for multiple matching elements - expect(screen.getAllByText("25 common.clicks")).toHaveLength(2); - expect(screen.queryByText("10 common.skips")).not.toBeInTheDocument(); // Should not show skips for required questions - - // Check CTR section - expect(screen.getByText("CTR")).toBeInTheDocument(); - expect(screen.getByText("25.00%")).toBeInTheDocument(); - - // Check progress bar - expect(screen.getByTestId("progress-bar")).toHaveTextContent("0.25-bg-brand-dark"); - }); - - test("renders skip count for non-required questions", () => { - const questionSummary = { - question: { id: "q1", headline: "CTA Question", required: false }, - impressionCount: 100, - clickCount: 20, - skipCount: 30, - ctr: { count: 20, percentage: 20 }, - } as unknown as TSurveyQuestionSummaryCta; - - render(); - - expect(screen.getByText("30 common.skips")).toBeInTheDocument(); - }); - - test("renders singular form for count = 1", () => { - const questionSummary = { - question: { id: "q1", headline: "CTA Question", required: true }, - impressionCount: 10, - clickCount: 1, - skipCount: 0, - ctr: { count: 1, percentage: 10 }, - } as unknown as TSurveyQuestionSummaryCta; - - render(); - - // Use getAllByText instead of getByText for multiple matching elements - expect(screen.getAllByText("1 common.click")).toHaveLength(1); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary.test.tsx deleted file mode 100644 index f914246fc1..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionSummaryCal } from "@formbricks/types/surveys/types"; -import { CalSummary } from "./CalSummary"; - -vi.mock("@/modules/ui/components/progress-bar", () => ({ - ProgressBar: ({ progress, barColor }: { progress: number; barColor: string }) => ( -
    {`${progress}-${barColor}`}
    - ), -})); - -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: () =>
    , -})); - -vi.mock("../lib/utils", () => ({ - convertFloatToNDecimal: (value: number) => value.toFixed(2), -})); - -describe("CalSummary", () => { - afterEach(() => { - cleanup(); - }); - - const environmentId = "env-123"; - const survey = {} as TSurvey; - - test("renders the correct components and data", () => { - const questionSummary = { - question: { id: "q1", headline: "Calendar Question" }, - booked: { count: 5, percentage: 75 }, - skipped: { count: 1, percentage: 25 }, - } as unknown as TSurveyQuestionSummaryCal; - - render(); - - expect(screen.getByTestId("question-summary-header")).toBeInTheDocument(); - - // Check if booked section is displayed - expect(screen.getByText("common.booked")).toBeInTheDocument(); - expect(screen.getByText("75.00%")).toBeInTheDocument(); - expect(screen.getByText("5 common.responses")).toBeInTheDocument(); - - // Check if skipped section is displayed - expect(screen.getByText("common.dismissed")).toBeInTheDocument(); - expect(screen.getByText("25.00%")).toBeInTheDocument(); - expect(screen.getByText("1 common.response")).toBeInTheDocument(); - - // Check progress bars - const progressBars = screen.getAllByTestId("progress-bar"); - expect(progressBars).toHaveLength(2); - expect(progressBars[0]).toHaveTextContent("0.75-bg-brand-dark"); - expect(progressBars[1]).toHaveTextContent("0.25-bg-brand-dark"); - }); - - test("renders singular and plural response counts correctly", () => { - const questionSummary = { - question: { id: "q1", headline: "Calendar Question" }, - booked: { count: 1, percentage: 50 }, - skipped: { count: 1, percentage: 50 }, - } as unknown as TSurveyQuestionSummaryCal; - - render(); - - // Use getAllByText directly since we know there are multiple matching elements - const responseElements = screen.getAllByText("1 common.response"); - expect(responseElements).toHaveLength(2); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.test.tsx deleted file mode 100644 index f97f35b5e4..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { - TSurvey, - TSurveyConsentQuestion, - TSurveyQuestionSummaryConsent, - TSurveyQuestionTypeEnum, -} from "@formbricks/types/surveys/types"; -import { ConsentSummary } from "./ConsentSummary"; - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader", - () => ({ - QuestionSummaryHeader: () =>
    QuestionSummaryHeader
    , - }) -); - -describe("ConsentSummary", () => { - afterEach(() => { - cleanup(); - }); - - const mockSetFilter = vi.fn(); - const questionSummary = { - question: { - id: "q1", - headline: { en: "Headline" }, - type: TSurveyQuestionTypeEnum.Consent, - } as unknown as TSurveyConsentQuestion, - accepted: { percentage: 60.5, count: 61 }, - dismissed: { percentage: 39.5, count: 40 }, - } as unknown as TSurveyQuestionSummaryConsent; - const survey = {} as TSurvey; - - test("renders accepted and dismissed with correct values", () => { - render(); - expect(screen.getByText("common.accepted")).toBeInTheDocument(); - expect(screen.getByText(/60\.5%/)).toBeInTheDocument(); - expect(screen.getByText(/61/)).toBeInTheDocument(); - expect(screen.getByText("common.dismissed")).toBeInTheDocument(); - expect(screen.getByText(/39\.5%/)).toBeInTheDocument(); - expect(screen.getByText(/40/)).toBeInTheDocument(); - }); - - test("calls setFilter with correct args on accepted click", async () => { - render(); - await userEvent.click(screen.getByText("common.accepted")); - expect(mockSetFilter).toHaveBeenCalledWith( - "q1", - { en: "Headline" }, - TSurveyQuestionTypeEnum.Consent, - "is", - "common.accepted" - ); - }); - - test("calls setFilter with correct args on dismissed click", async () => { - render(); - await userEvent.click(screen.getByText("common.dismissed")); - expect(mockSetFilter).toHaveBeenCalledWith( - "q1", - { en: "Headline" }, - TSurveyQuestionTypeEnum.Consent, - "is", - "common.dismissed" - ); - }); - - test("renders singular and plural response labels", () => { - const oneAndTwo = { - ...questionSummary, - accepted: { percentage: questionSummary.accepted.percentage, count: 1 }, - dismissed: { percentage: questionSummary.dismissed.percentage, count: 2 }, - }; - render(); - expect(screen.getByText(/1 common\.response/)).toBeInTheDocument(); - expect(screen.getByText(/2 common\.responses/)).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary.test.tsx deleted file mode 100644 index 5ed1adfe41..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary.test.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionSummaryContactInfo } from "@formbricks/types/surveys/types"; -import { ContactInfoSummary } from "./ContactInfoSummary"; - -vi.mock("@/lib/time", () => ({ - timeSince: () => "2 hours ago", -})); - -vi.mock("@/lib/utils/contact", () => ({ - getContactIdentifier: () => "contact@example.com", -})); - -vi.mock("@/modules/ui/components/avatars", () => ({ - PersonAvatar: ({ personId }: { personId: string }) =>
    {personId}
    , -})); - -vi.mock("@/modules/ui/components/array-response", () => ({ - ArrayResponse: ({ value }: { value: string[] }) => ( -
    {value.join(", ")}
    - ), -})); - -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: () =>
    , -})); - -describe("ContactInfoSummary", () => { - afterEach(() => { - cleanup(); - }); - - const environmentId = "env-123"; - const survey = {} as TSurvey; - const locale = "en-US"; - - test("renders table headers correctly", () => { - const questionSummary = { - question: { id: "q1", headline: "Contact Info Question" }, - samples: [], - } as unknown as TSurveyQuestionSummaryContactInfo; - - render( - - ); - - expect(screen.getByTestId("question-summary-header")).toBeInTheDocument(); - expect(screen.getByText("common.user")).toBeInTheDocument(); - expect(screen.getByText("common.response")).toBeInTheDocument(); - expect(screen.getByText("common.time")).toBeInTheDocument(); - }); - - test("renders contact information correctly", () => { - const questionSummary = { - question: { id: "q1", headline: "Contact Info Question" }, - samples: [ - { - id: "response1", - value: ["John Doe", "john@example.com", "+1234567890"], - updatedAt: new Date().toISOString(), - contact: { id: "contact1" }, - contactAttributes: { email: "user@example.com" }, - }, - ], - } as unknown as TSurveyQuestionSummaryContactInfo; - - render( - - ); - - expect(screen.getByTestId("person-avatar")).toHaveTextContent("contact1"); - expect(screen.getByText("contact@example.com")).toBeInTheDocument(); - expect(screen.getByTestId("array-response")).toHaveTextContent("John Doe, john@example.com, +1234567890"); - expect(screen.getByText("2 hours ago")).toBeInTheDocument(); - - // Check link to contact - const contactLink = screen.getByText("contact@example.com").closest("a"); - expect(contactLink).toHaveAttribute("href", `/environments/${environmentId}/contacts/contact1`); - }); - - test("renders anonymous user when no contact is provided", () => { - const questionSummary = { - question: { id: "q1", headline: "Contact Info Question" }, - samples: [ - { - id: "response2", - value: ["Anonymous User", "anonymous@example.com"], - updatedAt: new Date().toISOString(), - contact: null, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryContactInfo; - - render( - - ); - - expect(screen.getByTestId("person-avatar")).toHaveTextContent("anonymous"); - expect(screen.getByText("common.anonymous")).toBeInTheDocument(); - expect(screen.getByTestId("array-response")).toHaveTextContent("Anonymous User, anonymous@example.com"); - }); - - test("renders multiple responses correctly", () => { - const questionSummary = { - question: { id: "q1", headline: "Contact Info Question" }, - samples: [ - { - id: "response1", - value: ["John Doe", "john@example.com"], - updatedAt: new Date().toISOString(), - contact: { id: "contact1" }, - contactAttributes: {}, - }, - { - id: "response2", - value: ["Jane Smith", "jane@example.com"], - updatedAt: new Date().toISOString(), - contact: { id: "contact2" }, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryContactInfo; - - render( - - ); - - expect(screen.getAllByTestId("person-avatar")).toHaveLength(2); - expect(screen.getAllByTestId("array-response")).toHaveLength(2); - expect(screen.getAllByText("2 hours ago")).toHaveLength(2); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.test.tsx deleted file mode 100644 index 904b846389..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.test.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionSummaryDate } from "@formbricks/types/surveys/types"; -import { DateQuestionSummary } from "./DateQuestionSummary"; - -vi.mock("@/lib/time", () => ({ - timeSince: () => "2 hours ago", -})); - -vi.mock("@/lib/utils/contact", () => ({ - getContactIdentifier: () => "contact@example.com", -})); - -vi.mock("@/lib/utils/datetime", () => ({ - formatDateWithOrdinal: (_: Date) => "January 1st, 2023", -})); - -vi.mock("@/modules/ui/components/avatars", () => ({ - PersonAvatar: ({ personId }: { personId: string }) =>
    {personId}
    , -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => ( - - ), -})); - -vi.mock("next/link", () => ({ - default: ({ children, href }: { children: React.ReactNode; href: string }) => ( - - {children} - - ), -})); - -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: () =>
    , -})); - -describe("DateQuestionSummary", () => { - afterEach(() => { - cleanup(); - }); - - const environmentId = "env-123"; - const survey = {} as TSurvey; - const locale = "en-US"; - - test("renders table headers correctly", () => { - const questionSummary = { - question: { id: "q1", headline: "Date Question" }, - samples: [], - } as unknown as TSurveyQuestionSummaryDate; - - render( - - ); - - expect(screen.getByTestId("question-summary-header")).toBeInTheDocument(); - expect(screen.getByText("common.user")).toBeInTheDocument(); - expect(screen.getByText("common.response")).toBeInTheDocument(); - expect(screen.getByText("common.time")).toBeInTheDocument(); - }); - - test("renders date responses correctly", () => { - const questionSummary = { - question: { id: "q1", headline: "Date Question" }, - samples: [ - { - id: "response1", - value: "2023-01-01", - updatedAt: new Date().toISOString(), - contact: { id: "contact1" }, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryDate; - - render( - - ); - - expect(screen.getByText("January 1st, 2023")).toBeInTheDocument(); - expect(screen.getByText("contact@example.com")).toBeInTheDocument(); - expect(screen.getByText("2 hours ago")).toBeInTheDocument(); - }); - - test("renders invalid dates with special message", () => { - const questionSummary = { - question: { id: "q1", headline: "Date Question" }, - samples: [ - { - id: "response1", - value: "invalid-date", - updatedAt: new Date().toISOString(), - contact: { id: "contact1" }, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryDate; - - render( - - ); - - expect(screen.getByText("common.invalid_date(invalid-date)")).toBeInTheDocument(); - }); - - test("renders anonymous user when no contact is provided", () => { - const questionSummary = { - question: { id: "q1", headline: "Date Question" }, - samples: [ - { - id: "response1", - value: "2023-01-01", - updatedAt: new Date().toISOString(), - contact: null, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryDate; - - render( - - ); - - expect(screen.getByText("common.anonymous")).toBeInTheDocument(); - }); - - test("shows load more button when there are more responses and loads more on click", async () => { - const samples = Array.from({ length: 15 }, (_, i) => ({ - id: `response${i}`, - value: "2023-01-01", - updatedAt: new Date().toISOString(), - contact: null, - contactAttributes: {}, - })); - - const questionSummary = { - question: { id: "q1", headline: "Date Question" }, - samples, - } as unknown as TSurveyQuestionSummaryDate; - - render( - - ); - - // Initially 10 responses should be visible - expect(screen.getAllByText("January 1st, 2023")).toHaveLength(10); - - // "Load More" button should be visible - const loadMoreButton = screen.getByTestId("load-more-button"); - expect(loadMoreButton).toBeInTheDocument(); - - // Click "Load More" - await userEvent.click(loadMoreButton); - - // Now all 15 responses should be visible - expect(screen.getAllByText("January 1st, 2023")).toHaveLength(15); - - // "Load More" button should disappear - expect(screen.queryByTestId("load-more-button")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.test.tsx deleted file mode 100644 index 08ad1c1df5..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.test.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { - TSurvey, - TSurveyFileUploadQuestion, - TSurveyQuestionSummaryFileUpload, - TSurveyQuestionTypeEnum, -} from "@formbricks/types/surveys/types"; -import { FileUploadSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary"; - -// Mock child components and hooks -vi.mock("@/modules/ui/components/avatars", () => ({ - PersonAvatar: vi.fn(() =>
    PersonAvatarMock
    ), -})); - -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: vi.fn(() =>
    QuestionSummaryHeaderMock
    ), -})); - -// Mock utility functions -vi.mock("@/modules/storage/utils", () => ({ - getOriginalFileNameFromUrl: (url: string) => `original-${url.split("/").pop()}`, -})); - -vi.mock("@/lib/time", () => ({ - timeSince: () => "some time ago", -})); - -vi.mock("@/lib/utils/contact", () => ({ - getContactIdentifier: () => "contact@example.com", -})); - -const environmentId = "test-env-id"; -const survey = { id: "survey-1" } as TSurvey; -const locale = "en-US"; - -const createMockResponse = (id: string, value: string[], contactId: string | null = null) => ({ - id: `response-${id}`, - value, - updatedAt: new Date().toISOString(), - contact: contactId ? { id: contactId, name: `Contact ${contactId}` } : null, - contactAttributes: contactId ? { email: `contact${contactId}@example.com` } : {}, -}); - -const questionSummaryBase = { - question: { - id: "q1", - headline: { default: "Upload your file" }, - type: TSurveyQuestionTypeEnum.FileUpload, - } as unknown as TSurveyFileUploadQuestion, - responseCount: 0, - files: [], -} as unknown as TSurveyQuestionSummaryFileUpload; - -describe("FileUploadSummary", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the component with initial responses", () => { - const files = Array.from({ length: 5 }, (_, i) => - createMockResponse(i.toString(), [`https://example.com/file${i}.pdf`], `contact-${i}`) - ); - const questionSummary = { - ...questionSummaryBase, - files, - responseCount: files.length, - } as unknown as TSurveyQuestionSummaryFileUpload; - - render( - - ); - - expect(screen.getByText("QuestionSummaryHeaderMock")).toBeInTheDocument(); - expect(screen.getByText("common.user")).toBeInTheDocument(); - expect(screen.getByText("common.response")).toBeInTheDocument(); - expect(screen.getByText("common.time")).toBeInTheDocument(); - expect(screen.getAllByText("PersonAvatarMock")).toHaveLength(5); - expect(screen.getAllByText("contact@example.com")).toHaveLength(5); - expect(screen.getByText("original-file0.pdf")).toBeInTheDocument(); - expect(screen.getByText("original-file4.pdf")).toBeInTheDocument(); - expect(screen.queryByText("common.load_more")).not.toBeInTheDocument(); - }); - - test("renders 'Skipped' when value is an empty array", () => { - const files = [createMockResponse("skipped", [], "contact-skipped")]; - const questionSummary = { - ...questionSummaryBase, - files, - responseCount: files.length, - } as unknown as TSurveyQuestionSummaryFileUpload; - - render( - - ); - - expect(screen.getByText("common.skipped")).toBeInTheDocument(); - expect(screen.queryByText(/original-/)).not.toBeInTheDocument(); // No file name should be rendered - }); - - test("renders 'Anonymous' when contact is null", () => { - const files = [createMockResponse("anon", ["https://example.com/anonfile.jpg"], null)]; - const questionSummary = { - ...questionSummaryBase, - files, - responseCount: files.length, - } as unknown as TSurveyQuestionSummaryFileUpload; - - render( - - ); - - expect(screen.getByText("common.anonymous")).toBeInTheDocument(); - expect(screen.getByText("original-anonfile.jpg")).toBeInTheDocument(); - }); - - test("shows 'Load More' button when there are more than 10 responses and loads more on click", async () => { - const files = Array.from({ length: 15 }, (_, i) => - createMockResponse(i.toString(), [`https://example.com/file${i}.txt`], `contact-${i}`) - ); - const questionSummary = { - ...questionSummaryBase, - files, - responseCount: files.length, - } as unknown as TSurveyQuestionSummaryFileUpload; - - render( - - ); - - // Initially 10 responses should be visible - expect(screen.getAllByText("PersonAvatarMock")).toHaveLength(10); - expect(screen.getByText("original-file9.txt")).toBeInTheDocument(); - expect(screen.queryByText("original-file10.txt")).not.toBeInTheDocument(); - - // "Load More" button should be visible - const loadMoreButton = screen.getByText("common.load_more"); - expect(loadMoreButton).toBeInTheDocument(); - - // Click "Load More" - await userEvent.click(loadMoreButton); - - // Now all 15 responses should be visible - expect(screen.getAllByText("PersonAvatarMock")).toHaveLength(15); - expect(screen.getByText("original-file14.txt")).toBeInTheDocument(); - - // "Load More" button should disappear - expect(screen.queryByText("common.load_more")).not.toBeInTheDocument(); - }); - - test("renders multiple files for a single response", () => { - const files = [ - createMockResponse( - "multi", - ["https://example.com/fileA.png", "https://example.com/fileB.docx"], - "contact-multi" - ), - ]; - const questionSummary = { - ...questionSummaryBase, - files, - responseCount: files.length, - } as unknown as TSurveyQuestionSummaryFileUpload; - - render( - - ); - - expect(screen.getByText("original-fileA.png")).toBeInTheDocument(); - expect(screen.getByText("original-fileB.docx")).toBeInTheDocument(); - // Check that download links exist - const links = screen.getAllByRole("link"); - // 1 contact link + 2 file links - expect(links.filter((link) => link.getAttribute("target") === "_blank")).toHaveLength(2); - expect( - links.find((link) => link.getAttribute("href") === "https://example.com/fileA.png") - ).toBeInTheDocument(); - expect( - links.find((link) => link.getAttribute("href") === "https://example.com/fileB.docx") - ).toBeInTheDocument(); - }); - - test("renders contact link correctly", () => { - const contactId = "contact-link-test"; - const files = [createMockResponse("link", ["https://example.com/link.pdf"], contactId)]; - const questionSummary = { - ...questionSummaryBase, - files, - responseCount: files.length, - } as unknown as TSurveyQuestionSummaryFileUpload; - - render( - - ); - - const contactLink = screen.getByText("contact@example.com").closest("a"); - expect(contactLink).toBeInTheDocument(); - expect(contactLink).toHaveAttribute("href", `/environments/${environmentId}/contacts/${contactId}`); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.test.tsx deleted file mode 100644 index 7924f943fb..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.test.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TSurveyQuestionSummaryHiddenFields } from "@formbricks/types/surveys/types"; -import { HiddenFieldsSummary } from "./HiddenFieldsSummary"; - -// Mock dependencies -vi.mock("@/lib/time", () => ({ - timeSince: () => "2 hours ago", -})); - -vi.mock("@/lib/utils/contact", () => ({ - getContactIdentifier: () => "contact@example.com", -})); - -vi.mock("@/modules/ui/components/avatars", () => ({ - PersonAvatar: ({ personId }: { personId: string }) =>
    {personId}
    , -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => ( - - ), -})); - -// Mock lucide-react components -vi.mock("lucide-react", () => ({ - InboxIcon: () =>
    , - MessageSquareTextIcon: () =>
    , - Link: ({ children, href, className }: { children: React.ReactNode; href: string; className: string }) => ( - - {children} - - ), -})); - -// Mock Next.js Link -vi.mock("next/link", () => ({ - default: ({ children, href }: { children: React.ReactNode; href: string }) => ( - - {children} - - ), -})); - -describe("HiddenFieldsSummary", () => { - afterEach(() => { - cleanup(); - }); - - const environment = { id: "env-123" } as TEnvironment; - const locale = "en-US"; - - test("renders component with correct header and single response", () => { - const questionSummary = { - id: "hidden-field-1", - responseCount: 1, - samples: [ - { - id: "response1", - value: "Hidden value", - updatedAt: new Date().toISOString(), - contact: { id: "contact1" }, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryHiddenFields; - - render( - - ); - - expect(screen.getByText("hidden-field-1")).toBeInTheDocument(); - expect(screen.getByText("Hidden Field")).toBeInTheDocument(); - expect(screen.getByText("1 common.response")).toBeInTheDocument(); - - // Headers - expect(screen.getByText("common.user")).toBeInTheDocument(); - expect(screen.getByText("common.response")).toBeInTheDocument(); - expect(screen.getByText("common.time")).toBeInTheDocument(); - - // We can skip checking for PersonAvatar as it's inside hidden md:flex - expect(screen.getByText("contact@example.com")).toBeInTheDocument(); - expect(screen.getByText("Hidden value")).toBeInTheDocument(); - expect(screen.getByText("2 hours ago")).toBeInTheDocument(); - - // Check for link without checking for specific href - expect(screen.getByText("contact@example.com")).toBeInTheDocument(); - }); - - test("renders anonymous user when no contact is provided", () => { - const questionSummary = { - id: "hidden-field-1", - responseCount: 1, - samples: [ - { - id: "response1", - value: "Anonymous hidden value", - updatedAt: new Date().toISOString(), - contact: null, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryHiddenFields; - - render( - - ); - - // Instead of checking for avatar, just check for anonymous text - expect(screen.getByText("common.anonymous")).toBeInTheDocument(); - expect(screen.getByText("Anonymous hidden value")).toBeInTheDocument(); - }); - - test("renders plural response label when multiple responses", () => { - const questionSummary = { - id: "hidden-field-1", - responseCount: 2, - samples: [ - { - id: "response1", - value: "Hidden value 1", - updatedAt: new Date().toISOString(), - contact: { id: "contact1" }, - contactAttributes: {}, - }, - { - id: "response2", - value: "Hidden value 2", - updatedAt: new Date().toISOString(), - contact: { id: "contact2" }, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryHiddenFields; - - render( - - ); - - expect(screen.getByText("2 common.responses")).toBeInTheDocument(); - expect(screen.getAllByText("contact@example.com")).toHaveLength(2); - }); - - test("shows load more button when there are more responses and loads more on click", async () => { - const samples = Array.from({ length: 15 }, (_, i) => ({ - id: `response${i}`, - value: `Hidden value ${i}`, - updatedAt: new Date().toISOString(), - contact: null, - contactAttributes: {}, - })); - - const questionSummary = { - id: "hidden-field-1", - responseCount: samples.length, - samples, - } as unknown as TSurveyQuestionSummaryHiddenFields; - - render( - - ); - - // Initially 10 responses should be visible - expect(screen.getAllByText(/Hidden value \d+/)).toHaveLength(10); - - // "Load More" button should be visible - const loadMoreButton = screen.getByTestId("load-more-button"); - expect(loadMoreButton).toBeInTheDocument(); - - // Click "Load More" - await userEvent.click(loadMoreButton); - - // Now all 15 responses should be visible - expect(screen.getAllByText(/Hidden value \d+/)).toHaveLength(15); - - // "Load More" button should disappear - expect(screen.queryByTestId("load-more-button")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.test.tsx deleted file mode 100644 index 35e5c134a2..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { MatrixQuestionSummary } from "./MatrixQuestionSummary"; - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader", - () => ({ - QuestionSummaryHeader: () =>
    QuestionSummaryHeader
    , - }) -); - -describe("MatrixQuestionSummary", () => { - afterEach(() => { - cleanup(); - }); - - const survey = { id: "s1" } as any; - const questionSummary = { - question: { id: "q1", headline: "Q Head", type: "matrix" }, - data: [ - { - rowLabel: "Row1", - totalResponsesForRow: 10, - columnPercentages: [ - { column: "Yes", percentage: 50 }, - { column: "No", percentage: 50 }, - ], - }, - ], - } as any; - - test("renders headers and buttons, click triggers setFilter", async () => { - const setFilter = vi.fn(); - render(); - - // column headers - expect(screen.getByText("Yes")).toBeInTheDocument(); - expect(screen.getByText("No")).toBeInTheDocument(); - // row label - expect(screen.getByText("Row1")).toBeInTheDocument(); - // buttons - const btn = screen.getAllByRole("button", { name: /50/ }); - await userEvent.click(btn[0]); - expect(setFilter).toHaveBeenCalledWith("q1", "Q Head", "matrix", "Row1", "Yes"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx deleted file mode 100644 index e15259a9c1..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx +++ /dev/null @@ -1,405 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { MultipleChoiceSummary } from "./MultipleChoiceSummary"; - -vi.mock("@/modules/ui/components/avatars", () => ({ - PersonAvatar: ({ personId }: any) =>
    {personId}
    , -})); -vi.mock("./QuestionSummaryHeader", () => ({ QuestionSummaryHeader: () =>
    })); -vi.mock("@/modules/ui/components/id-badge", () => ({ - IdBadge: ({ id }: { id: string }) => ( -
    - ID: {id} -
    - ), -})); - -describe("MultipleChoiceSummary", () => { - afterEach(() => { - cleanup(); - }); - - const baseSurvey = { id: "s1" } as any; - const envId = "env"; - - test("renders header and choice button", async () => { - const setFilter = vi.fn(); - const q = { - question: { - id: "q", - headline: "H", - type: "multipleChoiceSingle", - choices: [{ id: "c", label: { default: "C" } }], - }, - choices: { C: { value: "C", count: 1, percentage: 100, others: [] } }, - type: "multipleChoiceSingle", - selectionCount: 0, - } as any; - render( - - ); - expect(screen.getByTestId("header")).toBeDefined(); - const btn = screen.getByText("1 - C"); - await userEvent.click(btn); - expect(setFilter).toHaveBeenCalledWith( - "q", - "H", - "multipleChoiceSingle", - "environments.surveys.summary.includes_either", - ["C"] - ); - }); - - test("renders others and load more for link", async () => { - const setFilter = vi.fn(); - const others = Array.from({ length: 12 }, (_, i) => ({ - value: `O${i}`, - contact: { id: `id${i}` }, - contactAttributes: {}, - })); - const q = { - question: { - id: "q2", - headline: "H2", - type: "multipleChoiceMulti", - choices: [{ id: "c2", label: { default: "X" } }], - }, - choices: { X: { value: "X", count: 0, percentage: 0, others } }, - type: "multipleChoiceMulti", - selectionCount: 5, - } as any; - render( - - ); - expect(screen.getByText("environments.surveys.summary.other_values_found")).toBeDefined(); - expect(screen.getAllByText(/^O/)).toHaveLength(10); - await userEvent.click(screen.getByText("common.load_more")); - expect(screen.getAllByText(/^O/)).toHaveLength(12); - }); - - test("renders others with avatar for app", () => { - const setFilter = vi.fn(); - const others = [{ value: "Val", contact: { id: "uid" }, contactAttributes: {} }]; - const q = { - question: { - id: "q3", - headline: "H3", - type: "multipleChoiceMulti", - choices: [{ id: "c3", label: { default: "L" } }], - }, - choices: { L: { value: "L", count: 0, percentage: 0, others } }, - type: "multipleChoiceMulti", - selectionCount: 1, - } as any; - render( - - ); - expect(screen.getByTestId("avatar")).toBeDefined(); - expect(screen.getByText("Val")).toBeDefined(); - }); - - test("places choice without others before one with others", () => { - const setFilter = vi.fn(); - const choices = { - A: { value: "A", count: 0, percentage: 0, others: [] }, - B: { value: "B", count: 0, percentage: 0, others: [{ value: "x" }] }, - }; - render( - - ); - const btns = screen.getAllByRole("button"); - expect(btns[0]).toHaveTextContent("2 - A"); - expect(btns[1]).toHaveTextContent("1 - B"); - }); - - test("sorts by count when neither has others", () => { - const setFilter = vi.fn(); - const choices = { - X: { value: "X", count: 1, percentage: 50, others: [] }, - Y: { value: "Y", count: 2, percentage: 50, others: [] }, - }; - render( - - ); - const btns = screen.getAllByRole("button"); - expect(btns[0]).toHaveTextContent("2 - YID: other2 common.selections50%"); - expect(btns[1]).toHaveTextContent("1 - XID: other1 common.selection50%"); - }); - - test("places choice with others after one without when reversed inputs", () => { - const setFilter = vi.fn(); - const choices = { - C: { value: "C", count: 1, percentage: 0, others: [{ value: "z" }] }, - D: { value: "D", count: 1, percentage: 0, others: [] }, - }; - render( - - ); - const btns = screen.getAllByRole("button"); - expect(btns[0]).toHaveTextContent("2 - D"); - expect(btns[1]).toHaveTextContent("1 - C"); - }); - - test("multi type non-other uses includes_all", async () => { - const setFilter = vi.fn(); - const q = { - question: { - id: "q4", - headline: "H4", - type: "multipleChoiceMulti", - choices: [ - { id: "other", label: { default: "O" } }, - { id: "c4", label: { default: "C4" } }, - ], - }, - choices: { - O: { value: "O", count: 1, percentage: 10, others: [] }, - C4: { value: "C4", count: 2, percentage: 20, others: [] }, - }, - type: "multipleChoiceMulti", - selectionCount: 0, - } as any; - - render( - - ); - - const btn = screen.getByText("2 - C4"); - await userEvent.click(btn); - expect(setFilter).toHaveBeenCalledWith( - "q4", - "H4", - "multipleChoiceMulti", - "environments.surveys.summary.includes_all", - ["C4"] - ); - }); - - test("multi type other uses includes_either", async () => { - const setFilter = vi.fn(); - const q = { - question: { - id: "q5", - headline: "H5", - type: "multipleChoiceMulti", - choices: [ - { id: "other", label: { default: "O5" } }, - { id: "c5", label: { default: "C5" } }, - ], - }, - choices: { - O5: { value: "O5", count: 1, percentage: 10, others: [] }, - C5: { value: "C5", count: 0, percentage: 0, others: [] }, - }, - type: "multipleChoiceMulti", - selectionCount: 0, - } as any; - - render( - - ); - - const btn = screen.getByText("2 - O5"); - await userEvent.click(btn); - expect(setFilter).toHaveBeenCalledWith( - "q5", - "H5", - "multipleChoiceMulti", - "environments.surveys.summary.includes_either", - ["O5"] - ); - }); - - // New tests for IdBadge functionality - test("renders IdBadge when choice ID is found", () => { - const setFilter = vi.fn(); - const q = { - question: { - id: "q6", - headline: "H6", - type: "multipleChoiceSingle", - choices: [ - { id: "choice1", label: { default: "Option A" } }, - { id: "choice2", label: { default: "Option B" } }, - ], - }, - choices: { - "Option A": { value: "Option A", count: 5, percentage: 50, others: [] }, - "Option B": { value: "Option B", count: 5, percentage: 50, others: [] }, - }, - type: "multipleChoiceSingle", - selectionCount: 0, - } as any; - - render( - - ); - - const idBadges = screen.getAllByTestId("id-badge"); - expect(idBadges).toHaveLength(2); - expect(idBadges[0]).toHaveAttribute("data-id", "choice1"); - expect(idBadges[1]).toHaveAttribute("data-id", "choice2"); - expect(idBadges[0]).toHaveTextContent("ID: choice1"); - expect(idBadges[1]).toHaveTextContent("ID: choice2"); - }); - - test("getChoiceIdByValue function correctly maps values to IDs", () => { - const setFilter = vi.fn(); - const q = { - question: { - id: "q8", - headline: "H8", - type: "multipleChoiceMulti", - choices: [ - { id: "id-apple", label: { default: "Apple" } }, - { id: "id-banana", label: { default: "Banana" } }, - { id: "id-cherry", label: { default: "Cherry" } }, - ], - }, - choices: { - Apple: { value: "Apple", count: 3, percentage: 30, others: [] }, - Banana: { value: "Banana", count: 4, percentage: 40, others: [] }, - Cherry: { value: "Cherry", count: 3, percentage: 30, others: [] }, - }, - type: "multipleChoiceMulti", - selectionCount: 0, - } as any; - - render( - - ); - - const idBadges = screen.getAllByTestId("id-badge"); - expect(idBadges).toHaveLength(3); - - // Check that each badge has the correct ID - const expectedMappings = [ - { text: "Banana", id: "id-banana" }, // Highest count appears first - { text: "Apple", id: "id-apple" }, - { text: "Cherry", id: "id-cherry" }, - ]; - - expectedMappings.forEach(({ text, id }, index) => { - expect(screen.getByText(`${3 - index} - ${text}`)).toBeInTheDocument(); - expect(idBadges[index]).toHaveAttribute("data-id", id); - }); - }); - - test("handles choices with special characters in labels", () => { - const setFilter = vi.fn(); - const q = { - question: { - id: "q9", - headline: "H9", - type: "multipleChoiceSingle", - choices: [ - { id: "special-1", label: { default: "Option & Choice" } }, - { id: "special-2", label: { default: "Choice with 'quotes'" } }, - ], - }, - choices: { - "Option & Choice": { value: "Option & Choice", count: 2, percentage: 50, others: [] }, - "Choice with 'quotes'": { value: "Choice with 'quotes'", count: 2, percentage: 50, others: [] }, - }, - type: "multipleChoiceSingle", - selectionCount: 0, - } as any; - - render( - - ); - - const idBadges = screen.getAllByTestId("id-badge"); - expect(idBadges).toHaveLength(2); - expect(idBadges[0]).toHaveAttribute("data-id", "special-1"); - expect(idBadges[1]).toHaveAttribute("data-id", "special-2"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx deleted file mode 100644 index 125c4e6754..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurveyQuestionSummaryNps } from "@formbricks/types/surveys/types"; -import { NPSSummary } from "./NPSSummary"; - -vi.mock("@/modules/ui/components/progress-bar", () => ({ - ProgressBar: ({ progress, barColor }: { progress: number; barColor: string }) => ( -
    {`${progress}-${barColor}`}
    - ), - HalfCircle: ({ value }: { value: number }) =>
    {value}
    , -})); -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: () =>
    , -})); - -describe("NPSSummary", () => { - afterEach(() => { - cleanup(); - }); - - const baseQuestion = { id: "q1", headline: "Question?", type: "nps" as const }; - const summary = { - question: baseQuestion, - promoters: { count: 2, percentage: 50 }, - passives: { count: 1, percentage: 25 }, - detractors: { count: 1, percentage: 25 }, - dismissed: { count: 0, percentage: 0 }, - score: 25, - } as unknown as TSurveyQuestionSummaryNps; - const survey = {} as any; - - test("renders header, groups, ProgressBar and HalfCircle", () => { - render( {}} />); - expect(screen.getByTestId("question-summary-header")).toBeDefined(); - ["promoters", "passives", "detractors", "dismissed"].forEach((g) => - expect(screen.getByText(g)).toBeDefined() - ); - expect(screen.getAllByTestId("progress-bar")[0]).toBeDefined(); - expect(screen.getByTestId("half-circle")).toHaveTextContent("25"); - }); - - test.each([ - ["promoters", "environments.surveys.summary.includes_either", ["9", "10"]], - ["passives", "environments.surveys.summary.includes_either", ["7", "8"]], - ["detractors", "environments.surveys.summary.is_less_than", "7"], - ["dismissed", "common.skipped", undefined], - ])("clicking %s calls setFilter correctly", async (group, cmp, vals) => { - const setFilter = vi.fn(); - render(); - await userEvent.click(screen.getByText(group)); - expect(setFilter).toHaveBeenCalledWith( - baseQuestion.id, - baseQuestion.headline, - baseQuestion.type, - cmp, - vals - ); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.test.tsx deleted file mode 100644 index 4f5866387c..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.test.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionSummaryOpenText } from "@formbricks/types/surveys/types"; -import { OpenTextSummary } from "./OpenTextSummary"; - -// Mock dependencies -vi.mock("@/lib/time", () => ({ - timeSince: () => "2 hours ago", -})); - -vi.mock("@/lib/utils/contact", () => ({ - getContactIdentifier: () => "contact@example.com", -})); - -vi.mock("@/modules/analysis/utils", () => ({ - renderHyperlinkedContent: (text: string) =>
    {text}
    , -})); - -vi.mock("@/modules/ui/components/avatars", () => ({ - PersonAvatar: ({ personId }: { personId: string }) =>
    {personId}
    , -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => ( - - ), -})); - -vi.mock("@/modules/ui/components/secondary-navigation", () => ({ - SecondaryNavigation: ({ activeId, navigation }: any) => ( -
    - {navigation.map((item: any) => ( - - ))} -
    - ), -})); - -vi.mock("@/modules/ui/components/table", () => ({ - Table: ({ children }: { children: React.ReactNode }) => {children}
    , - TableHeader: ({ children }: { children: React.ReactNode }) => {children}, - TableBody: ({ children }: { children: React.ReactNode }) => {children}, - TableRow: ({ children }: { children: React.ReactNode }) => {children}, - TableHead: ({ children }: { children: React.ReactNode }) => {children}, - TableCell: ({ children, width }: { children: React.ReactNode; width?: number }) => ( - {children} - ), -})); - -vi.mock("@/modules/ee/insights/components/insights-view", () => ({ - InsightView: () =>
    , -})); - -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: ({ additionalInfo }: { additionalInfo?: React.ReactNode }) => ( -
    {additionalInfo}
    - ), -})); - -describe("OpenTextSummary", () => { - afterEach(() => { - cleanup(); - }); - - const environmentId = "env-123"; - const survey = { id: "survey-1" } as TSurvey; - const locale = "en-US"; - - test("renders response mode by default when insights not enabled", () => { - const questionSummary = { - question: { id: "q1", headline: "Open Text Question" }, - samples: [ - { - id: "response1", - value: "Sample response text", - updatedAt: new Date().toISOString(), - contact: { id: "contact1" }, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryOpenText; - - render( - - ); - - expect(screen.getByTestId("question-summary-header")).toBeInTheDocument(); - expect(screen.getByTestId("table")).toBeInTheDocument(); - expect(screen.getByTestId("person-avatar")).toHaveTextContent("contact1"); - expect(screen.getByText("contact@example.com")).toBeInTheDocument(); - expect(screen.getByTestId("hyperlinked-content")).toHaveTextContent("Sample response text"); - expect(screen.getByText("2 hours ago")).toBeInTheDocument(); - - // No secondary navigation when insights not enabled - expect(screen.queryByTestId("secondary-navigation")).not.toBeInTheDocument(); - }); - - test("renders anonymous user when no contact is provided", () => { - const questionSummary = { - question: { id: "q1", headline: "Open Text Question" }, - samples: [ - { - id: "response1", - value: "Anonymous response", - updatedAt: new Date().toISOString(), - contact: null, - contactAttributes: {}, - }, - ], - } as unknown as TSurveyQuestionSummaryOpenText; - - render( - - ); - - expect(screen.getByTestId("person-avatar")).toHaveTextContent("anonymous"); - expect(screen.getByText("common.anonymous")).toBeInTheDocument(); - }); - - test("shows load more button when there are more responses and loads more on click", async () => { - const samples = Array.from({ length: 15 }, (_, i) => ({ - id: `response${i}`, - value: `Response ${i}`, - updatedAt: new Date().toISOString(), - contact: null, - contactAttributes: {}, - })); - - const questionSummary = { - question: { id: "q1", headline: "Open Text Question" }, - samples, - } as unknown as TSurveyQuestionSummaryOpenText; - - render( - - ); - - // Initially 10 responses should be visible - expect(screen.getAllByTestId("hyperlinked-content")).toHaveLength(10); - - // "Load More" button should be visible - const loadMoreButton = screen.getByTestId("load-more-button"); - expect(loadMoreButton).toBeInTheDocument(); - - // Click "Load More" - await userEvent.click(loadMoreButton); - - // Now all 15 responses should be visible - expect(screen.getAllByTestId("hyperlinked-content")).toHaveLength(15); - - // "Load More" button should disappear - expect(screen.queryByTestId("load-more-button")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.test.tsx deleted file mode 100644 index 2392285462..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { - TSurvey, - TSurveyPictureSelectionQuestion, - TSurveyQuestionTypeEnum, -} from "@formbricks/types/surveys/types"; -import { PictureChoiceSummary } from "./PictureChoiceSummary"; - -vi.mock("@/modules/ui/components/progress-bar", () => ({ - ProgressBar: ({ progress }: { progress: number }) => ( -
    - ), -})); -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: ({ additionalInfo }: any) =>
    {additionalInfo}
    , -})); -vi.mock("@/modules/ui/components/id-badge", () => ({ - IdBadge: ({ id }: { id: string }) => ( -
    - ID: {id} -
    - ), -})); - -vi.mock("@/lib/response/utils", () => ({ - getChoiceIdByValue: (value: string, question: TSurveyPictureSelectionQuestion) => { - return question.choices?.find((choice) => choice.imageUrl === value)?.id ?? "other"; - }, -})); - -// mock next image -vi.mock("next/image", () => ({ - __esModule: true, - // eslint-disable-next-line @next/next/no-img-element - default: ({ src }: { src: string }) => , -})); - -const survey = {} as TSurvey; - -describe("PictureChoiceSummary", () => { - afterEach(() => { - cleanup(); - }); - - test("renders choices with formatted percentages and counts", () => { - const choices = [ - { id: "1", imageUrl: "img1.png", percentage: 33.3333, count: 1 }, - { id: "2", imageUrl: "img2.png", percentage: 66.6667, count: 2 }, - ]; - const questionSummary = { - choices, - question: { id: "q1", type: TSurveyQuestionTypeEnum.PictureSelection, headline: "H", allowMulti: true }, - selectionCount: 3, - } as any; - render( {}} />); - - expect(screen.getAllByRole("button")).toHaveLength(2); - expect(screen.getByText("33.33%")).toBeInTheDocument(); - expect(screen.getByText("1 common.selection")).toBeInTheDocument(); - expect(screen.getByText("2 common.selections")).toBeInTheDocument(); - expect(screen.getAllByTestId("progress-bar")).toHaveLength(2); - }); - - test("calls setFilter with correct args on click", async () => { - const choices = [{ id: "1", imageUrl: "img1.png", percentage: 25, count: 10 }]; - const questionSummary = { - choices, - question: { - id: "q1", - type: TSurveyQuestionTypeEnum.PictureSelection, - headline: "H1", - allowMulti: true, - }, - selectionCount: 10, - } as any; - const setFilter = vi.fn(); - const user = userEvent.setup(); - render(); - - await user.click(screen.getByRole("button")); - expect(setFilter).toHaveBeenCalledWith( - "q1", - "H1", - TSurveyQuestionTypeEnum.PictureSelection, - "environments.surveys.summary.includes_all", - ["environments.surveys.edit.picture_idx"] - ); - }); - - test("hides additionalInfo when allowMulti is false", () => { - const choices = [{ id: "1", imageUrl: "img1.png", percentage: 50, count: 5 }]; - const questionSummary = { - choices, - question: { - id: "q1", - type: TSurveyQuestionTypeEnum.PictureSelection, - headline: "H2", - allowMulti: false, - }, - selectionCount: 5, - } as any; - render( {}} />); - - expect(screen.getByTestId("header")).toBeEmptyDOMElement(); - }); - - // New tests for IdBadge functionality - test("renders IdBadge when choice ID is found via imageUrl", () => { - const choices = [ - { id: "choice1", imageUrl: "https://example.com/img1.png", percentage: 50, count: 5 }, - { id: "choice2", imageUrl: "https://example.com/img2.png", percentage: 50, count: 5 }, - ]; - const questionSummary = { - choices, - question: { - id: "q2", - type: TSurveyQuestionTypeEnum.PictureSelection, - headline: "Picture Question", - allowMulti: true, - choices: [ - { id: "pic-choice-1", imageUrl: "https://example.com/img1.png" }, - { id: "pic-choice-2", imageUrl: "https://example.com/img2.png" }, - ], - }, - selectionCount: 10, - } as any; - - render( {}} />); - - const idBadges = screen.getAllByTestId("id-badge"); - expect(idBadges).toHaveLength(2); - expect(idBadges[0]).toHaveAttribute("data-id", "pic-choice-1"); - expect(idBadges[1]).toHaveAttribute("data-id", "pic-choice-2"); - expect(idBadges[0]).toHaveTextContent("ID: pic-choice-1"); - expect(idBadges[1]).toHaveTextContent("ID: pic-choice-2"); - }); - - test("getChoiceIdByValue function correctly maps imageUrl to choice ID", () => { - const choices = [ - { id: "choice1", imageUrl: "https://cdn.example.com/photo1.jpg", percentage: 33.33, count: 2 }, - { id: "choice2", imageUrl: "https://cdn.example.com/photo2.jpg", percentage: 33.33, count: 2 }, - { id: "choice3", imageUrl: "https://cdn.example.com/photo3.jpg", percentage: 33.33, count: 2 }, - ]; - const questionSummary = { - choices, - question: { - id: "q4", - type: TSurveyQuestionTypeEnum.PictureSelection, - headline: "Photo Selection", - allowMulti: true, - choices: [ - { id: "photo-a", imageUrl: "https://cdn.example.com/photo1.jpg" }, - { id: "photo-b", imageUrl: "https://cdn.example.com/photo2.jpg" }, - { id: "photo-c", imageUrl: "https://cdn.example.com/photo3.jpg" }, - ], - }, - selectionCount: 6, - } as any; - - render( {}} />); - - const idBadges = screen.getAllByTestId("id-badge"); - expect(idBadges).toHaveLength(3); - expect(idBadges[0]).toHaveAttribute("data-id", "photo-a"); - expect(idBadges[1]).toHaveAttribute("data-id", "photo-b"); - expect(idBadges[2]).toHaveAttribute("data-id", "photo-c"); - - // Verify the images are also rendered correctly - const images = screen.getAllByRole("img"); - expect(images).toHaveLength(3); - expect(images[0]).toHaveAttribute("src", "https://cdn.example.com/photo1.jpg"); - expect(images[1]).toHaveAttribute("src", "https://cdn.example.com/photo2.jpg"); - expect(images[2]).toHaveAttribute("src", "https://cdn.example.com/photo3.jpg"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader.test.tsx deleted file mode 100644 index 0a002be0ba..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader.test.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionSummary, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; - -// Mock dependencies -vi.mock("@/lib/utils/recall", () => ({ - recallToHeadline: () => ({ default: "Recalled Headline" }), -})); - -vi.mock("@/modules/survey/editor/lib/utils", () => ({ - formatTextWithSlashes: (text: string) => {text}, -})); - -vi.mock("@/modules/survey/lib/questions", () => ({ - getQuestionTypes: () => [ - { - id: "openText", - label: "Open Text", - icon: () =>
    Icon
    , - }, - { - id: "multipleChoice", - label: "Multiple Choice", - icon: () =>
    Icon
    , - }, - ], -})); - -vi.mock("@/modules/ui/components/id-badge", () => ({ - IdBadge: ({ label, id }: { label: string; id: string }) => ( -
    - {label}: {id} -
    - ), -})); - -// Mock InboxIcon -vi.mock("lucide-react", () => ({ - InboxIcon: () =>
    , -})); - -describe("QuestionSummaryHeader", () => { - afterEach(() => { - cleanup(); - }); - - const survey = {} as TSurvey; - - test("renders header with question headline and type", () => { - const questionSummary = { - question: { - id: "q1", - headline: { default: "Test Question" }, - type: "openText" as TSurveyQuestionTypeEnum, - required: true, - }, - responseCount: 42, - } as unknown as TSurveyQuestionSummary; - - render(); - - expect(screen.getByTestId("formatted-headline")).toHaveTextContent("Recalled Headline"); - - // Look for text content with a more specific approach - const questionTypeElement = screen.getByText((content) => { - return content.includes("Open Text") && !content.includes("common.question_id"); - }); - expect(questionTypeElement).toBeInTheDocument(); - - // Check for responses text specifically - expect( - screen.getByText((content) => { - return content.includes("42") && content.includes("common.responses"); - }) - ).toBeInTheDocument(); - - expect(screen.getByTestId("question-icon")).toBeInTheDocument(); - expect(screen.getByTestId("id-badge")).toHaveTextContent("common.question_id: q1"); - expect(screen.queryByText("environments.surveys.edit.optional")).not.toBeInTheDocument(); - }); - - test("shows 'optional' tag when question is not required", () => { - const questionSummary = { - question: { - id: "q2", - headline: { default: "Optional Question" }, - type: "multipleChoice" as TSurveyQuestionTypeEnum, - required: false, - }, - responseCount: 10, - } as unknown as TSurveyQuestionSummary; - - render(); - - expect(screen.getByText("environments.surveys.edit.optional")).toBeInTheDocument(); - }); - - test("hides response count when showResponses is false", () => { - const questionSummary = { - question: { - id: "q3", - headline: { default: "No Response Count Question" }, - type: "openText" as TSurveyQuestionTypeEnum, - required: true, - }, - responseCount: 15, - } as unknown as TSurveyQuestionSummary; - - render(); - - expect( - screen.queryByText((content) => content.includes("15") && content.includes("common.responses")) - ).not.toBeInTheDocument(); - }); - - test("shows unknown question type for unrecognized type", () => { - const questionSummary = { - question: { - id: "q4", - headline: { default: "Unknown Type Question" }, - type: "unknownType" as TSurveyQuestionTypeEnum, - required: true, - }, - responseCount: 5, - } as unknown as TSurveyQuestionSummary; - - render(); - - // Look for text in the question type element specifically - const unknownTypeElement = screen.getByText((content) => { - return ( - content.includes("environments.surveys.summary.unknown_question_type") && - !content.includes("common.question_id") - ); - }); - expect(unknownTypeElement).toBeInTheDocument(); - }); - - test("renders additional info when provided", () => { - const questionSummary = { - question: { - id: "q5", - headline: { default: "With Additional Info" }, - type: "openText" as TSurveyQuestionTypeEnum, - required: true, - }, - responseCount: 20, - } as unknown as TSurveyQuestionSummary; - - const additionalInfo =
    Extra Information
    ; - - render( - - ); - - expect(screen.getByTestId("additional-info")).toBeInTheDocument(); - expect(screen.getByText("Extra Information")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RankingSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RankingSummary.test.tsx deleted file mode 100644 index a93c27c15e..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RankingSummary.test.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionSummaryRanking } from "@formbricks/types/surveys/types"; -import { RankingSummary } from "./RankingSummary"; - -// Mock dependencies -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: () =>
    , -})); - -vi.mock("../lib/utils", () => ({ - convertFloatToNDecimal: (value: number) => value.toFixed(2), -})); - -vi.mock("@/modules/ui/components/id-badge", () => ({ - IdBadge: ({ id }: { id: string }) => ( -
    - ID: {id} -
    - ), -})); - -describe("RankingSummary", () => { - afterEach(() => { - cleanup(); - }); - - const survey = {} as TSurvey; - - test("renders ranking results in correct order", () => { - const questionSummary = { - question: { - id: "q1", - headline: "Rank the following", - choices: [ - { id: "choice1", label: { default: "Option A" } }, - { id: "choice2", label: { default: "Option B" } }, - { id: "choice3", label: { default: "Option C" } }, - ], - }, - choices: { - option1: { value: "Option A", avgRanking: 1.5, others: [] }, - option2: { value: "Option B", avgRanking: 2.3, others: [] }, - option3: { value: "Option C", avgRanking: 1.2, others: [] }, - }, - } as unknown as TSurveyQuestionSummaryRanking; - - render(); - - expect(screen.getByTestId("question-summary-header")).toBeInTheDocument(); - - // Check order: should be sorted by avgRanking (ascending) - const options = screen.getAllByText(/Option [A-C]/); - expect(options[0]).toHaveTextContent("Option C"); // 1.2 (lowest avgRanking first) - expect(options[1]).toHaveTextContent("Option A"); // 1.5 - expect(options[2]).toHaveTextContent("Option B"); // 2.3 - - // Check rankings are displayed - expect(screen.getByText("#1")).toBeInTheDocument(); - expect(screen.getByText("#2")).toBeInTheDocument(); - expect(screen.getByText("#3")).toBeInTheDocument(); - - // Check average values are displayed - expect(screen.getByText("#1.20")).toBeInTheDocument(); - expect(screen.getByText("#1.50")).toBeInTheDocument(); - expect(screen.getByText("#2.30")).toBeInTheDocument(); - }); - - test("doesn't show 'User' column for link survey type", () => { - const questionSummary = { - question: { - id: "q1", - headline: "Rank the following", - choices: [{ id: "choice1", label: { default: "Option A" } }], - }, - choices: { - option1: { - value: "Option A", - avgRanking: 1.0, - others: [{ value: "Other value", count: 1 }], - }, - }, - } as unknown as TSurveyQuestionSummaryRanking; - - render(); - - expect(screen.queryByText("common.user")).not.toBeInTheDocument(); - }); - - // New tests for IdBadge functionality - test("renders IdBadge when choice ID is found via label", () => { - const questionSummary = { - question: { - id: "q2", - headline: "Rank these options", - choices: [ - { id: "rank-choice-1", label: { default: "First Option" } }, - { id: "rank-choice-2", label: { default: "Second Option" } }, - { id: "rank-choice-3", label: { default: "Third Option" } }, - ], - }, - choices: { - option1: { value: "First Option", avgRanking: 1.5, others: [] }, - option2: { value: "Second Option", avgRanking: 2.1, others: [] }, - option3: { value: "Third Option", avgRanking: 2.8, others: [] }, - }, - } as unknown as TSurveyQuestionSummaryRanking; - - render(); - - const idBadges = screen.getAllByTestId("id-badge"); - expect(idBadges).toHaveLength(3); - expect(idBadges[0]).toHaveAttribute("data-id", "rank-choice-1"); - expect(idBadges[1]).toHaveAttribute("data-id", "rank-choice-2"); - expect(idBadges[2]).toHaveAttribute("data-id", "rank-choice-3"); - expect(idBadges[0]).toHaveTextContent("ID: rank-choice-1"); - expect(idBadges[1]).toHaveTextContent("ID: rank-choice-2"); - expect(idBadges[2]).toHaveTextContent("ID: rank-choice-3"); - }); - - test("getChoiceIdByValue function correctly maps ranking values to choice IDs", () => { - const questionSummary = { - question: { - id: "q4", - headline: "Rate importance", - choices: [ - { id: "importance-high", label: { default: "Very Important" } }, - { id: "importance-medium", label: { default: "Somewhat Important" } }, - { id: "importance-low", label: { default: "Not Important" } }, - ], - }, - choices: { - option1: { value: "Very Important", avgRanking: 1.2, others: [] }, - option2: { value: "Somewhat Important", avgRanking: 2.0, others: [] }, - option3: { value: "Not Important", avgRanking: 2.8, others: [] }, - }, - } as unknown as TSurveyQuestionSummaryRanking; - - render(); - - const idBadges = screen.getAllByTestId("id-badge"); - expect(idBadges).toHaveLength(3); - - // Should be ordered by avgRanking (ascending) - expect(screen.getByText("Very Important")).toBeInTheDocument(); // avgRanking: 1.2 - expect(screen.getByText("Somewhat Important")).toBeInTheDocument(); // avgRanking: 2.0 - expect(screen.getByText("Not Important")).toBeInTheDocument(); // avgRanking: 2.8 - - expect(idBadges[0]).toHaveAttribute("data-id", "importance-high"); - expect(idBadges[1]).toHaveAttribute("data-id", "importance-medium"); - expect(idBadges[2]).toHaveAttribute("data-id", "importance-low"); - }); - - test("handles mixed choices with and without matching IDs", () => { - const questionSummary = { - question: { - id: "q5", - headline: "Mixed options", - choices: [ - { id: "valid-choice-1", label: { default: "Valid Option" } }, - { id: "valid-choice-2", label: { default: "Another Valid Option" } }, - ], - }, - choices: { - option1: { value: "Valid Option", avgRanking: 1.5, others: [] }, - option2: { value: "Unknown Option", avgRanking: 2.0, others: [] }, - option3: { value: "Another Valid Option", avgRanking: 2.5, others: [] }, - }, - } as unknown as TSurveyQuestionSummaryRanking; - - render(); - - const idBadges = screen.getAllByTestId("id-badge"); - expect(idBadges).toHaveLength(3); // Only 2 out of 3 should have badges - - // Check that all options are still displayed - expect(screen.getByText("Valid Option")).toBeInTheDocument(); - expect(screen.getByText("Unknown Option")).toBeInTheDocument(); - expect(screen.getByText("Another Valid Option")).toBeInTheDocument(); - - // Check that only the valid choices have badges - expect(idBadges[0]).toHaveAttribute("data-id", "valid-choice-1"); - expect(idBadges[1]).toHaveAttribute("data-id", "other"); - expect(idBadges[2]).toHaveAttribute("data-id", "valid-choice-2"); - }); - - test("handles special characters in choice labels", () => { - const questionSummary = { - question: { - id: "q6", - headline: "Special characters test", - choices: [ - { id: "special-1", label: { default: "Option with 'quotes'" } }, - { id: "special-2", label: { default: "Option & Ampersand" } }, - ], - }, - choices: { - option1: { value: "Option with 'quotes'", avgRanking: 1.0, others: [] }, - option2: { value: "Option & Ampersand", avgRanking: 2.0, others: [] }, - }, - } as unknown as TSurveyQuestionSummaryRanking; - - render(); - - const idBadges = screen.getAllByTestId("id-badge"); - expect(idBadges).toHaveLength(2); - expect(idBadges[0]).toHaveAttribute("data-id", "special-1"); - expect(idBadges[1]).toHaveAttribute("data-id", "special-2"); - - expect(screen.getByText("Option with 'quotes'")).toBeInTheDocument(); - expect(screen.getByText("Option & Ampersand")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.test.tsx deleted file mode 100644 index da1e77641c..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurveyQuestionSummaryRating } from "@formbricks/types/surveys/types"; -import { RatingSummary } from "./RatingSummary"; - -vi.mock("./QuestionSummaryHeader", () => ({ - QuestionSummaryHeader: ({ additionalInfo }: any) =>
    {additionalInfo}
    , -})); - -describe("RatingSummary", () => { - afterEach(() => { - cleanup(); - }); - - test("renders overall average and choices", () => { - const questionSummary = { - question: { - id: "q1", - scale: "star", - headline: "Headline", - type: "rating", - range: [1, 5], - isColorCodingEnabled: false, - }, - average: 3.1415, - choices: [ - { rating: 1, percentage: 50, count: 2 }, - { rating: 2, percentage: 50, count: 3 }, - ], - dismissed: { count: 0 }, - } as unknown as TSurveyQuestionSummaryRating; - const survey = {}; - const setFilter = vi.fn(); - render(); - expect(screen.getByText("environments.surveys.summary.overall: 3.14")).toBeDefined(); - expect(screen.getAllByRole("button")).toHaveLength(2); - }); - - test("clicking a choice calls setFilter with correct args", async () => { - const questionSummary = { - question: { - id: "q1", - scale: "number", - headline: "Headline", - type: "rating", - range: [1, 5], - isColorCodingEnabled: false, - }, - average: 2, - choices: [{ rating: 3, percentage: 100, count: 1 }], - dismissed: { count: 0 }, - } as unknown as TSurveyQuestionSummaryRating; - const survey = {}; - const setFilter = vi.fn(); - render(); - await userEvent.click(screen.getByRole("button")); - expect(setFilter).toHaveBeenCalledWith( - "q1", - "Headline", - "rating", - "environments.surveys.summary.is_equal_to", - "3" - ); - }); - - test("renders dismissed section when dismissed count > 0", () => { - const questionSummary = { - question: { - id: "q1", - scale: "smiley", - headline: "Headline", - type: "rating", - range: [1, 5], - isColorCodingEnabled: false, - }, - average: 4, - choices: [], - dismissed: { count: 1 }, - } as unknown as TSurveyQuestionSummaryRating; - const survey = {}; - const setFilter = vi.fn(); - render(); - expect(screen.getByText("common.dismissed")).toBeDefined(); - expect(screen.getByText("1 common.response")).toBeDefined(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop.test.tsx deleted file mode 100644 index e3e7f8c3dc..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import ScrollToTop from "./ScrollToTop"; - -const containerId = "test-container"; - -describe("ScrollToTop", () => { - let mockContainer: HTMLElement; - - beforeEach(() => { - mockContainer = document.createElement("div"); - mockContainer.id = containerId; - mockContainer.scrollTop = 0; - mockContainer.scrollTo = vi.fn(); - mockContainer.addEventListener = vi.fn(); - mockContainer.removeEventListener = vi.fn(); - vi.spyOn(document, "getElementById").mockReturnValue(mockContainer); - }); - - afterEach(() => { - cleanup(); - vi.restoreAllMocks(); - }); - - test("renders hidden initially", () => { - render(); - const button = screen.getByRole("button"); - expect(button).toHaveClass("opacity-0"); - }); - - test("calls scrollTo on button click", async () => { - render(); - const button = screen.getByRole("button"); - - // Make button visible - mockContainer.scrollTop = 301; - const scrollEvent = new Event("scroll"); - mockContainer.dispatchEvent(scrollEvent); - - await userEvent.click(button); - expect(mockContainer.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: "smooth" }); - }); - - test("does nothing if container is not found", () => { - vi.spyOn(document, "getElementById").mockReturnValue(null); - render(); - const button = screen.getByRole("button"); - expect(button).toHaveClass("opacity-0"); // Stays hidden - - // Try to simulate scroll (though no listener would be attached) - fireEvent.scroll(window, { target: { scrollY: 400 } }); - expect(button).toHaveClass("opacity-0"); - - // Try to click - userEvent.click(button); - // No error should occur, and scrollTo should not be called on a null element - }); - - test("removes event listener on unmount", () => { - const { unmount } = render(); - expect(mockContainer.addEventListener).toHaveBeenCalledWith("scroll", expect.any(Function)); - - unmount(); - expect(mockContainer.removeEventListener).toHaveBeenCalledWith("scroll", expect.any(Function)); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.test.tsx deleted file mode 100644 index 911b6ef3af..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.test.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import { useSearchParams } from "next/navigation"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TLanguage } from "@formbricks/types/project"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { SuccessMessage } from "./SuccessMessage"; - -// Mock Confetti -vi.mock("@/modules/ui/components/confetti", () => ({ - Confetti: vi.fn(() =>
    ), -})); - -// Mock useSearchParams from next/navigation -vi.mock("next/navigation", () => ({ - useSearchParams: vi.fn(), - usePathname: vi.fn(() => "/"), // Default mock for usePathname if ever needed by underlying logic - useRouter: vi.fn(() => ({ push: vi.fn() })), // Default mock for useRouter -})); - -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - }, -})); - -const mockReplaceState = vi.fn(); - -describe("SuccessMessage", () => { - let mockUrlSearchParamsGet: ReturnType; - - const mockEnvironmentBase = { - id: "env1", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - appSetupCompleted: false, - } as unknown as TEnvironment; - - const mockSurveyBase = { - id: "survey1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Survey", - type: "app", - environmentId: "env1", - status: "draft", - questions: [], - displayOption: "displayOnce", - recontactDays: null, - autoClose: null, - delay: 0, - autoComplete: null, - welcomeCard: { - enabled: false, - headline: { default: "" }, - html: { default: "" }, - } as unknown as TSurvey["welcomeCard"], - triggers: [], - languages: [ - { - default: true, - enabled: true, - language: { id: "lang1", code: "en", alias: null } as unknown as TLanguage, - }, - ], - segment: null, - singleUse: null, - styling: null, - surveyClosedMessage: null, - hiddenFields: { enabled: false, fieldIds: [] }, - variables: [], - displayPercentage: null, - } as unknown as TSurvey; - - beforeEach(() => { - vi.clearAllMocks(); // Clears mock calls, instances, contexts and results - mockUrlSearchParamsGet = vi.fn(); - vi.mocked(useSearchParams).mockReturnValue({ - get: mockUrlSearchParamsGet, - } as any); - - Object.defineProperty(window, "location", { - value: new URL("http://localhost/somepath"), - writable: true, - }); - - Object.defineProperty(window, "history", { - value: { - replaceState: mockReplaceState, - pushState: vi.fn(), - go: vi.fn(), - }, - writable: true, - }); - mockReplaceState.mockClear(); // Ensure replaceState mock is clean for each test - }); - - afterEach(() => { - cleanup(); - }); - - test("should show 'almost_there' toast and confetti for app survey with widget not setup when success param is present", async () => { - mockUrlSearchParamsGet.mockImplementation((param) => (param === "success" ? "true" : null)); - const environment: TEnvironment = { ...mockEnvironmentBase, appSetupCompleted: false }; - const survey: TSurvey = { ...mockSurveyBase, type: "app" }; - - render(); - - await waitFor(() => { - expect(screen.getByTestId("confetti-mock")).toBeInTheDocument(); - }); - - expect(toast.success).toHaveBeenCalledWith("environments.surveys.summary.almost_there", { - id: "survey-publish-success-toast", - icon: "🤏", - duration: 5000, - position: "bottom-right", - }); - - expect(mockReplaceState).toHaveBeenCalledWith({}, "", "http://localhost/somepath"); - }); - - test("should show 'congrats' toast and confetti for app survey with widget setup when success param is present", async () => { - mockUrlSearchParamsGet.mockImplementation((param) => (param === "success" ? "true" : null)); - const environment: TEnvironment = { ...mockEnvironmentBase, appSetupCompleted: true }; - const survey: TSurvey = { ...mockSurveyBase, type: "app" }; - - render(); - - await waitFor(() => { - expect(screen.getByTestId("confetti-mock")).toBeInTheDocument(); - }); - - expect(toast.success).toHaveBeenCalledWith("environments.surveys.summary.congrats", { - id: "survey-publish-success-toast", - icon: "🎉", - duration: 5000, - position: "bottom-right", - }); - expect(mockReplaceState).toHaveBeenCalledWith({}, "", "http://localhost/somepath"); - }); - - test("should show 'congrats' toast, confetti, and update URL for link survey when success param is present", async () => { - mockUrlSearchParamsGet.mockImplementation((param) => (param === "success" ? "true" : null)); - const environment: TEnvironment = { ...mockEnvironmentBase }; - const survey: TSurvey = { ...mockSurveyBase, type: "link" }; - - Object.defineProperty(window, "location", { - value: new URL("http://localhost/somepath?success=true"), // initial URL with success - writable: true, - }); - - render(); - - await waitFor(() => { - expect(screen.getByTestId("confetti-mock")).toBeInTheDocument(); - }); - - expect(toast.success).toHaveBeenCalledWith("environments.surveys.summary.congrats", { - id: "survey-publish-success-toast", - icon: "🎉", - duration: 5000, - position: "bottom-right", - }); - expect(mockReplaceState).toHaveBeenCalledWith({}, "", "http://localhost/somepath?share=true"); - }); - - test("should not show confetti or toast if success param is not present", () => { - mockUrlSearchParamsGet.mockImplementation((param) => null); - const environment: TEnvironment = { ...mockEnvironmentBase }; - const survey: TSurvey = { ...mockSurveyBase, type: "app" }; - - render(); - - expect(screen.queryByTestId("confetti-mock")).not.toBeInTheDocument(); - expect(toast.success).not.toHaveBeenCalled(); - expect(mockReplaceState).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.test.tsx deleted file mode 100644 index ec67041960..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.test.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionTypeEnum, TSurveySummary } from "@formbricks/types/surveys/types"; -import { SummaryDropOffs } from "./SummaryDropOffs"; - -// Mock dependencies -vi.mock("@/lib/utils/recall", () => ({ - recallToHeadline: () => ({ default: "Recalled Question" }), -})); - -vi.mock("@/modules/survey/editor/lib/utils", () => ({ - formatTextWithSlashes: (text) => {text}, -})); - -vi.mock("@/modules/survey/lib/questions", () => ({ - getQuestionIcon: () => () =>
    , -})); - -vi.mock("@/modules/ui/components/tooltip", () => ({ - TooltipProvider: ({ children }: { children: React.ReactNode }) =>
    {children}
    , - Tooltip: ({ children }: { children: React.ReactNode }) =>
    {children}
    , - TooltipTrigger: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - TooltipContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("lucide-react", () => ({ - TimerIcon: () =>
    , -})); - -describe("SummaryDropOffs", () => { - afterEach(() => { - cleanup(); - }); - - const mockSurvey = {} as TSurvey; - const mockDropOff: TSurveySummary["dropOff"] = [ - { - questionId: "q1", - headline: "First Question", - questionType: TSurveyQuestionTypeEnum.OpenText, - ttc: 15000, // 15 seconds - impressions: 100, - dropOffCount: 20, - dropOffPercentage: 20, - }, - { - questionId: "q2", - headline: "Second Question", - questionType: TSurveyQuestionTypeEnum.MultipleChoiceMulti, - ttc: 30000, // 30 seconds - impressions: 80, - dropOffCount: 15, - dropOffPercentage: 18.75, - }, - { - questionId: "q3", - headline: "Third Question", - questionType: TSurveyQuestionTypeEnum.Rating, - ttc: 0, // No time data - impressions: 65, - dropOffCount: 10, - dropOffPercentage: 15.38, - }, - ]; - - test("renders header row with correct columns", () => { - render(); - - // Check header - expect(screen.getByText("common.questions")).toBeInTheDocument(); - expect(screen.getByTestId("tooltip-trigger")).toBeInTheDocument(); - expect(screen.getByTestId("timer-icon")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.impressions")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.drop_offs")).toBeInTheDocument(); - }); - - test("renders tooltip with correct content", () => { - render(); - - expect(screen.getByTestId("tooltip-content")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.ttc_tooltip")).toBeInTheDocument(); - }); - - test("renders all drop-off items with correct data", () => { - render(); - - // There should be 3 rows of data (one for each question) - expect(screen.getAllByTestId("question-icon")).toHaveLength(3); - expect(screen.getAllByTestId("formatted-text")).toHaveLength(3); - - // Check time to complete values - expect(screen.getByText("15.00s")).toBeInTheDocument(); // 15000ms converted to seconds - expect(screen.getByText("30.00s")).toBeInTheDocument(); // 30000ms converted to seconds - expect(screen.getByText("N/A")).toBeInTheDocument(); // 0ms shown as N/A - - // Check impressions values - expect(screen.getByText("100")).toBeInTheDocument(); - expect(screen.getByText("80")).toBeInTheDocument(); - expect(screen.getByText("65")).toBeInTheDocument(); - - // Check drop-off counts and percentages - expect(screen.getByText("20")).toBeInTheDocument(); - expect(screen.getByText("15")).toBeInTheDocument(); - expect(screen.getByText("10")).toBeInTheDocument(); - - // Check percentage values - const percentageElements = screen.getAllByText(/\d+%/); - expect(percentageElements).toHaveLength(3); - expect(percentageElements[0]).toHaveTextContent("20%"); - expect(percentageElements[1]).toHaveTextContent("19%"); - expect(percentageElements[2]).toHaveTextContent("15%"); - }); - - test("renders empty state when dropOff array is empty", () => { - render(); - - // Header should still be visible - expect(screen.getByText("common.questions")).toBeInTheDocument(); - - // But no question icons - expect(screen.queryByTestId("question-icon")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.test.tsx deleted file mode 100644 index 8138eaec7f..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.test.tsx +++ /dev/null @@ -1,461 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { toast } from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { - TI18nString, - TSurvey, - TSurveyQuestionTypeEnum, - TSurveySummary, -} from "@formbricks/types/surveys/types"; -import { TUserLocale } from "@formbricks/types/user"; -import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; -import { MultipleChoiceSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary"; -import { constructToastMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; -import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox"; -import { SummaryList } from "./SummaryList"; - -// Mock child components -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys", - () => ({ - EmptyAppSurveys: vi.fn(() =>
    Mocked EmptyAppSurveys
    ), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary", - () => ({ - CTASummary: vi.fn(({ questionSummary }) =>
    Mocked CTASummary: {questionSummary.question.id}
    ), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary", - () => ({ - CalSummary: vi.fn(({ questionSummary }) =>
    Mocked CalSummary: {questionSummary.question.id}
    ), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary", - () => ({ - ConsentSummary: vi.fn(({ questionSummary }) => ( -
    Mocked ConsentSummary: {questionSummary.question.id}
    - )), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary", - () => ({ - ContactInfoSummary: vi.fn(({ questionSummary }) => ( -
    Mocked ContactInfoSummary: {questionSummary.question.id}
    - )), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary", - () => ({ - DateQuestionSummary: vi.fn(({ questionSummary }) => ( -
    Mocked DateQuestionSummary: {questionSummary.question.id}
    - )), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary", - () => ({ - FileUploadSummary: vi.fn(({ questionSummary }) => ( -
    Mocked FileUploadSummary: {questionSummary.question.id}
    - )), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary", - () => ({ - HiddenFieldsSummary: vi.fn(({ questionSummary }) => ( -
    Mocked HiddenFieldsSummary: {questionSummary.id}
    - )), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary", - () => ({ - MatrixQuestionSummary: vi.fn(({ questionSummary }) => ( -
    Mocked MatrixQuestionSummary: {questionSummary.question.id}
    - )), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary", - () => ({ - MultipleChoiceSummary: vi.fn(({ questionSummary }) => ( -
    Mocked MultipleChoiceSummary: {questionSummary.question.id}
    - )), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary", - () => ({ - NPSSummary: vi.fn(({ questionSummary }) =>
    Mocked NPSSummary: {questionSummary.question.id}
    ), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary", - () => ({ - OpenTextSummary: vi.fn(({ questionSummary }) => ( -
    Mocked OpenTextSummary: {questionSummary.question.id}
    - )), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary", - () => ({ - PictureChoiceSummary: vi.fn(({ questionSummary }) => ( -
    Mocked PictureChoiceSummary: {questionSummary.question.id}
    - )), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RankingSummary", - () => ({ - RankingSummary: vi.fn(({ questionSummary }) => ( -
    Mocked RankingSummary: {questionSummary.question.id}
    - )), - }) -); -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary", - () => ({ - RatingSummary: vi.fn(({ questionSummary }) => ( -
    Mocked RatingSummary: {questionSummary.question.id}
    - )), - }) -); -vi.mock("./AddressSummary", () => ({ - AddressSummary: vi.fn(({ questionSummary }) => ( -
    Mocked AddressSummary: {questionSummary.question.id}
    - )), -})); - -// Mock hooks and utils -vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({ - useResponseFilter: vi.fn(), -})); -vi.mock("@/lib/i18n/utils", () => ({ - getLocalizedValue: vi.fn((label, _) => (typeof label === "string" ? label : label.default)), -})); -vi.mock("@/modules/ui/components/empty-space-filler", () => ({ - EmptySpaceFiller: vi.fn(() =>
    Mocked EmptySpaceFiller
    ), -})); -vi.mock("@/modules/ui/components/skeleton-loader", () => ({ - SkeletonLoader: vi.fn(() =>
    Mocked SkeletonLoader
    ), -})); -vi.mock("react-hot-toast", () => ({ - // This mock setup is for a named export 'toast' - toast: { - success: vi.fn(), - }, -})); -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils", () => ({ - constructToastMessage: vi.fn(), -})); - -const mockEnvironment = { - id: "env_test_id", - type: "production", - createdAt: new Date(), - updatedAt: new Date(), - appSetupCompleted: true, -} as unknown as TEnvironment; - -const mockSurvey = { - id: "survey_test_id", - name: "Test Survey", - type: "app", - environmentId: "env_test_id", - status: "inProgress", - questions: [], - hiddenFields: { enabled: false }, - displayOption: "displayOnce", - autoClose: null, - triggers: [], - languages: [], - singleUse: null, - styling: null, - surveyClosedMessage: null, - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - delay: 0, - displayPercentage: null, - recontactDays: null, - autoComplete: null, - segment: null, - variables: [], -} as unknown as TSurvey; - -const mockSelectedFilter = { filter: [], responseStatus: "all" }; -const mockSetSelectedFilter = vi.fn(); - -const defaultProps = { - summary: [] as TSurveySummary["summary"], - responseCount: 10, - environment: mockEnvironment, - survey: mockSurvey, - totalResponseCount: 20, - locale: "en" as TUserLocale, -}; - -const createMockQuestionSummary = ( - id: string, - type: TSurveyQuestionTypeEnum, - headline: string = "Test Question" -) => - ({ - question: { - id, - headline: { default: headline, en: headline }, - type, - required: false, - choices: - type === TSurveyQuestionTypeEnum.MultipleChoiceSingle || - type === TSurveyQuestionTypeEnum.MultipleChoiceMulti - ? [{ id: "choice1", label: { default: "Choice 1" } }] - : undefined, - logic: [], - }, - type, - responseCount: 5, - samples: type === TSurveyQuestionTypeEnum.OpenText ? [{ value: "sample" }] : [], - choices: - type === TSurveyQuestionTypeEnum.MultipleChoiceSingle || - type === TSurveyQuestionTypeEnum.MultipleChoiceMulti - ? [{ label: { default: "Choice 1" }, count: 5, percentage: 1 }] - : [], - dismissed: - type === TSurveyQuestionTypeEnum.MultipleChoiceSingle || - type === TSurveyQuestionTypeEnum.MultipleChoiceMulti - ? { count: 0, percentage: 0 } - : undefined, - others: - type === TSurveyQuestionTypeEnum.MultipleChoiceSingle || - type === TSurveyQuestionTypeEnum.MultipleChoiceMulti - ? [{ value: "other", count: 0, percentage: 0 }] - : [], - progress: type === TSurveyQuestionTypeEnum.NPS ? { total: 5, trend: 0.5 } : undefined, - average: type === TSurveyQuestionTypeEnum.Rating ? 3.5 : undefined, - accepted: type === TSurveyQuestionTypeEnum.Consent ? { count: 5, percentage: 1 } : undefined, - results: - type === TSurveyQuestionTypeEnum.PictureSelection - ? [{ imageUrl: "url", count: 5, percentage: 1 }] - : undefined, - files: type === TSurveyQuestionTypeEnum.FileUpload ? [{ url: "url", name: "file.pdf", size: 100 }] : [], - booked: type === TSurveyQuestionTypeEnum.Cal ? { count: 5, percentage: 1 } : undefined, - data: type === TSurveyQuestionTypeEnum.Matrix ? [{ rowLabel: "Row1", responses: {} }] : undefined, - ranking: type === TSurveyQuestionTypeEnum.Ranking ? [{ rank: 1, choiceLabel: "Choice1", count: 5 }] : [], - }) as unknown as TSurveySummary["summary"][number]; - -const createMockHiddenFieldSummary = (id: string, label: string = "Hidden Field") => - ({ - id, - type: "hiddenField", - label, - value: "some value", - count: 1, - samples: [{ personId: "person1", value: "Sample Value", updatedAt: new Date().toISOString() }], - responseCount: 1, - }) as unknown as TSurveySummary["summary"][number]; - -const typeToComponentMockNameMap: Record = { - [TSurveyQuestionTypeEnum.OpenText]: "OpenTextSummary", - [TSurveyQuestionTypeEnum.MultipleChoiceSingle]: "MultipleChoiceSummary", - [TSurveyQuestionTypeEnum.MultipleChoiceMulti]: "MultipleChoiceSummary", - [TSurveyQuestionTypeEnum.NPS]: "NPSSummary", - [TSurveyQuestionTypeEnum.CTA]: "CTASummary", - [TSurveyQuestionTypeEnum.Rating]: "RatingSummary", - [TSurveyQuestionTypeEnum.Consent]: "ConsentSummary", - [TSurveyQuestionTypeEnum.PictureSelection]: "PictureChoiceSummary", - [TSurveyQuestionTypeEnum.Date]: "DateQuestionSummary", - [TSurveyQuestionTypeEnum.FileUpload]: "FileUploadSummary", - [TSurveyQuestionTypeEnum.Cal]: "CalSummary", - [TSurveyQuestionTypeEnum.Matrix]: "MatrixQuestionSummary", - [TSurveyQuestionTypeEnum.Address]: "AddressSummary", - [TSurveyQuestionTypeEnum.Ranking]: "RankingSummary", - [TSurveyQuestionTypeEnum.ContactInfo]: "ContactInfoSummary", -}; - -describe("SummaryList", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - vi.mocked(useResponseFilter).mockReturnValue({ - selectedFilter: mockSelectedFilter, - setSelectedFilter: mockSetSelectedFilter, - resetFilter: vi.fn(), - } as any); - }); - - test("renders EmptyAppSurveys when survey type is app, responseCount is 0 and appSetupCompleted is false", () => { - const testEnv = { ...mockEnvironment, appSetupCompleted: false }; - const testSurvey = { ...mockSurvey, type: "app" as const }; - render(); - expect(screen.getByText("Mocked EmptyAppSurveys")).toBeInTheDocument(); - }); - - test("renders SkeletonLoader when summary is empty and responseCount is not 0", () => { - render(); - expect(screen.getByText("Mocked SkeletonLoader")).toBeInTheDocument(); - }); - - test("renders EmptySpaceFiller when responseCount is 0 and summary is not empty (no responses match filter)", () => { - const summaryWithItem = [createMockQuestionSummary("q1", TSurveyQuestionTypeEnum.OpenText)]; - render(); - expect(screen.getByText("Mocked EmptySpaceFiller")).toBeInTheDocument(); - }); - - test("renders EmptySpaceFiller when responseCount is 0 and totalResponseCount is 0 (no responses at all)", () => { - const summaryWithItem = [createMockQuestionSummary("q1", TSurveyQuestionTypeEnum.OpenText)]; - render(); - expect(screen.getByText("Mocked EmptySpaceFiller")).toBeInTheDocument(); - }); - - const questionTypesToTest: TSurveyQuestionTypeEnum[] = [ - TSurveyQuestionTypeEnum.OpenText, - TSurveyQuestionTypeEnum.MultipleChoiceSingle, - TSurveyQuestionTypeEnum.MultipleChoiceMulti, - TSurveyQuestionTypeEnum.NPS, - TSurveyQuestionTypeEnum.CTA, - TSurveyQuestionTypeEnum.Rating, - TSurveyQuestionTypeEnum.Consent, - TSurveyQuestionTypeEnum.PictureSelection, - TSurveyQuestionTypeEnum.Date, - TSurveyQuestionTypeEnum.FileUpload, - TSurveyQuestionTypeEnum.Cal, - TSurveyQuestionTypeEnum.Matrix, - TSurveyQuestionTypeEnum.Address, - TSurveyQuestionTypeEnum.Ranking, - TSurveyQuestionTypeEnum.ContactInfo, - ]; - - questionTypesToTest.forEach((type) => { - test(`renders ${type}Summary component`, () => { - const mockSummaryItem = createMockQuestionSummary(`q_${type}`, type); - const expectedComponentName = typeToComponentMockNameMap[type]; - render(); - expect( - screen.getByText(new RegExp(`Mocked ${expectedComponentName}:\\s*q_${type}`)) - ).toBeInTheDocument(); - }); - }); - - test("renders HiddenFieldsSummary component", () => { - const mockSummaryItem = createMockHiddenFieldSummary("hf1"); - render(); - expect(screen.getByText("Mocked HiddenFieldsSummary: hf1")).toBeInTheDocument(); - }); - - describe("setFilter function", () => { - const questionId = "q_mc_single"; - const label: TI18nString = { default: "MC Single Question" }; - const questionType = TSurveyQuestionTypeEnum.MultipleChoiceSingle; - const filterValue = "Choice 1"; - const filterComboBoxValue = "choice1_id"; - - beforeEach(() => { - // Render with a component that uses setFilter, e.g., MultipleChoiceSummary - const mockSummaryItem = createMockQuestionSummary(questionId, questionType, label.default); - render(); - }); - - const getSetFilterFn = () => { - const MultipleChoiceSummaryMock = vi.mocked(MultipleChoiceSummary); - return MultipleChoiceSummaryMock.mock.calls[0][0].setFilter; - }; - - test("adds a new filter", () => { - const setFilter = getSetFilterFn(); - vi.mocked(constructToastMessage).mockReturnValue("Custom add message"); - - setFilter(questionId, label, questionType, filterValue, filterComboBoxValue); - - expect(mockSetSelectedFilter).toHaveBeenCalledWith({ - filter: [ - { - questionType: { - id: questionId, - label: label.default, - questionType: questionType, - type: OptionsType.QUESTIONS, - }, - filterType: { - filterComboBoxValue: filterComboBoxValue, - filterValue: filterValue, - }, - }, - ], - responseStatus: "all", - }); - // Ensure vi.mocked(toast.success) refers to the spy from the named export - expect(vi.mocked(toast).success).toHaveBeenCalledWith("Custom add message", { duration: 5000 }); - expect(vi.mocked(constructToastMessage)).toHaveBeenCalledWith( - questionType, - filterValue, - mockSurvey, - questionId, - expect.any(Function), // t function - filterComboBoxValue - ); - }); - - test("updates an existing filter", () => { - const existingFilter = { - questionType: { - id: questionId, - label: label.default, - questionType: questionType, - type: OptionsType.QUESTIONS, - }, - filterType: { - filterComboBoxValue: "old_value_combo", - filterValue: "old_value", - }, - }; - vi.mocked(useResponseFilter).mockReturnValue({ - selectedFilter: { filter: [existingFilter], responseStatus: "all" }, - setSelectedFilter: mockSetSelectedFilter, - resetFilter: vi.fn(), - } as any); - // Re-render or get setFilter again as selectedFilter changed - cleanup(); - const mockSummaryItem = createMockQuestionSummary(questionId, questionType, label.default); - render(); - const setFilter = getSetFilterFn(); - - const newFilterValue = "New Choice"; - const newFilterComboBoxValue = "new_choice_id"; - setFilter(questionId, label, questionType, newFilterValue, newFilterComboBoxValue); - - expect(mockSetSelectedFilter).toHaveBeenCalledWith({ - filter: [ - { - questionType: { - id: questionId, - label: label.default, - questionType: questionType, - type: OptionsType.QUESTIONS, - }, - filterType: { - filterComboBoxValue: newFilterComboBoxValue, - filterValue: newFilterValue, - }, - }, - ], - responseStatus: "all", - }); - expect(vi.mocked(toast.success)).toHaveBeenCalledWith( - "environments.surveys.summary.filter_updated_successfully", - { - duration: 5000, - } - ); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx deleted file mode 100644 index ee5d1cfa26..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { useState } from "react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { SummaryMetadata } from "./SummaryMetadata"; - -vi.mock("lucide-react", () => ({ - ChevronDownIcon: () =>
    , - ChevronUpIcon: () =>
    , -})); -vi.mock("@/modules/ui/components/tooltip", () => ({ - TooltipProvider: ({ children }) => <>{children}, - Tooltip: ({ children }) => <>{children}, - TooltipTrigger: ({ children, onClick }) => ( - - ), - TooltipContent: ({ children }) => <>{children}, -})); - -const baseSummary = { - completedPercentage: 50, - completedResponses: 2, - displayCount: 3, - dropOffPercentage: 25, - dropOffCount: 1, - startsPercentage: 75, - totalResponses: 4, - ttcAverage: 65000, - quotasCompleted: 0, - quotasCompletedPercentage: 0, -}; - -describe("SummaryMetadata", () => { - afterEach(() => { - cleanup(); - }); - - test("renders loading skeletons when isLoading=true", () => { - const { container } = render( - {}} - surveySummary={baseSummary} - isLoading={true} - isQuotasAllowed={true} - /> - ); - - expect(container.getElementsByClassName("animate-pulse")).toHaveLength(6); - }); - - test("renders all stats and formats time correctly, toggles dropOffs icon", async () => { - const Wrapper = () => { - return ( - {}} - surveySummary={baseSummary} - isLoading={false} - isQuotasAllowed={false} - /> - ); - }; - render(); - // impressions, starts, completed, drop_offs, ttc - expect(screen.getByText("environments.surveys.summary.impressions")).toBeInTheDocument(); - expect(screen.getByText("3")).toBeInTheDocument(); - expect(screen.getByText("75%")).toBeInTheDocument(); - expect(screen.getByText("4")).toBeInTheDocument(); - expect(screen.getByText("50%")).toBeInTheDocument(); - expect(screen.getByText("2")).toBeInTheDocument(); - expect(screen.getByText("25%")).toBeInTheDocument(); - expect(screen.getByText("1")).toBeInTheDocument(); - expect(screen.getByText("1m 5.00s")).toBeInTheDocument(); - const btn = screen - .getAllByRole("button") - .find((el) => el.textContent?.includes("environments.surveys.summary.drop_offs")); - if (!btn) throw new Error("DropOffs toggle button not found"); - await userEvent.click(btn); - expect(screen.queryByTestId("up")).toBeInTheDocument(); - }); - - test("formats time correctly when < 60 seconds", () => { - const smallSummary = { ...baseSummary, ttcAverage: 5000 }; - render( - {}} - surveySummary={smallSummary} - isLoading={false} - isQuotasAllowed={false} - /> - ); - expect(screen.getByText("5.00s")).toBeInTheDocument(); - }); - - test("renders '-' for dropOffCount=0 and still toggles icon", async () => { - const zeroSummary = { ...baseSummary, dropOffCount: 0 }; - const Wrapper = () => { - return ( - {}} - surveySummary={zeroSummary} - isLoading={false} - isQuotasAllowed={false} - /> - ); - }; - render(); - expect(screen.getAllByText("-")).toHaveLength(1); - const btn = screen - .getAllByRole("button") - .find((el) => el.textContent?.includes("environments.surveys.summary.drop_offs")); - if (!btn) throw new Error("DropOffs toggle button not found"); - await userEvent.click(btn); - expect(screen.queryByTestId("up")).toBeInTheDocument(); - }); - - test("renders '-' for displayCount=0", () => { - const dispZero = { ...baseSummary, displayCount: 0 }; - render( - {}} - surveySummary={dispZero} - isLoading={false} - isQuotasAllowed={false} - /> - ); - expect(screen.getAllByText("-")).toHaveLength(1); - }); - - test("renders '-' for totalResponses=0", () => { - const totZero = { ...baseSummary, totalResponses: 0 }; - render( - {}} - surveySummary={totZero} - isLoading={false} - isQuotasAllowed={false} - /> - ); - expect(screen.getAllByText("-")).toHaveLength(1); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.test.tsx deleted file mode 100644 index 3debf1db68..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.test.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TUserLocale } from "@formbricks/types/user"; -import { SummaryPage } from "./SummaryPage"; - -// Mock actions -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions", () => ({ - getResponseCountAction: vi.fn().mockResolvedValue({ data: 42 }), - getSurveySummaryAction: vi.fn().mockResolvedValue({ - data: { - meta: { - completedPercentage: 80, - completedResponses: 40, - displayCount: 50, - dropOffPercentage: 20, - dropOffCount: 10, - startsPercentage: 100, - totalResponses: 50, - ttcAverage: 120, - quotasCompleted: 0, - quotasCompletedPercentage: 0, - }, - dropOff: [ - { - questionId: "q1", - headline: "Question 1", - questionType: "openText", - ttc: 20000, - impressions: 50, - dropOffCount: 5, - dropOffPercentage: 10, - }, - ], - summary: [ - { - question: { id: "q1", headline: "Question 1", type: "openText", required: true }, - responseCount: 45, - type: "openText", - samples: [], - }, - ], - }, - }), -})); - -// Mock components -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs", - () => ({ - SummaryDropOffs: () =>
    DropOffs Component
    , - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList", - () => ({ - SummaryList: ({ summary, responseCount }: any) => ( -
    - Response Count: {responseCount} - Summary Items: {summary.length} -
    - ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata", - () => ({ - SummaryMetadata: ({ tab, setTab, isLoading }: any) => ( -
    - Is Loading: {isLoading ? "true" : "false"} - -
    - ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop", - () => ({ - __esModule: true, - default: () =>
    Scroll To Top
    , - }) -); - -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter", () => ({ - CustomFilter: () =>
    Custom Filter
    , -})); - -// Mock context -vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({ - useResponseFilter: () => ({ - selectedFilter: { filter: [], onlyComplete: false }, - dateRange: { from: null, to: null }, - resetState: vi.fn(), - }), -})); - -// Mock hooks -vi.mock("@/lib/utils/hooks/useIntervalWhenFocused", () => ({ - useIntervalWhenFocused: vi.fn(), -})); - -vi.mock("@/lib/utils/recall", () => ({ - replaceHeadlineRecall: (survey: any) => survey, -})); - -vi.mock("next/navigation", () => ({ - useParams: () => ({}), - useSearchParams: () => ({ get: () => null }), -})); - -describe("SummaryPage", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockEnvironment = { id: "env-123" } as TEnvironment; - const mockSurvey = { - id: "survey-123", - environmentId: "env-123", - } as TSurvey; - const locale = "en-US" as TUserLocale; - - const defaultProps = { - environment: mockEnvironment, - survey: mockSurvey, - surveyId: "survey-123", - webAppUrl: "https://app.example.com", - totalResponseCount: 50, - locale, - }; - - test("renders loading state initially", () => { - render(); - - expect(screen.getByTestId("summary-metadata")).toBeInTheDocument(); - expect(screen.getByText("Is Loading: true")).toBeInTheDocument(); - }); - - test("renders summary components after loading", async () => { - render(); - - // Wait for loading to complete - await waitFor(() => { - expect(screen.getByText("Is Loading: false")).toBeInTheDocument(); - }); - - expect(screen.getByTestId("custom-filter")).toBeInTheDocument(); - expect(screen.getByTestId("scroll-to-top")).toBeInTheDocument(); - expect(screen.getByTestId("summary-list")).toBeInTheDocument(); - }); - - test("shows drop-offs component when toggled", async () => { - const user = userEvent.setup(); - render(); - - // Wait for loading to complete - await waitFor(() => { - expect(screen.getByText("Is Loading: false")).toBeInTheDocument(); - }); - - // Drop-offs should initially be hidden - expect(screen.queryByTestId("summary-drop-offs")).not.toBeInTheDocument(); - - // Toggle drop-offs - await user.click(screen.getByRole("button", { name: "Toggle Dropoffs" })); - - // Drop-offs should now be visible - expect(screen.getByTestId("summary-drop-offs")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx deleted file mode 100644 index 05ef755de8..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx +++ /dev/null @@ -1,1043 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TSegment } from "@formbricks/types/segment"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TUser } from "@formbricks/types/user"; -import { SurveyAnalysisCTA } from "./SurveyAnalysisCTA"; - -// Mock the useTranslate hook -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - if (key === "environments.surveys.summary.configure_alerts") { - return "Configure alerts"; - } - if (key === "common.preview") { - return "Preview"; - } - if (key === "common.edit") { - return "Edit"; - } - if (key === "environments.surveys.summary.share_survey") { - return "Share survey"; - } - if (key === "environments.surveys.summary.results_are_public") { - return "Results are public"; - } - if (key === "environments.surveys.survey_duplicated_successfully") { - return "Survey duplicated successfully"; - } - if (key === "environments.surveys.edit.caution_edit_duplicate") { - return "Duplicate & Edit"; - } - if (key === "environments.surveys.summary.reset_survey") { - return "Reset survey"; - } - if (key === "environments.surveys.summary.delete_all_existing_responses_and_displays") { - return "Delete all existing responses and displays"; - } - if (key === "environments.surveys.summary.reset_survey_warning") { - return "Resetting a survey removes all responses and metadata of this survey. This cannot be undone."; - } - if (key === "environments.surveys.summary.survey_reset_successfully") { - return "Survey reset successfully! 5 responses and 3 displays were deleted."; - } - return key; - }, - }), -})); - -// Mock Next.js hooks -const mockPush = vi.fn(); -const mockRefresh = vi.fn(); -const mockPathname = "/environments/test-env-id/surveys/test-survey-id/summary"; -const mockSearchParams = new URLSearchParams(); - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - push: mockPush, - refresh: mockRefresh, - }), - usePathname: () => mockPathname, - useSearchParams: () => mockSearchParams, -})); - -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -// Mock helper functions -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(() => "Error message"), -})); - -// Mock actions -vi.mock("@/modules/survey/list/actions", () => ({ - copySurveyToOtherEnvironmentAction: vi.fn(), -})); - -vi.mock("../actions", () => ({ - resetSurveyAction: vi.fn(), -})); - -// Mock the useSingleUseId hook -vi.mock("@/modules/survey/hooks/useSingleUseId", () => ({ - useSingleUseId: vi.fn(() => ({ - singleUseId: "test-single-use-id", - refreshSingleUseId: vi.fn().mockResolvedValue("test-single-use-id"), - })), -})); - -// Mock child components -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage", - () => ({ - SuccessMessage: ({ environment, survey }: any) => ( -
    - Success Message for {environment.id} - {survey.id} -
    - ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal", - () => ({ - ShareSurveyModal: ({ survey, open, setOpen, modalView, user }: any) => ( -
    - Share Survey Modal for {survey.id} - User: {user.id} - -
    - ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown", - () => ({ - SurveyStatusDropdown: ({ environment, survey }: any) => ( -
    - Status Dropdown for {environment.id} - {survey.id} -
    - ), - }) -); - -vi.mock("@/modules/survey/components/edit-public-survey-alert-dialog", () => ({ - EditPublicSurveyAlertDialog: ({ - open, - setOpen, - isLoading, - primaryButtonAction, - primaryButtonText, - secondaryButtonAction, - secondaryButtonText, - }: any) => ( -
    - - - -
    - ), -})); - -// Mock UI components -vi.mock("@/modules/ui/components/badge", () => ({ - Badge: ({ type, size, className, text }: any) => ( -
    - {text} -
    - ), -})); - -vi.mock("@/modules/ui/components/confirmation-modal", () => ({ - ConfirmationModal: ({ - open, - setOpen, - title, - body, - buttonText, - onConfirm, - buttonVariant, - buttonLoading, - }: any) => ( -
    -
    {title}
    -
    {body}
    - - -
    - ), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, className }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/iconbar", () => ({ - IconBar: ({ actions }: any) => ( -
    - {actions - .filter((action: any) => action.isVisible) - .map((action: any, index: number) => ( - - ))} -
    - ), -})); - -// Mock lucide-react icons -vi.mock("lucide-react", () => ({ - BellRing: () => , - Eye: () => , - ListRestart: () => , - SquarePenIcon: () => , -})); - -vi.mock("@/app/(app)/environments/[environmentId]/context/environment-context", () => ({ - useEnvironment: vi.fn(() => ({ - organizationId: "test-organization-id", - project: { id: "test-project-id" }, - })), -})); - -// Mock data -const mockEnvironment: TEnvironment = { - id: "test-env-id", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - projectId: "test-project-id", - appSetupCompleted: true, -}; - -const mockSurvey: TSurvey = { - id: "test-survey-id", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Survey", - type: "app", - environmentId: "test-env-id", - status: "inProgress", - displayOption: "displayOnce", - autoClose: null, - triggers: [], - - recontactDays: null, - displayLimit: null, - welcomeCard: { enabled: false, timeToFinish: false, showResponseCount: false }, - questions: [], - endings: [], - hiddenFields: { enabled: false }, - displayPercentage: null, - autoComplete: null, - - segment: null, - languages: [], - showLanguageSwitch: false, - singleUse: { enabled: false, isEncrypted: false }, - projectOverwrites: null, - surveyClosedMessage: null, - delay: 0, - isVerifyEmailEnabled: false, - createdBy: null, - variables: [], - followUps: [], - styling: null, - pin: null, - recaptcha: null, - isSingleResponsePerEmailEnabled: false, - isBackButtonHidden: false, -} as unknown as TSurvey; - -const mockUser: TUser = { - id: "test-user-id", - name: "Test User", - email: "test@example.com", - emailVerified: new Date(), - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - - role: "other", - objective: "other", - locale: "en-US", - lastLoginAt: new Date(), - isActive: true, - notificationSettings: { - alert: { - responseFinished: true, - }, - unsubscribedOrganizationIds: [], - }, -}; - -const mockSegments: TSegment[] = []; - -const defaultProps = { - survey: mockSurvey, - environment: mockEnvironment, - isReadOnly: false, - user: mockUser, - publicDomain: "https://example.com", - responseCount: 0, - displayCount: 0, - segments: mockSegments, - isContactsEnabled: true, - isFormbricksCloud: false, - isStorageConfigured: true, -}; - -describe("SurveyAnalysisCTA", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockSearchParams.delete("share"); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders share survey button", () => { - render(); - - expect(screen.getByText("Share survey")).toBeInTheDocument(); - }); - - test("renders success message component", () => { - render(); - - expect(screen.getByTestId("success-message")).toBeInTheDocument(); - }); - - test("renders survey status dropdown when app setup is completed", () => { - render(); - - expect(screen.getByTestId("survey-status-dropdown")).toBeInTheDocument(); - }); - - test("does not render survey status dropdown when read-only", () => { - render(); - - expect(screen.queryByTestId("survey-status-dropdown")).not.toBeInTheDocument(); - }); - - test("renders icon bar with correct actions", () => { - render(); - - expect(screen.getByTestId("icon-bar")).toBeInTheDocument(); - expect(screen.getByTestId("icon-bar-action-0")).toBeInTheDocument(); // Bell ring - expect(screen.getByTestId("icon-bar-action-1")).toBeInTheDocument(); // Square pen - }); - - test("shows preview icon for link surveys", () => { - const linkSurvey = { ...mockSurvey, type: "link" as const }; - render(); - - expect(screen.getByTestId("icon-bar-action-1")).toHaveAttribute("title", "Preview"); - }); - - test("opens share modal when share button is clicked", async () => { - const user = userEvent.setup(); - render(); - - await user.click(screen.getByText("Share survey")); - - expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument(); - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true"); - }); - - test("opens share modal when share param is true", () => { - mockSearchParams.set("share", "true"); - render(); - - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true"); - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-modal-view", "start"); - }); - - test("navigates to edit when edit button is clicked and no responses", async () => { - const user = userEvent.setup(); - render(); - - await user.click(screen.getByTestId("icon-bar-action-1")); - - expect(mockPush).toHaveBeenCalledWith("/environments/test-env-id/surveys/test-survey-id/edit"); - }); - - test("shows caution dialog when edit button is clicked and has responses", async () => { - const user = userEvent.setup(); - render(); - - // With responseCount > 0, the edit button should be at icon-bar-action-2 (after reset button) - await user.click(screen.getByTestId("icon-bar-action-2")); - - expect(screen.getByTestId("edit-public-survey-alert-dialog")).toHaveAttribute("data-open", "true"); - }); - - test("navigates to notifications when bell icon is clicked", async () => { - const user = userEvent.setup(); - render(); - - await user.click(screen.getByTestId("icon-bar-action-0")); - - expect(mockPush).toHaveBeenCalledWith("/environments/test-env-id/settings/notifications"); - }); - - test("opens preview window when preview icon is clicked", async () => { - const user = userEvent.setup(); - const linkSurvey = { ...mockSurvey, type: "link" as const }; - const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null); - - render(); - - await user.click(screen.getByTestId("icon-bar-action-1")); - - expect(windowOpenSpy).toHaveBeenCalledWith("https://example.com/s/test-survey-id?preview=true", "_blank"); - windowOpenSpy.mockRestore(); - }); - - test("does not show icon bar actions when read-only", () => { - render(); - - const iconBar = screen.getByTestId("icon-bar"); - expect(iconBar).toBeInTheDocument(); - // Should only show preview icon for link surveys, but this is app survey - expect(screen.queryByTestId("icon-bar-action-0")).not.toBeInTheDocument(); - }); - - test("handles modal close correctly", async () => { - mockSearchParams.set("share", "true"); - const user = userEvent.setup(); - render(); - - // Verify modal is open initially - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true"); - - await user.click(screen.getByText("Close Modal")); - - // Verify modal is closed - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "false"); - }); - - test("shows status dropdown for link surveys", () => { - const linkSurvey = { ...mockSurvey, type: "link" as const }; - render(); - - expect(screen.getByTestId("survey-status-dropdown")).toBeInTheDocument(); - }); - - test("does not show status dropdown for draft surveys", () => { - const draftSurvey = { ...mockSurvey, status: "draft" as const }; - render(); - - expect(screen.queryByTestId("survey-status-dropdown")).not.toBeInTheDocument(); - }); - - test("does not show status dropdown when app setup is not completed", () => { - const environmentWithoutAppSetup = { ...mockEnvironment, appSetupCompleted: false }; - render(); - - expect(screen.queryByTestId("survey-status-dropdown")).not.toBeInTheDocument(); - }); - - test("renders correctly with all props", () => { - render(); - - expect(screen.getByTestId("icon-bar")).toBeInTheDocument(); - expect(screen.getByText("Share survey")).toBeInTheDocument(); - expect(screen.getByTestId("success-message")).toBeInTheDocument(); - expect(screen.getByTestId("survey-status-dropdown")).toBeInTheDocument(); - }); - - test("duplicates survey when primary button is clicked in edit dialog", async () => { - const mockCopySurveyAction = vi.mocked( - await import("@/modules/survey/list/actions") - ).copySurveyToOtherEnvironmentAction; - mockCopySurveyAction.mockResolvedValue({ - data: { - ...mockSurvey, - id: "new-survey-id", - environmentId: "test-env-id", - triggers: [], - segment: null, - languages: [], - }, - }); - - const toast = await import("react-hot-toast"); - const user = userEvent.setup(); - - render(); - - // Click edit button to open dialog - await user.click(screen.getByTestId("icon-bar-action-1")); - - // Click primary button (duplicate & edit) - await user.click(screen.getByTestId("primary-button")); - - expect(mockCopySurveyAction).toHaveBeenCalledWith({ - environmentId: "test-env-id", - surveyId: "test-survey-id", - targetEnvironmentId: "test-env-id", - }); - expect(toast.default.success).toHaveBeenCalledWith("Survey duplicated successfully"); - expect(mockPush).toHaveBeenCalledWith("/environments/test-env-id/surveys/new-survey-id/edit"); - }); - - test("handles error when duplicating survey fails", async () => { - const mockCopySurveyAction = vi.mocked( - await import("@/modules/survey/list/actions") - ).copySurveyToOtherEnvironmentAction; - mockCopySurveyAction.mockResolvedValue({ - data: undefined, - serverError: "Duplication failed", - validationErrors: undefined, - bindArgsValidationErrors: [], - }); - - const toast = await import("react-hot-toast"); - const user = userEvent.setup(); - - render(); - - // Click edit button to open dialog - await user.click(screen.getByTestId("icon-bar-action-1")); - - // Click primary button (duplicate & edit) - await user.click(screen.getByTestId("primary-button")); - - expect(toast.default.error).toHaveBeenCalledWith("Error message"); - }); - - test("navigates to edit when secondary button is clicked in edit dialog", async () => { - const user = userEvent.setup(); - - render(); - - // Click edit button to open dialog - await user.click(screen.getByTestId("icon-bar-action-1")); - - // Click secondary button (edit) - await user.click(screen.getByTestId("secondary-button")); - - expect(mockPush).toHaveBeenCalledWith("/environments/test-env-id/surveys/test-survey-id/edit"); - }); - - test("shows loading state during duplication", async () => { - const mockCopySurveyAction = vi.mocked( - await import("@/modules/survey/list/actions") - ).copySurveyToOtherEnvironmentAction; - - // Mock a delayed response - mockCopySurveyAction.mockImplementation( - () => - new Promise((resolve) => - setTimeout( - () => - resolve({ - data: { - ...mockSurvey, - id: "new-survey-id", - environmentId: "test-env-id", - triggers: [], - segment: null, - languages: [], - }, - }), - 100 - ) - ) - ); - - const user = userEvent.setup(); - - render(); - - // Click edit button to open dialog - await user.click(screen.getByTestId("icon-bar-action-1")); - - // Click primary button (duplicate & edit) - await user.click(screen.getByTestId("primary-button")); - - // Check loading state - expect(screen.getByTestId("edit-public-survey-alert-dialog")).toHaveAttribute("data-loading", "true"); - }); - - test("closes dialog after successful duplication", async () => { - const mockCopySurveyAction = vi.mocked( - await import("@/modules/survey/list/actions") - ).copySurveyToOtherEnvironmentAction; - mockCopySurveyAction.mockResolvedValue({ - data: { - ...mockSurvey, - id: "new-survey-id", - environmentId: "test-env-id", - triggers: [], - segment: null, - languages: [], - }, - }); - - const user = userEvent.setup(); - - render(); - - // Click edit button to open dialog (should be icon-bar-action-2 with responses) - await user.click(screen.getByTestId("icon-bar-action-2")); - expect(screen.getByTestId("edit-public-survey-alert-dialog")).toHaveAttribute("data-open", "true"); - - // Click primary button (duplicate & edit) - await user.click(screen.getByTestId("primary-button")); - - // Dialog should be closed - expect(screen.getByTestId("edit-public-survey-alert-dialog")).toHaveAttribute("data-open", "false"); - }); - - test("opens preview with single use ID when enabled", async () => { - const mockUseSingleUseId = vi.mocked( - await import("@/modules/survey/hooks/useSingleUseId") - ).useSingleUseId; - mockUseSingleUseId.mockReturnValue({ - singleUseId: "test-single-use-id", - refreshSingleUseId: vi.fn().mockResolvedValue("new-single-use-id"), - }); - - const surveyWithSingleUse = { - ...mockSurvey, - type: "link" as const, - singleUse: { enabled: true, isEncrypted: false }, - }; - - const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null); - const user = userEvent.setup(); - - render(); - - await user.click(screen.getByTestId("icon-bar-action-1")); - - expect(windowOpenSpy).toHaveBeenCalledWith( - "https://example.com/s/test-survey-id?suId=new-single-use-id&preview=true", - "_blank" - ); - windowOpenSpy.mockRestore(); - }); - - test("handles single use ID generation failure", async () => { - const mockUseSingleUseId = vi.mocked( - await import("@/modules/survey/hooks/useSingleUseId") - ).useSingleUseId; - mockUseSingleUseId.mockReturnValue({ - singleUseId: "test-single-use-id", - refreshSingleUseId: vi.fn().mockResolvedValue(undefined), - }); - - const surveyWithSingleUse = { - ...mockSurvey, - type: "link" as const, - singleUse: { enabled: true, isEncrypted: false }, - }; - - const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null); - const user = userEvent.setup(); - - render(); - - await user.click(screen.getByTestId("icon-bar-action-1")); - - expect(windowOpenSpy).toHaveBeenCalledWith("https://example.com/s/test-survey-id?preview=true", "_blank"); - windowOpenSpy.mockRestore(); - }); - - test("opens share modal with correct modal view when share button clicked", async () => { - const user = userEvent.setup(); - render(); - - await user.click(screen.getByText("Share survey")); - - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-modal-view", "share"); - }); - - test("handles different survey statuses correctly", () => { - const completedSurvey = { ...mockSurvey, status: "completed" as const }; - render(); - - expect(screen.getByTestId("survey-status-dropdown")).toBeInTheDocument(); - }); - - test("handles paused survey status", () => { - const pausedSurvey = { ...mockSurvey, status: "paused" as const }; - render(); - - expect(screen.getByTestId("survey-status-dropdown")).toBeInTheDocument(); - }); - - test("does not render share modal when user is null", () => { - render(); - - expect(screen.queryByTestId("share-survey-modal")).not.toBeInTheDocument(); - }); - - test("renders with different isFormbricksCloud values", () => { - const { rerender } = render( - - ); - expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument(); - - rerender(); - expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument(); - }); - - test("renders with different isContactsEnabled values", () => { - const { rerender } = render( - - ); - expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument(); - - rerender(); - expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument(); - }); - - test("handles app survey type", () => { - const appSurvey = { ...mockSurvey, type: "app" as const }; - render(); - - // Should not show preview icon for app surveys - expect(screen.queryByTestId("icon-bar-action-1")).toBeInTheDocument(); // This should be edit button - expect(screen.getByTestId("icon-bar-action-1")).toHaveAttribute("title", "Edit"); - }); - - test("handles modal state changes correctly", async () => { - const user = userEvent.setup(); - render(); - - // Open modal via share button - await user.click(screen.getByText("Share survey")); - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true"); - - // Close modal - await user.click(screen.getByText("Close Modal")); - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "false"); - }); - - test("opens share modal via share button", async () => { - const user = userEvent.setup(); - render(); - - await user.click(screen.getByText("Share survey")); - - // Should open the modal with share view - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true"); - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-modal-view", "share"); - }); - - test("closes share modal and updates modal state", async () => { - mockSearchParams.set("share", "true"); - const user = userEvent.setup(); - render(); - - // Modal should be open initially due to share param - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true"); - - await user.click(screen.getByText("Close Modal")); - - // Should close the modal - expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "false"); - }); - - test("handles empty segments array", () => { - render(); - - expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument(); - }); - - test("handles zero response count", () => { - render(); - - expect(screen.queryByTestId("edit-public-survey-alert-dialog")).not.toBeInTheDocument(); - }); - - test("shows all icon actions for non-readonly app survey", () => { - render(); - - // Should show bell (notifications) and edit actions - expect(screen.getByTestId("icon-bar-action-0")).toHaveAttribute("title", "Configure alerts"); - expect(screen.getByTestId("icon-bar-action-1")).toHaveAttribute("title", "Edit"); - }); - - test("shows all icon actions for non-readonly link survey", () => { - const linkSurvey = { ...mockSurvey, type: "link" as const }; - render(); - - // Should show bell (notifications), preview, and edit actions - expect(screen.getByTestId("icon-bar-action-0")).toHaveAttribute("title", "Configure alerts"); - expect(screen.getByTestId("icon-bar-action-1")).toHaveAttribute("title", "Preview"); - expect(screen.getByTestId("icon-bar-action-2")).toHaveAttribute("title", "Edit"); - }); - - // Reset Survey Feature Tests - test("shows reset survey button when responses exist", () => { - render(); - - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - expect(resetButton).toBeInTheDocument(); - }); - - test("shows reset survey button when displays exist", () => { - render(); - - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - expect(resetButton).toBeInTheDocument(); - }); - - test("hides reset survey button when no responses or displays exist", () => { - render(); - - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - expect(resetButton).toBeUndefined(); - }); - - test("hides reset survey button for read-only users", () => { - render(); - - // For read-only users, there should be no icon bar actions - expect(screen.queryAllByTestId(/icon-bar-action-/)).toHaveLength(0); - }); - - test("opens reset confirmation modal when reset button is clicked", async () => { - const user = userEvent.setup(); - render(); - - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - - expect(resetButton).toBeDefined(); - await user.click(resetButton!); - - expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-open", "true"); - expect(screen.getByTestId("modal-title")).toHaveTextContent("Delete all existing responses and displays"); - expect(screen.getByTestId("modal-text")).toHaveTextContent( - "Resetting a survey removes all responses and metadata of this survey. This cannot be undone." - ); - }); - - test("executes reset survey action when confirmed", async () => { - const mockResetSurveyAction = vi.mocked(await import("../actions")).resetSurveyAction; - mockResetSurveyAction.mockResolvedValue({ - data: { - success: true, - deletedResponsesCount: 5, - deletedDisplaysCount: 3, - }, - }); - - const toast = await import("react-hot-toast"); - const user = userEvent.setup(); - - render(); - - // Open reset modal - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - expect(resetButton).toBeDefined(); - await user.click(resetButton!); - - // Confirm reset - await user.click(screen.getByTestId("confirm-button")); - - expect(mockResetSurveyAction).toHaveBeenCalledWith({ - surveyId: "test-survey-id", - organizationId: "test-organization-id", - projectId: "test-project-id", - }); - expect(toast.default.success).toHaveBeenCalledWith( - "Survey reset successfully! 5 responses and 3 displays were deleted." - ); - }); - - test("handles reset survey action error", async () => { - const mockResetSurveyAction = vi.mocked(await import("../actions")).resetSurveyAction; - mockResetSurveyAction.mockResolvedValue({ - data: undefined, - serverError: "Reset failed", - validationErrors: undefined, - bindArgsValidationErrors: [], - }); - - const toast = await import("react-hot-toast"); - const user = userEvent.setup(); - - render(); - - // Open reset modal - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - expect(resetButton).toBeDefined(); - await user.click(resetButton!); - - // Confirm reset - await user.click(screen.getByTestId("confirm-button")); - - expect(toast.default.error).toHaveBeenCalledWith("Error message"); - }); - - test("shows loading state during reset operation", async () => { - const mockResetSurveyAction = vi.mocked(await import("../actions")).resetSurveyAction; - - // Mock a delayed response - mockResetSurveyAction.mockImplementation( - () => - new Promise((resolve) => - setTimeout( - () => - resolve({ - data: { - success: true, - deletedResponsesCount: 5, - deletedDisplaysCount: 3, - }, - }), - 100 - ) - ) - ); - - const user = userEvent.setup(); - render(); - - // Open reset modal - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - expect(resetButton).toBeDefined(); - await user.click(resetButton!); - - // Confirm reset - await user.click(screen.getByTestId("confirm-button")); - - // Check loading state - expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-loading", "true"); - }); - - test("closes reset modal after successful reset", async () => { - const mockResetSurveyAction = vi.mocked(await import("../actions")).resetSurveyAction; - mockResetSurveyAction.mockResolvedValue({ - data: { - success: true, - deletedResponsesCount: 5, - deletedDisplaysCount: 3, - }, - }); - - const user = userEvent.setup(); - render(); - - // Open reset modal - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - expect(resetButton).toBeDefined(); - await user.click(resetButton!); - expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-open", "true"); - - // Confirm reset - wait for the action to complete - await user.click(screen.getByTestId("confirm-button")); - - // Wait for the action to complete and the modal to close - await vi.waitFor(() => { - expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-open", "false"); - }); - }); - - test("cancels reset operation when cancel button is clicked", async () => { - const user = userEvent.setup(); - render(); - - // Open reset modal - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - expect(resetButton).toBeDefined(); - await user.click(resetButton!); - expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-open", "true"); - - // Cancel reset - await user.click(screen.getByTestId("cancel-button")); - - // Modal should be closed - expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-open", "false"); - }); - - test("shows destructive button variant for reset confirmation", async () => { - const user = userEvent.setup(); - render(); - - // Open reset modal - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - expect(resetButton).toBeDefined(); - await user.click(resetButton!); - - expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-variant", "destructive"); - }); - - test("refreshes page after successful reset", async () => { - const mockResetSurveyAction = vi.mocked(await import("../actions")).resetSurveyAction; - - mockResetSurveyAction.mockResolvedValue({ - data: { - success: true, - deletedResponsesCount: 5, - deletedDisplaysCount: 3, - }, - }); - - const user = userEvent.setup(); - render(); - - // Open reset modal - const iconActions = screen.getAllByTestId(/icon-bar-action-/); - const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey"); - expect(resetButton).toBeDefined(); - await user.click(resetButton!); - - // Confirm reset - await user.click(screen.getByTestId("confirm-button")); - - expect(mockRefresh).toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/base-card.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/base-card.test.tsx deleted file mode 100644 index 7c07f9064c..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/base-card.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { BaseCard } from "./base-card"; - -vi.mock("@/modules/ui/components/tooltip", () => ({ - TooltipProvider: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - Tooltip: ({ children }: { children: React.ReactNode }) =>
    {children}
    , - TooltipTrigger: ({ - children, - onClick, - "data-testid": dataTestId, - }: { - children: React.ReactNode; - onClick?: () => void; - "data-testid"?: string; - }) => ( -
    - {children} -
    - ), - TooltipContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -describe("BaseCard", () => { - afterEach(() => { - cleanup(); - }); - - test("renders basic card with label and children", () => { - render( - -
    Test Content
    -
    - ); - - expect(screen.getByText("Test Label")).toBeInTheDocument(); - expect(screen.getByText("Test Content")).toBeInTheDocument(); - expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument(); - expect(screen.getByTestId("tooltip")).toBeInTheDocument(); - expect(screen.getByTestId("tooltip-trigger")).toBeInTheDocument(); - expect(screen.getByTestId("tooltip-content")).toBeInTheDocument(); - }); - - test("displays percentage when provided as valid number", () => { - render( - -
    Test Content
    -
    - ); - - expect(screen.getByText("75%")).toBeInTheDocument(); - }); - - test("does not display percentage when loading", () => { - render( - -
    Test Content
    -
    - ); - - expect(screen.queryByText("75%")).not.toBeInTheDocument(); - }); - - test("calls onClick when card is clicked", async () => { - const handleClick = vi.fn(); - const user = userEvent.setup(); - - render( - -
    Test Content
    -
    - ); - - await user.click(screen.getByTestId("tooltip-trigger")); - expect(handleClick).toHaveBeenCalledOnce(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/interactive-card.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/interactive-card.test.tsx deleted file mode 100644 index 1382efa168..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/interactive-card.test.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { InteractiveCard } from "./interactive-card"; - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/base-card", - () => ({ - BaseCard: ({ - label, - percentage, - tooltipText, - isLoading, - onClick, - testId, - id, - children, - }: { - label: React.ReactNode; - percentage?: number | null; - tooltipText?: React.ReactNode; - isLoading?: boolean; - onClick?: () => void; - testId?: string; - id?: string; - children: React.ReactNode; - }) => ( -
    -
    {label}
    - {percentage !== null && percentage !== undefined && ( -
    {percentage}%
    - )} - {tooltipText &&
    {tooltipText}
    } -
    {isLoading ? "loading" : "not-loading"}
    -
    {testId}
    -
    {id}
    -
    {children}
    -
    - ), - }) -); - -vi.mock("lucide-react", () => ({ - ChevronDownIcon: ({ className }: { className: string }) => ( -
    - ChevronDown -
    - ), - ChevronUpIcon: ({ className }: { className: string }) => ( -
    - ChevronUp -
    - ), -})); - -describe("InteractiveCard", () => { - afterEach(() => { - cleanup(); - }); - - const defaultProps = { - tab: "dropOffs" as const, - label: "Test Label", - percentage: 75, - value: "Test Value", - tooltipText: "Test tooltip", - isLoading: false, - onClick: vi.fn(), - isActive: false, - }; - - test("renders with basic props", () => { - render(); - - expect(screen.getByTestId("base-card")).toBeInTheDocument(); - expect(screen.getByTestId("base-card-label")).toHaveTextContent("Test Label"); - expect(screen.getByTestId("base-card-percentage")).toHaveTextContent("75%"); - expect(screen.getByTestId("base-card-tooltip")).toHaveTextContent("Test tooltip"); - expect(screen.getByTestId("base-card-loading")).toHaveTextContent("not-loading"); - expect(screen.getByText("Test Value")).toBeInTheDocument(); - }); - - test("generates correct testId and id based on tab", () => { - render(); - - expect(screen.getByTestId("base-card-testid")).toHaveTextContent("quotas-toggle"); - expect(screen.getByTestId("base-card-id")).toHaveTextContent("quotas-toggle"); - }); - - test("calls onClick when clicked", async () => { - const handleClick = vi.fn(); - const user = userEvent.setup(); - - render(); - - await user.click(screen.getByTestId("base-card")); - expect(handleClick).toHaveBeenCalledOnce(); - }); - - test("renders loading state with skeleton", () => { - render(); - - expect(screen.getByTestId("base-card-loading")).toHaveTextContent("loading"); - - const skeleton = screen.getByTestId("base-card-children").querySelector(".animate-pulse"); - expect(skeleton).toHaveClass("h-6", "w-12", "animate-pulse", "rounded-full", "bg-slate-200"); - - expect(screen.queryByText("Test Value")).not.toBeInTheDocument(); - expect(screen.queryByTestId("chevron-down-icon")).not.toBeInTheDocument(); - expect(screen.queryByTestId("chevron-up-icon")).not.toBeInTheDocument(); - }); - - test("shows chevron up icon when active", () => { - render(); - - expect(screen.getByTestId("chevron-up-icon")).toBeInTheDocument(); - expect(screen.getByTestId("chevron-up-icon")).toHaveClass("h-4", "w-4"); - expect(screen.queryByTestId("chevron-down-icon")).not.toBeInTheDocument(); - }); - - test("handles zero percentage", () => { - render(); - - expect(screen.getByTestId("base-card-percentage")).toHaveTextContent("0%"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx deleted file mode 100644 index 582259bd50..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx +++ /dev/null @@ -1,516 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSegment } from "@formbricks/types/segment"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TUser } from "@formbricks/types/user"; -import { ShareSurveyModal } from "./share-survey-modal"; - -// Mock getPublicDomain - must be first to prevent server-side env access -vi.mock("@/lib/getPublicUrl", () => ({ - getPublicDomain: vi.fn().mockReturnValue("https://example.com"), -})); - -// Mock env to prevent server-side env access -vi.mock("@/lib/env", () => ({ - env: { - IS_FORMBRICKS_CLOUD: "0", - NODE_ENV: "test", - E2E_TESTING: "0", - ENCRYPTION_KEY: "test-encryption-key-32-characters", - WEBAPP_URL: "https://example.com", - CRON_SECRET: "test-cron-secret", - PUBLIC_URL: "https://example.com", - VERCEL_URL: "", - }, -})); - -// Mock the useTranslate hook -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - const translations: Record = { - "environments.surveys.summary.single_use_links": "Single-use links", - "environments.surveys.summary.share_the_link": "Share the link", - "environments.surveys.summary.qr_code": "QR Code", - "environments.surveys.summary.personal_links": "Personal links", - "environments.surveys.summary.embed_in_an_email": "Embed in email", - "environments.surveys.summary.embed_on_website": "Embed on website", - "environments.surveys.summary.dynamic_popup": "Dynamic popup", - "environments.surveys.summary.in_app.title": "In-app survey", - "environments.surveys.summary.in_app.description": "Display survey in your app", - "environments.surveys.share.anonymous_links.nav_title": "Share the link", - "environments.surveys.share.single_use_links.nav_title": "Single-use links", - "environments.surveys.share.personal_links.nav_title": "Personal links", - "environments.surveys.share.embed_on_website.nav_title": "Embed on website", - "environments.surveys.share.send_email.nav_title": "Embed in email", - "environments.surveys.share.social_media.title": "Social media", - "environments.surveys.share.dynamic_popup.nav_title": "Dynamic popup", - }; - return translations[key] || key; - }, - }), -})); - -// Mock analysis utils -vi.mock("@/modules/analysis/utils", () => ({ - getSurveyUrl: vi.fn().mockResolvedValue("https://example.com/s/test-survey-id"), -})); - -// Mock logger -vi.mock("@formbricks/logger", () => ({ - logger: { - error: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), - log: vi.fn(), - }, -})); - -// Mock dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ open, onOpenChange, children }: any) => ( -
    onOpenChange(false)}> - {children} -
    - ), - DialogContent: ({ children, width }: any) => ( -
    - {children} -
    - ), - DialogTitle: ({ children }: any) =>
    {children}
    , -})); - -// Mock VisuallyHidden -vi.mock("@radix-ui/react-visually-hidden", () => ({ - VisuallyHidden: ({ asChild, children }: any) => ( -
    {asChild ? children : {children}}
    - ), -})); - -// Mock child components -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab", - () => ({ - AppTab: () =>
    App Tab Content
    , - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container", - () => ({ - TabContainer: ({ title, description, children }: any) => ( -
    -

    {title}

    -

    {description}

    - {children} -
    - ), - }) -); - -vi.mock("./shareEmbedModal/share-view", () => ({ - ShareView: ({ tabs, activeId, setActiveId }: any) => ( -
    -

    Share View

    -
    -
    Active Tab: {activeId}
    -
    -
    - {tabs.map((tab: any) => ( - - ))} -
    -
    - ), -})); - -vi.mock("./shareEmbedModal/success-view", () => ({ - SuccessView: ({ - survey, - surveyUrl, - publicDomain, - user, - tabs, - handleViewChange, - handleEmbedViewWithTab, - }: any) => ( -
    -

    Success View

    -
    -
    Survey: {survey?.id}
    -
    URL: {surveyUrl}
    -
    Domain: {publicDomain}
    -
    User: {user?.id}
    -
    -
    - {tabs.map((tab: any) => { - // Handle single-use links case - let displayLabel = tab.label; - if (tab.id === "anon-links" && survey?.singleUse?.enabled) { - displayLabel = "Single-use links"; - } - return ( - - ); - })} -
    - -
    - ), -})); - -// Mock LinkSettingsTab -vi.mock("./shareEmbedModal/link-settings-tab", () => ({ - LinkSettingsTab: ({ survey, isReadOnly, locale }: any) => ( -
    - LinkSettings for {survey.id} - ReadOnly: {isReadOnly.toString()} - Locale: {locale} -
    - ), -})); - -// Mock lucide-react icons -vi.mock("lucide-react", async (importOriginal) => { - const actual = (await importOriginal()) as any; - return { - ...actual, - Code2Icon: () => , - LinkIcon: () => , - MailIcon: () => , - QrCodeIcon: () => , - SmartphoneIcon: () => , - SquareStack: () => , - UserIcon: () => , - Settings: () => , - }; -}); - -// Mock data -const mockSurvey: TSurvey = { - id: "test-survey-id", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Survey", - type: "link", - environmentId: "test-env-id", - status: "inProgress", - displayOption: "displayOnce", - autoClose: null, - triggers: [], - - recontactDays: null, - displayLimit: null, - welcomeCard: { enabled: false, timeToFinish: false, showResponseCount: false }, - questions: [], - endings: [], - hiddenFields: { enabled: false }, - displayPercentage: null, - autoComplete: null, - - segment: null, - languages: [], - showLanguageSwitch: false, - singleUse: { enabled: false, isEncrypted: false }, - projectOverwrites: null, - surveyClosedMessage: null, - delay: 0, - isVerifyEmailEnabled: false, - createdBy: null, - variables: [], - followUps: [], - styling: null, - pin: null, - recaptcha: null, - isSingleResponsePerEmailEnabled: false, - isBackButtonHidden: false, - metadata: {}, -}; - -const mockAppSurvey: TSurvey = { - ...mockSurvey, - type: "app", -}; - -const mockUser: TUser = { - id: "test-user-id", - name: "Test User", - email: "test@example.com", - emailVerified: new Date(), - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - - role: "other", - objective: "other", - locale: "en-US", - lastLoginAt: new Date(), - isActive: true, - notificationSettings: { - alert: {}, - unsubscribedOrganizationIds: [], - }, -}; - -const mockSegments: TSegment[] = []; - -const mockSetOpen = vi.fn(); - -const defaultProps = { - survey: mockSurvey, - publicDomain: "https://example.com", - open: true, - modalView: "start" as const, - setOpen: mockSetOpen, - user: mockUser, - segments: mockSegments, - isContactsEnabled: true, - isFormbricksCloud: false, - isReadOnly: false, - isStorageConfigured: true, -}; - -describe("ShareSurveyModal", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders dialog when open is true", () => { - render(); - - expect(screen.getByTestId("dialog")).toHaveAttribute("data-open", "true"); - expect(screen.getByTestId("dialog-content")).toBeInTheDocument(); - }); - - test("renders success view when modalView is start", () => { - render(); - - expect(screen.getByTestId("success-view")).toBeInTheDocument(); - expect(screen.getByText("Success View")).toBeInTheDocument(); - }); - - test("renders share view when modalView is share and survey is link type", () => { - render(); - - expect(screen.getByTestId("share-view")).toBeInTheDocument(); - expect(screen.getByText("Share View")).toBeInTheDocument(); - }); - - test("renders app tab when survey is app type and modalView is share", () => { - render(); - - expect(screen.getByTestId("tab-container")).toBeInTheDocument(); - expect(screen.getByTestId("app-tab")).toBeInTheDocument(); - expect(screen.getByText("In-app survey")).toBeInTheDocument(); - expect(screen.getByText("Display survey in your app")).toBeInTheDocument(); - }); - - test("renders success view when survey is app type and modalView is start", () => { - render(); - - expect(screen.getByTestId("success-view")).toBeInTheDocument(); - expect(screen.queryByTestId("tab-container")).not.toBeInTheDocument(); - }); - - test("sets correct width for dialog content based on survey type", () => { - const { rerender } = render(); - - expect(screen.getByTestId("dialog-content")).toHaveAttribute("data-width", "wide"); - - rerender(); - - expect(screen.getByTestId("dialog-content")).toHaveAttribute("data-width", "default"); - }); - - test("generates correct tabs for link survey", () => { - render(); - - expect(screen.getByTestId("success-tab-anon-links")).toHaveTextContent("Share the link"); - expect(screen.getByTestId("success-tab-qr-code")).toHaveTextContent("QR Code"); - expect(screen.getByTestId("success-tab-personal-links")).toHaveTextContent("Personal links"); - expect(screen.getByTestId("success-tab-email")).toHaveTextContent("Embed in email"); - expect(screen.getByTestId("success-tab-website-embed")).toHaveTextContent("Embed on website"); - expect(screen.getByTestId("success-tab-dynamic-popup")).toHaveTextContent("Dynamic popup"); - }); - - test("shows single-use links label when singleUse is enabled", () => { - const singleUseSurvey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } }; - render(); - - expect(screen.getByTestId("success-tab-anon-links")).toHaveTextContent("Single-use links"); - }); - - test("calls setOpen when dialog is closed", async () => { - const user = userEvent.setup(); - render(); - - await user.click(screen.getByTestId("dialog")); - - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - - test("fetches survey URL on mount", async () => { - const { getSurveyUrl } = await import("@/modules/analysis/utils"); - - render(); - - await waitFor(() => { - expect(getSurveyUrl).toHaveBeenCalledWith(mockSurvey, "https://example.com", "default"); - }); - }); - - test("handles getSurveyUrl failure gracefully", async () => { - const { getSurveyUrl } = await import("@/modules/analysis/utils"); - vi.mocked(getSurveyUrl).mockRejectedValue(new Error("Failed to fetch")); - - // Render and verify it doesn't crash, even if nothing renders due to the error - expect(() => { - render(); - }).not.toThrow(); - }); - - test("renders ShareView with correct active tab", () => { - render(); - - const shareViewData = screen.getByTestId("share-view-data"); - expect(shareViewData).toHaveTextContent("Active Tab: anon-links"); - }); - - test("passes correct props to SuccessView", () => { - render(); - - const successViewData = screen.getByTestId("success-view-data"); - expect(successViewData).toHaveTextContent("Survey: test-survey-id"); - expect(successViewData).toHaveTextContent("Domain: https://example.com"); - expect(successViewData).toHaveTextContent("User: test-user-id"); - }); - - test("resets to start view when modal is closed and reopened", async () => { - const { rerender } = render(); - - expect(screen.getByTestId("share-view")).toBeInTheDocument(); - - rerender(); - rerender(); - - expect(screen.getByTestId("share-view")).toBeInTheDocument(); - }); - - test("sets correct active tab for link survey", () => { - render(); - - expect(screen.getByTestId("share-view")).toHaveAttribute("data-active-id", "anon-links"); - }); - - test("renders tab container for app survey in share mode", () => { - render(); - - expect(screen.getByTestId("tab-container")).toBeInTheDocument(); - expect(screen.getByTestId("app-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("share-view")).not.toBeInTheDocument(); - }); - - test("renders with contacts disabled", () => { - render(); - - // Just verify the ShareView renders correctly regardless of isContactsEnabled prop - expect(screen.getByTestId("share-view")).toBeInTheDocument(); - expect(screen.getByTestId("share-view")).toHaveAttribute("data-active-id", "anon-links"); - }); - - test("renders with formbricks cloud enabled", () => { - render(); - - // Just verify the ShareView renders correctly regardless of isFormbricksCloud prop - expect(screen.getByTestId("share-view")).toBeInTheDocument(); - }); - - test("correctly handles direct navigation to share view", () => { - render(); - - expect(screen.getByTestId("share-view")).toBeInTheDocument(); - expect(screen.queryByTestId("success-view")).not.toBeInTheDocument(); - }); - - test("handler functions are passed to child components", () => { - render(); - - // Verify SuccessView receives the handler functions by checking buttons exist - expect(screen.getByTestId("go-to-share-view")).toBeInTheDocument(); - expect(screen.getByTestId("success-tab-anon-links")).toBeInTheDocument(); - expect(screen.getByTestId("success-tab-qr-code")).toBeInTheDocument(); - }); - - test("tab switching functionality is available in ShareView", () => { - render(); - - // Verify ShareView has tab switching buttons - expect(screen.getByTestId("tab-anon-links")).toBeInTheDocument(); - expect(screen.getByTestId("tab-qr-code")).toBeInTheDocument(); - expect(screen.getByTestId("tab-personal-links")).toBeInTheDocument(); - }); - - test("renders different content based on survey type", () => { - // Link survey renders ShareView - const { rerender } = render(); - expect(screen.getByTestId("share-view")).toBeInTheDocument(); - - // App survey renders TabContainer with AppTab - rerender(); - expect(screen.getByTestId("tab-container")).toBeInTheDocument(); - expect(screen.getByTestId("app-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("share-view")).not.toBeInTheDocument(); - }); - - test("includes link settings tab in share tabs for link surveys", () => { - render(); - - const shareView = screen.getByTestId("share-view"); - expect(shareView).toBeInTheDocument(); - - // Check that tabs are rendered and include link settings - const tabsContainer = screen.getByTestId("tabs"); - expect(tabsContainer).toBeInTheDocument(); - - // Look for the link-settings tab button - expect(screen.getByTestId("tab-link-settings")).toBeInTheDocument(); - }); - - test("does not include link settings for app surveys", () => { - render(); - - // App surveys should render TabContainer, not ShareView with link settings - expect(screen.getByTestId("tab-container")).toBeInTheDocument(); - expect(screen.getByTestId("app-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("share-view")).not.toBeInTheDocument(); - expect(screen.queryByTestId("tab-link-settings")).not.toBeInTheDocument(); - }); - - test("handles locale prop correctly for link settings", () => { - const customProps = { - ...defaultProps, - modalView: "share" as const, - }; - - render(); - - // Verify that ShareView is rendered (which would contain LinkSettingsTab) - expect(screen.getByTestId("share-view")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.test.tsx deleted file mode 100644 index 8f929f3c41..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.test.tsx +++ /dev/null @@ -1,432 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TUserLocale } from "@formbricks/types/user"; -import { AnonymousLinksTab } from "./anonymous-links-tab"; - -// Mock actions -vi.mock("../../actions", () => ({ - updateSingleUseLinksAction: vi.fn(), -})); - -vi.mock("@/modules/survey/list/actions", () => ({ - generateSingleUseIdsAction: vi.fn(), -})); - -// Mock components -vi.mock("@/modules/analysis/components/ShareSurveyLink", () => ({ - ShareSurveyLink: ({ surveyUrl, publicDomain }: any) => ( -
    -

    Survey URL: {surveyUrl}

    -

    Public Domain: {publicDomain}

    -
    - ), -})); - -vi.mock("@/modules/ui/components/advanced-option-toggle", () => ({ - AdvancedOptionToggle: ({ children, htmlId, isChecked, onToggle, title }: any) => ( -
    - - {children} -
    - ), -})); - -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children, variant, size }: any) => ( -
    - {children} -
    - ), - AlertTitle: ({ children }: any) =>
    {children}
    , - AlertDescription: ({ children }: any) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, disabled, variant }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/input", () => ({ - Input: ({ value, onChange, type, max, min, className }: any) => ( - - ), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container", - () => ({ - TabContainer: ({ children, title }: any) => ( -
    -

    {title}

    - {children} -
    - ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal", - () => ({ - DisableLinkModal: ({ open, type, onDisable }: any) => ( -
    - - -
    - ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links", - () => ({ - DocumentationLinks: ({ links }: any) => ( -
    - {links.map((link: any, index: number) => ( - - {link.title} - - ))} -
    - ), - }) -); - -// Mock translations -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -// Mock Next.js router -const mockRefresh = vi.fn(); -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - refresh: mockRefresh, - }), -})); - -// Mock toast -vi.mock("react-hot-toast", () => ({ - default: { - error: vi.fn(), - success: vi.fn(), - }, -})); - -// Mock URL and Blob for download functionality -global.URL.createObjectURL = vi.fn(() => "mock-url"); -global.URL.revokeObjectURL = vi.fn(); -global.Blob = vi.fn(() => ({}) as any); - -describe("AnonymousLinksTab", () => { - const mockSurvey = { - id: "test-survey-id", - environmentId: "test-env-id", - type: "link" as const, - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Survey", - createdBy: null, - status: "draft" as const, - questions: [], - welcomeCard: { enabled: false }, - hiddenFields: { enabled: false }, - singleUse: { - enabled: false, - isEncrypted: false, - }, - } as unknown as TSurvey; - - const surveyWithSingleUse = { - ...mockSurvey, - singleUse: { - enabled: true, - isEncrypted: false, - }, - } as TSurvey; - - const surveyWithEncryption = { - ...mockSurvey, - singleUse: { - enabled: true, - isEncrypted: true, - }, - } as TSurvey; - - const defaultProps = { - survey: mockSurvey, - surveyUrl: "https://example.com/survey", - publicDomain: "https://example.com", - setSurveyUrl: vi.fn(), - locale: "en-US" as TUserLocale, - }; - - beforeEach(async () => { - vi.clearAllMocks(); - const { updateSingleUseLinksAction } = await import("../../actions"); - const { generateSingleUseIdsAction } = await import("@/modules/survey/list/actions"); - - vi.mocked(updateSingleUseLinksAction).mockResolvedValue({ data: mockSurvey }); - vi.mocked(generateSingleUseIdsAction).mockResolvedValue({ data: ["link1", "link2"] }); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders with single-use link enabled when survey has singleUse enabled", () => { - render(); - - expect(screen.getByTestId("toggle-multi-use-link-switch")).toHaveAttribute("data-checked", "false"); - expect(screen.getByTestId("toggle-single-use-link-switch")).toHaveAttribute("data-checked", "true"); - }); - - test("handles multi-use toggle when single-use is disabled", async () => { - const user = userEvent.setup(); - const { updateSingleUseLinksAction } = await import("../../actions"); - - render(); - - // When multi-use is enabled and we click it, it should show a modal to turn it off - const multiUseToggle = screen.getByTestId("toggle-button-multi-use-link-switch"); - await user.click(multiUseToggle); - - // Should show confirmation modal - expect(screen.getByTestId("disable-link-modal")).toHaveAttribute("data-open", "true"); - expect(screen.getByTestId("disable-link-modal")).toHaveAttribute("data-type", "multi-use"); - - // Confirm the modal action - const confirmButton = screen.getByText("Confirm"); - await user.click(confirmButton); - - await waitFor(() => { - expect(updateSingleUseLinksAction).toHaveBeenCalledWith({ - surveyId: "test-survey-id", - environmentId: "test-env-id", - isSingleUse: true, - isSingleUseEncryption: true, - }); - }); - - expect(mockRefresh).toHaveBeenCalled(); - }); - - test("shows confirmation modal when toggling from single-use to multi-use", async () => { - const user = userEvent.setup(); - render(); - - const multiUseToggle = screen.getByTestId("toggle-button-multi-use-link-switch"); - await user.click(multiUseToggle); - - expect(screen.getByTestId("disable-link-modal")).toHaveAttribute("data-open", "true"); - expect(screen.getByTestId("disable-link-modal")).toHaveAttribute("data-type", "single-use"); - }); - - test("shows confirmation modal when toggling from multi-use to single-use", async () => { - const user = userEvent.setup(); - render(); - - const singleUseToggle = screen.getByTestId("toggle-button-single-use-link-switch"); - await user.click(singleUseToggle); - - expect(screen.getByTestId("disable-link-modal")).toHaveAttribute("data-open", "true"); - expect(screen.getByTestId("disable-link-modal")).toHaveAttribute("data-type", "multi-use"); - }); - - test("handles single-use encryption toggle", async () => { - const user = userEvent.setup(); - const { updateSingleUseLinksAction } = await import("../../actions"); - - render(); - - const encryptionToggle = screen.getByTestId("toggle-button-single-use-encryption-switch"); - await user.click(encryptionToggle); - - await waitFor(() => { - expect(updateSingleUseLinksAction).toHaveBeenCalledWith({ - surveyId: "test-survey-id", - environmentId: "test-env-id", - isSingleUse: true, - isSingleUseEncryption: true, - }); - }); - }); - - test("shows encryption info alert when encryption is disabled", () => { - render(); - - const alerts = screen.getAllByTestId("alert-info"); - const encryptionAlert = alerts.find( - (alert) => - alert.querySelector('[data-testid="alert-title"]')?.textContent === - "environments.surveys.share.anonymous_links.custom_single_use_id_title" - ); - - expect(encryptionAlert).toBeInTheDocument(); - expect(encryptionAlert?.querySelector('[data-testid="alert-title"]')).toHaveTextContent( - "environments.surveys.share.anonymous_links.custom_single_use_id_title" - ); - }); - - test("shows link generation section when encryption is enabled", () => { - render(); - - expect(screen.getByTestId("number-input")).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.share.anonymous_links.generate_and_download_links") - ).toBeInTheDocument(); - }); - - test("handles number of links input change", async () => { - const user = userEvent.setup(); - render(); - - const input = screen.getByTestId("number-input"); - await user.clear(input); - await user.type(input, "5"); - - expect(input).toHaveValue(5); - }); - - test("handles link generation error", async () => { - const user = userEvent.setup(); - const { generateSingleUseIdsAction } = await import("@/modules/survey/list/actions"); - vi.mocked(generateSingleUseIdsAction).mockResolvedValue({ data: undefined }); - - render(); - - const generateButton = screen.getByText( - "environments.surveys.share.anonymous_links.generate_and_download_links" - ); - await user.click(generateButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith( - "environments.surveys.share.anonymous_links.generate_links_error" - ); - }); - }); - - test("handles action error with generic message", async () => { - const user = userEvent.setup(); - const { updateSingleUseLinksAction } = await import("../../actions"); - vi.mocked(updateSingleUseLinksAction).mockResolvedValue({ data: undefined }); - - render(); - - // Click multi-use toggle to show modal - const multiUseToggle = screen.getByTestId("toggle-button-multi-use-link-switch"); - await user.click(multiUseToggle); - - // Confirm the modal action - const confirmButton = screen.getByText("Confirm"); - await user.click(confirmButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("common.something_went_wrong_please_try_again"); - }); - }); - - test("confirms modal action when disable link modal is confirmed", async () => { - const user = userEvent.setup(); - const { updateSingleUseLinksAction } = await import("../../actions"); - - render(); - - const multiUseToggle = screen.getByTestId("toggle-button-multi-use-link-switch"); - await user.click(multiUseToggle); - - const confirmButton = screen.getByText("Confirm"); - await user.click(confirmButton); - - await waitFor(() => { - expect(updateSingleUseLinksAction).toHaveBeenCalledWith({ - surveyId: "test-survey-id", - environmentId: "test-env-id", - isSingleUse: false, - isSingleUseEncryption: false, - }); - }); - }); - - test("renders documentation links", () => { - render(); - - expect(screen.getByTestId("documentation-links")).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.share.anonymous_links.single_use_links") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.share.anonymous_links.data_prefilling") - ).toBeInTheDocument(); - }); - - test("shows read-only input with copy button when encryption is disabled", async () => { - // surveyWithSingleUse has encryption disabled - render(); - - // Check if single-use link is enabled - expect(screen.getByTestId("toggle-single-use-link-switch")).toHaveAttribute("data-checked", "true"); - - // Check if encryption is disabled - expect(screen.getByTestId("toggle-single-use-encryption-switch")).toHaveAttribute( - "data-checked", - "false" - ); - - // Check for the custom URL display - const surveyUrlWithCustomSuid = `${defaultProps.surveyUrl}?suId=CUSTOM-ID`; - expect(screen.getByText(surveyUrlWithCustomSuid)).toBeInTheDocument(); - - // Check for the copy button and try to click it - const copyButton = screen.getByText("common.copy"); - expect(copyButton).toBeInTheDocument(); - await userEvent.click(copyButton); - - // check if toast is called - expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard"); - - // Check for the alert - expect( - screen.getByText("environments.surveys.share.anonymous_links.custom_single_use_id_title") - ).toBeInTheDocument(); - - // Ensure the number of links input is not visible - expect( - screen.queryByText("environments.surveys.share.anonymous_links.number_of_links_label") - ).not.toBeInTheDocument(); - }); - - test("hides read-only input with copy button when encryption is enabled", async () => { - // surveyWithEncryption has encryption enabled - render(); - - // Check if single-use link is enabled - expect(screen.getByTestId("toggle-single-use-link-switch")).toHaveAttribute("data-checked", "true"); - - // Check if encryption is enabled - expect(screen.getByTestId("toggle-single-use-encryption-switch")).toHaveAttribute("data-checked", "true"); - - // Ensure the number of links input is visible - expect( - screen.getByText("environments.surveys.share.anonymous_links.number_of_links_label") - ).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.test.tsx deleted file mode 100644 index 921f719d57..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TActionClass, TActionClassNoCodeConfig } from "@formbricks/types/action-classes"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TProject } from "@formbricks/types/project"; -import { TBaseFilter, TSegment } from "@formbricks/types/segment"; -import { TSurvey, TSurveyWelcomeCard } from "@formbricks/types/surveys/types"; -import { EnvironmentContextWrapper } from "@/app/(app)/environments/[environmentId]/context/environment-context"; -import { SurveyContextWrapper } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context"; -import { AppTab } from "./app-tab"; - -// Mock Next.js Link component -vi.mock("next/link", () => ({ - default: ({ href, children, ...props }: any) => ( - - {children} - - ), -})); - -// Mock DocumentationLinksSection -vi.mock("./documentation-links-section", () => ({ - DocumentationLinksSection: ({ title, links }: { title: string; links: any[] }) => ( -
    -

    {title}

    - {links.map((link) => ( - - ))} -
    - ), -})); - -// Mock segment -const mockSegment: TSegment = { - id: "test-segment-id", - title: "Test Segment", - description: "Test segment description", - environmentId: "test-env-id", - createdAt: new Date(), - updatedAt: new Date(), - isPrivate: false, - filters: [ - { - id: "test-filter-id", - connector: "and", - resource: "contact", - attributeKey: "test-attribute-key", - attributeType: "string", - condition: "equals", - value: "test", - } as unknown as TBaseFilter, - ], - surveys: ["test-survey-id"], -}; - -// Mock action class -const mockActionClass: TActionClass = { - id: "test-action-id", - name: "Test Action", - type: "code", - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "test-env-id", - description: "Test action description", - noCodeConfig: null, - key: "test-action-key", -}; - -const mockNoCodeActionClass: TActionClass = { - id: "test-no-code-action-id", - name: "Test No Code Action", - type: "noCode", - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "test-env-id", - description: "Test no code action description", - noCodeConfig: { - type: "click", - elementSelector: { - cssSelector: ".test-button", - innerHtml: "Click me", - }, - } as TActionClassNoCodeConfig, - key: "test-no-code-action-key", -}; - -// Mock environment data -const mockEnvironment: TEnvironment = { - id: "test-env-id", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - projectId: "test-project-id", - appSetupCompleted: true, -}; - -// Mock project data -const mockProject = { - id: "test-project-id", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "test-org-id", - recontactDays: 7, - config: { - channel: "app", - industry: "saas", - }, - linkSurveyBranding: true, - styling: { - allowStyleOverwrite: true, - brandColor: { light: "#ffffff", dark: "#000000" }, - questionColor: { light: "#000000", dark: "#ffffff" }, - inputColor: { light: "#000000", dark: "#ffffff" }, - inputBorderColor: { light: "#cccccc", dark: "#444444" }, - cardBackgroundColor: { light: "#ffffff", dark: "#000000" }, - cardBorderColor: { light: "#cccccc", dark: "#444444" }, - highlightBorderColor: { light: "#007bff", dark: "#0056b3" }, - isDarkModeEnabled: false, - isLogoHidden: false, - hideProgressBar: false, - roundness: 8, - cardArrangement: { linkSurveys: "casual", appSurveys: "casual" }, - }, - inAppSurveyBranding: true, - placement: "bottomRight", - clickOutsideClose: true, - darkOverlay: false, - logo: { url: "test-logo.png", bgColor: "#ffffff" }, -} as TProject; - -// Mock survey data -const mockSurvey: TSurvey = { - id: "test-survey-id", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Survey", - type: "app", - environmentId: "test-env-id", - status: "inProgress", - displayOption: "displayOnce", - autoClose: null, - triggers: [{ actionClass: mockActionClass }], - recontactDays: null, - displayLimit: null, - welcomeCard: { enabled: false } as unknown as TSurveyWelcomeCard, - questions: [], - endings: [], - hiddenFields: { enabled: false }, - displayPercentage: null, - autoComplete: null, - segment: null, - languages: [], - showLanguageSwitch: false, - singleUse: { enabled: false, isEncrypted: false }, - projectOverwrites: null, - surveyClosedMessage: null, - delay: 0, - isVerifyEmailEnabled: false, - inlineTriggers: {}, -} as unknown as TSurvey; - -describe("AppTab", () => { - afterEach(() => { - cleanup(); - }); - - const renderWithProviders = (appSetupCompleted = true, surveyOverrides = {}, projectOverrides = {}) => { - const environmentWithSetup = { - ...mockEnvironment, - appSetupCompleted, - }; - - const surveyWithOverrides = { - ...mockSurvey, - ...surveyOverrides, - }; - - const projectWithOverrides = { - ...mockProject, - ...projectOverrides, - }; - - return render( - - - - - - ); - }; - - test("renders setup completed content when app setup is completed", () => { - renderWithProviders(true); - expect(screen.getByText("environments.surveys.summary.in_app.connection_title")).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.summary.in_app.connection_description") - ).toBeInTheDocument(); - }); - - test("renders setup required content when app setup is not completed", () => { - renderWithProviders(false); - expect(screen.getByText("environments.surveys.summary.in_app.no_connection_title")).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.summary.in_app.no_connection_description") - ).toBeInTheDocument(); - expect(screen.getByText("common.connect_formbricks")).toBeInTheDocument(); - }); - - test("displays correct wait time when survey has recontact days", () => { - renderWithProviders(true, { recontactDays: 5 }); - expect( - screen.getByText("5 environments.surveys.summary.in_app.display_criteria.time_based_days") - ).toBeInTheDocument(); - expect( - screen.getByText("(environments.surveys.summary.in_app.display_criteria.overwritten)") - ).toBeInTheDocument(); - }); - - test("displays correct wait time when survey has 1 recontact day", () => { - renderWithProviders(true, { recontactDays: 1 }); - expect( - screen.getByText("1 environments.surveys.summary.in_app.display_criteria.time_based_day") - ).toBeInTheDocument(); - expect( - screen.getByText("(environments.surveys.summary.in_app.display_criteria.overwritten)") - ).toBeInTheDocument(); - }); - - test("displays correct wait time when survey has 0 recontact days", () => { - renderWithProviders(true, { recontactDays: 0 }); - expect( - screen.getByText("environments.surveys.summary.in_app.display_criteria.time_based_always") - ).toBeInTheDocument(); - expect( - screen.getByText("(environments.surveys.summary.in_app.display_criteria.overwritten)") - ).toBeInTheDocument(); - }); - - test("displays project recontact days when survey has no recontact days", () => { - renderWithProviders(true, { recontactDays: null }, { recontactDays: 3 }); - expect( - screen.getByText("3 environments.surveys.summary.in_app.display_criteria.time_based_days") - ).toBeInTheDocument(); - }); - - test("displays always when project has 0 recontact days", () => { - renderWithProviders(true, { recontactDays: null }, { recontactDays: 0 }); - expect( - screen.getByText("environments.surveys.summary.in_app.display_criteria.time_based_always") - ).toBeInTheDocument(); - }); - - test("displays always when both survey and project have null recontact days", () => { - renderWithProviders(true, { recontactDays: null }, { recontactDays: null }); - expect( - screen.getByText("environments.surveys.summary.in_app.display_criteria.time_based_always") - ).toBeInTheDocument(); - }); - - test("displays correct display option for displayOnce", () => { - renderWithProviders(true, { displayOption: "displayOnce" }); - expect(screen.getByText("environments.surveys.edit.show_only_once")).toBeInTheDocument(); - }); - - test("displays correct display option for displayMultiple", () => { - renderWithProviders(true, { displayOption: "displayMultiple" }); - expect(screen.getByText("environments.surveys.edit.until_they_submit_a_response")).toBeInTheDocument(); - }); - - test("displays correct display option for respondMultiple", () => { - renderWithProviders(true, { displayOption: "respondMultiple" }); - expect( - screen.getByText("environments.surveys.edit.keep_showing_while_conditions_match") - ).toBeInTheDocument(); - }); - - test("displays correct display option for displaySome", () => { - renderWithProviders(true, { displayOption: "displaySome" }); - expect(screen.getByText("environments.surveys.edit.show_multiple_times")).toBeInTheDocument(); - }); - - test("displays everyone when survey has no segment", () => { - renderWithProviders(true, { segment: null }); - expect( - screen.getByText("environments.surveys.summary.in_app.display_criteria.everyone") - ).toBeInTheDocument(); - }); - - test("displays targeted when survey has segment with filters", () => { - renderWithProviders(true, { - segment: mockSegment, - }); - expect(screen.getByText("Test Segment")).toBeInTheDocument(); - }); - - test("displays segment title when survey has public segment with filters", () => { - const publicSegment = { ...mockSegment, isPrivate: false, title: "Public Segment" }; - renderWithProviders(true, { - segment: publicSegment, - }); - expect(screen.getByText("Public Segment")).toBeInTheDocument(); - }); - - test("displays targeted when survey has private segment with filters", () => { - const privateSegment = { ...mockSegment, isPrivate: true }; - renderWithProviders(true, { - segment: privateSegment, - }); - expect( - screen.getByText("environments.surveys.summary.in_app.display_criteria.targeted") - ).toBeInTheDocument(); - }); - - test("displays everyone when survey has segment with no filters", () => { - const emptySegment = { ...mockSegment, filters: [] }; - renderWithProviders(true, { - segment: emptySegment, - }); - expect( - screen.getByText("environments.surveys.summary.in_app.display_criteria.everyone") - ).toBeInTheDocument(); - }); - - test("displays code trigger description correctly", () => { - renderWithProviders(true, { triggers: [{ actionClass: mockActionClass }] }); - expect(screen.getByText("Test Action")).toBeInTheDocument(); - expect( - screen.getByText("(environments.surveys.summary.in_app.display_criteria.code_trigger)") - ).toBeInTheDocument(); - }); - - test("displays no-code trigger description correctly", () => { - renderWithProviders(true, { triggers: [{ actionClass: mockNoCodeActionClass }] }); - expect(screen.getByText("Test No Code Action")).toBeInTheDocument(); - expect( - screen.getByText( - "(environments.surveys.summary.in_app.display_criteria.no_code_trigger, environments.actions.click)" - ) - ).toBeInTheDocument(); - }); - - test("displays randomizer when displayPercentage is set", () => { - renderWithProviders(true, { displayPercentage: 25 }); - expect( - screen.getAllByText(/environments\.surveys\.summary\.in_app\.display_criteria\.randomizer/)[0] - ).toBeInTheDocument(); - }); - - test("does not display randomizer when displayPercentage is null", () => { - renderWithProviders(true, { displayPercentage: null }); - expect(screen.queryByText("Show to")).not.toBeInTheDocument(); - }); - - test("does not display randomizer when displayPercentage is 0", () => { - renderWithProviders(true, { displayPercentage: 0 }); - expect(screen.queryByText("Show to")).not.toBeInTheDocument(); - }); - - test("renders documentation links section", () => { - renderWithProviders(true); - expect(screen.getByTestId("documentation-links")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.in_app.documentation_title")).toBeInTheDocument(); - }); - - test("renders all display criteria items", () => { - renderWithProviders(true); - expect( - screen.getByText("environments.surveys.summary.in_app.display_criteria.time_based_description") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.summary.in_app.display_criteria.audience_description") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.summary.in_app.display_criteria.trigger_description") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.summary.in_app.display_criteria.recontact_description") - ).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal.test.tsx deleted file mode 100644 index a05167cba8..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { DisableLinkModal } from "./disable-link-modal"; - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -const onOpenChange = vi.fn(); -const onDisable = vi.fn(); - -describe("DisableLinkModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("should render the modal for multi-use link", () => { - render( - - ); - - expect( - screen.getByText("environments.surveys.share.anonymous_links.disable_multi_use_link_modal_title") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.share.anonymous_links.disable_multi_use_link_modal_description") - ).toBeInTheDocument(); - expect( - screen.getByText( - "environments.surveys.share.anonymous_links.disable_multi_use_link_modal_description_subtext" - ) - ).toBeInTheDocument(); - }); - - test("should render the modal for single-use link", () => { - render( - - ); - - expect(screen.getByText("common.are_you_sure")).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.share.anonymous_links.disable_single_use_link_modal_description") - ).toBeInTheDocument(); - }); - - test("should call onDisable and onOpenChange when the disable button is clicked for multi-use", async () => { - render( - - ); - - const disableButton = screen.getByText( - "environments.surveys.share.anonymous_links.disable_multi_use_link_modal_button" - ); - await userEvent.click(disableButton); - - expect(onDisable).toHaveBeenCalled(); - expect(onOpenChange).toHaveBeenCalledWith(false); - }); - - test("should call onDisable and onOpenChange when the disable button is clicked for single-use", async () => { - render( - - ); - - const disableButton = screen.getByText( - "environments.surveys.share.anonymous_links.disable_single_use_link_modal_button" - ); - await userEvent.click(disableButton); - - expect(onDisable).toHaveBeenCalled(); - expect(onOpenChange).toHaveBeenCalledWith(false); - }); - - test("should call onOpenChange when the cancel button is clicked", async () => { - render( - - ); - - const cancelButton = screen.getByText("common.cancel"); - await userEvent.click(cancelButton); - - expect(onOpenChange).toHaveBeenCalledWith(false); - }); - - test("should not render the modal when open is false", () => { - const { container } = render( - - ); - expect(container.firstChild).toBeNull(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links.test.tsx deleted file mode 100644 index 2ea06df099..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links.test.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test } from "vitest"; -import { DocumentationLinks } from "./documentation-links"; - -describe("DocumentationLinks", () => { - afterEach(() => { - cleanup(); - }); - - const mockLinks = [ - { - title: "Getting Started Guide", - href: "https://docs.formbricks.com/getting-started", - }, - { - title: "API Documentation", - href: "https://docs.formbricks.com/api", - }, - { - title: "Integration Guide", - href: "https://docs.formbricks.com/integrations", - }, - ]; - - test("renders all documentation links", () => { - render(); - - expect(screen.getByText("Getting Started Guide")).toBeInTheDocument(); - expect(screen.getByText("API Documentation")).toBeInTheDocument(); - expect(screen.getByText("Integration Guide")).toBeInTheDocument(); - }); - - test("renders correct number of alert components", () => { - render(); - - const alerts = screen.getAllByRole("alert"); - expect(alerts).toHaveLength(3); - }); - - test("renders learn more links with correct href attributes", () => { - render(); - - const learnMoreLinks = screen.getAllByText("common.learn_more"); - expect(learnMoreLinks).toHaveLength(3); - - expect(learnMoreLinks[0]).toHaveAttribute("href", "https://docs.formbricks.com/getting-started"); - expect(learnMoreLinks[1]).toHaveAttribute("href", "https://docs.formbricks.com/api"); - expect(learnMoreLinks[2]).toHaveAttribute("href", "https://docs.formbricks.com/integrations"); - }); - - test("renders learn more links with target blank", () => { - render(); - - const learnMoreLinks = screen.getAllByText("common.learn_more"); - learnMoreLinks.forEach((link) => { - expect(link).toHaveAttribute("target", "_blank"); - }); - }); - - test("renders learn more links with correct CSS classes", () => { - render(); - - const learnMoreLinks = screen.getAllByText("common.learn_more"); - learnMoreLinks.forEach((link) => { - expect(link).toHaveClass("text-slate-900", "hover:underline"); - }); - }); - - test("renders empty list when no links provided", () => { - render(); - - const alerts = screen.queryAllByRole("alert"); - expect(alerts).toHaveLength(0); - }); - - test("renders single link correctly", () => { - const singleLink = [mockLinks[0]]; - render(); - - expect(screen.getByText("Getting Started Guide")).toBeInTheDocument(); - expect(screen.getByText("common.learn_more")).toBeInTheDocument(); - expect(screen.getByText("common.learn_more")).toHaveAttribute( - "href", - "https://docs.formbricks.com/getting-started" - ); - }); - - test("renders with correct container structure", () => { - const { container } = render(); - - const mainContainer = container.firstChild as HTMLElement; - expect(mainContainer).toHaveClass("flex", "w-full", "flex-col", "space-y-2"); - - const linkContainers = mainContainer.children; - expect(linkContainers).toHaveLength(3); - - Array.from(linkContainers).forEach((linkContainer) => { - expect(linkContainer).toHaveClass("flex", "w-full", "flex-col", "gap-3"); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentationL-links-section.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentationL-links-section.test.tsx deleted file mode 100644 index a304ffc0d0..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentationL-links-section.test.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { DocumentationLinksSection } from "./documentation-links-section"; - -// Mock the useTranslate hook -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - if (key === "common.read_docs") { - return "Read docs"; - } - return key; - }, - }), -})); - -// Mock Next.js Link component -vi.mock("next/link", () => ({ - default: ({ href, children, ...props }: any) => ( - - {children} - - ), -})); - -// Mock Alert components -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children, size, variant }: any) => ( -
    - {children} -
    - ), - AlertButton: ({ children }: any) =>
    {children}
    , - AlertTitle: ({ children }: any) =>
    {children}
    , -})); - -// Mock Typography components -vi.mock("@/modules/ui/components/typography", () => ({ - H4: ({ children }: any) =>

    {children}

    , -})); - -// Mock lucide-react icons -vi.mock("lucide-react", () => ({ - ArrowUpRight: ({ className }: any) => , -})); - -describe("DocumentationLinksSection", () => { - afterEach(() => { - cleanup(); - }); - - const mockLinks = [ - { - href: "https://example.com/docs/html", - title: "HTML Documentation", - }, - { - href: "https://example.com/docs/react", - title: "React Documentation", - }, - { - href: "https://example.com/docs/javascript", - title: "JavaScript Documentation", - }, - ]; - - test("renders title correctly", () => { - render(); - - expect(screen.getByTestId("h4")).toHaveTextContent("Test Documentation Title"); - }); - - test("renders all documentation links", () => { - render(); - - expect(screen.getAllByTestId("alert")).toHaveLength(3); - expect(screen.getByText("HTML Documentation")).toBeInTheDocument(); - expect(screen.getByText("React Documentation")).toBeInTheDocument(); - expect(screen.getByText("JavaScript Documentation")).toBeInTheDocument(); - }); - - test("renders links with correct href attributes", () => { - render(); - - const links = screen.getAllByRole("link"); - expect(links[0]).toHaveAttribute("href", "https://example.com/docs/html"); - expect(links[1]).toHaveAttribute("href", "https://example.com/docs/react"); - expect(links[2]).toHaveAttribute("href", "https://example.com/docs/javascript"); - }); - - test("renders links with correct target and rel attributes", () => { - render(); - - const links = screen.getAllByRole("link"); - links.forEach((link) => { - expect(link).toHaveAttribute("target", "_blank"); - expect(link).toHaveAttribute("rel", "noopener noreferrer"); - }); - }); - - test("renders read docs button for each link", () => { - render(); - - const readDocsButtons = screen.getAllByText("Read docs"); - expect(readDocsButtons).toHaveLength(3); - }); - - test("renders icons for each alert", () => { - render(); - - const icons = screen.getAllByTestId("arrow-up-right-icon"); - expect(icons).toHaveLength(3); - }); - - test("renders alerts with correct props", () => { - render(); - - const alerts = screen.getAllByTestId("alert"); - alerts.forEach((alert) => { - expect(alert).toHaveAttribute("data-size", "small"); - expect(alert).toHaveAttribute("data-variant", "default"); - }); - }); - - test("renders with empty links array", () => { - render(); - - expect(screen.getByTestId("h4")).toHaveTextContent("Test Documentation Title"); - expect(screen.queryByTestId("alert")).not.toBeInTheDocument(); - }); - - test("renders single link correctly", () => { - const singleLink = [ - { - href: "https://example.com/docs/single", - title: "Single Documentation", - }, - ]; - - render(); - - expect(screen.getAllByTestId("alert")).toHaveLength(1); - expect(screen.getByText("Single Documentation")).toBeInTheDocument(); - expect(screen.getByRole("link")).toHaveAttribute("href", "https://example.com/docs/single"); - }); - - test("renders with special characters in title and links", () => { - const specialLinks = [ - { - href: "https://example.com/docs/special?param=value&other=test", - title: "Special Characters & Symbols", - }, - ]; - - render(); - - expect(screen.getByTestId("h4")).toHaveTextContent("Special Title & Characters"); - expect(screen.getByText("Special Characters & Symbols")).toBeInTheDocument(); - expect(screen.getByRole("link")).toHaveAttribute( - "href", - "https://example.com/docs/special?param=value&other=test" - ); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.test.tsx deleted file mode 100644 index f57a1466ba..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.test.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { DynamicPopupTab } from "./dynamic-popup-tab"; - -// Mock components -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: (props: { variant?: string; size?: string; children: React.ReactNode }) => ( -
    - {props.children} -
    - ), - AlertButton: (props: { asChild?: boolean; children: React.ReactNode }) => ( -
    - {props.children} -
    - ), - AlertDescription: (props: { children: React.ReactNode }) => ( -
    {props.children}
    - ), - AlertTitle: (props: { children: React.ReactNode }) =>
    {props.children}
    , -})); - -// Mock DocumentationLinks -vi.mock("./documentation-links", () => ({ - DocumentationLinks: (props: { links: Array<{ href: string; title: string }> }) => ( -
    - {props.links.map((link) => ( -
    - {link.title} -
    - ))} -
    - ), -})); - -// Mock Next.js Link -vi.mock("next/link", () => ({ - default: (props: { href: string; target?: string; className?: string; children: React.ReactNode }) => ( - - {props.children} - - ), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -describe("DynamicPopupTab", () => { - afterEach(() => { - cleanup(); - }); - - const defaultProps = { - environmentId: "env-123", - surveyId: "survey-123", - }; - - test("renders with correct container structure", () => { - render(); - - const container = screen.getByTestId("dynamic-popup-container"); - expect(container).toHaveClass("flex", "h-full", "flex-col", "justify-between", "space-y-4"); - }); - - test("renders alert with correct props", () => { - render(); - - const alert = screen.getByTestId("alert"); - expect(alert).toBeInTheDocument(); - expect(alert).toHaveAttribute("data-variant", "info"); - expect(alert).toHaveAttribute("data-size", "default"); - }); - - test("renders alert title with correct translation key", () => { - render(); - - const alertTitle = screen.getByTestId("alert-title"); - expect(alertTitle).toBeInTheDocument(); - expect(alertTitle).toHaveTextContent("environments.surveys.share.dynamic_popup.alert_title"); - }); - - test("renders alert description with correct translation key", () => { - render(); - - const alertDescription = screen.getByTestId("alert-description"); - expect(alertDescription).toBeInTheDocument(); - expect(alertDescription).toHaveTextContent("environments.surveys.share.dynamic_popup.alert_description"); - }); - - test("renders alert button with link to survey edit page", () => { - render(); - - const alertButton = screen.getByTestId("alert-button"); - expect(alertButton).toBeInTheDocument(); - expect(alertButton).toHaveAttribute("data-as-child", "true"); - - const link = screen.getByTestId("next-link"); - expect(link).toHaveAttribute("href", "/environments/env-123/surveys/survey-123/edit"); - expect(link).toHaveTextContent("environments.surveys.share.dynamic_popup.alert_button"); - }); - - test("renders DocumentationLinks component", () => { - render(); - - const documentationLinks = screen.getByTestId("documentation-links"); - expect(documentationLinks).toBeInTheDocument(); - }); - - test("passes correct documentation links to DocumentationLinks component", () => { - render(); - - const documentationLinks = screen.getAllByTestId("documentation-link"); - expect(documentationLinks).toHaveLength(3); - - // Check attribute-based targeting link - const attributeLink = documentationLinks.find( - (link) => - link.getAttribute("data-href") === - "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting" - ); - expect(attributeLink).toBeInTheDocument(); - expect(attributeLink).toHaveAttribute( - "data-title", - "environments.surveys.share.dynamic_popup.attribute_based_targeting" - ); - - // Check code and no code triggers link - const actionsLink = documentationLinks.find( - (link) => - link.getAttribute("data-href") === - "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions" - ); - expect(actionsLink).toBeInTheDocument(); - expect(actionsLink).toHaveAttribute( - "data-title", - "environments.surveys.share.dynamic_popup.code_no_code_triggers" - ); - - // Check recontact options link - const recontactLink = documentationLinks.find( - (link) => - link.getAttribute("data-href") === - "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/recontact" - ); - expect(recontactLink).toBeInTheDocument(); - expect(recontactLink).toHaveAttribute( - "data-title", - "environments.surveys.share.dynamic_popup.recontact_options" - ); - }); - - test("renders documentation links with correct titles", () => { - render(); - - const documentationLinks = screen.getAllByTestId("documentation-link"); - - const expectedTitles = [ - "environments.surveys.share.dynamic_popup.attribute_based_targeting", - "environments.surveys.share.dynamic_popup.code_no_code_triggers", - "environments.surveys.share.dynamic_popup.recontact_options", - ]; - - expectedTitles.forEach((title) => { - const link = documentationLinks.find((link) => link.getAttribute("data-title") === title); - expect(link).toBeInTheDocument(); - expect(link).toHaveTextContent(title); - }); - }); - - test("renders documentation links with correct URLs", () => { - render(); - - const documentationLinks = screen.getAllByTestId("documentation-link"); - - const expectedUrls = [ - "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting", - "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions", - "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/recontact", - ]; - - expectedUrls.forEach((url) => { - const link = documentationLinks.find((link) => link.getAttribute("data-href") === url); - expect(link).toBeInTheDocument(); - }); - }); - - test("calls translation function for all text content", () => { - render(); - - // Check alert translations - expect(screen.getByTestId("alert-title")).toHaveTextContent( - "environments.surveys.share.dynamic_popup.alert_title" - ); - expect(screen.getByTestId("alert-description")).toHaveTextContent( - "environments.surveys.share.dynamic_popup.alert_description" - ); - expect(screen.getByTestId("next-link")).toHaveTextContent( - "environments.surveys.share.dynamic_popup.alert_button" - ); - }); - - test("renders with correct props when environmentId and surveyId change", () => { - const newProps = { - environmentId: "env-456", - surveyId: "survey-456", - }; - - render(); - - const link = screen.getByTestId("next-link"); - expect(link).toHaveAttribute("href", "/environments/env-456/surveys/survey-456/edit"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.test.tsx deleted file mode 100644 index b837bc00b4..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.test.tsx +++ /dev/null @@ -1,294 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { AuthenticationError } from "@formbricks/types/errors"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { getEmailHtmlAction, sendEmbedSurveyPreviewEmailAction } from "../../actions"; -import { EmailTab } from "./email-tab"; - -// Mock actions -vi.mock("../../actions", () => ({ - getEmailHtmlAction: vi.fn(), - sendEmbedSurveyPreviewEmailAction: vi.fn(), -})); - -// Mock helper -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn((val) => val?.serverError || "Formatted error message"), -})); - -// Mock UI components -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, variant, title, "aria-label": ariaLabel, ...props }: any) => ( - - ), -})); -vi.mock("@/modules/ui/components/code-block", () => ({ - CodeBlock: ({ - children, - language, - showCopyToClipboard, - }: { - children: React.ReactNode; - language: string; - showCopyToClipboard?: boolean; - }) => ( -
    - {children} -
    - ), -})); -vi.mock("@/modules/ui/components/loading-spinner", () => ({ - LoadingSpinner: () =>
    LoadingSpinner
    , -})); - -// Mock lucide-react icons -vi.mock("lucide-react", () => ({ - Code2Icon: () =>
    , - CopyIcon: () =>
    , - EyeIcon: () =>
    , - MailIcon: () =>
    , - SendIcon: () =>
    , -})); - -// Mock navigator.clipboard -const mockWriteText = vi.fn().mockResolvedValue(undefined); -Object.defineProperty(navigator, "clipboard", { - value: { - writeText: mockWriteText, - }, - configurable: true, -}); - -const surveyId = "test-survey-id"; -const userEmail = "test@example.com"; -const mockEmailHtmlPreview = "

    Hello World ?preview=true&foo=bar

    "; -const mockCleanedEmailHtml = "

    Hello World ?foo=bar

    "; - -describe("EmailTab", () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(getEmailHtmlAction).mockResolvedValue({ data: mockEmailHtmlPreview }); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders initial state correctly and fetches email HTML", async () => { - render(); - - expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalledWith({ surveyId }); - - // Buttons - expect( - screen.getByRole("button", { name: "environments.surveys.share.send_email.send_preview_email" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "environments.surveys.share.send_email.embed_code_tab" }) - ).toBeInTheDocument(); - expect(screen.getByTestId("send-icon")).toBeInTheDocument(); - // Note: code2-icon is only visible in the embed code tab, not in initial render - - // Email preview section - await waitFor(() => { - const emailToElements = screen.getAllByText((content, element) => { - return ( - element?.textContent?.includes("environments.surveys.share.send_email.email_to_label") || false - ); - }); - expect(emailToElements.length).toBeGreaterThan(0); - }); - expect( - screen.getAllByText((content, element) => { - return ( - element?.textContent?.includes("environments.surveys.share.send_email.email_subject_label") || false - ); - }).length - ).toBeGreaterThan(0); - expect( - screen.getAllByText((content, element) => { - return ( - element?.textContent?.includes( - "environments.surveys.share.send_email.formbricks_email_survey_preview" - ) || false - ); - }).length - ).toBeGreaterThan(0); - await waitFor(() => { - expect(screen.getByText("Hello World ?foo=bar")).toBeInTheDocument(); // HTML content rendered as text (preview=true removed) - }); - expect(screen.queryByTestId("code-block")).not.toBeInTheDocument(); - }); - - test("toggles embed code view", async () => { - render(); - await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled()); - - const viewEmbedButton = screen.getByRole("button", { - name: "environments.surveys.share.send_email.embed_code_tab", - }); - await userEvent.click(viewEmbedButton); - - // Embed code view - expect( - screen.getByRole("button", { name: "environments.surveys.share.send_email.copy_embed_code" }) - ).toBeInTheDocument(); - expect(screen.getByTestId("copy-icon")).toBeInTheDocument(); - const codeBlock = screen.getByTestId("code-block"); - expect(codeBlock).toBeInTheDocument(); - expect(codeBlock).toHaveTextContent(mockCleanedEmailHtml); // Cleaned HTML - // The email_to_label should not be visible in embed code view - expect( - screen.queryByText((content, element) => { - return ( - element?.textContent?.includes("environments.surveys.share.send_email.email_to_label") || false - ); - }) - ).not.toBeInTheDocument(); - - // Toggle back to preview - const previewButton = screen.getByRole("button", { - name: "environments.surveys.share.send_email.email_preview_tab", - }); - await userEvent.click(previewButton); - - expect( - screen.getByRole("button", { name: "environments.surveys.share.send_email.send_preview_email" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "environments.surveys.share.send_email.embed_code_tab" }) - ).toBeInTheDocument(); - await waitFor(() => { - const emailToElements = screen.getAllByText((content, element) => { - return ( - element?.textContent?.includes("environments.surveys.share.send_email.email_to_label") || false - ); - }); - expect(emailToElements.length).toBeGreaterThan(0); - }); - expect(screen.queryByTestId("code-block")).not.toBeInTheDocument(); - }); - - test("copies code to clipboard", async () => { - render(); - await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled()); - - const viewEmbedButton = screen.getByRole("button", { - name: "environments.surveys.share.send_email.embed_code_tab", - }); - await userEvent.click(viewEmbedButton); - - const copyCodeButton = screen.getByRole("button", { - name: "environments.surveys.share.send_email.copy_embed_code", - }); - await userEvent.click(copyCodeButton); - - expect(mockWriteText).toHaveBeenCalledWith(mockCleanedEmailHtml); - expect(toast.success).toHaveBeenCalledWith( - "environments.surveys.share.send_email.embed_code_copied_to_clipboard" - ); - }); - - test("sends preview email successfully", async () => { - vi.mocked(sendEmbedSurveyPreviewEmailAction).mockResolvedValue({ data: true }); - render(); - await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled()); - - const sendPreviewButton = screen.getByRole("button", { - name: "environments.surveys.share.send_email.send_preview_email", - }); - await userEvent.click(sendPreviewButton); - - expect(sendEmbedSurveyPreviewEmailAction).toHaveBeenCalledWith({ surveyId }); - expect(toast.success).toHaveBeenCalledWith("environments.surveys.share.send_email.email_sent"); - }); - - test("handles send preview email failure (server error)", async () => { - const errorResponse = { serverError: "Server issue" }; - vi.mocked(sendEmbedSurveyPreviewEmailAction).mockResolvedValue(errorResponse as any); - render(); - await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled()); - - const sendPreviewButton = screen.getByRole("button", { - name: "environments.surveys.share.send_email.send_preview_email", - }); - await userEvent.click(sendPreviewButton); - - expect(sendEmbedSurveyPreviewEmailAction).toHaveBeenCalledWith({ surveyId }); - expect(getFormattedErrorMessage).toHaveBeenCalledWith(errorResponse); - expect(toast.error).toHaveBeenCalledWith("Server issue"); - }); - - test("handles send preview email failure (authentication error)", async () => { - vi.mocked(sendEmbedSurveyPreviewEmailAction).mockRejectedValue(new AuthenticationError("Auth failed")); - render(); - await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled()); - - const sendPreviewButton = screen.getByRole("button", { - name: "environments.surveys.share.send_email.send_preview_email", - }); - await userEvent.click(sendPreviewButton); - - expect(sendEmbedSurveyPreviewEmailAction).toHaveBeenCalledWith({ surveyId }); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("common.not_authenticated"); - }); - }); - - test("handles send preview email failure (generic error)", async () => { - vi.mocked(sendEmbedSurveyPreviewEmailAction).mockRejectedValue(new Error("Generic error")); - render(); - await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled()); - - const sendPreviewButton = screen.getByRole("button", { - name: "environments.surveys.share.send_email.send_preview_email", - }); - await userEvent.click(sendPreviewButton); - - expect(sendEmbedSurveyPreviewEmailAction).toHaveBeenCalledWith({ surveyId }); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("common.something_went_wrong_please_try_again"); - }); - }); - - test("renders loading spinner if email HTML is not yet fetched", () => { - vi.mocked(getEmailHtmlAction).mockReturnValue(new Promise(() => {})); // Never resolves - render(); - expect(screen.getByTestId("loading-spinner")).toBeInTheDocument(); - }); - - test("renders default email if email prop is not provided", async () => { - render(); - await waitFor(() => { - expect( - screen.getByText((content, element) => { - return ( - element?.textContent === "environments.surveys.share.send_email.email_to_label : user@mail.com" - ); - }) - ).toBeInTheDocument(); - }); - }); - - test("emailHtml memo removes various ?preview=true patterns", async () => { - const htmlWithVariants = - "

    Test1 ?preview=true

    Test2 ?preview=true&next

    Test3 ?preview=true&;next

    "; - const expectedCleanHtml = "

    Test1

    Test2 ?next

    Test3 ?next

    "; - vi.mocked(getEmailHtmlAction).mockResolvedValue({ data: htmlWithVariants }); - - render(); - await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled()); - - const viewEmbedButton = screen.getByRole("button", { - name: "environments.surveys.share.send_email.embed_code_tab", - }); - await userEvent.click(viewEmbedButton); - - const codeBlock = screen.getByTestId("code-block"); - expect(codeBlock).toHaveTextContent(expectedCleanHtml); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab.test.tsx deleted file mode 100644 index e2e35ff2c5..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab.test.tsx +++ /dev/null @@ -1,426 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils"; -import { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types"; -import { useSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context"; -import { createI18nString, extractLanguageCodes, getEnabledLanguages } from "@/lib/i18n/utils"; -import { updateSurveyAction } from "@/modules/survey/editor/actions"; -import { LinkSettingsTab } from "./link-settings-tab"; - -// Mock dependencies -vi.mock("@/lib/i18n/utils", () => ({ - createI18nString: vi.fn(), - extractLanguageCodes: vi.fn(), - getEnabledLanguages: vi.fn(), -})); - -vi.mock("@/modules/survey/editor/actions", () => ({ - updateSurveyAction: vi.fn(), -})); - -vi.mock("@formbricks/i18n-utils/src/utils", () => ({ - getLanguageLabel: vi.fn(), -})); - -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context", () => ({ - useSurvey: vi.fn(), -})); - -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock("@/modules/ui/components/file-input", () => ({ - FileInput: vi.fn(({ onFileUpload, fileUrl, disabled, id }) => ( -
    - onFileUpload(e.target.value ? [e.target.value] : undefined)} - disabled={disabled} - /> -
    - )), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: vi.fn(({ children, disabled, type, ...props }) => ( - - )), -})); - -const mockSurvey: TSurvey = { - id: "test-survey-id", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Survey", - type: "link", - environmentId: "test-env-id", - status: "inProgress", - displayOption: "displayOnce", - autoClose: null, - triggers: [], - recontactDays: null, - displayLimit: null, - welcomeCard: { enabled: false, timeToFinish: false, showResponseCount: false }, - questions: [], - endings: [], - hiddenFields: { enabled: false }, - displayPercentage: null, - autoComplete: null, - segment: null, - languages: [ - { language: { id: "lang1", code: "default" }, default: true, enabled: true } as TSurveyLanguage, - { language: { id: "lang2", code: "en" }, default: false, enabled: true } as TSurveyLanguage, - ], - showLanguageSwitch: false, - singleUse: { enabled: false, isEncrypted: false }, - projectOverwrites: null, - surveyClosedMessage: null, - delay: 0, - isVerifyEmailEnabled: false, - createdBy: null, - variables: [], - followUps: [], - styling: null, - pin: null, - recaptcha: null, - isSingleResponsePerEmailEnabled: false, - isBackButtonHidden: false, - metadata: { - title: { default: "Test Title", en: "Test Title EN" }, - description: { default: "Test Description", en: "Test Description EN" }, - ogImage: "https://example.com/image.png", - }, -}; - -const mockSingleLanguageSurvey: TSurvey = { - ...mockSurvey, - languages: [ - { language: { id: "lang1", code: "default" }, default: true, enabled: true }, - ] as TSurveyLanguage[], -}; - -describe("LinkSettingsTab", () => { - beforeEach(() => { - vi.clearAllMocks(); - - vi.mocked(useSurvey).mockReturnValue({ - survey: mockSurvey, - }); - - vi.mocked(getEnabledLanguages).mockReturnValue([ - { language: { id: "lang1", code: "default" }, default: true, enabled: true } as TSurveyLanguage, - { language: { id: "lang2", code: "en" }, default: false, enabled: true } as TSurveyLanguage, - ]); - - vi.mocked(extractLanguageCodes).mockReturnValue(["default", "en"]); - - vi.mocked(createI18nString).mockImplementation((text, languages) => { - const result = {}; - languages.forEach((lang) => { - result[lang] = typeof text === "string" ? text : ""; - }); - return result; - }); - - vi.mocked(getLanguageLabel).mockImplementation((code) => { - const labels = { - default: "Default", - en: "English", - }; - return labels[code] || code; - }); - - vi.mocked(updateSurveyAction).mockResolvedValue({ data: mockSurvey }); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders form fields correctly", () => { - render(); - - expect(screen.getByText("common.language")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.share.link_settings.link_title")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.share.link_settings.link_description")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.share.link_settings.preview_image")).toBeInTheDocument(); - expect(screen.getByTestId("save-button")).toBeInTheDocument(); - }); - - test("initializes form with existing metadata", () => { - render(); - - const titleInput = screen.getByDisplayValue("Test Title"); - const descriptionInput = screen.getByDisplayValue("Test Description"); - - expect(titleInput).toBeInTheDocument(); - expect(descriptionInput).toBeInTheDocument(); - }); - - test("initializes form with empty values when metadata is undefined", () => { - const mockSurveyWithoutMetadata: TSurvey = { - ...mockSurvey, - metadata: {}, - }; - - vi.mocked(useSurvey).mockReturnValue({ - survey: mockSurveyWithoutMetadata, - }); - - vi.mocked(createI18nString).mockReturnValue({ default: "", en: "" }); - - render(); - - expect(vi.mocked(createI18nString)).toHaveBeenCalledWith("", ["default", "en"]); - }); - - test("does not show language selector for single language surveys", () => { - vi.mocked(useSurvey).mockReturnValue({ - survey: mockSingleLanguageSurvey, - }); - - vi.mocked(getEnabledLanguages).mockReturnValue([ - { language: { id: "lang1", code: "default" }, default: true, enabled: true } as TSurveyLanguage, - ]); - - render(); - - expect(screen.queryByText("common.language")).not.toBeInTheDocument(); - }); - - test("shows language selector for multi-language surveys", () => { - render(); - - expect(screen.getByText("common.language")).toBeInTheDocument(); - }); - - test("handles language change correctly", async () => { - render(); - - // Since the Select component is complex to test in JSDOM, let's test that - // the language selector is rendered and has the expected options - const languageSelect = screen.getByRole("combobox"); - expect(languageSelect).toBeInTheDocument(); - - // Check that the language options are available in the hidden select - const hiddenSelect = screen.getByDisplayValue("Default"); - expect(hiddenSelect).toBeInTheDocument(); - - // Check for English option in the select - const englishOption = screen - .getByDisplayValue("Default") - .closest("select") - ?.querySelector('option[value="en"]'); - expect(englishOption).toBeInTheDocument(); - }); - - test("handles title input change", async () => { - const user = userEvent.setup(); - render(); - - const titleInput = screen.getByDisplayValue("Test Title"); - await user.clear(titleInput); - await user.type(titleInput, "New Title"); - - expect(titleInput).toHaveValue("New Title"); - }); - - test("handles description input change", async () => { - const user = userEvent.setup(); - render(); - - const descriptionInput = screen.getByDisplayValue("Test Description"); - await user.clear(descriptionInput); - await user.type(descriptionInput, "New Description"); - - expect(descriptionInput).toHaveValue("New Description"); - }); - - test("handles file upload", async () => { - render(); - - const fileInput = screen.getByTestId("file-input"); - fireEvent.change(fileInput, { target: { value: "https://example.com/new-image.png" } }); - - expect(fileInput).toHaveValue("https://example.com/new-image.png"); - }); - - test("handles file removal", async () => { - render(); - - const fileInput = screen.getByTestId("file-input"); - fireEvent.change(fileInput, { target: { value: "" } }); - - expect(fileInput).toHaveValue(""); - }); - - test("disables form when isReadOnly is true", () => { - render(); - - const titleInput = screen.getByDisplayValue("Test Title"); - const descriptionInput = screen.getByDisplayValue("Test Description"); - const saveButton = screen.getByTestId("save-button"); - - expect(titleInput).toBeDisabled(); - expect(descriptionInput).toBeDisabled(); - expect(saveButton).toBeDisabled(); - }); - - test("submits form successfully", async () => { - const user = userEvent.setup(); - render(); - - const titleInput = screen.getByDisplayValue("Test Title"); - await user.clear(titleInput); - await user.type(titleInput, "Updated Title"); - - const saveButton = screen.getByTestId("save-button"); - await user.click(saveButton); - - await waitFor(() => { - expect(vi.mocked(updateSurveyAction)).toHaveBeenCalled(); - }); - - const callArgs = vi.mocked(updateSurveyAction).mock.calls[0][0]; - expect(callArgs.metadata.title?.default).toBe("Updated Title"); - }); - - test("handles submission error", async () => { - const user = userEvent.setup(); - vi.mocked(updateSurveyAction).mockResolvedValue({ data: mockSurvey }); - - render(); - - const titleInput = screen.getByDisplayValue("Test Title"); - await user.clear(titleInput); - await user.type(titleInput, "Updated Title"); - - const saveButton = screen.getByTestId("save-button"); - await user.click(saveButton); - - await waitFor(() => { - expect(vi.mocked(updateSurveyAction)).toHaveBeenCalled(); - }); - }); - - test("does not submit when form is saving", async () => { - const user = userEvent.setup(); - let resolvePromise: (value: any) => void; - const pendingPromise = new Promise((resolve) => { - resolvePromise = resolve; - }); - vi.mocked(updateSurveyAction).mockReturnValue(pendingPromise as any); - - render(); - - // Make form dirty first - const titleInput = screen.getByDisplayValue("Test Title"); - await user.clear(titleInput); - await user.type(titleInput, "Modified Title"); - - const saveButton = screen.getByTestId("save-button"); - - // First click should trigger submission - await user.click(saveButton); - - // Wait a bit to ensure the first submission started - await new Promise((resolve) => setTimeout(resolve, 10)); - - // Second click should be prevented because form is saving - await user.click(saveButton); - - // Should only be called once - expect(vi.mocked(updateSurveyAction)).toHaveBeenCalledTimes(1); - - // Clean up by resolving the promise - resolvePromise!({ data: mockSurvey }); - }); - - test("does not submit when isReadOnly is true", async () => { - const user = userEvent.setup(); - render(); - - const saveButton = screen.getByTestId("save-button"); - await user.click(saveButton); - - expect(vi.mocked(updateSurveyAction)).not.toHaveBeenCalled(); - }); - - test("handles ogImage correctly in form submission", async () => { - const user = userEvent.setup(); - render(); - - const fileInput = screen.getByTestId("file-input"); - fireEvent.change(fileInput, { target: { value: "https://example.com/new-image.png" } }); - - const saveButton = screen.getByTestId("save-button"); - await user.click(saveButton); - - await waitFor(() => { - expect(vi.mocked(updateSurveyAction)).toHaveBeenCalled(); - }); - - const callArgs = vi.mocked(updateSurveyAction).mock.calls[0][0]; - expect(callArgs.metadata.ogImage).toBe("https://example.com/new-image.png"); - }); - - test("handles empty ogImage correctly in form submission", async () => { - const user = userEvent.setup(); - render(); - - const fileInput = screen.getByTestId("file-input"); - fireEvent.change(fileInput, { target: { value: "" } }); - - const saveButton = screen.getByTestId("save-button"); - await user.click(saveButton); - - await waitFor(() => { - expect(vi.mocked(updateSurveyAction)).toHaveBeenCalled(); - }); - - const callArgs = vi.mocked(updateSurveyAction).mock.calls[0][0]; - expect(callArgs.metadata.ogImage).toBeUndefined(); - }); - - test("merges form data with existing metadata correctly", async () => { - const user = userEvent.setup(); - const surveyWithPartialMetadata: TSurvey = { - ...mockSurvey, - metadata: { - title: { default: "Existing Title" }, - description: { default: "Existing Description" }, - }, - }; - - vi.mocked(useSurvey).mockReturnValue({ - survey: surveyWithPartialMetadata, - }); - - render(); - - const titleInput = screen.getByDisplayValue("Existing Title"); - await user.clear(titleInput); - await user.type(titleInput, "New Title"); - - const saveButton = screen.getByTestId("save-button"); - await user.click(saveButton); - - await waitFor(() => { - expect(vi.mocked(updateSurveyAction)).toHaveBeenCalled(); - }); - - const callArgs = vi.mocked(updateSurveyAction).mock.calls[0][0]; - expect(callArgs.metadata.title?.default).toBe("New Title"); - expect(callArgs.metadata.description?.default).toBe("Existing Description"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx deleted file mode 100644 index 180cdbdf85..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx +++ /dev/null @@ -1,526 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { generatePersonalLinksAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { PersonalLinksTab } from "./personal-links-tab"; - -// Mock dependencies -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions", () => ({ - generatePersonalLinksAction: vi.fn(), -})); - -vi.mock("react-hot-toast", () => ({ - default: { - loading: vi.fn(), - success: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -// Mock UI components -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children, variant }: any) => ( -
    - {children} -
    - ), - AlertButton: ({ children }: any) =>
    {children}
    , - AlertDescription: ({ children }: any) =>
    {children}
    , - AlertTitle: ({ children }: any) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, disabled, loading, className, ...props }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/date-picker", () => ({ - DatePicker: ({ date, updateSurveyDate, minDate, onClearDate }: any) => ( -
    - { - const newDate = e.target.value ? new Date(e.target.value) : null; - updateSurveyDate(newDate); - }} - /> - -
    {minDate ? minDate.toISOString() : ""}
    -
    - ), -})); - -vi.mock("@/modules/ui/components/select", () => { - let globalOnValueChange: ((value: string) => void) | null = null; - - return { - Select: ({ children, value, onValueChange, disabled }: any) => { - globalOnValueChange = onValueChange; - return ( -
    -
    {value || "Select option"}
    - {children} -
    - ); - }, - SelectContent: ({ children }: any) =>
    {children}
    , - SelectItem: ({ children, value }: any) => ( -
    { - if (globalOnValueChange) { - globalOnValueChange(value); - } - }}> - {children} -
    - ), - SelectTrigger: ({ children, className }: any) => ( -
    - {children} -
    - ), - SelectValue: ({ placeholder }: any) =>
    {placeholder}
    , - }; -}); - -// Mock icons -vi.mock("lucide-react", () => ({ - DownloadIcon: () =>
    , - KeyIcon: () =>
    , -})); - -// Mock Next.js Link -vi.mock("next/link", () => ({ - default: ({ children, href, target, rel }: any) => ( - - {children} - - ), -})); - -const mockGeneratePersonalLinksAction = vi.mocked(generatePersonalLinksAction); -const mockToast = vi.mocked(toast); -const mockGetFormattedErrorMessage = vi.mocked(getFormattedErrorMessage); - -// Mock segments data -const mockSegments = [ - { - id: "segment1", - title: "Public Segment 1", - isPrivate: false, - description: "Test segment 1", - filters: [], - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "env1", - surveys: [], - }, - { - id: "segment2", - title: "Public Segment 2", - isPrivate: false, - description: "Test segment 2", - filters: [], - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "env1", - surveys: [], - }, - { - id: "segment3", - title: "Private Segment", - isPrivate: true, - description: "Test private segment", - filters: [], - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "env1", - surveys: [], - }, -]; - -const defaultProps = { - environmentId: "test-env-id", - surveyId: "test-survey-id", - segments: mockSegments, - isContactsEnabled: true, - isFormbricksCloud: false, -}; - -// Helper function to trigger select change -const selectOption = (value: string) => { - const selectItems = screen.getAllByTestId("select-item"); - const targetItem = selectItems.find((item) => item.getAttribute("data-value") === value); - if (targetItem) { - fireEvent.click(targetItem); - } -}; - -describe("PersonalLinksTab", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders recipients section with segment selection", () => { - render(); - - expect(screen.getByText("common.recipients")).toBeInTheDocument(); - expect(screen.getByTestId("select")).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.share.personal_links.create_and_manage_segments") - ).toBeInTheDocument(); - }); - - test("renders expiry date section with date picker", () => { - render(); - - expect( - screen.getByText("environments.surveys.share.personal_links.expiry_date_optional") - ).toBeInTheDocument(); - expect(screen.getByTestId("date-picker")).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.share.personal_links.expiry_date_description") - ).toBeInTheDocument(); - }); - - test("renders generate button with correct initial state", () => { - render(); - - const button = screen.getByTestId("button"); - expect(button).toBeInTheDocument(); - expect(button).toBeDisabled(); - expect( - screen.getByText("environments.surveys.share.personal_links.generate_and_download_links") - ).toBeInTheDocument(); - expect(screen.getByTestId("download-icon")).toBeInTheDocument(); - }); - - test("renders info alert with correct content", () => { - render(); - - expect(screen.getByTestId("alert")).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.share.personal_links.work_with_segments") - ).toBeInTheDocument(); - expect(screen.getByTestId("link")).toHaveAttribute( - "href", - "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting#segment-configuration" - ); - }); - - test("filters out private segments and shows only public segments", () => { - render(); - - const selectItems = screen.getAllByTestId("select-item"); - expect(selectItems).toHaveLength(2); // Only public segments - expect(selectItems[0]).toHaveTextContent("Public Segment 1"); - expect(selectItems[1]).toHaveTextContent("Public Segment 2"); - }); - - test("shows no segments message when no public segments available", () => { - const propsWithPrivateSegments = { - ...defaultProps, - segments: [mockSegments[2]], // Only private segment - }; - - render(); - - expect( - screen.getByText("environments.surveys.share.personal_links.no_segments_available") - ).toBeInTheDocument(); - expect(screen.getByTestId("select")).toHaveAttribute("data-disabled", "true"); - expect(screen.getByTestId("button")).toBeDisabled(); - }); - - test("enables button when segment is selected", () => { - render(); - - // Initially disabled - expect(screen.getByTestId("button")).toBeDisabled(); - - // Select a segment - selectOption("segment1"); - - // Should now be enabled - expect(screen.getByTestId("button")).not.toBeDisabled(); - }); - - test("handles date selection correctly", () => { - render(); - - const dateInput = screen.getByTestId("date-input"); - const testDate = "2024-12-31"; - - fireEvent.change(dateInput, { target: { value: testDate } }); - - expect(dateInput).toHaveValue(testDate); - }); - - test("clears date when clear button is clicked", () => { - render(); - - const dateInput = screen.getByTestId("date-input"); - const clearButton = screen.getByTestId("clear-date"); - - // Set a date first - fireEvent.change(dateInput, { target: { value: "2024-12-31" } }); - - // Clear the date - fireEvent.click(clearButton); - - expect(dateInput).toHaveValue(""); - }); - - test("sets minimum date to tomorrow", () => { - render(); - - const minDateElement = screen.getByTestId("min-date"); - // Should have some ISO date string for a future date - expect(minDateElement.textContent).toMatch(/\d{4}-\d{2}-\d{2}T/); - }); - - test("successfully generates and downloads links", async () => { - const mockResult = { - data: { - downloadUrl: "https://example.com/download/file.csv", - fileName: "personal-links.csv", - count: 5, - }, - }; - mockGeneratePersonalLinksAction.mockResolvedValue(mockResult); - - render(); - - // Select a segment - selectOption("segment1"); - - // Click generate button - const generateButton = screen.getByTestId("button"); - fireEvent.click(generateButton); - - // Verify action was called - await waitFor(() => { - expect(mockGeneratePersonalLinksAction).toHaveBeenCalledWith({ - surveyId: "test-survey-id", - segmentId: "segment1", - environmentId: "test-env-id", - expirationDays: undefined, - }); - }); - - // Verify loading toast - expect(mockToast.loading).toHaveBeenCalledWith( - "environments.surveys.share.personal_links.generating_links_toast", - { - duration: 5000, - id: "generating-links", - } - ); - }); - - test("generates links with expiry date when date is selected", async () => { - const mockResult = { - data: { - downloadUrl: "https://example.com/download/file.csv", - fileName: "personal-links.csv", - count: 3, - }, - }; - mockGeneratePersonalLinksAction.mockResolvedValue(mockResult); - - render(); - - // Select a segment - selectOption("segment1"); - - // Set expiry date (10 days from now) - const dateInput = screen.getByTestId("date-input"); - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + 10); - const expiryDate = futureDate.toISOString().split("T")[0]; - fireEvent.change(dateInput, { target: { value: expiryDate } }); - - // Click generate button - const generateButton = screen.getByTestId("button"); - fireEvent.click(generateButton); - - await waitFor(() => { - expect(mockGeneratePersonalLinksAction).toHaveBeenCalledWith({ - surveyId: "test-survey-id", - segmentId: "segment1", - environmentId: "test-env-id", - expirationDays: expect.any(Number), - }); - }); - - // Verify that expirationDays is a reasonable value (between 9-10 days) - const callArgs = mockGeneratePersonalLinksAction.mock.calls[0][0]; - expect(callArgs.expirationDays).toBeGreaterThanOrEqual(9); - expect(callArgs.expirationDays).toBeLessThanOrEqual(10); - }); - - test("handles error response from generatePersonalLinksAction", async () => { - const mockErrorResult = { - serverError: "Test error message", - }; - mockGeneratePersonalLinksAction.mockResolvedValue(mockErrorResult); - mockGetFormattedErrorMessage.mockReturnValue("Formatted error message"); - - render(); - - // Select a segment - selectOption("segment1"); - - // Click generate button - const generateButton = screen.getByTestId("button"); - fireEvent.click(generateButton); - - // Wait for the action to be called - await waitFor(() => { - expect(mockGeneratePersonalLinksAction).toHaveBeenCalledWith({ - surveyId: "test-survey-id", - segmentId: "segment1", - environmentId: "test-env-id", - expirationDays: undefined, - }); - }); - - // Wait for error handling - await waitFor(() => { - expect(mockGetFormattedErrorMessage).toHaveBeenCalledWith(mockErrorResult); - expect(mockToast.error).toHaveBeenCalledWith("Formatted error message", { - duration: 5000, - id: "generating-links", - }); - }); - }); - - test("shows generating state when triggered", async () => { - // Mock a promise that resolves quickly - const mockResult = { data: { downloadUrl: "test", fileName: "test.csv", count: 1 } }; - mockGeneratePersonalLinksAction.mockResolvedValue(mockResult); - - render(); - - // Select a segment - selectOption("segment1"); - - // Click generate button - const generateButton = screen.getByTestId("button"); - fireEvent.click(generateButton); - - // Verify loading toast is called - expect(mockToast.loading).toHaveBeenCalledWith( - "environments.surveys.share.personal_links.generating_links_toast", - { - duration: 5000, - id: "generating-links", - } - ); - }); - - test("button is disabled when no segment is selected", () => { - render(); - - const generateButton = screen.getByTestId("button"); - expect(generateButton).toBeDisabled(); - }); - - test("button is disabled when no public segments are available", () => { - const propsWithNoPublicSegments = { - ...defaultProps, - segments: [mockSegments[2]], // Only private segments - }; - - render(); - - const generateButton = screen.getByTestId("button"); - expect(generateButton).toBeDisabled(); - }); - - test("handles empty segments array", () => { - const propsWithEmptySegments = { - ...defaultProps, - segments: [], - }; - - render(); - - expect( - screen.getByText("environments.surveys.share.personal_links.no_segments_available") - ).toBeInTheDocument(); - expect(screen.getByTestId("button")).toBeDisabled(); - }); - - test("calculates expiration days correctly for different dates", async () => { - const mockResult = { - data: { - downloadUrl: "https://example.com/download/file.csv", - fileName: "test.csv", - count: 1, - }, - }; - mockGeneratePersonalLinksAction.mockResolvedValue(mockResult); - - render(); - - // Select a segment - selectOption("segment1"); - - // Set expiry date to 5 days from now - const dateInput = screen.getByTestId("date-input"); - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + 5); - const expiryDate = futureDate.toISOString().split("T")[0]; - fireEvent.change(dateInput, { target: { value: expiryDate } }); - - // Click generate button - const generateButton = screen.getByTestId("button"); - fireEvent.click(generateButton); - - await waitFor(() => { - expect(mockGeneratePersonalLinksAction).toHaveBeenCalledWith({ - surveyId: "test-survey-id", - segmentId: "segment1", - environmentId: "test-env-id", - expirationDays: expect.any(Number), - }); - }); - - // Verify that expirationDays is a reasonable value (between 4-5 days) - const callArgs = mockGeneratePersonalLinksAction.mock.calls[0][0]; - expect(callArgs.expirationDays).toBeGreaterThanOrEqual(4); - expect(callArgs.expirationDays).toBeLessThanOrEqual(5); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx deleted file mode 100644 index c8492f5f42..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { QRCodeTab } from "./qr-code-tab"; - -// Mock the QR code options utility -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/get-qr-code-options", - () => ({ - getQRCodeOptions: vi.fn((width: number, height: number) => ({ - width, - height, - type: "svg", - data: "", - margin: 0, - qrOptions: { - typeNumber: 0, - mode: "Byte", - errorCorrectionLevel: "L", - }, - imageOptions: { - saveAsBlob: true, - hideBackgroundDots: false, - imageSize: 0, - margin: 0, - }, - dotsOptions: { - type: "extra-rounded", - color: "#000000", - roundSize: true, - }, - backgroundOptions: { - color: "#ffffff", - }, - cornersSquareOptions: { - type: "dot", - color: "#000000", - }, - cornersDotOptions: { - type: "dot", - color: "#000000", - }, - })), - }) -); - -// Mock UI components -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children, variant }: { children: React.ReactNode; variant?: string }) => ( -
    - {children} -
    - ), - AlertDescription: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - AlertTitle: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ - children, - onClick, - disabled, - variant, - size, - className, - }: { - children: React.ReactNode; - onClick?: () => void; - disabled?: boolean; - variant?: string; - size?: string; - className?: string; - }) => ( - - ), -})); - -// Mock lucide-react icons -vi.mock("lucide-react", () => ({ - Download: () =>
    Download
    , - LoaderCircle: ({ className }: { className?: string }) => ( -
    - LoaderCircle -
    - ), - RefreshCw: ({ className }: { className?: string }) => ( -
    - RefreshCw -
    - ), -})); - -// Mock logger -vi.mock("@formbricks/logger", () => ({ - logger: { - error: vi.fn(), - }, -})); - -// Mock QRCodeStyling -const mockQRCodeStyling = { - update: vi.fn(), - append: vi.fn(), - download: vi.fn(), -}; - -// Simple boolean flag to control mock behavior -let shouldMockThrowError = false; - -// @ts-ignore - Ignore TypeScript error for mock -vi.mock("qr-code-styling", () => ({ - default: vi.fn(() => { - // Default to success, only throw error when explicitly requested - if (shouldMockThrowError) { - throw new Error("QR code generation failed"); - } - return mockQRCodeStyling; - }), -})); - -const mockSurveyUrl = "https://example.com/survey/123"; - -describe("QRCodeTab", () => { - beforeEach(() => { - vi.resetAllMocks(); - vi.clearAllMocks(); - - // Reset to success state by default - shouldMockThrowError = false; - - // Reset mock implementations - mockQRCodeStyling.update.mockReset(); - mockQRCodeStyling.append.mockReset(); - mockQRCodeStyling.download.mockReset(); - - // Set up default mock behavior - mockQRCodeStyling.update.mockImplementation(() => {}); - mockQRCodeStyling.append.mockImplementation(() => {}); - mockQRCodeStyling.download.mockImplementation(() => {}); - }); - - afterEach(() => { - cleanup(); - }); - - describe("QR Code generation", () => { - test("attempts to generate QR code when surveyUrl is provided", async () => { - render(); - - // Wait for either success or error state - await waitFor(() => { - const hasButton = screen.queryByTestId("button"); - const hasAlert = screen.queryByTestId("alert"); - expect(hasButton || hasAlert).toBeTruthy(); - }); - }); - - test("shows download button when QR code generation succeeds", async () => { - render(); - - await waitFor(() => { - expect(screen.getByTestId("button")).toBeInTheDocument(); - }); - }); - }); - - describe("Error handling", () => { - test("shows error state when QR code generation fails", async () => { - shouldMockThrowError = true; - - render(); - - await waitFor(() => { - expect(screen.getByTestId("alert")).toBeInTheDocument(); - }); - - expect(screen.getByTestId("alert-title")).toHaveTextContent("common.something_went_wrong"); - expect(screen.getByTestId("alert-description")).toHaveTextContent( - "environments.surveys.summary.qr_code_generation_failed" - ); - }); - }); - - describe("Download functionality", () => { - test("has clickable download button when QR code is available", async () => { - render(); - - await waitFor(() => { - expect(screen.getByTestId("button")).toBeInTheDocument(); - }); - - const downloadButton = screen.getByTestId("button"); - expect(downloadButton).toBeInTheDocument(); - expect(downloadButton).toHaveAttribute("type", "button"); - - // Button should be clickable - await userEvent.click(downloadButton); - // If the button is clicked without throwing, it's working - }); - - test("handles button interactions properly", async () => { - render(); - - await waitFor(() => { - expect(screen.getByTestId("button")).toBeInTheDocument(); - }); - - const button = screen.getByTestId("button"); - expect(button).toBeInTheDocument(); - - // Test that button can be interacted with - await userEvent.click(button); - - // Button should still be present after click - expect(screen.getByTestId("button")).toBeInTheDocument(); - }); - - test("shows appropriate state when surveyUrl is empty", async () => { - render(); - - // Should show button (but disabled) when URL is empty, no alert - const button = screen.getByTestId("button"); - expect(button).toBeInTheDocument(); - expect(button).toBeDisabled(); - expect(screen.queryByTestId("alert")).not.toBeInTheDocument(); - }); - }); - - describe("Component lifecycle", () => { - test("responds to surveyUrl changes", async () => { - const { rerender } = render(); - - // Initial render should show download button - await waitFor(() => { - expect(screen.getByTestId("button")).toBeInTheDocument(); - }); - - const newSurveyUrl = "https://example.com/survey/456"; - rerender(); - - // After rerender, button should still be present - await waitFor(() => { - expect(screen.getByTestId("button")).toBeInTheDocument(); - }); - }); - }); - - describe("Accessibility", () => { - test("has proper button labels and states", async () => { - render(); - - await waitFor(() => { - const downloadButton = screen.getByTestId("button"); - expect(downloadButton).toBeInTheDocument(); - expect(downloadButton).toHaveAttribute("type", "button"); - }); - }); - - test("shows appropriate loading or success state", async () => { - render(); - - // Component should show either loading or success content - await waitFor(() => { - const hasButton = screen.queryByTestId("button"); - const hasLoader = screen.queryByTestId("loader-circle"); - expect(hasButton || hasLoader).toBeTruthy(); - }); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx deleted file mode 100644 index 2c6604e11d..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx +++ /dev/null @@ -1,624 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TUserLocale } from "@formbricks/types/user"; -import { LinkTabsType, ShareSettingsType, ShareViaType } from "../../types/share"; -import { ShareView } from "./share-view"; - -// Mock sidebar components -vi.mock("@/modules/ui/components/sidebar", () => ({ - SidebarProvider: ({ children, open, className, style }: any) => ( -
    - {children} -
    - ), - Sidebar: ({ children }: { children: React.ReactNode }) =>
    {children}
    , - SidebarContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - SidebarGroup: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - SidebarGroupContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - SidebarGroupLabel: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - SidebarMenu: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - SidebarMenuItem: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - SidebarMenuButton: ({ - children, - onClick, - tooltip, - className, - isActive, - }: { - children: React.ReactNode; - onClick: () => void; - tooltip: string; - className?: string; - isActive?: boolean; - }) => ( - - ), -})); - -// Mock child components -vi.mock("./EmailTab", () => ({ - EmailTab: (props: { surveyId: string; email: string }) => ( -
    - EmailTab Content for {props.surveyId} with {props.email} -
    - ), -})); - -vi.mock("./anonymous-links-tab", () => ({ - AnonymousLinksTab: (props: { - survey: TSurvey; - surveyUrl: string; - publicDomain: string; - setSurveyUrl: (url: string) => void; - locale: TUserLocale; - }) => ( -
    - AnonymousLinksTab Content for {props.survey.id} at {props.surveyUrl} -
    - ), -})); - -vi.mock("./qr-code-tab", () => ({ - QRCodeTab: (props: { surveyUrl: string }) => ( -
    QRCodeTab Content for {props.surveyUrl}
    - ), -})); -vi.mock("./website-embed-tab", () => ({ - WebsiteEmbedTab: (props: { surveyUrl: string }) => ( -
    WebsiteEmbedTab Content for {props.surveyUrl}
    - ), -})); - -vi.mock("./dynamic-popup-tab", () => ({ - DynamicPopupTab: (props: { environmentId: string; surveyId: string }) => ( -
    - DynamicPopupTab Content for {props.surveyId} in {props.environmentId} -
    - ), -})); -vi.mock("./tab-container", () => ({ - TabContainer: (props: { children: React.ReactNode; title: string; description: string }) => ( -
    -
    {props.title}
    -
    {props.description}
    - {props.children} -
    - ), -})); - -vi.mock("./personal-links-tab", () => ({ - PersonalLinksTab: (props: { surveyId: string; environmentId: string }) => ( -
    - PersonalLinksTab Content for {props.surveyId} in {props.environmentId} -
    - ), -})); - -vi.mock("./social-media-tab", () => ({ - SocialMediaTab: (props: { surveyUrl: string; surveyTitle: string }) => ( -
    - SocialMediaTab Content for {props.surveyTitle} at {props.surveyUrl} -
    - ), -})); - -vi.mock("@/modules/ui/components/upgrade-prompt", () => ({ - UpgradePrompt: (props: { title: string; description: string }) => ( -
    - {props.title} - {props.description} -
    - ), -})); - -// Mock lucide-react -vi.mock("lucide-react", () => ({ - CopyIcon: () =>
    CopyIcon
    , - ArrowLeftIcon: () =>
    ArrowLeftIcon
    , - ArrowUpRightIcon: () =>
    ArrowUpRightIcon
    , - MailIcon: () =>
    MailIcon
    , - LinkIcon: () =>
    LinkIcon
    , - GlobeIcon: () =>
    GlobeIcon
    , - SmartphoneIcon: () =>
    SmartphoneIcon
    , - CheckCircle2Icon: () =>
    CheckCircle2Icon
    , - AlertCircleIcon: ({ className }: { className?: string }) => ( -
    - AlertCircleIcon -
    - ), - AlertTriangleIcon: ({ className }: { className?: string }) => ( -
    - AlertTriangleIcon -
    - ), - InfoIcon: ({ className }: { className?: string }) => ( -
    - InfoIcon -
    - ), - Download: ({ className }: { className?: string }) => ( -
    - Download -
    - ), - Code2Icon: () =>
    Code2Icon
    , - QrCodeIcon: () =>
    QrCodeIcon
    , - Share2Icon: () =>
    Share2Icon
    , - SquareStack: () =>
    SquareStack
    , - UserIcon: () =>
    UserIcon
    , - Settings: () =>
    Settings
    , -})); - -// Mock tooltip and typography components -vi.mock("@/modules/ui/components/tooltip", () => ({ - TooltipRenderer: ({ children }: { children: React.ReactNode }) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/typography", () => ({ - Small: ({ children }: { children: React.ReactNode }) => {children}, -})); - -// Mock button component -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ - children, - onClick, - className, - variant, - }: { - children: React.ReactNode; - onClick: () => void; - className?: string; - variant?: string; - }) => ( - - ), -})); - -// Mock cn utility -vi.mock("@/lib/cn", () => ({ - cn: (...args: any[]) => args.filter(Boolean).join(" "), -})); - -// Mock i18n -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -// Mock component imports for tabs -const MockEmailTab = ({ surveyId, email }: { surveyId: string; email: string }) => ( -
    - EmailTab Content for {surveyId} with {email} -
    -); - -const MockAnonymousLinksTab = ({ survey, surveyUrl }: { survey: any; surveyUrl: string }) => ( -
    - AnonymousLinksTab Content for {survey.id} at {surveyUrl} -
    -); - -const MockWebsiteEmbedTab = ({ surveyUrl }: { surveyUrl: string }) => ( -
    WebsiteEmbedTab Content for {surveyUrl}
    -); - -const MockDynamicPopupTab = ({ environmentId, surveyId }: { environmentId: string; surveyId: string }) => ( -
    - DynamicPopupTab Content for {surveyId} in {environmentId} -
    -); - -const MockQRCodeTab = ({ surveyUrl }: { surveyUrl: string }) => ( -
    QRCodeTab Content for {surveyUrl}
    -); - -const MockPersonalLinksTab = ({ surveyId, environmentId }: { surveyId: string; environmentId: string }) => ( -
    - PersonalLinksTab Content for {surveyId} in {environmentId} -
    -); - -const MockSocialMediaTab = ({ surveyUrl, surveyTitle }: { surveyUrl: string; surveyTitle: string }) => ( -
    - SocialMediaTab Content for {surveyTitle} at {surveyUrl} -
    -); - -const mockSurvey = { - id: "survey1", - type: "link", - name: "Test Survey", - status: "inProgress", - environmentId: "env1", - createdAt: new Date(), - updatedAt: new Date(), - questions: [], - displayOption: "displayOnce", - recontactDays: 0, - triggers: [], - languages: [], - autoClose: null, - delay: 0, - autoComplete: null, - singleUse: { enabled: false, isEncrypted: false }, - styling: null, -} as any; - -// Mock LinkSettingsTab -const MockLinkSettingsTab = ({ - survey, - isReadOnly, - locale, -}: { - survey: any; - isReadOnly: boolean; - locale: string; -}) => ( -
    - LinkSettingsTab Content for {survey.id} - ReadOnly: {isReadOnly.toString()} - Locale: {locale} -
    -); - -const mockTabs = [ - { - id: ShareViaType.EMAIL, - type: LinkTabsType.SHARE_VIA, - label: "Email", - icon: () =>
    , - componentType: MockEmailTab, - componentProps: { surveyId: "survey1", email: "test@example.com" }, - title: "Send Email", - description: "Send survey via email", - }, - { - id: ShareViaType.WEBSITE_EMBED, - type: LinkTabsType.SHARE_VIA, - label: "Website Embed", - icon: () =>
    , - componentType: MockWebsiteEmbedTab, - componentProps: { surveyUrl: "http://example.com/survey1" }, - title: "Embed on Website", - description: "Embed survey on your website", - }, - { - id: ShareViaType.DYNAMIC_POPUP, - type: LinkTabsType.SHARE_VIA, - label: "Dynamic Popup", - icon: () =>
    , - componentType: MockDynamicPopupTab, - componentProps: { environmentId: "env1", surveyId: "survey1" }, - title: "Dynamic Popup", - description: "Show survey as popup", - }, - { - id: ShareViaType.ANON_LINKS, - type: LinkTabsType.SHARE_VIA, - label: "Anonymous Links", - icon: () =>
    , - componentType: MockAnonymousLinksTab, - componentProps: { - survey: mockSurvey, - surveyUrl: "http://example.com/survey1", - publicDomain: "http://example.com", - setSurveyUrl: vi.fn(), - locale: "en" as any, - }, - title: "Anonymous Links", - description: "Share anonymous links", - }, - { - id: ShareViaType.QR_CODE, - type: LinkTabsType.SHARE_VIA, - label: "QR Code", - icon: () =>
    , - componentType: MockQRCodeTab, - componentProps: { surveyUrl: "http://example.com/survey1" }, - title: "QR Code", - description: "Generate QR code", - }, - { - id: ShareViaType.PERSONAL_LINKS, - type: LinkTabsType.SHARE_VIA, - label: "Personal Links", - icon: () =>
    , - componentType: MockPersonalLinksTab, - componentProps: { surveyId: "survey1", environmentId: "env1" }, - title: "Personal Links", - description: "Create personal links", - }, - { - id: ShareViaType.SOCIAL_MEDIA, - type: LinkTabsType.SHARE_VIA, - label: "Social Media", - icon: () =>
    , - componentType: MockSocialMediaTab, - componentProps: { surveyUrl: "http://example.com/survey1", surveyTitle: "Test Survey" }, - title: "Social Media", - description: "Share on social media", - }, - { - id: ShareSettingsType.LINK_SETTINGS, - type: LinkTabsType.SHARE_SETTING, - label: "Link Settings", - icon: () =>
    , - componentType: MockLinkSettingsTab, - componentProps: { survey: mockSurvey, isReadOnly: false, locale: "en-US" }, - title: "Link Settings", - description: "Configure link settings", - }, -]; - -const defaultProps = { - tabs: mockTabs, - activeId: ShareViaType.EMAIL, - setActiveId: vi.fn(), -}; - -// Mock window object for resize testing -Object.defineProperty(window, "innerWidth", { - writable: true, - configurable: true, - value: 1024, -}); - -describe("ShareView", () => { - beforeEach(() => { - // Reset window size to default before each test - window.innerWidth = 1024; - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders sidebar with tabs", () => { - render(); - - // Sidebar should always be rendered - const sidebarLabel = screen.getByText("environments.surveys.share.share_view_title"); - expect(sidebarLabel).toBeInTheDocument(); - }); - - test("renders desktop tabs", () => { - render(); - - // Desktop sidebar should be rendered - const sidebarLabel = screen.getByText("environments.surveys.share.share_view_title"); - expect(sidebarLabel).toBeInTheDocument(); - }); - - test("renders share settings section", () => { - render(); - - // Share settings section should be rendered - const shareSettingsLabel = screen.getByText("environments.surveys.share.share_settings_title"); - expect(shareSettingsLabel).toBeInTheDocument(); - }); - - test("calls setActiveId when a tab is clicked (desktop)", async () => { - render(); - - const websiteEmbedTabButton = screen.getByLabelText("Website Embed"); - await userEvent.click(websiteEmbedTabButton); - expect(defaultProps.setActiveId).toHaveBeenCalledWith(ShareViaType.WEBSITE_EMBED); - }); - - test("renders EmailTab when activeId is EMAIL", () => { - render(); - expect(screen.getByTestId("email-tab")).toBeInTheDocument(); - expect(screen.getByText("EmailTab Content for survey1 with test@example.com")).toBeInTheDocument(); - }); - - test("renders WebsiteEmbedTab when activeId is WEBSITE_EMBED", () => { - render(); - expect(screen.getByTestId("tab-container")).toBeInTheDocument(); - expect(screen.getByTestId("website-embed-tab")).toBeInTheDocument(); - expect(screen.getByText("WebsiteEmbedTab Content for http://example.com/survey1")).toBeInTheDocument(); - }); - - test("renders DynamicPopupTab when activeId is DYNAMIC_POPUP", () => { - render(); - expect(screen.getByTestId("tab-container")).toBeInTheDocument(); - expect(screen.getByTestId("dynamic-popup-tab")).toBeInTheDocument(); - expect(screen.getByText("DynamicPopupTab Content for survey1 in env1")).toBeInTheDocument(); - }); - - test("renders AnonymousLinksTab when activeId is ANON_LINKS", () => { - render(); - expect(screen.getByTestId("anonymous-links-tab")).toBeInTheDocument(); - expect( - screen.getByText("AnonymousLinksTab Content for survey1 at http://example.com/survey1") - ).toBeInTheDocument(); - }); - - test("renders QRCodeTab when activeId is QR_CODE", () => { - render(); - expect(screen.getByTestId("qr-code-tab")).toBeInTheDocument(); - }); - - test("renders LinkSettingsTab when activeId is LINK_SETTINGS", () => { - render(); - expect(screen.getByTestId("link-settings-tab")).toBeInTheDocument(); - expect( - screen.getByText("LinkSettingsTab Content for survey1 - ReadOnly: false - Locale: en-US") - ).toBeInTheDocument(); - }); - - test("renders nothing when activeId doesn't match any tab", () => { - // Create a special case with no matching tab - const propsWithNoMatchingTab = { - ...defaultProps, - tabs: mockTabs.slice(0, 3), // Only include first 3 tabs - activeId: ShareViaType.SOCIAL_MEDIA, // Use a tab not in the subset - }; - - render(); - - // Should not render any tab content for non-matching activeId - expect(screen.queryByTestId("email-tab")).not.toBeInTheDocument(); - expect(screen.queryByTestId("website-embed-tab")).not.toBeInTheDocument(); - expect(screen.queryByTestId("dynamic-popup-tab")).not.toBeInTheDocument(); - expect(screen.queryByTestId("anonymous-links-tab")).not.toBeInTheDocument(); - expect(screen.queryByTestId("qr-code-tab")).not.toBeInTheDocument(); - expect(screen.queryByTestId("personal-links-tab")).not.toBeInTheDocument(); - expect(screen.queryByTestId("social-media-tab")).not.toBeInTheDocument(); - expect(screen.queryByTestId("link-settings-tab")).not.toBeInTheDocument(); - }); - - test("renders PersonalLinksTab when activeId is PERSONAL_LINKS", () => { - render(); - expect(screen.getByTestId("personal-links-tab")).toBeInTheDocument(); - expect(screen.getByText("PersonalLinksTab Content for survey1 in env1")).toBeInTheDocument(); - }); - - test("renders SocialMediaTab when activeId is SOCIAL_MEDIA", () => { - render(); - expect(screen.getByTestId("social-media-tab")).toBeInTheDocument(); - expect( - screen.getByText("SocialMediaTab Content for Test Survey at http://example.com/survey1") - ).toBeInTheDocument(); - }); - - test("calls setActiveId when a responsive tab is clicked", async () => { - render(); - - // Get responsive buttons - these are Button components containing icons - const responsiveButtons = screen.getAllByTestId("website-embed-tab-icon"); - // The responsive button should be the one inside the md:hidden container - const responsiveButton = responsiveButtons - .find((icon) => { - const button = icon.closest("button"); - return button && button.getAttribute("data-variant") === "ghost"; - }) - ?.closest("button"); - - if (responsiveButton) { - await userEvent.click(responsiveButton); - expect(defaultProps.setActiveId).toHaveBeenCalledWith(ShareViaType.WEBSITE_EMBED); - } - }); - - test("applies active styles to the active tab (desktop)", () => { - render(); - - const emailTabButton = screen.getByLabelText("Email"); - expect(emailTabButton).toHaveClass("bg-slate-100"); - expect(emailTabButton).toHaveClass("font-medium"); - expect(emailTabButton).toHaveClass("text-slate-900"); - - const websiteEmbedTabButton = screen.getByLabelText("Website Embed"); - expect(websiteEmbedTabButton).not.toHaveClass("bg-slate-100"); - expect(websiteEmbedTabButton).not.toHaveClass("font-medium"); - }); - - test("applies active styles to the active tab (responsive)", () => { - render(); - - // Get responsive buttons - these are Button components with ghost variant - const responsiveButtons = screen.getAllByTestId("email-tab-icon"); - const responsiveEmailButton = responsiveButtons - .find((icon) => { - const button = icon.closest("button"); - return button && button.getAttribute("data-variant") === "ghost"; - }) - ?.closest("button"); - - if (responsiveEmailButton) { - // Check that the button has the active classes - expect(responsiveEmailButton).toHaveClass("bg-white text-slate-900 shadow-sm hover:bg-white"); - } - - const responsiveWebsiteEmbedButtons = screen.getAllByTestId("website-embed-tab-icon"); - const responsiveWebsiteEmbedButton = responsiveWebsiteEmbedButtons - .find((icon) => { - const button = icon.closest("button"); - return button && button.getAttribute("data-variant") === "ghost"; - }) - ?.closest("button"); - - if (responsiveWebsiteEmbedButton) { - expect(responsiveWebsiteEmbedButton).toHaveClass( - "border-transparent text-slate-700 hover:text-slate-900" - ); - } - }); - - test("renders all tabs from props", () => { - render(); - - // Check that all tabs are rendered in the sidebar - mockTabs.forEach((tab) => { - expect(screen.getByLabelText(tab.label)).toBeInTheDocument(); - }); - }); - - test("renders responsive buttons for all tabs", () => { - render(); - - // Check that responsive buttons are rendered for all tabs - const expectedTestIds = [ - "email-tab-icon", - "website-embed-tab-icon", - "dynamic-popup-tab-icon", - "anonymous-links-tab-icon", - "qr-code-tab-icon", - "personal-links-tab-icon", - "social-media-tab-icon", - "link-settings-tab-icon", - ]; - - expectedTestIds.forEach((testId) => { - const responsiveButtons = screen.getAllByTestId(testId); - const responsiveButton = responsiveButtons.find((icon) => { - const button = icon.closest("button"); - return button && button.getAttribute("data-variant") === "ghost"; - }); - expect(responsiveButton).toBeTruthy(); - }); - }); - - test("separates share via and share settings tabs correctly", () => { - render(); - - // Check that share via tabs are rendered - expect(screen.getByLabelText("Email")).toBeInTheDocument(); - expect(screen.getByLabelText("Website Embed")).toBeInTheDocument(); - expect(screen.getByLabelText("Anonymous Links")).toBeInTheDocument(); - - // Check that share settings tabs are rendered - expect(screen.getByLabelText("Link Settings")).toBeInTheDocument(); - - // Check that both sections have their titles - expect(screen.getByText("environments.surveys.share.share_view_title")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.share.share_settings_title")).toBeInTheDocument(); - }); - - test("calls setActiveId when a share settings tab is clicked", async () => { - render(); - - const linkSettingsTabButton = screen.getByLabelText("Link Settings"); - await userEvent.click(linkSettingsTabButton); - expect(defaultProps.setActiveId).toHaveBeenCalledWith(ShareSettingsType.LINK_SETTINGS); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.test.tsx deleted file mode 100644 index aad6e879d2..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.test.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { SocialMediaTab } from "./social-media-tab"; - -// Mock next/link -vi.mock("next/link", () => ({ - default: ({ href, children, ...props }: any) => ( - - {children} - - ), -})); - -// Mock window.open -Object.defineProperty(window, "open", { - writable: true, - value: vi.fn(), -}); - -const mockSurveyUrl = "https://app.formbricks.com/s/survey1"; -const mockSurveyTitle = "Test Survey"; - -const expectedPlatforms = [ - { name: "LinkedIn", description: "Share on LinkedIn" }, - { name: "Threads", description: "Share on Threads" }, - { name: "Facebook", description: "Share on Facebook" }, - { name: "Reddit", description: "Share on Reddit" }, - { name: "X", description: "Share on X (formerly Twitter)" }, -]; - -describe("SocialMediaTab", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders all social media platforms with correct names", () => { - render(); - - expectedPlatforms.forEach((platform) => { - expect(screen.getByText(platform.name)).toBeInTheDocument(); - }); - }); - - test("renders source tracking alert with correct content", () => { - render(); - - expect( - screen.getByText("environments.surveys.share.social_media.source_tracking_enabled") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.share.social_media.source_tracking_enabled_alert_description") - ).toBeInTheDocument(); - expect(screen.getByText("common.learn_more")).toBeInTheDocument(); - - const learnMoreButton = screen.getByRole("button", { name: "common.learn_more" }); - expect(learnMoreButton).toBeInTheDocument(); - }); - - test("renders platform buttons for all platforms", () => { - render(); - - const platformButtons = expectedPlatforms.map((platform) => - screen.getByRole("button", { name: new RegExp(platform.name, "i") }) - ); - expect(platformButtons).toHaveLength(expectedPlatforms.length); - }); - - test("opens sharing window when LinkedIn button is clicked", async () => { - const mockWindowOpen = vi.spyOn(window, "open"); - render(); - - const linkedInButton = screen.getByRole("button", { name: /linkedin/i }); - await userEvent.click(linkedInButton); - - expect(mockWindowOpen).toHaveBeenCalledWith( - expect.stringContaining("linkedin.com/shareArticle"), - "share-dialog", - "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes" - ); - }); - - test("includes source tracking in shared URLs", async () => { - const mockWindowOpen = vi.spyOn(window, "open"); - render(); - - const linkedInButton = screen.getByRole("button", { name: /linkedin/i }); - await userEvent.click(linkedInButton); - - const calledUrl = mockWindowOpen.mock.calls[0][0] as string; - const decodedUrl = decodeURIComponent(calledUrl); - expect(decodedUrl).toContain("source=linkedin"); - }); - - test("opens sharing window when Facebook button is clicked", async () => { - const mockWindowOpen = vi.spyOn(window, "open"); - render(); - - const facebookButton = screen.getByRole("button", { name: /facebook/i }); - await userEvent.click(facebookButton); - - expect(mockWindowOpen).toHaveBeenCalledWith( - expect.stringContaining("facebook.com/sharer"), - "share-dialog", - "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes" - ); - }); - - test("opens sharing window when X button is clicked", async () => { - const mockWindowOpen = vi.spyOn(window, "open"); - render(); - - const xButton = screen.getByRole("button", { name: /^x$/i }); - await userEvent.click(xButton); - - expect(mockWindowOpen).toHaveBeenCalledWith( - expect.stringContaining("twitter.com/intent/tweet"), - "share-dialog", - "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes" - ); - }); - - test("encodes URLs and titles correctly for sharing", async () => { - const specialCharUrl = "https://app.formbricks.com/s/survey1?param=test&other=value"; - const specialCharTitle = "Test Survey & More"; - const mockWindowOpen = vi.spyOn(window, "open"); - - render(); - - const linkedInButton = screen.getByRole("button", { name: /linkedin/i }); - await userEvent.click(linkedInButton); - - const calledUrl = mockWindowOpen.mock.calls[0][0] as string; - expect(calledUrl).toContain(encodeURIComponent(specialCharTitle)); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container.test.tsx deleted file mode 100644 index 1e9740e762..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container.test.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TabContainer } from "./tab-container"; - -// Mock components -vi.mock("@/modules/ui/components/typography", () => ({ - H3: (props: { children: React.ReactNode }) =>

    {props.children}

    , - Small: (props: { color?: string; margin?: string; children: React.ReactNode }) => ( -

    - {props.children} -

    - ), -})); - -describe("TabContainer", () => { - afterEach(() => { - cleanup(); - }); - - const defaultProps = { - title: "Test Tab Title", - description: "Test tab description", - children:
    Tab content
    , - }; - - test("renders title with correct props", () => { - render(); - - const title = screen.getByTestId("h3"); - expect(title).toBeInTheDocument(); - expect(title).toHaveTextContent("Test Tab Title"); - }); - - test("renders description with correct text and props", () => { - render(); - - const description = screen.getByTestId("small"); - expect(description).toBeInTheDocument(); - expect(description).toHaveTextContent("Test tab description"); - expect(description).toHaveAttribute("data-color", "muted"); - expect(description).toHaveAttribute("data-margin", "headerDescription"); - }); - - test("renders children content", () => { - render(); - - const tabContent = screen.getByTestId("tab-content"); - expect(tabContent).toBeInTheDocument(); - expect(tabContent).toHaveTextContent("Tab content"); - }); - - test("renders header with correct structure", () => { - render(); - - const header = screen.getByTestId("h3").parentElement; - expect(header).toBeInTheDocument(); - expect(header).toContainElement(screen.getByTestId("h3")); - expect(header).toContainElement(screen.getByTestId("small")); - }); - - test("renders children directly in container", () => { - render(); - - const container = screen.getByTestId("h3").parentElement?.parentElement; - expect(container).toContainElement(screen.getByTestId("tab-content")); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.test.tsx deleted file mode 100644 index a2940c726c..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.test.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { WebsiteEmbedTab } from "./website-embed-tab"; - -// Mock components -vi.mock("@/modules/ui/components/advanced-option-toggle", () => ({ - AdvancedOptionToggle: (props: { - htmlId: string; - isChecked: boolean; - onToggle: (checked: boolean) => void; - title: string; - description: string; - customContainerClass?: string; - }) => ( -
    - - props.onToggle(e.target.checked)} - data-testid="embed-mode-toggle" - /> - {props.description} - {props.customContainerClass && ( - {props.customContainerClass} - )} -
    - ), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: (props: { - title?: string; - "aria-label"?: string; - onClick?: () => void; - children: React.ReactNode; - type?: "button" | "submit" | "reset"; - }) => ( - - ), -})); - -vi.mock("@/modules/ui/components/code-block", () => ({ - CodeBlock: (props: { - language: string; - showCopyToClipboard: boolean; - noMargin?: boolean; - children: string; - }) => ( -
    - {props.language} - {props.showCopyToClipboard?.toString() || "false"} - {props.noMargin && true} -
    {props.children}
    -
    - ), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("lucide-react", () => ({ - CopyIcon: () =>
    CopyIcon
    , -})); - -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - }, -})); - -// Mock clipboard API -Object.assign(navigator, { - clipboard: { - writeText: vi.fn().mockImplementation(() => Promise.resolve()), - }, -}); - -describe("WebsiteEmbedTab", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const defaultProps = { - surveyUrl: "https://example.com/survey/123", - }; - - test("renders all components correctly", () => { - render(); - - expect(screen.getByTestId("code-block")).toBeInTheDocument(); - expect(screen.getByTestId("advanced-option-toggle")).toBeInTheDocument(); - expect(screen.getByTestId("copy-button")).toBeInTheDocument(); - expect(screen.getByTestId("copy-icon")).toBeInTheDocument(); - }); - - test("renders correct iframe code without embed mode", () => { - render(); - - const codeBlock = screen.getByTestId("code-block"); - expect(codeBlock).toBeInTheDocument(); - - const code = codeBlock.querySelector("pre")?.textContent; - expect(code).toContain(defaultProps.surveyUrl); - expect(code).toContain(" { - render(); - - const toggle = screen.getByTestId("embed-mode-toggle"); - await userEvent.click(toggle); - - const codeBlock = screen.getByTestId("code-block"); - const code = codeBlock.querySelector("pre")?.textContent; - expect(code).toContain('src="https://example.com/survey/123?embed=true"'); - }); - - test("toggle changes embed mode state", async () => { - render(); - - const toggle = screen.getByTestId("embed-mode-toggle"); - expect(toggle).not.toBeChecked(); - - await userEvent.click(toggle); - expect(toggle).toBeChecked(); - - await userEvent.click(toggle); - expect(toggle).not.toBeChecked(); - }); - - test("copy button copies iframe code to clipboard", async () => { - render(); - - const copyButton = screen.getByTestId("copy-button"); - await userEvent.click(copyButton); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith( - expect.stringContaining(defaultProps.surveyUrl) - ); - const toast = await import("react-hot-toast"); - expect(toast.default.success).toHaveBeenCalledWith( - "environments.surveys.share.embed_on_website.embed_code_copied_to_clipboard" - ); - }); - - test("copy button copies correct code with embed mode enabled", async () => { - render(); - - const toggle = screen.getByTestId("embed-mode-toggle"); - await userEvent.click(toggle); - - const copyButton = screen.getByTestId("copy-button"); - await userEvent.click(copyButton); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expect.stringContaining("?embed=true")); - }); - - test("renders code block with correct props", () => { - render(); - - expect(screen.getByTestId("language")).toHaveTextContent("html"); - expect(screen.getByTestId("show-copy")).toHaveTextContent("false"); - expect(screen.getByTestId("no-margin")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/stat-card.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/stat-card.test.tsx deleted file mode 100644 index 1a012fd040..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/stat-card.test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { StatCard } from "./stat-card"; - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/base-card", - () => ({ - BaseCard: ({ - label, - percentage, - tooltipText, - isLoading, - children, - }: { - label: React.ReactNode; - percentage?: number | null; - tooltipText?: React.ReactNode; - isLoading?: boolean; - children: React.ReactNode; - }) => ( -
    -
    {label}
    - {percentage !== null && percentage !== undefined && ( -
    {percentage}%
    - )} - {tooltipText &&
    {tooltipText}
    } -
    {isLoading ? "loading" : "not-loading"}
    -
    {children}
    -
    - ), - }) -); - -describe("StatCard", () => { - afterEach(() => { - cleanup(); - }); - - test("renders with basic props", () => { - render(); - - expect(screen.getByTestId("base-card")).toBeInTheDocument(); - expect(screen.getByTestId("base-card-label")).toHaveTextContent("Test Label"); - expect(screen.getByTestId("base-card-loading")).toHaveTextContent("not-loading"); - expect(screen.getByText("Test Value")).toBeInTheDocument(); - }); - - test("passes percentage prop to BaseCard", () => { - render(); - - expect(screen.getByTestId("base-card-percentage")).toHaveTextContent("75%"); - }); - - test("renders loading state with skeleton", () => { - render(); - - expect(screen.getByTestId("base-card-loading")).toHaveTextContent("loading"); - - const skeleton = screen.getByTestId("base-card-children").querySelector("div"); - expect(skeleton).toHaveClass("h-6", "w-12", "animate-pulse", "rounded-full", "bg-slate-200"); - - expect(screen.queryByText("Test Value")).not.toBeInTheDocument(); - }); - - test("renders value when not loading", () => { - render(); - - expect(screen.getByTestId("base-card-loading")).toHaveTextContent("not-loading"); - expect(screen.getByText("Test Value")).toBeInTheDocument(); - expect(screen.getByText("Test Value")).toHaveClass("text-2xl", "font-bold", "text-slate-800"); - }); - - test("renders dash value correctly", () => { - const dashValue = -; - render(); - - expect(screen.getByText("-")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate.test.tsx deleted file mode 100644 index a413bfda35..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate.test.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { cleanup } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TProject } from "@formbricks/types/project"; -import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { getPublicDomain } from "@/lib/getPublicUrl"; -import { getProjectByEnvironmentId } from "@/lib/project/service"; -import { getSurvey } from "@/lib/survey/service"; -import { getStyling } from "@/lib/utils/styling"; -import { getPreviewEmailTemplateHtml } from "@/modules/email/components/preview-email-template"; -import { getEmailTemplateHtml } from "./emailTemplate"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", -})); - -vi.mock("@/lib/env", () => ({ - env: { - PUBLIC_URL: "https://public-domain.com", - }, -})); - -vi.mock("@/lib/getPublicUrl", () => ({ - getPublicDomain: vi.fn().mockReturnValue("https://public-domain.com"), -})); - -vi.mock("@/lib/project/service"); -vi.mock("@/lib/survey/service"); -vi.mock("@/lib/utils/styling"); -vi.mock("@/modules/email/components/preview-email-template"); -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -const mockSurveyId = "survey123"; -const mockLocale = "en"; -const doctype = - ''; - -const mockSurvey = { - id: mockSurveyId, - name: "Test Survey", - environmentId: "env456", - type: "app", - status: "inProgress", - questions: [ - { - id: "q1", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Question?" }, - } as unknown as TSurveyQuestion, - ], - styling: null, - createdAt: new Date(), - updatedAt: new Date(), - languages: [], - triggers: [], - recontactDays: null, - displayOption: "displayOnce", - displayPercentage: null, - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - surveyClosedMessage: null, - singleUse: null, - variables: [], - segment: null, - autoClose: null, - delay: 0, - autoComplete: null, -} as unknown as TSurvey; - -const mockProject = { - id: "proj789", - name: "Test Project", - environments: [{ id: "env456", type: "production" } as unknown as TEnvironment], - styling: { - allowStyleOverwrite: true, - brandColor: { light: "#007BFF", dark: "#007BFF" }, - highlightBorderColor: null, - cardBackgroundColor: { light: "#FFFFFF", dark: "#000000" }, - cardBorderColor: { light: "#FFFFFF", dark: "#000000" }, - - questionColor: { light: "#FFFFFF", dark: "#000000" }, - inputColor: { light: "#FFFFFF", dark: "#000000" }, - inputBorderColor: { light: "#FFFFFF", dark: "#000000" }, - }, - createdAt: new Date(), - updatedAt: new Date(), - linkSurveyBranding: true, - placement: "bottomRight", - clickOutsideClose: true, - darkOverlay: false, - recontactDays: 30, - logo: null, -} as unknown as TProject; - -const mockComputedStyling = { - brandColor: "#007BFF", - questionColor: "#000000", - inputColor: "#000000", - inputBorderColor: "#000000", - cardBackgroundColor: "#FFFFFF", - cardBorderColor: "#EEEEEE", - - highlightBorderColor: null, - thankYouCardIconColor: "#007BFF", - thankYouCardIconBgColor: "#DDDDDD", -} as any; - -const mockPublicDomain = "https://app.formbricks.com"; -const mockRawHtml = `${doctype}Test Email Content for ${mockSurvey.name}`; -const mockCleanedHtml = `Test Email Content for ${mockSurvey.name}`; - -describe("getEmailTemplateHtml", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.resetAllMocks(); - - vi.mocked(getSurvey).mockResolvedValue(mockSurvey); - vi.mocked(getProjectByEnvironmentId).mockResolvedValue(mockProject); - vi.mocked(getStyling).mockReturnValue(mockComputedStyling); - vi.mocked(getPublicDomain).mockReturnValue(mockPublicDomain); - vi.mocked(getPreviewEmailTemplateHtml).mockResolvedValue(mockRawHtml); - }); - - test("should return cleaned HTML when all services provide data", async () => { - const html = await getEmailTemplateHtml(mockSurveyId, mockLocale); - - expect(html).toBe(mockCleanedHtml); - expect(getSurvey).toHaveBeenCalledWith(mockSurveyId); - expect(getProjectByEnvironmentId).toHaveBeenCalledWith(mockSurvey.environmentId); - expect(getStyling).toHaveBeenCalledWith(mockProject, mockSurvey); - expect(getPublicDomain).toHaveBeenCalledTimes(1); - const expectedSurveyUrl = `${mockPublicDomain}/s/${mockSurvey.id}`; - expect(getPreviewEmailTemplateHtml).toHaveBeenCalledWith( - mockSurvey, - expectedSurveyUrl, - mockComputedStyling, - mockLocale, - expect.any(Function) - ); - }); - - test("should throw error if survey is not found", async () => { - vi.mocked(getSurvey).mockResolvedValue(null); - await expect(getEmailTemplateHtml(mockSurveyId, mockLocale)).rejects.toThrow("Survey not found"); - }); - - test("should throw error if project is not found", async () => { - vi.mocked(getProjectByEnvironmentId).mockResolvedValue(null); - await expect(getEmailTemplateHtml(mockSurveyId, mockLocale)).rejects.toThrow("Project not found"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/loading.test.tsx deleted file mode 100644 index d657b0fb37..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/loading.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Loading from "./loading"; - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ pageTitle }) =>

    {pageTitle}

    , -})); - -vi.mock("@/modules/ui/components/skeleton-loader", () => ({ - SkeletonLoader: ({ type }) =>
    {`Skeleton type: ${type}`}
    , -})); - -describe("Loading Component", () => { - afterEach(() => { - cleanup(); - }); - - test("should render the loading state correctly", () => { - render(); - - expect(screen.getByText("common.summary")).toBeInTheDocument(); - expect(screen.getByTestId("skeleton-loader")).toHaveTextContent("Skeleton type: summary"); - - const pulseDivs = screen.getAllByRole("generic", { hidden: true }); // Using generic role as divs don't have implicit roles - // Filter divs that are part of the pulse animation - const animatedDivs = pulseDivs.filter( - (div) => - div.classList.contains("h-9") && - div.classList.contains("w-36") && - div.classList.contains("rounded-full") && - div.classList.contains("bg-slate-200") - ); - expect(animatedDivs.length).toBe(4); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.test.tsx deleted file mode 100644 index d42a4e0b6c..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.test.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { notFound } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TUser } from "@formbricks/types/user"; -import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; -import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation"; -import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage"; -import { getSurveySummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary"; -import SurveyPage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page"; -import { DEFAULT_LOCALE } from "@/lib/constants"; -import { getPublicDomain } from "@/lib/getPublicUrl"; -import { getResponseCountBySurveyId } from "@/lib/response/service"; -import { getSurvey } from "@/lib/survey/service"; -import { getUser } from "@/lib/user/service"; -import { getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; -import { getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization"; -import { getOrganizationBilling } from "@/modules/survey/lib/survey"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_STORAGE_CONFIGURED: true, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - RESPONSES_PER_PAGE: 10, - SESSION_MAX_AGE: 1000, -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation", - () => ({ - SurveyAnalysisNavigation: vi.fn(() =>
    ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage", - () => ({ - SummaryPage: vi.fn(() =>
    ), - }) -); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA", - () => ({ - SurveyAnalysisCTA: vi.fn(() =>
    ), - }) -); - -vi.mock("@/lib/getPublicUrl", () => ({ - getPublicDomain: vi.fn(), -})); - -vi.mock("@/lib/response/service", () => ({ - getResponseCountBySurveyId: vi.fn(), -})); - -vi.mock("@/lib/survey/service", () => ({ - getSurvey: vi.fn(), -})); - -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); - -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary", - () => ({ - getSurveySummary: vi.fn(), - }) -); - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: vi.fn(({ children }) =>
    {children}
    ), -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: vi.fn(({ children }) =>
    {children}
    ), -})); - -vi.mock("@/modules/ui/components/id-badge", () => ({ - IdBadge: vi.fn(() =>
    ), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -vi.mock("next/navigation", () => ({ - notFound: vi.fn(), - useParams: () => ({ - environmentId: "test-environment-id", - surveyId: "test-survey-id", - }), -})); - -vi.mock("@/modules/survey/lib/organization", () => ({ - getOrganizationIdFromEnvironmentId: vi.fn(), -})); - -vi.mock("@/modules/survey/lib/survey", () => ({ - getOrganizationBilling: vi.fn(), -})); - -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getIsQuotasEnabled: vi.fn(), - getIsContactsEnabled: vi.fn(), -})); - -const mockEnvironmentId = "test-environment-id"; -const mockSurveyId = "test-survey-id"; -const mockUserId = "test-user-id"; - -const mockEnvironment = { - id: mockEnvironmentId, - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - appSetupCompleted: false, -} as unknown as TEnvironment; - -const mockSurvey = { - id: mockSurveyId, - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Survey", - type: "app", - environmentId: mockEnvironmentId, - status: "draft", - questions: [], - displayOption: "displayOnce", - autoClose: null, - triggers: [], - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - autoComplete: null, - delay: 0, - displayPercentage: null, - languages: [], - singleUse: null, - surveyClosedMessage: null, - segment: null, - styling: null, - variables: [], - hiddenFields: { enabled: true, fieldIds: [] }, -} as unknown as TSurvey; - -const mockUser = { - id: mockUserId, - name: "Test User", - email: "test@example.com", - emailVerified: new Date(), - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - onboardingCompleted: true, - role: "project_manager", - locale: "en-US", - objective: "other", -} as unknown as TUser; - -const mockSession = { - user: { - id: mockUserId, - name: mockUser.name, - email: mockUser.email, - role: mockUser.role, - plan: "free", - status: "active", - objective: "other", - }, - expires: new Date(Date.now() + 3600 * 1000).toISOString(), // 1 hour from now -} as any; - -const mockSurveySummary = { - meta: { - completedPercentage: 75, - completedResponses: 15, - displayCount: 20, - dropOffPercentage: 25, - dropOffCount: 5, - startsPercentage: 80, - totalResponses: 20, - ttcAverage: 120, - quotasCompleted: 0, - quotasCompletedPercentage: 0, - }, - quotas: [], - dropOff: [], - summary: [], -}; - -describe("SurveyPage", () => { - beforeEach(() => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: mockSession, - environment: mockEnvironment, - isReadOnly: false, - } as unknown as TEnvironmentAuth); - vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValue("mock-organization-id"); - vi.mocked(getOrganizationBilling).mockResolvedValue({ plan: "scale" }); - vi.mocked(getIsQuotasEnabled).mockResolvedValue(false); - vi.mocked(getSurvey).mockResolvedValue(mockSurvey); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getResponseCountBySurveyId).mockResolvedValue(10); - vi.mocked(getPublicDomain).mockReturnValue("http://localhost:3000"); - vi.mocked(getSurveySummary).mockResolvedValue(mockSurveySummary); - vi.mocked(notFound).mockClear(); - }); - - afterEach(() => { - cleanup(); - vi.resetAllMocks(); - }); - - test("renders correctly with valid data", async () => { - const params = Promise.resolve({ environmentId: mockEnvironmentId, surveyId: mockSurveyId }); - const jsx = await SurveyPage({ params }); - render({jsx}); - - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("survey-analysis-navigation")).toBeInTheDocument(); - expect(screen.getByTestId("summary-page")).toBeInTheDocument(); - expect(screen.getByTestId("id-badge")).toBeInTheDocument(); - - expect(vi.mocked(getEnvironmentAuth)).toHaveBeenCalledWith(mockEnvironmentId); - expect(vi.mocked(getSurvey)).toHaveBeenCalledWith(mockSurveyId); - expect(vi.mocked(getUser)).toHaveBeenCalledWith(mockUserId); - expect(vi.mocked(getPublicDomain)).toHaveBeenCalled(); - - expect(vi.mocked(SurveyAnalysisNavigation).mock.calls[0][0]).toEqual( - expect.objectContaining({ - environmentId: mockEnvironmentId, - survey: mockSurvey, - activeId: "summary", - }) - ); - - expect(vi.mocked(SummaryPage).mock.calls[0][0]).toEqual( - expect.objectContaining({ - environment: mockEnvironment, - survey: mockSurvey, - surveyId: mockSurveyId, - locale: mockUser.locale ?? DEFAULT_LOCALE, - initialSurveySummary: mockSurveySummary, - }) - ); - }); - - test("calls notFound if surveyId is not present in params", async () => { - const params = Promise.resolve({ environmentId: mockEnvironmentId, surveyId: undefined }) as any; - const jsx = await SurveyPage({ params }); - render({jsx}); - expect(vi.mocked(notFound)).toHaveBeenCalled(); - }); - - test("throws error if survey is not found", async () => { - vi.mocked(getSurvey).mockResolvedValue(null); - const params = Promise.resolve({ environmentId: mockEnvironmentId, surveyId: mockSurveyId }); - try { - // We need to await the component itself because it's an async component - const SurveyPageComponent = await SurveyPage({ params }); - render({SurveyPageComponent}); - } catch (e: any) { - expect(e.message).toBe("common.survey_not_found"); - } - // Ensure notFound was not called for this specific error - expect(vi.mocked(notFound)).not.toHaveBeenCalled(); - }); - - test("throws error if user is not found", async () => { - vi.mocked(getUser).mockResolvedValue(null); - const params = Promise.resolve({ environmentId: mockEnvironmentId, surveyId: mockSurveyId }); - try { - const SurveyPageComponent = await SurveyPage({ params }); - render({SurveyPageComponent}); - } catch (e: any) { - expect(e.message).toBe("common.user_not_found"); - } - expect(vi.mocked(notFound)).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.test.tsx deleted file mode 100644 index 1a619bc4ad..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.test.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { format } from "date-fns"; -import { useParams } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; -import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions"; -import { getFormattedFilters, getTodayDate } from "@/app/lib/surveys/surveys"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { useClickOutside } from "@/lib/utils/hooks/useClickOutside"; -import { CustomFilter } from "./CustomFilter"; - -vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({ - useResponseFilter: vi.fn(), -})); - -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions", () => ({ - getResponsesDownloadUrlAction: vi.fn(), -})); - -vi.mock("@/app/lib/surveys/surveys", async (importOriginal) => { - const actual = (await importOriginal()) as any; - return { - ...actual, - getFormattedFilters: vi.fn(), - getTodayDate: vi.fn(), - }; -}); - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(), -})); - -vi.mock("@/lib/utils/hooks/useClickOutside", () => ({ - useClickOutside: vi.fn(), -})); - -vi.mock("@/modules/ui/components/calendar", () => ({ - Calendar: vi.fn( - ({ - onDayClick, - onDayMouseEnter, - onDayMouseLeave, - selected, - defaultMonth, - mode, - numberOfMonths, - classNames, - autoFocus, - }) => ( -
    - Calendar Mock - -
    onDayMouseEnter?.(new Date("2024-01-10"))}> - Hover Day -
    -
    onDayMouseLeave?.()}> - Leave Day -
    -
    - Selected: {selected?.from?.toISOString()} - {selected?.to?.toISOString()} -
    -
    Default Month: {defaultMonth?.toISOString()}
    -
    Mode: {mode}
    -
    Number of Months: {numberOfMonths}
    -
    ClassNames: {JSON.stringify(classNames)}
    -
    AutoFocus: {String(autoFocus)}
    -
    - ) - ), -})); - -vi.mock("next/navigation", () => ({ - useParams: vi.fn(), -})); - -vi.mock("./ResponseFilter", () => ({ - ResponseFilter: vi.fn(() =>
    ResponseFilter Mock
    ), -})); - -const mockSurvey = { - id: "survey-1", - name: "Test Survey", - questions: [], - createdAt: new Date(), - updatedAt: new Date(), - type: "app", - environmentId: "env-1", - status: "inProgress", - displayOption: "displayOnce", - recontactDays: null, - autoClose: null, - delay: 0, - autoComplete: null, - surveyClosedMessage: null, - singleUse: null, - displayPercentage: null, - languages: [], - triggers: [], - welcomeCard: { enabled: false } as TSurvey["welcomeCard"], -} as unknown as TSurvey; - -const mockDateToday = new Date("2023-11-20T00:00:00.000Z"); - -const initialMockUseResponseFilterState = () => ({ - selectedFilter: {}, - dateRange: { from: undefined, to: mockDateToday }, - setDateRange: vi.fn(), - resetState: vi.fn(), -}); - -let mockUseResponseFilterState = initialMockUseResponseFilterState(); - -describe("CustomFilter", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.clearAllMocks(); - mockUseResponseFilterState = initialMockUseResponseFilterState(); // Reset state for each test - - vi.mocked(useResponseFilter).mockImplementation(() => mockUseResponseFilterState as any); - vi.mocked(useParams).mockReturnValue({ environmentId: "test-env", surveyId: "test-survey" }); - vi.mocked(getFormattedFilters).mockReturnValue({}); - vi.mocked(getTodayDate).mockReturnValue(mockDateToday); - vi.mocked(getResponsesDownloadUrlAction).mockResolvedValue({ data: "mock-download-url" }); - vi.mocked(getFormattedErrorMessage).mockReturnValue("Mock error message"); - }); - - test("renders correctly with initial props", () => { - render(); - expect(screen.getByTestId("response-filter-mock")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.all_time")).toBeInTheDocument(); - expect(screen.getByText("common.download")).toBeInTheDocument(); - }); - - test("opens custom date picker when 'Custom range' is clicked", async () => { - const user = userEvent.setup(); - render(); - const dropdownTrigger = screen.getByText("environments.surveys.summary.all_time").closest("button")!; - // Similar to above, assuming direct clickability. - await user.click(dropdownTrigger); - const customRangeOption = screen.getByText("environments.surveys.summary.custom_range"); - await user.click(customRangeOption); - - expect(screen.getByTestId("calendar-mock")).toBeVisible(); - expect(screen.getByText(`Select first date - ${format(mockDateToday, "dd LLL")}`)).toBeInTheDocument(); - }); - - test("useEffect logic for resetState and firstMountRef (as per current component code)", () => { - // This test verifies the current behavior of the useEffects related to firstMountRef. - // Based on the component's code, resetState() is not expected to be called by these effects, - // and firstMountRef.current is not changed by the first useEffect. - const { rerender } = render(); - expect(mockUseResponseFilterState.resetState).not.toHaveBeenCalled(); - - const newSurvey = { ...mockSurvey, id: "survey-2" }; - rerender(); - expect(mockUseResponseFilterState.resetState).not.toHaveBeenCalled(); - }); - - test("closes date picker when clicking outside", async () => { - const user = userEvent.setup(); - let clickOutsideCallback: Function = () => {}; - vi.mocked(useClickOutside).mockImplementation((_, callback) => { - clickOutsideCallback = callback; - }); - - render(); - const dropdownTrigger = screen.getByText("environments.surveys.summary.all_time").closest("button")!; // Ensure targeting button - await user.click(dropdownTrigger); - const customRangeOption = screen.getByText("environments.surveys.summary.custom_range"); - await user.click(customRangeOption); - expect(screen.getByTestId("calendar-mock")).toBeVisible(); - - clickOutsideCallback(); // Simulate click outside - - await waitFor(() => { - expect(screen.queryByTestId("calendar-mock")).not.toBeInTheDocument(); - }); - }); - - test("downloading all and filtered responses in csv and xlsx formats", async () => { - const user = userEvent.setup(); - - render(); - - // Mock the action to return undefined data to avoid DOM manipulation - vi.mocked(getResponsesDownloadUrlAction).mockResolvedValue({ - data: undefined, - }); - - // Test CSV download - const downloadButton = screen.getByTestId("fb__custom-filter-download-responses-button"); - await user.click(downloadButton); - const downloadAllCsv = screen.getByTestId("fb__custom-filter-download-all-csv"); - await user.click(downloadAllCsv); - - await waitFor(() => { - expect(getResponsesDownloadUrlAction).toHaveBeenCalledWith({ - surveyId: "survey-1", - format: "csv", - filterCriteria: {}, - }); - }); - - // Test XLSX download - await user.click(downloadButton); - const downloadAllXlsx = screen.getByTestId("fb__custom-filter-download-all-xlsx"); - await user.click(downloadAllXlsx); - - await waitFor(() => { - expect(getResponsesDownloadUrlAction).toHaveBeenCalledWith({ - surveyId: "survey-1", - format: "xlsx", - filterCriteria: {}, - }); - }); - - // Test filtered CSV download - await user.click(downloadButton); - const downloadFilteredCsv = screen.getByTestId("fb__custom-filter-download-filtered-csv"); - await user.click(downloadFilteredCsv); - - await waitFor(() => { - expect(getResponsesDownloadUrlAction).toHaveBeenCalledWith({ - surveyId: "survey-1", - format: "csv", - filterCriteria: {}, - }); - }); - - // Test filtered XLSX download - await user.click(downloadButton); - const downloadFilteredXlsx = screen.getByTestId("fb__custom-filter-download-filtered-xlsx"); - await user.click(downloadFilteredXlsx); - - await waitFor(() => { - expect(getResponsesDownloadUrlAction).toHaveBeenCalledWith({ - surveyId: "survey-1", - format: "xlsx", - filterCriteria: {}, - }); - }); - - vi.restoreAllMocks(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.test.tsx deleted file mode 100644 index b781563af1..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.test.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { QuestionFilterComboBox } from "./QuestionFilterComboBox"; - -describe("QuestionFilterComboBox", () => { - afterEach(() => { - cleanup(); - }); - - const defaultProps = { - filterOptions: ["A", "B"], - filterComboBoxOptions: ["X", "Y"], - filterValue: undefined, - filterComboBoxValue: undefined, - onChangeFilterValue: vi.fn(), - onChangeFilterComboBoxValue: vi.fn(), - handleRemoveMultiSelect: vi.fn(), - disabled: false, - }; - - test("renders select placeholders", () => { - render(); - expect(screen.getAllByText("common.select...").length).toBe(2); - }); - - test("calls onChangeFilterValue when selecting filter", async () => { - render(); - await userEvent.click(screen.getAllByRole("button")[0]); - await userEvent.click(screen.getByText("A")); - expect(defaultProps.onChangeFilterValue).toHaveBeenCalledWith("A"); - }); - - test("calls onChangeFilterComboBoxValue when selecting combo box option", async () => { - render(); - await userEvent.click(screen.getAllByRole("button")[1]); - await userEvent.click(screen.getByText("X")); - expect(defaultProps.onChangeFilterComboBoxValue).toHaveBeenCalledWith("X"); - }); - - test("multi-select removal works", async () => { - const props = { - ...defaultProps, - type: "multipleChoiceMulti", - filterValue: "A", - filterComboBoxValue: ["X", "Y"], - }; - render(); - const removeButtons = screen.getAllByRole("button", { name: /X/i }); - await userEvent.click(removeButtons[0]); - expect(props.handleRemoveMultiSelect).toHaveBeenCalledWith(["Y"]); - }); - - test("disabled state prevents opening", async () => { - render(); - await userEvent.click(screen.getAllByRole("button")[0]); - expect(screen.queryByText("A")).toBeNull(); - }); - - test("handles object options correctly", async () => { - const obj = { default: "Obj1", en: "ObjEN" }; - const props = { - ...defaultProps, - type: "multipleChoiceMulti", - filterValue: "A", - filterComboBoxOptions: [obj], - filterComboBoxValue: [], - } as any; - render(); - await userEvent.click(screen.getAllByRole("button")[1]); - await userEvent.click(screen.getByText("Obj1")); - expect(props.onChangeFilterComboBoxValue).toHaveBeenCalledWith(["Obj1"]); - }); - - test("combobox is disabled when filterValue is 'Submitted' for NPS questions", async () => { - const props = { ...defaultProps, type: "nps", filterValue: "Submitted" } as any; - render(); - const comboBoxOpenerButton = screen.getAllByRole("button")[1]; - expect(comboBoxOpenerButton).toBeDisabled(); - await userEvent.click(comboBoxOpenerButton); - expect(screen.queryByText("X")).not.toBeInTheDocument(); - }); - - test("combobox is disabled when filterValue is 'Skipped' for rating questions", async () => { - const props = { ...defaultProps, type: "rating", filterValue: "Skipped" } as any; - render(); - const comboBoxOpenerButton = screen.getAllByRole("button")[1]; - expect(comboBoxOpenerButton).toBeDisabled(); - await userEvent.click(comboBoxOpenerButton); - expect(screen.queryByText("X")).not.toBeInTheDocument(); - }); - - test("shows text input for URL meta field", () => { - const props = { - ...defaultProps, - type: "Meta", - fieldId: "url", - filterValue: "Contains", - filterComboBoxValue: "example.com", - } as any; - render(); - const textInput = screen.getByDisplayValue("example.com"); - expect(textInput).toBeInTheDocument(); - expect(textInput).toHaveAttribute("type", "text"); - }); - - test("text input is disabled when no filter value is selected for URL field", () => { - const props = { - ...defaultProps, - type: "Meta", - fieldId: "url", - filterValue: undefined, - } as any; - render(); - const textInput = screen.getByRole("textbox"); - expect(textInput).toBeDisabled(); - }); - - test("text input calls onChangeFilterComboBoxValue when typing for URL field", async () => { - const props = { - ...defaultProps, - type: "Meta", - fieldId: "url", - filterValue: "Contains", - filterComboBoxValue: "", - } as any; - render(); - const textInput = screen.getByRole("textbox"); - await userEvent.type(textInput, "t"); - expect(props.onChangeFilterComboBoxValue).toHaveBeenCalledWith("t"); - }); - - test("shows regular combobox for non-URL meta fields", () => { - const props = { - ...defaultProps, - type: "Meta", - fieldId: "source", - filterValue: "Equals", - } as any; - render(); - expect(screen.queryByRole("textbox")).not.toBeInTheDocument(); - expect(screen.getAllByRole("button").length).toBeGreaterThanOrEqual(2); - }); - - test("shows regular combobox for URL field with non-text operations", () => { - const props = { - ...defaultProps, - type: "Other", - fieldId: "url", - filterValue: "Equals", - } as any; - render(); - expect(screen.queryByRole("textbox")).not.toBeInTheDocument(); - expect(screen.getAllByRole("button").length).toBeGreaterThanOrEqual(2); - }); - - test("text input handles string filter combo box values correctly", () => { - const props = { - ...defaultProps, - type: "Meta", - fieldId: "url", - filterValue: "Contains", - filterComboBoxValue: "test-url", - } as any; - render(); - const textInput = screen.getByDisplayValue("test-url"); - expect(textInput).toBeInTheDocument(); - }); - - test("text input handles non-string filter combo box values gracefully", () => { - const props = { - ...defaultProps, - type: "Meta", - fieldId: "url", - filterValue: "Contains", - filterComboBoxValue: ["array-value"], - } as any; - render(); - const textInput = screen.getByRole("textbox"); - expect(textInput).toHaveValue(""); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.test.tsx deleted file mode 100644 index 4fd1ce3c4c..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.test.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { - OptionsType, - QuestionOption, - QuestionOptions, - QuestionsComboBox, - SelectedCommandItem, -} from "./QuestionsComboBox"; - -describe("QuestionsComboBox", () => { - afterEach(() => { - cleanup(); - }); - - const mockOptions: QuestionOptions[] = [ - { - header: OptionsType.QUESTIONS, - option: [{ label: "Q1", type: OptionsType.QUESTIONS, questionType: undefined, id: "1" }], - }, - { - header: OptionsType.TAGS, - option: [{ label: "Tag1", type: OptionsType.TAGS, id: "t1" }], - }, - ]; - - test("renders selected label when closed", () => { - const selected: Partial = { label: "Q1", type: OptionsType.QUESTIONS, id: "1" }; - render( {}} />); - expect(screen.getByText("Q1")).toBeInTheDocument(); - }); - - test("opens dropdown, selects an option, and closes", async () => { - let currentSelected: Partial = {}; - const onChange = vi.fn((option) => { - currentSelected = option; - }); - - const { rerender } = render( - - ); - - // Open the dropdown - await userEvent.click(screen.getByRole("button")); - expect(screen.getByPlaceholderText("common.search...")).toBeInTheDocument(); - - // Select an option - await userEvent.click(screen.getByText("Q1")); - - // Check if onChange was called - expect(onChange).toHaveBeenCalledWith(mockOptions[0].option[0]); - - // Rerender with the new selected value - rerender(); - - // Check if the input is gone and the selected item is displayed - expect(screen.queryByPlaceholderText("common.search...")).toBeNull(); - 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( - - ); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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( - - ); - expect(container.textContent).toContain("NonString"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.test.tsx deleted file mode 100644 index 51dedba29a..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.test.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { useParams } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; -import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions"; -import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys"; -import { ResponseFilter } from "./ResponseFilter"; - -// Mock dependencies -vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({ - useResponseFilter: vi.fn(), -})); - -vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions", () => ({ - getSurveyFilterDataAction: vi.fn(), -})); - -vi.mock("@/app/lib/surveys/surveys", () => ({ - generateQuestionAndFilterOptions: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - useParams: vi.fn(), -})); - -vi.mock("@formkit/auto-animate/react", () => ({ - useAutoAnimate: () => [[vi.fn()]], -})); - -// Mock the Select components -const mockOnValueChange = vi.fn(); -vi.mock("@/modules/ui/components/select", () => ({ - Select: ({ children, onValueChange, defaultValue }) => { - // Store the onValueChange callback for testing - mockOnValueChange.mockImplementation(onValueChange); - return ( -
    - {children} -
    - ); - }, - SelectTrigger: ({ children, className }) => ( - - ), - SelectValue: () => environments.surveys.filter.complete_and_partial_responses, - SelectContent: ({ children }) =>
    {children}
    , - SelectItem: ({ value, children, ...props }) => ( -
    mockOnValueChange(value)} - onKeyDown={(e) => e.key === "Enter" && mockOnValueChange(value)} - role="option" - tabIndex={0} - {...props}> - {children} -
    - ), -})); - -vi.mock("./QuestionsComboBox", () => ({ - QuestionsComboBox: ({ onChangeValue }) => ( -
    - -
    - ), - OptionsType: { - QUESTIONS: "Questions", - ATTRIBUTES: "Attributes", - TAGS: "Tags", - LANGUAGES: "Languages", - }, -})); - -// Update the mock for QuestionFilterComboBox to always render -vi.mock( - "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox", - () => ({ - QuestionFilterComboBox: () => ( -
    - - -
    - ), - }) -); - -describe("ResponseFilter", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockSelectedFilter = { - filter: [], - responseStatus: "all", - }; - - const mockSelectedOptions = { - questionFilterOptions: [ - { - type: TSurveyQuestionTypeEnum.OpenText, - filterOptions: ["equals", "does not equal"], - filterComboBoxOptions: [], - id: "q1", - }, - ], - questionOptions: [ - { - label: "Questions", - type: "Questions", - option: [ - { id: "q1", label: "Question 1", type: "OpenText", questionType: TSurveyQuestionTypeEnum.OpenText }, - ], - }, - ], - } as any; - - const mockSetSelectedFilter = vi.fn(); - const mockSetSelectedOptions = vi.fn(); - - const mockSurvey = { - id: "survey1", - environmentId: "env1", - name: "Test Survey", - createdAt: new Date(), - updatedAt: new Date(), - status: "draft", - createdBy: "user1", - questions: [], - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - triggers: [], - displayOption: "displayOnce", - } as unknown as TSurvey; - - beforeEach(() => { - vi.mocked(useResponseFilter).mockReturnValue({ - selectedFilter: mockSelectedFilter, - setSelectedFilter: mockSetSelectedFilter, - selectedOptions: mockSelectedOptions, - setSelectedOptions: mockSetSelectedOptions, - } as any); - - vi.mocked(useParams).mockReturnValue({ environmentId: "env1", surveyId: "survey1" }); - - vi.mocked(getSurveyFilterDataAction).mockResolvedValue({ - data: { - attributes: [], - meta: {}, - environmentTags: [], - hiddenFields: [], - } as any, - }); - - vi.mocked(generateQuestionAndFilterOptions).mockReturnValue({ - questionFilterOptions: mockSelectedOptions.questionFilterOptions, - questionOptions: mockSelectedOptions.questionOptions, - }); - }); - - test("renders with default state", () => { - render(); - expect(screen.getByText("Filter")).toBeInTheDocument(); - }); - - test("opens the filter popover when clicked", async () => { - render(); - - await userEvent.click(screen.getByText("Filter")); - - expect( - screen.getByText("environments.surveys.summary.show_all_responses_that_match") - ).toBeInTheDocument(); - expect(screen.getByTestId("select-trigger")).toBeInTheDocument(); - }); - - test("fetches filter data when opened", async () => { - render(); - - await userEvent.click(screen.getByText("Filter")); - - expect(getSurveyFilterDataAction).toHaveBeenCalledWith({ surveyId: "survey1" }); - expect(mockSetSelectedOptions).toHaveBeenCalled(); - }); - - test("handles adding new filter", async () => { - // Start with an empty filter - vi.mocked(useResponseFilter).mockReturnValue({ - selectedFilter: { filter: [], responseStatus: "all" }, - setSelectedFilter: mockSetSelectedFilter, - selectedOptions: mockSelectedOptions, - setSelectedOptions: mockSetSelectedOptions, - } as any); - - render(); - - await userEvent.click(screen.getByText("Filter")); - // Verify there's no filter yet - expect(screen.queryByTestId("questions-combo-box")).not.toBeInTheDocument(); - - // Add a new filter and check that the questions combo box appears - await userEvent.click(screen.getByText("common.add_filter")); - - expect(screen.getByTestId("questions-combo-box")).toBeInTheDocument(); - }); - - test("handles response status filter change to complete", async () => { - render(); - - await userEvent.click(screen.getByText("Filter")); - - // Simulate selecting "complete" by calling the mock function - mockOnValueChange("complete"); - - await userEvent.click(screen.getByText("common.apply_filters")); - - expect(mockSetSelectedFilter).toHaveBeenCalledWith( - expect.objectContaining({ - responseStatus: "complete", - }) - ); - }); - - test("handles response status filter change to partial", async () => { - render(); - - await userEvent.click(screen.getByText("Filter")); - - // Simulate selecting "partial" by calling the mock function - mockOnValueChange("partial"); - - await userEvent.click(screen.getByText("common.apply_filters")); - - expect(mockSetSelectedFilter).toHaveBeenCalledWith( - expect.objectContaining({ - responseStatus: "partial", - }) - ); - }); - - test("handles selecting question and filter options", async () => { - // Setup with a pre-populated filter to ensure the filter components are rendered - const setSelectedFilterMock = vi.fn(); - vi.mocked(useResponseFilter).mockReturnValue({ - selectedFilter: { - filter: [ - { - questionType: { id: "q1", label: "Question 1", type: "OpenText" }, - filterType: { filterComboBoxValue: undefined, filterValue: undefined }, - }, - ], - responseStatus: "all", - }, - setSelectedFilter: setSelectedFilterMock, - selectedOptions: mockSelectedOptions, - setSelectedOptions: mockSetSelectedOptions, - } as any); - - render(); - - await userEvent.click(screen.getByText("Filter")); - - // Verify both combo boxes are rendered - expect(screen.getByTestId("questions-combo-box")).toBeInTheDocument(); - expect(screen.getByTestId("filter-combo-box")).toBeInTheDocument(); - - // Use data-testid to find our buttons instead of text - await userEvent.click(screen.getByText("Select Question")); - await userEvent.click(screen.getByTestId("select-filter-btn")); - await userEvent.click(screen.getByText("common.apply_filters")); - - expect(setSelectedFilterMock).toHaveBeenCalled(); - }); - - test("handles clear all filters", async () => { - render(); - - await userEvent.click(screen.getByText("Filter")); - await userEvent.click(screen.getByText("common.clear_all")); - - expect(mockSetSelectedFilter).toHaveBeenCalledWith({ filter: [], responseStatus: "all" }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.test.tsx deleted file mode 100644 index 39170edda0..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.test.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { cleanup, render, screen, within } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { SurveyStatusDropdown } from "./SurveyStatusDropdown"; - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn((error) => error?.message || "An error occurred"), -})); - -vi.mock("@/modules/ui/components/select", () => ({ - Select: vi.fn(({ value, onValueChange, disabled, children }) => ( -
    -
    {value}
    - {children} - -
    - )), - SelectContent: vi.fn(({ children }) =>
    {children}
    ), - SelectItem: vi.fn(({ value, children }) =>
    {children}
    ), - SelectTrigger: vi.fn(({ children }) =>
    {children}
    ), - SelectValue: vi.fn(({ children }) =>
    {children}
    ), -})); - -vi.mock("@/modules/ui/components/survey-status-indicator", () => ({ - SurveyStatusIndicator: vi.fn(({ status }) => ( -
    {`Status: ${status}`}
    - )), -})); - -vi.mock("@/modules/ui/components/tooltip", () => ({ - Tooltip: vi.fn(({ children }) =>
    {children}
    ), - TooltipContent: vi.fn(({ children }) =>
    {children}
    ), - TooltipProvider: vi.fn(({ children }) =>
    {children}
    ), - TooltipTrigger: vi.fn(({ children }) =>
    {children}
    ), -})); - -vi.mock("../actions", () => ({ - updateSurveyAction: vi.fn(), -})); - -const mockEnvironment: TEnvironment = { - id: "env_1", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "proj_1", - type: "production", - appSetupCompleted: true, - productOverwrites: null, - brandLinks: null, - recontactDays: 30, - displayBranding: true, - highlightBorderColor: null, - placement: "bottomRight", - clickOutsideClose: true, - darkOverlay: false, -}; - -const baseSurvey: TSurvey = { - id: "survey_1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Survey", - type: "app", - environmentId: "env_1", - status: "draft", - questions: [], - hiddenFields: { enabled: true, fieldIds: [] }, - displayOption: "displayOnce", - recontactDays: null, - autoClose: null, - delay: 0, - displayPercentage: null, - redirectUrl: null, - welcomeCard: { enabled: true } as TSurvey["welcomeCard"], - languages: [], - styling: null, - variables: [], - triggers: [], - numDisplays: 0, - responseRate: 0, - responses: [], - summary: { completedResponses: 0, displays: 0, totalResponses: 0, startsPercentage: 0 }, - isResponseEncryptionEnabled: false, - isSingleUse: false, - segment: null, - surveyClosedMessage: null, - singleUse: null, - pin: null, - productOverwrites: null, - analytics: { - numCTA: 0, - numDisplays: 0, - numResponses: 0, - numStarts: 0, - responseRate: 0, - startRate: 0, - totalCompletedResponses: 0, - totalDisplays: 0, - totalResponses: 0, - }, - createdBy: null, - autoComplete: null, - endings: [], -}; - -describe("SurveyStatusDropdown", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders draft status correctly", () => { - render( - - ); - expect(screen.getByText("common.draft")).toBeInTheDocument(); - expect(screen.queryByTestId("select-container")).toBeNull(); - }); - - test("renders SurveyStatusIndicator for link survey", () => { - render( - - ); - const actualSelectTrigger = screen.getByTestId("actual-select-trigger"); - expect(within(actualSelectTrigger).getByTestId("survey-status-indicator")).toBeInTheDocument(); - }); - - test("renders SurveyStatusIndicator when appSetupCompleted is true", () => { - render( - - ); - const actualSelectTrigger = screen.getByTestId("actual-select-trigger"); - expect(within(actualSelectTrigger).getByTestId("survey-status-indicator")).toBeInTheDocument(); - }); - - test("does not render SurveyStatusIndicator when appSetupCompleted is false for non-link survey", () => { - render( - - ); - const actualSelectTrigger = screen.getByTestId("actual-select-trigger"); - expect(within(actualSelectTrigger).queryByTestId("survey-status-indicator")).toBeNull(); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context.test.tsx deleted file mode 100644 index 5ac458aeca..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context.test.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { SurveyContextWrapper, useSurvey } from "./survey-context"; - -// Mock survey data -const mockSurvey: TSurvey = { - id: "test-survey-id", - createdAt: new Date("2023-01-01T00:00:00.000Z"), - updatedAt: new Date("2023-01-01T00:00:00.000Z"), - name: "Test Survey", - type: "link", - environmentId: "test-env-id", - createdBy: "test-user-id", - status: "draft", - displayOption: "displayOnce", - autoClose: null, - triggers: [], - recontactDays: null, - displayLimit: null, - welcomeCard: { - enabled: false, - headline: { default: "Welcome" }, - subheader: { default: "" }, - timeToFinish: false, - showResponseCount: false, - buttonLabel: { default: "Start" }, - fileUrl: undefined, - videoUrl: undefined, - }, - questions: [ - { - id: "question-1", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "What's your name?" }, - required: true, - inputType: "text", - logic: [], - buttonLabel: { default: "Next" }, - backButtonLabel: { default: "Back" }, - placeholder: { default: "Enter your name" }, - longAnswer: false, - subheader: { default: "" }, - charLimit: { enabled: false, min: 0, max: 255 }, - }, - ], - endings: [ - { - id: "ending-1", - type: "endScreen", - headline: { default: "Thank you!" }, - subheader: { default: "We appreciate your feedback." }, - buttonLabel: { default: "Done" }, - buttonLink: undefined, - imageUrl: undefined, - videoUrl: undefined, - }, - ], - hiddenFields: { - enabled: false, - fieldIds: [], - }, - variables: [], - followUps: [], - delay: 0, - autoComplete: null, - projectOverwrites: null, - styling: null, - showLanguageSwitch: null, - surveyClosedMessage: null, - segment: null, - singleUse: null, - isVerifyEmailEnabled: false, - recaptcha: null, - isSingleResponsePerEmailEnabled: false, - isBackButtonHidden: false, - pin: null, - displayPercentage: null, - languages: [ - { - language: { - id: "en", - code: "en", - alias: "English", - projectId: "test-project-id", - createdAt: new Date("2023-01-01T00:00:00.000Z"), - updatedAt: new Date("2023-01-01T00:00:00.000Z"), - }, - default: true, - enabled: true, - }, - ], -}; - -// Test component that uses the hook -const TestComponent = () => { - const { survey } = useSurvey(); - return ( -
    -
    {survey.id}
    -
    {survey.name}
    -
    {survey.type}
    -
    {survey.status}
    -
    {survey.environmentId}
    -
    - ); -}; - -describe("SurveyContext", () => { - afterEach(() => { - cleanup(); - }); - - test("provides survey data to child components", () => { - render( - - - - ); - - expect(screen.getByTestId("survey-id")).toHaveTextContent("test-survey-id"); - expect(screen.getByTestId("survey-name")).toHaveTextContent("Test Survey"); - expect(screen.getByTestId("survey-type")).toHaveTextContent("link"); - expect(screen.getByTestId("survey-status")).toHaveTextContent("draft"); - expect(screen.getByTestId("survey-environment-id")).toHaveTextContent("test-env-id"); - }); - - test("throws error when useSurvey is used outside of provider", () => { - const TestComponentWithoutProvider = () => { - useSurvey(); - return
    Should not render
    ; - }; - - expect(() => { - render(); - }).toThrow("useSurvey must be used within a SurveyContextWrapper"); - }); - - test("updates context value when survey changes", () => { - const updatedSurvey = { - ...mockSurvey, - name: "Updated Survey", - status: "inProgress" as const, - }; - - const { rerender } = render( - - - - ); - - expect(screen.getByTestId("survey-name")).toHaveTextContent("Test Survey"); - expect(screen.getByTestId("survey-status")).toHaveTextContent("draft"); - - rerender( - - - - ); - - expect(screen.getByTestId("survey-name")).toHaveTextContent("Updated Survey"); - expect(screen.getByTestId("survey-status")).toHaveTextContent("inProgress"); - }); - - test("verifies memoization by tracking render counts", () => { - let renderCount = 0; - const renderSpy = vi.fn(() => { - renderCount++; - }); - - const TestComponentWithRenderTracking = () => { - renderSpy(); - const { survey } = useSurvey(); - return ( -
    -
    {survey.id}
    -
    {renderCount}
    -
    - ); - }; - - const { rerender } = render( - - - - ); - - expect(screen.getByTestId("survey-id")).toHaveTextContent("test-survey-id"); - expect(renderSpy).toHaveBeenCalledTimes(1); - - // Rerender with the same survey object - should not trigger additional renders - // if memoization is working correctly - rerender( - - - - ); - - expect(screen.getByTestId("survey-id")).toHaveTextContent("test-survey-id"); - expect(renderSpy).toHaveBeenCalledTimes(2); // Should only be called once more for the rerender - }); - - test("prevents unnecessary re-renders when survey object is unchanged", () => { - const childRenderSpy = vi.fn(); - - const ChildComponent = () => { - childRenderSpy(); - const { survey } = useSurvey(); - return
    {survey.name}
    ; - }; - - const ParentComponent = ({ survey }: { survey: TSurvey }) => { - return ( - - - - ); - }; - - const { rerender } = render(); - - expect(screen.getByTestId("child-survey-name")).toHaveTextContent("Test Survey"); - expect(childRenderSpy).toHaveBeenCalledTimes(1); - - // Rerender with the same survey object reference - rerender(); - - expect(screen.getByTestId("child-survey-name")).toHaveTextContent("Test Survey"); - expect(childRenderSpy).toHaveBeenCalledTimes(2); // Should only be called once more - - // Rerender with a different survey object should trigger re-render - const updatedSurvey = { ...mockSurvey, name: "Updated Survey" }; - rerender(); - - expect(screen.getByTestId("child-survey-name")).toHaveTextContent("Updated Survey"); - expect(childRenderSpy).toHaveBeenCalledTimes(3); // Should be called again due to prop change - }); - - test("renders children correctly", () => { - const TestChild = () =>
    Child Component
    ; - - render( - - - - ); - - expect(screen.getByTestId("child")).toHaveTextContent("Child Component"); - }); - - test("handles multiple child components", () => { - const TestChild1 = () => { - const { survey } = useSurvey(); - return
    {survey.name}
    ; - }; - - const TestChild2 = () => { - const { survey } = useSurvey(); - return
    {survey.type}
    ; - }; - - render( - - - - - ); - - expect(screen.getByTestId("child-1")).toHaveTextContent("Test Survey"); - expect(screen.getByTestId("child-2")).toHaveTextContent("link"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/layout.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/layout.test.tsx deleted file mode 100644 index 671567d473..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/layout.test.tsx +++ /dev/null @@ -1,192 +0,0 @@ -// Import the mocked function to access it in tests -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { getSurvey } from "@/lib/survey/service"; -import SurveyLayout from "./layout"; - -// Mock the getSurvey function -vi.mock("@/lib/survey/service", () => ({ - getSurvey: vi.fn(), -})); - -// Mock the SurveyContextWrapper component -vi.mock("./context/survey-context", () => ({ - SurveyContextWrapper: ({ survey, children }: { survey: TSurvey; children: React.ReactNode }) => ( -
    - {children} -
    - ), -})); - -describe("SurveyLayout", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockSurvey: TSurvey = { - id: "survey-123", - name: "Test Survey", - environmentId: "env-123", - status: "inProgress", - type: "link", - createdAt: new Date(), - updatedAt: new Date(), - questions: [], - endings: [], - hiddenFields: { - enabled: false, - }, - variables: [], - welcomeCard: { - enabled: false, - timeToFinish: true, - showResponseCount: false, - }, - displayOption: "displayOnce", - recontactDays: null, - displayLimit: null, - autoClose: null, - delay: 0, - displayPercentage: null, - autoComplete: null, - isVerifyEmailEnabled: false, - isSingleResponsePerEmailEnabled: false, - isBackButtonHidden: false, - projectOverwrites: { - brandColor: null, - highlightBorderColor: null, - placement: null, - clickOutsideClose: null, - darkOverlay: null, - }, - styling: null, - surveyClosedMessage: null, - singleUse: null, - pin: null, - showLanguageSwitch: false, - recaptcha: null, - languages: [], - triggers: [], - segment: null, - followUps: [], - createdBy: null, - }; - - const mockParams = Promise.resolve({ - surveyId: "survey-123", - environmentId: "env-123", - }); - - test("renders SurveyContextWrapper with survey and children when survey is found", async () => { - vi.mocked(getSurvey).mockResolvedValue(mockSurvey); - - render( - await SurveyLayout({ - params: mockParams, - children:
    Test Content
    , - }) - ); - - expect(getSurvey).toHaveBeenCalledWith("survey-123"); - expect(screen.getByTestId("survey-context-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("survey-context-wrapper")).toHaveAttribute("data-survey-id", "survey-123"); - expect(screen.getByTestId("test-children")).toBeInTheDocument(); - expect(screen.getByText("Test Content")).toBeInTheDocument(); - }); - - test("throws error when survey is not found", async () => { - vi.mocked(getSurvey).mockResolvedValue(null); - - await expect( - SurveyLayout({ - params: mockParams, - children:
    Test Content
    , - }) - ).rejects.toThrow("Survey not found"); - - expect(getSurvey).toHaveBeenCalledWith("survey-123"); - }); - - test("awaits params before calling getSurvey", async () => { - vi.mocked(getSurvey).mockResolvedValue(mockSurvey); - - const delayedParams = new Promise<{ surveyId: string; environmentId: string }>((resolve) => { - setTimeout(() => { - resolve({ - surveyId: "survey-456", - environmentId: "env-456", - }); - }, 10); - }); - - render( - await SurveyLayout({ - params: delayedParams, - children:
    Test Content
    , - }) - ); - - expect(getSurvey).toHaveBeenCalledWith("survey-456"); - expect(screen.getByTestId("survey-context-wrapper")).toHaveAttribute("data-survey-id", "survey-123"); - }); - - test("calls getSurvey with correct surveyId from params", async () => { - vi.mocked(getSurvey).mockResolvedValue(mockSurvey); - - const customParams = Promise.resolve({ - surveyId: "custom-survey-id", - environmentId: "custom-env-id", - }); - - render( - await SurveyLayout({ - params: customParams, - children:
    Test Content
    , - }) - ); - - expect(getSurvey).toHaveBeenCalledWith("custom-survey-id"); - expect(getSurvey).toHaveBeenCalledTimes(1); - }); - - test("passes children to SurveyContextWrapper", async () => { - vi.mocked(getSurvey).mockResolvedValue(mockSurvey); - - const complexChildren = ( -
    -

    Survey Title

    -

    Survey description

    - -
    - ); - - render( - await SurveyLayout({ - params: mockParams, - children: complexChildren, - }) - ); - - expect(screen.getByTestId("complex-children")).toBeInTheDocument(); - expect(screen.getByText("Survey Title")).toBeInTheDocument(); - expect(screen.getByText("Survey description")).toBeInTheDocument(); - expect(screen.getByText("Submit")).toBeInTheDocument(); - }); - - test("handles getSurvey rejection correctly", async () => { - const mockError = new Error("Database connection failed"); - vi.mocked(getSurvey).mockRejectedValue(mockError); - - await expect( - SurveyLayout({ - params: mockParams, - children:
    Test Content
    , - }) - ).rejects.toThrow("Database connection failed"); - - expect(getSurvey).toHaveBeenCalledWith("survey-123"); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/page.test.tsx deleted file mode 100644 index 26ff9515ee..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/page.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { redirect } from "next/navigation"; -import { describe, expect, test, vi } from "vitest"; -import Page from "./page"; - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -describe("SurveyPage", () => { - test("should redirect to the survey summary page", async () => { - const params = { - environmentId: "testEnvId", - surveyId: "testSurveyId", - }; - const props = { params }; - - await Page(props); - - expect(vi.mocked(redirect)).toHaveBeenCalledWith( - `/environments/${params.environmentId}/surveys/${params.surveyId}/summary` - ); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/loading.test.tsx deleted file mode 100644 index 361c21670f..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/loading.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { SurveyListLoading as OriginalSurveyListLoading } from "@/modules/survey/list/loading"; -import SurveyListLoading from "./loading"; - -// Mock the original component to ensure we are testing the re-export -vi.mock("@/modules/survey/list/loading", () => ({ - SurveyListLoading: () =>
    Mock SurveyListLoading
    , -})); - -describe("SurveyListLoadingPage Re-export", () => { - test("should re-export SurveyListLoading from the correct module", () => { - // Check if the re-exported component is the same as the original (mocked) component - expect(SurveyListLoading).toBe(OriginalSurveyListLoading); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/page.test.tsx deleted file mode 100644 index 05b744bf08..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/page.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { cleanup, render } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import SurveysPage, { metadata as layoutMetadata } from "./page"; - -vi.mock("@/modules/survey/list/page", () => ({ - SurveysPage: ({ children }) =>
    {children}
    , - metadata: { title: "Mocked Surveys Page" }, -})); - -describe("SurveysPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders SurveysPage", () => { - const { getByTestId } = render(); - expect(getByTestId("surveys-page")).toBeInTheDocument(); - expect(getByTestId("surveys-page")).toHaveTextContent(""); - }); - - test("exports metadata from @/modules/survey/list/page", () => { - expect(layoutMetadata).toEqual({ title: "Mocked Surveys Page" }); - }); -}); diff --git a/apps/web/app/(app)/environments/page.test.tsx b/apps/web/app/(app)/environments/page.test.tsx deleted file mode 100644 index a4021f7000..0000000000 --- a/apps/web/app/(app)/environments/page.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { cleanup, render } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Page from "./page"; - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -describe("Page", () => { - afterEach(() => { - cleanup(); - }); - - test("should redirect to /", () => { - render(); - expect(vi.mocked(redirect)).toHaveBeenCalledWith("/"); - }); -}); diff --git a/apps/web/app/(app)/layout.test.tsx b/apps/web/app/(app)/layout.test.tsx deleted file mode 100644 index 23512b5574..0000000000 --- a/apps/web/app/(app)/layout.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { getServerSession } from "next-auth"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TUser } from "@formbricks/types/user"; -import { getUser } from "@/lib/user/service"; -import AppLayout from "./layout"; - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); - -vi.mock("@/lib/constants", () => ({ - INTERCOM_SECRET_KEY: "test-secret-key", - IS_INTERCOM_CONFIGURED: true, - INTERCOM_APP_ID: "test-app-id", - ENCRYPTION_KEY: "test-encryption-key", - ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key", - GITHUB_ID: "test-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_POSTHOG_CONFIGURED: true, - POSTHOG_API_HOST: "test-posthog-api-host", - POSTHOG_API_KEY: "test-posthog-api-key", - FORMBRICKS_ENVIRONMENT_ID: "mock-formbricks-environment-id", - IS_FORMBRICKS_ENABLED: true, - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -vi.mock("@/app/intercom/IntercomClientWrapper", () => ({ - IntercomClientWrapper: () =>
    , -})); -vi.mock("@/modules/ui/components/no-mobile-overlay", () => ({ - NoMobileOverlay: () =>
    , -})); -vi.mock("@/modules/ui/components/toaster-client", () => ({ - ToasterClient: () =>
    , -})); - -describe("(app) AppLayout", () => { - afterEach(() => { - cleanup(); - }); - - test("renders child content and all sub-components when user exists", async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } }); - vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser); - - // Because AppLayout is async, call it like a function - const element = await AppLayout({ - children:
    Hello from children
    , - }); - - render(element); - - expect(screen.getByTestId("no-mobile-overlay")).toBeInTheDocument(); - expect(screen.getByTestId("mock-intercom-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("toaster-client")).toBeInTheDocument(); - expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children"); - }); -}); diff --git a/apps/web/app/(auth)/auth/forgot-password/page.test.tsx b/apps/web/app/(auth)/auth/forgot-password/page.test.tsx deleted file mode 100644 index 14d4e78196..0000000000 --- a/apps/web/app/(auth)/auth/forgot-password/page.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import ForgotPasswordPage from "./page"; - -vi.mock("@/modules/auth/forgot-password/page", () => ({ - ForgotPasswordPage: () => ( -
    -
    -
    Forgot Password Form
    -
    -
    - ), -})); - -describe("ForgotPasswordPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the forgot password page", () => { - render(); - expect(screen.getByTestId("forgot-password-page")).toBeInTheDocument(); - }); - - test("renders the form wrapper", () => { - render(); - expect(screen.getByTestId("form-wrapper")).toBeInTheDocument(); - }); - - test("renders the forgot password form", () => { - render(); - expect(screen.getByTestId("forgot-password-form")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(auth)/email-change-without-verification-success/page.test.tsx b/apps/web/app/(auth)/email-change-without-verification-success/page.test.tsx deleted file mode 100644 index df6aa37986..0000000000 --- a/apps/web/app/(auth)/email-change-without-verification-success/page.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { cleanup, render } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import EmailChangeWithoutVerificationSuccessPage from "./page"; - -vi.mock("@/modules/auth/email-change-without-verification-success/page", () => ({ - EmailChangeWithoutVerificationSuccessPage: ({ children }) => ( -
    {children}
    - ), -})); - -describe("EmailChangeWithoutVerificationSuccessPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders EmailChangeWithoutVerificationSuccessPage", () => { - const { getByTestId } = render(); - expect(getByTestId("email-change-success-page")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/(auth)/layout.test.tsx b/apps/web/app/(auth)/layout.test.tsx deleted file mode 100644 index daeef3c8e1..0000000000 --- a/apps/web/app/(auth)/layout.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { render, screen } from "@testing-library/react"; -import { describe, expect, test, vi } from "vitest"; -import AppLayout from "../(auth)/layout"; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_INTERCOM_CONFIGURED: true, - INTERCOM_SECRET_KEY: "mock-intercom-secret-key", - INTERCOM_APP_ID: "mock-intercom-app-id", -})); - -vi.mock("@/app/intercom/IntercomClientWrapper", () => ({ - IntercomClientWrapper: () =>
    , -})); -vi.mock("@/modules/ui/components/no-mobile-overlay", () => ({ - NoMobileOverlay: () =>
    , -})); - -describe("(auth) AppLayout", () => { - test("renders the NoMobileOverlay and IntercomClient, plus children", async () => { - const appLayoutElement = await AppLayout({ - children:
    Hello from children!
    , - }); - - const childContentText = "Hello from children!"; - - render(appLayoutElement); - - expect(screen.getByTestId("mock-no-mobile-overlay")).toBeInTheDocument(); - expect(screen.getByTestId("mock-intercom-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("child-content")).toHaveTextContent(childContentText); - }); -}); diff --git a/apps/web/app/ClientEnvironmentRedirect.test.tsx b/apps/web/app/ClientEnvironmentRedirect.test.tsx deleted file mode 100644 index 2eb041a683..0000000000 --- a/apps/web/app/ClientEnvironmentRedirect.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render } from "@testing-library/react"; -import { useRouter } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage"; -import ClientEnvironmentRedirect from "./ClientEnvironmentRedirect"; - -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), -})); - -describe("ClientEnvironmentRedirect", () => { - afterEach(() => { - cleanup(); - }); - - test("should redirect to the first environment ID when no last environment exists", () => { - const mockPush = vi.fn(); - vi.mocked(useRouter).mockReturnValue({ push: mockPush } as any); - - // Mock localStorage - const localStorageMock = { - getItem: vi.fn().mockReturnValue(null), - removeItem: vi.fn(), - }; - - Object.defineProperty(window, "localStorage", { - value: localStorageMock, - }); - - render(); - - expect(mockPush).toHaveBeenCalledWith("/environments/test-env-id"); - }); - - test("should redirect to the last environment ID when it exists in localStorage and is valid", () => { - const mockPush = vi.fn(); - vi.mocked(useRouter).mockReturnValue({ push: mockPush } as any); - - // Mock localStorage with a last environment ID - const localStorageMock = { - getItem: vi.fn().mockReturnValue("last-env-id"), - removeItem: vi.fn(), - }; - Object.defineProperty(window, "localStorage", { - value: localStorageMock, - }); - - render(); - - expect(localStorageMock.getItem).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS); - expect(mockPush).toHaveBeenCalledWith("/environments/last-env-id"); - }); - - test("should clear invalid environment ID and redirect to default when stored ID is not in user environments", () => { - const mockPush = vi.fn(); - vi.mocked(useRouter).mockReturnValue({ push: mockPush } as any); - - // Mock localStorage with an invalid environment ID - const localStorageMock = { - getItem: vi.fn().mockReturnValue("invalid-env-id"), - removeItem: vi.fn(), - }; - Object.defineProperty(window, "localStorage", { - value: localStorageMock, - }); - - render(); - - expect(localStorageMock.getItem).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS); - expect(localStorageMock.removeItem).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS); - expect(mockPush).toHaveBeenCalledWith("/environments/valid-env-1"); - }); - - test("should update redirect when environment ID prop changes", () => { - const mockPush = vi.fn(); - vi.mocked(useRouter).mockReturnValue({ push: mockPush } as any); - - // Mock localStorage - const localStorageMock = { - getItem: vi.fn().mockReturnValue(null), - removeItem: vi.fn(), - }; - Object.defineProperty(window, "localStorage", { - value: localStorageMock, - }); - - const { rerender } = render(); - expect(mockPush).toHaveBeenCalledWith("/environments/initial-env-id"); - - // Clear mock calls - mockPush.mockClear(); - - // Rerender with new environment ID - rerender(); - expect(mockPush).toHaveBeenCalledWith("/environments/new-env-id"); - }); -}); diff --git a/apps/web/app/error.test.tsx b/apps/web/app/error.test.tsx deleted file mode 100644 index 90cb5752ae..0000000000 --- a/apps/web/app/error.test.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import * as Sentry from "@sentry/nextjs"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import ErrorBoundary from "./error"; - -vi.mock("@/modules/ui/components/button", () => ({ - Button: (props: any) => , -})); - -vi.mock("@/modules/ui/components/error-component", () => ({ - ErrorComponent: ({ title, description }: { title: string; description: string }) => ( -
    -
    {title}
    -
    {description}
    -
    - ), -})); - -vi.mock("@sentry/nextjs", () => ({ - captureException: vi.fn(), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - const translations: Record = { - "common.error_rate_limit_title": "Too Many Requests", - "common.error_rate_limit_description": "You're making too many requests. Please slow down.", - "common.error_component_title": "Something went wrong", - "common.error_component_description": "An unexpected error occurred. Please try again.", - "common.try_again": "Try Again", - "common.go_to_dashboard": "Go to Dashboard", - }; - return translations[key] || key; - }, - }), -})); - -vi.mock("@formbricks/types/errors", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - getClientErrorData: vi.fn(), - }; -}); - -describe("ErrorBoundary", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const dummyError = new Error("Test error"); - const resetMock = vi.fn(); - - test("logs error via console.error in development", async () => { - (process.env as { [key: string]: string }).NODE_ENV = "development"; - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - vi.mocked(Sentry.captureException).mockImplementation((() => {}) as any); - - const { getClientErrorData } = await import("@formbricks/types/errors"); - vi.mocked(getClientErrorData).mockReturnValue({ - type: "general", - showButtons: true, - }); - - render(); - - await waitFor(() => { - expect(consoleErrorSpy).toHaveBeenCalledWith("Test error"); - }); - expect(Sentry.captureException).not.toHaveBeenCalled(); - }); - - test("captures error with Sentry in production", async () => { - (process.env as { [key: string]: string }).NODE_ENV = "production"; - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - vi.mocked(Sentry.captureException).mockImplementation((() => {}) as any); - - const { getClientErrorData } = await import("@formbricks/types/errors"); - vi.mocked(getClientErrorData).mockReturnValue({ - type: "general", - showButtons: true, - }); - - render(); - - await waitFor(() => { - expect(Sentry.captureException).toHaveBeenCalled(); - }); - expect(consoleErrorSpy).not.toHaveBeenCalled(); - }); - - test("calls reset when try again button is clicked for general errors", async () => { - const { getClientErrorData } = await import("@formbricks/types/errors"); - vi.mocked(getClientErrorData).mockReturnValue({ - type: "general", - showButtons: true, - }); - - render(); - const tryAgainBtn = screen.getByRole("button", { name: "Try Again" }); - userEvent.click(tryAgainBtn); - await waitFor(() => expect(resetMock).toHaveBeenCalled()); - }); - - test("sets window.location.href to '/' when dashboard button is clicked for general errors", async () => { - const { getClientErrorData } = await import("@formbricks/types/errors"); - vi.mocked(getClientErrorData).mockReturnValue({ - type: "general", - showButtons: true, - }); - - const originalLocation = window.location; - (window as any).location = undefined; - (window as any).location = { href: "" }; - render(); - const dashBtn = screen.getByRole("button", { name: "Go to Dashboard" }); - userEvent.click(dashBtn); - await waitFor(() => { - expect(window.location.href).toBe("/"); - }); - (window as any).location = originalLocation; - }); - - test("does not show buttons for rate limit errors", async () => { - const { getClientErrorData } = await import("@formbricks/types/errors"); - vi.mocked(getClientErrorData).mockReturnValue({ - type: "rate_limit", - showButtons: false, - }); - - render(); - - expect(screen.queryByRole("button", { name: "Try Again" })).not.toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "Go to Dashboard" })).not.toBeInTheDocument(); - }); - - test("shows error component with rate limit messages for rate limit errors", async () => { - const { getClientErrorData } = await import("@formbricks/types/errors"); - vi.mocked(getClientErrorData).mockReturnValue({ - type: "rate_limit", - showButtons: false, - }); - - render(); - - expect(screen.getByTestId("ErrorComponent")).toBeInTheDocument(); - expect(screen.getByTestId("error-title")).toHaveTextContent("Too Many Requests"); - expect(screen.getByTestId("error-description")).toHaveTextContent( - "You're making too many requests. Please slow down." - ); - expect(getClientErrorData).toHaveBeenCalledWith(dummyError); - }); - - test("shows error component with general messages for general errors", async () => { - const { getClientErrorData } = await import("@formbricks/types/errors"); - vi.mocked(getClientErrorData).mockReturnValue({ - type: "general", - showButtons: true, - }); - - render(); - - expect(screen.getByTestId("ErrorComponent")).toBeInTheDocument(); - expect(screen.getByTestId("error-title")).toHaveTextContent("Something went wrong"); - expect(screen.getByTestId("error-description")).toHaveTextContent( - "An unexpected error occurred. Please try again." - ); - expect(getClientErrorData).toHaveBeenCalledWith(dummyError); - }); - - test("shows buttons for general errors", async () => { - const { getClientErrorData } = await import("@formbricks/types/errors"); - vi.mocked(getClientErrorData).mockReturnValue({ - type: "general", - showButtons: true, - }); - - render(); - - expect(screen.getByRole("button", { name: "Try Again" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Go to Dashboard" })).toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/global-error.test.tsx b/apps/web/app/global-error.test.tsx deleted file mode 100644 index 52b339d031..0000000000 --- a/apps/web/app/global-error.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as Sentry from "@sentry/nextjs"; -import { cleanup, render, waitFor } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import GlobalError from "./global-error"; - -vi.mock("@sentry/nextjs", () => ({ - captureException: vi.fn(), -})); - -describe("GlobalError", () => { - const dummyError = new Error("Test error"); - - afterEach(() => { - cleanup(); - }); - - test("logs error using console.error in development", async () => { - (process.env as { [key: string]: string }).NODE_ENV = "development"; - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - vi.mocked(Sentry.captureException).mockImplementation((() => {}) as any); - - render(); - - await waitFor(() => { - expect(consoleErrorSpy).toHaveBeenCalledWith("Test error"); - }); - expect(Sentry.captureException).not.toHaveBeenCalled(); - }); - - test("captures error with Sentry in production", async () => { - (process.env as { [key: string]: string }).NODE_ENV = "production"; - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - render(); - - await waitFor(() => { - expect(Sentry.captureException).toHaveBeenCalled(); - }); - expect(consoleErrorSpy).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/app/intercom/IntercomClient.test.tsx b/apps/web/app/intercom/IntercomClient.test.tsx deleted file mode 100644 index 6f96920bd7..0000000000 --- a/apps/web/app/intercom/IntercomClient.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import Intercom from "@intercom/messenger-js-sdk"; -import "@testing-library/jest-dom/vitest"; -import { cleanup, render } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TUser } from "@formbricks/types/user"; -import { IntercomClient } from "./IntercomClient"; - -vi.mock("@intercom/messenger-js-sdk", () => ({ - default: vi.fn(), -})); - -describe("IntercomClient", () => { - let originalWindowIntercom: any; - let mockWindowIntercom = vi.fn(); - - beforeEach(() => { - // Save original window.Intercom so we can restore it later - originalWindowIntercom = global.window?.Intercom; - // Mock window.Intercom so we can verify the shutdown call on unmount - global.window.Intercom = mockWindowIntercom; - }); - - afterEach(() => { - cleanup(); - // Restore the original window.Intercom - global.window.Intercom = originalWindowIntercom; - }); - - test("calls Intercom with user data when isIntercomConfigured is true and user is provided", () => { - const testUser = { - id: "test-id", - name: "Test User", - email: "test@example.com", - createdAt: new Date("2020-01-01T00:00:00Z"), - } as TUser; - - render( - - ); - - // Verify Intercom was called with the expected params - expect(Intercom).toHaveBeenCalledTimes(1); - expect(Intercom).toHaveBeenCalledWith({ - app_id: "my-app-id", - user_id: "test-id", - user_hash: "my-user-hash", - name: "Test User", - email: "test@example.com", - created_at: 1577836800, // Epoch for 2020-01-01T00:00:00Z - }); - }); - - test("calls Intercom with user data without createdAt", () => { - const testUser = { - id: "test-id", - name: "Test User", - email: "test@example.com", - } as TUser; - - render( - - ); - - // Verify Intercom was called with the expected params - expect(Intercom).toHaveBeenCalledTimes(1); - expect(Intercom).toHaveBeenCalledWith({ - app_id: "my-app-id", - user_id: "test-id", - user_hash: "my-user-hash", - name: "Test User", - email: "test@example.com", - created_at: undefined, - }); - }); - - test("calls Intercom with minimal params if user is not provided", () => { - render( - - ); - - expect(Intercom).toHaveBeenCalledTimes(1); - expect(Intercom).toHaveBeenCalledWith({ - app_id: "my-app-id", - }); - }); - - test("does not call Intercom if isIntercomConfigured is false", () => { - render( - - ); - - expect(Intercom).not.toHaveBeenCalled(); - }); - - test("shuts down Intercom on unmount", () => { - const { unmount } = render( - - ); - - // Reset call count; we only care about the shutdown after unmount - mockWindowIntercom.mockClear(); - - unmount(); - - // Intercom should be shut down on unmount - expect(mockWindowIntercom).toHaveBeenCalledWith("shutdown"); - }); - - test("logs an error if Intercom initialization fails", () => { - // Spy on console.error - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - // Force Intercom to throw an error on invocation - vi.mocked(Intercom).mockImplementationOnce(() => { - throw new Error("Intercom test error"); - }); - - // Render the component with isIntercomConfigured=true so it tries to initialize - render( - - ); - - // Verify that console.error was called with the correct message - expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to initialize Intercom:", expect.any(Error)); - - // Clean up the spy - consoleErrorSpy.mockRestore(); - }); - - test("logs an error if isIntercomConfigured is true but no intercomAppId is provided", () => { - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - render( - - ); - - // We expect a caught error: "Intercom app ID is required" - expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to initialize Intercom:", expect.any(Error)); - const [, caughtError] = consoleErrorSpy.mock.calls[0]; - expect((caughtError as Error).message).toBe("Intercom app ID is required"); - consoleErrorSpy.mockRestore(); - }); - - test("logs an error if isIntercomConfigured is true but no intercomUserHash is provided", () => { - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const testUser = { - id: "test-id", - name: "Test User", - email: "test@example.com", - } as TUser; - - render( - - ); - - // We expect a caught error: "Intercom user hash is required" - expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to initialize Intercom:", expect.any(Error)); - const [, caughtError] = consoleErrorSpy.mock.calls[0]; - expect((caughtError as Error).message).toBe("Intercom user hash is required"); - consoleErrorSpy.mockRestore(); - }); -}); diff --git a/apps/web/app/intercom/IntercomClientWrapper.test.tsx b/apps/web/app/intercom/IntercomClientWrapper.test.tsx deleted file mode 100644 index 59bcc1989b..0000000000 --- a/apps/web/app/intercom/IntercomClientWrapper.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TUser } from "@formbricks/types/user"; -import { IntercomClientWrapper } from "./IntercomClientWrapper"; - -vi.mock("@/lib/constants", () => ({ - IS_INTERCOM_CONFIGURED: true, - INTERCOM_APP_ID: "mock-intercom-app-id", - INTERCOM_SECRET_KEY: "mock-intercom-secret-key", -})); - -// Mock the crypto createHmac function to return a fake hash. -// Vite global setup doesn't work here due to Intercom probably using crypto themselves. -vi.mock("crypto", () => ({ - default: { - createHmac: vi.fn(() => ({ - update: vi.fn().mockReturnThis(), - digest: vi.fn().mockReturnValue("fake-hash"), - })), - }, -})); - -vi.mock("./IntercomClient", () => ({ - IntercomClient: (props: any) => ( -
    - ), -})); - -describe("IntercomClientWrapper", () => { - afterEach(() => { - cleanup(); - }); - - test("renders IntercomClient with computed user hash when user is provided", () => { - const testUser = { id: "user-123", name: "Test User", email: "test@example.com" } as TUser; - - render(); - - const intercomClientEl = screen.getByTestId("mock-intercom-client"); - expect(intercomClientEl).toBeInTheDocument(); - - const props = JSON.parse(intercomClientEl.getAttribute("data-props") ?? "{}"); - - // Check that the computed hash equals "fake-hash" (as per our crypto mock) - expect(props.intercomUserHash).toBe("fake-hash"); - expect(props.intercomAppId).toBe("mock-intercom-app-id"); - expect(props.isIntercomConfigured).toBe(true); - expect(props.user).toEqual(testUser); - }); - - test("renders IntercomClient without computing a hash when no user is provided", () => { - render(); - - const intercomClientEl = screen.getByTestId("mock-intercom-client"); - expect(intercomClientEl).toBeInTheDocument(); - - const props = JSON.parse(intercomClientEl.getAttribute("data-props") ?? "{}"); - - expect(props.intercomUserHash).toBeUndefined(); - expect(props.intercomAppId).toBe("mock-intercom-app-id"); - expect(props.isIntercomConfigured).toBe(true); - expect(props.user).toBeNull(); - }); -}); diff --git a/apps/web/app/layout.test.tsx b/apps/web/app/layout.test.tsx deleted file mode 100644 index 2c9eec3088..0000000000 --- a/apps/web/app/layout.test.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { cleanup } from "@testing-library/react"; -import { TolgeeInstance } from "@tolgee/react"; -import React from "react"; -import { renderToString } from "react-dom/server"; -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { getLocale } from "@/tolgee/language"; -import { getTolgee } from "@/tolgee/server"; -import RootLayout, { metadata } from "./layout"; - -// Mock dependencies for the layout - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - SENTRY_RELEASE: "mock-sentry-release", - SENTRY_ENVIRONMENT: "mock-sentry-environment", -})); - -vi.mock("@/tolgee/language", () => ({ - getLocale: vi.fn(), -})); - -vi.mock("@/tolgee/server", () => ({ - getTolgee: vi.fn(), -})); - -vi.mock("@/tolgee/client", () => ({ - TolgeeNextProvider: ({ - children, - language, - staticData, - }: { - children: React.ReactNode; - language: string; - staticData: any; - }) => ( -
    - TolgeeNextProvider: {language} {JSON.stringify(staticData)} - {children} -
    - ), -})); - -vi.mock("@/app/sentry/SentryProvider", () => ({ - SentryProvider: ({ - children, - sentryDsn, - sentryRelease, - }: { - children: React.ReactNode; - sentryDsn?: string; - sentryRelease?: string; - }) => ( -
    - SentryProvider: {sentryDsn} - {sentryRelease && ` - Release: ${sentryRelease}`} - {children} -
    - ), -})); - -describe("RootLayout", () => { - beforeEach(() => { - cleanup(); - process.env.VERCEL = "1"; - }); - - test("renders the layout with the correct structure and providers", async () => { - const fakeLocale = "en-US"; - // Mock getLocale to resolve to a fake locale - vi.mocked(getLocale).mockResolvedValue(fakeLocale); - - const fakeStaticData = { key: "value" }; - const fakeTolgee = { - loadRequired: vi.fn().mockResolvedValue(fakeStaticData), - }; - // Mock getTolgee to return our fake tolgee object - vi.mocked(getTolgee).mockResolvedValue(fakeTolgee as unknown as TolgeeInstance); - - const children =
    Child Content
    ; - const element = await RootLayout({ children }); - const html = renderToString(element); - - // Create a container and set its innerHTML - const container = document.createElement("div"); - container.innerHTML = html; - document.body.appendChild(container); - - // Now we can use screen queries on the rendered content - expect(container.querySelector('[data-testid="tolgee-next-provider"]')).toBeInTheDocument(); - expect(container.querySelector('[data-testid="sentry-provider"]')).toBeInTheDocument(); - expect(container.querySelector('[data-testid="child"]')).toHaveTextContent("Child Content"); - - // Cleanup - document.body.removeChild(container); - }); - - test("renders with different locale", async () => { - const fakeLocale = "de-DE"; - vi.mocked(getLocale).mockResolvedValue(fakeLocale); - - const fakeStaticData = { key: "value" }; - const fakeTolgee = { - loadRequired: vi.fn().mockResolvedValue(fakeStaticData), - }; - vi.mocked(getTolgee).mockResolvedValue(fakeTolgee as unknown as TolgeeInstance); - - const children =
    Child Content
    ; - const element = await RootLayout({ children }); - const html = renderToString(element); - - const container = document.createElement("div"); - container.innerHTML = html; - document.body.appendChild(container); - - const tolgeeProvider = container.querySelector('[data-testid="tolgee-next-provider"]'); - expect(tolgeeProvider).toHaveTextContent(fakeLocale); - - document.body.removeChild(container); - }); - - test("exports correct metadata", () => { - expect(metadata).toEqual({ - title: { - template: "%s | Formbricks", - default: "Formbricks", - }, - description: "Open-Source Survey Suite", - }); - }); -}); diff --git a/apps/web/app/not-found.test.tsx b/apps/web/app/not-found.test.tsx deleted file mode 100644 index ece5afabef..0000000000 --- a/apps/web/app/not-found.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup } from "@testing-library/preact"; -import { render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test } from "vitest"; -import NotFound from "./not-found"; - -describe("NotFound", () => { - afterEach(() => { - cleanup(); - }); - - test("renders 404 page with correct content", () => { - render(); - - // Check for the 404 text - const errorCode = screen.getByTestId("error-code"); - expect(errorCode).toBeInTheDocument(); - expect(errorCode).toHaveClass("text-sm", "font-semibold"); - expect(errorCode).toHaveTextContent("404"); - - // Check for the heading - const heading = screen.getByRole("heading", { name: "Page not found" }); - expect(heading).toBeInTheDocument(); - expect(heading).toHaveClass("mt-2", "text-2xl", "font-bold"); - - // Check for the error message - const errorMessage = screen.getByTestId("error-message"); - expect(errorMessage).toBeInTheDocument(); - expect(errorMessage).toHaveClass("mt-2", "text-base"); - expect(errorMessage).toHaveTextContent("Sorry, we couldn't find the page you're looking for."); - - // Check for the button - const button = screen.getByRole("button", { name: "Back to home" }); - expect(button).toBeInTheDocument(); - expect(button).toHaveClass("mt-8"); - }); -}); diff --git a/apps/web/app/page.test.tsx b/apps/web/app/page.test.tsx deleted file mode 100644 index f327a2b59d..0000000000 --- a/apps/web/app/page.test.tsx +++ /dev/null @@ -1,442 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup } from "@testing-library/react"; -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { TMembership } from "@formbricks/types/memberships"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TProject } from "@formbricks/types/project"; -import { TUser } from "@formbricks/types/user"; -import Page from "./page"; - -vi.mock("@/lib/project/service", () => ({ - getUserProjectEnvironmentsByOrganizationIds: vi.fn(), -})); - -vi.mock("@/lib/instance/service", () => ({ - getIsFreshInstance: vi.fn(), -})); - -vi.mock("@/lib/membership/service", () => ({ - getMembershipByUserIdOrganizationId: vi.fn(), -})); - -vi.mock("@/lib/membership/utils", () => ({ - getAccessFlags: vi.fn(), -})); - -vi.mock("@/lib/organization/service", () => ({ - getOrganizationsByUserId: vi.fn(), -})); - -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); - -vi.mock("@/modules/auth/lib/authOptions", () => ({ - authOptions: {}, -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -vi.mock("@/modules/ui/components/client-logout", () => ({ - ClientLogout: () =>
    Client Logout
    , -})); - -vi.mock("@/app/ClientEnvironmentRedirect", () => ({ - default: ({ environmentId, userEnvironments }: { environmentId: string; userEnvironments?: string[] }) => ( -
    - Environment ID: {environmentId} - {userEnvironments && ` | User Environments: ${userEnvironments.join(", ")}`} -
    - ), -})); - -describe("Page", () => { - beforeEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("redirects to setup/intro when no session and fresh instance", async () => { - const { getServerSession } = await import("next-auth"); - const { getIsFreshInstance } = await import("@/lib/instance/service"); - const { redirect } = await import("next/navigation"); - - vi.mocked(getServerSession).mockResolvedValue(null); - vi.mocked(getIsFreshInstance).mockResolvedValue(true); - - await Page(); - - expect(redirect).toHaveBeenCalledWith("/setup/intro"); - }); - - test("redirects to auth/login when no session and not fresh instance", async () => { - const { getServerSession } = await import("next-auth"); - const { getIsFreshInstance } = await import("@/lib/instance/service"); - const { redirect } = await import("next/navigation"); - - vi.mocked(getServerSession).mockResolvedValue(null); - vi.mocked(getIsFreshInstance).mockResolvedValue(false); - - await Page(); - - expect(redirect).toHaveBeenCalledWith("/auth/login"); - }); - - test("shows client logout when user is not found", async () => { - const { getServerSession } = await import("next-auth"); - const { getIsFreshInstance } = await import("@/lib/instance/service"); - const { getUser } = await import("@/lib/user/service"); - const { render } = await import("@testing-library/react"); - - vi.mocked(getServerSession).mockResolvedValue({ - user: { id: "test-user-id" }, - } as any); - vi.mocked(getIsFreshInstance).mockResolvedValue(false); - vi.mocked(getUser).mockResolvedValue(null); - - const result = await Page(); - const { container } = render(result); - - expect(container.querySelector('[data-testid="client-logout"]')).toBeInTheDocument(); - }); - - test("redirects to organization creation when user has no organizations", async () => { - const { getServerSession } = await import("next-auth"); - const { getIsFreshInstance } = await import("@/lib/instance/service"); - const { getUser } = await import("@/lib/user/service"); - const { getOrganizationsByUserId } = await import("@/lib/organization/service"); - const { redirect } = await import("next/navigation"); - - const mockUser: TUser = { - id: "test-user-id", - name: "Test User", - email: "test@example.com", - emailVerified: null, - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - role: null, - objective: null, - notificationSettings: { - alert: {}, - unsubscribedOrganizationIds: [], - }, - locale: "en-US", - lastLoginAt: null, - isActive: true, - }; - - vi.mocked(getServerSession).mockResolvedValue({ - user: { id: "test-user-id" }, - } as any); - vi.mocked(getIsFreshInstance).mockResolvedValue(false); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getOrganizationsByUserId).mockResolvedValue([]); - - await Page(); - - expect(redirect).toHaveBeenCalledWith("/setup/organization/create"); - }); - - test("redirects to project creation when user has organizations but no environment", async () => { - const { getServerSession } = await import("next-auth"); - const { getIsFreshInstance } = await import("@/lib/instance/service"); - const { getUser } = await import("@/lib/user/service"); - const { getOrganizationsByUserId } = await import("@/lib/organization/service"); - const { getUserProjectEnvironmentsByOrganizationIds } = await import("@/lib/project/service"); - const { getMembershipByUserIdOrganizationId } = await import("@/lib/membership/service"); - const { getAccessFlags } = await import("@/lib/membership/utils"); - const { redirect } = await import("next/navigation"); - - const mockUser: TUser = { - id: "test-user-id", - name: "Test User", - email: "test@example.com", - emailVerified: null, - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - role: null, - objective: null, - notificationSettings: { - alert: {}, - unsubscribedOrganizationIds: [], - }, - locale: "en-US", - lastLoginAt: null, - isActive: true, - }; - - const mockOrganization: TOrganization = { - id: "test-org-id", - name: "Test Organization", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - stripeCustomerId: null, - plan: "free", - period: "monthly", - limits: { - projects: 3, - monthly: { - responses: 1500, - miu: 2000, - }, - }, - periodStart: new Date(), - }, - isAIEnabled: false, - }; - - const mockMembership: TMembership = { - organizationId: "test-org-id", - userId: "test-user-id", - accepted: true, - role: "owner", - }; - - const mockUserProjects = [ - { - id: "test-project-id", - name: "Test Project", - environments: [], - }, - ]; - - vi.mocked(getServerSession).mockResolvedValue({ - user: { id: "test-user-id" }, - } as any); - vi.mocked(getIsFreshInstance).mockResolvedValue(false); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getUserProjectEnvironmentsByOrganizationIds).mockResolvedValue( - mockUserProjects as unknown as TProject[] - ); - vi.mocked(getOrganizationsByUserId).mockResolvedValue([mockOrganization]); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - vi.mocked(getAccessFlags).mockReturnValue({ - isManager: false, - isOwner: true, - isBilling: false, - isMember: true, - }); - - await Page(); - - expect(redirect).toHaveBeenCalledWith(`/organizations/${mockOrganization.id}/projects/new/mode`); - }); - - test("redirects to landing when user has organizations but no environment and is not owner/manager", async () => { - const { getServerSession } = await import("next-auth"); - const { getIsFreshInstance } = await import("@/lib/instance/service"); - const { getUser } = await import("@/lib/user/service"); - const { getUserProjectEnvironmentsByOrganizationIds } = await import("@/lib/project/service"); - const { getOrganizationsByUserId } = await import("@/lib/organization/service"); - const { getMembershipByUserIdOrganizationId } = await import("@/lib/membership/service"); - const { getAccessFlags } = await import("@/lib/membership/utils"); - const { redirect } = await import("next/navigation"); - - const mockUser: TUser = { - id: "test-user-id", - name: "Test User", - email: "test@example.com", - emailVerified: null, - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - role: null, - objective: null, - notificationSettings: { - alert: {}, - unsubscribedOrganizationIds: [], - }, - locale: "en-US", - lastLoginAt: null, - isActive: true, - }; - - const mockOrganization: TOrganization = { - id: "test-org-id", - name: "Test Organization", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - stripeCustomerId: null, - plan: "free", - period: "monthly", - limits: { - projects: 3, - monthly: { - responses: 1500, - miu: 2000, - }, - }, - periodStart: new Date(), - }, - isAIEnabled: false, - }; - - const mockMembership: TMembership = { - organizationId: "test-org-id", - userId: "test-user-id", - accepted: true, - role: "member", - }; - - const mockUserProjects = [ - { - id: "test-project-id", - name: "Test Project", - environments: [], - }, - ]; - - vi.mocked(getServerSession).mockResolvedValue({ - user: { id: "test-user-id" }, - } as any); - vi.mocked(getIsFreshInstance).mockResolvedValue(false); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getUserProjectEnvironmentsByOrganizationIds).mockResolvedValue( - mockUserProjects as unknown as TProject[] - ); - vi.mocked(getOrganizationsByUserId).mockResolvedValue([mockOrganization]); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - vi.mocked(getAccessFlags).mockReturnValue({ - isManager: false, - isOwner: false, - isBilling: false, - isMember: true, - }); - - await Page(); - - expect(redirect).toHaveBeenCalledWith(`/organizations/${mockOrganization.id}/landing`); - }); - - test("renders ClientEnvironmentRedirect when user has environment", async () => { - const { getServerSession } = await import("next-auth"); - const { getIsFreshInstance } = await import("@/lib/instance/service"); - const { getUser } = await import("@/lib/user/service"); - const { getOrganizationsByUserId } = await import("@/lib/organization/service"); - const { getMembershipByUserIdOrganizationId } = await import("@/lib/membership/service"); - const { getAccessFlags } = await import("@/lib/membership/utils"); - const { getUserProjectEnvironmentsByOrganizationIds } = await import("@/lib/project/service"); - const { render } = await import("@testing-library/react"); - - const mockUser: TUser = { - id: "test-user-id", - name: "Test User", - email: "test@example.com", - emailVerified: null, - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - role: null, - objective: null, - notificationSettings: { - alert: {}, - unsubscribedOrganizationIds: [], - }, - locale: "en-US", - lastLoginAt: null, - isActive: true, - }; - - const mockOrganization: TOrganization = { - id: "test-org-id", - name: "Test Organization", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - stripeCustomerId: null, - plan: "free", - period: "monthly", - limits: { - projects: 3, - monthly: { - responses: 1500, - miu: 2000, - }, - }, - periodStart: new Date(), - }, - isAIEnabled: false, - }; - - const mockMembership: TMembership = { - organizationId: "test-org-id", - userId: "test-user-id", - accepted: true, - role: "member", - }; - - const mockUserProjects = [ - { - id: "project-1", - name: "Test Project", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "test-org-id", - styling: { allowStyleOverwrite: true }, - recontactDays: 0, - inAppSurveyBranding: false, - linkSurveyBranding: false, - config: { channel: "link" as const, industry: "saas" as const }, - placement: "bottomRight" as const, - clickOutsideClose: false, - darkOverlay: false, - languages: [], - logo: null, - environments: [ - { - id: "test-env-id", - type: "production" as const, - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project-1", - appSetupCompleted: true, - }, - { - id: "test-env-dev", - type: "development" as const, - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project-1", - appSetupCompleted: true, - }, - ], - }, - ] as any; - - vi.mocked(getServerSession).mockResolvedValue({ - user: { id: "test-user-id" }, - } as any); - vi.mocked(getIsFreshInstance).mockResolvedValue(false); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getOrganizationsByUserId).mockResolvedValue([mockOrganization]); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - vi.mocked(getUserProjectEnvironmentsByOrganizationIds).mockResolvedValue(mockUserProjects); - vi.mocked(getAccessFlags).mockReturnValue({ - isManager: false, - isOwner: false, - isBilling: false, - isMember: true, - }); - - const result = await Page(); - const { container } = render(result); - - expect(container.querySelector('[data-testid="client-environment-redirect"]')).toHaveTextContent( - `User Environments: test-env-id, test-env-dev` - ); - }); -}); diff --git a/apps/web/app/sentry/SentryProvider.test.tsx b/apps/web/app/sentry/SentryProvider.test.tsx deleted file mode 100644 index 6be78aeab0..0000000000 --- a/apps/web/app/sentry/SentryProvider.test.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import * as Sentry from "@sentry/nextjs"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { SentryProvider } from "./SentryProvider"; - -vi.mock("@sentry/nextjs", async () => { - const actual = await vi.importActual("@sentry/nextjs"); - return { - ...actual, - replayIntegration: (options: any) => { - return { - name: "Replay", - id: "Replay", - options, - }; - }, - }; -}); - -const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0"; - -describe("SentryProvider", () => { - afterEach(() => { - cleanup(); - }); - - test("calls Sentry.init when sentryDsn is provided", () => { - const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); - - render( - -
    Test Content
    -
    - ); - - // The useEffect runs after mount, so Sentry.init should have been called. - expect(initSpy).toHaveBeenCalled(); - expect(initSpy).toHaveBeenCalledWith( - expect.objectContaining({ - dsn: sentryDsn, - tracesSampleRate: 0, - debug: false, - replaysOnErrorSampleRate: 1.0, - replaysSessionSampleRate: 0.1, - integrations: expect.any(Array), - beforeSend: expect.any(Function), - }) - ); - }); - - test("calls Sentry.init with sentryRelease when provided", () => { - const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); - const testRelease = "v1.2.3"; - - render( - -
    Test Content
    -
    - ); - - expect(initSpy).toHaveBeenCalledWith( - expect.objectContaining({ - dsn: sentryDsn, - release: testRelease, - }) - ); - }); - - test("does not call Sentry.init when sentryDsn is not provided", () => { - const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); - - render( - -
    Test Content
    -
    - ); - - expect(initSpy).not.toHaveBeenCalled(); - }); - - test("does not call Sentry.init when isEnabled is not provided", () => { - const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); - - render( - -
    Test Content
    -
    - ); - - expect(initSpy).not.toHaveBeenCalled(); - }); - - test("renders children", () => { - render( - -
    Test Content
    -
    - ); - expect(screen.getByTestId("child")).toHaveTextContent("Test Content"); - }); - - test("does not reinitialize Sentry when props change after initial render", () => { - const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); - - const { rerender } = render( - -
    Test Content
    -
    - ); - - expect(initSpy).toHaveBeenCalledTimes(1); - - rerender( - -
    Test Content
    -
    - ); - - expect(initSpy).toHaveBeenCalledTimes(1); - }); - - test("processes beforeSend correctly", () => { - const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); - - render( - -
    Test Content
    -
    - ); - - const config = initSpy.mock.calls[0][0]; - expect(config).toHaveProperty("beforeSend"); - const beforeSend = config.beforeSend; - - if (!beforeSend) { - throw new Error("beforeSend is not defined"); - } - - const dummyEvent = { some: "event" } as unknown as Sentry.ErrorEvent; - - const hintWithNextNotFound = { originalException: { digest: "NEXT_NOT_FOUND" } }; - expect(beforeSend(dummyEvent, hintWithNextNotFound)).toBeNull(); - - const hintWithOtherError = { originalException: { digest: "OTHER_ERROR" } }; - expect(beforeSend(dummyEvent, hintWithOtherError)).toEqual(dummyEvent); - - const hintWithoutError = { originalException: undefined }; - expect(beforeSend(dummyEvent, hintWithoutError)).toEqual(dummyEvent); - }); - - test("processes beforeSend correctly when hint.originalException is not an Error object", () => { - const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); - - render( - -
    Test Content
    -
    - ); - - const config = initSpy.mock.calls[0][0]; - expect(config).toHaveProperty("beforeSend"); - const beforeSend = config.beforeSend; - - if (!beforeSend) { - throw new Error("beforeSend is not defined"); - } - - const dummyEvent = { some: "event" } as unknown as Sentry.ErrorEvent; - - const hintWithString = { originalException: "string exception" }; - expect(() => beforeSend(dummyEvent, hintWithString)).not.toThrow(); - expect(beforeSend(dummyEvent, hintWithString)).toEqual(dummyEvent); - - const hintWithNumber = { originalException: 123 }; - expect(() => beforeSend(dummyEvent, hintWithNumber)).not.toThrow(); - expect(beforeSend(dummyEvent, hintWithNumber)).toEqual(dummyEvent); - - const hintWithNull = { originalException: null }; - expect(() => beforeSend(dummyEvent, hintWithNull)).not.toThrow(); - expect(beforeSend(dummyEvent, hintWithNull)).toEqual(dummyEvent); - }); -}); diff --git a/apps/web/lib/membership/hooks/useMembershipRole.test.tsx b/apps/web/lib/membership/hooks/useMembershipRole.test.tsx deleted file mode 100644 index 0d7d0f0e70..0000000000 --- a/apps/web/lib/membership/hooks/useMembershipRole.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { renderHook, waitFor } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TOrganizationRole } from "@formbricks/types/memberships"; -import { getMembershipByUserIdOrganizationIdAction } from "./actions"; -import { useMembershipRole } from "./useMembershipRole"; - -vi.mock("./actions", () => ({ - getMembershipByUserIdOrganizationIdAction: vi.fn(), -})); - -describe("useMembershipRole", () => { - afterEach(() => { - vi.clearAllMocks(); - }); - - test("should fetch and return membership role", async () => { - const mockRole: TOrganizationRole = "owner"; - vi.mocked(getMembershipByUserIdOrganizationIdAction).mockResolvedValue(mockRole); - - const { result } = renderHook(() => useMembershipRole("env-123", "user-123")); - - expect(result.current.isLoading).toBe(true); - expect(result.current.membershipRole).toBeUndefined(); - expect(result.current.error).toBe(""); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(result.current.membershipRole).toBe(mockRole); - expect(result.current.error).toBe(""); - expect(getMembershipByUserIdOrganizationIdAction).toHaveBeenCalledWith("env-123", "user-123"); - }); - - test("should handle error when fetching membership role fails", async () => { - const errorMessage = "Failed to fetch role"; - vi.mocked(getMembershipByUserIdOrganizationIdAction).mockRejectedValue(new Error(errorMessage)); - - const { result } = renderHook(() => useMembershipRole("env-123", "user-123")); - - expect(result.current.isLoading).toBe(true); - expect(result.current.membershipRole).toBeUndefined(); - expect(result.current.error).toBe(""); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(result.current.membershipRole).toBeUndefined(); - expect(result.current.error).toBe(errorMessage); - expect(getMembershipByUserIdOrganizationIdAction).toHaveBeenCalledWith("env-123", "user-123"); - }); -}); diff --git a/apps/web/modules/account/components/DeleteAccountModal/index.test.tsx b/apps/web/modules/account/components/DeleteAccountModal/index.test.tsx deleted file mode 100644 index 004ff45e37..0000000000 --- a/apps/web/modules/account/components/DeleteAccountModal/index.test.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TUser } from "@formbricks/types/user"; -import * as actions from "./actions"; -import { DeleteAccountModal } from "./index"; - -// Mock constants that this test needs -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - WEBAPP_URL: "http://localhost:3000", -})); - -// Mock server actions that this test needs -vi.mock("@/modules/auth/actions/sign-out", () => ({ - logSignOutAction: vi.fn().mockResolvedValue(undefined), -})); - -// Mock our useSignOut hook -const mockSignOut = vi.fn(); -vi.mock("@/modules/auth/hooks/use-sign-out", () => ({ - useSignOut: () => ({ - signOut: mockSignOut, - }), -})); - -vi.mock("./actions", () => ({ - deleteUserAction: vi.fn(), -})); - -describe("DeleteAccountModal", () => { - const mockUser: TUser = { - email: "test@example.com", - } as TUser; - - const mockOrgs: TOrganization[] = [{ name: "Org1" }, { name: "Org2" }] as TOrganization[]; - - const mockSetOpen = vi.fn(); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders modal with correct props", () => { - render( - - ); - - expect(screen.getByText("Org1")).toBeInTheDocument(); - expect(screen.getByText("Org2")).toBeInTheDocument(); - }); - - test("disables delete button when email does not match", () => { - render( - - ); - - const input = screen.getByRole("textbox"); - fireEvent.change(input, { target: { value: "wrong@example.com" } }); - expect(input).toHaveValue("wrong@example.com"); - }); - - test("allows account deletion flow (non-cloud)", async () => { - const deleteUserAction = vi - .spyOn(actions, "deleteUserAction") - .mockResolvedValue("deleted-user-id" as any); // the return doesn't matter here - - Object.defineProperty(window, "localStorage", { - writable: true, - value: { removeItem: vi.fn() }, - }); - - // Mock window.location.replace - Object.defineProperty(window, "location", { - writable: true, - value: { replace: vi.fn() }, - }); - - render( - - ); - - const input = screen.getByTestId("deleteAccountConfirmation"); - fireEvent.change(input, { target: { value: mockUser.email } }); - - const form = screen.getByTestId("deleteAccountForm"); - fireEvent.submit(form); - - await waitFor(() => { - expect(deleteUserAction).toHaveBeenCalled(); - expect(mockSignOut).toHaveBeenCalledWith({ - reason: "account_deletion", - redirect: false, // Updated to match new implementation - clearEnvironmentId: true, - }); - expect(window.location.replace).toHaveBeenCalledWith("/auth/login"); - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - }); - - test("allows account deletion flow (cloud)", async () => { - const deleteUserAction = vi - .spyOn(actions, "deleteUserAction") - .mockResolvedValue("deleted-user-id" as any); // the return doesn't matter here - - Object.defineProperty(window, "localStorage", { - writable: true, - value: { removeItem: vi.fn() }, - }); - - Object.defineProperty(window, "location", { - writable: true, - value: { replace: vi.fn() }, - }); - - render( - - ); - - const input = screen.getByTestId("deleteAccountConfirmation"); - fireEvent.change(input, { target: { value: mockUser.email } }); - - const form = screen.getByTestId("deleteAccountForm"); - fireEvent.submit(form); - - await waitFor(() => { - expect(deleteUserAction).toHaveBeenCalled(); - expect(mockSignOut).toHaveBeenCalledWith({ - reason: "account_deletion", - redirect: false, // Updated to match new implementation - clearEnvironmentId: true, - }); - expect(window.location.replace).toHaveBeenCalledWith( - "https://app.formbricks.com/s/clri52y3z8f221225wjdhsoo2" - ); - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - }); - - test("handles deletion errors", async () => { - const deleteUserAction = vi.spyOn(actions, "deleteUserAction").mockRejectedValue(new Error("fail")); - - render( - - ); - - const input = screen.getByTestId("deleteAccountConfirmation"); - fireEvent.change(input, { target: { value: mockUser.email } }); - - const form = screen.getByTestId("deleteAccountForm"); - fireEvent.submit(form); - - await waitFor(() => { - expect(deleteUserAction).toHaveBeenCalled(); - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - }); -}); diff --git a/apps/web/modules/analysis/components/RatingSmiley/index.test.tsx b/apps/web/modules/analysis/components/RatingSmiley/index.test.tsx deleted file mode 100644 index cd1830b917..0000000000 --- a/apps/web/modules/analysis/components/RatingSmiley/index.test.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { cleanup, render } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { RatingSmiley } from "./index"; - -// Mock the smiley components from ../SingleResponseCard/components/Smileys -vi.mock("../SingleResponseCard/components/Smileys", () => ({ - TiredFace: (props: any) => ( - - TiredFace - - ), - WearyFace: (props: any) => ( - - WearyFace - - ), - PerseveringFace: (props: any) => ( - - PerseveringFace - - ), - FrowningFace: (props: any) => ( - - FrowningFace - - ), - ConfusedFace: (props: any) => ( - - ConfusedFace - - ), - NeutralFace: (props: any) => ( - - NeutralFace - - ), - SlightlySmilingFace: (props: any) => ( - - SlightlySmilingFace - - ), - SmilingFaceWithSmilingEyes: (props: any) => ( - - SmilingFaceWithSmilingEyes - - ), - GrinningFaceWithSmilingEyes: (props: any) => ( - - GrinningFaceWithSmilingEyes - - ), - GrinningSquintingFace: (props: any) => ( - - GrinningSquintingFace - - ), -})); - -describe("RatingSmiley", () => { - afterEach(() => { - cleanup(); - }); - - const activeClass = "bg-rating-fill"; - - // Test branch: range === 10 => iconsIdx = [0,1,2,...,9] - test("renders correct icon for range 10 when active", () => { - // For idx 0, iconsIdx[0] === 0, which corresponds to TiredFace. - const { getByTestId } = render(); - const icon = getByTestId("tired"); - expect(icon).toBeDefined(); - expect(icon.className).toContain(activeClass); - }); - - test("renders correct icon for range 10 when inactive", () => { - const { getByTestId } = render(); - const icon = getByTestId("tired"); - expect(icon).toBeDefined(); - expect(icon.className).toContain("fill-none"); - }); - - // Test branch: range === 7 => iconsIdx = [1,3,4,5,6,8,9] - test("renders correct icon for range 7 when active", () => { - // For idx 0, iconsIdx[0] === 1, which corresponds to WearyFace. - const { getByTestId } = render(); - const icon = getByTestId("weary"); - expect(icon).toBeDefined(); - expect(icon.className).toContain(activeClass); - }); - - // Test branch: range === 5 => iconsIdx = [3,4,5,6,7] - test("renders correct icon for range 5 when active", () => { - // For idx 0, iconsIdx[0] === 3, which corresponds to FrowningFace. - const { getByTestId } = render(); - const icon = getByTestId("frowning"); - expect(icon).toBeDefined(); - expect(icon.className).toContain(activeClass); - }); - - // Test branch: range === 4 => iconsIdx = [4,5,6,7] - test("renders correct icon for range 4 when active", () => { - // For idx 0, iconsIdx[0] === 4, corresponding to ConfusedFace. - const { getByTestId } = render(); - const icon = getByTestId("confused"); - expect(icon).toBeDefined(); - expect(icon.className).toContain(activeClass); - }); - - // Test branch: range === 3 => iconsIdx = [4,5,7] - test("renders correct icon for range 3 when active", () => { - // For idx 0, iconsIdx[0] === 4, corresponding to ConfusedFace. - const { getByTestId } = render(); - const icon = getByTestId("confused"); - expect(icon).toBeDefined(); - expect(icon.className).toContain(activeClass); - }); -}); diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.test.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.test.tsx deleted file mode 100644 index 4062b25044..0000000000 --- a/apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils"; -import { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types"; -import { getEnabledLanguages } from "@/lib/i18n/utils"; -import { LanguageDropdown } from "./LanguageDropdown"; - -vi.mock("@/lib/i18n/utils", () => ({ - getEnabledLanguages: vi.fn(), -})); - -vi.mock("@formbricks/i18n-utils/src/utils", () => ({ - getLanguageLabel: vi.fn(), -})); - -describe("LanguageDropdown", () => { - const dummySurveyMultiple = { - languages: [ - { language: { code: "en" } } as TSurveyLanguage, - { language: { code: "fr" } } as TSurveyLanguage, - ], - } as TSurvey; - const dummySurveySingle = { - languages: [{ language: { code: "en" } }], - } as TSurvey; - const dummyLocale = "en-US"; - const setLanguageMock = vi.fn(); - - afterEach(() => { - cleanup(); - }); - - test("renders nothing when enabledLanguages length is 1", () => { - vi.mocked(getEnabledLanguages).mockReturnValueOnce([{ language: { code: "en" } } as TSurveyLanguage]); - render( - - ); - // Since enabledLanguages.length === 1, component should render null. - expect(screen.queryByRole("button")).toBeNull(); - }); - - test("renders button and toggles dropdown when multiple languages exist", async () => { - vi.mocked(getEnabledLanguages).mockReturnValue(dummySurveyMultiple.languages); - vi.mocked(getLanguageLabel).mockImplementation((code: string, _locale: string) => code.toUpperCase()); - - render( - - ); - - const button = screen.getByRole("button", { name: "Select Language" }); - expect(button).toBeDefined(); - - await userEvent.click(button); - // Wait for the dropdown options to appear. They are wrapped in a div with no specific role, - // so we query for texts (our mock labels) instead. - const optionEn = await screen.findByText("EN"); - const optionFr = await screen.findByText("FR"); - - expect(optionEn).toBeDefined(); - expect(optionFr).toBeDefined(); - - await userEvent.click(optionFr); - expect(setLanguageMock).toHaveBeenCalledWith("fr"); - - // After clicking, dropdown should no longer be visible. - await waitFor(() => { - expect(screen.queryByText("EN")).toBeNull(); - expect(screen.queryByText("FR")).toBeNull(); - }); - }); - - test("closes dropdown when clicking outside", async () => { - vi.mocked(getEnabledLanguages).mockReturnValue(dummySurveyMultiple.languages); - vi.mocked(getLanguageLabel).mockImplementation((code: string, _locale: string) => code); - - render( - - ); - const button = screen.getByRole("button", { name: "Select Language" }); - await userEvent.click(button); - - // Confirm dropdown shown - expect(await screen.findByText("en")).toBeDefined(); - - // Simulate clicking outside by dispatching a click event on the container's parent. - await userEvent.click(document.body); - - // Wait for dropdown to close - await waitFor(() => { - expect(screen.queryByText("en")).toBeNull(); - }); - }); -}); diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.test.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.test.tsx deleted file mode 100644 index ea6ffc749d..0000000000 --- a/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test } from "vitest"; -import { SurveyLinkDisplay } from "./SurveyLinkDisplay"; - -describe("SurveyLinkDisplay", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the Input when surveyUrl is provided", () => { - const surveyUrl = "http://example.com/s/123"; - render(); - const input = screen.getByTestId("survey-url-input"); - expect(input).toBeInTheDocument(); - }); - - test("renders loading state when surveyUrl is empty", () => { - render(); - const loadingDiv = screen.getByTestId("loading-div"); - expect(loadingDiv).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx deleted file mode 100644 index 4315f31bec..0000000000 --- a/apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { toast } from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { TUserLocale } from "@formbricks/types/user"; -import { ShareSurveyLink } from "@/modules/analysis/components/ShareSurveyLink/index"; -import { getSurveyUrl } from "../../utils"; - -vi.mock("react-hot-toast", () => ({ - toast: { - success: vi.fn(), - }, -})); - -// Mock the useSingleUseId hook -vi.mock("@/modules/survey/hooks/useSingleUseId", () => ({ - useSingleUseId: vi.fn(() => ({ - singleUseId: "test-single-use-id", - refreshSingleUseId: vi.fn().mockResolvedValue("test-single-use-id"), - })), -})); - -// Mock the survey utils -vi.mock("../../utils", () => ({ - getSurveyUrl: vi.fn((survey, publicDomain, language) => { - if (language && language !== "en") { - return `${publicDomain}/s/${survey.id}?lang=${language}`; - } - return `${publicDomain}/s/${survey.id}`; - }), -})); - -const survey: TSurvey = { - id: "survey-id", - name: "Test Survey", - type: "link", - status: "inProgress", - questions: [ - { - id: "question-id", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Question headline" }, - subheader: { default: "Question subheader" }, - required: true, - buttonLabel: { default: "Next" }, - inputType: "text", - charLimit: { enabled: false }, - }, - ], - recontactDays: 1, - autoClose: null, - delay: 0, - displayPercentage: null, - displayLimit: null, - triggers: [], - redirectUrl: null, - numDisplays: 0, - numDisplaysGlobally: 0, - numResponses: 0, - numResponsesGlobally: 0, - createdAt: new Date(), - updatedAt: new Date(), - languages: [ - { - default: true, - enabled: true, - language: { - id: "lang-1", - code: "en", - alias: "English", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "proj-1", - }, - }, - { - default: false, - enabled: true, - language: { - id: "lang-2", - code: "de", - alias: "German", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "proj-1", - }, - }, - ], - styling: null, - variables: [], - welcomeCard: { - enabled: true, - headline: { default: "Welcome!" }, - timeToFinish: false, - showResponseCount: false, - }, - surveyClosedMessage: null, - singleUse: null, - productOverwrites: null, - pin: null, - attributeFilters: [], - autoComplete: null, - hiddenFields: { enabled: true }, - environmentId: "env-id", - endings: [], - displayOption: "displayOnce", - isBackButtonHidden: false, - isSingleResponsePerEmailEnabled: false, - isVerifyEmailEnabled: false, - recaptcha: { enabled: false, threshold: 0.5 }, - segment: null, - showLanguageSwitch: false, - createdBy: "user-id", - followUps: [], -} as unknown as TSurvey; - -const publicDomain = "http://localhost:3000"; -let surveyUrl = `${publicDomain}/s/survey-id`; -const setSurveyUrl = vi.fn((url: string) => { - surveyUrl = url; -}); -const locale: TUserLocale = "en-US"; - -// Mocking dependencies -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -Object.assign(navigator, { - clipboard: { - writeText: vi.fn(), - }, -}); - -global.open = vi.fn(); - -describe("ShareSurveyLink", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - surveyUrl = `${publicDomain}/s/survey-id`; - }); - - test("renders the component with initial values", () => { - render( - - ); - - expect(screen.getByDisplayValue(surveyUrl)).toBeInTheDocument(); - expect(screen.getByText("common.copy")).toBeInTheDocument(); - expect(screen.getByText("common.preview")).toBeInTheDocument(); - }); - - test("copies the survey link to the clipboard when copy button is clicked", () => { - render( - - ); - - const copyButton = screen.getByLabelText("environments.surveys.copy_survey_link_to_clipboard"); - fireEvent.click(copyButton); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith(surveyUrl); - expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard"); - }); - - test("opens the preview link in a new tab when preview button is clicked (no query params)", async () => { - render( - - ); - - const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab"); - fireEvent.click(previewButton); - - // Wait for the async function to complete - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(global.open).toHaveBeenCalledWith(`${surveyUrl}?preview=true`, "_blank"); - }); - - test("opens the preview link in a new tab when preview button is clicked (with query params)", async () => { - const surveyWithParamsUrl = `${publicDomain}/s/survey-id?foo=bar`; - render( - - ); - - const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab"); - fireEvent.click(previewButton); - - // Wait for the async function to complete - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(global.open).toHaveBeenCalledWith(`${surveyWithParamsUrl}&preview=true`, "_blank"); - }); - - test("disables copy and preview buttons when surveyUrl is empty", () => { - render( - - ); - - const copyButton = screen.getByLabelText("environments.surveys.copy_survey_link_to_clipboard"); - const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab"); - - expect(copyButton).toBeDisabled(); - expect(previewButton).toBeDisabled(); - }); - - test("updates the survey URL when the language is changed", () => { - const mockGetSurveyUrl = vi.mocked(getSurveyUrl); - - render( - - ); - - const languageDropdown = screen.getByTitle("Select Language"); - fireEvent.click(languageDropdown); - - const germanOption = screen.getByText("German"); - fireEvent.click(germanOption); - - expect(mockGetSurveyUrl).toHaveBeenCalledWith(survey, publicDomain, "de"); - expect(setSurveyUrl).toHaveBeenCalledWith(`${publicDomain}/s/${survey.id}?lang=de`); - }); -}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.test.tsx deleted file mode 100644 index f0c1bc8689..0000000000 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurveyHiddenFields } from "@formbricks/types/surveys/types"; -import { HiddenFields } from "./HiddenFields"; - -// Mock tooltip components to always render their children -vi.mock("@/modules/ui/components/tooltip", () => ({ - Tooltip: ({ children }: { children: React.ReactNode }) =>
    {children}
    , - TooltipContent: ({ children }: { children: React.ReactNode }) =>
    {children}
    , - TooltipProvider: ({ children }: { children: React.ReactNode }) =>
    {children}
    , - TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
    {children}
    , -})); - -describe("HiddenFields", () => { - afterEach(() => { - cleanup(); - }); - - test("does not render empty container when no fieldIds are provided", () => { - render( - - ); - const container = screen.queryByTestId("main-hidden-fields-div"); - expect(container).not.toBeInTheDocument(); - }); - - test("renders nothing for fieldIds with no corresponding response data", () => { - render( - - ); - expect(screen.queryByText("field1")).toBeNull(); - }); - - test("renders field and value when responseData exists and is a string", async () => { - render( - - ); - expect(screen.getByText("field1")).toBeInTheDocument(); - expect(screen.getByText("Value 1")).toBeInTheDocument(); - expect(screen.queryByText("field2")).toBeNull(); - }); - - test("renders empty text when responseData value is not a string", () => { - render( - - ); - expect(screen.getByText("field1")).toBeInTheDocument(); - const valueParagraphs = screen.getAllByText("", { selector: "p" }); - expect(valueParagraphs.length).toBeGreaterThan(0); - }); - - test("displays tooltip content for hidden field", async () => { - render( - - ); - expect(screen.getByText("common.hidden_field")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.test.tsx deleted file mode 100644 index 895236d261..0000000000 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurveyQuestion } from "@formbricks/types/surveys/types"; -import { parseRecallInfo } from "@/lib/utils/recall"; -import { QuestionSkip } from "./QuestionSkip"; - -vi.mock("@/modules/ui/components/tooltip", () => ({ - Tooltip: ({ children }: any) =>
    {children}
    , - TooltipContent: ({ children }: any) =>
    {children}
    , - TooltipProvider: ({ children }: any) =>
    {children}
    , - TooltipTrigger: ({ children }: any) =>
    {children}
    , -})); - -vi.mock("@/modules/i18n/utils", () => ({ - getLocalizedValue: vi.fn((value, _) => value), -})); - -// Mock recall utils -vi.mock("@/lib/utils/recall", () => ({ - parseRecallInfo: vi.fn((headline, _) => { - return `parsed: ${headline}`; - }), -})); - -const dummyQuestions = [ - { id: "f1", headline: "headline1" }, - { id: "f2", headline: "headline2" }, -] as unknown as TSurveyQuestion[]; - -const dummyResponseData = { f1: "Answer 1", f2: "Answer 2" }; - -describe("QuestionSkip", () => { - afterEach(() => { - cleanup(); - }); - - test("renders nothing when skippedQuestions is falsy", () => { - render( - - ); - expect(screen.queryByText("headline1")).toBeNull(); - expect(screen.queryByText("headline2")).toBeNull(); - }); - - test("renders welcomeCard branch", () => { - render( - - ); - expect(screen.getByText("common.welcome_card")).toBeInTheDocument(); - }); - - test("renders skipped branch with tooltip and parsed headlines", () => { - vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline1"); - vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline2"); - - render( - - ); - // Check tooltip text from TooltipContent - expect(screen.getByTestId("tooltip-respondent_skipped_questions")).toBeInTheDocument(); - // Check mapping: parseRecallInfo should be called on each headline value, so expect the parsed text to appear. - expect(screen.getByText("parsed: headline1")).toBeInTheDocument(); - expect(screen.getByText("parsed: headline2")).toBeInTheDocument(); - }); - - test("renders aborted branch with closed message and parsed headlines", () => { - vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline1"); - vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline2"); - - render( - - ); - expect(screen.getByTestId("tooltip-survey_closed")).toBeInTheDocument(); - expect(screen.getByText("parsed: headline1")).toBeInTheDocument(); - expect(screen.getByText("parsed: headline2")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.test.tsx deleted file mode 100644 index a97421bc71..0000000000 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.test.tsx +++ /dev/null @@ -1,517 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { RenderResponse } from "./RenderResponse"; - -// Mocks for dependencies -vi.mock("@/modules/ui/components/rating-response", () => ({ - RatingResponse: ({ answer }: any) =>
    Rating: {answer}
    , -})); -vi.mock("@/modules/ui/components/file-upload-response", () => ({ - FileUploadResponse: ({ selected }: any) => ( -
    FileUpload: {selected.join(",")}
    - ), -})); -vi.mock("@/modules/ui/components/picture-selection-response", () => ({ - PictureSelectionResponse: ({ selected, isExpanded, showId }: any) => ( -
    - PictureSelection: {selected.join(",")} ({isExpanded ? "expanded" : "collapsed"}) showId:{" "} - {String(showId)} -
    - ), -})); -vi.mock("@/modules/ui/components/array-response", () => ({ - ArrayResponse: ({ value }: any) =>
    {value.join(",")}
    , -})); -vi.mock("@/modules/ui/components/response-badges", () => ({ - ResponseBadges: ({ items, showId }: any) => ( -
    - {Array.isArray(items) - ? items - .map((item) => (typeof item === "object" ? `${item.value}:${item.id || "no-id"}` : item)) - .join(",") - : items}{" "} - showId: {String(showId)} -
    - ), -})); -vi.mock("@/modules/ui/components/ranking-response", () => ({ - RankingResponse: ({ value, showId }: any) => ( -
    - {Array.isArray(value) - ? value - .map((item) => (typeof item === "object" ? `${item.value}:${item.id || "no-id"}` : item)) - .join(",") - : value}{" "} - showId: {String(showId)} -
    - ), -})); -vi.mock("@/modules/analysis/utils", () => ({ - renderHyperlinkedContent: vi.fn((text: string) => "hyper:" + text), -})); -vi.mock("@/lib/responses", () => ({ - processResponseData: (val: any) => "processed:" + val, -})); -vi.mock("@/lib/utils/datetime", () => ({ - formatDateWithOrdinal: (d: Date) => "formatted_" + d.toISOString(), -})); -vi.mock("@/lib/cn", () => ({ - cn: (...classes: (string | boolean | undefined)[]) => classes.filter(Boolean).join(" "), -})); -vi.mock("@/lib/i18n/utils", () => ({ - getLocalizedValue: vi.fn((val, _) => val["default"]), - getLanguageCode: vi.fn().mockReturnValue("default"), -})); - -describe("RenderResponse", () => { - afterEach(() => { - cleanup(); - }); - - const defaultSurvey = { languages: [] } as any; - const defaultQuestion = { - id: "q1", - type: "Unknown", - choices: [ - { id: "choice1", label: { default: "Option 1" } }, - { id: "choice2", label: { default: "Option 2" } }, - ], - } as any; - const dummyLanguage = "default"; - - test("returns '-' for empty responseData (string)", () => { - const { container } = render( - - ); - expect(container.textContent).toBe("-"); - }); - - test("returns '-' for empty responseData (array)", () => { - const { container } = render( - - ); - expect(container.textContent).toBe("-"); - }); - - test("returns '-' for empty responseData (object)", () => { - const { container } = render( - - ); - expect(container.textContent).toBe("-"); - }); - - test("renders RatingResponse for 'Rating' question with number", () => { - const question = { ...defaultQuestion, type: "rating", scale: 5, range: [1, 5] }; - render( - - ); - expect(screen.getByTestId("RatingResponse")).toHaveTextContent("Rating: 4"); - }); - - test("renders formatted date for 'Date' question", () => { - const question = { ...defaultQuestion, type: "date" }; - const dateStr = new Date("2023-01-01T12:00:00Z").toISOString(); - render( - - ); - expect(screen.getByText(/formatted_/)).toBeInTheDocument(); - }); - - test("renders PictureSelectionResponse for 'PictureSelection' question", () => { - const question = { ...defaultQuestion, type: "pictureSelection", choices: ["a", "b"] }; - render( - - ); - expect(screen.getByTestId("PictureSelectionResponse")).toHaveTextContent( - "PictureSelection: choice1,choice2" - ); - }); - - test("renders FileUploadResponse for 'FileUpload' question", () => { - const question = { ...defaultQuestion, type: "fileUpload" }; - render( - - ); - expect(screen.getByTestId("FileUploadResponse")).toHaveTextContent("FileUpload: file1,file2"); - }); - - test("renders Matrix response", () => { - const question = { - id: "q1", - type: "matrix", - rows: [ - { id: "row1", label: { default: "row1" } }, - { id: "row2", label: { default: "row2" } }, - ], - columns: [ - { id: "col1", label: { default: "answer1" } }, - { id: "col2", label: { default: "answer2" } }, - ], - } as any; - // getLocalizedValue returns the row value itself - const responseData = { row1: "answer1", row2: "answer2" }; - render( - - ); - expect(screen.getByText("row1:processed:answer1")).toBeInTheDocument(); - expect(screen.getByText("row2:processed:answer2")).toBeInTheDocument(); - }); - - test("renders ArrayResponse for 'Address' question", () => { - const question = { ...defaultQuestion, type: "address" }; - render( - - ); - expect(screen.getByTestId("ArrayResponse")).toHaveTextContent("addr1,addr2"); - }); - - test("renders ResponseBadges for 'Cal' question (string)", () => { - const question = { ...defaultQuestion, type: "cal" }; - render( - - ); - expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("value"); - }); - - test("renders ResponseBadges for 'Consent' question (number)", () => { - const question = { ...defaultQuestion, type: "consent" }; - render( - - ); - expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("5"); - }); - - test("renders ResponseBadges for 'CTA' question (string)", () => { - const question = { ...defaultQuestion, type: "cta" }; - render( - - ); - expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("click"); - }); - - test("renders ResponseBadges for 'MultipleChoiceSingle' question (string)", () => { - const question = { ...defaultQuestion, type: "multipleChoiceSingle", choices: [] }; - render( - - ); - expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("option1"); - }); - - test("renders ResponseBadges for 'MultipleChoiceMulti' question (array)", () => { - const question = { ...defaultQuestion, type: "multipleChoiceMulti", choices: [] }; - render( - - ); - expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("opt1:other,opt2:other"); - }); - - test("renders ResponseBadges for 'NPS' question (number)", () => { - const question = { ...defaultQuestion, type: "nps" }; - render( - - ); - // NPS questions render as simple text, not ResponseBadges - expect(screen.getByText("9")).toBeInTheDocument(); - }); - - test("renders RankingResponse for 'Ranking' question", () => { - const question = { ...defaultQuestion, type: "ranking", choices: [] }; - render( - - ); - expect(screen.getByTestId("RankingResponse")).toHaveTextContent("first:other,second:other showId: false"); - }); - - test("renders default branch for unknown question type with string", () => { - const question = { ...defaultQuestion, type: "unknown" }; - render( - - ); - expect(screen.getByText("hyper:some text")).toBeInTheDocument(); - }); - - test("renders default branch for unknown question type with array", () => { - const question = { ...defaultQuestion, type: "unknown" }; - render( - - ); - expect(screen.getByText("a, b")).toBeInTheDocument(); - }); - - // New tests for showId functionality - test("passes showId prop to PictureSelectionResponse", () => { - const question = { - ...defaultQuestion, - type: "pictureSelection", - choices: [{ id: "choice1", imageUrl: "url1" }], - }; - render( - - ); - const component = screen.getByTestId("PictureSelectionResponse"); - expect(component).toHaveAttribute("data-show-id", "true"); - expect(component).toHaveTextContent("showId: true"); - }); - - test("passes showId prop to RankingResponse with choice ID extraction", () => { - const question = { - ...defaultQuestion, - type: "ranking", - choices: [ - { id: "choice1", label: { default: "Option 1" } }, - { id: "choice2", label: { default: "Option 2" } }, - ], - }; - render( - - ); - const component = screen.getByTestId("RankingResponse"); - expect(component).toHaveAttribute("data-show-id", "true"); - expect(component).toHaveTextContent("showId: true"); - // Should extract choice IDs and pass them as value objects - expect(component).toHaveTextContent("Option 1:choice1,Option 2:choice2"); - }); - - test("handles ranking response with missing choice IDs", () => { - const question = { - ...defaultQuestion, - type: "ranking", - choices: [ - { id: "choice1", label: { default: "Option 1" } }, - { id: "choice2", label: { default: "Option 2" } }, - ], - }; - render( - - ); - const component = screen.getByTestId("RankingResponse"); - expect(component).toHaveTextContent("Option 1:choice1,Unknown Option:other"); - }); - - test("passes showId prop to ResponseBadges for multiple choice single", () => { - const question = { - ...defaultQuestion, - type: "multipleChoiceSingle", - choices: [{ id: "choice1", label: { default: "Option 1" } }], - }; - render( - - ); - const component = screen.getByTestId("ResponseBadges"); - expect(component).toHaveAttribute("data-show-id", "true"); - expect(component).toHaveTextContent("showId: true"); - expect(component).toHaveTextContent("Option 1:choice1"); - }); - - test("passes showId prop to ResponseBadges for multiple choice multi", () => { - const question = { - ...defaultQuestion, - type: "multipleChoiceMulti", - choices: [ - { id: "choice1", label: { default: "Option 1" } }, - { id: "choice2", label: { default: "Option 2" } }, - ], - }; - render( - - ); - const component = screen.getByTestId("ResponseBadges"); - expect(component).toHaveAttribute("data-show-id", "true"); - expect(component).toHaveTextContent("showId: true"); - expect(component).toHaveTextContent("Option 1:choice1,Option 2:choice2"); - }); - - test("handles multiple choice with missing choice IDs", () => { - const question = { - ...defaultQuestion, - type: "multipleChoiceMulti", - choices: [{ id: "choice1", label: { default: "Option 1" } }], - }; - render( - - ); - const component = screen.getByTestId("ResponseBadges"); - expect(component).toHaveTextContent("Option 1:choice1,Unknown Option:other"); - }); - - test("passes showId=false to components when showId is false", () => { - const question = { - ...defaultQuestion, - type: "multipleChoiceMulti", - choices: [{ id: "choice1", label: { default: "Option 1" } }], - }; - render( - - ); - const component = screen.getByTestId("ResponseBadges"); - expect(component).toHaveAttribute("data-show-id", "false"); - expect(component).toHaveTextContent("showId: false"); - // Should still extract IDs but showId=false - expect(component).toHaveTextContent("Option 1:choice1"); - }); - - test("handles questions without choices property", () => { - const question = { ...defaultQuestion, type: "multipleChoiceSingle" }; // No choices property - render( - - ); - const component = screen.getByTestId("ResponseBadges"); - expect(component).toHaveTextContent("Option 1:choice1"); - }); -}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.test.tsx deleted file mode 100644 index 87f46de3af..0000000000 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.test.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TTag } from "@formbricks/types/tags"; -import { TagError } from "@/modules/projects/settings/types/tag"; -import { createTagAction, createTagToResponseAction, deleteTagOnResponseAction } from "../actions"; -import { ResponseTagsWrapper } from "./ResponseTagsWrapper"; - -const dummyTags = [ - { tagId: "tag1", tagName: "Tag One" }, - { tagId: "tag2", tagName: "Tag Two" }, -]; -const dummyEnvironmentId = "env1"; -const dummyResponseId = "resp1"; -const dummyEnvironmentTags = [ - { id: "tag1", name: "Tag One" }, - { id: "tag2", name: "Tag Two" }, - { id: "tag3", name: "Tag Three" }, -] as TTag[]; -const dummyUpdateFetchedResponses = vi.fn(); -const dummyRouterPush = vi.fn(); - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - push: dummyRouterPush, - }), -})); - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn((res) => res.error?.details[0].issue || "error"), -})); - -vi.mock("../actions", () => ({ - createTagAction: vi.fn(), - createTagToResponseAction: vi.fn(), - deleteTagOnResponseAction: vi.fn(), -})); - -// Mock Button, Tag and TagsCombobox components -vi.mock("@/modules/ui/components/button", () => ({ - Button: (props: any) => , -})); -vi.mock("@/modules/ui/components/tag", () => ({ - Tag: (props: any) => ( -
    - {props.tagName} - {props.allowDelete && } -
    - ), -})); -vi.mock("@/modules/ui/components/tags-combobox", () => ({ - TagsCombobox: (props: any) => ( -
    - - -
    - ), -})); - -describe("ResponseTagsWrapper", () => { - afterEach(() => { - cleanup(); - }); - - test("renders settings button when not readOnly and navigates on click", async () => { - render( - - ); - const settingsButton = screen.getByRole("button", { name: "" }); - await userEvent.click(settingsButton); - expect(dummyRouterPush).toHaveBeenCalledWith(`/environments/${dummyEnvironmentId}/project/tags`); - }); - - test("does not render settings button when readOnly", () => { - render( - - ); - expect(screen.queryByRole("button")).toBeNull(); - }); - - test("renders provided tags", () => { - render( - - ); - expect(screen.getAllByTestId("tag").length).toBe(2); - expect(screen.getByText("Tag One")).toBeInTheDocument(); - expect(screen.getByText("Tag Two")).toBeInTheDocument(); - }); - - test("calls deleteTagOnResponseAction on tag delete success", async () => { - vi.mocked(deleteTagOnResponseAction).mockResolvedValueOnce({ data: "deleted" } as any); - render( - - ); - const deleteButtons = screen.getAllByText("Delete"); - await userEvent.click(deleteButtons[0]); - await waitFor(() => { - expect(deleteTagOnResponseAction).toHaveBeenCalledWith({ responseId: dummyResponseId, tagId: "tag1" }); - expect(dummyUpdateFetchedResponses).toHaveBeenCalled(); - }); - }); - - test("shows toast error on deleteTagOnResponseAction error", async () => { - vi.mocked(deleteTagOnResponseAction).mockRejectedValueOnce(new Error("delete error")); - render( - - ); - const deleteButtons = screen.getAllByText("Delete"); - await userEvent.click(deleteButtons[0]); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith( - "environments.surveys.responses.an_error_occurred_deleting_the_tag" - ); - }); - }); - - test("creates a new tag via TagsCombobox and calls updateFetchedResponses on success", async () => { - vi.mocked(createTagAction).mockResolvedValueOnce({ - data: { ok: true, data: { id: "newTagId", name: "NewTag" } }, - } as any); - vi.mocked(createTagToResponseAction).mockResolvedValueOnce({ data: "tagAdded" } as any); - render( - - ); - const createButton = screen.getByTestId("tags-combobox").querySelector("button"); - await userEvent.click(createButton!); - await waitFor(() => { - expect(createTagAction).toHaveBeenCalledWith({ environmentId: dummyEnvironmentId, tagName: "NewTag" }); - expect(createTagToResponseAction).toHaveBeenCalledWith({ - responseId: dummyResponseId, - tagId: "newTagId", - }); - expect(dummyUpdateFetchedResponses).toHaveBeenCalled(); - }); - }); - - test("handles createTagAction failure and shows toast error", async () => { - vi.mocked(createTagAction).mockResolvedValueOnce({ - data: { - ok: false, - error: { message: "Unique constraint failed on the fields", code: TagError.TAG_NAME_ALREADY_EXISTS }, - }, - } as any); - render( - - ); - const createButton = screen.getByTestId("tags-combobox").querySelector("button"); - await userEvent.click(createButton!); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("environments.surveys.responses.tag_already_exists", { - duration: 2000, - icon: expect.anything(), - }); - }); - }); - - test("calls addTag correctly via TagsCombobox", async () => { - vi.mocked(createTagToResponseAction).mockResolvedValueOnce({ data: "tagAdded" } as any); - render( - - ); - const addButton = screen.getByTestId("tags-combobox").querySelectorAll("button")[1]; - await userEvent.click(addButton); - await waitFor(() => { - expect(createTagToResponseAction).toHaveBeenCalledWith({ responseId: dummyResponseId, tagId: "tag3" }); - expect(dummyUpdateFetchedResponses).toHaveBeenCalled(); - }); - }); - - test("clears tagIdToHighlight after timeout", async () => { - vi.useFakeTimers(); - - render( - - ); - // We simulate that tagIdToHighlight is set (simulate via setState if possible) - // Here we directly invoke the effect by accessing component instance is not trivial in RTL; - // Instead, we manually advance timers to ensure cleanup timeout is executed. - - await act(async () => { - vi.advanceTimersByTime(2000); - }); - - // No error expected; test passes if timer runs without issue. - expect(true).toBe(true); - }); -}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseVariables.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseVariables.test.tsx deleted file mode 100644 index 94a7a36e2c..0000000000 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseVariables.test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TResponseVariables } from "@formbricks/types/responses"; -import { TSurveyVariables } from "@formbricks/types/surveys/types"; -import { ResponseVariables } from "./ResponseVariables"; - -const dummyVariables = [ - { id: "v1", name: "Variable One", type: "number" }, - { id: "v2", name: "Variable Two", type: "string" }, - { id: "v3", name: "Variable Three", type: "object" }, -] as unknown as TSurveyVariables; - -const dummyVariablesData = { - v1: 123, - v2: "abc", - v3: { not: "valid" }, -} as unknown as TResponseVariables; - -// Mock tooltip components -vi.mock("@/modules/ui/components/tooltip", () => ({ - Tooltip: ({ children }: any) =>
    {children}
    , - TooltipContent: ({ children }: any) =>
    {children}
    , - TooltipProvider: ({ children }: any) =>
    {children}
    , - TooltipTrigger: ({ children }: any) =>
    {children}
    , -})); - -// Mock useTranslate -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ t: (key: string) => key }), -})); - -// Mock i18n utils -vi.mock("@/modules/i18n/utils", () => ({ - getLocalizedValue: vi.fn((val, _) => val), - getLanguageCode: vi.fn().mockReturnValue("default"), -})); - -// Mock lucide-react icons to render identifiable elements -vi.mock("lucide-react", () => ({ - FileDigitIcon: () =>
    , - FileType2Icon: () =>
    , -})); - -describe("ResponseVariables", () => { - afterEach(() => { - cleanup(); - }); - - test("renders nothing when no variable in variablesData meets type check", () => { - render( - - ); - expect(screen.queryByText("Variable One")).toBeNull(); - expect(screen.queryByText("Variable Two")).toBeNull(); - }); - - test("renders variables with valid response data", () => { - render(); - expect(screen.getByText("Variable One")).toBeInTheDocument(); - expect(screen.getByText("Variable Two")).toBeInTheDocument(); - // Check that the value is rendered - expect(screen.getByText("123")).toBeInTheDocument(); - expect(screen.getByText("abc")).toBeInTheDocument(); - }); - - test("renders FileDigitIcon for number type and FileType2Icon for string type", () => { - render(); - expect(screen.getByTestId("FileDigitIcon")).toBeInTheDocument(); - expect(screen.getByTestId("FileType2Icon")).toBeInTheDocument(); - }); - - test("displays tooltip content with 'common.variable'", () => { - render(); - // TooltipContent mock always renders its children directly. - expect(screen.getAllByText("common.variable")[0]).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.test.tsx deleted file mode 100644 index 866619c9ca..0000000000 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.test.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TResponse } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { SingleResponseCardBody } from "./SingleResponseCardBody"; - -// Mocks for imported components to return identifiable elements -vi.mock("./QuestionSkip", () => ({ - QuestionSkip: (props: any) =>
    {props.status}
    , -})); -vi.mock("./RenderResponse", () => ({ - RenderResponse: (props: any) =>
    {props.responseData.toString()}
    , -})); -vi.mock("./ResponseVariables", () => ({ - ResponseVariables: (props: any) =>
    Variables
    , -})); -vi.mock("./HiddenFields", () => ({ - HiddenFields: (props: any) =>
    Hidden
    , -})); -vi.mock("./VerifiedEmail", () => ({ - VerifiedEmail: (props: any) =>
    VerifiedEmail
    , -})); - -// Mocks for utility functions used inside component -vi.mock("@/lib/utils/recall", () => ({ - parseRecallInfo: vi.fn((headline, data) => "parsed:" + headline), -})); -vi.mock("@/lib/i18n/utils", () => ({ - getLocalizedValue: vi.fn((headline) => headline), -})); -vi.mock("../util", () => ({ - isValidValue: (val: any) => { - if (typeof val === "string") return val.trim() !== ""; - if (Array.isArray(val)) return val.length > 0; - if (typeof val === "number") return true; - if (typeof val === "object") return Object.keys(val).length > 0; - return false; - }, -})); -// Mock CheckCircle2Icon from lucide-react -vi.mock("lucide-react", () => ({ - CheckCircle2Icon: () =>
    CheckCircle
    , -})); - -describe("SingleResponseCardBody", () => { - afterEach(() => { - cleanup(); - }); - - const dummySurvey = { - welcomeCard: { enabled: true }, - isVerifyEmailEnabled: true, - questions: [ - { id: "q1", headline: "headline1" }, - { id: "q2", headline: "headline2" }, - ], - variables: [{ id: "var1", name: "Variable1", type: "string" }], - hiddenFields: { enabled: true, fieldIds: ["hf1"] }, - } as unknown as TSurvey; - const dummyResponse = { - id: "resp1", - finished: true, - data: { q1: "answer1", q2: "", verifiedEmail: true, hf1: "hiddenVal" }, - variables: { var1: "varValue" }, - language: "en", - } as unknown as TResponse; - - test("renders welcomeCard branch when enabled", () => { - render(); - expect(screen.getAllByTestId("QuestionSkip")[0]).toHaveTextContent("welcomeCard"); - }); - - test("renders VerifiedEmail when enabled and response verified", () => { - render(); - expect(screen.getByTestId("VerifiedEmail")).toBeInTheDocument(); - }); - - test("renders RenderResponse for valid answer", () => { - const surveyCopy = { ...dummySurvey, welcomeCard: { enabled: false } } as TSurvey; - const responseCopy = { ...dummyResponse, data: { q1: "answer1", q2: "" } }; - render(); - // For question q1 answer is valid so RenderResponse is rendered - expect(screen.getByTestId("RenderResponse")).toHaveTextContent("answer1"); - }); - - test("renders QuestionSkip for invalid answer", () => { - const surveyCopy = { ...dummySurvey, welcomeCard: { enabled: false } } as TSurvey; - const responseCopy = { ...dummyResponse, data: { q1: "", q2: "" } }; - render( - - ); - // Renders QuestionSkip for q1 or q2 branch - expect(screen.getAllByTestId("QuestionSkip")[1]).toBeInTheDocument(); - }); - - test("renders ResponseVariables when variables exist", () => { - render(); - expect(screen.getByTestId("ResponseVariables")).toBeInTheDocument(); - }); - - test("renders HiddenFields when hiddenFields enabled", () => { - render(); - expect(screen.getByTestId("HiddenFields")).toBeInTheDocument(); - }); - - test("renders completion indicator when response finished", () => { - render(); - expect(screen.getByTestId("CheckCircle2Icon")).toBeInTheDocument(); - expect(screen.getByText("common.completed")).toBeInTheDocument(); - }); - - test("processes question mapping correctly with skippedQuestions modification", () => { - // Provide one question valid and one not valid, with skippedQuestions for the invalid one. - const surveyCopy = { ...dummySurvey, welcomeCard: { enabled: false } } as TSurvey; - const responseCopy = { ...dummyResponse, data: { q1: "answer1", q2: "" } }; - // Initially, skippedQuestions contains ["q2"]. - render( - - ); - // For q1, RenderResponse is rendered since answer valid. - expect(screen.getByTestId("RenderResponse")).toBeInTheDocument(); - // For q2, QuestionSkip is rendered. Our mock for QuestionSkip returns text "skipped". - expect(screen.getByText("skipped")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.test.tsx deleted file mode 100644 index d404befdfa..0000000000 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TResponse } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TUser } from "@formbricks/types/user"; -import { isSubmissionTimeMoreThan5Minutes } from "@/modules/analysis/components/SingleResponseCard/util"; -import { SingleResponseCardHeader } from "./SingleResponseCardHeader"; - -// Mocks -vi.mock("@/modules/ui/components/avatars", () => ({ - PersonAvatar: ({ personId }: any) =>
    Avatar: {personId}
    , -})); -vi.mock("@/modules/ui/components/survey-status-indicator", () => ({ - SurveyStatusIndicator: ({ status }: any) =>
    Status: {status}
    , -})); -vi.mock("@/modules/ui/components/tooltip", () => ({ - Tooltip: ({ children }: any) =>
    {children}
    , - TooltipContent: ({ children }: any) =>
    {children}
    , - TooltipProvider: ({ children }: any) =>
    {children}
    , - TooltipTrigger: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@formbricks/i18n-utils/src/utils", () => ({ - getLanguageLabel: vi.fn(), -})); -vi.mock("@/modules/lib/time", () => ({ - timeSince: vi.fn(() => "5 minutes ago"), -})); -vi.mock("@/modules/lib/utils/contact", () => ({ - getContactIdentifier: vi.fn((contact, attributes) => attributes?.email || contact?.userId || ""), -})); -vi.mock("../util", () => ({ - isSubmissionTimeMoreThan5Minutes: vi.fn(), -})); - -describe("SingleResponseCardHeader", () => { - afterEach(() => { - cleanup(); - }); - - const dummySurvey = { - id: "survey1", - name: "Test Survey", - environmentId: "env1", - } as TSurvey; - const dummyResponse = { - id: "resp1", - finished: false, - updatedAt: new Date("2023-01-01T12:00:00Z"), - createdAt: new Date("2023-01-01T11:00:00Z"), - language: "en", - contact: { id: "contact1", name: "Alice" }, - contactAttributes: { attr: "value" }, - meta: { - userAgent: { browser: "Chrome", os: "Windows", device: "PC" }, - url: "http://example.com", - action: "click", - source: "web", - country: "USA", - }, - singleUseId: "su123", - } as unknown as TResponse; - const dummyEnvironment = { id: "env1" } as TEnvironment; - const dummyUser = { id: "user1", email: "user1@example.com" } as TUser; - const dummyLocale = "en-US"; - - test("renders response view with contact (user exists)", () => { - vi.mocked(isSubmissionTimeMoreThan5Minutes).mockReturnValue(true); - render( - - ); - // Expect Link wrapping PersonAvatar and display identifier - expect(screen.getByTestId("PersonAvatar")).toHaveTextContent("Avatar: contact1"); - expect(screen.getByRole("link")).toBeInTheDocument(); - }); - - test("renders response view with no contact (anonymous)", () => { - const responseNoContact = { ...dummyResponse, contact: null }; - render( - - ); - expect(screen.getByText("common.anonymous")).toBeInTheDocument(); - }); - - test("renders people view", () => { - render( - - ); - expect(screen.getByRole("link")).toBeInTheDocument(); - expect(screen.getByText("Test Survey")).toBeInTheDocument(); - expect(screen.getByTestId("SurveyStatusIndicator")).toBeInTheDocument(); - }); - - test("renders enabled trash icon and handles click", async () => { - vi.mocked(isSubmissionTimeMoreThan5Minutes).mockReturnValue(true); - const setDeleteDialogOpen = vi.fn(); - render( - - ); - const trashIcon = screen.getByLabelText("Delete response"); - await userEvent.click(trashIcon); - expect(setDeleteDialogOpen).toHaveBeenCalledWith(true); - }); - - test("renders disabled trash icon when deletion not allowed", async () => { - vi.mocked(isSubmissionTimeMoreThan5Minutes).mockReturnValue(false); - render( - - ); - const disabledTrash = screen.getByLabelText("Cannot delete response in progress"); - expect(disabledTrash).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/VerifiedEmail.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/VerifiedEmail.test.tsx deleted file mode 100644 index 092d802139..0000000000 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/VerifiedEmail.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { VerifiedEmail } from "./VerifiedEmail"; - -vi.mock("lucide-react", () => ({ - MailIcon: (props: any) => ( -
    - MailIcon -
    - ), -})); - -describe("VerifiedEmail", () => { - afterEach(() => { - cleanup(); - }); - - test("renders verified email text and value when provided", () => { - render(); - expect(screen.getByText("common.verified_email")).toBeInTheDocument(); - expect(screen.getByText("test@example.com")).toBeInTheDocument(); - expect(screen.getByTestId("MailIcon")).toBeInTheDocument(); - }); - - test("renders empty value when verifiedEmail is not a string", () => { - render(); - expect(screen.getByText("common.verified_email")).toBeInTheDocument(); - const emptyParagraph = screen.getByText("", { selector: "p.ph-no-capture" }); - expect(emptyParagraph.textContent).toBe(""); - }); -}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/index.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/index.test.tsx deleted file mode 100644 index 564c9c22d4..0000000000 --- a/apps/web/modules/analysis/components/SingleResponseCard/index.test.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TResponse } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TUser } from "@formbricks/types/user"; -import { deleteResponseAction, getResponseAction } from "./actions"; -import { SingleResponseCard } from "./index"; - -// Dummy data for props -const dummySurvey = { - id: "survey1", - environmentId: "env1", - name: "Test Survey", - status: "completed", - type: "link", - questions: [{ id: "q1" }, { id: "q2" }], - responseCount: 10, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), -} as unknown as TSurvey; -const dummyResponse = { - id: "resp1", - finished: true, - data: { q1: "answer1", q2: null }, - tags: [], -} as unknown as TResponse; -const dummyEnvironment = { id: "env1" } as TEnvironment; -const dummyUser = { id: "user1", email: "user1@example.com", name: "User One" } as TUser; -const dummyLocale = "en-US"; - -const dummyUpdateResponseList = vi.fn(); -const dummyUpdateResponse = vi.fn(); -const dummySetSelectedResponseId = vi.fn(); - -// Mock internal components to return identifiable elements -vi.mock("./components/SingleResponseCardHeader", () => ({ - SingleResponseCardHeader: (props: any) => ( -
    - -
    - ), -})); -vi.mock("./components/SingleResponseCardBody", () => ({ - SingleResponseCardBody: () =>
    Body Content
    , -})); -vi.mock("./components/ResponseTagsWrapper", () => ({ - ResponseTagsWrapper: (props: any) => ( -
    - -
    - ), -})); -vi.mock("@/modules/ui/components/delete-dialog", () => ({ - DeleteDialog: ({ open, onDelete }: any) => - open ? ( - - ) : null, -})); -vi.mock("./actions", () => ({ - deleteResponseAction: vi.fn().mockResolvedValue("deletedResponse"), - getResponseAction: vi.fn(), -})); - -vi.mock("./util", () => ({ - isValidValue: (value: any) => value !== null && value !== undefined, -})); - -describe("SingleResponseCard", () => { - afterEach(() => { - cleanup(); - }); - - test("renders as a plain div when survey is draft and isReadOnly", () => { - const draftSurvey = { ...dummySurvey, status: "draft" } as TSurvey; - render( - - ); - - expect(screen.getByTestId("SingleResponseCardHeader")).toBeInTheDocument(); - expect(screen.queryByRole("link")).toBeNull(); - }); - - test("calls deleteResponseAction and refreshes router on successful deletion", async () => { - render( - - ); - - userEvent.click(screen.getByText("Open Delete")); - - const deleteButton = await screen.findByTestId("DeleteDialog"); - await userEvent.click(deleteButton); - await waitFor(() => { - expect(deleteResponseAction).toHaveBeenCalledWith({ - responseId: dummyResponse.id, - decrementQuotas: false, - }); - }); - - expect(dummyUpdateResponseList).toHaveBeenCalledWith([dummyResponse.id]); - }); - - test("calls toast.error when deleteResponseAction throws error", async () => { - vi.mocked(deleteResponseAction).mockRejectedValueOnce(new Error("Delete failed")); - render( - - ); - await userEvent.click(screen.getByText("Open Delete")); - const deleteButton = await screen.findByTestId("DeleteDialog"); - await userEvent.click(deleteButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Delete failed"); - }); - }); - - test("calls updateResponse when getResponseAction returns updated response", async () => { - vi.mocked(getResponseAction).mockResolvedValueOnce({ data: { updated: true } as any }); - render( - - ); - - expect(screen.getByTestId("ResponseTagsWrapper")).toBeInTheDocument(); - - await userEvent.click(screen.getByText("Update Responses")); - - await waitFor(() => { - expect(getResponseAction).toHaveBeenCalledWith({ responseId: dummyResponse.id }); - }); - - await waitFor(() => { - expect(dummyUpdateResponse).toHaveBeenCalledWith(dummyResponse.id, { updated: true }); - }); - }); -}); diff --git a/apps/web/modules/analysis/utils.test.tsx b/apps/web/modules/analysis/utils.test.tsx deleted file mode 100644 index 424ef6f461..0000000000 --- a/apps/web/modules/analysis/utils.test.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { cleanup } from "@testing-library/react"; -import { isValidElement } from "react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { renderHyperlinkedContent } from "./utils"; - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(), -})); - -vi.mock("@/modules/survey/list/actions", () => ({ - generateSingleUseIdAction: vi.fn(), -})); - -describe("renderHyperlinkedContent", () => { - afterEach(() => { - cleanup(); - }); - - test("returns a single span element when input has no url", () => { - const input = "Hello world"; - const elements = renderHyperlinkedContent(input); - expect(elements).toHaveLength(1); - const element = elements[0]; - expect(isValidElement(element)).toBe(true); - // element.type should be "span" - expect(element.type).toBe("span"); - expect(element.props.children).toEqual("Hello world"); - }); - - test("splits input with a valid url into span, anchor, span", () => { - const input = "Visit https://example.com for info"; - const elements = renderHyperlinkedContent(input); - // Expect three elements: before text, URL link, after text. - expect(elements).toHaveLength(3); - // First element should be span with "Visit " - expect(elements[0].type).toBe("span"); - expect(elements[0].props.children).toEqual("Visit "); - // Second element should be an anchor with the URL. - expect(elements[1].type).toBe("a"); - expect(elements[1].props.href).toEqual("https://example.com"); - expect(elements[1].props.className).toContain("text-blue-500"); - // Third element: span with " for info" - expect(elements[2].type).toBe("span"); - expect(elements[2].props.children).toEqual(" for info"); - }); - - test("handles multiple valid urls in the input", () => { - const input = "Link1: https://example.com and Link2: https://vitejs.dev"; - const elements = renderHyperlinkedContent(input); - // Expected parts: "Link1: ", "https://example.com", " and Link2: ", "https://vitejs.dev", "" - expect(elements).toHaveLength(5); - expect(elements[1].type).toBe("a"); - expect(elements[1].props.href).toEqual("https://example.com"); - expect(elements[3].type).toBe("a"); - expect(elements[3].props.href).toEqual("https://vitejs.dev"); - }); - - test("renders a span instead of anchor when URL constructor throws", () => { - // Force global.URL to throw for this test. - const originalURL = global.URL; - vi.spyOn(global, "URL").mockImplementation(() => { - throw new Error("Invalid URL"); - }); - const input = "Visit https://broken-url.com now"; - const elements = renderHyperlinkedContent(input); - // Expect the URL not to be rendered as anchor because isValidUrl returns false - // The split will still occur, but the element corresponding to the URL should be a span. - expect(elements).toHaveLength(3); - // Check the element that would have been an anchor is now a span. - expect(elements[1].type).toBe("span"); - expect(elements[1].props.children).toEqual("https://broken-url.com"); - // Restore original URL - global.URL = originalURL; - }); -}); diff --git a/apps/web/modules/auth/components/back-to-login-button.test.tsx b/apps/web/modules/auth/components/back-to-login-button.test.tsx deleted file mode 100644 index 91f02e34e9..0000000000 --- a/apps/web/modules/auth/components/back-to-login-button.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { TFnType } from "@tolgee/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getTranslate } from "@/tolgee/server"; -import { BackToLoginButton } from "./back-to-login-button"; - -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(), -})); - -vi.mock("next/link", () => ({ - default: ({ children, href }: { children: React.ReactNode; href: string }) => {children}, -})); - -describe("BackToLoginButton", () => { - afterEach(() => { - cleanup(); - }); - - test("renders login button with correct link and translation", async () => { - const mockTranslate = vi.mocked(getTranslate); - const mockT: TFnType = (key) => { - if (key === "auth.signup.log_in") return "Back to Login"; - return key; - }; - mockTranslate.mockResolvedValue(mockT); - - render(await BackToLoginButton()); - - const link = screen.getByRole("link", { name: "Back to Login" }); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute("href", "/auth/login"); - }); -}); diff --git a/apps/web/modules/auth/components/form-wrapper.test.tsx b/apps/web/modules/auth/components/form-wrapper.test.tsx deleted file mode 100644 index d1373819b2..0000000000 --- a/apps/web/modules/auth/components/form-wrapper.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { FormWrapper } from "./form-wrapper"; - -vi.mock("@/modules/ui/components/logo", () => ({ - Logo: () =>
    Logo
    , -})); - -vi.mock("next/link", () => ({ - default: ({ - children, - href, - target, - rel, - }: { - children: React.ReactNode; - href: string; - target?: string; - rel?: string; - }) => ( - - {children} - - ), -})); - -describe("FormWrapper", () => { - afterEach(() => { - cleanup(); - }); - - test("renders logo and children content", () => { - render( - -
    Test Content
    -
    - ); - - // Check if logo is rendered - const logo = screen.getByTestId("mock-logo"); - expect(logo).toBeInTheDocument(); - - // Check if logo link has correct attributes - const logoLink = screen.getByTestId("mock-link"); - expect(logoLink).toHaveAttribute("href", "https://formbricks.com?utm_source=ce"); - expect(logoLink).toHaveAttribute("target", "_blank"); - expect(logoLink).toHaveAttribute("rel", "noopener noreferrer"); - - // Check if children content is rendered - const content = screen.getByTestId("test-content"); - expect(content).toBeInTheDocument(); - expect(content).toHaveTextContent("Test Content"); - }); -}); diff --git a/apps/web/modules/auth/email-change-without-verification-success/page.test.tsx b/apps/web/modules/auth/email-change-without-verification-success/page.test.tsx deleted file mode 100644 index 98772b5cc1..0000000000 --- a/apps/web/modules/auth/email-change-without-verification-success/page.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { getServerSession } from "next-auth"; -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { EmailChangeWithoutVerificationSuccessPage } from "./page"; - -// Mock the necessary dependencies -vi.mock("@/modules/auth/components/back-to-login-button", () => ({ - BackToLoginButton: () =>
    Back to Login
    , -})); - -vi.mock("@/modules/auth/components/form-wrapper", () => ({ - FormWrapper: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/auth/lib/authOptions", () => ({ - authOptions: {}, -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -vi.mock("@/tolgee/server", () => ({ getTranslate: () => Promise.resolve((key: string) => key) })); - -describe("EmailChangeWithoutVerificationSuccessPage", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders success page with correct translations when user is not logged in", async () => { - vi.mocked(getServerSession).mockResolvedValue(null); - - const page = await EmailChangeWithoutVerificationSuccessPage(); - render(page); - - expect(screen.getByTestId("form-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("back-to-login")).toBeInTheDocument(); - expect(screen.getByText("auth.email-change.email_change_success")).toBeInTheDocument(); - expect(screen.getByText("auth.email-change.email_change_success_description")).toBeInTheDocument(); - }); - - test("redirects to home page when user is logged in", async () => { - vi.mocked(getServerSession).mockResolvedValue({ - user: { id: "123", email: "test@example.com" }, - expires: new Date().toISOString(), - }); - - await EmailChangeWithoutVerificationSuccessPage(); - - expect(redirect).toHaveBeenCalledWith("/"); - }); -}); diff --git a/apps/web/modules/auth/forgot-password/email-sent/page.test.tsx b/apps/web/modules/auth/forgot-password/email-sent/page.test.tsx deleted file mode 100644 index f41db2c587..0000000000 --- a/apps/web/modules/auth/forgot-password/email-sent/page.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { EmailSentPage } from "./page"; - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -vi.mock("@/modules/auth/components/back-to-login-button", () => ({ - BackToLoginButton: () =>
    Back to Login
    , -})); - -describe("EmailSentPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the email sent page with correct translations", async () => { - render(await EmailSentPage()); - - expect(screen.getByText("auth.forgot-password.email-sent.heading")).toBeInTheDocument(); - expect(screen.getByText("auth.forgot-password.email-sent.text")).toBeInTheDocument(); - expect(screen.getByText("Back to Login")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/auth/forgot-password/page.test.tsx b/apps/web/modules/auth/forgot-password/page.test.tsx deleted file mode 100644 index e05ea81596..0000000000 --- a/apps/web/modules/auth/forgot-password/page.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ForgotPasswordPage } from "./page"; - -vi.mock("@/modules/auth/components/form-wrapper", () => ({ - FormWrapper: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/auth/forgot-password/components/forgot-password-form", () => ({ - ForgotPasswordForm: () =>
    Forgot Password Form
    , -})); - -describe("ForgotPasswordPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the forgot password page with form wrapper and form", () => { - render(); - - expect(screen.getByTestId("form-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("forgot-password-form")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/auth/forgot-password/reset/components/reset-password-form.test.tsx b/apps/web/modules/auth/forgot-password/reset/components/reset-password-form.test.tsx deleted file mode 100644 index c36a2d3f7f..0000000000 --- a/apps/web/modules/auth/forgot-password/reset/components/reset-password-form.test.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { useRouter, useSearchParams } from "next/navigation"; -import { toast } from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { resetPasswordAction } from "@/modules/auth/forgot-password/reset/actions"; -import { ResetPasswordForm } from "./reset-password-form"; - -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), - useSearchParams: vi.fn(), -})); - -vi.mock("@/modules/auth/forgot-password/reset/actions", () => ({ - resetPasswordAction: vi.fn(), -})); - -vi.mock("react-hot-toast", () => ({ - toast: { - error: vi.fn(), - }, -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -describe("ResetPasswordForm", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockRouter = { - push: vi.fn(), - back: vi.fn(), - forward: vi.fn(), - refresh: vi.fn(), - replace: vi.fn(), - prefetch: vi.fn(), - }; - - const mockSearchParams = { - get: vi.fn(), - append: vi.fn(), - delete: vi.fn(), - set: vi.fn(), - sort: vi.fn(), - toString: vi.fn(), - forEach: vi.fn(), - entries: vi.fn(), - keys: vi.fn(), - values: vi.fn(), - has: vi.fn(), - }; - - beforeEach(() => { - vi.mocked(useRouter).mockReturnValue(mockRouter as any); - vi.mocked(useSearchParams).mockReturnValue(mockSearchParams as any); - vi.mocked(mockSearchParams.get).mockReturnValue("test-token"); - }); - - test("renders the form with password fields", () => { - render(); - - expect(screen.getByLabelText("auth.forgot-password.reset.new_password")).toBeInTheDocument(); - expect(screen.getByLabelText("auth.forgot-password.reset.confirm_password")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "auth.forgot-password.reset_password" })).toBeInTheDocument(); - }); - - test("shows error when passwords do not match", async () => { - render(); - - const passwordInput = screen.getByLabelText("auth.forgot-password.reset.new_password"); - const confirmPasswordInput = screen.getByLabelText("auth.forgot-password.reset.confirm_password"); - - await userEvent.type(passwordInput, "Password123!"); - await userEvent.type(confirmPasswordInput, "Different123!"); - - const submitButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" }); - await userEvent.click(submitButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("auth.forgot-password.reset.passwords_do_not_match"); - }); - }); - - test("successfully resets password and redirects", async () => { - vi.mocked(resetPasswordAction).mockResolvedValueOnce({ data: { success: true } }); - - render(); - - const passwordInput = screen.getByLabelText("auth.forgot-password.reset.new_password"); - const confirmPasswordInput = screen.getByLabelText("auth.forgot-password.reset.confirm_password"); - - await userEvent.type(passwordInput, "Password123!"); - await userEvent.type(confirmPasswordInput, "Password123!"); - - const submitButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" }); - await userEvent.click(submitButton); - - await waitFor(() => { - expect(resetPasswordAction).toHaveBeenCalledWith({ - token: "test-token", - password: "Password123!", - }); - expect(mockRouter.push).toHaveBeenCalledWith("/auth/forgot-password/reset/success"); - }); - }); - - test("shows error when no token is provided", async () => { - vi.mocked(mockSearchParams.get).mockReturnValueOnce(null); - - render(); - - const passwordInput = screen.getByLabelText("auth.forgot-password.reset.new_password"); - const confirmPasswordInput = screen.getByLabelText("auth.forgot-password.reset.confirm_password"); - - await userEvent.type(passwordInput, "Password123!"); - await userEvent.type(confirmPasswordInput, "Password123!"); - - const submitButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" }); - await userEvent.click(submitButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("auth.forgot-password.reset.no_token_provided"); - }); - }); -}); diff --git a/apps/web/modules/auth/forgot-password/reset/success/page.test.tsx b/apps/web/modules/auth/forgot-password/reset/success/page.test.tsx deleted file mode 100644 index 31c9374d93..0000000000 --- a/apps/web/modules/auth/forgot-password/reset/success/page.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ResetPasswordSuccessPage } from "./page"; - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -vi.mock("@/modules/auth/components/back-to-login-button", () => ({ - BackToLoginButton: () => , -})); - -vi.mock("@/modules/auth/components/form-wrapper", () => ({ - FormWrapper: ({ children }: { children: React.ReactNode }) =>
    {children}
    , -})); - -describe("ResetPasswordSuccessPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders success page with correct translations", async () => { - render(await ResetPasswordSuccessPage()); - - expect(screen.getByText("auth.forgot-password.reset.success.heading")).toBeInTheDocument(); - expect(screen.getByText("auth.forgot-password.reset.success.text")).toBeInTheDocument(); - expect(screen.getByText("Back to Login")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/auth/hooks/use-sign-out.test.tsx b/apps/web/modules/auth/hooks/use-sign-out.test.tsx deleted file mode 100644 index 820860abc6..0000000000 --- a/apps/web/modules/auth/hooks/use-sign-out.test.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, renderHook } from "@testing-library/react"; -import { signOut } from "next-auth/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { logger } from "@formbricks/logger"; -import { logSignOutAction } from "@/modules/auth/actions/sign-out"; - -// Import the actual hook (unmock it for testing) -vi.unmock("@/modules/auth/hooks/use-sign-out"); -const { useSignOut } = await import("./use-sign-out"); - -// Mock dependencies -vi.mock("@/modules/auth/actions/sign-out", () => ({ - logSignOutAction: vi.fn().mockResolvedValue(undefined), -})); - -vi.mock("next-auth/react", () => ({ - signOut: vi.fn().mockResolvedValue(undefined), -})); - -vi.mock("@formbricks/logger", () => ({ - logger: { - error: vi.fn(), - }, -})); - -describe("useSignOut", () => { - const mockSessionUser = { - id: "user-123", - email: "test@example.com", - }; - - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - test("should return signOut function", () => { - const { result } = renderHook(() => useSignOut()); - - expect(result.current.signOut).toBeDefined(); - expect(typeof result.current.signOut).toBe("function"); - }); - - test("should sign out without audit logging when no session user", async () => { - const { result } = renderHook(() => useSignOut()); - - await result.current.signOut(); - - expect(logSignOutAction).not.toHaveBeenCalled(); - expect(signOut).toHaveBeenCalledWith({ - redirect: undefined, - callbackUrl: undefined, - }); - }); - - test("should sign out with audit logging when session user exists", async () => { - const { result } = renderHook(() => useSignOut(mockSessionUser)); - - await result.current.signOut({ - reason: "user_initiated", - redirectUrl: "/dashboard", - organizationId: "org-123", - }); - - expect(logSignOutAction).toHaveBeenCalledWith("user-123", "test@example.com", { - reason: "user_initiated", - redirectUrl: "/dashboard", - organizationId: "org-123", - }); - - expect(signOut).toHaveBeenCalledWith({ - redirect: undefined, - callbackUrl: undefined, - }); - }); - - test("should handle null session user", async () => { - const { result } = renderHook(() => useSignOut(null)); - - await result.current.signOut(); - - expect(logSignOutAction).not.toHaveBeenCalled(); - expect(signOut).toHaveBeenCalledWith({ - redirect: undefined, - callbackUrl: undefined, - }); - }); - - test("should use default reason when not provided", async () => { - const { result } = renderHook(() => useSignOut(mockSessionUser)); - - await result.current.signOut(); - - expect(logSignOutAction).toHaveBeenCalledWith("user-123", "test@example.com", { - reason: "user_initiated", - redirectUrl: undefined, - organizationId: undefined, - }); - }); - - test("should use callbackUrl as redirectUrl when redirectUrl not provided", async () => { - const { result } = renderHook(() => useSignOut(mockSessionUser)); - - await result.current.signOut({ - callbackUrl: "/auth/login", - organizationId: "org-456", - }); - - expect(logSignOutAction).toHaveBeenCalledWith("user-123", "test@example.com", { - reason: "user_initiated", - redirectUrl: "/auth/login", - organizationId: "org-456", - }); - - expect(signOut).toHaveBeenCalledWith({ - redirect: undefined, - callbackUrl: "/auth/login", - }); - }); - - test("should pass through NextAuth signOut options", async () => { - const { result } = renderHook(() => useSignOut(mockSessionUser)); - - await result.current.signOut({ - redirect: false, - callbackUrl: "/custom-redirect", - }); - - expect(signOut).toHaveBeenCalledWith({ - redirect: false, - callbackUrl: "/custom-redirect", - }); - }); - - test("should handle different sign out reasons", async () => { - const { result } = renderHook(() => useSignOut(mockSessionUser)); - - const reasons = ["account_deletion", "email_change", "session_timeout", "forced_logout"] as const; - - for (const reason of reasons) { - vi.clearAllMocks(); - - await result.current.signOut({ reason }); - - expect(logSignOutAction).toHaveBeenCalledWith("user-123", "test@example.com", { - reason, - redirectUrl: undefined, - organizationId: undefined, - }); - } - }); - - test("should handle session user without email", async () => { - const userWithoutEmail = { id: "user-456" }; - const { result } = renderHook(() => useSignOut(userWithoutEmail)); - - await result.current.signOut(); - - expect(logSignOutAction).toHaveBeenCalledWith("user-456", "", { - reason: "user_initiated", - redirectUrl: undefined, - organizationId: undefined, - }); - }); - - test("should not block sign out when audit logging fails", async () => { - vi.mocked(logSignOutAction).mockRejectedValueOnce(new Error("Audit logging failed")); - - const { result } = renderHook(() => useSignOut(mockSessionUser)); - - await result.current.signOut(); - - expect(logger.error).toHaveBeenCalledWith("Failed to log signOut event:", expect.any(Error)); - - expect(signOut).toHaveBeenCalledWith({ - redirect: undefined, - callbackUrl: undefined, - }); - }); - - test("should return NextAuth signOut result", async () => { - const mockSignOutResult = { url: "https://example.com/signed-out" }; - vi.mocked(signOut).mockResolvedValueOnce(mockSignOutResult); - - const { result } = renderHook(() => useSignOut(mockSessionUser)); - - const signOutResult = await result.current.signOut(); - - expect(signOutResult).toBe(mockSignOutResult); - }); - - test("should handle audit logging error and still return NextAuth result", async () => { - const mockSignOutResult = { url: "https://example.com/signed-out" }; - vi.mocked(logSignOutAction).mockRejectedValueOnce(new Error("Network error")); - vi.mocked(signOut).mockResolvedValueOnce(mockSignOutResult); - - const { result } = renderHook(() => useSignOut(mockSessionUser)); - - const signOutResult = await result.current.signOut(); - - expect(logger.error).toHaveBeenCalled(); - expect(signOutResult).toBe(mockSignOutResult); - }); - - test("should handle complex sign out scenario", async () => { - const { result } = renderHook(() => useSignOut(mockSessionUser)); - - await result.current.signOut({ - reason: "email_change", - redirectUrl: "/profile/email-changed", - organizationId: "org-complex-123", - redirect: true, - callbackUrl: "/dashboard", - }); - - expect(logSignOutAction).toHaveBeenCalledWith("user-123", "test@example.com", { - reason: "email_change", - redirectUrl: "/profile/email-changed", // redirectUrl takes precedence over callbackUrl - organizationId: "org-complex-123", - }); - - expect(signOut).toHaveBeenCalledWith({ - redirect: true, - callbackUrl: "/dashboard", - }); - }); - - test("should wait for audit logging before calling NextAuth signOut", async () => { - let auditLogResolved = false; - vi.mocked(logSignOutAction).mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)); - auditLogResolved = true; - }); - - const { result } = renderHook(() => useSignOut(mockSessionUser)); - - const signOutPromise = result.current.signOut(); - - // NextAuth signOut should not be called immediately - expect(signOut).not.toHaveBeenCalled(); - - await signOutPromise; - - expect(auditLogResolved).toBe(true); - expect(signOut).toHaveBeenCalled(); - }); -}); diff --git a/apps/web/modules/auth/invite/components/content-layout.test.tsx b/apps/web/modules/auth/invite/components/content-layout.test.tsx deleted file mode 100644 index f4b44302b3..0000000000 --- a/apps/web/modules/auth/invite/components/content-layout.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test } from "vitest"; -import { ContentLayout } from "./content-layout"; - -describe("ContentLayout", () => { - afterEach(() => { - cleanup(); - }); - - test("renders headline and description", () => { - render(); - - expect(screen.getByText("Test Headline")).toBeInTheDocument(); - expect(screen.getByText("Test Description")).toBeInTheDocument(); - }); - - test("renders children when provided", () => { - render( - -
    Test Child
    -
    - ); - - expect(screen.getByText("Test Child")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/auth/invite/page.test.tsx b/apps/web/modules/auth/invite/page.test.tsx deleted file mode 100644 index b93d3b95e1..0000000000 --- a/apps/web/modules/auth/invite/page.test.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup } from "@testing-library/preact"; -import { getServerSession } from "next-auth"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { verifyInviteToken } from "@/lib/jwt"; -import { getInvite } from "./lib/invite"; -import { InvitePage } from "./page"; - -// Mock Next.js headers to avoid `headers()` request scope error -vi.mock("next/headers", () => ({ - headers: () => ({ - get: () => "en", - }), -})); - -// Include AVAILABLE_LOCALES for locale matching -vi.mock("@/lib/constants", () => ({ - AVAILABLE_LOCALES: ["en"], - WEBAPP_URL: "http://localhost:3000", - ENCRYPTION_KEY: "test-encryption-key-32-chars-long!!", - IS_FORMBRICKS_CLOUD: false, - IS_PRODUCTION: false, - ENTERPRISE_LICENSE_KEY: undefined, - FB_LOGO_URL: "https://formbricks.com/logo.png", - SMTP_HOST: "smtp.example.com", - SMTP_PORT: "587", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -vi.mock("@/lib/env", () => ({ - env: { - PUBLIC_URL: "https://public-domain.com", - }, -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); - -vi.mock("./lib/invite", () => ({ - getInvite: vi.fn(), -})); - -vi.mock("@/lib/jwt", () => ({ - verifyInviteToken: vi.fn(), -})); - -vi.mock("@tolgee/react", async () => { - const actual = await vi.importActual("@tolgee/react"); - return { - ...actual, - useTranslate: () => ({ - t: (key: string) => key, - }), - T: ({ keyName }: { keyName: string }) => keyName, - }; -}); - -vi.mock("@formbricks/logger", () => ({ - logger: { - error: vi.fn(), - warn: vi.fn(), - info: vi.fn(), - debug: vi.fn(), - fatal: vi.fn(), - }, -})); - -vi.mock("@/modules/ee/lib/ee", () => ({ - ee: { - sso: { - getSSOConfig: vi.fn().mockResolvedValue(null), - }, - }, -})); - -describe("InvitePage", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("should show invite not found when invite doesn't exist", async () => { - vi.mocked(getServerSession).mockResolvedValue(null); - vi.mocked(verifyInviteToken).mockReturnValue({ inviteId: "123", email: "test@example.com" }); - vi.mocked(getInvite).mockResolvedValue(null); - - const result = await InvitePage({ searchParams: Promise.resolve({ token: "test-token" }) }); - - expect(result.props.headline).toContain("auth.invite.invite_not_found"); - expect(result.props.description).toContain("auth.invite.invite_not_found_description"); - }); -}); diff --git a/apps/web/modules/auth/signup-without-verification-success/page.test.tsx b/apps/web/modules/auth/signup-without-verification-success/page.test.tsx deleted file mode 100644 index 86867bc6ae..0000000000 --- a/apps/web/modules/auth/signup-without-verification-success/page.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getEmailFromEmailToken } from "@/lib/jwt"; -import { SignupWithoutVerificationSuccessPage } from "@/modules/auth/signup-without-verification-success/page"; - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, - T: ({ keyName, params }) => { - if (params && params.email) { - return `${keyName} ${params.email}`; - } - return keyName; - }, -})); - -vi.mock("@/lib/constants", () => ({ - INTERCOM_SECRET_KEY: "test-secret-key", - IS_INTERCOM_CONFIGURED: true, - INTERCOM_APP_ID: "test-app-id", - ENCRYPTION_KEY: "test-encryption-key", - ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key", - GITHUB_ID: "test-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_POSTHOG_CONFIGURED: true, - POSTHOG_API_HOST: "test-posthog-api-host", - POSTHOG_API_KEY: "test-posthog-api-key", - FORMBRICKS_ENVIRONMENT_ID: "mock-formbricks-environment-id", - IS_FORMBRICKS_ENABLED: true, - SESSION_MAX_AGE: 1000, - AVAILABLE_LOCALES: ["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW", "pt-PT"], -})); - -vi.mock("@/modules/auth/components/back-to-login-button", () => ({ - BackToLoginButton: () =>
    Mocked BackToLoginButton
    , -})); - -vi.mock("@/modules/auth/components/form-wrapper", () => ({ - FormWrapper: ({ children }) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children }) =>
    {children}
    , -})); - -vi.mock("@/lib/jwt", () => ({ - getEmailFromEmailToken: vi.fn(), -})); - -describe("SignupWithoutVerificationSuccessPage", () => { - afterEach(() => { - cleanup(); - vi.resetAllMocks(); - }); - - test("renders the success page correctly", async () => { - vi.mocked(getEmailFromEmailToken).mockReturnValue("test@example.com"); - - const Page = await SignupWithoutVerificationSuccessPage({ searchParams: { token: "test-token" } }); - render(Page); - - expect( - screen.getByText("auth.signup_without_verification_success.user_successfully_created") - ).toBeInTheDocument(); - expect( - screen.getByText( - "auth.signup_without_verification_success.user_successfully_created_info test@example.com" - ) - ).toBeInTheDocument(); - expect(screen.getByText("Mocked BackToLoginButton")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/auth/signup/components/signup-form.test.tsx b/apps/web/modules/auth/signup/components/signup-form.test.tsx deleted file mode 100644 index 06a28eea11..0000000000 --- a/apps/web/modules/auth/signup/components/signup-form.test.tsx +++ /dev/null @@ -1,406 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { useSearchParams } from "next/navigation"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { createUserAction } from "@/modules/auth/signup/actions"; -import { createEmailTokenAction } from "../../../auth/actions"; -import { SignupForm } from "./signup-form"; - -// Mock dependencies - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - FB_LOGO_URL: "mock-fb-logo-url", - SMTP_HOST: "smtp.example.com", - SMTP_PORT: 587, - SMTP_USER: "smtp-user", -})); - -// Set up a push mock for useRouter -const pushMock = vi.fn(); -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - push: pushMock, - }), - useSearchParams: vi.fn(), -})); - -vi.mock("react-turnstile", () => ({ - useTurnstile: () => ({ - reset: vi.fn(), - }), - default: (props: any) => ( -
    { - if (props.onSuccess) { - props.onSuccess("test-turnstile-token"); - } - }} - {...props} - /> - ), -})); - -vi.mock("react-hot-toast", () => ({ - default: { - error: vi.fn(), - toast: { - error: vi.fn(), - }, - }, -})); - -vi.mock("@/modules/auth/signup/actions", () => ({ - createUserAction: vi.fn(), -})); - -vi.mock("../../../auth/actions", () => ({ - createEmailTokenAction: vi.fn(), -})); - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(), -})); - -// Mock components - -vi.mock("@/modules/ee/sso/components/sso-options", () => ({ - SSOOptions: () =>
    SSOOptions
    , -})); -vi.mock("@/modules/auth/signup/components/terms-privacy-links", () => ({ - TermsPrivacyLinks: () =>
    TermsPrivacyLinks
    , -})); -vi.mock("@/modules/ui/components/button", () => ({ - Button: (props: any) => , -})); -vi.mock("@/modules/ui/components/input", () => ({ - Input: (props: any) => , -})); -vi.mock("@/modules/ui/components/password-input", () => ({ - PasswordInput: (props: any) => , -})); - -const defaultProps = { - webAppUrl: "http://localhost", - privacyUrl: "http://localhost/privacy", - termsUrl: "http://localhost/terms", - emailAuthEnabled: true, - googleOAuthEnabled: false, - githubOAuthEnabled: false, - azureOAuthEnabled: false, - oidcOAuthEnabled: false, - userLocale: "en-US", - emailVerificationDisabled: false, - isSsoEnabled: false, - samlSsoEnabled: false, - isTurnstileConfigured: false, - samlTenant: "", - samlProduct: "", - turnstileSiteKey: "dummy", // not used since isTurnstileConfigured is false -} as const; - -describe("SignupForm", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("toggles the signup form on button click", () => { - render(); - - // Initially, the signup form is hidden. - try { - screen.getByTestId("signup-name"); - } catch (e) { - expect(e).toBeInstanceOf(Error); - } - - // Click the button to reveal the signup form. - const toggleButton = screen.getByTestId("signup-show-login"); - fireEvent.click(toggleButton); - - // Now the input fields should appear. - expect(screen.getByTestId("signup-name")).toBeInTheDocument(); - expect(screen.getByTestId("signup-email")).toBeInTheDocument(); - expect(screen.getByTestId("signup-password")).toBeInTheDocument(); - }); - - test("submits the form successfully", async () => { - // Set up mocks for the API actions. - vi.mocked(createUserAction).mockResolvedValue({ data: true } as any); - vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" }); - - render(); - - // Click the button to reveal the signup form. - const toggleButton = screen.getByTestId("signup-show-login"); - fireEvent.click(toggleButton); - - const nameInput = screen.getByTestId("signup-name"); - const emailInput = screen.getByTestId("signup-email"); - const passwordInput = screen.getByTestId("signup-password"); - - fireEvent.change(nameInput, { target: { value: "Test User" } }); - fireEvent.change(emailInput, { target: { value: "test@example.com" } }); - fireEvent.change(passwordInput, { target: { value: "Password123" } }); - - const submitButton = screen.getByTestId("signup-submit"); - fireEvent.submit(submitButton); - - await waitFor(() => { - expect(createUserAction).toHaveBeenCalledWith({ - name: "Test User", - email: "test@example.com", - password: "Password123", - userLocale: defaultProps.userLocale, - inviteToken: "", - emailVerificationDisabled: defaultProps.emailVerificationDisabled, - turnstileToken: undefined, - }); - }); - - await waitFor(() => { - expect(createEmailTokenAction).toHaveBeenCalledWith({ email: "test@example.com" }); - }); - - // Since email verification is enabled (emailVerificationDisabled is false), - // router.push should be called with the verification URL. - expect(pushMock).toHaveBeenCalledWith("/auth/verification-requested?token=token123"); - }); - - test("submits the form successfully when turnstile is configured", async () => { - // Override props to enable Turnstile - const props = { - ...defaultProps, - isTurnstileConfigured: true, - turnstileSiteKey: "dummy", - emailVerificationDisabled: true, - }; - - // Set up mocks for the API actions - vi.mocked(createUserAction).mockResolvedValue({ data: true } as any); - vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" }); - - render(); - - // Click the button to reveal the signup form - const toggleButton = screen.getByTestId("signup-show-login"); - fireEvent.click(toggleButton); - - // Fill out the form fields - fireEvent.change(screen.getByTestId("signup-name"), { target: { value: "Test User" } }); - fireEvent.change(screen.getByTestId("signup-email"), { target: { value: "test@example.com" } }); - fireEvent.change(screen.getByTestId("signup-password"), { target: { value: "Password123" } }); - - // Simulate receiving a turnstile token by clicking the Turnstile element. - const turnstileElement = screen.getByTestId("turnstile"); - fireEvent.click(turnstileElement); - - // Submit the form. - const submitButton = screen.getByTestId("signup-submit"); - fireEvent.submit(submitButton); - await waitFor(() => { - expect(createUserAction).toHaveBeenCalledWith({ - name: "Test User", - email: "test@example.com", - password: "Password123", - userLocale: props.userLocale, - inviteToken: "", - emailVerificationDisabled: true, - turnstileToken: "test-turnstile-token", - }); - }); - - await waitFor(() => { - expect(createEmailTokenAction).toHaveBeenCalledWith({ email: "test@example.com" }); - }); - - expect(pushMock).toHaveBeenCalledWith("/auth/signup-without-verification-success?token=token123"); - }); - - test("submits the form successfully when turnstile is configured, but createEmailTokenAction don't return data", async () => { - // Override props to enable Turnstile - const props = { - ...defaultProps, - isTurnstileConfigured: true, - turnstileSiteKey: "dummy", - emailVerificationDisabled: true, - }; - - // Set up mocks for the API actions - vi.mocked(createUserAction).mockResolvedValue({ data: true } as any); - vi.mocked(createEmailTokenAction).mockResolvedValue(undefined); - vi.mocked(getFormattedErrorMessage).mockReturnValue("error"); - - render(); - - // Click the button to reveal the signup form - const toggleButton = screen.getByTestId("signup-show-login"); - fireEvent.click(toggleButton); - - // Fill out the form fields - fireEvent.change(screen.getByTestId("signup-name"), { target: { value: "Test User" } }); - fireEvent.change(screen.getByTestId("signup-email"), { target: { value: "test@example.com" } }); - fireEvent.change(screen.getByTestId("signup-password"), { target: { value: "Password123" } }); - - // Simulate receiving a turnstile token by clicking the Turnstile element. - const turnstileElement = screen.getByTestId("turnstile"); - fireEvent.click(turnstileElement); - - // Submit the form. - const submitButton = screen.getByTestId("signup-submit"); - fireEvent.submit(submitButton); - await waitFor(() => { - expect(createUserAction).toHaveBeenCalledWith({ - name: "Test User", - email: "test@example.com", - password: "Password123", - userLocale: props.userLocale, - inviteToken: "", - emailVerificationDisabled: true, - turnstileToken: "test-turnstile-token", - }); - }); - - // Since Turnstile is configured, but no token is received, an error message should be shown. - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("error"); - }); - }); - - test("shows an error message if turnstile is configured, but no token is received", async () => { - // Override props to enable Turnstile - const props = { - ...defaultProps, - isTurnstileConfigured: true, - turnstileSiteKey: "dummy", - emailVerificationDisabled: true, - }; - - // Set up mocks for the API actions - vi.mocked(createUserAction).mockResolvedValue({ data: true } as any); - vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" }); - - render(); - - // Click the button to reveal the signup form - const toggleButton = screen.getByTestId("signup-show-login"); - fireEvent.click(toggleButton); - - // Fill out the form fields - fireEvent.change(screen.getByTestId("signup-name"), { target: { value: "Test User" } }); - fireEvent.change(screen.getByTestId("signup-email"), { target: { value: "test@example.com" } }); - fireEvent.change(screen.getByTestId("signup-password"), { target: { value: "Password123" } }); - - // Submit the form. - const submitButton = screen.getByTestId("signup-submit"); - fireEvent.submit(submitButton); - - // Since Turnstile is configured, but no token is received, an error message should be shown. - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("auth.signup.please_verify_captcha"); - }); - }); - - test("Invite token is in the search params", async () => { - // Set up mocks for the API actions - vi.mocked(createUserAction).mockResolvedValue({ data: true } as any); - vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" }); - vi.mocked(useSearchParams).mockReturnValue(new URLSearchParams("inviteToken=token123") as any); - - render(); - - // Click the button to reveal the signup form - const toggleButton = screen.getByTestId("signup-show-login"); - fireEvent.click(toggleButton); - - // Fill out the form fields - fireEvent.change(screen.getByTestId("signup-name"), { target: { value: "Test User" } }); - fireEvent.change(screen.getByTestId("signup-email"), { target: { value: "test@example.com" } }); - fireEvent.change(screen.getByTestId("signup-password"), { target: { value: "Password123" } }); - - // Submit the form. - const submitButton = screen.getByTestId("signup-submit"); - fireEvent.submit(submitButton); - - // Check that the invite token is passed to the createUserAction - await waitFor(() => { - expect(createUserAction).toHaveBeenCalledWith({ - name: "Test User", - email: "test@example.com", - password: "Password123", - userLocale: defaultProps.userLocale, - inviteToken: "token123", - emailVerificationDisabled: defaultProps.emailVerificationDisabled, - turnstileToken: undefined, - }); - }); - - await waitFor(() => { - expect(createEmailTokenAction).toHaveBeenCalledWith({ email: "test@example.com" }); - }); - - expect(pushMock).toHaveBeenCalledWith("/auth/verification-requested?token=token123"); - }); - - test("shows an error message when createUserAction fails", async () => { - // Set up mocks for the API actions - vi.mocked(createUserAction).mockResolvedValue(undefined); - vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" }); - vi.mocked(getFormattedErrorMessage).mockReturnValue("user creation failed"); - - render(); - - // Click the button to reveal the signup form - const toggleButton = screen.getByTestId("signup-show-login"); - fireEvent.click(toggleButton); - - // Fill out the form fields - fireEvent.change(screen.getByTestId("signup-name"), { target: { value: "Test User" } }); - fireEvent.change(screen.getByTestId("signup-email"), { target: { value: "test@example.com" } }); - fireEvent.change(screen.getByTestId("signup-password"), { target: { value: "Password123" } }); - - // Submit the form. - const submitButton = screen.getByTestId("signup-submit"); - fireEvent.submit(submitButton); - - await waitFor(() => { - expect(createUserAction).toHaveBeenCalled(); - }); - - await waitFor(() => { - expect(createEmailTokenAction).toHaveBeenCalledWith({ email: "test@example.com" }); - }); - - // An error message should be shown. - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("user creation failed"); - }); - - // router.push should not have been called. - expect(pushMock).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/modules/auth/signup/page.test.tsx b/apps/web/modules/auth/signup/page.test.tsx deleted file mode 100644 index 6efea7dc22..0000000000 --- a/apps/web/modules/auth/signup/page.test.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { notFound } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { verifyInviteToken } from "@/lib/jwt"; -import { findMatchingLocale } from "@/lib/utils/locale"; -import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite"; -import { - getIsMultiOrgEnabled, - getIsSamlSsoEnabled, - getIsSsoEnabled, -} from "@/modules/ee/license-check/lib/utils"; -import { SignupPage } from "./page"; - -// Mock the necessary dependencies -vi.mock("@/modules/auth/components/form-wrapper", () => ({ - FormWrapper: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/auth/signup/components/signup-form", () => ({ - SignupForm: () =>
    SignupForm
    , -})); - -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getIsMultiOrgEnabled: vi.fn(), - getIsSsoEnabled: vi.fn(), - getIsSamlSsoEnabled: vi.fn(), - getIsWhitelabelEnabled: vi.fn(), - getIsRemoveBrandingEnabled: vi.fn(), - getIsContactsEnabled: vi.fn(), - getIsAiEnabled: vi.fn(), - getIsSpamProtectionEnabled: vi.fn(), - getIsPendingDowngrade: vi.fn(), -})); - -vi.mock("@/modules/auth/signup/lib/invite", () => ({ - getIsValidInviteToken: vi.fn(), -})); - -vi.mock("@/lib/jwt", () => ({ - verifyInviteToken: vi.fn(), -})); - -vi.mock("@/lib/utils/locale", () => ({ - findMatchingLocale: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - notFound: vi.fn(), -})); - -// Mock environment variables and constants -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - FB_LOGO_URL: "mock-fb-logo-url", - SMTP_HOST: "smtp.example.com", - SMTP_PORT: 587, - SMTP_USER: "smtp-user", - SAML_AUDIENCE: "test-saml-audience", - SAML_PATH: "test-saml-path", - SAML_DATABASE_URL: "test-saml-database-url", - TERMS_URL: "test-terms-url", - SIGNUP_ENABLED: true, - PRIVACY_URL: "test-privacy-url", - EMAIL_VERIFICATION_DISABLED: false, - EMAIL_AUTH_ENABLED: true, - GOOGLE_OAUTH_ENABLED: true, - GITHUB_OAUTH_ENABLED: true, - AZURE_OAUTH_ENABLED: true, - OIDC_OAUTH_ENABLED: true, - DEFAULT_ORGANIZATION_ID: "test-default-organization-id", - IS_TURNSTILE_CONFIGURED: true, - SAML_TENANT: "test-saml-tenant", - SAML_PRODUCT: "test-saml-product", - TURNSTILE_SITE_KEY: "test-turnstile-site-key", - SAML_OAUTH_ENABLED: true, -})); - -describe("SignupPage", () => { - const mockSearchParams = { - inviteToken: "test-token", - email: "test@example.com", - }; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders the signup page with all components when signup is enabled", async () => { - // Mock the license check functions to return true - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); - vi.mocked(getIsSsoEnabled).mockResolvedValue(true); - vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(true); - vi.mocked(findMatchingLocale).mockResolvedValue("en-US"); - vi.mocked(verifyInviteToken).mockReturnValue({ - inviteId: "test-invite-id", - email: "test@example.com", - }); - vi.mocked(getIsValidInviteToken).mockResolvedValue(true); - - const result = await SignupPage({ searchParams: mockSearchParams }); - render(result); - - // Verify main components are rendered - expect(screen.getByTestId("form-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("signup-form")).toBeInTheDocument(); - }); - - test("calls notFound when signup is disabled and no valid invite token is provided", async () => { - // Mock the license check functions to return false - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false); - vi.mocked(verifyInviteToken).mockImplementation(() => { - throw new Error("Invalid token"); - }); - - await SignupPage({ searchParams: {} }); - - expect(notFound).toHaveBeenCalled(); - }); - - test("calls notFound when invite token is invalid", async () => { - // Mock the license check functions to return false - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false); - vi.mocked(verifyInviteToken).mockImplementation(() => { - throw new Error("Invalid token"); - }); - - await SignupPage({ searchParams: { inviteToken: "invalid-token" } }); - - expect(notFound).toHaveBeenCalled(); - }); - - test("calls notFound when invite token is valid but invite is not found", async () => { - // Mock the license check functions to return false - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false); - vi.mocked(verifyInviteToken).mockReturnValue({ - inviteId: "test-invite-id", - email: "test@example.com", - }); - vi.mocked(getIsValidInviteToken).mockResolvedValue(false); - - await SignupPage({ searchParams: { inviteToken: "test-token" } }); - - expect(notFound).toHaveBeenCalled(); - }); - - test("renders the page with email from search params", async () => { - // Mock the license check functions to return true - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); - vi.mocked(getIsSsoEnabled).mockResolvedValue(true); - vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(true); - vi.mocked(findMatchingLocale).mockResolvedValue("en-US"); - vi.mocked(verifyInviteToken).mockReturnValue({ - inviteId: "test-invite-id", - email: "test@example.com", - }); - vi.mocked(getIsValidInviteToken).mockResolvedValue(true); - - const result = await SignupPage({ searchParams: { email: "test@example.com" } }); - render(result); - - // Verify that the form is rendered with the email from search params - expect(screen.getByTestId("signup-form")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/auth/verification-requested/components/request-verification-email.test.tsx b/apps/web/modules/auth/verification-requested/components/request-verification-email.test.tsx deleted file mode 100644 index 89117806d8..0000000000 --- a/apps/web/modules/auth/verification-requested/components/request-verification-email.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { resendVerificationEmailAction } from "../actions"; -import { RequestVerificationEmail } from "./request-verification-email"; - -// Mock dependencies -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string, params?: { email?: string }) => { - if (key === "auth.verification-requested.no_email_provided") { - return "No email provided"; - } - if (key === "auth.verification-requested.verification_email_resent_successfully") { - return `Verification email sent! Please check your inbox.`; - } - if (key === "auth.verification-requested.resend_verification_email") { - return "Resend verification email"; - } - return key; - }, - }), -})); - -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock("../actions", () => ({ - resendVerificationEmailAction: vi.fn(), -})); - -describe("RequestVerificationEmail", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders resend verification email button", () => { - render(); - expect(screen.getByText("Resend verification email")).toBeInTheDocument(); - }); - - test("shows error toast when no email is provided", async () => { - render(); - const button = screen.getByText("Resend verification email"); - await fireEvent.click(button); - expect(toast.error).toHaveBeenCalledWith("No email provided"); - }); - - test("shows success toast when verification email is sent successfully", async () => { - const mockEmail = "test@example.com"; - vi.mocked(resendVerificationEmailAction).mockResolvedValueOnce({ data: true }); - - render(); - const button = screen.getByText("Resend verification email"); - await fireEvent.click(button); - - expect(resendVerificationEmailAction).toHaveBeenCalledWith({ email: mockEmail }); - expect(toast.success).toHaveBeenCalledWith(`Verification email sent! Please check your inbox.`); - }); - - test("reloads page when visibility changes to visible", () => { - const mockReload = vi.fn(); - Object.defineProperty(window, "location", { - value: { reload: mockReload }, - writable: true, - }); - - render(); - - // Simulate visibility change - document.dispatchEvent(new Event("visibilitychange")); - - expect(mockReload).toHaveBeenCalled(); - }); -}); diff --git a/apps/web/modules/auth/verification-requested/page.test.tsx b/apps/web/modules/auth/verification-requested/page.test.tsx deleted file mode 100644 index fd3f9d999a..0000000000 --- a/apps/web/modules/auth/verification-requested/page.test.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { getEmailFromEmailToken } from "@/lib/jwt"; -import { VerificationRequestedPage } from "@/modules/auth/verification-requested/page"; - -vi.mock("@/lib/jwt", () => ({ - getEmailFromEmailToken: vi.fn(), -})); - -vi.mock("@formbricks/logger", () => ({ - logger: { - error: vi.fn(), - }, -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, - T: ({ keyName, params }) => { - if (params && params.email) { - return `${keyName} ${params.email}`; - } - return keyName; - }, -})); - -vi.mock("@/lib/constants", () => ({ - INTERCOM_SECRET_KEY: "test-secret-key", - IS_INTERCOM_CONFIGURED: true, - INTERCOM_APP_ID: "test-app-id", - ENCRYPTION_KEY: "test-encryption-key", - ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key", - GITHUB_ID: "test-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_POSTHOG_CONFIGURED: true, - POSTHOG_API_HOST: "test-posthog-api-host", - POSTHOG_API_KEY: "test-posthog-api-key", - FORMBRICKS_ENVIRONMENT_ID: "mock-formbricks-environment-id", - IS_FORMBRICKS_ENABLED: true, - SESSION_MAX_AGE: 1000, - AVAILABLE_LOCALES: ["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW", "pt-PT"], -})); - -vi.mock("@/modules/auth/components/form-wrapper", () => ({ - FormWrapper: ({ children }) =>
    {children}
    , -})); - -vi.mock("@/modules/auth/verification-requested/components/request-verification-email", () => ({ - RequestVerificationEmail: ({ email }) =>
    Mocked RequestVerificationEmail: {email}
    , -})); - -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children }) =>
    {children}
    , -})); - -describe("VerificationRequestedPage", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - test("renders the page with valid email", async () => { - const mockEmail = "test@example.com"; - vi.mocked(getEmailFromEmailToken).mockReturnValue(mockEmail); - - const searchParams = { token: "valid-token" }; - const Page = await VerificationRequestedPage({ searchParams }); - render(Page); - - expect( - screen.getByText("auth.verification-requested.please_confirm_your_email_address") - ).toBeInTheDocument(); - expect(screen.getAllByText(/test@example\.com/)).toHaveLength(2); - expect( - screen.getByText( - "auth.verification-requested.verification_email_successfully_sent_info test@example.com" - ) - ).toBeInTheDocument(); - expect( - screen.getByText(`Mocked RequestVerificationEmail: ${mockEmail.toLowerCase()}`) - ).toBeInTheDocument(); - }); - - test("renders invalid email message when email parsing fails", async () => { - vi.mocked(getEmailFromEmailToken).mockReturnValue("invalid-email"); - - const searchParams = { token: "valid-token" }; - const Page = await VerificationRequestedPage({ searchParams }); - render(Page); - - expect(screen.getByText("auth.verification-requested.invalid_email_address")).toBeInTheDocument(); - }); - - test("renders invalid token message when token is invalid", async () => { - const mockError = new Error("Invalid token"); - const { logger } = await import("@formbricks/logger"); - - vi.mocked(getEmailFromEmailToken).mockImplementation(() => { - throw mockError; - }); - - const searchParams = { token: "invalid-token" }; - const Page = await VerificationRequestedPage({ searchParams }); - render(Page); - - expect(logger.error).toHaveBeenCalledWith(mockError, "Invalid token"); - expect(screen.getByText("auth.verification-requested.invalid_token")).toBeInTheDocument(); - }); - - test("calls logger.error when token parsing throws an error", async () => { - const mockError = new Error("JWT malformed"); - const { logger } = await import("@formbricks/logger"); - - vi.mocked(getEmailFromEmailToken).mockImplementation(() => { - throw mockError; - }); - - const searchParams = { token: "malformed-token" }; - await VerificationRequestedPage({ searchParams }); - - expect(logger.error).toHaveBeenCalledWith(mockError, "Invalid token"); - expect(logger.error).toHaveBeenCalledTimes(1); - }); -}); diff --git a/apps/web/modules/auth/verify-email-change/components/email-change-sign-in.test.tsx b/apps/web/modules/auth/verify-email-change/components/email-change-sign-in.test.tsx deleted file mode 100644 index 717211e068..0000000000 --- a/apps/web/modules/auth/verify-email-change/components/email-change-sign-in.test.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import { signOut } from "next-auth/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { verifyEmailChangeAction } from "@/modules/auth/verify-email-change/actions"; -import { EmailChangeSignIn } from "./email-change-sign-in"; - -// Mock dependencies -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("next-auth/react", () => ({ - signOut: vi.fn(), -})); - -vi.mock("@/modules/auth/verify-email-change/actions", () => ({ - verifyEmailChangeAction: vi.fn(), -})); - -describe("EmailChangeSignIn", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("shows loading state initially", () => { - render(); - expect(screen.getByText("auth.email-change.email_verification_loading")).toBeInTheDocument(); - }); - - test("handles successful email change verification", async () => { - vi.mocked(verifyEmailChangeAction).mockResolvedValueOnce({ - data: { - id: "123", - email: "test@example.com", - emailVerified: new Date().toISOString(), - locale: "en-US", - }, - }); - - render(); - - await waitFor(() => { - expect(screen.getByText("auth.email-change.email_change_success")).toBeInTheDocument(); - expect(screen.getByText("auth.email-change.email_change_success_description")).toBeInTheDocument(); - }); - - await waitFor(() => { - expect(signOut).toHaveBeenCalledWith({ redirect: false }); - }); - }); - - test("handles failed email change verification", async () => { - vi.mocked(verifyEmailChangeAction).mockResolvedValueOnce({ serverError: "Error" }); - - render(); - - await waitFor(() => { - expect(screen.getByText("auth.email-change.email_verification_failed")).toBeInTheDocument(); - expect(screen.getByText("auth.email-change.invalid_or_expired_token")).toBeInTheDocument(); - }); - - expect(signOut).not.toHaveBeenCalled(); - }); - - test("handles empty token", () => { - render(); - - expect(screen.getByText("auth.email-change.email_verification_failed")).toBeInTheDocument(); - expect(screen.getByText("auth.email-change.invalid_or_expired_token")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/auth/verify-email-change/page.test.tsx b/apps/web/modules/auth/verify-email-change/page.test.tsx deleted file mode 100644 index fd9d8d6a36..0000000000 --- a/apps/web/modules/auth/verify-email-change/page.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { VerifyEmailChangePage } from "./page"; - -// Mock the necessary dependencies -vi.mock("@/modules/auth/components/back-to-login-button", () => ({ - BackToLoginButton: () =>
    Back to Login
    , -})); - -vi.mock("@/modules/auth/components/form-wrapper", () => ({ - FormWrapper: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/auth/verify-email-change/components/email-change-sign-in", () => ({ - EmailChangeSignIn: ({ token }: { token: string }) => ( -
    Email Change Sign In with token: {token}
    - ), -})); - -describe("VerifyEmailChangePage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the page with form wrapper and components", async () => { - const searchParams = { token: "test-token" }; - render(await VerifyEmailChangePage({ searchParams })); - - expect(screen.getByTestId("form-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("email-change-sign-in")).toBeInTheDocument(); - expect(screen.getByTestId("back-to-login")).toBeInTheDocument(); - expect(screen.getByText("Email Change Sign In with token: test-token")).toBeInTheDocument(); - }); - - test("handles missing token", async () => { - const searchParams = {}; - render(await VerifyEmailChangePage({ searchParams })); - - expect(screen.getByTestId("form-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("email-change-sign-in")).toBeInTheDocument(); - expect(screen.getByTestId("back-to-login")).toBeInTheDocument(); - expect(screen.getByText("Email Change Sign In with token:")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/billing/components/pricing-table.test.tsx b/apps/web/modules/ee/billing/components/pricing-table.test.tsx deleted file mode 100644 index 78ecaac999..0000000000 --- a/apps/web/modules/ee/billing/components/pricing-table.test.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { useState } from "react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TOrganizationBillingPeriod } from "@formbricks/types/organizations"; -import { PricingTable } from "./pricing-table"; - -// Mock the env module -vi.mock("@/lib/env", () => ({ - env: { - IS_FORMBRICKS_CLOUD: "0", - NODE_ENV: "test", - }, -})); - -// Mock the useRouter hook -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - push: vi.fn(), - }), -})); - -// Mock the actions module -vi.mock("@/modules/ee/billing/actions", () => { - const mockDate = new Date("2024-03-15T00:00:00.000Z"); - return { - isSubscriptionCancelledAction: vi.fn(() => Promise.resolve({ data: { date: mockDate } })), - manageSubscriptionAction: vi.fn(() => Promise.resolve({ data: null })), - upgradePlanAction: vi.fn(() => Promise.resolve({ data: null })), - }; -}); - -// Mock the useTranslate hook -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -describe("PricingTable", () => { - afterEach(() => { - cleanup(); - }); - - test("should display a 'Cancelling' badge with the correct date if the subscription is being cancelled", async () => { - const mockOrganization = { - id: "org-123", - name: "Test Organization", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - plan: "free", - period: "yearly", - periodStart: new Date(), - stripeCustomerId: null, - limits: { - monthly: { - responses: 100, - miu: 100, - }, - projects: 1, - }, - }, - isAIEnabled: false, - }; - - const mockStripePriceLookupKeys = { - STARTUP_MONTHLY: "startup_monthly", - STARTUP_YEARLY: "startup_yearly", - SCALE_MONTHLY: "scale_monthly", - SCALE_YEARLY: "scale_yearly", - }; - - const mockProjectFeatureKeys = { - FREE: "free", - STARTUP: "startup", - SCALE: "scale", - ENTERPRISE: "enterprise", - }; - - render( - - ); - - const expectedDate = new Date("2024-03-15T00:00:00.000Z").toLocaleDateString("en-US", { - weekday: "short", - year: "numeric", - month: "short", - day: "numeric", - timeZone: "UTC", - }); - const cancellingBadge = await screen.findByText(`Cancelling: ${expectedDate}`); - expect(cancellingBadge).toBeInTheDocument(); - }); - - test("billing period toggle buttons have correct aria-pressed attributes", async () => { - const MockPricingTable = () => { - const [planPeriod, setPlanPeriod] = useState("yearly"); - - const mockOrganization = { - id: "org-123", - name: "Test Organization", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - plan: "free", - period: "yearly", - periodStart: new Date(), - stripeCustomerId: null, - limits: { - monthly: { - responses: 100, - miu: 100, - }, - projects: 1, - }, - }, - isAIEnabled: false, - }; - - const mockStripePriceLookupKeys = { - STARTUP_MONTHLY: "startup_monthly", - STARTUP_YEARLY: "startup_yearly", - SCALE_MONTHLY: "scale_monthly", - SCALE_YEARLY: "scale_yearly", - }; - - const mockProjectFeatureKeys = { - FREE: "free", - STARTUP: "startup", - SCALE: "scale", - ENTERPRISE: "enterprise", - }; - - const handleMonthlyToggle = (period: TOrganizationBillingPeriod) => { - setPlanPeriod(period); - }; - - return ( - - ); - }; - - render(); - - const monthlyButton = screen.getByText("environments.settings.billing.monthly"); - const yearlyButton = screen.getByText("environments.settings.billing.annually"); - - expect(yearlyButton).toHaveAttribute("aria-pressed", "true"); - expect(monthlyButton).toHaveAttribute("aria-pressed", "false"); - - fireEvent.click(monthlyButton); - - expect(yearlyButton).toHaveAttribute("aria-pressed", "false"); - expect(monthlyButton).toHaveAttribute("aria-pressed", "true"); - }); -}); diff --git a/apps/web/modules/ee/contacts/[contactId]/components/attributes-section.test.tsx b/apps/web/modules/ee/contacts/[contactId]/components/attributes-section.test.tsx deleted file mode 100644 index ae15540b00..0000000000 --- a/apps/web/modules/ee/contacts/[contactId]/components/attributes-section.test.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TResponse } from "@formbricks/types/responses"; -import { getResponsesByContactId } from "@/lib/response/service"; -import { getContactAttributes } from "@/modules/ee/contacts/lib/contact-attributes"; -import { getContact } from "@/modules/ee/contacts/lib/contacts"; -import { AttributesSection } from "./attributes-section"; - -vi.mock("@/lib/response/service", () => ({ - getResponsesByContactId: vi.fn(), -})); - -vi.mock("@/modules/ee/contacts/lib/contact-attributes", () => ({ - getContactAttributes: vi.fn(), -})); - -vi.mock("@/modules/ee/contacts/lib/contacts", () => ({ - getContact: vi.fn(), -})); - -const mockGetTranslate = vi.fn(async () => (key: string) => key); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: () => mockGetTranslate(), -})); - -describe("AttributesSection", () => { - afterEach(() => { - cleanup(); - }); - - test("renders contact attributes correctly", async () => { - const mockContact = { - id: "test-contact-id", - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "test-env", - }; - - const mockAttributes = { - email: "test@example.com", - language: "en", - userId: "test-user", - name: "Test User", - }; - - const mockResponses: TResponse[] = [ - { - id: "response1", - createdAt: new Date(), - updatedAt: new Date(), - surveyId: "survey1", - finished: true, - data: {}, - meta: {}, - ttc: {}, - variables: {}, - contactAttributes: {}, - singleUseId: null, - contact: null, - language: null, - tags: [], - endingId: null, - displayId: null, - }, - { - id: "response2", - createdAt: new Date(), - updatedAt: new Date(), - surveyId: "survey1", - finished: true, - data: {}, - meta: {}, - ttc: {}, - variables: {}, - contactAttributes: {}, - singleUseId: null, - contact: null, - language: null, - tags: [], - endingId: null, - displayId: null, - }, - ]; - - vi.mocked(getContact).mockResolvedValue(mockContact); - vi.mocked(getContactAttributes).mockResolvedValue(mockAttributes); - vi.mocked(getResponsesByContactId).mockResolvedValue(mockResponses); - - const { container } = render(await AttributesSection({ contactId: "test-contact-id" })); - - expect(screen.getByText("common.attributes")).toBeInTheDocument(); - expect(screen.getByText("test@example.com")).toBeInTheDocument(); - expect(screen.getByText("en")).toBeInTheDocument(); - expect(screen.getByText("test-user")).toBeInTheDocument(); - expect(screen.getByText("test-contact-id")).toBeInTheDocument(); - expect(screen.getByText("Test User")).toBeInTheDocument(); - expect(screen.getByText("2")).toBeInTheDocument(); - }); - - test("shows not provided text when attributes are missing", async () => { - const mockContact = { - id: "test-contact-id", - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "test-env", - }; - - const mockAttributes = { - email: "", - language: "", - userId: "", - }; - - const mockResponses: TResponse[] = []; - - vi.mocked(getContact).mockResolvedValue(mockContact); - vi.mocked(getContactAttributes).mockResolvedValue(mockAttributes); - vi.mocked(getResponsesByContactId).mockResolvedValue(mockResponses); - - render(await AttributesSection({ contactId: "test-contact-id" })); - - const notProvidedElements = screen.getAllByText("environments.contacts.not_provided"); - expect(notProvidedElements).toHaveLength(3); - }); -}); diff --git a/apps/web/modules/ee/contacts/[contactId]/components/delete-contact-button.test.tsx b/apps/web/modules/ee/contacts/[contactId]/components/delete-contact-button.test.tsx deleted file mode 100644 index 76d2f60378..0000000000 --- a/apps/web/modules/ee/contacts/[contactId]/components/delete-contact-button.test.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { useTranslate } from "@tolgee/react"; -import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"; -import { useRouter } from "next/navigation"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { deleteContactAction } from "@/modules/ee/contacts/actions"; -import { DeleteContactButton } from "./delete-contact-button"; - -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: vi.fn(), -})); - -vi.mock("@/modules/ee/contacts/actions", () => ({ - deleteContactAction: vi.fn(), -})); - -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn((result: any) => { - if (result.serverError) return result.serverError; - if (result.validationErrors) { - return Object.entries(result.validationErrors) - .map(([key, value]: [string, any]) => { - if (key === "_errors") return Array.isArray(value) ? value.join(", ") : ""; - return `${key}${value?._errors?.join(", ") || ""}`; - }) - .join("\n"); - } - return "Unknown error"; - }), -})); - -describe("DeleteContactButton", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockRouter: Partial = { - refresh: vi.fn(), - push: vi.fn(), - }; - - const mockTranslate = { - t: vi.fn((key) => key), - isLoading: false, - }; - - beforeEach(() => { - vi.mocked(useRouter).mockReturnValue(mockRouter as AppRouterInstance); - vi.mocked(useTranslate).mockReturnValue(mockTranslate); - }); - - test("should not render when isReadOnly is true", () => { - render(); - - expect(screen.queryByRole("button")).not.toBeInTheDocument(); - }); - - test("should render delete button when isReadOnly is false", () => { - render(); - - expect(screen.getByRole("button")).toBeInTheDocument(); - }); - - test("should open delete dialog when clicking delete button", async () => { - const user = userEvent.setup(); - render(); - - await user.click(screen.getByRole("button")); - expect(screen.getByText("common.delete person")).toBeInTheDocument(); - }); - - test("should handle successful contact deletion", async () => { - const user = userEvent.setup(); - vi.mocked(deleteContactAction).mockResolvedValue({ - data: { - environmentId: "env-123", - id: "contact-123", - createdAt: new Date(), - updatedAt: new Date(), - }, - }); - - render(); - - await user.click(screen.getByRole("button")); - await user.click(screen.getByText("common.delete")); - - expect(deleteContactAction).toHaveBeenCalledWith({ contactId: "contact-123" }); - expect(mockRouter.refresh).toHaveBeenCalled(); - expect(mockRouter.push).toHaveBeenCalledWith("/environments/env-123/contacts"); - expect(toast.success).toHaveBeenCalledWith("environments.contacts.contact_deleted_successfully"); - }); - - test("should handle failed contact deletion", async () => { - const user = userEvent.setup(); - const errorResponse = { - serverError: "Failed to delete contact", - }; - vi.mocked(deleteContactAction).mockResolvedValue(errorResponse); - - render(); - - await user.click(screen.getByRole("button")); - await user.click(screen.getByText("common.delete")); - - expect(deleteContactAction).toHaveBeenCalledWith({ contactId: "contact-123" }); - expect(toast.error).toHaveBeenCalledWith("Failed to delete contact"); - }); -}); diff --git a/apps/web/modules/ee/contacts/[contactId]/components/response-feed.test.tsx b/apps/web/modules/ee/contacts/[contactId]/components/response-feed.test.tsx deleted file mode 100644 index f2dab481a3..0000000000 --- a/apps/web/modules/ee/contacts/[contactId]/components/response-feed.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TResponse } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TTag } from "@formbricks/types/tags"; -import { TUser, TUserLocale } from "@formbricks/types/user"; -import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team"; -import { ResponseFeed } from "./response-feed"; - -// Mock the hooks and components -vi.mock("@/lib/membership/hooks/useMembershipRole", () => ({ - useMembershipRole: () => ({ - membershipRole: "owner", - }), -})); - -vi.mock("@/lib/utils/recall", () => ({ - replaceHeadlineRecall: (survey: TSurvey) => survey, -})); - -vi.mock("@/modules/analysis/components/SingleResponseCard", () => ({ - SingleResponseCard: ({ response }: { response: TResponse }) => ( -
    {response.id}
    - ), -})); - -vi.mock("@/modules/ui/components/empty-space-filler", () => ({ - EmptySpaceFiller: () =>
    No responses
    , -})); - -describe("ResponseFeed", () => { - afterEach(() => { - cleanup(); - }); - - const mockProps = { - surveys: [ - { - id: "survey1", - name: "Test Survey", - environmentId: "env1", - questions: [], - createdAt: new Date(), - updatedAt: new Date(), - type: "link", - createdBy: null, - status: "draft", - autoClose: null, - triggers: [], - redirectUrl: null, - recontactDays: null, - welcomeCard: { - enabled: false, - headline: "", - subheader: "", - }, - displayLimit: null, - autoComplete: null, - productOverwrites: null, - styling: null, - pin: null, - endings: [], - hiddenFields: {}, - variables: [], - followUps: [], - delay: 0, - displayPercentage: 100, - surveyClosedMessage: "", - singleUse: { - enabled: false, - heading: "", - subheading: "", - }, - attributeFilters: [], - responseCount: 0, - displayOption: "displayOnce", - recurring: { - enabled: false, - frequency: 0, - }, - language: "en", - isDraft: true, - } as unknown as TSurvey, - ], - user: { - id: "user1", - } as TUser, - responses: [ - { - id: "response1", - surveyId: "survey1", - } as TResponse, - ], - environment: { - id: "env1", - } as TEnvironment, - environmentTags: [] as TTag[], - locale: "en" as TUserLocale, - projectPermission: null as TTeamPermission | null, - }; - - test("renders empty state when no responses", () => { - render(); - expect(screen.getByTestId("empty-space-filler")).toBeInTheDocument(); - }); - - test("renders response cards when responses exist", () => { - render(); - expect(screen.getByTestId("single-response-card")).toBeInTheDocument(); - expect(screen.getByText("response1")).toBeInTheDocument(); - }); - - test("updates responses when deleteResponses is called", () => { - const { rerender } = render(); - expect(screen.getByText("response1")).toBeInTheDocument(); - - // Simulate response deletion - rerender(); - expect(screen.getByTestId("empty-space-filler")).toBeInTheDocument(); - }); - - test("updates single response when updateResponse is called", () => { - const updatedResponse = { - ...mockProps.responses[0], - id: "response1-updated", - } as TResponse; - - const { rerender } = render(); - expect(screen.getByText("response1")).toBeInTheDocument(); - - // Simulate response update - rerender(); - expect(screen.getByText("response1-updated")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/contacts/[contactId]/components/response-section.test.tsx b/apps/web/modules/ee/contacts/[contactId]/components/response-section.test.tsx deleted file mode 100644 index 123ddc8d2f..0000000000 --- a/apps/web/modules/ee/contacts/[contactId]/components/response-section.test.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { TFnType } from "@tolgee/react"; -import { getServerSession } from "next-auth"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TTag } from "@formbricks/types/tags"; -import { getProjectByEnvironmentId } from "@/lib/project/service"; -import { getResponsesByContactId } from "@/lib/response/service"; -import { getSurveys } from "@/lib/survey/service"; -import { getUser } from "@/lib/user/service"; -import { findMatchingLocale } from "@/lib/utils/locale"; -import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; -import { getTranslate } from "@/tolgee/server"; -import { ResponseSection } from "./response-section"; - -vi.mock("@/lib/project/service", () => ({ - getProjectByEnvironmentId: vi.fn(), -})); - -vi.mock("@/lib/response/service", () => ({ - getResponsesByContactId: vi.fn(), -})); - -vi.mock("@/lib/survey/service", () => ({ - getSurveys: vi.fn(), -})); - -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); - -vi.mock("@/lib/utils/locale", () => ({ - findMatchingLocale: vi.fn(), -})); - -vi.mock("@/modules/auth/lib/authOptions", () => ({ - authOptions: {}, -})); - -vi.mock("@/modules/ee/teams/lib/roles", () => ({ - getProjectPermissionByUserId: vi.fn(), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(), -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -vi.mock("./response-timeline", () => ({ - ResponseTimeline: () =>
    Response Timeline
    , -})); - -describe("ResponseSection", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockEnvironment: TEnvironment = { - id: "env1", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - projectId: "project1", - appSetupCompleted: true, - }; - - const mockProps = { - environment: mockEnvironment, - contactId: "contact1", - environmentTags: [] as TTag[], - }; - - test("renders ResponseTimeline component when all data is available", async () => { - const mockSession = { - user: { id: "user1" }, - }; - - const mockUser = { - id: "user1", - name: "Test User", - email: "test@example.com", - }; - - const mockResponses = [ - { - id: "response1", - surveyId: "survey1", - }, - ]; - - const mockSurveys = [ - { - id: "survey1", - name: "Test Survey", - }, - ]; - - const mockProject = { - id: "project1", - }; - - const mockProjectPermission = { - role: "owner", - }; - - vi.mocked(getServerSession).mockResolvedValue(mockSession as any); - vi.mocked(getUser).mockResolvedValue(mockUser as any); - vi.mocked(getResponsesByContactId).mockResolvedValue(mockResponses as any); - vi.mocked(getSurveys).mockResolvedValue(mockSurveys as any); - vi.mocked(getProjectByEnvironmentId).mockResolvedValue(mockProject as any); - vi.mocked(getProjectPermissionByUserId).mockResolvedValue(mockProjectPermission as any); - vi.mocked(findMatchingLocale).mockResolvedValue("en-US"); - vi.mocked(getTranslate).mockResolvedValue({ - t: (key: string) => key, - } as any); - - const { container } = render(await ResponseSection(mockProps)); - expect(screen.getByTestId("response-timeline")).toBeInTheDocument(); - }); - - test("throws error when session is not found", async () => { - vi.mocked(getServerSession).mockResolvedValue(null); - vi.mocked(getTranslate).mockResolvedValue(((key: string) => key) as TFnType); - - await expect(ResponseSection(mockProps)).rejects.toThrow("common.session_not_found"); - }); - - test("throws error when user is not found", async () => { - const mockSession = { - user: { id: "user1" }, - }; - - vi.mocked(getServerSession).mockResolvedValue(mockSession as any); - vi.mocked(getUser).mockResolvedValue(null); - vi.mocked(getTranslate).mockResolvedValue(((key: string) => key) as TFnType); - - await expect(ResponseSection(mockProps)).rejects.toThrow("common.user_not_found"); - }); - - test("throws error when no responses are found", async () => { - const mockSession = { - user: { id: "user1" }, - }; - - const mockUser = { - id: "user1", - name: "Test User", - email: "test@example.com", - }; - - vi.mocked(getServerSession).mockResolvedValue(mockSession as any); - vi.mocked(getUser).mockResolvedValue(mockUser as any); - vi.mocked(getResponsesByContactId).mockResolvedValue(null); - vi.mocked(getTranslate).mockResolvedValue(((key: string) => key) as TFnType); - - await expect(ResponseSection(mockProps)).rejects.toThrow("environments.contacts.no_responses_found"); - }); - - test("throws error when project is not found", async () => { - const mockSession = { - user: { id: "user1" }, - }; - - const mockUser = { - id: "user1", - name: "Test User", - email: "test@example.com", - }; - - const mockResponses = [ - { - id: "response1", - surveyId: "survey1", - }, - ]; - - vi.mocked(getServerSession).mockResolvedValue(mockSession as any); - vi.mocked(getUser).mockResolvedValue(mockUser as any); - vi.mocked(getResponsesByContactId).mockResolvedValue(mockResponses as any); - vi.mocked(getProjectByEnvironmentId).mockResolvedValue(null); - vi.mocked(getTranslate).mockResolvedValue(((key: string) => key) as TFnType); - - await expect(ResponseSection(mockProps)).rejects.toThrow("common.project_not_found"); - }); -}); diff --git a/apps/web/modules/ee/contacts/[contactId]/components/response-timeline.test.tsx b/apps/web/modules/ee/contacts/[contactId]/components/response-timeline.test.tsx deleted file mode 100644 index 14e77b2d7b..0000000000 --- a/apps/web/modules/ee/contacts/[contactId]/components/response-timeline.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { useTranslate } from "@tolgee/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TResponse } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TTag } from "@formbricks/types/tags"; -import { TUser } from "@formbricks/types/user"; -import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team"; -import { ResponseTimeline } from "./response-timeline"; - -vi.mock("@tolgee/react", () => ({ - useTranslate: vi.fn(), -})); - -vi.mock("./response-feed", () => ({ - ResponseFeed: () =>
    Response Feed
    , -})); - -describe("ResponseTimeline", () => { - afterEach(() => { - cleanup(); - }); - - const mockUser: TUser = { - id: "user1", - name: "Test User", - createdAt: new Date(), - updatedAt: new Date(), - objective: null, - role: "founder", - email: "test@example.com", - emailVerified: new Date(), - twoFactorEnabled: false, - identityProvider: "email", - isActive: true, - notificationSettings: { - alert: {}, - }, - locale: "en-US", - lastLoginAt: new Date(), - }; - - const mockResponse: TResponse = { - id: "response1", - createdAt: new Date(), - updatedAt: new Date(), - surveyId: "survey1", - contact: null, - contactAttributes: null, - finished: true, - data: {}, - meta: {}, - variables: {}, - singleUseId: null, - language: "en", - ttc: {}, - tags: [], - }; - - const mockEnvironment: TEnvironment = { - id: "env1", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", - projectId: "project1", - appSetupCompleted: true, - }; - - const mockProps = { - surveys: [] as TSurvey[], - user: mockUser, - responses: [mockResponse, { ...mockResponse, id: "response2" }], - environment: mockEnvironment, - environmentTags: [] as TTag[], - locale: "en-US" as const, - projectPermission: null as TTeamPermission | null, - }; - - test("renders the component with responses title", () => { - vi.mocked(useTranslate).mockReturnValue({ - t: (key: string) => key, - } as any); - - render(); - expect(screen.getByText("common.responses")).toBeInTheDocument(); - }); - - test("renders ResponseFeed component", () => { - vi.mocked(useTranslate).mockReturnValue({ - t: (key: string) => key, - } as any); - - render(); - expect(screen.getByTestId("response-feed")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/add-filter-modal.test.tsx b/apps/web/modules/ee/contacts/segments/components/add-filter-modal.test.tsx deleted file mode 100644 index e12dbac82d..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/add-filter-modal.test.tsx +++ /dev/null @@ -1,705 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -// Added waitFor -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { TSegment } from "@formbricks/types/segment"; -import { AddFilterModal } from "@/modules/ee/contacts/segments/components/add-filter-modal"; - -// Mock the Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ - children, - open, - onOpenChange, - }: { - children: React.ReactNode; - open: boolean; - onOpenChange: (open: boolean) => void; - }) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ - children, - className, - hideCloseButton, - }: { - children: React.ReactNode; - className?: string; - hideCloseButton?: boolean; - }) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -// Mock the Input component -vi.mock("@/modules/ui/components/input", () => ({ - Input: ({ placeholder, onChange, autoFocus }: any) => ( - - ), -})); - -// Mock the TabBar component -vi.mock("@/modules/ui/components/tab-bar", () => ({ - TabBar: ({ tabs, activeId, setActiveId }: any) => ( -
    - {tabs.map((tab: any) => ( - - ))} -
    - ), -})); - -// Mock the useTranslate hook -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - const translations = { - "common.add_filter": "Add Filter", - "common.all": "All", - "environments.segments.person_and_attributes": "Person & Attributes", - "common.segments": "Segments", - "environments.segments.devices": "Devices", - "environments.segments.phone": "Phone", - "environments.segments.desktop": "Desktop", - "environments.segments.no_filters_yet": "No filters yet", - "environments.segments.no_segments_yet": "No segments yet", - "environments.segments.no_attributes_yet": "No attributes yet", - "common.user_id": "userId", - "common.person": "Person", - "common.attributes": "Attributes", - }; - return translations[key] || key; - }, - }), -})); - -// Mock createId -vi.mock("@paralleldrive/cuid2", () => ({ - createId: vi.fn(() => "mockCuid"), -})); - -// Mock the AttributeTabContent component -vi.mock("./attribute-tab-content", () => ({ - default: ({ contactAttributeKeys, onAddFilter, setOpen, handleAddFilter }: any) => ( -
    -

    Person

    - -
    -

    Attributes

    - {contactAttributeKeys.length === 0 ? ( -

    No attributes yet

    - ) : ( - contactAttributeKeys.map((attr: any) => ( - - )) - )} -
    - ), -})); - -// Mock the FilterButton component -vi.mock("./filter-button", () => ({ - default: ({ icon, label, onClick, onKeyDown, tabIndex = 0, ...props }: any) => ( - - ), -})); - -const mockContactAttributeKeys: TContactAttributeKey[] = [ - { - id: "attr1", - key: "email", - name: "Email Address", - environmentId: "env1", - } as unknown as TContactAttributeKey, - { id: "attr2", key: "plan", name: "Plan Type", environmentId: "env1" } as unknown as TContactAttributeKey, -]; - -const mockSegments: TSegment[] = [ - { - id: "seg1", - title: "Active Users", - description: "Users active in the last 7 days", - isPrivate: false, - filters: [], - environmentId: "env1", - surveys: [], - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: "seg2", - title: "Paying Customers", - description: "Users with plan type 'paid'", - isPrivate: false, - filters: [], - environmentId: "env1", - surveys: [], - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: "seg3", - title: "Private Segment", - description: "This is private", - isPrivate: true, - filters: [], - environmentId: "env1", - surveys: [], - createdAt: new Date(), - updatedAt: new Date(), - }, -]; - -// Helper function to check filter payload -const expectFilterPayload = ( - callArgs: any[], - expectedType: string, - expectedRoot: object, - expectedQualifierOp: string, - expectedValue: string | undefined -) => { - expect(callArgs[0]).toEqual( - expect.objectContaining({ - id: "mockCuid", - connector: "and", - resource: expect.objectContaining({ - id: "mockCuid", - root: expect.objectContaining({ type: expectedType, ...expectedRoot }), - qualifier: expect.objectContaining({ operator: expectedQualifierOp }), - value: expectedValue, - }), - }) - ); -}; - -describe("AddFilterModal", () => { - let onAddFilter: ReturnType; - let setOpen: ReturnType; - const user = userEvent.setup(); - - beforeEach(() => { - onAddFilter = vi.fn(); - setOpen = vi.fn(); - vi.clearAllMocks(); // Clear mocks before each test - }); - - afterEach(() => { - cleanup(); - }); - - // --- Existing Tests (Rendering, Search, Tab Switching) --- - test("renders correctly when open", () => { - render( - - ); - // ... assertions ... - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-content")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Add Filter"); - expect(screen.getByTestId("search-input")).toBeInTheDocument(); - expect(screen.getByPlaceholderText("Browse filters...")).toBeInTheDocument(); - expect(screen.getByTestId("tab-all")).toHaveTextContent("All (Active)"); - expect(screen.getByText("Email Address")).toBeInTheDocument(); - expect(screen.getByText("Plan Type")).toBeInTheDocument(); - expect(screen.getByText("userId")).toBeInTheDocument(); - expect(screen.getByText("Active Users")).toBeInTheDocument(); - expect(screen.getByText("Paying Customers")).toBeInTheDocument(); - expect(screen.queryByText("Private Segment")).not.toBeInTheDocument(); - expect(screen.getByText("Phone")).toBeInTheDocument(); - expect(screen.getByText("Desktop")).toBeInTheDocument(); - }); - - test("does not render when closed", () => { - render( - - ); - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - expect(screen.queryByPlaceholderText("Browse filters...")).not.toBeInTheDocument(); - }); - - test("filters items based on search input in 'All' tab", async () => { - render( - - ); - const searchInput = screen.getByPlaceholderText("Browse filters..."); - await user.type(searchInput, "Email"); - // ... assertions ... - expect(screen.getByText("Email Address")).toBeInTheDocument(); - expect(screen.queryByText("Plan Type")).not.toBeInTheDocument(); - }); - - test("switches tabs and displays correct content", async () => { - render( - - ); - // Switch to Attributes tab - const attributesTabButton = screen.getByTestId("tab-attributes"); - await user.click(attributesTabButton); - // ... assertions ... - expect(attributesTabButton).toHaveTextContent("Person & Attributes (Active)"); - expect(screen.getByText("userId")).toBeInTheDocument(); - - // Switch to Segments tab - const segmentsTabButton = screen.getByTestId("tab-segments"); - await user.click(segmentsTabButton); - // ... assertions ... - expect(segmentsTabButton).toHaveTextContent("Segments (Active)"); - expect(screen.getByText("Active Users")).toBeInTheDocument(); - - // Switch to Devices tab - const devicesTabButton = screen.getByTestId("tab-devices"); - await user.click(devicesTabButton); - // ... assertions ... - expect(devicesTabButton).toHaveTextContent("Devices (Active)"); - expect(screen.getByText("Phone")).toBeInTheDocument(); - }); - - // --- Click and Keydown Tests --- - - const testFilterInteraction = async ( - elementFinder: () => HTMLElement, - expectedType: string, - expectedRoot: object, - expectedQualifierOp: string, - expectedValue: string | undefined - ) => { - // Test Click - const elementClick = elementFinder(); - await user.click(elementClick); - expect(onAddFilter).toHaveBeenCalledTimes(1); - expectFilterPayload( - onAddFilter.mock.calls[0], - expectedType, - expectedRoot, - expectedQualifierOp, - expectedValue - ); - expect(setOpen).toHaveBeenCalledWith(false); - onAddFilter.mockClear(); - setOpen.mockClear(); - - // Test Enter Keydown - const elementEnter = elementFinder(); - elementEnter.focus(); - await user.keyboard("{Enter}"); - expect(onAddFilter).toHaveBeenCalledTimes(1); - expectFilterPayload( - onAddFilter.mock.calls[0], - expectedType, - expectedRoot, - expectedQualifierOp, - expectedValue - ); - expect(setOpen).toHaveBeenCalledWith(false); - onAddFilter.mockClear(); - setOpen.mockClear(); - - // Test Space Keydown - const elementSpace = elementFinder(); - elementSpace.focus(); - await user.keyboard(" "); - expect(onAddFilter).toHaveBeenCalledTimes(1); - expectFilterPayload( - onAddFilter.mock.calls[0], - expectedType, - expectedRoot, - expectedQualifierOp, - expectedValue - ); - expect(setOpen).toHaveBeenCalledWith(false); - onAddFilter.mockClear(); - setOpen.mockClear(); - }; - - describe("All Tab Interactions", () => { - beforeEach(() => { - render( - - ); - }); - - test("handles Person (userId) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-person-userId"), - "person", - { personIdentifier: "userId" }, - "equals", - "" - ); - }); - - test("handles Attribute (Email Address) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-attribute-email"), - "attribute", - { contactAttributeKey: "email" }, - "equals", - "" - ); - }); - - test("handles Attribute (Plan Type) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-attribute-plan"), - "attribute", - { contactAttributeKey: "plan" }, - "equals", - "" - ); - }); - - test("handles Segment (Active Users) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-segment-seg1"), - "segment", - { segmentId: "seg1" }, - "userIsIn", - "seg1" - ); - }); - - test("handles Segment (Paying Customers) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-segment-seg2"), - "segment", - { segmentId: "seg2" }, - "userIsIn", - "seg2" - ); - }); - - test("handles Device (Phone) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-device-phone"), - "device", - { deviceType: "phone" }, - "equals", - "phone" - ); - }); - - test("handles Device (Desktop) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-device-desktop"), - "device", - { deviceType: "desktop" }, - "equals", - "desktop" - ); - }); - }); - - describe("Attributes Tab Interactions", () => { - beforeEach(async () => { - render( - - ); - await user.click(screen.getByTestId("tab-attributes")); - await waitFor(() => expect(screen.getByTestId("tab-attributes")).toHaveTextContent("(Active)")); - }); - - test("handles Person (userId) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-person-userId"), - "person", - { personIdentifier: "userId" }, - "equals", - "" - ); - }); - - test("handles Attribute (Email Address) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-attribute-email"), - "attribute", - { contactAttributeKey: "email" }, - "equals", - "" - ); - }); - - test("handles Attribute (Plan Type) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-attribute-plan"), - "attribute", - { contactAttributeKey: "plan" }, - "equals", - "" - ); - }); - }); - - describe("Segments Tab Interactions", () => { - beforeEach(async () => { - render( - - ); - await user.click(screen.getByTestId("tab-segments")); - await waitFor(() => expect(screen.getByTestId("tab-segments")).toHaveTextContent("(Active)")); - }); - - test("handles Segment (Active Users) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-segment-seg1"), - "segment", - { segmentId: "seg1" }, - "userIsIn", - "seg1" - ); - }); - - test("handles Segment (Paying Customers) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-segment-seg2"), - "segment", - { segmentId: "seg2" }, - "userIsIn", - "seg2" - ); - }); - }); - - describe("Devices Tab Interactions", () => { - beforeEach(async () => { - render( - - ); - await user.click(screen.getByTestId("tab-devices")); - await waitFor(() => expect(screen.getByTestId("tab-devices")).toHaveTextContent("(Active)")); - }); - - test("handles Device (Phone) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-device-phone"), - "device", - { deviceType: "phone" }, - "equals", - "phone" - ); - }); - - test("handles Device (Desktop) filter add (click/keydown)", async () => { - await testFilterInteraction( - () => screen.getByTestId("filter-btn-device-desktop"), - "device", - { deviceType: "desktop" }, - "equals", - "desktop" - ); - }); - }); - - // --- Edge Case Tests --- - test("displays 'no attributes yet' message", async () => { - render( - - ); - await user.click(screen.getByTestId("tab-attributes")); - expect(await screen.findByText("No attributes yet")).toBeInTheDocument(); - }); - - test("displays 'no segments yet' message", async () => { - render( - - ); - await user.click(screen.getByTestId("tab-segments")); - expect(await screen.findByText("No segments yet")).toBeInTheDocument(); - }); - - test("displays 'no filters match' message when search yields no results", async () => { - render( - - ); - const searchInput = screen.getByPlaceholderText("Browse filters..."); - await user.type(searchInput, "nonexistentfilter"); - expect(await screen.findByText("No filters yet")).toBeInTheDocument(); - }); - - test("verifies keyboard navigation through filter buttons", async () => { - render( - - ); - - // Get the search input to start tabbing from - const searchInput = screen.getByPlaceholderText("Browse filters..."); - searchInput.focus(); - - // Tab to the first tab button ("all") - await user.tab(); - expect(document.activeElement).toHaveTextContent(/All/); - - // Tab to the second tab button ("attributes") - await user.tab(); - expect(document.activeElement).toHaveTextContent(/Person & Attributes/); - - // Tab to the third tab button ("segments") - await user.tab(); - expect(document.activeElement).toHaveTextContent(/Segments/); - - // Tab to the fourth tab button ("devices") - await user.tab(); - expect(document.activeElement).toHaveTextContent(/Devices/); - - // Tab to the first filter button ("Email Address") - await user.tab(); - expect(document.activeElement).toHaveTextContent("Email Address"); - - // Tab to the second filter button ("Plan Type") - await user.tab(); - expect(document.activeElement).toHaveTextContent("Plan Type"); - - // Tab to the third filter button ("userId") - await user.tab(); - expect(document.activeElement).toHaveTextContent("userId"); - }); - - test("button elements are accessible to screen readers", () => { - render( - - ); - - const buttons = screen.getAllByRole("button"); - expect(buttons.length).toBeGreaterThan(0); // Verify buttons exist - - // Check that buttons are focusable (they should be by default) - buttons.forEach((button) => { - expect(button).not.toHaveAttribute("aria-hidden", "true"); - expect(button).not.toHaveAttribute("tabIndex", "-1"); // Should not be unfocusable - }); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/attribute-tab-content.test.tsx b/apps/web/modules/ee/contacts/segments/components/attribute-tab-content.test.tsx deleted file mode 100644 index a7c5fa8d59..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/attribute-tab-content.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import AttributeTabContent from "./attribute-tab-content"; - -describe("AttributeTabContent", () => { - afterEach(() => { - cleanup(); - }); - - const mockContactAttributeKeys: TContactAttributeKey[] = [ - { id: "attr1", key: "email", name: "Email Address", environmentId: "env1" } as TContactAttributeKey, - { id: "attr2", key: "plan", name: "Plan Type", environmentId: "env1" } as TContactAttributeKey, - ]; - - test("renders person and attribute buttons", () => { - render( - - ); - expect(screen.getByTestId("filter-btn-person-userId")).toBeInTheDocument(); - expect(screen.getByTestId("filter-btn-attribute-email")).toBeInTheDocument(); - expect(screen.getByTestId("filter-btn-attribute-plan")).toBeInTheDocument(); - }); - - test("shows empty state when no attributes", () => { - render( - - ); - expect(screen.getByText(/no_attributes_yet/i)).toBeInTheDocument(); - }); - - test("calls handleAddFilter with correct args for person", async () => { - const handleAddFilter = vi.fn(); - render( - - ); - await userEvent.click(screen.getByTestId("filter-btn-person-userId")); - expect(handleAddFilter).toHaveBeenCalledWith(expect.objectContaining({ type: "person" })); - }); - - test("calls handleAddFilter with correct args for attribute", async () => { - const handleAddFilter = vi.fn(); - render( - - ); - await userEvent.click(screen.getByTestId("filter-btn-attribute-email")); - expect(handleAddFilter).toHaveBeenCalledWith( - expect.objectContaining({ type: "attribute", contactAttributeKey: "email" }) - ); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/create-segment-modal.test.tsx b/apps/web/modules/ee/contacts/segments/components/create-segment-modal.test.tsx deleted file mode 100644 index ced1ecc690..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/create-segment-modal.test.tsx +++ /dev/null @@ -1,346 +0,0 @@ -import { cleanup, render, screen, waitFor, within } from "@testing-library/react"; -// Import within -import userEvent from "@testing-library/user-event"; -// Removed beforeEach -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { TSegment } from "@formbricks/types/segment"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { createSegmentAction } from "@/modules/ee/contacts/segments/actions"; -import { CreateSegmentModal } from "@/modules/ee/contacts/segments/components/create-segment-modal"; - -// Mock dependencies -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn((_) => "Formatted error"), -})); - -vi.mock("@/modules/ee/contacts/segments/actions", () => ({ - createSegmentAction: vi.fn(), -})); - -// Mock child components that are complex or have their own tests -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ - open, - onOpenChange, - children, - }: { - open: boolean; - onOpenChange: (open: boolean) => void; - children: React.ReactNode; - }) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ - children, - className, - disableCloseOnOutsideClick, - }: { - children: React.ReactNode; - className?: string; - disableCloseOnOutsideClick?: boolean; - }) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogFooter: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("./add-filter-modal", () => ({ - AddFilterModal: ({ open, setOpen, onAddFilter }) => - open ? ( -
    - - -
    - ) : null, -})); - -vi.mock("./segment-editor", () => ({ - SegmentEditor: ({ group }) =>
    Filters: {group.length}
    , -})); - -const environmentId = "test-env-id"; -const contactAttributeKeys = [ - { name: "userId", label: "User ID", type: "identifier" } as unknown as TContactAttributeKey, -]; -const segments = [] as unknown as TSegment[]; -const defaultProps = { - environmentId, - contactAttributeKeys, - segments, -}; - -describe("CreateSegmentModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders create button and opens modal on click", async () => { - render(); - const createButton = screen.getByText("common.create_segment"); - expect(createButton).toBeInTheDocument(); - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - - await userEvent.click(createButton); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByText("common.create_segment", { selector: "h2" })).toBeInTheDocument(); // Modal title - }); - - test("closes modal on cancel button click", async () => { - render(); - const createButton = screen.getByText("common.create_segment"); - await userEvent.click(createButton); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - const cancelButton = screen.getByText("common.cancel"); - await userEvent.click(cancelButton); - - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); - - test("updates title and description state on input change", async () => { - render(); - const createButton = screen.getByText("common.create_segment"); - await userEvent.click(createButton); - - const titleInput = screen.getByPlaceholderText("environments.segments.ex_power_users"); - const descriptionInput = screen.getByPlaceholderText( - "environments.segments.ex_fully_activated_recurring_users" - ); - - await userEvent.type(titleInput, "My New Segment"); - await userEvent.type(descriptionInput, "Segment description"); - - expect(titleInput).toHaveValue("My New Segment"); - expect(descriptionInput).toHaveValue("Segment description"); - }); - - test("save button is disabled initially and when title is empty", async () => { - render(); - const createButton = screen.getByText("common.create_segment"); - await userEvent.click(createButton); - - const saveButton = screen.getByText("common.create_segment", { selector: "button[type='submit']" }); - expect(saveButton).toBeDisabled(); - - const titleInput = screen.getByPlaceholderText("environments.segments.ex_power_users"); - await userEvent.type(titleInput, " "); // Empty title - expect(saveButton).toBeDisabled(); - - await userEvent.clear(titleInput); - await userEvent.type(titleInput, "Valid Title"); - expect(saveButton).not.toBeDisabled(); - }); - - test("shows error toast if title is missing on save", async () => { - render(); - const openModalButton = screen.getByRole("button", { name: "common.create_segment" }); - await userEvent.click(openModalButton); - - // Get modal and scope queries - const modal = await screen.findByTestId("dialog"); - - // Find the save button using getByText with a specific selector within the modal - const saveButton = within(modal).getByText("common.create_segment", { - selector: "button[type='submit']", - }); - - // Verify the button is disabled because the title is empty - expect(saveButton).toBeDisabled(); - - // Attempt to click the disabled button (optional, confirms no unexpected action occurs) - await userEvent.click(saveButton); - - // Ensure the action was not called, as the button click should be prevented or the handler check fails early - expect(createSegmentAction).not.toHaveBeenCalled(); - }); - - test("calls createSegmentAction on save with valid data", async () => { - vi.mocked(createSegmentAction).mockResolvedValue({ data: { id: "new-segment-id" } as any }); - render(); - const createButton = screen.getByText("common.create_segment"); - await userEvent.click(createButton); - - // Get modal and scope queries - const modal = await screen.findByTestId("dialog"); - - const titleInput = within(modal).getByPlaceholderText("environments.segments.ex_power_users"); - const descriptionInput = within(modal).getByPlaceholderText( - "environments.segments.ex_fully_activated_recurring_users" - ); - await userEvent.type(titleInput, "Power Users"); - await userEvent.type(descriptionInput, "Active users"); - - // Find the save button within the modal - const saveButton = await within(modal).findByRole("button", { - name: "common.create_segment", - }); - // Button should be enabled: title is valid, filters=[] is valid. - expect(saveButton).not.toBeDisabled(); - await userEvent.click(saveButton); - - await waitFor(() => { - expect(createSegmentAction).toHaveBeenCalledWith({ - title: "Power Users", - description: "Active users", - isPrivate: false, - filters: [], // Expect empty array as no filters were added - environmentId, - surveyId: "", - }); - }); - expect(toast.success).toHaveBeenCalledWith("environments.segments.segment_saved_successfully"); - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); // Modal should close on success - }); - - test("shows error toast if createSegmentAction fails", async () => { - const errorResponse = { error: { message: "API Error" } } as any; // Mock error response - vi.mocked(createSegmentAction).mockResolvedValue(errorResponse); - vi.mocked(getFormattedErrorMessage).mockReturnValue("Formatted API Error"); - - render(); - const createButton = screen.getByText("common.create_segment"); - await userEvent.click(createButton); - - const titleInput = screen.getByPlaceholderText("environments.segments.ex_power_users"); - await userEvent.type(titleInput, "Fail Segment"); - - const saveButton = screen.getByText("common.create_segment", { selector: "button[type='submit']" }); - await userEvent.click(saveButton); - - await waitFor(() => { - expect(createSegmentAction).toHaveBeenCalled(); - }); - expect(getFormattedErrorMessage).toHaveBeenCalledWith(errorResponse); - expect(toast.error).toHaveBeenCalledWith("Formatted API Error"); - expect(screen.getByTestId("dialog")).toBeInTheDocument(); // Modal should stay open on error - }); - - test("shows generic error toast if Zod parsing succeeds during save error handling", async () => { - vi.mocked(createSegmentAction).mockRejectedValue(new Error("Network error")); // Simulate action throwing - - render(); - const openModalButton = screen.getByRole("button", { name: "common.create_segment" }); // Get the button outside the modal first - await userEvent.click(openModalButton); - - // Get the modal element - const modal = await screen.findByTestId("dialog"); - - const titleInput = within(modal).getByPlaceholderText("environments.segments.ex_power_users"); - await userEvent.type(titleInput, "Generic Error Segment"); - - // DO NOT add any filters - segment.filters will remain [] - - // Use findByRole scoped within the modal to wait for the submit button to be enabled - const saveButton = await within(modal).findByRole("button", { - name: "common.create_segment", // Match the accessible name (text content) - // Implicitly waits for the button to not have the 'disabled' attribute - }); - - // Now click the enabled button - await userEvent.click(saveButton); - - // Wait for the expected toast message, implying the action failed and catch block ran - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("common.something_went_wrong_please_try_again"); - }); - - // Now that we know the catch block ran, verify the action was called - expect(createSegmentAction).toHaveBeenCalled(); - expect(screen.getByTestId("dialog")).toBeInTheDocument(); // Modal should stay open - }); - - test("opens AddFilterModal when 'Add Filter' button is clicked", async () => { - render(); - const createButton = screen.getByText("common.create_segment"); - await userEvent.click(createButton); - - expect(screen.queryByTestId("add-filter-modal")).not.toBeInTheDocument(); - const addFilterButton = screen.getByText("common.add_filter"); - await userEvent.click(addFilterButton); - - expect(screen.getByTestId("add-filter-modal")).toBeInTheDocument(); - }); - - test("adds filter when onAddFilter is called from AddFilterModal", async () => { - render(); - const createButton = screen.getByText("common.create_segment"); - await userEvent.click(createButton); - - const segmentEditor = screen.getByTestId("segment-editor"); - expect(segmentEditor).toHaveTextContent("Filters: 0"); - - const addFilterButton = screen.getByText("common.add_filter"); - await userEvent.click(addFilterButton); - - const addMockFilterButton = screen.getByText("Add Mock Filter"); - await userEvent.click(addMockFilterButton); // This calls onAddFilter in the mock - - expect(screen.queryByTestId("add-filter-modal")).not.toBeInTheDocument(); // Modal should close - expect(segmentEditor).toHaveTextContent("Filters: 1"); // Check if filter count increased - }); - - test("adds second filter correctly with default connector", async () => { - render(); - const createButton = screen.getByText("common.create_segment"); - await userEvent.click(createButton); - - const segmentEditor = screen.getByTestId("segment-editor"); - const addFilterButton = screen.getByText("common.add_filter"); - - // Add first filter - await userEvent.click(addFilterButton); - await userEvent.click(screen.getByText("Add Mock Filter")); - expect(segmentEditor).toHaveTextContent("Filters: 1"); - - // Add second filter - await userEvent.click(addFilterButton); - await userEvent.click(screen.getByText("Add Mock Filter")); - expect(segmentEditor).toHaveTextContent("Filters: 2"); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/edit-segment-modal.test.tsx b/apps/web/modules/ee/contacts/segments/components/edit-segment-modal.test.tsx deleted file mode 100644 index df822dce82..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/edit-segment-modal.test.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSegmentWithSurveyNames } from "@formbricks/types/segment"; -import { EditSegmentModal } from "@/modules/ee/contacts/segments/components/edit-segment-modal"; - -// Mock child components -vi.mock("@/modules/ee/contacts/segments/components/segment-settings", () => ({ - SegmentSettings: vi.fn(() =>
    SegmentSettingsMock
    ), -})); -vi.mock("@/modules/ee/contacts/segments/components/segment-activity-tab", () => ({ - SegmentActivityTab: vi.fn(() =>
    SegmentActivityTabMock
    ), -})); - -// Mock the Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ - open, - onOpenChange, - children, - }: { - open: boolean; - onOpenChange: (open: boolean) => void; - children: React.ReactNode; - }) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ - children, - disableCloseOnOutsideClick, - }: { - children: React.ReactNode; - disableCloseOnOutsideClick?: boolean; - }) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -// Mock useTranslate -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - const translations = { - "common.activity": "Activity", - "common.settings": "Settings", - }; - return translations[key] || key; - }, - }), -})); - -// Mock lucide-react -vi.mock("lucide-react", () => ({ - UsersIcon: ({ className }: { className?: string }) => ( - - 👥 - - ), -})); - -const mockSegment = { - id: "seg1", - title: "Test Segment", - description: "This is a test segment", - environmentId: "env1", - surveys: ["Survey 1", "Survey 2"], - filters: [], - isPrivate: false, - createdAt: new Date(), - updatedAt: new Date(), -} as unknown as TSegmentWithSurveyNames; - -const defaultProps = { - environmentId: "env1", - open: true, - setOpen: vi.fn(), - currentSegment: mockSegment, - segments: [], - contactAttributeKeys: [], - isContactsEnabled: true, - isReadOnly: false, -}; - -describe("EditSegmentModal", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - test("renders correctly when open and contacts enabled", () => { - render(); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Test Segment"); - expect(screen.getByTestId("dialog-description")).toHaveTextContent("This is a test segment"); - expect(screen.getByTestId("users-icon")).toBeInTheDocument(); - expect(screen.getByText("Activity")).toBeInTheDocument(); - expect(screen.getByText("Settings")).toBeInTheDocument(); - // Only the first tab (Activity) should be active initially - expect(screen.getByTestId("segment-activity-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("segment-settings")).not.toBeInTheDocument(); - }); - - test("renders correctly when open and contacts disabled", () => { - render(); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Test Segment"); - expect(screen.getByTestId("dialog-description")).toHaveTextContent("This is a test segment"); - expect(screen.getByText("Activity")).toBeInTheDocument(); - expect(screen.getByText("Settings")).toBeInTheDocument(); - expect(screen.getByTestId("segment-activity-tab")).toBeInTheDocument(); - // Settings tab content should not render when contacts are disabled - expect(screen.queryByTestId("segment-settings")).not.toBeInTheDocument(); - }); - - test("does not render when open is false", () => { - render(); - - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - expect(screen.queryByText("Test Segment")).not.toBeInTheDocument(); - expect(screen.queryByText("Activity")).not.toBeInTheDocument(); - expect(screen.queryByText("Settings")).not.toBeInTheDocument(); - }); - - test("switches tabs correctly", async () => { - const user = userEvent.setup(); - render(); - - // Initially shows activity tab (first tab is active) - expect(screen.getByTestId("segment-activity-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("segment-settings")).not.toBeInTheDocument(); - - // Click settings tab - const settingsTab = screen.getByText("Settings"); - await user.click(settingsTab); - - // Now shows settings tab content - expect(screen.queryByTestId("segment-activity-tab")).not.toBeInTheDocument(); - expect(screen.getByTestId("segment-settings")).toBeInTheDocument(); - - // Click activity tab again - const activityTab = screen.getByText("Activity"); - await user.click(activityTab); - - // Back to activity tab content - expect(screen.getByTestId("segment-activity-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("segment-settings")).not.toBeInTheDocument(); - }); - - test("resets to first tab when modal is reopened", async () => { - const user = userEvent.setup(); - const { rerender } = render(); - - // Switch to settings tab - const settingsTab = screen.getByText("Settings"); - await user.click(settingsTab); - expect(screen.getByTestId("segment-settings")).toBeInTheDocument(); - - // Close modal - rerender(); - - // Reopen modal - rerender(); - - // Should be back to activity tab (first tab) - expect(screen.getByTestId("segment-activity-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("segment-settings")).not.toBeInTheDocument(); - }); - - test("handles segment without description", () => { - const segmentWithoutDescription = { ...mockSegment, description: "" }; - render(); - - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Test Segment"); - expect(screen.getByTestId("dialog-description")).toHaveTextContent(""); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/filter-button.test.tsx b/apps/web/modules/ee/contacts/segments/components/filter-button.test.tsx deleted file mode 100644 index 2eda1dbeb8..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/filter-button.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import FilterButton from "./filter-button"; - -describe("FilterButton", () => { - afterEach(() => { - cleanup(); - }); - - test("renders icon and label", () => { - render( - icon} label="Test Label" onClick={() => {}} /> - ); - expect(screen.getByTestId("icon")).toBeInTheDocument(); - expect(screen.getByText("Test Label")).toBeInTheDocument(); - }); - - test("calls onClick when clicked", async () => { - const onClick = vi.fn(); - render(} label="Click Me" onClick={onClick} />); - const button = screen.getByRole("button"); - await userEvent.click(button); - expect(onClick).toHaveBeenCalled(); - }); - - test("calls onKeyDown when Enter or Space is pressed", async () => { - const onKeyDown = vi.fn(); - render(} label="Key Test" onClick={() => {}} onKeyDown={onKeyDown} />); - const button = screen.getByRole("button"); - button.focus(); - await userEvent.keyboard("{Enter}"); - expect(onKeyDown).toHaveBeenCalled(); - onKeyDown.mockClear(); - await userEvent.keyboard(" "); - expect(onKeyDown).toHaveBeenCalled(); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/segment-activity-tab.test.tsx b/apps/web/modules/ee/contacts/segments/components/segment-activity-tab.test.tsx deleted file mode 100644 index 2931ec3570..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/segment-activity-tab.test.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSegment } from "@formbricks/types/segment"; -import { convertDateTimeStringShort } from "@/lib/time"; -import { SegmentActivityTab } from "@/modules/ee/contacts/segments/components/segment-activity-tab"; - -const mockSegmentBase: TSegment & { activeSurveys: string[]; inactiveSurveys: string[] } = { - id: "seg123", - title: "Test Segment", - description: "A segment for testing", - environmentId: "env456", - filters: [], - isPrivate: false, - surveys: [], - createdAt: new Date("2024-01-01T10:00:00.000Z"), - updatedAt: new Date("2024-01-02T11:30:00.000Z"), - activeSurveys: [], - inactiveSurveys: [], -}; - -describe("SegmentActivityTab", () => { - afterEach(() => { - cleanup(); - }); - - test("renders correctly with active and inactive surveys", () => { - const segmentWithSurveys = { - ...mockSegmentBase, - activeSurveys: ["Active Survey 1", "Active Survey 2"], - inactiveSurveys: ["Inactive Survey 1"], - }; - render(); - - expect(screen.getByText("common.active_surveys")).toBeInTheDocument(); - expect(screen.getByText("Active Survey 1")).toBeInTheDocument(); - expect(screen.getByText("Active Survey 2")).toBeInTheDocument(); - - expect(screen.getByText("common.inactive_surveys")).toBeInTheDocument(); - expect(screen.getByText("Inactive Survey 1")).toBeInTheDocument(); - - expect(screen.getByText("common.created_at")).toBeInTheDocument(); - expect( - screen.getByText(convertDateTimeStringShort(segmentWithSurveys.createdAt.toString())) - ).toBeInTheDocument(); - expect(screen.getByText("common.updated_at")).toBeInTheDocument(); - expect( - screen.getByText(convertDateTimeStringShort(segmentWithSurveys.updatedAt.toString())) - ).toBeInTheDocument(); - expect(screen.getByText("environments.segments.segment_id")).toBeInTheDocument(); - expect(screen.getByText(segmentWithSurveys.id)).toBeInTheDocument(); - }); - - test("renders correctly with only active surveys", () => { - const segmentOnlyActive = { - ...mockSegmentBase, - activeSurveys: ["Active Survey Only"], - inactiveSurveys: [], - }; - render(); - - expect(screen.getByText("common.active_surveys")).toBeInTheDocument(); - expect(screen.getByText("Active Survey Only")).toBeInTheDocument(); - - expect(screen.getByText("common.inactive_surveys")).toBeInTheDocument(); - // Check for the placeholder when no inactive surveys exist - const inactiveSurveyElements = screen.queryAllByText("-"); - expect(inactiveSurveyElements.length).toBeGreaterThan(0); // Should find at least one '-' - - expect( - screen.getByText(convertDateTimeStringShort(segmentOnlyActive.createdAt.toString())) - ).toBeInTheDocument(); - expect( - screen.getByText(convertDateTimeStringShort(segmentOnlyActive.updatedAt.toString())) - ).toBeInTheDocument(); - expect(screen.getByText(segmentOnlyActive.id)).toBeInTheDocument(); - }); - - test("renders correctly with only inactive surveys", () => { - const segmentOnlyInactive = { - ...mockSegmentBase, - activeSurveys: [], - inactiveSurveys: ["Inactive Survey Only"], - }; - render(); - - expect(screen.getByText("common.active_surveys")).toBeInTheDocument(); - // Check for the placeholder when no active surveys exist - const activeSurveyElements = screen.queryAllByText("-"); - expect(activeSurveyElements.length).toBeGreaterThan(0); // Should find at least one '-' - - expect(screen.getByText("common.inactive_surveys")).toBeInTheDocument(); - expect(screen.getByText("Inactive Survey Only")).toBeInTheDocument(); - - expect( - screen.getByText(convertDateTimeStringShort(segmentOnlyInactive.createdAt.toString())) - ).toBeInTheDocument(); - expect( - screen.getByText(convertDateTimeStringShort(segmentOnlyInactive.updatedAt.toString())) - ).toBeInTheDocument(); - expect(screen.getByText(segmentOnlyInactive.id)).toBeInTheDocument(); - }); - - test("renders correctly with no surveys", () => { - const segmentNoSurveys = { - ...mockSegmentBase, - activeSurveys: [], - inactiveSurveys: [], - }; - render(); - - expect(screen.getByText("common.active_surveys")).toBeInTheDocument(); - expect(screen.getByText("common.inactive_surveys")).toBeInTheDocument(); - - // Check for placeholders when no surveys exist - const placeholders = screen.queryAllByText("-"); - expect(placeholders.length).toBe(2); // Should find two '-' placeholders - - expect( - screen.getByText(convertDateTimeStringShort(segmentNoSurveys.createdAt.toString())) - ).toBeInTheDocument(); - expect( - screen.getByText(convertDateTimeStringShort(segmentNoSurveys.updatedAt.toString())) - ).toBeInTheDocument(); - expect(screen.getByText(segmentNoSurveys.id)).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/segment-editor.test.tsx b/apps/web/modules/ee/contacts/segments/components/segment-editor.test.tsx deleted file mode 100644 index aaeb678ef5..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/segment-editor.test.tsx +++ /dev/null @@ -1,497 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { TBaseFilter, TBaseFilters, TSegment } from "@formbricks/types/segment"; -import * as segmentUtils from "@/modules/ee/contacts/segments/lib/utils"; -import { SegmentEditor } from "./segment-editor"; - -// Mock child components -vi.mock("./segment-filter", () => ({ - SegmentFilter: vi.fn(({ resource }) =>
    SegmentFilter Mock: {resource.attributeKey}
    ), -})); -vi.mock("./add-filter-modal", () => ({ - AddFilterModal: vi.fn(({ open, setOpen }) => ( -
    - AddFilterModal Mock {open ? "Open" : "Closed"} - -
    - )), -})); - -// Mock utility functions -vi.mock("@/modules/ee/contacts/segments/lib/utils", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - addFilterBelow: vi.fn(), - addFilterInGroup: vi.fn(), - createGroupFromResource: vi.fn(), - deleteResource: vi.fn(), - moveResource: vi.fn(), - toggleGroupConnector: vi.fn(), - }; -}); - -const mockSetSegment = vi.fn(); -const mockEnvironmentId = "test-env-id"; -const mockContactAttributeKeys: TContactAttributeKey[] = [ - { name: "email", type: "default" } as unknown as TContactAttributeKey, - { name: "userId", type: "default" } as unknown as TContactAttributeKey, -]; -const mockSegments: TSegment[] = []; - -const mockSegmentBase: TSegment = { - id: "seg1", - environmentId: mockEnvironmentId, - title: "Test Segment", - description: "A segment for testing", - isPrivate: false, - filters: [], // Will be populated in tests - surveys: [], - createdAt: new Date(), - updatedAt: new Date(), -}; - -const filterResource1 = { - id: "filter1", - attributeKey: "email", - attributeValue: "test@example.com", - condition: "equals", - root: { - connector: null, - filterId: "filter1", - }, -}; - -const filterResource2 = { - id: "filter2", - attributeKey: "userId", - attributeValue: "user123", - condition: "equals", - root: { - connector: "and", - filterId: "filter2", - }, -}; - -const groupResource1 = { - id: "group1", - connector: "and", - resource: [ - { - connector: null, - resource: filterResource1, - id: "filter1", - }, - ], -} as unknown as TBaseFilter; - -const groupResource2 = { - id: "group2", - connector: "or", - resource: [ - { - connector: null, - resource: filterResource2, - id: "filter2", - }, - ], -} as unknown as TBaseFilter; - -const mockGroupWithFilters = [ - { - connector: null, - resource: filterResource1, - id: "filter1", - } as unknown as TBaseFilter, - { - connector: "and", - resource: filterResource2, - id: "filter2", - } as unknown as TBaseFilter, -] as unknown as TBaseFilters; - -const mockGroupWithNestedGroup = [ - { - connector: null, - resource: filterResource1, - id: "filter1", - }, - groupResource1, -] as unknown as TBaseFilters; - -describe("SegmentEditor", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders SegmentFilter for filter resources", () => { - const segment = { ...mockSegmentBase, filters: mockGroupWithFilters }; - render( - - ); - expect(screen.getByText("SegmentFilter Mock: email")).toBeInTheDocument(); - expect(screen.getByText("SegmentFilter Mock: userId")).toBeInTheDocument(); - }); - - test("renders nested SegmentEditor for group resources", () => { - const segment = { ...mockSegmentBase, filters: mockGroupWithNestedGroup }; - render( - - ); - // Check that both instances of the email filter are rendered - expect(screen.getAllByText("SegmentFilter Mock: email")).toHaveLength(2); - // Nested group rendering - expect(screen.getByText("and")).toBeInTheDocument(); // Group connector - expect(screen.getByText("common.add_filter")).toBeInTheDocument(); // Add filter button inside group - }); - - test("handles connector click", async () => { - const user = userEvent.setup(); - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - const connectorElement = screen.getByText("and"); - await user.click(connectorElement); - - expect(segmentUtils.toggleGroupConnector).toHaveBeenCalledWith( - expect.any(Array), - groupResource1.id, - "or" - ); - expect(mockSetSegment).toHaveBeenCalled(); - }); - - test("handles 'Add Filter' button click inside a group", async () => { - const user = userEvent.setup(); - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - const addButton = screen.getByText("common.add_filter"); - await user.click(addButton); - - expect(screen.getByText("AddFilterModal Mock Open")).toBeInTheDocument(); - // Further tests could simulate adding a filter via the modal mock if needed - }); - - test("handles 'Add Filter Below' dropdown action", async () => { - const user = userEvent.setup(); - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - const menuTrigger = screen.getByTestId("segment-editor-group-menu-trigger"); - await user.click(menuTrigger); - const addBelowItem = await screen.findByText("environments.segments.add_filter_below"); // Changed to findByText - await user.click(addBelowItem); - - expect(screen.getByText("AddFilterModal Mock Open")).toBeInTheDocument(); - // Further tests could simulate adding a filter via the modal mock and check addFilterBelow call - }); - - test("handles 'Create Group' dropdown action", async () => { - const user = userEvent.setup(); - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - const menuTrigger = screen.getByTestId("segment-editor-group-menu-trigger"); // Use data-testid - await user.click(menuTrigger); - const createGroupItem = await screen.findByText("environments.segments.create_group"); // Use findByText for async rendering - await user.click(createGroupItem); - - expect(segmentUtils.createGroupFromResource).toHaveBeenCalledWith(expect.any(Array), groupResource1.id); - expect(mockSetSegment).toHaveBeenCalled(); - }); - - test("handles 'Move Up' dropdown action", async () => { - const user = userEvent.setup(); - const segment = { ...mockSegmentBase, filters: [groupResource1, groupResource2] }; // Need at least two items - render( - - ); - - // Target the second group's menu - const menuTriggers = screen.getAllByTestId("segment-editor-group-menu-trigger"); - await user.click(menuTriggers[1]); // Click the second MoreVertical icon trigger - const moveUpItem = await screen.findByText("common.move_up"); // Changed to findByText - await user.click(moveUpItem); - - expect(segmentUtils.moveResource).toHaveBeenCalledWith(expect.any(Array), groupResource2.id, "up"); - expect(mockSetSegment).toHaveBeenCalled(); - }); - - test("handles 'Move Down' dropdown action", async () => { - const user = userEvent.setup(); - const segment = { ...mockSegmentBase, filters: [groupResource1, groupResource2] }; // Need at least two items - render( - - ); - - // Target the first group's menu - const menuTriggers = screen.getAllByTestId("segment-editor-group-menu-trigger"); - await user.click(menuTriggers[0]); // Click the first MoreVertical icon trigger - const moveDownItem = await screen.findByText("common.move_down"); // Changed to findByText - await user.click(moveDownItem); - - expect(segmentUtils.moveResource).toHaveBeenCalledWith(expect.any(Array), groupResource1.id, "down"); - expect(mockSetSegment).toHaveBeenCalled(); - }); - - test("handles delete group button click", async () => { - const user = userEvent.setup(); - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - const deleteButton = screen.getByTestId("delete-resource"); - await user.click(deleteButton); - - expect(segmentUtils.deleteResource).toHaveBeenCalledWith(expect.any(Array), groupResource1.id); - expect(mockSetSegment).toHaveBeenCalled(); - }); - - test("renders correctly in viewOnly mode", () => { - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - // Check if interactive elements are disabled or have specific styles - const connectorElement = screen.getByText("and"); - expect(connectorElement).toHaveClass("cursor-not-allowed"); - - const addButton = screen.getByText("common.add_filter"); - expect(addButton).toBeDisabled(); - - const menuTrigger = screen.getByTestId("segment-editor-group-menu-trigger"); // Updated selector - expect(menuTrigger).toBeDisabled(); - - const deleteButton = screen.getByTestId("delete-resource"); - expect(deleteButton).toBeDisabled(); - expect(deleteButton.querySelector("svg")).toHaveClass("cursor-not-allowed"); // Check icon style - }); - - test("does not call handlers in viewOnly mode", async () => { - const user = userEvent.setup(); - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - // Attempt to click connector - const connectorElement = screen.getByText("and"); - await user.click(connectorElement); - expect(segmentUtils.toggleGroupConnector).not.toHaveBeenCalled(); - - // Attempt to click add filter - const addButton = screen.getByText("common.add_filter"); - await user.click(addButton); - // Modal should not open - expect(screen.queryByText("AddFilterModal Mock Open")).not.toBeInTheDocument(); - - // Attempt to click delete - const deleteButton = screen.getByTestId("delete-resource"); - await user.click(deleteButton); - expect(segmentUtils.deleteResource).not.toHaveBeenCalled(); - - // Dropdown menu trigger is disabled, so no need to test clicking items inside - }); - - test("connector button is focusable and activates on Enter/Space", async () => { - const user = userEvent.setup(); - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - const connectorButton = screen.getByText("and"); - // Focus the button directly instead of tabbing to it - connectorButton.focus(); - - // Simulate pressing Enter - await user.keyboard("[Enter]"); - expect(segmentUtils.toggleGroupConnector).toHaveBeenCalledWith( - expect.any(Array), - groupResource1.id, - "or" - ); - - vi.mocked(segmentUtils.toggleGroupConnector).mockClear(); // Clear mock for next assertion - - // Simulate pressing Space - await user.keyboard(" "); - expect(segmentUtils.toggleGroupConnector).toHaveBeenCalledWith( - expect.any(Array), - groupResource1.id, - "or" - ); - }); - - test("connector button has accessibility attributes", () => { - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - const connectorElement = screen.getByText("and"); - expect(connectorElement.tagName.toLowerCase()).toBe("button"); - }); - - test("connector button and add filter button are both keyboard focusable and reachable via tabbing", async () => { - const user = userEvent.setup(); - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - const connectorButton = screen.getByText("and"); - const addFilterButton = screen.getByTestId("add-filter-button"); - - // Tab through the page and collect focusable elements - const focusable: (Element | null)[] = []; - for (let i = 0; i < 10; i++) { - // Arbitrary upper bound to avoid infinite loop - await user.tab(); - focusable.push(document.activeElement); - if (document.activeElement === document.body) break; - } - - // Filter out nulls for the assertion - const nonNullFocusable = focusable.filter((el): el is Element => el !== null); - expect(nonNullFocusable).toContain(connectorButton); - expect(nonNullFocusable).toContain(addFilterButton); - }); - - test("connector button and add filter button can be focused independently", () => { - const segment = { ...mockSegmentBase, filters: [groupResource1] }; - render( - - ); - - const connectorButton = screen.getByText("and"); - const addFilterButton = screen.getByTestId("add-filter-button"); - - connectorButton.focus(); - expect(document.activeElement).toBe(connectorButton); - - addFilterButton.focus(); - expect(document.activeElement).toBe(addFilterButton); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/segment-filter.test.tsx b/apps/web/modules/ee/contacts/segments/components/segment-filter.test.tsx deleted file mode 100644 index 0cde7216c2..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/segment-filter.test.tsx +++ /dev/null @@ -1,1198 +0,0 @@ -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { - TSegment, - TSegmentAttributeFilter, - TSegmentDeviceFilter, - TSegmentPersonFilter, - TSegmentSegmentFilter, -} from "@formbricks/types/segment"; -import { SegmentFilter } from "@/modules/ee/contacts/segments/components/segment-filter"; -import * as segmentUtils from "@/modules/ee/contacts/segments/lib/utils"; - -// Mock ResizeObserver -const ResizeObserverMock = vi.fn(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -vi.stubGlobal("ResizeObserver", ResizeObserverMock); - -// Mock dependencies -vi.mock("@/lib/utils/strings", () => ({ - isCapitalized: vi.fn((str) => str === "Email"), -})); - -vi.mock("@/modules/ee/contacts/segments/lib/utils", () => ({ - convertOperatorToText: vi.fn((op) => op), - convertOperatorToTitle: vi.fn((op) => op), - toggleFilterConnector: vi.fn(), - updateContactAttributeKeyInFilter: vi.fn(), - updateDeviceTypeInFilter: vi.fn(), - updateFilterValue: vi.fn(), - updateOperatorInFilter: vi.fn(), - updatePersonIdentifierInFilter: vi.fn(), - updateSegmentIdInFilter: vi.fn(), - getOperatorOptions: vi.fn(() => []), - validateFilterValue: vi.fn(() => ({ isValid: true, message: "" })), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, disabled, ...props }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/dropdown-menu", () => ({ - DropdownMenu: ({ children }: { children: React.ReactNode }) =>
    {children}
    , - DropdownMenuTrigger: ({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) => ( - - ), - DropdownMenuContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DropdownMenuItem: ({ children, onClick, icon }: any) => ( - - ), -})); - -// Remove the mock for Input component - -vi.mock("./add-filter-modal", () => ({ - AddFilterModal: ({ open, setOpen, onAddFilter }: any) => - open ? ( -
    - Add Filter Modal - -
    - -
    - ) : null, -})); - -vi.mock("lucide-react", () => ({ - ArrowDownIcon: () =>
    ArrowDown
    , - ArrowUpIcon: () =>
    ArrowUp
    , - FingerprintIcon: () =>
    Fingerprint
    , - MonitorSmartphoneIcon: () =>
    Monitor
    , - MoreVertical: () =>
    MoreVertical
    , - TagIcon: () =>
    Tag
    , - Trash2: () =>
    Trash
    , - Users2Icon: () =>
    Users
    , -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -const mockSetSegment = vi.fn(); -const mockHandleAddFilterBelow = vi.fn(); -const mockOnCreateGroup = vi.fn(); -const mockOnDeleteFilter = vi.fn(); -const mockOnMoveFilter = vi.fn(); - -const environmentId = "test-env-id"; -const segment = { - id: "seg1", - environmentId, - title: "Test Segment", - isPrivate: false, - filters: [], - surveys: ["survey1"], - createdAt: new Date(), - updatedAt: new Date(), -} as unknown as TSegment; -const segments: TSegment[] = [ - segment, - { - id: "seg2", - environmentId, - title: "Another Segment", - isPrivate: false, - filters: [], - surveys: ["survey1"], - createdAt: new Date(), - updatedAt: new Date(), - } as unknown as TSegment, - { - id: "seg3", - environmentId, - title: "Third Segment", - isPrivate: false, - filters: [], - surveys: ["survey1"], - createdAt: new Date(), - updatedAt: new Date(), - } as unknown as TSegment, -]; -const contactAttributeKeys: TContactAttributeKey[] = [ - { - id: "attr1", - key: "email", - name: "Email", - environmentId, - createdAt: new Date(), - updatedAt: new Date(), - } as TContactAttributeKey, - { - id: "attr2", - key: "userId", - name: "User ID", - environmentId, - createdAt: new Date(), - updatedAt: new Date(), - } as TContactAttributeKey, - { - id: "attr3", - key: "plan", - name: "Plan", - environmentId, - createdAt: new Date(), - updatedAt: new Date(), - } as TContactAttributeKey, -]; - -const baseProps = { - environmentId, - segment, - segments, - contactAttributeKeys, - setSegment: mockSetSegment, - handleAddFilterBelow: mockHandleAddFilterBelow, - onCreateGroup: mockOnCreateGroup, - onDeleteFilter: mockOnDeleteFilter, - onMoveFilter: mockOnMoveFilter, -}; - -describe("SegmentFilter", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - beforeEach(() => { - // Remove the implementation that modifies baseProps.segment during the test. - // vi.clearAllMocks() in afterEach handles mock reset. - }); - - test("SegmentFilterItemConnector displays correct connector value or default text", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "equals", - }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter }; - - render(); - expect(screen.getByText("and")).toBeInTheDocument(); - - cleanup(); - render(); - expect(screen.getByText("environments.segments.where")).toBeInTheDocument(); - }); - - test("SegmentFilterItemConnector applies correct CSS classes based on props", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "equals", - }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter }; - - // Test case 1: connector is "and", viewOnly is false - render(); - const connectorButton1 = screen.getByText("and").closest("button"); - expect(connectorButton1).toHaveClass("cursor-pointer"); - expect(connectorButton1).toHaveClass("underline"); - expect(connectorButton1).not.toHaveClass("cursor-not-allowed"); - - cleanup(); - - // Test case 2: connector is null, viewOnly is false - render(); - const connectorButton2 = screen.getByText("environments.segments.where").closest("button"); - expect(connectorButton2).not.toHaveClass("cursor-pointer"); - expect(connectorButton2).not.toHaveClass("underline"); - expect(connectorButton2).not.toHaveClass("cursor-not-allowed"); - - cleanup(); - - // Test case 3: connector is "and", viewOnly is true - render( - - ); - const connectorButton3 = screen.getByText("and").closest("button"); - expect(connectorButton3).not.toHaveClass("cursor-pointer"); - expect(connectorButton3).toHaveClass("underline"); - expect(connectorButton3).toHaveClass("cursor-not-allowed"); - }); - - test("SegmentFilterItemConnector applies cursor-not-allowed class when viewOnly is true", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "equals", - }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter, viewOnly: true }; - - render(); - const connectorButton = screen.getByText("and"); - expect(connectorButton).toHaveClass("cursor-not-allowed"); - }); - - test("toggles connector on Enter key press", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { type: "attribute", contactAttributeKey: "email" }, - qualifier: { operator: "equals" }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: structuredClone(segmentWithAttributeFilter) }; - - render(); - const connectorButton = screen.getByText("and"); - connectorButton.focus(); - await userEvent.keyboard("{Enter}"); - - expect(vi.mocked(segmentUtils.toggleFilterConnector)).toHaveBeenCalledWith( - currentProps.segment.filters, - attributeFilterResource.id, - "or" - ); - expect(mockSetSegment).toHaveBeenCalled(); - }); - - test("SegmentFilterItemConnector button shows a visible focus indicator when focused via keyboard navigation", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "equals", - }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter }; - render(); - - const connectorButton = screen.getByText("and"); - await userEvent.tab(); - expect(connectorButton).toHaveFocus(); - }); - - test("SegmentFilterItemConnector button has aria-label for screen readers", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "equals", - }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter }; - - render(); - const andButton = screen.getByRole("button", { name: "and" }); - expect(andButton).toHaveAttribute("aria-label", "and"); - - cleanup(); - render(); - const orButton = screen.getByRole("button", { name: "or" }); - expect(orButton).toHaveAttribute("aria-label", "or"); - - cleanup(); - render(); - const whereButton = screen.getByRole("button", { name: "environments.segments.where" }); - expect(whereButton).toHaveAttribute("aria-label", "environments.segments.where"); - }); - - describe("Attribute Filter", () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "equals", - }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - test("renders correctly", async () => { - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter }; - render(); - expect(screen.getByText("and")).toBeInTheDocument(); - await waitFor(() => expect(screen.getByText("Email").closest("button")).toBeInTheDocument()); - await waitFor(() => expect(screen.getByText("equals").closest("button")).toBeInTheDocument()); - expect(screen.getByDisplayValue("test@example.com")).toBeInTheDocument(); - expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument(); - expect(screen.getByTestId("trash-icon")).toBeInTheDocument(); - }); - - test("renders attribute key select correctly", async () => { - const currentProps = { ...baseProps, segment: structuredClone(segmentWithAttributeFilter) }; - render(); - - await waitFor(() => expect(screen.getByText("Email").closest("button")).toBeInTheDocument()); - - expect(vi.mocked(segmentUtils.updateContactAttributeKeyInFilter)).not.toHaveBeenCalled(); - expect(mockSetSegment).not.toHaveBeenCalled(); - }); - - test("renders operator select correctly", async () => { - const currentProps = { ...baseProps, segment: structuredClone(segmentWithAttributeFilter) }; - render(); - - await waitFor(() => expect(screen.getByText("equals").closest("button")).toBeInTheDocument()); - - expect(vi.mocked(segmentUtils.updateOperatorInFilter)).not.toHaveBeenCalled(); - expect(mockSetSegment).not.toHaveBeenCalled(); - }); - - test("handles value change", async () => { - const initialSegment = structuredClone(segmentWithAttributeFilter); - const currentProps = { ...baseProps, segment: initialSegment, setSegment: mockSetSegment }; - - render(); - const valueInput = screen.getByDisplayValue("test@example.com"); - - // Clear the input - await userEvent.clear(valueInput); - // Fire a single change event with the final value - fireEvent.change(valueInput, { target: { value: "new@example.com" } }); - - // Check the call to the update function (might be called once or twice by checkValueAndUpdate) - await waitFor(() => { - // Check if it was called AT LEAST once with the correct final value - expect(vi.mocked(segmentUtils.updateFilterValue)).toHaveBeenCalledWith( - expect.anything(), - attributeFilterResource.id, - "new@example.com" - ); - }); - - // Ensure the state update function was called - expect(mockSetSegment).toHaveBeenCalled(); - }); - - test("renders viewOnly mode correctly", async () => { - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter }; - render( - - ); - expect(screen.getByText("and")).toHaveClass("cursor-not-allowed"); - await waitFor(() => expect(screen.getByText("Email").closest("button")).toBeDisabled()); - await waitFor(() => expect(screen.getByText("equals").closest("button")).toBeDisabled()); - expect(screen.getByDisplayValue("test@example.com")).toBeDisabled(); - expect(screen.getByTestId("dropdown-trigger")).toBeDisabled(); - expect(screen.getByTestId("trash-icon").closest("button")).toBeDisabled(); - }); - - test("displays error message for non-numeric input with arithmetic operator", async () => { - const arithmeticFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-arithmetic-1", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "greaterThan", - }, - value: "hello", - }; - - const segmentWithArithmeticFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: arithmeticFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: segmentWithArithmeticFilter }; - render(); - - const valueInput = screen.getByDisplayValue("hello"); - await userEvent.clear(valueInput); - fireEvent.change(valueInput, { target: { value: "abc" } }); - - await waitFor(() => - expect(screen.getByText("environments.segments.value_must_be_a_number")).toBeInTheDocument() - ); - }); - - test("navigates with tab key", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "equals", - }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter }; - render(); - - const connectorButton = screen.getByText("and").closest("button"); - const attributeSelect = screen.getByText("Email").closest("button"); - const operatorSelect = screen.getByText("equals").closest("button"); - const valueInput = screen.getByDisplayValue("test@example.com"); - const dropdownTrigger = screen.getByTestId("dropdown-trigger"); - const trashButton = screen.getByTestId("trash-icon").closest("button"); - - // Set focus on the first element (connector button) - connectorButton?.focus(); - await waitFor(() => expect(connectorButton).toHaveFocus()); - - // Tab to attribute select - await userEvent.tab(); - if (!attributeSelect) throw new Error("attributeSelect is null"); - await waitFor(() => expect(attributeSelect).toHaveFocus()); - - // Tab to operator select - await userEvent.tab(); - if (!operatorSelect) throw new Error("operatorSelect is null"); - await waitFor(() => expect(operatorSelect).toHaveFocus()); - - // Tab to value input - await userEvent.tab(); - await waitFor(() => expect(valueInput).toHaveFocus()); - - // Tab to dropdown trigger - await userEvent.tab(); - await waitFor(() => expect(dropdownTrigger).toHaveFocus()); - - // Tab through dropdown menu items (4 items) - for (let i = 0; i < 4; i++) { - await userEvent.tab(); - } - - // Tab to trash button - await userEvent.tab(); - if (!trashButton) throw new Error("trashButton is null"); - await waitFor(() => expect(trashButton).toHaveFocus()); - }); - - test("interactive buttons have type='button' attribute", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "equals", - }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter }; - render(); - - const connectorButton = await screen.findByText("and"); - expect(connectorButton.closest("button")).toHaveAttribute("type", "button"); - }); - }); - - describe("Person Filter", () => { - const personFilterResource: TSegmentPersonFilter = { - id: "filter-person-1", - root: { type: "person", personIdentifier: "userId" }, - qualifier: { operator: "equals" }, - value: "person123", - }; - const segmentWithPersonFilter: TSegment = { - ...segment, - filters: [{ id: "group-1", connector: "and", resource: personFilterResource }], - }; - - test("renders correctly", async () => { - const currentProps = { ...baseProps, segment: segmentWithPersonFilter }; - render(); - expect(screen.getByText("or")).toBeInTheDocument(); - await waitFor(() => expect(screen.getByText("userId").closest("button")).toBeInTheDocument()); - await waitFor(() => expect(screen.getByText("equals").closest("button")).toBeInTheDocument()); - expect(screen.getByDisplayValue("person123")).toBeInTheDocument(); - }); - - test("renders operator select correctly", async () => { - const currentProps = { ...baseProps, segment: structuredClone(segmentWithPersonFilter) }; - render(); - - await waitFor(() => expect(screen.getByText("equals").closest("button")).toBeInTheDocument()); - - expect(vi.mocked(segmentUtils.updateOperatorInFilter)).not.toHaveBeenCalled(); - expect(mockSetSegment).not.toHaveBeenCalled(); - }); - - test("handles value change", async () => { - const initialSegment = structuredClone(segmentWithPersonFilter); - const currentProps = { ...baseProps, segment: initialSegment, setSegment: mockSetSegment }; - - render(); - const valueInput = screen.getByDisplayValue("person123"); - - // Clear the input - await userEvent.clear(valueInput); - // Fire a single change event with the final value - fireEvent.change(valueInput, { target: { value: "person456" } }); - - // Check the call to the update function (might be called once or twice by checkValueAndUpdate) - await waitFor(() => { - // Check if it was called AT LEAST once with the correct final value - expect(vi.mocked(segmentUtils.updateFilterValue)).toHaveBeenCalledWith( - expect.anything(), - personFilterResource.id, - "person456" - ); - }); - // Ensure the state update function was called - expect(mockSetSegment).toHaveBeenCalled(); - }); - - test("displays error message for non-numeric input with arithmetic operator", async () => { - const personFilterResourceWithArithmeticOperator: TSegmentPersonFilter = { - id: "filter-person-2", - root: { type: "person", personIdentifier: "userId" }, - qualifier: { operator: "greaterThan" }, - value: "hello", - }; - - const segmentWithPersonFilterArithmetic: TSegment = { - ...segment, - filters: [{ id: "group-2", connector: "and", resource: personFilterResourceWithArithmeticOperator }], - }; - - const currentProps = { - ...baseProps, - segment: structuredClone(segmentWithPersonFilterArithmetic), - setSegment: mockSetSegment, - }; - - render( - - ); - const valueInput = screen.getByDisplayValue("hello"); - - await userEvent.clear(valueInput); - fireEvent.change(valueInput, { target: { value: "abc" } }); - - await waitFor(() => { - expect(screen.getByText("environments.segments.value_must_be_a_number")).toBeInTheDocument(); - }); - }); - - test("handles empty value input", async () => { - const initialSegment = structuredClone(segmentWithPersonFilter); - const currentProps = { ...baseProps, segment: initialSegment, setSegment: mockSetSegment }; - - render(); - const valueInput = screen.getByDisplayValue("person123"); - - // Clear the input - await userEvent.clear(valueInput); - // Fire a single change event with the final value - fireEvent.change(valueInput, { target: { value: "" } }); - - // Check the call to the update function (might be called once or twice by checkValueAndUpdate) - await waitFor(() => { - // Check if it was called AT LEAST once with the correct final value - expect(vi.mocked(segmentUtils.updateFilterValue)).toHaveBeenCalledWith( - expect.anything(), - personFilterResource.id, - "" - ); - }); - - const errorMessage = await screen.findByText("environments.segments.value_cannot_be_empty"); - expect(errorMessage).toBeVisible(); - - // Ensure the state update function was called - expect(mockSetSegment).toHaveBeenCalled(); - }); - - test("is keyboard accessible", async () => { - const currentProps = { ...baseProps, segment: segmentWithPersonFilter }; - render(); - - // Tab to the connector button - await userEvent.tab(); - expect(screen.getByText("or")).toHaveFocus(); - - // Tab to the person identifier select - await userEvent.tab(); - await waitFor(() => expect(screen.getByText("userId").closest("button")).toHaveFocus()); - - // Tab to the operator select - await userEvent.tab(); - await waitFor(() => expect(screen.getByText("equals").closest("button")).toHaveFocus()); - - // Tab to the value input - await userEvent.tab(); - expect(screen.getByDisplayValue("person123")).toHaveFocus(); - - // Tab to the context menu trigger - await userEvent.tab(); - await waitFor(() => expect(screen.getByTestId("dropdown-trigger")).toHaveFocus()); - }); - - describe("Person Filter - Multiple Identifiers", () => { - const personFilterResourceWithMultipleIdentifiers: TSegmentPersonFilter = { - id: "filter-person-multi-1", - root: { type: "person", personIdentifier: "userId" }, // Even though it's a single value, the component should handle the possibility of multiple - qualifier: { operator: "equals" }, - value: "person123", - }; - const segmentWithPersonFilterWithMultipleIdentifiers: TSegment = { - ...segment, - filters: [ - { id: "group-multi-1", connector: "and", resource: personFilterResourceWithMultipleIdentifiers }, - ], - }; - - test("renders correctly with multiple person identifiers", async () => { - const currentProps = { ...baseProps, segment: segmentWithPersonFilterWithMultipleIdentifiers }; - render( - - ); - expect(screen.getByText("or")).toBeInTheDocument(); - await waitFor(() => expect(screen.getByText("userId").closest("button")).toBeInTheDocument()); - await waitFor(() => expect(screen.getByText("equals").closest("button")).toBeInTheDocument()); - expect(screen.getByDisplayValue("person123")).toBeInTheDocument(); - }); - }); - }); - - describe("Segment Filter", () => { - const segmentFilterResource = { - id: "filter-segment-1", - root: { type: "segment", segmentId: "seg2" }, - qualifier: { operator: "userIsIn" }, - } as unknown as TSegmentSegmentFilter; - const segmentWithSegmentFilter: TSegment = { - ...segment, - filters: [{ id: "group-1", connector: "and", resource: segmentFilterResource }], - }; - - test("renders correctly", async () => { - const currentProps = { ...baseProps, segment: segmentWithSegmentFilter }; - render(); - expect(screen.getByText("environments.segments.where")).toBeInTheDocument(); - expect(screen.getByText("userIsIn")).toBeInTheDocument(); - await waitFor(() => expect(screen.getByText("Another Segment").closest("button")).toBeInTheDocument()); - }); - - test("renders segment select correctly", async () => { - const currentProps = { ...baseProps, segment: structuredClone(segmentWithSegmentFilter) }; - render(); - - await waitFor(() => expect(screen.getByText("Another Segment").closest("button")).toBeInTheDocument()); - - expect(vi.mocked(segmentUtils.updateSegmentIdInFilter)).not.toHaveBeenCalled(); - expect(mockSetSegment).not.toHaveBeenCalled(); - }); - - test("updates the segment ID in the filter when a new segment is selected", async () => { - const segmentFilterResource = { - id: "filter-segment-1", - root: { type: "segment", segmentId: "seg2" }, - qualifier: { operator: "userIsIn" }, - } as unknown as TSegmentSegmentFilter; - const segmentWithSegmentFilter: TSegment = { - ...segment, - filters: [{ id: "group-1", connector: "and", resource: segmentFilterResource }], - }; - - const currentProps = { - ...baseProps, - segment: structuredClone(segmentWithSegmentFilter), - setSegment: mockSetSegment, - }; - - render(); - - // Mock the updateSegmentIdInFilter function call directly - // This simulates what would happen when a segment is selected - vi.mocked(segmentUtils.updateSegmentIdInFilter).mockImplementationOnce(() => {}); - - // Directly call the mocked function with the expected arguments - segmentUtils.updateSegmentIdInFilter(currentProps.segment.filters, "filter-segment-1", "seg3"); - - // Verify the function was called with the correct arguments - expect(vi.mocked(segmentUtils.updateSegmentIdInFilter)).toHaveBeenCalledWith( - expect.anything(), - "filter-segment-1", - "seg3" - ); - - // Call the setSegment function to simulate the state update - mockSetSegment(currentProps.segment); - expect(mockSetSegment).toHaveBeenCalled(); - }); - }); - - describe("Device Filter", () => { - const deviceFilterResource: TSegmentDeviceFilter = { - id: "filter-device-1", - root: { type: "device", deviceType: "desktop" }, - qualifier: { operator: "equals" }, - value: "desktop", - }; - const segmentWithDeviceFilter: TSegment = { - ...segment, - filters: [{ id: "group-1", connector: "and", resource: deviceFilterResource }], - }; - - test("renders correctly", async () => { - const currentProps = { ...baseProps, segment: segmentWithDeviceFilter }; - render(); - expect(screen.getByText("and")).toBeInTheDocument(); - expect(screen.getByText("Device")).toBeInTheDocument(); - await waitFor(() => expect(screen.getByText("equals").closest("button")).toBeInTheDocument()); - await waitFor(() => - expect(screen.getByText("environments.segments.desktop").closest("button")).toBeInTheDocument() - ); - }); - - test("renders operator select correctly", async () => { - const currentProps = { ...baseProps, segment: structuredClone(segmentWithDeviceFilter) }; - render(); - - await waitFor(() => expect(screen.getByText("equals").closest("button")).toBeInTheDocument()); - - expect(vi.mocked(segmentUtils.updateOperatorInFilter)).not.toHaveBeenCalled(); - expect(mockSetSegment).not.toHaveBeenCalled(); - }); - - test("renders device type select correctly", async () => { - const currentProps = { ...baseProps, segment: structuredClone(segmentWithDeviceFilter) }; - render(); - - await waitFor(() => - expect(screen.getByText("environments.segments.desktop").closest("button")).toBeInTheDocument() - ); - - expect(vi.mocked(segmentUtils.updateDeviceTypeInFilter)).not.toHaveBeenCalled(); - expect(mockSetSegment).not.toHaveBeenCalled(); - }); - }); - - test("toggles connector on click", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { type: "attribute", contactAttributeKey: "email" }, - qualifier: { operator: "equals" }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: structuredClone(segmentWithAttributeFilter) }; - - render(); - const connectorSpan = screen.getByText("and"); - await userEvent.click(connectorSpan); - expect(vi.mocked(segmentUtils.toggleFilterConnector)).toHaveBeenCalledWith( - currentProps.segment.filters, - attributeFilterResource.id, - "or" - ); - expect(mockSetSegment).toHaveBeenCalled(); - }); - - test("does not toggle connector in viewOnly mode", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { type: "attribute", contactAttributeKey: "email" }, - qualifier: { operator: "equals" }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter }; - - render( - - ); - const connectorSpan = screen.getByText("and"); - await userEvent.click(connectorSpan); - expect(vi.mocked(segmentUtils.toggleFilterConnector)).not.toHaveBeenCalled(); - expect(mockSetSegment).not.toHaveBeenCalled(); - }); - - describe("Segment Filter - Empty Segments", () => { - const segmentFilterResource = { - id: "filter-segment-1", - root: { type: "segment", segmentId: "seg2" }, - qualifier: { operator: "userIsIn" }, - } as unknown as TSegmentSegmentFilter; - const segmentWithSegmentFilter: TSegment = { - ...segment, - filters: [{ id: "group-1", connector: "and", resource: segmentFilterResource }], - }; - - test("renders correctly when segments array is empty", async () => { - const currentProps = { ...baseProps, segment: segmentWithSegmentFilter, segments: [] }; - render(); - - // Find the combobox element - const selectElement = screen.getByRole("combobox"); - // Verify it has the empty placeholder attribute - expect(selectElement).toHaveAttribute("data-placeholder", ""); - }); - - test("renders correctly when segments array contains only private segments", async () => { - const privateSegments: TSegment[] = [ - { - id: "seg3", - environmentId, - title: "Private Segment", - isPrivate: true, - filters: [], - surveys: ["survey1"], - createdAt: new Date(), - updatedAt: new Date(), - } as unknown as TSegment, - ]; - const currentProps = { ...baseProps, segment: segmentWithSegmentFilter, segments: privateSegments }; - render(); - - // Find the combobox element - const selectElement = screen.getByRole("combobox"); - // Verify it has the empty placeholder attribute - expect(selectElement).toHaveAttribute("data-placeholder", ""); - }); - }); - - test("deletes the entire group when deleting the last SegmentSegmentFilter", async () => { - const segmentFilterResource: TSegmentSegmentFilter = { - id: "filter-segment-1", - root: { type: "segment", segmentId: "seg2" }, - qualifier: { operator: "userIsIn" }, - } as unknown as TSegmentSegmentFilter; - - const segmentWithSegmentFilter: TSegment = { - ...segment, - filters: [{ id: "group-1", connector: "and", resource: segmentFilterResource }], - }; - - const currentProps = { ...baseProps, segment: segmentWithSegmentFilter }; - render(); - - const deleteButton = screen.getByTestId("trash-icon").closest("button"); - expect(deleteButton).toBeInTheDocument(); - - if (!deleteButton) throw new Error("deleteButton is null"); - await userEvent.click(deleteButton); - - expect(mockOnDeleteFilter).toHaveBeenCalledWith("filter-segment-1"); - }); - - describe("SegmentSegmentFilter", () => { - const segmentFilterResource = { - id: "filter-segment-1", - root: { type: "segment", segmentId: "seg2" }, - qualifier: { operator: "userIsIn" }, - } as unknown as TSegmentSegmentFilter; - const segmentWithSegmentFilter: TSegment = { - ...segment, - filters: [{ id: "group-1", connector: "and", resource: segmentFilterResource }], - }; - - test("operator toggle button has accessible name", async () => { - const currentProps = { ...baseProps, segment: segmentWithSegmentFilter }; - render(); - - // Find the operator button by its text content - const operatorButton = screen.getByText("userIsIn"); - - // Check that the button is accessible by its visible name - const operatorToggleButton = operatorButton.closest("button"); - expect(operatorToggleButton).toHaveAccessibleName("userIsIn"); - }); - }); - - test("renders AttributeSegmentFilter in viewOnly mode with disabled interactive elements and accessibility attributes", async () => { - const attributeFilterResource: TSegmentAttributeFilter = { - id: "filter-attr-1", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "equals", - }, - value: "test@example.com", - }; - const segmentWithAttributeFilter: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: attributeFilterResource, - }, - ], - }; - - const currentProps = { ...baseProps, segment: segmentWithAttributeFilter, viewOnly: true }; - render(); - - // Check if the connector button is disabled and has the correct class - const connectorButton = screen.getByText("and"); - expect(connectorButton).toHaveClass("cursor-not-allowed"); - - // Check if the attribute key select is disabled - const attributeKeySelect = await screen.findByRole("combobox", { - name: (content, element) => { - return element.textContent?.toLowerCase().includes("email") ?? false; - }, - }); - expect(attributeKeySelect).toBeDisabled(); - - // Check if the operator select is disabled - const operatorSelect = await screen.findByRole("combobox", { - name: (content, element) => { - return element.textContent?.toLowerCase().includes("equals") ?? false; - }, - }); - expect(operatorSelect).toBeDisabled(); - - // Check if the value input is disabled - const valueInput = screen.getByDisplayValue("test@example.com"); - expect(valueInput).toBeDisabled(); - - // Check if the context menu trigger is disabled - const contextMenuTrigger = screen.getByTestId("dropdown-trigger"); - expect(contextMenuTrigger).toBeDisabled(); - - // Check if the delete button is disabled - const deleteButton = screen.getByTestId("trash-icon").closest("button"); - expect(deleteButton).toBeDisabled(); - }); - - test("handles complex nested structures without error", async () => { - const nestedAttributeFilter: TSegmentAttributeFilter = { - id: "nested-filter", - root: { - type: "attribute", - contactAttributeKey: "plan", - }, - qualifier: { - operator: "equals", - }, - value: "premium", - }; - - const complexAttributeFilter: TSegmentAttributeFilter = { - id: "complex-filter", - root: { - type: "attribute", - contactAttributeKey: "email", - }, - qualifier: { - operator: "contains", - }, - value: "example", - }; - - const deeplyNestedSegment: TSegment = { - ...segment, - filters: [ - { - id: "group-1", - connector: "and", - resource: [ - { - id: "group-2", - connector: "or", - resource: [ - { - id: "group-3", - connector: "and", - resource: complexAttributeFilter, - }, - ], - }, - ], - }, - { - id: "group-4", - connector: "and", - resource: nestedAttributeFilter, - }, - ], - }; - - const currentProps = { ...baseProps, segment: deeplyNestedSegment }; - - // Act & Assert: Render the component and expect no error to be thrown - expect(() => { - render(); - }).not.toThrow(); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/segment-settings.test.tsx b/apps/web/modules/ee/contacts/segments/components/segment-settings.test.tsx deleted file mode 100644 index 1038c4e3d7..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/segment-settings.test.tsx +++ /dev/null @@ -1,516 +0,0 @@ -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { SafeParseReturnType } from "zod"; -import { TBaseFilters, ZSegmentFilters } from "@formbricks/types/segment"; -import * as helper from "@/lib/utils/helper"; -import * as actions from "@/modules/ee/contacts/segments/actions"; -import { SegmentSettings } from "./segment-settings"; - -// Mock dependencies -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - refresh: vi.fn(), - }), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock("@/modules/ee/contacts/segments/actions", () => ({ - updateSegmentAction: vi.fn(), - deleteSegmentAction: vi.fn(), -})); - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(), -})); - -// Mock ZSegmentFilters validation -vi.mock("@formbricks/types/segment", () => ({ - ZSegmentFilters: { - safeParse: vi.fn().mockReturnValue({ success: true }), - }, -})); - -// Mock components used by SegmentSettings -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, loading, disabled }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/input", () => ({ - Input: ({ value, onChange, disabled, placeholder }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/confirm-delete-segment-modal", () => ({ - ConfirmDeleteSegmentModal: ({ open, setOpen, onDelete }: any) => - open ? ( -
    - - -
    - ) : null, -})); - -vi.mock("./segment-editor", () => ({ - SegmentEditor: ({ group }) => ( -
    - Segment Editor -
    {group?.length || 0}
    -
    - ), -})); - -vi.mock("./add-filter-modal", () => ({ - AddFilterModal: ({ open, setOpen, onAddFilter }: any) => - open ? ( -
    - - -
    - ) : null, -})); - -describe("SegmentSettings", () => { - const mockProps = { - environmentId: "env-123", - initialSegment: { - id: "segment-123", - title: "Test Segment", - description: "Test Description", - isPrivate: false, - filters: [], - activeSurveys: [], - inactiveSurveys: [], - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "env-123", - surveys: [], - }, - setOpen: vi.fn(), - contactAttributeKeys: [], - segments: [], - isReadOnly: false, - }; - - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(helper.getFormattedErrorMessage).mockReturnValue(""); - // Default to valid filters - vi.mocked(ZSegmentFilters.safeParse).mockReturnValue({ success: true } as unknown as SafeParseReturnType< - TBaseFilters, - TBaseFilters - >); - }); - - afterEach(() => { - cleanup(); - }); - - test("should update the segment and display a success message when valid data is provided", async () => { - // Mock successful update - vi.mocked(actions.updateSegmentAction).mockResolvedValue({ - data: { - title: "Updated Segment", - description: "Updated Description", - isPrivate: false, - filters: [], - createdAt: new Date(), - environmentId: "env-123", - id: "segment-123", - surveys: [], - updatedAt: new Date(), - }, - }); - - // Render component - render(); - - // Find and click the save button using data-testid - const saveButton = screen.getByTestId("save-button"); - fireEvent.click(saveButton); - - // Verify updateSegmentAction was called with correct parameters - await waitFor(() => { - expect(actions.updateSegmentAction).toHaveBeenCalledWith({ - environmentId: mockProps.environmentId, - segmentId: mockProps.initialSegment.id, - data: { - title: mockProps.initialSegment.title, - description: mockProps.initialSegment.description, - isPrivate: mockProps.initialSegment.isPrivate, - filters: mockProps.initialSegment.filters, - }, - }); - }); - - // Verify success toast was displayed - expect(toast.success).toHaveBeenCalledWith("Segment updated successfully!"); - - // Verify state was reset and router was refreshed - expect(mockProps.setOpen).toHaveBeenCalledWith(false); - }); - - test("should update segment title when input changes", () => { - render(); - - // Find title input and change its value - const titleInput = screen.getAllByTestId("input")[0]; - fireEvent.change(titleInput, { target: { value: "Updated Title" } }); - - // Find and click the save button using data-testid - const saveButton = screen.getByTestId("save-button"); - fireEvent.click(saveButton); - - // Verify updateSegmentAction was called with updated title - expect(actions.updateSegmentAction).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - title: "Updated Title", - }), - }) - ); - }); - - test("should reset state after successfully updating a segment", async () => { - // Mock successful update - vi.mocked(actions.updateSegmentAction).mockResolvedValue({ - data: { - title: "Updated Segment", - description: "Updated Description", - isPrivate: false, - filters: [], - createdAt: new Date(), - environmentId: "env-123", - id: "segment-123", - surveys: [], - updatedAt: new Date(), - }, - }); - - // Render component - render(); - - // Modify the segment state by changing the title - const titleInput = screen.getAllByTestId("input")[0]; - fireEvent.change(titleInput, { target: { value: "Modified Title" } }); - - // Find and click the save button - const saveButton = screen.getByTestId("save-button"); - fireEvent.click(saveButton); - - // Wait for the update to complete - await waitFor(() => { - // Verify updateSegmentAction was called - expect(actions.updateSegmentAction).toHaveBeenCalled(); - }); - - // Verify success toast was displayed - expect(toast.success).toHaveBeenCalledWith("Segment updated successfully!"); - - // Verify state was reset by checking that setOpen was called with false - expect(mockProps.setOpen).toHaveBeenCalledWith(false); - - // Re-render the component to verify it would use the initialSegment - cleanup(); - render(); - - // Check that the title is back to the initial value - const titleInputAfterReset = screen.getAllByTestId("input")[0]; - expect(titleInputAfterReset).toHaveValue("Test Segment"); - }); - - test("should not reset state if update returns an error message", async () => { - // Mock update with error - vi.mocked(actions.updateSegmentAction).mockResolvedValue({}); - vi.mocked(helper.getFormattedErrorMessage).mockReturnValue("Recursive segment filter detected"); - - // Render component - render(); - - // Modify the segment state - const titleInput = screen.getAllByTestId("input")[0]; - fireEvent.change(titleInput, { target: { value: "Modified Title" } }); - - // Find and click the save button - const saveButton = screen.getByTestId("save-button"); - fireEvent.click(saveButton); - - // Wait for the update to complete - await waitFor(() => { - expect(actions.updateSegmentAction).toHaveBeenCalled(); - }); - - // Verify error toast was displayed - expect(toast.error).toHaveBeenCalledWith("Recursive segment filter detected"); - - // Verify state was NOT reset (setOpen should not be called) - expect(mockProps.setOpen).not.toHaveBeenCalled(); - - // Verify isUpdatingSegment was set back to false - expect(saveButton).not.toHaveAttribute("data-loading", "true"); - }); - test("should delete the segment and display a success message when delete operation is successful", async () => { - // Mock successful delete - vi.mocked(actions.deleteSegmentAction).mockResolvedValue({}); - - // Render component - render(); - - // Find and click the delete button to open the confirmation modal - const deleteButton = screen.getByText("common.delete"); - fireEvent.click(deleteButton); - - // Verify the delete confirmation modal is displayed - expect(screen.getByTestId("delete-modal")).toBeInTheDocument(); - - // Click the confirm delete button in the modal - const confirmDeleteButton = screen.getByTestId("confirm-delete"); - fireEvent.click(confirmDeleteButton); - - // Verify deleteSegmentAction was called with correct segment ID - await waitFor(() => { - expect(actions.deleteSegmentAction).toHaveBeenCalledWith({ - segmentId: mockProps.initialSegment.id, - }); - }); - - // Verify success toast was displayed with the correct message - expect(toast.success).toHaveBeenCalledWith("environments.segments.segment_deleted_successfully"); - - // Verify state was reset and router was refreshed - expect(mockProps.setOpen).toHaveBeenCalledWith(false); - }); - - test("should disable the save button if the segment title is empty or filters are invalid", async () => { - render(); - - // Initially the save button should be enabled because we have a valid title and filters - const saveButton = screen.getByTestId("save-button"); - expect(saveButton).not.toBeDisabled(); - - // Change the title to empty string - const titleInput = screen.getAllByTestId("input")[0]; - fireEvent.change(titleInput, { target: { value: "" } }); - - // Save button should now be disabled due to empty title - await waitFor(() => { - expect(saveButton).toBeDisabled(); - }); - - // Reset title to valid value - fireEvent.change(titleInput, { target: { value: "Valid Title" } }); - - // Save button should be enabled again - await waitFor(() => { - expect(saveButton).not.toBeDisabled(); - }); - - // Now simulate invalid filters - vi.mocked(ZSegmentFilters.safeParse).mockReturnValue({ success: false } as unknown as SafeParseReturnType< - TBaseFilters, - TBaseFilters - >); - - // We need to trigger a re-render to see the effect of the mocked validation - // Adding a filter would normally trigger this, but we can simulate by changing any state - const descriptionInput = screen.getAllByTestId("input")[1]; - fireEvent.change(descriptionInput, { target: { value: "Updated description" } }); - - // Save button should be disabled due to invalid filters - await waitFor(() => { - expect(saveButton).toBeDisabled(); - }); - - // Reset filters to valid - vi.mocked(ZSegmentFilters.safeParse).mockReturnValue({ success: true } as unknown as SafeParseReturnType< - TBaseFilters, - TBaseFilters - >); - - // Change description again to trigger re-render - fireEvent.change(descriptionInput, { target: { value: "Another description update" } }); - - // Save button should be enabled again - await waitFor(() => { - expect(saveButton).not.toBeDisabled(); - }); - }); - - test("should display error message and not proceed with update when recursive segment filter is detected", async () => { - // Mock updateSegmentAction to return data that would contain an error - const mockData = { someData: "value" }; - vi.mocked(actions.updateSegmentAction).mockResolvedValue(mockData as unknown as any); - - // Mock getFormattedErrorMessage to return a recursive filter error message - const recursiveErrorMessage = "Segment cannot reference itself in filters"; - vi.mocked(helper.getFormattedErrorMessage).mockReturnValue(recursiveErrorMessage); - - // Render component - render(); - - // Find and click the save button - const saveButton = screen.getByTestId("save-button"); - fireEvent.click(saveButton); - - // Verify updateSegmentAction was called - await waitFor(() => { - expect(actions.updateSegmentAction).toHaveBeenCalledWith({ - environmentId: mockProps.environmentId, - segmentId: mockProps.initialSegment.id, - data: { - title: mockProps.initialSegment.title, - description: mockProps.initialSegment.description, - isPrivate: mockProps.initialSegment.isPrivate, - filters: mockProps.initialSegment.filters, - }, - }); - }); - - // Verify getFormattedErrorMessage was called with the data returned from updateSegmentAction - expect(helper.getFormattedErrorMessage).toHaveBeenCalledWith(mockData); - - // Verify error toast was displayed with the recursive filter error message - expect(toast.error).toHaveBeenCalledWith(recursiveErrorMessage); - - // Verify that the update operation was halted (router.refresh and setOpen should not be called) - expect(mockProps.setOpen).not.toHaveBeenCalled(); - - // Verify that success toast was not displayed - expect(toast.success).not.toHaveBeenCalled(); - - // Verify that the button is no longer in loading state - // This is checking that setIsUpdatingSegment(false) was called - const updatedSaveButton = screen.getByTestId("save-button"); - expect(updatedSaveButton.getAttribute("data-loading")).not.toBe("true"); - }); - - test("should display server error message when updateSegmentAction returns a non-recursive filter error", async () => { - // Mock server error response - const serverErrorMessage = "Database connection error"; - vi.mocked(actions.updateSegmentAction).mockResolvedValue({ serverError: "Database connection error" }); - vi.mocked(helper.getFormattedErrorMessage).mockReturnValue(serverErrorMessage); - - // Render component - render(); - - // Find and click the save button - const saveButton = screen.getByTestId("save-button"); - fireEvent.click(saveButton); - - // Verify updateSegmentAction was called - await waitFor(() => { - expect(actions.updateSegmentAction).toHaveBeenCalled(); - }); - - // Verify getFormattedErrorMessage was called with the response from updateSegmentAction - expect(helper.getFormattedErrorMessage).toHaveBeenCalledWith({ - serverError: "Database connection error", - }); - - // Verify error toast was displayed with the server error message - expect(toast.error).toHaveBeenCalledWith(serverErrorMessage); - - // Verify that setOpen was not called (update process should stop) - expect(mockProps.setOpen).not.toHaveBeenCalled(); - - // Verify that the loading state was reset - const updatedSaveButton = screen.getByTestId("save-button"); - expect(updatedSaveButton.getAttribute("data-loading")).not.toBe("true"); - }); - - test("should add a filter to the segment when a valid filter is selected in the filter modal", async () => { - // Render component - render(); - - // Verify initial filter count is 0 - expect(screen.getByTestId("filter-count").textContent).toBe("0"); - - // Find and click the add filter button - const addFilterButton = screen.getByTestId("add-filter-button"); - fireEvent.click(addFilterButton); - - // Verify filter modal is open - expect(screen.getByTestId("add-filter-modal")).toBeInTheDocument(); - - // Select a filter from the modal - const addTestFilterButton = screen.getByTestId("add-test-filter"); - fireEvent.click(addTestFilterButton); - - // Verify filter modal is closed and filter is added - expect(screen.queryByTestId("add-filter-modal")).not.toBeInTheDocument(); - - // Verify filter count is now 1 - expect(screen.getByTestId("filter-count").textContent).toBe("1"); - - // Verify the save button is enabled - const saveButton = screen.getByTestId("save-button"); - expect(saveButton).not.toBeDisabled(); - - // Click save and verify the segment with the new filter is saved - fireEvent.click(saveButton); - - await waitFor(() => { - expect(actions.updateSegmentAction).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - filters: expect.arrayContaining([ - expect.objectContaining({ - type: "attribute", - attributeKey: "testKey", - connector: null, - }), - ]), - }), - }) - ); - }); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/segment-table-data-row-container.test.tsx b/apps/web/modules/ee/contacts/segments/components/segment-table-data-row-container.test.tsx deleted file mode 100644 index 9a1088e587..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/segment-table-data-row-container.test.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { cleanup } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { TSegment } from "@formbricks/types/segment"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { getSurveysBySegmentId } from "@/lib/survey/service"; -import { SegmentTableDataRow } from "./segment-table-data-row"; -import { SegmentTableDataRowContainer } from "./segment-table-data-row-container"; - -// Mock the child component -vi.mock("./segment-table-data-row", () => ({ - SegmentTableDataRow: vi.fn(() =>
    Mocked SegmentTableDataRow
    ), -})); - -// Mock the service function -vi.mock("@/lib/survey/service", () => ({ - getSurveysBySegmentId: vi.fn(), -})); - -const mockSegment: TSegment = { - id: "seg1", - title: "Segment 1", - description: "Description 1", - isPrivate: false, - filters: [], - environmentId: "env1", - createdAt: new Date(), - updatedAt: new Date(), - surveys: [], -}; - -const mockSegments: TSegment[] = [ - mockSegment, - { - id: "seg2", - title: "Segment 2", - description: "Description 2", - isPrivate: false, - filters: [], - environmentId: "env1", - createdAt: new Date(), - updatedAt: new Date(), - surveys: [], - }, -]; - -const mockContactAttributeKeys: TContactAttributeKey[] = [ - { key: "email", label: "Email" } as unknown as TContactAttributeKey, - { key: "userId", label: "User ID" } as unknown as TContactAttributeKey, -]; - -const mockSurveys: TSurvey[] = [ - { - id: "survey1", - name: "Active Survey 1", - status: "inProgress", - type: "link", - environmentId: "env1", - questions: [], - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - segment: null, - createdAt: new Date(), - updatedAt: new Date(), - languages: [], - variables: [], - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - hiddenFields: { enabled: false }, - styling: null, - singleUse: null, - pin: null, - surveyClosedMessage: null, - autoComplete: null, - createdBy: null, - } as unknown as TSurvey, - { - id: "survey2", - name: "Inactive Survey 1", - status: "draft", - type: "link", - environmentId: "env1", - questions: [], - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - segment: null, - createdAt: new Date(), - updatedAt: new Date(), - languages: [], - variables: [], - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - hiddenFields: { enabled: false }, - styling: null, - singleUse: null, - pin: null, - surveyClosedMessage: null, - autoComplete: null, - createdBy: null, - } as unknown as TSurvey, - { - id: "survey3", - name: "Inactive Survey 2", - status: "paused", - type: "link", - environmentId: "env1", - questions: [], - triggers: [], - recontactDays: null, - autoClose: null, - delay: 0, - displayOption: "displayOnce", - displayPercentage: null, - segment: null, - createdAt: new Date(), - updatedAt: new Date(), - languages: [], - variables: [], - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - hiddenFields: { enabled: false }, - styling: null, - productOverwrites: null, - singleUse: null, - pin: null, - surveyClosedMessage: null, - autoComplete: null, - createdBy: null, - } as unknown as TSurvey, -]; - -describe("SegmentTableDataRowContainer", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("fetches surveys, processes them, filters segments, and passes correct props", async () => { - vi.mocked(getSurveysBySegmentId).mockResolvedValue(mockSurveys); - - const result = await SegmentTableDataRowContainer({ - currentSegment: mockSegment, - segments: mockSegments, - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: true, - isReadOnly: false, - }); - - expect(getSurveysBySegmentId).toHaveBeenCalledWith(mockSegment.id); - - expect(result.type).toBe(SegmentTableDataRow); - expect(result.props).toEqual({ - currentSegment: { - ...mockSegment, - activeSurveys: ["Active Survey 1"], - inactiveSurveys: ["Inactive Survey 1", "Inactive Survey 2"], - }, - segments: mockSegments.filter((s) => s.id !== mockSegment.id), - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: true, - isReadOnly: false, - }); - }); - - test("handles case with no surveys found", async () => { - vi.mocked(getSurveysBySegmentId).mockResolvedValue([]); - - const result = await SegmentTableDataRowContainer({ - currentSegment: mockSegment, - segments: mockSegments, - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: false, - isReadOnly: true, - }); - - expect(getSurveysBySegmentId).toHaveBeenCalledWith(mockSegment.id); - - expect(result.type).toBe(SegmentTableDataRow); - expect(result.props).toEqual({ - currentSegment: { - ...mockSegment, - activeSurveys: [], - inactiveSurveys: [], - }, - segments: mockSegments.filter((s) => s.id !== mockSegment.id), - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: false, - isReadOnly: true, - }); - }); - - test("handles case where getSurveysBySegmentId returns null", async () => { - vi.mocked(getSurveysBySegmentId).mockResolvedValue(null as any); - - const result = await SegmentTableDataRowContainer({ - currentSegment: mockSegment, - segments: mockSegments, - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: true, - isReadOnly: false, - }); - - expect(getSurveysBySegmentId).toHaveBeenCalledWith(mockSegment.id); - - expect(result.type).toBe(SegmentTableDataRow); - expect(result.props).toEqual({ - currentSegment: { - ...mockSegment, - activeSurveys: [], - inactiveSurveys: [], - }, - segments: mockSegments.filter((s) => s.id !== mockSegment.id), - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: true, - isReadOnly: false, - }); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/segment-table-data-row.test.tsx b/apps/web/modules/ee/contacts/segments/components/segment-table-data-row.test.tsx deleted file mode 100644 index e5657ebb15..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/segment-table-data-row.test.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { format, formatDistanceToNow } from "date-fns"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { TSegmentWithSurveyNames } from "@formbricks/types/segment"; -import { EditSegmentModal } from "./edit-segment-modal"; -import { SegmentTableDataRow } from "./segment-table-data-row"; - -vi.mock("./edit-segment-modal", () => ({ - EditSegmentModal: vi.fn(() => null), -})); - -const mockCurrentSegment = { - id: "seg1", - title: "Test Segment", - description: "This is a test segment", - isPrivate: false, - filters: [], - environmentId: "env1", - surveys: ["survey1", "survey2"], - createdAt: new Date("2023-01-15T10:00:00.000Z"), - updatedAt: new Date("2023-01-20T12:00:00.000Z"), -} as unknown as TSegmentWithSurveyNames; - -const mockSegments = [mockCurrentSegment]; -const mockContactAttributeKeys = [{ key: "email", label: "Email" } as unknown as TContactAttributeKey]; -const mockIsContactsEnabled = true; -const mockIsReadOnly = false; - -describe("SegmentTableDataRow", () => { - afterEach(() => { - cleanup(); - }); - - test("renders segment data correctly", () => { - render( - - ); - - expect(screen.getByText(mockCurrentSegment.title)).toBeInTheDocument(); - expect(screen.getByText(mockCurrentSegment.description!)).toBeInTheDocument(); - expect(screen.getByText(mockCurrentSegment.surveys.length.toString())).toBeInTheDocument(); - expect( - screen.getByText( - formatDistanceToNow(mockCurrentSegment.updatedAt, { - addSuffix: true, - }).replace("about", "") - ) - ).toBeInTheDocument(); - expect(screen.getByText(format(mockCurrentSegment.createdAt, "do 'of' MMMM, yyyy"))).toBeInTheDocument(); - }); - - test("opens EditSegmentModal when row is clicked", async () => { - const user = userEvent.setup(); - render( - - ); - - const row = screen.getByText(mockCurrentSegment.title).closest("button.grid"); - expect(row).toBeInTheDocument(); - - // Initially modal should not be called with open: true - expect(vi.mocked(EditSegmentModal)).toHaveBeenCalledWith( - expect.objectContaining({ open: false }), - undefined // Expect undefined as the second argument - ); - - await user.click(row!); - - // After click, modal should be called with open: true - expect(vi.mocked(EditSegmentModal)).toHaveBeenCalledWith( - expect.objectContaining({ - open: true, - currentSegment: mockCurrentSegment, - environmentId: mockCurrentSegment.environmentId, - segments: mockSegments, - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: mockIsContactsEnabled, - isReadOnly: mockIsReadOnly, - }), - undefined // Expect undefined as the second argument - ); - }); - - test("passes isReadOnly prop correctly to EditSegmentModal", async () => { - const user = userEvent.setup(); - render( - - ); - - // Check initial call (open: false) - expect(vi.mocked(EditSegmentModal)).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - open: false, - isReadOnly: true, - }), - undefined // Expect undefined as the second argument - ); - - const row = screen.getByText(mockCurrentSegment.title).closest("button.grid"); - await user.click(row!); - - // Check second call (open: true) - expect(vi.mocked(EditSegmentModal)).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - open: true, - isReadOnly: true, - }), - undefined // Expect undefined as the second argument - ); - }); - - test("has focus styling for keyboard navigation", async () => { - const user = userEvent.setup(); - render( - - ); - - const row = screen.getByText(mockCurrentSegment.title).closest("button.grid"); - expect(row).toBeInTheDocument(); - - await user.tab(); - expect(document.activeElement).toBe(row); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/segment-table.test.tsx b/apps/web/modules/ee/contacts/segments/components/segment-table.test.tsx deleted file mode 100644 index 9365511982..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/segment-table.test.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { TSegment } from "@formbricks/types/segment"; -import { SegmentTable } from "./segment-table"; -import { SegmentTableDataRowContainer } from "./segment-table-data-row-container"; - -// Mock the getTranslate function -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -// Mock the SegmentTableDataRowContainer component -vi.mock("./segment-table-data-row-container", () => ({ - SegmentTableDataRowContainer: vi.fn(({ currentSegment }) => ( -
    {currentSegment.title}
    - )), -})); - -const mockSegments = [ - { - id: "1", - title: "Segment 1", - description: "Description 1", - isPrivate: false, - filters: [], - surveyIds: ["survey1", "survey2"], - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "env1", - }, - { - id: "2", - title: "Segment 2", - description: "Description 2", - isPrivate: true, - filters: [], - surveyIds: ["survey3"], - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "env1", - }, -] as unknown as TSegment[]; - -const mockContactAttributeKeys = [ - { key: "email", label: "Email" } as unknown as TContactAttributeKey, - { key: "userId", label: "User ID" } as unknown as TContactAttributeKey, -]; - -describe("SegmentTable", () => { - afterEach(() => { - cleanup(); - }); - - test("renders table headers", async () => { - render( - await SegmentTable({ - segments: [], - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: true, - isReadOnly: false, - }) - ); - - expect(screen.getByText("common.title")).toBeInTheDocument(); - expect(screen.getByText("common.surveys")).toBeInTheDocument(); - expect(screen.getByText("common.updated")).toBeInTheDocument(); - expect(screen.getByText("common.created")).toBeInTheDocument(); - }); - - test('renders "create your first segment" message when no segments are provided', async () => { - render( - await SegmentTable({ - segments: [], - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: true, - isReadOnly: false, - }) - ); - - expect(screen.getByText("environments.segments.create_your_first_segment")).toBeInTheDocument(); - }); - - test("renders segment rows when segments are provided", async () => { - render( - await SegmentTable({ - segments: mockSegments, - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: true, - isReadOnly: false, - }) - ); - - expect(screen.queryByText("environments.segments.create_your_first_segment")).not.toBeInTheDocument(); - expect(vi.mocked(SegmentTableDataRowContainer)).toHaveBeenCalledTimes(mockSegments.length); - - mockSegments.forEach((segment) => { - expect(screen.getByTestId(`segment-row-${segment.id}`)).toBeInTheDocument(); - expect(screen.getByText(segment.title)).toBeInTheDocument(); - // Check both arguments passed to the component - expect(vi.mocked(SegmentTableDataRowContainer)).toHaveBeenCalledWith( - expect.objectContaining({ - currentSegment: segment, - segments: mockSegments, - contactAttributeKeys: mockContactAttributeKeys, - isContactsEnabled: true, - isReadOnly: false, - }), - undefined // Explicitly check for the second argument being undefined - ); - }); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/components/targeting-card.test.tsx b/apps/web/modules/ee/contacts/segments/components/targeting-card.test.tsx deleted file mode 100644 index 6bfee2baae..0000000000 --- a/apps/web/modules/ee/contacts/segments/components/targeting-card.test.tsx +++ /dev/null @@ -1,414 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { TSegment } from "@formbricks/types/segment"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TargetingCard } from "@/modules/ee/contacts/segments/components/targeting-card"; - -// Mock Data (Moved from mocks.ts) -const mockInitialSegment: TSegment = { - id: "segment-1", - title: "Initial Segment", - description: "Initial segment description", - isPrivate: false, - filters: [ - { - id: "base-filter-1", // ID for the base filter group/node - connector: "and", - resource: { - // This holds the actual filter condition (TSegmentFilter) - id: "segment-filter-1", // ID for the specific filter rule - root: { - type: "attribute", - contactAttributeKey: "attr1", - }, - qualifier: { - operator: "equals", - }, - value: "value1", - }, - }, - ], - surveys: ["survey-1"], - environmentId: "test-env-id", - createdAt: new Date(), - updatedAt: new Date(), -}; - -const mockSurvey = { - id: "survey-1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Survey", - type: "app", // Changed from "link" to "web" - environmentId: "test-env-id", - status: "inProgress", - questions: [], - displayOption: "displayOnce", - recontactDays: 7, - autoClose: null, - delay: 0, - displayPercentage: 100, - autoComplete: null, - surveyClosedMessage: null, - segment: mockInitialSegment, - languages: [], - triggers: [], - pin: null, - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - singleUse: null, - styling: null, -} as unknown as TSurvey; - -const mockContactAttributeKeys: TContactAttributeKey[] = [ - { id: "attr1", description: "Desc 1", type: "default" } as unknown as TContactAttributeKey, - { id: "attr2", description: "Desc 2", type: "default" } as unknown as TContactAttributeKey, -]; - -const mockSegments: TSegment[] = [ - mockInitialSegment, - { - id: "segment-2", - title: "Segment 2", - description: "Segment 2 description", - isPrivate: true, - filters: [], - surveys: ["survey-2"], - environmentId: "test-env-id", - createdAt: new Date(), - updatedAt: new Date(), - }, -]; -// End Mock Data - -// Mock actions -const mockCloneSegmentAction = vi.fn(); -const mockCreateSegmentAction = vi.fn(); -const mockLoadNewSegmentAction = vi.fn(); -const mockResetSegmentFiltersAction = vi.fn(); -const mockUpdateSegmentAction = vi.fn(); - -vi.mock("@/modules/ee/contacts/segments/actions", () => ({ - cloneSegmentAction: (...args) => mockCloneSegmentAction(...args), - createSegmentAction: (...args) => mockCreateSegmentAction(...args), - loadNewSegmentAction: (...args) => mockLoadNewSegmentAction(...args), - resetSegmentFiltersAction: (...args) => mockResetSegmentFiltersAction(...args), - updateSegmentAction: (...args) => mockUpdateSegmentAction(...args), -})); - -// Mock components -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children }) =>
    {children}
    , - AlertDescription: ({ children }) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/alert-dialog", () => ({ - // Update the mock to render headerText - AlertDialog: ({ children, open, headerText }) => - open ? ( -
    - AlertDialog Mock {headerText} {children} -
    - ) : null, -})); -vi.mock("@/modules/ui/components/load-segment-modal", () => ({ - LoadSegmentModal: ({ open }) => (open ?
    LoadSegmentModal Mock
    : null), -})); -vi.mock("@/modules/ui/components/save-as-new-segment-modal", () => ({ - SaveAsNewSegmentModal: ({ open }) => (open ?
    SaveAsNewSegmentModal Mock
    : null), -})); -vi.mock("@/modules/ui/components/segment-title", () => ({ - SegmentTitle: ({ title, description }) => ( -
    - SegmentTitle Mock: {title} {description} -
    - ), -})); -vi.mock("@/modules/ui/components/targeting-indicator", () => ({ - TargetingIndicator: () =>
    TargetingIndicator Mock
    , -})); -vi.mock("./add-filter-modal", () => ({ - AddFilterModal: ({ open }) => (open ?
    AddFilterModal Mock
    : null), -})); -vi.mock("./segment-editor", () => ({ - SegmentEditor: ({ viewOnly }) =>
    SegmentEditor Mock {viewOnly ? "(View Only)" : "(Editable)"}
    , -})); - -// Mock hooks -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -const mockSetLocalSurvey = vi.fn(); -const environmentId = "test-env-id"; - -describe("TargetingCard", () => { - afterEach(() => { - cleanup(); - }); - - beforeEach(() => { - // Reset mocks before each test if needed - mockCloneSegmentAction.mockResolvedValue({ data: { ...mockInitialSegment, id: "cloned-segment-id" } }); - mockResetSegmentFiltersAction.mockResolvedValue({ data: { ...mockInitialSegment, filters: [] } }); - mockUpdateSegmentAction.mockResolvedValue({ data: mockInitialSegment }); - }); - - test("renders null for link surveys", () => { - const linkSurvey: TSurvey = { ...mockSurvey, type: "link" }; - const { container } = render( - - ); - expect(container.firstChild).toBeNull(); - }); - - test("renders correctly for web/app surveys", () => { - render( - - ); - expect(screen.getByText("environments.segments.target_audience")).toBeInTheDocument(); - expect(screen.getByText("environments.segments.pre_segment_users")).toBeInTheDocument(); - }); - - test("opens and closes collapsible content", async () => { - const user = userEvent.setup(); - render( - - ); - - // Initially open because segment has filters - expect(screen.getByText("TargetingIndicator Mock")).toBeVisible(); - - // Click trigger to close (assuming it's open) - await user.click(screen.getByText("environments.segments.target_audience")); - // Check that the element is no longer in the document - expect(screen.queryByText("TargetingIndicator Mock")).not.toBeInTheDocument(); - - // Click trigger to open - await user.click(screen.getByText("environments.segments.target_audience")); - expect(screen.getByText("TargetingIndicator Mock")).toBeVisible(); - }); - - test("opens Add Filter modal", async () => { - const user = userEvent.setup(); - render( - - ); - await user.click(screen.getByText("common.add_filter")); - expect(screen.getByText("AddFilterModal Mock")).toBeInTheDocument(); - }); - - test("opens Load Segment modal", async () => { - const user = userEvent.setup(); - render( - - ); - await user.click(screen.getByText("environments.segments.load_segment")); - expect(screen.getByText("LoadSegmentModal Mock")).toBeInTheDocument(); - }); - - test("opens Reset All Filters confirmation dialog", async () => { - const user = userEvent.setup(); - render( - - ); - await user.click(screen.getByText("environments.segments.reset_all_filters")); - // Check that the mock container with the text exists - expect(screen.getByText(/AlertDialog Mock\s*common.are_you_sure/)).toBeInTheDocument(); - // Use regex to find the specific text, ignoring whitespace - expect(screen.getByText(/common\.are_you_sure/)).toBeInTheDocument(); - }); - - test("toggles segment editor view", async () => { - const user = userEvent.setup(); - render( - - ); - - // Initially view only, editor is visible - expect(screen.getByText("SegmentEditor Mock (View Only)")).toBeInTheDocument(); - expect(screen.getByText("environments.segments.hide_filters")).toBeInTheDocument(); - - // Click to hide filters - await user.click(screen.getByText("environments.segments.hide_filters")); - // Editor should now be removed from the DOM - expect(screen.queryByText("SegmentEditor Mock (View Only)")).not.toBeInTheDocument(); - // Button text should change to "View Filters" - expect(screen.getByText("environments.segments.view_filters")).toBeInTheDocument(); - expect(screen.queryByText("environments.segments.hide_filters")).not.toBeInTheDocument(); - - // Click again to show filters - await user.click(screen.getByText("environments.segments.view_filters")); - // Editor should be back in the DOM - expect(screen.getByText("SegmentEditor Mock (View Only)")).toBeInTheDocument(); - // Button text should change back to "Hide Filters" - expect(screen.getByText("environments.segments.hide_filters")).toBeInTheDocument(); - expect(screen.queryByText("environments.segments.view_filters")).not.toBeInTheDocument(); - }); - - test("opens segment editor on 'Edit Segment' click", async () => { - const user = userEvent.setup(); - render( - - ); - - expect(screen.getByText("SegmentEditor Mock (View Only)")).toBeInTheDocument(); - await user.click(screen.getByText("environments.segments.edit_segment")); - expect(screen.getByText("SegmentEditor Mock (Editable)")).toBeInTheDocument(); - expect(screen.getByText("common.add_filter")).toBeInTheDocument(); // Editor controls visible - }); - - test("calls clone action on 'Clone and Edit Segment' click", async () => { - const user = userEvent.setup(); - const surveyWithSharedSegment: TSurvey = { - ...mockSurvey, - segment: { ...mockInitialSegment, surveys: ["survey1", "survey2"] }, // Used in > 1 survey - }; - render( - - ); - - expect( - screen.getByText("environments.segments.this_segment_is_used_in_other_surveys") - ).toBeInTheDocument(); - await user.click(screen.getByText("environments.segments.clone_and_edit_segment")); - expect(mockCloneSegmentAction).toHaveBeenCalledWith({ - segmentId: mockInitialSegment.id, - surveyId: mockSurvey.id, - }); - // Check if setSegment was called (indirectly via useEffect) - // We need to wait for the promise to resolve and state update - // await vi.waitFor(() => expect(mockSetLocalSurvey).toHaveBeenCalled()); // This might be tricky due to internal state - }); - - test("opens Save As New Segment modal when editor is open", async () => { - const user = userEvent.setup(); - render( - - ); - await user.click(screen.getByText("environments.segments.save_as_new_segment")); - expect(screen.getByText("SaveAsNewSegmentModal Mock")).toBeInTheDocument(); - }); - - test("calls update action on 'Save Changes' click (non-private segment)", async () => { - const user = userEvent.setup(); - render( - - ); - - // Open editor - await user.click(screen.getByText("environments.segments.edit_segment")); - expect(screen.getByText("SegmentEditor Mock (Editable)")).toBeInTheDocument(); - - // Click save - await user.click(screen.getByText("common.save_changes")); - expect(mockUpdateSegmentAction).toHaveBeenCalledWith({ - segmentId: mockInitialSegment.id, - environmentId: environmentId, - data: { filters: mockInitialSegment.filters }, - }); - }); - - test("closes editor on 'Cancel' click (non-private segment)", async () => { - const user = userEvent.setup(); - render( - - ); - - // Open editor - await user.click(screen.getByText("environments.segments.edit_segment")); - expect(screen.getByText("SegmentEditor Mock (Editable)")).toBeInTheDocument(); - - // Click cancel - await user.click(screen.getByText("common.cancel")); - expect(screen.getByText("SegmentEditor Mock (View Only)")).toBeInTheDocument(); - expect(screen.queryByText("common.add_filter")).not.toBeInTheDocument(); // Editor controls hidden - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/loading.test.tsx b/apps/web/modules/ee/contacts/segments/loading.test.tsx deleted file mode 100644 index f0d71c8260..0000000000 --- a/apps/web/modules/ee/contacts/segments/loading.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Loading from "./loading"; - -// Mock the getTranslate function -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -// Mock the ContactsSecondaryNavigation component -vi.mock("@/modules/ee/contacts/components/contacts-secondary-navigation", () => ({ - ContactsSecondaryNavigation: () =>
    ContactsSecondaryNavigation
    , -})); - -describe("Loading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders loading state correctly", async () => { - render(await Loading()); - - // Check for the presence of the secondary navigation mock - expect(screen.getByText("ContactsSecondaryNavigation")).toBeInTheDocument(); - - // Check for table headers based on tolgee keys - expect(screen.getByText("common.title")).toBeInTheDocument(); - expect(screen.getByText("common.surveys")).toBeInTheDocument(); - expect(screen.getByText("common.updated_at")).toBeInTheDocument(); - expect(screen.getByText("common.created_at")).toBeInTheDocument(); - - // Check for the presence of multiple skeleton loaders (at least one) - const skeletonLoaders = screen.getAllByRole("generic", { name: "" }); // Assuming skeleton divs don't have specific roles/names - // Filter for elements with animate-pulse class - const pulseElements = skeletonLoaders.filter((el) => el.classList.contains("animate-pulse")); - expect(pulseElements.length).toBeGreaterThan(0); - }); -}); diff --git a/apps/web/modules/ee/contacts/segments/page.test.tsx b/apps/web/modules/ee/contacts/segments/page.test.tsx deleted file mode 100644 index 7c55d07118..0000000000 --- a/apps/web/modules/ee/contacts/segments/page.test.tsx +++ /dev/null @@ -1,220 +0,0 @@ -// Import the actual constants module to get its type/shape for mocking -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { TSegment } from "@formbricks/types/segment"; -import * as constants from "@/lib/constants"; -import { ContactsSecondaryNavigation } from "@/modules/ee/contacts/components/contacts-secondary-navigation"; -import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys"; -import { SegmentTable } from "@/modules/ee/contacts/segments/components/segment-table"; -import { getSegments } from "@/modules/ee/contacts/segments/lib/segments"; -import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; -import { PageHeader } from "@/modules/ui/components/page-header"; -import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt"; -import { getTranslate } from "@/tolgee/server"; -import { CreateSegmentModal } from "./components/create-segment-modal"; -import { SegmentsPage } from "./page"; - -// Mock dependencies -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: true, -})); - -vi.mock("@/modules/ee/contacts/components/contacts-secondary-navigation", () => ({ - ContactsSecondaryNavigation: vi.fn(() =>
    ContactsSecondaryNavigation
    ), -})); - -vi.mock("@/modules/ee/contacts/lib/contact-attribute-keys", () => ({ - getContactAttributeKeys: vi.fn(), -})); - -vi.mock("@/modules/ee/contacts/segments/components/segment-table", () => ({ - SegmentTable: vi.fn(() =>
    SegmentTable
    ), -})); - -vi.mock("@/modules/ee/contacts/segments/lib/segments", () => ({ - getSegments: vi.fn(), -})); - -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getIsContactsEnabled: vi.fn(), -})); - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: vi.fn(({ children }) =>
    {children}
    ), -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: vi.fn(({ children, cta }) => ( -
    - PageHeader - {cta} - {children} -
    - )), -})); - -vi.mock("@/modules/ui/components/upgrade-prompt", () => ({ - UpgradePrompt: vi.fn(() =>
    UpgradePrompt
    ), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(), -})); - -vi.mock("./components/create-segment-modal", () => ({ - CreateSegmentModal: vi.fn(() =>
    CreateSegmentModal
    ), -})); - -const mockEnvironmentId = "test-env-id"; -const mockParams = { environmentId: mockEnvironmentId }; -const mockSegments = [ - { id: "seg1", title: "Segment 1", isPrivate: false, filters: [], surveys: [] }, - { id: "seg2", title: "Segment 2", isPrivate: true, filters: [], surveys: [] }, - { id: "seg3", title: "Segment 3", isPrivate: false, filters: [], surveys: [] }, -] as unknown as TSegment[]; -const mockFilteredSegments = mockSegments.filter((s) => !s.isPrivate); -const mockContactAttributeKeys = [{ name: "email", type: "text" } as unknown as TContactAttributeKey]; -const mockT = vi.fn((key) => key); // Simple mock translation function - -describe("SegmentsPage", () => { - beforeEach(() => { - vi.resetAllMocks(); - // Explicitly set the mocked constant value before each test if needed, - // otherwise it defaults to the value in vi.mock - vi.mocked(constants).IS_FORMBRICKS_CLOUD = true; - - vi.mocked(getTranslate).mockResolvedValue(mockT); - vi.mocked(getSegments).mockResolvedValue(mockSegments); - vi.mocked(getContactAttributeKeys).mockResolvedValue(mockContactAttributeKeys); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders segment table and create button when contacts enabled and not read-only", async () => { - vi.mocked(getIsContactsEnabled).mockResolvedValue(true); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ isReadOnly: false } as TEnvironmentAuth); - - const promise = Promise.resolve(mockParams); - render(await SegmentsPage({ params: promise })); - - await screen.findByText("PageHeader"); // Wait for async component to render - - expect(screen.getByText("PageHeader")).toBeInTheDocument(); - expect(screen.getByText("ContactsSecondaryNavigation")).toBeInTheDocument(); - expect(screen.getByText("CreateSegmentModal")).toBeInTheDocument(); - expect(screen.getByText("SegmentTable")).toBeInTheDocument(); - expect(screen.queryByText("UpgradePrompt")).not.toBeInTheDocument(); - - expect(vi.mocked(PageHeader).mock.calls[0][0].pageTitle).toBe("Contacts"); - expect(vi.mocked(ContactsSecondaryNavigation).mock.calls[0][0].activeId).toBe("segments"); - expect(vi.mocked(ContactsSecondaryNavigation).mock.calls[0][0].environmentId).toBe(mockEnvironmentId); - expect(vi.mocked(CreateSegmentModal).mock.calls[0][0].environmentId).toBe(mockEnvironmentId); - expect(vi.mocked(CreateSegmentModal).mock.calls[0][0].contactAttributeKeys).toEqual( - mockContactAttributeKeys - ); - expect(vi.mocked(CreateSegmentModal).mock.calls[0][0].segments).toEqual(mockFilteredSegments); - expect(vi.mocked(SegmentTable).mock.calls[0][0].segments).toEqual(mockFilteredSegments); - expect(vi.mocked(SegmentTable).mock.calls[0][0].contactAttributeKeys).toEqual(mockContactAttributeKeys); - expect(vi.mocked(SegmentTable).mock.calls[0][0].isContactsEnabled).toBe(true); - expect(vi.mocked(SegmentTable).mock.calls[0][0].isReadOnly).toBe(false); - }); - - test("renders segment table without create button when contacts enabled and read-only", async () => { - vi.mocked(getIsContactsEnabled).mockResolvedValue(true); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ isReadOnly: true } as TEnvironmentAuth); - - const promise = Promise.resolve(mockParams); - render(await SegmentsPage({ params: promise })); - - await screen.findByText("PageHeader"); - - expect(screen.getByText("PageHeader")).toBeInTheDocument(); - expect(screen.getByText("ContactsSecondaryNavigation")).toBeInTheDocument(); - expect(screen.queryByText("CreateSegmentModal")).not.toBeInTheDocument(); // CTA should be undefined - expect(screen.getByText("SegmentTable")).toBeInTheDocument(); - expect(screen.queryByText("UpgradePrompt")).not.toBeInTheDocument(); - - expect(vi.mocked(SegmentTable).mock.calls[0][0].isReadOnly).toBe(true); - }); - - test("renders upgrade prompt when contacts disabled (Cloud)", async () => { - vi.mocked(getIsContactsEnabled).mockResolvedValue(false); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ isReadOnly: false } as TEnvironmentAuth); - - const promise = Promise.resolve(mockParams); - render(await SegmentsPage({ params: promise })); - - await screen.findByText("PageHeader"); - - expect(screen.getByText("PageHeader")).toBeInTheDocument(); - expect(screen.getByText("ContactsSecondaryNavigation")).toBeInTheDocument(); - expect(screen.queryByText("CreateSegmentModal")).not.toBeInTheDocument(); - expect(screen.queryByText("SegmentTable")).not.toBeInTheDocument(); - expect(screen.getByText("UpgradePrompt")).toBeInTheDocument(); - - expect(vi.mocked(UpgradePrompt).mock.calls[0][0].title).toBe( - "environments.segments.unlock_segments_title" - ); - expect(vi.mocked(UpgradePrompt).mock.calls[0][0].description).toBe( - "environments.segments.unlock_segments_description" - ); - expect(vi.mocked(UpgradePrompt).mock.calls[0][0].buttons).toEqual([ - { - text: "common.start_free_trial", - href: `/environments/${mockEnvironmentId}/settings/billing`, - }, - { - text: "common.learn_more", - href: `/environments/${mockEnvironmentId}/settings/billing`, - }, - ]); - }); - - test("renders upgrade prompt when contacts disabled (Self-hosted)", async () => { - // Modify the mocked constant for this specific test - vi.mocked(constants).IS_FORMBRICKS_CLOUD = false; - vi.mocked(getIsContactsEnabled).mockResolvedValue(false); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ isReadOnly: false } as TEnvironmentAuth); - - const promise = Promise.resolve(mockParams); - render(await SegmentsPage({ params: promise })); - - await screen.findByText("PageHeader"); - - expect(screen.getByText("PageHeader")).toBeInTheDocument(); - expect(screen.getByText("ContactsSecondaryNavigation")).toBeInTheDocument(); - expect(screen.queryByText("CreateSegmentModal")).not.toBeInTheDocument(); - expect(screen.queryByText("SegmentTable")).not.toBeInTheDocument(); - expect(screen.getByText("UpgradePrompt")).toBeInTheDocument(); - - expect(vi.mocked(UpgradePrompt).mock.calls[0][0].buttons).toEqual([ - { - text: "common.request_trial_license", - href: "https://formbricks.com/upgrade-self-hosting-license", - }, - { - text: "common.learn_more", - href: "https://formbricks.com/learn-more-self-hosting-license", - }, - ]); - }); - - test("throws error if getSegments returns null", async () => { - // Change mockResolvedValue from [] to null to trigger the error condition - vi.mocked(getSegments).mockResolvedValue(null as any); - vi.mocked(getIsContactsEnabled).mockResolvedValue(true); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ isReadOnly: false } as TEnvironmentAuth); - - const promise = Promise.resolve(mockParams); - await expect(SegmentsPage({ params: promise })).rejects.toThrow("Failed to fetch segments"); - }); -}); diff --git a/apps/web/modules/ee/quotas/components/ending-card-selector.test.tsx b/apps/web/modules/ee/quotas/components/ending-card-selector.test.tsx deleted file mode 100644 index 45f2aad9ed..0000000000 --- a/apps/web/modules/ee/quotas/components/ending-card-selector.test.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { EndingCardSelector } from "./ending-card-selector"; - -// Mock Radix UI Select components -vi.mock("@/modules/ui/components/select", () => ({ - Select: ({ children, value, onValueChange }: any) => ( -
    onValueChange?.("test-value")}> - {children} -
    - ), - SelectContent: ({ children }: any) =>
    {children}
    , - SelectGroup: ({ children }: any) =>
    {children}
    , - SelectItem: ({ children, value }: any) => ( -
    - {children} -
    - ), - SelectTrigger: ({ children }: any) =>
    {children}
    , - SelectValue: ({ placeholder }: any) =>
    {placeholder}
    , -})); - -// Mock localization utils -vi.mock("@/lib/i18n/utils", () => ({ - getLocalizedValue: (value: any, locale: string) => { - if (typeof value === "object" && value !== null) { - return value[locale] || value.default || "Test Headline"; - } - return value || "Test Headline"; - }, -})); - -describe("EndingCardSelector", () => { - const mockOnChange = vi.fn(); - - const mockSurveyWithEndings: TSurvey = { - id: "survey1", - endings: [ - { - id: "ending1", - type: "endScreen", - headline: { default: "Thank you!" }, - subheader: { default: "Survey complete" }, - }, - { - id: "ending2", - type: "endScreen", - headline: { default: "Survey Complete" }, - subheader: { default: "Thanks for participating" }, - }, - { - id: "redirect1", - type: "redirectToUrl", - url: "https://example.com", - }, - { - id: "redirect2", - type: "redirectToUrl", - url: "https://test.com", - }, - ], - } as TSurvey; - - const mockSurveyEmpty: TSurvey = { - id: "survey2", - endings: [], - } as unknown as TSurvey; - - beforeEach(() => { - mockOnChange.mockClear(); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders select component", () => { - render(); - - expect(screen.getByTestId("select")).toBeInTheDocument(); - expect(screen.getByTestId("select-trigger")).toBeInTheDocument(); - expect(screen.getByTestId("select-value")).toBeInTheDocument(); - }); - - test("shows placeholder when no value selected", () => { - render(); - - expect(screen.getByText("environments.surveys.edit.quotas.select_ending_card")).toBeInTheDocument(); - }); - - test("displays ending card options", () => { - render(); - - // Should show ending card section - expect(screen.getByText("common.ending_card")).toBeInTheDocument(); - - // Should show ending card items - const endingItems = screen.getAllByTestId("select-item"); - const endingCardItems = endingItems.filter( - (item) => item.getAttribute("data-value") === "ending1" || item.getAttribute("data-value") === "ending2" - ); - expect(endingCardItems).toHaveLength(2); - }); - - test("displays redirect URL options", () => { - render(); - - // Should show redirect URL section - expect(screen.getByText("environments.surveys.edit.redirect_to_url")).toBeInTheDocument(); - - // Should show redirect items with generic labels - expect(screen.getByText("environments.surveys.edit.redirect_to_url")).toBeInTheDocument(); - }); - - test("calls onChange when selection is made", async () => { - const user = userEvent.setup(); - - render(); - - const select = screen.getByTestId("select"); - await user.click(select); - - expect(mockOnChange).toHaveBeenCalledWith("test-value"); - }); - - test("handles survey with no endings", () => { - render(); - - expect(screen.getByTestId("select")).toBeInTheDocument(); - expect(screen.queryByText("common.ending_card")).not.toBeInTheDocument(); - expect(screen.queryByText("environments.surveys.edit.redirect_to_url")).not.toBeInTheDocument(); - }); - - test("filters endings correctly by type", () => { - render(); - - // Should only show endScreen endings in ending card section - const endingCardSection = screen.getByText("common.ending_card").closest("[data-testid='select-group']"); - expect(endingCardSection).toBeInTheDocument(); - - // Should only show redirectToUrl endings in redirect section - const redirectSection = screen - .getByText("environments.surveys.edit.redirect_to_url") - .closest("[data-testid='select-group']"); - expect(redirectSection).toBeInTheDocument(); - }); - - test("shows correct value when selected", () => { - render( - - ); - - const select = screen.getByTestId("select"); - expect(select).toHaveAttribute("data-value", "ending1"); - }); - - test("handles ending without headline gracefully", () => { - const surveyWithEndingNoHeadline: TSurvey = { - id: "survey4", - endings: [ - { - id: "ending3", - type: "endScreen", - subheader: { default: "Just subheader" }, - }, - ], - } as unknown as TSurvey; - - render( - - ); - - // Should still render the component without errors - expect(screen.getByTestId("select")).toBeInTheDocument(); - expect(screen.getByText("common.ending_card")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/quotas/components/quota-condition-builder.test.tsx b/apps/web/modules/ee/quotas/components/quota-condition-builder.test.tsx deleted file mode 100644 index 0414e23577..0000000000 --- a/apps/web/modules/ee/quotas/components/quota-condition-builder.test.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurveyQuotaLogic } from "@formbricks/types/quota"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { QuotaConditionBuilder } from "./quota-condition-builder"; - -// Mock the ConditionsEditor component -vi.mock("@/modules/ui/components/conditions-editor", () => ({ - ConditionsEditor: ({ conditions, config, callbacks }: any) => ( -
    -
    {JSON.stringify(conditions)}
    -
    {JSON.stringify(config)}
    - -
    - ), -})); - -// Mock the shared conditions factory -vi.mock("@/modules/survey/editor/lib/shared-conditions-factory", () => ({ - quotaConditionsToGeneric: vi.fn((conditions) => ({ - id: "root", - connector: conditions.connector, - conditions: conditions.conditions, - })), - genericConditionsToQuota: vi.fn((genericConditions) => ({ - connector: genericConditions.connector, - conditions: genericConditions.conditions, - })), - createSharedConditionsFactory: vi.fn(() => ({ - config: { - getLeftOperandOptions: vi.fn(), - getOperatorOptions: vi.fn(), - getValueProps: vi.fn(), - getDefaultOperator: vi.fn(() => "equals"), - formatLeftOperandValue: vi.fn(), - }, - callbacks: { - onAddConditionBelow: vi.fn(), - onRemoveCondition: vi.fn(), - onDuplicateCondition: vi.fn(), - onUpdateCondition: vi.fn(), - onToggleGroupConnector: vi.fn(), - }, - })), -})); - -// Mock @tolgee/react -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -// Mock @paralleldrive/cuid2 -vi.mock("@paralleldrive/cuid2", () => ({ - createId: () => "test-id-123", -})); - -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, - toast: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -describe("QuotaConditionBuilder", () => { - const mockOnChange = vi.fn(); - - const mockSurvey: TSurvey = { - id: "survey1", - questions: [ - { - id: "q1", - type: "openText", - headline: { default: "What is your name?" }, - required: false, - inputType: "text", - }, - { - id: "q2", - type: "multipleChoiceSingle", - headline: { default: "Choose an option" }, - required: false, - choices: [ - { id: "choice1", label: { default: "Option 1" } }, - { id: "choice2", label: { default: "Option 2" } }, - ], - }, - ], - } as unknown as TSurvey; - - const mockConditions: TSurveyQuotaLogic = { - connector: "and", - conditions: [ - { - id: "condition1", - leftOperand: { type: "question", value: "q1" }, - operator: "equals", - rightOperand: { type: "static", value: "test" }, - }, - ], - }; - - const mockEmptyConditions: TSurveyQuotaLogic = { - connector: "and", - conditions: [], - }; - - beforeEach(() => { - mockOnChange.mockClear(); - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders conditions editor", () => { - render(); - - expect(screen.getByTestId("conditions-editor")).toBeInTheDocument(); - }); - - test("passes converted conditions to editor", async () => { - render(); - - const conditionsData = screen.getByTestId("conditions-data"); - expect(conditionsData).toBeInTheDocument(); - }); - - test("creates configuration for conditions editor", async () => { - const { createSharedConditionsFactory } = await vi.importMock( - "@/modules/survey/editor/lib/shared-conditions-factory" - ); - - render(); - - const configData = screen.getByTestId("config-data"); - expect(configData).toBeInTheDocument(); - - // Verify that createSharedConditionsFactory was called - expect(createSharedConditionsFactory).toHaveBeenCalled(); - }); - - test("does not initialize when conditions already exist", () => { - render(); - - // Should not call onChange for initialization when conditions exist - expect(mockOnChange).not.toHaveBeenCalled(); - }); - - test("does not initialize when survey has no questions", () => { - const surveyWithoutQuestions = { - ...mockSurvey, - questions: [], - }; - - render( - - ); - - // Should not call onChange when no questions available - expect(mockOnChange).not.toHaveBeenCalled(); - }); - - test("creates callbacks for conditions editor", async () => { - const { createSharedConditionsFactory } = await vi.importMock( - "@/modules/survey/editor/lib/shared-conditions-factory" - ); - - render(); - - // Verify that createSharedConditionsFactory was called which creates both config and callbacks - expect(createSharedConditionsFactory).toHaveBeenCalled(); - }); - - test("handles conditions with different connectors", async () => { - const { quotaConditionsToGeneric } = await vi.importMock( - "@/modules/survey/editor/lib/shared-conditions-factory" - ); - - const orConditions: TSurveyQuotaLogic = { - connector: "or", - conditions: [ - { - id: "condition1", - leftOperand: { type: "question", value: "q1" }, - operator: "contains", - rightOperand: { type: "static", value: "test" }, - }, - ], - }; - - render(); - - expect(quotaConditionsToGeneric).toHaveBeenCalledWith(orConditions); - }); - - test("handles multiple criteria", async () => { - const { quotaConditionsToGeneric } = await vi.importMock( - "@/modules/survey/editor/lib/shared-conditions-factory" - ); - - const multipleConditions: TSurveyQuotaLogic = { - connector: "and", - conditions: [ - { - id: "condition1", - leftOperand: { type: "question", value: "q1" }, - operator: "equals", - rightOperand: { type: "static", value: "test1" }, - }, - { - id: "condition2", - leftOperand: { type: "question", value: "q2" }, - operator: "contains", - rightOperand: { type: "static", value: "test2" }, - }, - ], - }; - - render( - - ); - - expect(screen.getByTestId("conditions-editor")).toBeInTheDocument(); - - expect(quotaConditionsToGeneric).toHaveBeenCalledWith(multipleConditions); - }); -}); diff --git a/apps/web/modules/ee/quotas/components/quota-list.test.tsx b/apps/web/modules/ee/quotas/components/quota-list.test.tsx deleted file mode 100644 index 4cc70e294d..0000000000 --- a/apps/web/modules/ee/quotas/components/quota-list.test.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurveyQuota, TSurveyQuotaInput } from "@formbricks/types/quota"; -import { QuotaList } from "./quota-list"; - -// Mock the createQuotaAction -vi.mock("@/modules/ee/quotas/actions", () => ({ - createQuotaAction: (quota: TSurveyQuotaInput) => { - return { - data: { - ...quota, - }, - }; - }, -})); - -// Mock Next.js router -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - refresh: vi.fn(), - push: vi.fn(), - replace: vi.fn(), - }), -})); - -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -// Mock UI components -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, className, variant, size, ...props }: any) => ( - - ), -})); - -vi.mock("@radix-ui/react-dropdown-menu", () => ({ - Label: ({ children, className }: any) => , -})); - -describe("QuotaList", () => { - const mockOnEdit = vi.fn(); - const mockDeleteQuota = vi.fn(); - const mockDuplicateQuota = vi.fn(); - - const mockQuotas: TSurveyQuota[] = [ - { - id: "quota1", - surveyId: "survey1", - name: "Test Quota 1", - limit: 100, - logic: { - connector: "and", - conditions: [], - }, - action: "endSurvey", - endingCardId: null, - countPartialSubmissions: false, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: "quota2", - surveyId: "survey1", - name: "Test Quota 2", - limit: 50, - logic: { - connector: "or", - conditions: [], - }, - action: "continueSurvey", - endingCardId: "ending1", - countPartialSubmissions: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - ]; - - beforeEach(() => { - mockOnEdit.mockClear(); - mockDeleteQuota.mockClear(); - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders list of quotas", () => { - render( - - ); - - expect(screen.getByText("Test Quota 1")).toBeInTheDocument(); - expect(screen.getByText("Test Quota 2")).toBeInTheDocument(); - }); - - test("calls onEdit when quota item is clicked", async () => { - const user = userEvent.setup(); - - render( - - ); - - const quotaItem = screen.getByText("Test Quota 1").closest("div"); - expect(quotaItem).toBeInTheDocument(); - - await user.click(quotaItem!); - - expect(mockOnEdit).toHaveBeenCalledWith(mockQuotas[0]); - }); - - test("renders empty list when no quotas", () => { - render( - - ); - - expect(screen.queryByText("Test Quota 1")).not.toBeInTheDocument(); - expect(screen.queryByText("Test Quota 2")).not.toBeInTheDocument(); - }); - - test("renders quota items with correct styling classes", () => { - render( - - ); - - const quotaItems = screen - .getAllByRole("button") - .filter((button) => button.className?.includes("cursor-pointer")); - - quotaItems.forEach((item) => { - expect(item).toHaveClass("cursor-pointer"); - expect(item).toHaveClass("rounded-lg"); - expect(item).toHaveClass("bg-slate-50"); - }); - }); - - test("renders action buttons with correct variants", () => { - render( - - ); - - const actionButtons = screen - .getAllByRole("button") - .filter((button) => button.getAttribute("data-variant") === "ghost"); - - expect(actionButtons.length).toBeGreaterThan(0); - - actionButtons.forEach((button) => { - expect(button).toHaveAttribute("data-variant", "ghost"); - expect(button).toHaveAttribute("data-size", "sm"); - }); - }); - - test("handles quota with special characters in name", () => { - const quotaWithSpecialChars: TSurveyQuota = { - ...mockQuotas[0], - name: "Test Quota with @#$%^&*()_+ characters", - }; - - render( - - ); - - expect(screen.getByText("Test Quota with @#$%^&*()_+ characters")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/quotas/components/quota-modal.test.tsx b/apps/web/modules/ee/quotas/components/quota-modal.test.tsx deleted file mode 100644 index 4015c3eec1..0000000000 --- a/apps/web/modules/ee/quotas/components/quota-modal.test.tsx +++ /dev/null @@ -1,645 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurveyQuota } from "@formbricks/types/quota"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { createQuotaAction, updateQuotaAction } from "@/modules/ee/quotas/actions"; -import { QuotaModal } from "./quota-modal"; - -// Mock @paralleldrive/cuid2 -vi.mock("@paralleldrive/cuid2", () => ({ - createId: () => "test-id", -})); - -// Mock helper functions -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn((result: any) => result?.serverError || "Unknown error"), -})); - -// Mock zodResolver -vi.mock("@hookform/resolvers/zod", () => ({ - zodResolver: vi.fn(() => ({})), -})); - -// Mock server actions -vi.mock("@/modules/ee/quotas/actions", () => ({ - createQuotaAction: vi.fn(), - updateQuotaAction: vi.fn(), -})); - -// Mock Next.js router -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - refresh: vi.fn(), - push: vi.fn(), - replace: vi.fn(), - }), -})); - -// Mock UI components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open }: any) => (open ?
    {children}
    : null), - DialogContent: ({ children }: any) =>
    {children}
    , - DialogHeader: ({ children }: any) =>
    {children}
    , - DialogTitle: ({ children }: any) =>

    {children}

    , - DialogDescription: ({ children }: any) =>

    {children}

    , - DialogBody: ({ children }: any) =>
    {children}
    , - DialogFooter: ({ children }: any) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/form", () => ({ - FormProvider: ({ children }: any) =>
    {children}
    , - FormField: ({ render, name }: any) => { - const field = { - value: - name === "conditions" - ? { connector: "and", criteria: [] } - : name === "limit" - ? 100 - : name === "action" - ? "endSurvey" - : name === "countPartialSubmissions" - ? false - : "", - onChange: vi.fn(), - onBlur: vi.fn(), - }; - const fieldState = { - error: undefined, - }; - return
    {render({ field, fieldState })}
    ; - }, - FormItem: ({ children }: any) =>
    {children}
    , - FormLabel: ({ children }: any) => , - FormControl: ({ children }: any) =>
    {children}
    , - FormDescription: ({ children }: any) =>

    {children}

    , - FormError: ({ children }: any) => ( - - {children} - - ), -})); - -vi.mock("@/modules/ui/components/input", () => ({ - Input: (props: any) => , -})); - -vi.mock("@/modules/ui/components/select", () => ({ - Select: ({ children, value, onValueChange }: any) => ( -
    onValueChange?.("endSurvey")}> - {children} -
    - ), - SelectContent: ({ children }: any) =>
    {children}
    , - SelectItem: ({ children, value }: any) => ( -
    - {children} -
    - ), - SelectTrigger: ({ children }: any) =>
    {children}
    , - SelectValue: ({ placeholder }: any) =>
    {placeholder}
    , -})); - -vi.mock("@/modules/ui/components/switch", () => ({ - Switch: ({ checked, onCheckedChange }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, loading, disabled, type, variant }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/confirmation-modal", () => ({ - ConfirmationModal: ({ open, onConfirm }: any) => - open ? ( -
    - Confirmation Modal -
    - ) : null, -})); - -// Mock child components -vi.mock("./ending-card-selector", () => ({ - EndingCardSelector: ({ value, onChange }: any) => ( -
    onChange?.("ending1")}> - Ending Card Selector -
    - ), -})); - -vi.mock("./quota-condition-builder", () => ({ - QuotaConditionBuilder: ({ onChange }: any) => ( -
    onChange?.({ connector: "and", criteria: [] })}> - Quota Condition Builder -
    - ), -})); - -// Mock react-hook-form -vi.mock("react-hook-form", () => ({ - useForm: () => ({ - handleSubmit: (fn: any) => (e: any) => { - e.preventDefault(); - fn({ - name: "Test Quota", - limit: 100, - logic: { connector: "and", conditions: [] }, - action: "endSurvey", - endingCardId: null, - countPartialSubmissions: false, - }); - }, - reset: vi.fn(), - watch: vi.fn(() => "endSurvey"), - setValue: vi.fn(), - getValues: vi.fn((field: string) => { - if (field === "logic") { - return { connector: "and", conditions: [] }; - } - return ""; - }), - control: {}, - formState: { - isSubmitting: false, - isDirty: false, // Default to false - errors: {}, - isValid: true, - }, - }), -})); - -describe("QuotaModal", () => { - const mockOnClose = vi.fn(); - const mockOnOpenChange = vi.fn(); - const mockDeleteQuota = vi.fn(); - const mockDuplicateQuota = vi.fn(); - const mockSurvey: TSurvey = { - id: "survey1", - environmentId: "env1", - questions: [ - { - id: "q1", - type: "openText", - headline: { default: "What is your name?" }, - required: false, - inputType: "text", - }, - ], - endings: [ - { - id: "ending1", - type: "endScreen", - headline: { default: "Thank you!" }, - }, - ], - } as unknown as TSurvey; - - const mockQuota: TSurveyQuota = { - id: "quota1", - surveyId: "survey1", - name: "Test Quota", - limit: 100, - logic: { - connector: "and", - conditions: [], - }, - action: "endSurvey", - endingCardId: null, - countPartialSubmissions: false, - createdAt: new Date(), - updatedAt: new Date(), - }; - - beforeEach(() => { - mockOnClose.mockClear(); - mockOnOpenChange.mockClear(); - mockDeleteQuota.mockClear(); - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders modal when open", () => { - render( - - ); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-content")).toBeInTheDocument(); - }); - - test("does not render modal when closed", () => { - render( - - ); - - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); - - test("shows create title when no quota provided", () => { - render( - - ); - - expect(screen.getByTestId("dialog-title")).toHaveTextContent( - "environments.surveys.edit.quotas.new_quota" - ); - }); - - test("shows edit title when quota provided", () => { - render( - - ); - - expect(screen.getByTestId("dialog-title")).toHaveTextContent( - "environments.surveys.edit.quotas.edit_quota" - ); - }); - - test("renders all form fields", () => { - render( - - ); - - expect(screen.getByTestId("form-field-name")).toBeInTheDocument(); - expect(screen.getByTestId("form-field-limit")).toBeInTheDocument(); - expect(screen.getByTestId("form-field-logic")).toBeInTheDocument(); - expect(screen.getByTestId("form-field-action")).toBeInTheDocument(); - expect(screen.getByTestId("form-field-countPartialSubmissions")).toBeInTheDocument(); - }); - - test("renders quota condition builder", () => { - render( - - ); - - expect(screen.getByTestId("form-field-logic")).toBeInTheDocument(); - }); - - test("shows ending card selector when action is endSurvey", () => { - render( - - ); - - expect(screen.getByTestId("ending-card-selector")).toBeInTheDocument(); - }); - - test("calls createQuotaAction when creating new quota", async () => { - vi.mocked(createQuotaAction).mockResolvedValue({ - data: { - id: "new-quota", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Quota", - logic: { connector: "and", conditions: [] }, - action: "endSurvey", - endingCardId: null, - countPartialSubmissions: false, - surveyId: "survey1", - limit: 100, - }, - }); - - const { container } = render( - - ); - - const form = container.querySelector("form"); - const submitEvent = new Event("submit", { bubbles: true, cancelable: true }); - form!.dispatchEvent(submitEvent); - - await waitFor(() => { - expect(vi.mocked(createQuotaAction)).toHaveBeenCalledWith({ - quota: expect.objectContaining({ - surveyId: "survey1", - name: "Test Quota", - limit: 100, - action: "endSurvey", - logic: { connector: "and", conditions: [] }, - countPartialSubmissions: false, - }), - }); - }); - }); - - test("calls updateQuotaAction when updating existing quota", async () => { - vi.mocked(updateQuotaAction).mockResolvedValue({ - data: { - id: "quota1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Quota", - logic: { connector: "and", conditions: [] }, - action: "endSurvey", - endingCardId: null, - countPartialSubmissions: false, - surveyId: "survey1", - limit: 100, - }, - }); - - const { container } = render( - - ); - - const form = container.querySelector("form"); - const submitEvent = new Event("submit", { bubbles: true, cancelable: true }); - form!.dispatchEvent(submitEvent); - - await waitFor(() => { - expect(vi.mocked(updateQuotaAction)).toHaveBeenCalledWith({ - quota: expect.objectContaining({ - name: "Test Quota", - limit: 100, - }), - quotaId: "quota1", - }); - }); - }); - - test("shows success toast on successful create", async () => { - vi.mocked(createQuotaAction).mockResolvedValue({ - data: { - id: "new-quota", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Quota", - logic: { connector: "and", conditions: [] }, - action: "endSurvey", - endingCardId: null, - countPartialSubmissions: false, - surveyId: "survey1", - limit: 100, - }, - }); - - const { container } = render( - - ); - - const form = container.querySelector("form"); - const submitEvent = new Event("submit", { bubbles: true, cancelable: true }); - form!.dispatchEvent(submitEvent); - - await waitFor(() => { - expect(vi.mocked(toast.success)).toHaveBeenCalledWith( - "environments.surveys.edit.quotas.quota_created_successfull_toast" - ); - }); - }); - - test("shows error toast on failed create", async () => { - vi.mocked(createQuotaAction).mockResolvedValue({ - serverError: "Failed", - }); - - const { container } = render( - - ); - - const form = container.querySelector("form"); - const submitEvent = new Event("submit", { bubbles: true, cancelable: true }); - form!.dispatchEvent(submitEvent); - - await waitFor(() => { - expect(vi.mocked(toast.error)).toHaveBeenCalledWith("Failed"); - }); - }); - - test("shows delete button when editing quota", () => { - render( - - ); - - const deleteButton = screen - .getAllByTestId("button") - .find((button) => button.getAttribute("data-variant") === "destructive"); - - expect(deleteButton).toBeInTheDocument(); - expect(deleteButton).toHaveTextContent("common.delete"); - }); - - test("shows cancel button when creating new quota", () => { - render( - - ); - - const cancelButton = screen - .getAllByTestId("button") - .find((button) => button.getAttribute("data-variant") === "outline"); - - expect(cancelButton).toBeInTheDocument(); - expect(cancelButton).toHaveTextContent("common.cancel"); - }); - - test("calls deleteQuota when delete button is clicked", async () => { - const user = userEvent.setup(); - - render( - - ); - - const deleteButton = screen - .getAllByTestId("button") - .find((button) => button.getAttribute("data-variant") === "destructive"); - - await user.click(deleteButton!); - - expect(mockDeleteQuota).toHaveBeenCalledWith(mockQuota); - }); - - test("calls onClose when cancel button is clicked", async () => { - const user = userEvent.setup(); - - render( - - ); - - const cancelButton = screen - .getAllByTestId("button") - .find((button) => button.getAttribute("data-variant") === "outline"); - - await user.click(cancelButton!); - - expect(mockOnClose).toHaveBeenCalled(); - }); - - test("handles condition changes", async () => { - const user = userEvent.setup(); - - render( - - ); - - const conditionBuilder = screen.getByTestId("form-field-logic"); - await user.click(conditionBuilder); - - // The click should trigger the onChange callback in the mocked component - expect(conditionBuilder).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/quotas/components/quotas-card.test.tsx b/apps/web/modules/ee/quotas/components/quotas-card.test.tsx deleted file mode 100644 index 74763d10ed..0000000000 --- a/apps/web/modules/ee/quotas/components/quotas-card.test.tsx +++ /dev/null @@ -1,486 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurveyQuota } from "@formbricks/types/quota"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { deleteQuotaAction, getQuotaResponseCountAction } from "@/modules/ee/quotas/actions"; -import { QuotasCard } from "./quotas-card"; - -// Mock server actions -vi.mock("@/modules/ee/quotas/actions", () => ({ - deleteQuotaAction: vi.fn(), - getQuotaResponseCountAction: vi.fn(), -})); - -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -// Mock @tolgee/react -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string, params?: any) => { - if (params) { - let result = key; - Object.keys(params).forEach((param) => { - result = result.replace(`{{${param}}}`, params[param]); - }); - return result; - } - return key; - }, - }), -})); - -// Mock next/navigation -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - refresh: vi.fn(), - push: vi.fn(), - }), -})); - -// Mock @formkit/auto-animate/react -vi.mock("@formkit/auto-animate/react", () => ({ - useAutoAnimate: () => [null, () => {}], -})); - -// Mock Radix UI Collapsible -vi.mock("@radix-ui/react-collapsible", () => ({ - Root: ({ children, open, onOpenChange }: any) => ( -
    onOpenChange?.(!open)}> - {children} -
    - ), - Trigger: ({ children, asChild }: any) => - asChild ? children : , - Content: ({ children }: any) =>
    {children}
    , -})); - -// Mock UI components -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, variant, size, disabled, loading }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/upgrade-prompt", () => ({ - UpgradePrompt: ({ title, description, buttons }: any) => ( -
    -

    {title}

    -

    {description}

    - {buttons?.map((button: any, index: number) => ( - - {button.text} - - ))} -
    - ), -})); - -vi.mock("@/modules/ui/components/confirmation-modal", () => ({ - ConfirmationModal: ({ open, title, text, onConfirm, buttonText, buttonLoading }: any) => - open ? ( -
    -

    {title}

    -

    {text}

    - -
    - ) : null, -})); - -vi.mock("@/modules/ui/components/delete-dialog", () => ({ - DeleteDialog: ({ open, onDelete, deleteWhat, text, isDeleting, setOpen, ...props }: any) => - open ? ( -
    -

    Delete {deleteWhat}

    -

    {text}

    - - -
    - ) : null, -})); - -// Mock child components -vi.mock("./quota-list", () => ({ - QuotaList: ({ quotas, onEdit, deleteQuota }: any) => ( -
    - {quotas.map((quota: any) => ( -
    - {quota.name} - - -
    - ))} -
    - ), -})); - -vi.mock("./quota-modal", () => ({ - QuotaModal: ({ open, quota, onClose, setQuotaToDelete }: any) => - open ? ( -
    - {quota?.id || "new"} - - -
    - ) : null, -})); - -describe("QuotasCard", () => { - const mockSurvey: TSurvey = { - id: "survey1", - environmentId: "env1", - questions: [ - { - id: "q1", - type: "openText", - headline: { default: "Test question" }, - required: false, - inputType: "text", - }, - ], - } as unknown as TSurvey; - - const mockQuotas: TSurveyQuota[] = [ - { - id: "quota1", - surveyId: "survey1", - name: "Test Quota 1", - limit: 100, - logic: { connector: "and", conditions: [] }, - action: "endSurvey", - endingCardId: null, - countPartialSubmissions: false, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: "quota2", - surveyId: "survey1", - name: "Test Quota 2", - limit: 50, - logic: { connector: "or", conditions: [] }, - action: "continueSurvey", - endingCardId: "ending1", - countPartialSubmissions: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - ]; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders quotas card", () => { - render( - - ); - - expect(screen.getByTestId("collapsible-root")).toBeInTheDocument(); - expect(screen.getByText("common.quotas")).toBeInTheDocument(); - expect(screen.getByText("common.quotas_description")).toBeInTheDocument(); - }); - - test("shows upgrade prompt when quotas not enabled", () => { - render(); - - expect(screen.getByTestId("upgrade-prompt")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.edit.quotas.upgrade_prompt_title")).toBeInTheDocument(); - }); - - test("shows quota list when quotas enabled and quotas exist", () => { - render( - - ); - - expect(screen.getByTestId("quota-list")).toBeInTheDocument(); - expect(screen.getByTestId("quota-item-quota1")).toBeInTheDocument(); - expect(screen.getByTestId("quota-item-quota2")).toBeInTheDocument(); - }); - - test("shows no quotas message when enabled but no quotas exist", () => { - render(); - - expect(screen.getByText("environments.surveys.edit.quotas.add_quota")).toBeInTheDocument(); - }); - - test("opens quota modal when add quota button is clicked (no existing quotas)", async () => { - const user = userEvent.setup(); - - render(); - - const addButton = screen.getByRole("button", { name: /environments.surveys.edit.quotas.add_quota/i }); - await user.click(addButton); - - expect(screen.getByTestId("quota-modal")).toBeInTheDocument(); - expect(screen.getByTestId("modal-quota-id")).toHaveTextContent("new"); - }); - - test("opens quota modal when add quota button is clicked (with existing quotas)", async () => { - const user = userEvent.setup(); - - render( - - ); - - const addButtons = screen.getAllByRole("button", { name: /environments.surveys.edit.quotas.add_quota/i }); - const addButton = addButtons[0]; // Should be the add button in the bottom section - await user.click(addButton); - - expect(screen.getByTestId("quota-modal")).toBeInTheDocument(); - expect(screen.getByTestId("modal-quota-id")).toHaveTextContent("new"); - }); - - test("opens quota modal for editing when edit is clicked", async () => { - const user = userEvent.setup(); - - render( - - ); - vi.mocked(getQuotaResponseCountAction).mockResolvedValue({ data: { count: 10 } }); - const editButton = screen.getByTestId("edit-quota1"); - await user.click(editButton); - - expect(screen.getByTestId("quota-modal")).toBeInTheDocument(); - expect(screen.getByTestId("modal-quota-id")).toHaveTextContent("quota1"); - }); - - test("shows confirmation modal when delete is clicked", async () => { - const user = userEvent.setup(); - - render( - - ); - - const deleteButton = screen.getByTestId("delete-quota1"); - await user.click(deleteButton); - - expect(screen.getByTestId("delete-quota-dialog")).toBeInTheDocument(); - }); - - test("deletes quota when confirmed", async () => { - const user = userEvent.setup(); - - vi.mocked(deleteQuotaAction).mockResolvedValue({ data: mockQuotas[0], serverError: undefined }); - - render( - - ); - - // Click delete button - const deleteButton = screen.getByTestId("delete-quota1"); - await user.click(deleteButton); - - // Confirm deletion - const confirmButton = screen.getByTestId("confirm-delete-button"); - await user.click(confirmButton); - - await waitFor(() => { - expect(vi.mocked(deleteQuotaAction)).toHaveBeenCalledWith({ - quotaId: "quota1", - surveyId: "survey1", - }); - }); - }); - - test("shows success toast on successful delete", async () => { - const user = userEvent.setup(); - - vi.mocked(deleteQuotaAction).mockResolvedValue({ data: mockQuotas[0], serverError: undefined }); - - render( - - ); - - const deleteButton = screen.getByTestId("delete-quota1"); - await user.click(deleteButton); - - const confirmButton = screen.getByTestId("confirm-delete-button"); - await user.click(confirmButton); - - await waitFor(() => { - expect(vi.mocked(toast.success)).toHaveBeenCalledWith( - "environments.surveys.edit.quotas.quota_deleted_successfull_toast" - ); - }); - }); - - test("shows error toast on failed delete", async () => { - const user = userEvent.setup(); - - vi.mocked(deleteQuotaAction).mockResolvedValue({ serverError: "Failed" }); - - render( - - ); - - const deleteButton = screen.getByTestId("delete-quota1"); - await user.click(deleteButton); - - const confirmButton = screen.getByTestId("confirm-delete-button"); - await user.click(confirmButton); - - await waitFor(() => { - expect(vi.mocked(toast.error)).toHaveBeenCalledWith("Failed"); - }); - }); - - test("closes quota modal when onClose is called", async () => { - const user = userEvent.setup(); - - render(); - - // Open modal - const addButton = screen.getByRole("button", { name: /environments.surveys.edit.quotas.add_quota/i }); - await user.click(addButton); - - expect(screen.getByTestId("quota-modal")).toBeInTheDocument(); - - // Close modal - const closeButton = screen.getByTestId("modal-close"); - await user.click(closeButton); - - expect(screen.queryByTestId("quota-modal")).not.toBeInTheDocument(); - }); - - test("shows correct upgrade buttons for Formbricks Cloud", () => { - render( - - ); - - const upgradeLinks = screen.getAllByTestId("upgrade-link"); - expect(upgradeLinks[0]).toHaveTextContent("common.start_free_trial"); - expect(upgradeLinks[0]).toHaveAttribute("href", "/environments/env1/settings/billing"); - }); - - test("shows correct upgrade buttons for self-hosted", () => { - render( - - ); - - const upgradeLinks = screen.getAllByTestId("upgrade-link"); - expect(upgradeLinks[0]).toHaveTextContent("common.request_trial_license"); - expect(upgradeLinks[0]).toHaveAttribute("href", "https://formbricks.com/upgrade-self-hosting-license"); - }); - - test("toggles collapsible state", async () => { - const user = userEvent.setup(); - - const { container } = render( - - ); - - const collapsibleRoot = container.querySelector("[data-testid='collapsible-root']"); - expect(collapsibleRoot).toHaveAttribute("data-open", "false"); - - await user.click(collapsibleRoot!); - - expect(collapsibleRoot).toHaveAttribute("data-open", "true"); - }); - - test("handles quota deletion from modal", async () => { - const user = userEvent.setup(); - - const { container } = render( - - ); - vi.mocked(getQuotaResponseCountAction).mockResolvedValue({ data: { count: 10 } }); - - // Open edit modal - use container to be more specific - const editButton = container.querySelector("[data-testid='edit-quota1']"); - await user.click(editButton!); - - // Delete from modal - const modalDeleteButton = screen.getByTestId("modal-delete"); - await user.click(modalDeleteButton); - - // Should show delete dialog - expect(screen.getByTestId("delete-quota-dialog")).toBeInTheDocument(); - }); - - test("disables delete button when deletion is in progress", async () => { - const user = userEvent.setup(); - - // Make delete action slow - vi.mocked(deleteQuotaAction).mockImplementation( - () => - new Promise((resolve) => - setTimeout(() => resolve({ data: mockQuotas[0], serverError: undefined }), 1000) - ) - ); - - render( - - ); - - const deleteButton = screen.getByTestId("delete-quota1"); - await user.click(deleteButton); - - const confirmButton = screen.getByTestId("confirm-delete-button"); - await user.click(confirmButton); - - // Button should be disabled while deletion is in progress - expect(confirmButton).toHaveAttribute("data-loading", "true"); - }); -}); diff --git a/apps/web/modules/ee/quotas/components/quotas-summary.test.tsx b/apps/web/modules/ee/quotas/components/quotas-summary.test.tsx deleted file mode 100644 index 97004344f2..0000000000 --- a/apps/web/modules/ee/quotas/components/quotas-summary.test.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurveySummary } from "@formbricks/types/surveys/types"; -import { QuotasSummary } from "./quotas-summary"; - -vi.mock("@/modules/ui/components/progress-bar", () => ({ - ProgressBar: ({ progress, barColor, height }: { progress: number; barColor: string; height: number }) => ( -
    - ), -})); - -describe("QuotasSummary", () => { - afterEach(() => { - cleanup(); - }); - - const mockQuotas: TSurveySummary["quotas"] = [ - { - id: "quota1", - name: "Demographics Quota", - limit: 100, - count: 75, - percentage: 75, - }, - { - id: "quota2", - name: "Age Group Quota", - limit: 50, - count: 25, - percentage: 50, - }, - ]; - - test("renders quotas table header correctly", () => { - render(); - - expect(screen.getByText("common.progress")).toBeInTheDocument(); - expect(screen.getByText("common.label")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.limit")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.current_count")).toBeInTheDocument(); - }); - - test("renders quotas data correctly", () => { - render(); - - expect(screen.getByText("Demographics Quota")).toBeInTheDocument(); - expect(screen.getByText("Age Group Quota")).toBeInTheDocument(); - expect(screen.getByText("100")).toBeInTheDocument(); - expect(screen.getByText("50")).toBeInTheDocument(); - expect(screen.getByText("75")).toBeInTheDocument(); - expect(screen.getByText("25")).toBeInTheDocument(); - expect(screen.getByText("75%")).toBeInTheDocument(); - expect(screen.getByText("50%")).toBeInTheDocument(); - }); - - test("renders progress bars with correct props", () => { - render(); - - const progressBars = screen.getAllByTestId("progress-bar"); - expect(progressBars).toHaveLength(2); - - expect(progressBars[0]).toHaveAttribute("data-progress", "0.75"); - expect(progressBars[0]).toHaveAttribute("data-bar-color", "bg-brand-dark"); - expect(progressBars[0]).toHaveAttribute("data-height", "2"); - - expect(progressBars[1]).toHaveAttribute("data-progress", "0.5"); - expect(progressBars[1]).toHaveAttribute("data-bar-color", "bg-brand-dark"); - expect(progressBars[1]).toHaveAttribute("data-height", "2"); - }); - - test("renders no quotas message when quotas array is empty", () => { - render(); - - expect(screen.getByText("common.no_quotas_found")).toBeInTheDocument(); - expect(screen.queryByTestId("progress-bar")).not.toBeInTheDocument(); - expect(screen.queryByText("Demographics Quota")).not.toBeInTheDocument(); - }); - - test("renders single quota correctly", () => { - const singleQuota: TSurveySummary["quotas"] = [ - { - id: "quota1", - name: "Single Quota", - limit: 200, - count: 150, - percentage: 75, - }, - ]; - - render(); - - expect(screen.getByText("Single Quota")).toBeInTheDocument(); - expect(screen.getByText("200")).toBeInTheDocument(); - expect(screen.getByText("150")).toBeInTheDocument(); - expect(screen.getByText("75%")).toBeInTheDocument(); - - const progressBar = screen.getByTestId("progress-bar"); - expect(progressBar).toHaveAttribute("data-progress", "0.75"); - }); - - test("handles zero percentage correctly", () => { - const zeroPercentageQuota: TSurveySummary["quotas"] = [ - { - id: "quota1", - name: "Zero Quota", - limit: 100, - count: 0, - percentage: 0, - }, - ]; - - render(); - - expect(screen.getByText("Zero Quota")).toBeInTheDocument(); - expect(screen.getByText("0%")).toBeInTheDocument(); - - const progressBar = screen.getByTestId("progress-bar"); - expect(progressBar).toHaveAttribute("data-progress", "0"); - }); - - test("handles 100 percentage correctly", () => { - const fullPercentageQuota: TSurveySummary["quotas"] = [ - { - id: "quota1", - name: "Full Quota", - limit: 50, - count: 50, - percentage: 100, - }, - ]; - - render(); - - expect(screen.getByText("Full Quota")).toBeInTheDocument(); - expect(screen.getByText("100%")).toBeInTheDocument(); - - const progressBar = screen.getByTestId("progress-bar"); - expect(progressBar).toHaveAttribute("data-progress", "1"); - }); -}); diff --git a/apps/web/modules/ee/quotas/components/single-response-card-quotas.test.tsx b/apps/web/modules/ee/quotas/components/single-response-card-quotas.test.tsx deleted file mode 100644 index 5592996927..0000000000 --- a/apps/web/modules/ee/quotas/components/single-response-card-quotas.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ResponseCardQuotas } from "./single-response-card-quotas"; - -vi.mock("@/modules/ui/components/response-badges", () => ({ - ResponseBadges: ({ items, showId }: { items: { value: string }[]; showId: boolean }) => ( -
    {items.map((item) => item.value).join(", ")}
    - ), -})); - -describe("ResponseCardQuotas", () => { - afterEach(() => { - cleanup(); - }); - - test("renders response card quotas", () => { - render(); - - expect(screen.getByText("Quota 1")).toBeInTheDocument(); - expect(screen.getByTestId("response-badges")).toBeInTheDocument(); - }); - - test("renders no response card quotas", () => { - render(); - - expect(screen.queryByTestId("response-badges")).not.toBeInTheDocument(); - expect(screen.queryByTestId("main-quotas-div")).toBeNull(); - }); -}); diff --git a/apps/web/modules/ee/role-management/components/add-member.test.tsx b/apps/web/modules/ee/role-management/components/add-member.test.tsx deleted file mode 100644 index 621cff869b..0000000000 --- a/apps/web/modules/ee/role-management/components/add-member.test.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { FormProvider, useForm } from "react-hook-form"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { AddMemberRole } from "./add-member-role"; - -// Mock dependencies -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -// Create a wrapper component that provides the form context -const FormWrapper = ({ - children, - defaultValues, - membershipRole, - isAccessControlAllowed, - isFormbricksCloud, -}) => { - const methods = useForm({ defaultValues }); - return ( - - - {children} - - ); -}; - -describe("AddMemberRole Component", () => { - afterEach(() => { - cleanup(); - }); - - const defaultValues = { - name: "Test User", - email: "test@example.com", - role: "member", - teamIds: [], - }; - - describe("Rendering", () => { - test("renders role selector when user is owner", () => { - render( - -
    - - ); - - const roleLabel = screen.getByText("common.role_organization"); - expect(roleLabel).toBeInTheDocument(); - }); - - test("does not render anything when user is member", () => { - render( - -
    - - ); - - expect(screen.queryByText("common.role_organization")).not.toBeInTheDocument(); - expect(screen.getByTestId("child")).toBeInTheDocument(); - }); - - test("disables the role selector when isAccessControlAllowed is false", () => { - render( - -
    - - ); - - const selectTrigger = screen.getByRole("combobox"); - expect(selectTrigger).toBeDisabled(); - }); - }); - - describe("Default values", () => { - test("displays the default role value", () => { - render( - -
    - - ); - - const selectTrigger = screen.getByRole("combobox"); - expect(selectTrigger).toHaveTextContent("member"); - }); - }); -}); diff --git a/apps/web/modules/ee/role-management/components/edit-membership-role.test.tsx b/apps/web/modules/ee/role-management/components/edit-membership-role.test.tsx deleted file mode 100644 index 3b00d0a30a..0000000000 --- a/apps/web/modules/ee/role-management/components/edit-membership-role.test.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { useRouter } from "next/navigation"; -import { type Mock, afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { EditMembershipRole } from "./edit-membership-role"; - -// Mock dependencies -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), -})); - -vi.mock("react-hot-toast", () => ({ - default: { - error: vi.fn(), - }, -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("../actions", () => ({ - updateMembershipAction: vi.fn(), - updateInviteAction: vi.fn(), -})); - -vi.mock("@/lib/membership/utils", () => ({ - getAccessFlags: (role: string) => ({ - isOwner: role === "owner", - isManager: role === "manager", - isMember: role === "member", - isBilling: role === "billing", - }), -})); - -describe("EditMembershipRole Component", () => { - const mockRouter = { - refresh: vi.fn(), - }; - - beforeEach(() => { - vi.clearAllMocks(); - (useRouter as Mock).mockReturnValue(mockRouter); - }); - - afterEach(() => { - cleanup(); - }); - - const defaultProps = { - memberRole: "member" as const, - organizationId: "org-123", - currentUserRole: "owner" as const, - memberId: "member-123", - userId: "user-456", - memberAccepted: true, - inviteId: undefined, - doesOrgHaveMoreThanOneOwner: true, - isFormbricksCloud: true, - }; - - describe("Rendering", () => { - test("renders a dropdown when user is owner", () => { - render(); - - const button = screen.queryByRole("button-role"); - expect(button).toBeInTheDocument(); - expect(button).toHaveTextContent("member"); - }); - - test("renders a badge when user is not owner or manager", () => { - render( - - ); - - const badge = screen.queryByRole("badge-role"); - expect(badge).toBeInTheDocument(); - const button = screen.queryByRole("button-role"); - expect(button).not.toBeInTheDocument(); - }); - - test("disables the dropdown when editing own role", () => { - render( - - ); - - const button = screen.getByRole("button-role"); - expect(button).toBeDisabled(); - }); - - test("disables the dropdown when the user is the only owner", () => { - render( - - ); - - const button = screen.getByRole("button-role"); - expect(button).toBeDisabled(); - }); - - test("disables the dropdown when a manager tries to edit an owner", () => { - render( - - ); - - const button = screen.getByRole("button-role"); - expect(button).toBeDisabled(); - }); - }); -}); diff --git a/apps/web/modules/ee/sso/components/azure-button.test.tsx b/apps/web/modules/ee/sso/components/azure-button.test.tsx deleted file mode 100644 index fb94688473..0000000000 --- a/apps/web/modules/ee/sso/components/azure-button.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { signIn } from "next-auth/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@/lib/localStorage"; -import { AzureButton } from "./azure-button"; - -// Mock next-auth/react -vi.mock("next-auth/react", () => ({ - signIn: vi.fn(), -})); - -// Mock localStorage -const mockLocalStorage = { - setItem: vi.fn(), -}; -Object.defineProperty(window, "localStorage", { - value: mockLocalStorage, - writable: true, -}); - -describe("AzureButton", () => { - const defaultProps = { - source: "signin" as const, - }; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders correctly with default props", () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_azure" }); - expect(button).toBeInTheDocument(); - }); - - test("renders with last used indicator when lastUsed is true", () => { - render(); - expect(screen.getByText("auth.last_used")).toBeInTheDocument(); - }); - - test("sets localStorage item and calls signIn on click", async () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_azure" }); - fireEvent.click(button); - - expect(mockLocalStorage.setItem).toHaveBeenCalledWith(FORMBRICKS_LOGGED_IN_WITH_LS, "Azure"); - expect(signIn).toHaveBeenCalledWith("azure-ad", { - redirect: true, - callbackUrl: "/?source=signin", - }); - }); - - test("uses inviteUrl in callbackUrl when provided", async () => { - const inviteUrl = "https://example.com/invite"; - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_azure" }); - fireEvent.click(button); - - expect(signIn).toHaveBeenCalledWith("azure-ad", { - redirect: true, - callbackUrl: "https://example.com/invite?source=signin", - }); - }); - - test("handles signup source correctly", async () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_azure" }); - fireEvent.click(button); - - expect(signIn).toHaveBeenCalledWith("azure-ad", { - redirect: true, - callbackUrl: "/?source=signup", - }); - }); - - test("triggers direct redirect when directRedirect is true", () => { - render(); - expect(signIn).toHaveBeenCalledWith("azure-ad", { - redirect: true, - callbackUrl: "/?source=signin", - }); - }); -}); diff --git a/apps/web/modules/ee/sso/components/github-button.test.tsx b/apps/web/modules/ee/sso/components/github-button.test.tsx deleted file mode 100644 index 24749efd22..0000000000 --- a/apps/web/modules/ee/sso/components/github-button.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { signIn } from "next-auth/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@/lib/localStorage"; -import { GithubButton } from "./github-button"; - -// Mock next-auth/react -vi.mock("next-auth/react", () => ({ - signIn: vi.fn(), -})); - -// Mock localStorage -const mockLocalStorage = { - setItem: vi.fn(), -}; -Object.defineProperty(window, "localStorage", { - value: mockLocalStorage, - writable: true, -}); - -describe("GithubButton", () => { - const defaultProps = { - source: "signin" as const, - }; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders correctly with default props", () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_github" }); - expect(button).toBeInTheDocument(); - }); - - test("renders with last used indicator when lastUsed is true", () => { - render(); - expect(screen.getByText("auth.last_used")).toBeInTheDocument(); - }); - - test("sets localStorage item and calls signIn on click", async () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_github" }); - fireEvent.click(button); - - expect(mockLocalStorage.setItem).toHaveBeenCalledWith(FORMBRICKS_LOGGED_IN_WITH_LS, "Github"); - expect(signIn).toHaveBeenCalledWith("github", { - redirect: true, - callbackUrl: "/?source=signin", - }); - }); - - test("uses inviteUrl in callbackUrl when provided", async () => { - const inviteUrl = "https://example.com/invite"; - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_github" }); - fireEvent.click(button); - - expect(signIn).toHaveBeenCalledWith("github", { - redirect: true, - callbackUrl: "https://example.com/invite?source=signin", - }); - }); - - test("handles signup source correctly", async () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_github" }); - fireEvent.click(button); - - expect(signIn).toHaveBeenCalledWith("github", { - redirect: true, - callbackUrl: "/?source=signup", - }); - }); -}); diff --git a/apps/web/modules/ee/sso/components/google-button.test.tsx b/apps/web/modules/ee/sso/components/google-button.test.tsx deleted file mode 100644 index e0e86d2f19..0000000000 --- a/apps/web/modules/ee/sso/components/google-button.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { signIn } from "next-auth/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@/lib/localStorage"; -import { GoogleButton } from "./google-button"; - -// Mock next-auth/react -vi.mock("next-auth/react", () => ({ - signIn: vi.fn(), -})); - -// Mock localStorage -const mockLocalStorage = { - setItem: vi.fn(), -}; -Object.defineProperty(window, "localStorage", { - value: mockLocalStorage, - writable: true, -}); - -describe("GoogleButton", () => { - const defaultProps = { - source: "signin" as const, - }; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders correctly with default props", () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_google" }); - expect(button).toBeInTheDocument(); - }); - - test("renders with last used indicator when lastUsed is true", () => { - render(); - expect(screen.getByText("auth.last_used")).toBeInTheDocument(); - }); - - test("sets localStorage item and calls signIn on click", async () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_google" }); - fireEvent.click(button); - - expect(mockLocalStorage.setItem).toHaveBeenCalledWith(FORMBRICKS_LOGGED_IN_WITH_LS, "Google"); - expect(signIn).toHaveBeenCalledWith("google", { - redirect: true, - callbackUrl: "/?source=signin", - }); - }); - - test("uses inviteUrl in callbackUrl when provided", async () => { - const inviteUrl = "https://example.com/invite"; - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_google" }); - fireEvent.click(button); - - expect(signIn).toHaveBeenCalledWith("google", { - redirect: true, - callbackUrl: "https://example.com/invite?source=signin", - }); - }); - - test("handles signup source correctly", async () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_google" }); - fireEvent.click(button); - - expect(signIn).toHaveBeenCalledWith("google", { - redirect: true, - callbackUrl: "/?source=signup", - }); - }); -}); diff --git a/apps/web/modules/ee/sso/components/open-id-button.test.tsx b/apps/web/modules/ee/sso/components/open-id-button.test.tsx deleted file mode 100644 index 7c0e95f2b2..0000000000 --- a/apps/web/modules/ee/sso/components/open-id-button.test.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { signIn } from "next-auth/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@/lib/localStorage"; -import { OpenIdButton } from "./open-id-button"; - -// Mock next-auth/react -vi.mock("next-auth/react", () => ({ - signIn: vi.fn(), -})); - -// Mock localStorage -const mockLocalStorage = { - setItem: vi.fn(), -}; -Object.defineProperty(window, "localStorage", { - value: mockLocalStorage, - writable: true, -}); - -describe("OpenIdButton", () => { - const defaultProps = { - source: "signin" as const, - }; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders correctly with default props", () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_openid" }); - expect(button).toBeInTheDocument(); - }); - - test("renders with custom text when provided", () => { - const customText = "Custom OpenID Text"; - render(); - const button = screen.getByRole("button", { name: customText }); - expect(button).toBeInTheDocument(); - }); - - test("renders with last used indicator when lastUsed is true", () => { - render(); - expect(screen.getByText("auth.last_used")).toBeInTheDocument(); - }); - - test("sets localStorage item and calls signIn on click", async () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_openid" }); - fireEvent.click(button); - - expect(mockLocalStorage.setItem).toHaveBeenCalledWith(FORMBRICKS_LOGGED_IN_WITH_LS, "OpenID"); - expect(signIn).toHaveBeenCalledWith("openid", { - redirect: true, - callbackUrl: "/?source=signin", - }); - }); - - test("uses inviteUrl in callbackUrl when provided", async () => { - const inviteUrl = "https://example.com/invite"; - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_openid" }); - fireEvent.click(button); - - expect(signIn).toHaveBeenCalledWith("openid", { - redirect: true, - callbackUrl: "https://example.com/invite?source=signin", - }); - }); - - test("handles signup source correctly", async () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_openid" }); - fireEvent.click(button); - - expect(signIn).toHaveBeenCalledWith("openid", { - redirect: true, - callbackUrl: "/?source=signup", - }); - }); - - test("triggers direct redirect when directRedirect is true", () => { - render(); - expect(signIn).toHaveBeenCalledWith("openid", { - redirect: true, - callbackUrl: "/?source=signin", - }); - }); -}); diff --git a/apps/web/modules/ee/sso/components/saml-button.test.tsx b/apps/web/modules/ee/sso/components/saml-button.test.tsx deleted file mode 100644 index c8d378b0d4..0000000000 --- a/apps/web/modules/ee/sso/components/saml-button.test.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { signIn } from "next-auth/react"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@/lib/localStorage"; -import { doesSamlConnectionExistAction } from "@/modules/ee/sso/actions"; -import { SamlButton } from "./saml-button"; - -// Mock next-auth/react -vi.mock("next-auth/react", () => ({ - signIn: vi.fn().mockResolvedValue(undefined), -})); - -// Mock localStorage -const mockLocalStorage = { - setItem: vi.fn(), -}; -Object.defineProperty(window, "localStorage", { - value: mockLocalStorage, - writable: true, -}); - -// Mock actions -vi.mock("@/modules/ee/sso/actions", () => ({ - doesSamlConnectionExistAction: vi.fn(), -})); - -// Mock toast -vi.mock("react-hot-toast", () => ({ - default: { - error: vi.fn(), - }, -})); - -describe("SamlButton", () => { - const defaultProps = { - source: "signin" as const, - samlTenant: "test-tenant", - samlProduct: "test-product", - }; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders correctly with default props", () => { - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_saml" }); - expect(button).toBeInTheDocument(); - }); - - test("renders with last used indicator when lastUsed is true", () => { - render(); - expect(screen.getByText("auth.last_used")).toBeInTheDocument(); - }); - - test("sets localStorage item and calls signIn on click when SAML connection exists", async () => { - vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: true }); - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_saml" }); - - await fireEvent.click(button); - - expect(mockLocalStorage.setItem).toHaveBeenCalledWith(FORMBRICKS_LOGGED_IN_WITH_LS, "Saml"); - expect(signIn).toHaveBeenCalledWith( - "saml", - { - redirect: true, - callbackUrl: "/?source=signin", - }, - { - tenant: "test-tenant", - product: "test-product", - } - ); - }); - - test("shows error toast when SAML connection does not exist", async () => { - vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: false }); - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_saml" }); - - await fireEvent.click(button); - - expect(toast.error).toHaveBeenCalledWith("auth.saml_connection_error"); - expect(signIn).not.toHaveBeenCalled(); - }); - - test("uses inviteUrl in callbackUrl when provided", async () => { - vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: true }); - const inviteUrl = "https://example.com/invite"; - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_saml" }); - - await fireEvent.click(button); - - expect(signIn).toHaveBeenCalledWith( - "saml", - { - redirect: true, - callbackUrl: "https://example.com/invite?source=signin", - }, - { - tenant: "test-tenant", - product: "test-product", - } - ); - }); - - test("handles signup source correctly", async () => { - vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: true }); - render(); - const button = screen.getByRole("button", { name: "auth.continue_with_saml" }); - - await fireEvent.click(button); - - expect(signIn).toHaveBeenCalledWith( - "saml", - { - redirect: true, - callbackUrl: "/?source=signup", - }, - { - tenant: "test-tenant", - product: "test-product", - } - ); - }); -}); diff --git a/apps/web/modules/ee/sso/components/sso-options.test.tsx b/apps/web/modules/ee/sso/components/sso-options.test.tsx deleted file mode 100644 index feac4fa1ac..0000000000 --- a/apps/web/modules/ee/sso/components/sso-options.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { SSOOptions } from "./sso-options"; - -// Mock environment variables -vi.mock("@/lib/env", () => ({ - env: { - IS_FORMBRICKS_CLOUD: "0", - }, -})); - -// Mock the translation hook -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -// Mock the individual SSO buttons -vi.mock("./google-button", () => ({ - GoogleButton: ({ lastUsed, source }: any) => ( -
    - Google Button -
    - ), -})); - -vi.mock("./github-button", () => ({ - GithubButton: ({ lastUsed, source }: any) => ( -
    - Github Button -
    - ), -})); - -vi.mock("./azure-button", () => ({ - AzureButton: ({ lastUsed, source }: any) => ( -
    - Azure Button -
    - ), -})); - -vi.mock("./open-id-button", () => ({ - OpenIdButton: ({ lastUsed, source, text }: any) => ( -
    - {text} -
    - ), -})); - -vi.mock("./saml-button", () => ({ - SamlButton: ({ lastUsed, source, samlTenant, samlProduct }: any) => ( -
    - Saml Button -
    - ), -})); - -describe("SSOOptions Component", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const defaultProps = { - googleOAuthEnabled: true, - githubOAuthEnabled: true, - azureOAuthEnabled: true, - oidcOAuthEnabled: true, - oidcDisplayName: "OpenID", - callbackUrl: "http://localhost:3000", - samlSsoEnabled: true, - samlTenant: "test-tenant", - samlProduct: "test-product", - source: "signin" as const, - }; - - test("renders all SSO options when all are enabled", () => { - render(); - - expect(screen.getByTestId("google-button")).toBeInTheDocument(); - expect(screen.getByTestId("github-button")).toBeInTheDocument(); - expect(screen.getByTestId("azure-button")).toBeInTheDocument(); - expect(screen.getByTestId("openid-button")).toBeInTheDocument(); - expect(screen.getByTestId("saml-button")).toBeInTheDocument(); - }); - - test("only renders enabled SSO options", () => { - render( - - ); - - expect(screen.queryByTestId("google-button")).not.toBeInTheDocument(); - expect(screen.queryByTestId("github-button")).not.toBeInTheDocument(); - expect(screen.queryByTestId("azure-button")).not.toBeInTheDocument(); - expect(screen.getByTestId("openid-button")).toBeInTheDocument(); - expect(screen.getByTestId("saml-button")).toBeInTheDocument(); - }); - - test("passes correct props to OpenID button", () => { - render(); - const openIdButton = screen.getByTestId("openid-button"); - - expect(openIdButton).toHaveAttribute("data-source", "signin"); - expect(openIdButton).toHaveTextContent("auth.continue_with_oidc"); - }); - - test("passes correct props to SAML button", () => { - render(); - const samlButton = screen.getByTestId("saml-button"); - - expect(samlButton).toHaveAttribute("data-source", "signin"); - expect(samlButton).toHaveAttribute("data-tenant", "test-tenant"); - expect(samlButton).toHaveAttribute("data-product", "test-product"); - }); - - test("passes correct source prop to all buttons", () => { - render(); - - expect(screen.getByTestId("google-button")).toHaveAttribute("data-source", "signup"); - expect(screen.getByTestId("github-button")).toHaveAttribute("data-source", "signup"); - expect(screen.getByTestId("azure-button")).toHaveAttribute("data-source", "signup"); - expect(screen.getByTestId("openid-button")).toHaveAttribute("data-source", "signup"); - expect(screen.getByTestId("saml-button")).toHaveAttribute("data-source", "signup"); - }); -}); diff --git a/apps/web/modules/ee/teams/project-teams/components/access-table.test.tsx b/apps/web/modules/ee/teams/project-teams/components/access-table.test.tsx deleted file mode 100644 index ed427b9a3f..0000000000 --- a/apps/web/modules/ee/teams/project-teams/components/access-table.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TProjectTeam } from "@/modules/ee/teams/project-teams/types/team"; -import { TeamPermissionMapping } from "@/modules/ee/teams/utils/teams"; -import { AccessTable } from "./access-table"; - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ t: (k: string) => k }), -})); - -describe("AccessTable", () => { - afterEach(() => { - cleanup(); - }); - - test("renders no teams found row when teams is empty", () => { - render(); - expect(screen.getByText("environments.project.teams.no_teams_found")).toBeInTheDocument(); - }); - - test("renders team rows with correct data and permission mapping", () => { - const teams: TProjectTeam[] = [ - { id: "1", name: "Team A", memberCount: 1, permission: "readWrite" }, - { id: "2", name: "Team B", memberCount: 2, permission: "read" }, - ]; - render(); - expect(screen.getByText("Team A")).toBeInTheDocument(); - expect(screen.getByText("Team B")).toBeInTheDocument(); - expect(screen.getByText("1 common.member")).toBeInTheDocument(); - expect(screen.getByText("2 common.members")).toBeInTheDocument(); - expect(screen.getByText(TeamPermissionMapping["readWrite"])).toBeInTheDocument(); - expect(screen.getByText(TeamPermissionMapping["read"])).toBeInTheDocument(); - }); - - test("renders table headers with tolgee keys", () => { - render(); - expect(screen.getByText("environments.project.teams.team_name")).toBeInTheDocument(); - expect(screen.getByText("common.size")).toBeInTheDocument(); - expect(screen.getByText("environments.project.teams.permission")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/teams/project-teams/components/access-view.test.tsx b/apps/web/modules/ee/teams/project-teams/components/access-view.test.tsx deleted file mode 100644 index 0f3e541cf5..0000000000 --- a/apps/web/modules/ee/teams/project-teams/components/access-view.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TProjectTeam } from "@/modules/ee/teams/project-teams/types/team"; -import { AccessView } from "./access-view"; - -vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({ - SettingsCard: ({ title, description, children }: any) => ( -
    -
    {title}
    -
    {description}
    - {children} -
    - ), -})); - -vi.mock("@/modules/ee/teams/project-teams/components/manage-team", () => ({ - ManageTeam: ({ environmentId, isOwnerOrManager }: any) => ( - - ), -})); - -vi.mock("@/modules/ee/teams/project-teams/components/access-table", () => ({ - AccessTable: ({ teams }: any) => ( -
    - {teams.length === 0 ? "No teams" : `Teams: ${teams.map((t: any) => t.name).join(",")}`} -
    - ), -})); - -describe("AccessView", () => { - afterEach(() => { - cleanup(); - }); - - const baseProps = { - environmentId: "env-1", - isOwnerOrManager: true, - teams: [ - { id: "1", name: "Team A", memberCount: 2, permission: "readWrite" } as TProjectTeam, - { id: "2", name: "Team B", memberCount: 1, permission: "read" } as TProjectTeam, - ], - }; - - test("renders SettingsCard with tolgee strings and children", () => { - render(); - expect(screen.getByTestId("SettingsCard")).toBeInTheDocument(); - expect(screen.getByText("common.team_access")).toBeInTheDocument(); - expect(screen.getByText("environments.project.teams.team_settings_description")).toBeInTheDocument(); - }); - - test("renders ManageTeam with correct props", () => { - render(); - expect(screen.getByTestId("ManageTeam")).toHaveTextContent("ManageTeam env-1 owner"); - }); - - test("renders AccessTable with teams", () => { - render(); - expect(screen.getByTestId("AccessTable")).toHaveTextContent("Teams: Team A,Team B"); - }); - - test("renders AccessTable with no teams", () => { - render(); - expect(screen.getByTestId("AccessTable")).toHaveTextContent("No teams"); - }); - - test("renders ManageTeam as not-owner when isOwnerOrManager is false", () => { - render(); - expect(screen.getByTestId("ManageTeam")).toHaveTextContent("not-owner"); - }); -}); diff --git a/apps/web/modules/ee/teams/project-teams/components/manage-team.test.tsx b/apps/web/modules/ee/teams/project-teams/components/manage-team.test.tsx deleted file mode 100644 index b74bfb37e5..0000000000 --- a/apps/web/modules/ee/teams/project-teams/components/manage-team.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ManageTeam } from "./manage-team"; - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ push: vi.fn() }), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, ...props }: any) => , -})); - -vi.mock("@/modules/ui/components/tooltip", () => ({ - TooltipRenderer: ({ tooltipContent, children }: any) => ( -
    - {tooltipContent} - {children} -
    - ), -})); - -describe("ManageTeam", () => { - afterEach(() => { - cleanup(); - }); - - test("renders enabled button and navigates when isOwnerOrManager is true", async () => { - render(); - const button = screen.getByRole("button"); - expect(button).toBeEnabled(); - expect(screen.getByText("environments.project.teams.manage_teams")).toBeInTheDocument(); - await userEvent.click(button); - }); - - test("renders disabled button with tooltip when isOwnerOrManager is false", () => { - render(); - const button = screen.getByRole("button"); - expect(button).toBeDisabled(); - expect(screen.getByText("environments.project.teams.manage_teams")).toBeInTheDocument(); - expect(screen.getByTestId("TooltipRenderer")).toBeInTheDocument(); - expect( - screen.getByText("environments.project.teams.only_organization_owners_and_managers_can_manage_teams") - ).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/teams/project-teams/loading.test.tsx b/apps/web/modules/ee/teams/project-teams/loading.test.tsx deleted file mode 100644 index dba81dd2dc..0000000000 --- a/apps/web/modules/ee/teams/project-teams/loading.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TeamsLoading } from "./loading"; - -vi.mock("@/modules/projects/settings/components/project-config-navigation", () => ({ - ProjectConfigNavigation: ({ activeId, loading }: any) => ( -
    {`${activeId}-${loading}`}
    - ), -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }: any) => ( -
    - {pageTitle} - {children} -
    - ), -})); - -describe("TeamsLoading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders loading skeletons and navigation", () => { - render(); - expect(screen.getByTestId("PageContentWrapper")).toBeInTheDocument(); - expect(screen.getByTestId("PageHeader")).toBeInTheDocument(); - expect(screen.getByTestId("ProjectConfigNavigation")).toHaveTextContent("teams-true"); - - // Check for the presence of multiple skeleton loaders (at least one) - const skeletonLoaders = screen.getAllByRole("generic", { name: "" }); // Assuming skeleton divs don't have specific roles/names - // Filter for elements with animate-pulse class - const pulseElements = skeletonLoaders.filter((el) => el.classList.contains("animate-pulse")); - expect(pulseElements.length).toBeGreaterThan(0); - - expect(screen.getByText("common.project_configuration")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/teams/project-teams/page.test.tsx b/apps/web/modules/ee/teams/project-teams/page.test.tsx deleted file mode 100644 index 71bd9d09ab..0000000000 --- a/apps/web/modules/ee/teams/project-teams/page.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { getTranslate } from "@/tolgee/server"; -import { getTeamsByProjectId } from "./lib/team"; -import { ProjectTeams } from "./page"; - -vi.mock("@/modules/ee/teams/project-teams/components/access-view", () => ({ - AccessView: (props: any) =>
    {JSON.stringify(props)}
    , -})); -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); -vi.mock("@/modules/projects/settings/components/project-config-navigation", () => ({ - ProjectConfigNavigation: (props: any) => ( -
    {JSON.stringify(props)}
    - ), -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }: any) => ( -
    - {pageTitle} - {children} -
    - ), -})); -vi.mock("./lib/team", () => ({ - getTeamsByProjectId: vi.fn(), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(), -})); - -describe("ProjectTeams", () => { - const params = Promise.resolve({ environmentId: "env-1" }); - - beforeEach(() => { - vi.mocked(getTeamsByProjectId).mockResolvedValue([ - { id: "team-1", name: "Team 1", memberCount: 2, permission: "readWrite" }, - { id: "team-2", name: "Team 2", memberCount: 1, permission: "read" }, - ]); - vi.mocked(getTranslate).mockResolvedValue((key) => key); - - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - project: { id: "project-1" }, - isOwner: true, - isManager: false, - } as any); - }); - afterEach(() => { - cleanup(); - }); - - test("renders all main components and passes correct props", async () => { - const ui = await ProjectTeams({ params }); - render(ui); - expect(screen.getByTestId("PageContentWrapper")).toBeInTheDocument(); - expect(screen.getByTestId("PageHeader")).toBeInTheDocument(); - expect(screen.getByText("common.project_configuration")).toBeInTheDocument(); - expect(screen.getByTestId("ProjectConfigNavigation")).toBeInTheDocument(); - expect(screen.getByTestId("AccessView")).toHaveTextContent('"environmentId":"env-1"'); - expect(screen.getByTestId("AccessView")).toHaveTextContent('"isOwnerOrManager":true'); - }); - - test("throws error if teams is null", async () => { - vi.mocked(getTeamsByProjectId).mockResolvedValue(null); - await expect(ProjectTeams({ params })).rejects.toThrow("common.teams_not_found"); - }); -}); diff --git a/apps/web/modules/ee/teams/team-list/components/create-team-button.test.tsx b/apps/web/modules/ee/teams/team-list/components/create-team-button.test.tsx deleted file mode 100644 index 5a9e7cf105..0000000000 --- a/apps/web/modules/ee/teams/team-list/components/create-team-button.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { CreateTeamButton } from "./create-team-button"; - -vi.mock("@/modules/ee/teams/team-list/components/create-team-modal", () => ({ - CreateTeamModal: ({ open, setOpen, organizationId }: any) => - open ?
    {organizationId}
    : null, -})); - -describe("CreateTeamButton", () => { - afterEach(() => { - cleanup(); - }); - - test("renders button with tolgee string", () => { - render(); - expect(screen.getByRole("button")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.create_new_team")).toBeInTheDocument(); - }); - - test("opens CreateTeamModal on button click", async () => { - render(); - await userEvent.click(screen.getByRole("button")); - expect(screen.getByTestId("CreateTeamModal")).toHaveTextContent("org-2"); - }); -}); diff --git a/apps/web/modules/ee/teams/team-list/components/create-team-modal.test.tsx b/apps/web/modules/ee/teams/team-list/components/create-team-modal.test.tsx deleted file mode 100644 index e0657d9e39..0000000000 --- a/apps/web/modules/ee/teams/team-list/components/create-team-modal.test.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { createTeamAction } from "@/modules/ee/teams/team-list/actions"; -import { CreateTeamModal } from "./create-team-modal"; - -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) => - open ?
    {children}
    : null, - DialogContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogFooter: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/ee/teams/team-list/actions", () => ({ - createTeamAction: vi.fn(), -})); -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(() => "error-message"), -})); - -describe("CreateTeamModal", () => { - afterEach(() => { - cleanup(); - }); - - const setOpen = vi.fn(); - - test("renders dialog, form, and tolgee strings", () => { - render(); - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-header")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-body")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-footer")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.create_new_team")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.team_name")).toBeInTheDocument(); - expect(screen.getByText("common.cancel")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.create")).toBeInTheDocument(); - }); - - test("calls setOpen(false) and resets teamName on cancel", async () => { - render(); - const input = screen.getByPlaceholderText("environments.settings.teams.enter_team_name"); - await userEvent.type(input, "My Team"); - await userEvent.click(screen.getByText("common.cancel")); - expect(setOpen).toHaveBeenCalledWith(false); - expect((input as HTMLInputElement).value).toBe(""); - }); - - test("submit button is disabled when input is empty", () => { - render(); - expect(screen.getByText("environments.settings.teams.create")).toBeDisabled(); - }); - - test("calls createTeamAction, shows success toast, calls onCreate, refreshes and closes dialog on success", async () => { - vi.mocked(createTeamAction).mockResolvedValue({ data: "team-123" }); - const onCreate = vi.fn(); - render(); - const input = screen.getByPlaceholderText("environments.settings.teams.enter_team_name"); - await userEvent.type(input, "My Team"); - await userEvent.click(screen.getByText("environments.settings.teams.create")); - await waitFor(() => { - expect(createTeamAction).toHaveBeenCalledWith({ name: "My Team", organizationId: "org-1" }); - expect(toast.success).toHaveBeenCalledWith("environments.settings.teams.team_created_successfully"); - expect(onCreate).toHaveBeenCalledWith("team-123"); - expect(setOpen).toHaveBeenCalledWith(false); - expect((input as HTMLInputElement).value).toBe(""); - }); - }); - - test("shows error toast if createTeamAction fails", async () => { - vi.mocked(createTeamAction).mockResolvedValue({}); - render(); - const input = screen.getByPlaceholderText("environments.settings.teams.enter_team_name"); - await userEvent.type(input, "My Team"); - await userEvent.click(screen.getByText("environments.settings.teams.create")); - await waitFor(() => { - expect(getFormattedErrorMessage).toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalledWith("error-message"); - }); - }); -}); diff --git a/apps/web/modules/ee/teams/team-list/components/manage-team-button.test.tsx b/apps/web/modules/ee/teams/team-list/components/manage-team-button.test.tsx deleted file mode 100644 index df0bdc309d..0000000000 --- a/apps/web/modules/ee/teams/team-list/components/manage-team-button.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ManageTeamButton } from "./manage-team-button"; - -vi.mock("@/modules/ui/components/tooltip", () => ({ - TooltipRenderer: ({ shouldRender, tooltipContent, children }: any) => - shouldRender ? ( -
    - {tooltipContent} - {children} -
    - ) : ( - <>{children} - ), -})); - -describe("ManageTeamButton", () => { - afterEach(() => { - cleanup(); - }); - - test("renders enabled button and calls onClick", async () => { - const onClick = vi.fn(); - render(); - const button = screen.getByRole("button"); - expect(button).toBeEnabled(); - expect(screen.getByText("environments.settings.teams.manage_team")).toBeInTheDocument(); - await userEvent.click(button); - expect(onClick).toHaveBeenCalled(); - }); - - test("renders disabled button with tooltip", () => { - const onClick = vi.fn(); - render(); - const button = screen.getByRole("button"); - expect(button).toBeDisabled(); - expect(screen.getByText("environments.settings.teams.manage_team")).toBeInTheDocument(); - expect(screen.getByTestId("TooltipRenderer")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.manage_team_disabled")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/teams/team-list/components/team-settings/delete-team.test.tsx b/apps/web/modules/ee/teams/team-list/components/team-settings/delete-team.test.tsx deleted file mode 100644 index 06c7f9cbdd..0000000000 --- a/apps/web/modules/ee/teams/team-list/components/team-settings/delete-team.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { deleteTeamAction } from "@/modules/ee/teams/team-list/actions"; -import { TTeam } from "@/modules/ee/teams/team-list/types/team"; -import { DeleteTeam } from "./delete-team"; - -vi.mock("@/modules/ui/components/label", () => ({ - Label: ({ children }: any) => , -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, ...props }: any) => , -})); -vi.mock("@/modules/ui/components/tooltip", () => ({ - TooltipRenderer: ({ shouldRender, tooltipContent, children }: any) => - shouldRender ? ( -
    - {tooltipContent} - {children} -
    - ) : ( - <>{children} - ), -})); -vi.mock("@/modules/ui/components/delete-dialog", () => ({ - DeleteDialog: ({ open, setOpen, deleteWhat, text, onDelete, isDeleting }: any) => - open ? ( -
    - {deleteWhat} - {text} - -
    - ) : null, -})); -vi.mock("next/navigation", () => ({ - useRouter: () => ({ refresh: vi.fn() }), -})); - -vi.mock("@/modules/ee/teams/team-list/actions", () => ({ - deleteTeamAction: vi.fn(), -})); - -describe("DeleteTeam", () => { - afterEach(() => { - cleanup(); - }); - - const baseProps = { - teamId: "team-1" as TTeam["id"], - onDelete: vi.fn(), - isOwnerOrManager: true, - }; - - test("renders danger zone label and delete button enabled for owner/manager", () => { - render(); - expect(screen.getByRole("button", { name: "environments.settings.teams.delete_team" })).toBeEnabled(); - }); - - test("renders tooltip and disables button if not owner/manager", () => { - render(); - expect(screen.getByTestId("TooltipRenderer")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.team_deletion_not_allowed")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "environments.settings.teams.delete_team" })).toBeDisabled(); - }); - - test("opens dialog on delete button click", async () => { - render(); - await userEvent.click(screen.getByRole("button", { name: "environments.settings.teams.delete_team" })); - expect(screen.getByTestId("DeleteDialog")).toBeInTheDocument(); - expect(screen.getByText("common.team")).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.teams.are_you_sure_you_want_to_delete_this_team") - ).toBeInTheDocument(); - }); - - test("calls deleteTeamAction, shows success toast, calls onDelete, and refreshes on confirm", async () => { - vi.mocked(deleteTeamAction).mockResolvedValue({ data: true }); - const onDelete = vi.fn(); - render(); - await userEvent.click(screen.getByRole("button", { name: "environments.settings.teams.delete_team" })); - await userEvent.click(screen.getByText("Confirm")); - expect(deleteTeamAction).toHaveBeenCalledWith({ teamId: baseProps.teamId }); - expect(toast.success).toHaveBeenCalledWith("environments.settings.teams.team_deleted_successfully"); - expect(onDelete).toHaveBeenCalled(); - }); - - test("shows error toast if deleteTeamAction fails", async () => { - vi.mocked(deleteTeamAction).mockResolvedValue({ data: false }); - render(); - await userEvent.click(screen.getByRole("button", { name: "environments.settings.teams.delete_team" })); - await userEvent.click(screen.getByText("Confirm")); - expect(toast.error).toHaveBeenCalledWith("common.something_went_wrong_please_try_again"); - }); -}); diff --git a/apps/web/modules/ee/teams/team-list/components/team-settings/team-settings-modal.test.tsx b/apps/web/modules/ee/teams/team-list/components/team-settings/team-settings-modal.test.tsx deleted file mode 100644 index d462b1cfa6..0000000000 --- a/apps/web/modules/ee/teams/team-list/components/team-settings/team-settings-modal.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ZTeamPermission } from "@/modules/ee/teams/project-teams/types/team"; -import { updateTeamDetailsAction } from "@/modules/ee/teams/team-list/actions"; -import { TOrganizationMember, TTeamDetails, ZTeamRole } from "@/modules/ee/teams/team-list/types/team"; -import { TeamSettingsModal } from "./team-settings-modal"; - -// Mock the Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ - open, - onOpenChange, - children, - }: { - open: boolean; - onOpenChange: (open: boolean) => void; - children: React.ReactNode; - }) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogHeader: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogBody: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogFooter: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("@/modules/ee/teams/team-list/components/team-settings/delete-team", () => ({ - DeleteTeam: () =>
    , -})); -vi.mock("@/modules/ee/teams/team-list/actions", () => ({ - updateTeamDetailsAction: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ refresh: vi.fn() }), -})); - -describe("TeamSettingsModal", () => { - afterEach(() => { - cleanup(); - }); - - const orgMembers: TOrganizationMember[] = [ - { id: "1", name: "Alice", role: "member" }, - { id: "2", name: "Bob", role: "manager" }, - ]; - const orgProjects = [ - { id: "p1", name: "Project 1" }, - { id: "p2", name: "Project 2" }, - ]; - const team: TTeamDetails = { - id: "t1", - name: "Team 1", - members: [{ name: "Alice", userId: "1", role: ZTeamRole.enum.contributor }], - projects: [ - { projectName: "pro1", projectId: "p1", permission: ZTeamPermission.enum.read }, - { projectName: "pro2", projectId: "p2", permission: ZTeamPermission.enum.readWrite }, - ], - organizationId: "org1", - }; - const setOpen = vi.fn(); - - test("renders modal, form, and tolgee strings", () => { - render( - - ); - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.team_name_settings_title")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.team_settings_description")).toBeInTheDocument(); - expect(screen.getByText("common.team_name")).toBeInTheDocument(); - expect(screen.getByText("common.members")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.add_members_description")).toBeInTheDocument(); - expect(screen.getByText("common.add_member")).toBeInTheDocument(); - expect(screen.getByText("common.projects")).toBeInTheDocument(); - expect(screen.getByText("common.add_project")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.add_projects_description")).toBeInTheDocument(); - expect(screen.getByText("common.cancel")).toBeInTheDocument(); - expect(screen.getByText("common.save")).toBeInTheDocument(); - expect(screen.getByTestId("DeleteTeam")).toBeInTheDocument(); - }); - - test("calls setOpen(false) when cancel button is clicked", async () => { - render( - - ); - await userEvent.click(screen.getByText("common.cancel")); - expect(setOpen).toHaveBeenCalledWith(false); - }); - - test("calls updateTeamDetailsAction and shows success toast on submit", async () => { - vi.mocked(updateTeamDetailsAction).mockResolvedValue({ data: true }); - render( - - ); - await userEvent.click(screen.getByText("common.save")); - await waitFor(() => { - expect(updateTeamDetailsAction).toHaveBeenCalled(); - expect(toast.success).toHaveBeenCalledWith("environments.settings.teams.team_updated_successfully"); - expect(setOpen).toHaveBeenCalledWith(false); - }); - }); - - test("shows error toast if updateTeamDetailsAction fails", async () => { - vi.mocked(updateTeamDetailsAction).mockResolvedValue({ data: false }); - render( - - ); - await userEvent.click(screen.getByText("common.save")); - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/web/modules/ee/teams/team-list/components/teams-table.test.tsx b/apps/web/modules/ee/teams/team-list/components/teams-table.test.tsx deleted file mode 100644 index 14c165d22a..0000000000 --- a/apps/web/modules/ee/teams/team-list/components/teams-table.test.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getTeamDetailsAction, getTeamRoleAction } from "@/modules/ee/teams/team-list/actions"; -import { TOrganizationMember, TOtherTeam, TUserTeam } from "@/modules/ee/teams/team-list/types/team"; -import { TeamsTable } from "./teams-table"; - -vi.mock("@/modules/ee/teams/team-list/components/create-team-button", () => ({ - CreateTeamButton: ({ organizationId }: any) => ( - - ), -})); - -vi.mock("@/modules/ee/teams/team-list/components/manage-team-button", () => ({ - ManageTeamButton: ({ disabled, onClick }: any) => ( - - ), -})); -vi.mock("@/modules/ee/teams/team-list/components/team-settings/team-settings-modal", () => ({ - TeamSettingsModal: (props: any) =>
    {props.team?.name}
    , -})); - -vi.mock("@/modules/ee/teams/team-list/actions", () => ({ - getTeamDetailsAction: vi.fn(), - getTeamRoleAction: vi.fn(), -})); - -vi.mock("@/modules/ui/components/badge", () => ({ - Badge: ({ text }: any) => {text}, -})); - -const userTeams: TUserTeam[] = [ - { id: "1", name: "Alpha", memberCount: 2, userRole: "admin" }, - { id: "2", name: "Beta", memberCount: 1, userRole: "contributor" }, -]; -const otherTeams: TOtherTeam[] = [ - { id: "3", name: "Gamma", memberCount: 3 }, - { id: "4", name: "Delta", memberCount: 1 }, -]; -const orgMembers: TOrganizationMember[] = [{ id: "u1", name: "User 1", role: "manager" }]; -const orgProjects = [{ id: "p1", name: "Project 1" }]; - -describe("TeamsTable", () => { - afterEach(() => { - cleanup(); - }); - - test("renders CreateTeamButton for owner/manager", () => { - render( - - ); - expect(screen.getByTestId("CreateTeamButton")).toHaveTextContent("org-1"); - }); - - test("does not render CreateTeamButton for non-owner/manager", () => { - render( - - ); - expect(screen.queryByTestId("CreateTeamButton")).toBeNull(); - }); - - test("renders empty state row if no teams", () => { - render( - - ); - expect(screen.getByText("environments.settings.teams.empty_teams_state")).toBeInTheDocument(); - }); - - test("renders userTeams and otherTeams rows", () => { - render( - - ); - expect(screen.getByText("Alpha")).toBeInTheDocument(); - expect(screen.getByText("Beta")).toBeInTheDocument(); - expect(screen.getByText("Gamma")).toBeInTheDocument(); - expect(screen.getByText("Delta")).toBeInTheDocument(); - expect(screen.getAllByTestId("ManageTeamButton").length).toBe(4); - expect(screen.getAllByTestId("Badge")[0]).toHaveTextContent( - "environments.settings.teams.you_are_a_member" - ); - expect(screen.getByText("2 common.members")).toBeInTheDocument(); - }); - - test("opens TeamSettingsModal when ManageTeamButton is clicked and team details are returned", async () => { - vi.mocked(getTeamDetailsAction).mockResolvedValue({ - data: { id: "1", name: "Alpha", organizationId: "org-1", members: [], projects: [] }, - }); - vi.mocked(getTeamRoleAction).mockResolvedValue({ data: "admin" }); - render( - - ); - await userEvent.click(screen.getAllByTestId("ManageTeamButton")[0]); - await waitFor(() => { - expect(screen.getByTestId("TeamSettingsModal")).toHaveTextContent("Alpha"); - }); - }); - - test("shows error toast if getTeamDetailsAction fails", async () => { - vi.mocked(getTeamDetailsAction).mockResolvedValue({ data: undefined }); - vi.mocked(getTeamRoleAction).mockResolvedValue({ data: undefined }); - render( - - ); - await userEvent.click(screen.getAllByTestId("ManageTeamButton")[0]); - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/web/modules/ee/two-factor-auth/components/confirm-password-form.test.tsx b/apps/web/modules/ee/two-factor-auth/components/confirm-password-form.test.tsx deleted file mode 100644 index 5dd188fb5e..0000000000 --- a/apps/web/modules/ee/two-factor-auth/components/confirm-password-form.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ConfirmPasswordForm } from "./confirm-password-form"; - -vi.mock("react-hot-toast", () => ({ - default: { - error: vi.fn(), - }, -})); - -vi.mock("@/modules/ee/two-factor-auth/actions", () => ({ - setupTwoFactorAuthAction: vi.fn(), -})); - -// Mock the translation function -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - const translations: Record = { - "environments.settings.profile.two_factor_authentication": "Two-Factor Authentication", - "environments.settings.profile.confirm_your_current_password_to_get_started": - "Confirm your current password to get started", - "common.password": "Password", - "common.confirm": "Confirm", - "common.cancel": "Cancel", - }; - return translations[key] || key; - }, - }), -})); - -describe("ConfirmPasswordForm", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockProps = { - setCurrentStep: vi.fn(), - setBackupCodes: vi.fn(), - setDataUri: vi.fn(), - setSecret: vi.fn(), - setOpen: vi.fn(), - }; - - test("renders the form with password input", () => { - render(); - - expect(screen.getByLabelText("Password")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Confirm" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); - }); - - test("handles form submission successfully", async () => { - const user = userEvent.setup(); - const mockResponse = { - data: { - backupCodes: ["code1", "code2"], - dataUri: "data:image/png;base64,test", - secret: "test-secret", - keyUri: "otpauth://totp/test", - }, - }; - - const { setupTwoFactorAuthAction } = await import("@/modules/ee/two-factor-auth/actions"); - vi.mocked(setupTwoFactorAuthAction).mockResolvedValueOnce(mockResponse); - - render(); - - const passwordInput = screen.getByLabelText("Password"); - await user.type(passwordInput, "testPassword123!"); - const submitButton = screen.getByRole("button", { name: "Confirm" }); - await user.click(submitButton); - - await waitFor(() => { - expect(setupTwoFactorAuthAction).toHaveBeenCalledWith({ password: "testPassword123!" }); - expect(mockProps.setBackupCodes).toHaveBeenCalledWith(["code1", "code2"]); - expect(mockProps.setDataUri).toHaveBeenCalledWith("data:image/png;base64,test"); - expect(mockProps.setSecret).toHaveBeenCalledWith("test-secret"); - expect(mockProps.setCurrentStep).toHaveBeenCalledWith("scanQRCode"); - }); - }); - - test("handles cancel button click", async () => { - const user = userEvent.setup(); - render(); - - await user.click(screen.getByRole("button", { name: "Cancel" })); - expect(mockProps.setOpen).toHaveBeenCalledWith(false); - }); -}); diff --git a/apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.test.tsx b/apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.test.tsx deleted file mode 100644 index 07e202b316..0000000000 --- a/apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.test.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { disableTwoFactorAuthAction } from "@/modules/ee/two-factor-auth/actions"; -import { DisableTwoFactorModal } from "./disable-two-factor-modal"; - -// Mock the Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ - children, - open, - onOpenChange, - }: { - children: React.ReactNode; - open: boolean; - onOpenChange: () => void; - }) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogBody: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogFooter: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), -})); - -vi.mock("@/modules/ee/two-factor-auth/actions", () => ({ - disableTwoFactorAuthAction: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - refresh: vi.fn(), - }), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -describe("DisableTwoFactorModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders dialog with correct title and description", () => { - render( {}} />); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-content")).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.profile.disable_two_factor_authentication") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.profile.disable_two_factor_authentication_description") - ).toBeInTheDocument(); - }); - - test("shows password input field", () => { - render( {}} />); - - expect(screen.getByLabelText("common.password")).toBeInTheDocument(); - }); - - test("toggles between 2FA code and backup code", async () => { - render( {}} />); - - // Initially shows 2FA code input - expect(screen.getByText("environments.settings.profile.two_factor_code")).toBeInTheDocument(); - - // Click to show backup code - await userEvent.click(screen.getByText("environments.settings.profile.lost_access")); - - // Now shows backup code input - expect(screen.getByText("environments.settings.profile.backup_code")).toBeInTheDocument(); - - // Click to go back - await userEvent.click(screen.getByText("common.go_back")); - - // Back to 2FA code - expect(screen.getByText("environments.settings.profile.two_factor_code")).toBeInTheDocument(); - }); - - test("submits form with 2FA code", async () => { - const mockSetOpen = vi.fn(); - vi.mocked(disableTwoFactorAuthAction).mockResolvedValue({ data: { message: "Success" } }); - - render(); - - // Fill in password - await userEvent.type(screen.getByLabelText("common.password"), "testPassword123!"); - - // Fill in 2FA code - const otpInputs = screen.getAllByRole("textbox"); - for (let i = 0; i < 6; i++) { - await userEvent.type(otpInputs[i], "1"); - } - - // Submit form - await userEvent.click(screen.getByText("common.disable")); - - expect(disableTwoFactorAuthAction).toHaveBeenCalledWith({ - password: "testPassword123!", - code: "111111", - backupCode: "", - }); - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - - test("submits form with backup code", async () => { - const mockSetOpen = vi.fn(); - vi.mocked(disableTwoFactorAuthAction).mockResolvedValue({ data: { message: "Success" } }); - - render(); - - // Fill in password - await userEvent.type(screen.getByLabelText("common.password"), "testPassword123!"); - - // Switch to backup code - await userEvent.click(screen.getByText("environments.settings.profile.lost_access")); - - // Fill in backup code - await userEvent.type(screen.getByPlaceholderText("XXXXX-XXXXX"), "12345-67890"); - - // Submit form - await userEvent.click(screen.getByText("common.disable")); - - expect(disableTwoFactorAuthAction).toHaveBeenCalledWith({ - password: "testPassword123!", - code: "", - backupCode: "12345-67890", - }); - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); -}); diff --git a/apps/web/modules/ee/two-factor-auth/components/display-backup-codes.test.tsx b/apps/web/modules/ee/two-factor-auth/components/display-backup-codes.test.tsx deleted file mode 100644 index cd2fe982c6..0000000000 --- a/apps/web/modules/ee/two-factor-auth/components/display-backup-codes.test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @vitest-environment jsdom - */ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { DisplayBackupCodes } from "./display-backup-codes"; - -vi.mock("react-hot-toast", () => ({ - toast: { - success: vi.fn(), - }, -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, variant, size, "data-testid": testId }: any) => ( - - ), -})); - -const translations: Record = { - "environments.settings.profile.enable_two_factor_authentication": "Enable Two-Factor Authentication", - "environments.settings.profile.save_the_following_backup_codes_in_a_safe_place": - "Save the following backup codes in a safe place", - "common.close": "Close", - "common.copy": "Copy", - "common.download": "Download", - "common.copied_to_clipboard": "Copied to clipboard", -}; - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => translations[key] || key, - }), -})); - -describe("DisplayBackupCodes", () => { - const mockBackupCodes = ["1234567890", "0987654321"]; - const mockSetOpen = vi.fn(); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders component structure correctly", () => { - render(); - - // Check main structural elements - expect(screen.getByTestId("backup-codes-title")).toBeInTheDocument(); - expect(screen.getByTestId("backup-codes-description")).toBeInTheDocument(); - expect(screen.getByTestId("backup-codes-grid")).toBeInTheDocument(); - - // Check buttons - expect(screen.getByTestId("close-button")).toBeInTheDocument(); - expect(screen.getByTestId("copy-button")).toBeInTheDocument(); - expect(screen.getByTestId("download-button")).toBeInTheDocument(); - }); - - test("displays formatted backup codes", () => { - render(); - - mockBackupCodes.forEach((code) => { - const formattedCode = `${code.slice(0, 5)}-${code.slice(5, 10)}`; - const codeElement = screen.getByTestId(`backup-code-${code}`); - expect(codeElement).toHaveTextContent(formattedCode); - }); - }); - - test("closes modal when close button is clicked", async () => { - const user = userEvent.setup(); - render(); - - await user.click(screen.getByTestId("close-button")); - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); -}); diff --git a/apps/web/modules/ee/two-factor-auth/components/enable-two-factor-modal.test.tsx b/apps/web/modules/ee/two-factor-auth/components/enable-two-factor-modal.test.tsx deleted file mode 100644 index 68c37c01b6..0000000000 --- a/apps/web/modules/ee/two-factor-auth/components/enable-two-factor-modal.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { EnableTwoFactorModal } from "./enable-two-factor-modal"; - -// Mock the Dialog component to expose the close functionality -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ - children, - open, - onOpenChange, - }: { - children: React.ReactNode; - open: boolean; - onOpenChange: () => void; - }) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -// Mock the child components -vi.mock("./confirm-password-form", () => ({ - ConfirmPasswordForm: ({ - setCurrentStep, - setDataUri, - setSecret, - }: { - setCurrentStep: (step: string) => void; - setDataUri: (uri: string) => void; - setSecret: (secret: string) => void; - }) => ( -
    - -
    - ), -})); - -vi.mock("./scan-qr-code", () => ({ - ScanQRCode: ({ - setCurrentStep, - dataUri, - secret, - }: { - setCurrentStep: (step: string) => void; - dataUri: string; - secret: string; - }) => ( -
    -
    {dataUri}
    -
    {secret}
    - -
    - ), -})); - -vi.mock("./enter-code", () => ({ - EnterCode: ({ setCurrentStep }: { setCurrentStep: (step: string) => void }) => ( -
    - -
    - ), -})); - -vi.mock("./display-backup-codes", () => ({ - DisplayBackupCodes: ({ backupCodes }: { backupCodes: string[] }) => ( -
    - {backupCodes.map((code, index) => ( -
    - {code} -
    - ))} -
    - ), -})); - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - refresh: vi.fn(), - }), -})); - -describe("EnableTwoFactorModal", () => { - afterEach(() => { - cleanup(); - }); - - test("renders confirm password form when open", () => { - const setOpen = vi.fn(); - render(); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-content")).toBeInTheDocument(); - expect(screen.getByTestId("confirm-password-form")).toBeInTheDocument(); - }); - - test("does not render when closed", () => { - const setOpen = vi.fn(); - render(); - - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - expect(screen.queryByTestId("confirm-password-form")).not.toBeInTheDocument(); - }); - - test("transitions through all steps correctly", async () => { - const setOpen = vi.fn(); - const user = userEvent.setup(); - render(); - - // Start at confirm password - expect(screen.getByTestId("confirm-password-form")).toBeInTheDocument(); - - // Move to scan QR code - await user.click(screen.getByText("Next")); - expect(screen.getByTestId("scan-qr-code")).toBeInTheDocument(); - expect(screen.getByTestId("data-uri")).toHaveTextContent("test-uri"); - expect(screen.getByTestId("secret")).toHaveTextContent("test-secret"); - - // Move to enter code - await user.click(screen.getByText("Next")); - expect(screen.getByTestId("enter-code")).toBeInTheDocument(); - - // Move to backup codes - await user.click(screen.getByText("Next")); - expect(screen.getByTestId("display-backup-codes")).toBeInTheDocument(); - }); - - test("resets state when dialog is closed", async () => { - const setOpen = vi.fn(); - const user = userEvent.setup(); - const { rerender } = render(); - - // Move to scan QR code - await user.click(screen.getByText("Next")); - expect(screen.getByTestId("scan-qr-code")).toBeInTheDocument(); - - // Close dialog using the close button - await user.click(screen.getByTestId("dialog-close")); - - // Verify setOpen was called with false - expect(setOpen).toHaveBeenCalledWith(false); - - // Reopen dialog - rerender(); - - // Should be back at the first step - expect(screen.getByTestId("confirm-password-form")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/ee/two-factor-auth/components/enter-code.test.tsx b/apps/web/modules/ee/two-factor-auth/components/enter-code.test.tsx deleted file mode 100644 index c9940d81d2..0000000000 --- a/apps/web/modules/ee/two-factor-auth/components/enter-code.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { enableTwoFactorAuthAction } from "@/modules/ee/two-factor-auth/actions"; -import { EnterCode } from "./enter-code"; - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock("@/modules/ee/two-factor-auth/actions", () => ({ - enableTwoFactorAuthAction: vi.fn(), -})); - -describe("EnterCode", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockProps = { - setCurrentStep: vi.fn(), - setOpen: vi.fn(), - refreshData: vi.fn(), - }; - - test("renders the component with correct title and description", () => { - render(); - - expect( - screen.getByText("environments.settings.profile.enable_two_factor_authentication") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.profile.enter_the_code_from_your_authenticator_app_below") - ).toBeInTheDocument(); - }); - - test("handles successful code submission", async () => { - const user = userEvent.setup(); - const mockResponse = { data: { message: "2FA enabled successfully" } }; - vi.mocked(enableTwoFactorAuthAction).mockResolvedValue(mockResponse); - - render(); - - // Find all input fields - const inputs = screen.getAllByRole("textbox"); - expect(inputs).toHaveLength(6); - - // Enter a valid 6-digit code - for (let i = 0; i < 6; i++) { - await user.type(inputs[i], "1"); - } - - const confirmButton = screen.getByText("common.confirm"); - await user.click(confirmButton); - - await waitFor(() => { - expect(enableTwoFactorAuthAction).toHaveBeenCalledWith({ code: "111111" }); - expect(mockProps.setCurrentStep).toHaveBeenCalledWith("backupCodes"); - expect(mockProps.refreshData).toHaveBeenCalled(); - }); - }); - - test("handles error during code submission", async () => { - const user = userEvent.setup(); - const mockError = { message: "Invalid code" }; - vi.mocked(enableTwoFactorAuthAction).mockRejectedValue(mockError); - - render(); - - // Find all input fields - const inputs = screen.getAllByRole("textbox"); - - // Enter a valid 6-digit code - for (let i = 0; i < 6; i++) { - await user.type(inputs[i], "1"); - } - - const confirmButton = screen.getByText("common.confirm"); - await user.click(confirmButton); - - await waitFor(() => { - expect(enableTwoFactorAuthAction).toHaveBeenCalledWith({ code: "111111" }); - }); - }); -}); diff --git a/apps/web/modules/ee/two-factor-auth/components/scan-qr-code.test.tsx b/apps/web/modules/ee/two-factor-auth/components/scan-qr-code.test.tsx deleted file mode 100644 index 6a24e22282..0000000000 --- a/apps/web/modules/ee/two-factor-auth/components/scan-qr-code.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ScanQRCode } from "./scan-qr-code"; - -describe("ScanQRCode", () => { - afterEach(() => { - cleanup(); - }); - - const mockProps = { - setCurrentStep: vi.fn(), - dataUri: "data:image/png;base64,test", - secret: "TEST123", - setOpen: vi.fn(), - }; - - test("renders the component with correct title and instructions", () => { - render(); - - expect( - screen.getByText("environments.settings.profile.enable_two_factor_authentication") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.profile.scan_the_qr_code_below_with_your_authenticator_app") - ).toBeInTheDocument(); - }); - - test("displays the QR code image", () => { - render(); - - const qrCodeImage = screen.getByAltText("QR code"); - expect(qrCodeImage).toBeInTheDocument(); - expect(qrCodeImage).toHaveAttribute("src", mockProps.dataUri); - }); - - test("displays the secret code and copy button", () => { - render(); - - expect(screen.getByText(mockProps.secret)).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.profile.or_enter_the_following_code_manually") - ).toBeInTheDocument(); - }); - - test("copies secret to clipboard when copy button is clicked", async () => { - const user = userEvent.setup(); - const mockWriteText = vi.fn().mockResolvedValue(undefined); - vi.stubGlobal("navigator", { - clipboard: { - writeText: mockWriteText, - }, - }); - - render(); - - const copyButton = screen.getAllByRole("button")[0]; - await user.click(copyButton); - - expect(mockWriteText).toHaveBeenCalledWith(mockProps.secret); - }); - - test("navigates to next step when next button is clicked", async () => { - const user = userEvent.setup(); - render(); - - const nextButton = screen.getByText("common.next"); - await user.click(nextButton); - - expect(mockProps.setCurrentStep).toHaveBeenCalledWith("enterCode"); - }); - - test("closes modal when cancel button is clicked", async () => { - const user = userEvent.setup(); - render(); - - const cancelButton = screen.getByText("common.cancel"); - await user.click(cancelButton); - - expect(mockProps.setOpen).toHaveBeenCalledWith(false); - }); -}); diff --git a/apps/web/modules/ee/two-factor-auth/components/two-factor-backup.test.tsx b/apps/web/modules/ee/two-factor-auth/components/two-factor-backup.test.tsx deleted file mode 100644 index 7021e7cf75..0000000000 --- a/apps/web/modules/ee/two-factor-auth/components/two-factor-backup.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { FormProvider, useForm } from "react-hook-form"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TwoFactorBackup } from "./two-factor-backup"; - -const mockUseTranslate = vi.fn(() => ({ - t: (key: string) => key, -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => mockUseTranslate(), -})); - -type FormValues = { - email: string; - password: string; - totpCode?: string; - backupCode?: string; -}; - -const TestComponent = () => { - const form = useForm({ - defaultValues: { - email: "", - password: "", - }, - }); - - return ( - - - - ); -}; - -describe("TwoFactorBackup", () => { - afterEach(() => { - cleanup(); - }); - - test("renders backup code input field", () => { - render(); - - const input = screen.getByPlaceholderText("XXXXX-XXXXX"); - expect(input).toBeInTheDocument(); - expect(input).toHaveAttribute("required"); - }); -}); diff --git a/apps/web/modules/ee/two-factor-auth/components/two-factor.test.tsx b/apps/web/modules/ee/two-factor-auth/components/two-factor.test.tsx deleted file mode 100644 index 34e40d7b0a..0000000000 --- a/apps/web/modules/ee/two-factor-auth/components/two-factor.test.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { FormProvider, useForm } from "react-hook-form"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TwoFactor } from "./two-factor"; - -const mockUseTranslate = vi.fn(() => ({ - t: (key: string) => key, -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => mockUseTranslate(), -})); - -type FormValues = { - email: string; - password: string; - totpCode?: string; - backupCode?: string; -}; - -const TestWrapper = () => { - const form = useForm({ - defaultValues: { - email: "", - password: "", - }, - }); - - return ( - - - - ); -}; - -describe("TwoFactor", () => { - afterEach(() => { - cleanup(); - }); - - test("renders OTP input fields", () => { - render(); - const inputs = screen.getAllByRole("textbox"); - expect(inputs).toHaveLength(6); - inputs.forEach((input) => { - expect(input).toHaveAttribute("inputmode", "numeric"); - expect(input).toHaveAttribute("maxlength", "6"); - expect(input).toHaveAttribute("pattern", "\\d{1}"); - }); - }); -}); diff --git a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.test.tsx b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.test.tsx deleted file mode 100644 index 37b872ebd9..0000000000 --- a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.test.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TUser } from "@formbricks/types/user"; -import { - removeOrganizationEmailLogoUrlAction, - sendTestEmailAction, - updateOrganizationEmailLogoUrlAction, -} from "@/modules/ee/whitelabel/email-customization/actions"; -import { handleFileUpload } from "@/modules/storage/file-upload"; -import { EmailCustomizationSettings } from "./email-customization-settings"; - -vi.mock("@/lib/constants", () => ({ - IS_STORAGE_CONFIGURED: true, -})); - -vi.mock("@/modules/ee/whitelabel/email-customization/actions", () => ({ - removeOrganizationEmailLogoUrlAction: vi.fn(), - sendTestEmailAction: vi.fn(), - updateOrganizationEmailLogoUrlAction: vi.fn(), -})); - -vi.mock("@/modules/storage/file-upload", () => ({ - handleFileUpload: vi.fn(), -})); - -const defaultProps = { - organization: { - id: "org-123", - whitelabel: { - logoUrl: "https://example.com/current-logo.png", - }, - billing: { - plan: "enterprise", - }, - } as TOrganization, - hasWhiteLabelPermission: true, - environmentId: "env-123", - isReadOnly: false, - isFormbricksCloud: false, - user: { - id: "user-123", - name: "Test User", - } as TUser, - fbLogoUrl: "https://example.com/fallback-logo.png", - isStorageConfigured: true, -}; - -describe("EmailCustomizationSettings", () => { - beforeEach(() => { - cleanup(); - }); - - test("renders the logo if one is set and shows Replace/Remove buttons", () => { - render(); - - const logoImage = screen.getByTestId("email-customization-preview-image"); - - expect(logoImage).toBeInTheDocument(); - - const srcUrl = new URL(logoImage.getAttribute("src")!, window.location.origin); - const originalUrl = srcUrl.searchParams.get("url"); - expect(originalUrl).toBe("https://example.com/current-logo.png"); - - // Since a logo is present, the “Replace Logo” and “Remove Logo” buttons should appear - expect(screen.getByTestId("replace-logo-button")).toBeInTheDocument(); - expect(screen.getByTestId("remove-logo-button")).toBeInTheDocument(); - }); - - test("calls removeOrganizationEmailLogoUrlAction when removing logo", async () => { - vi.mocked(removeOrganizationEmailLogoUrlAction).mockResolvedValue({ - data: true, - }); - - render(); - - const user = userEvent.setup(); - const removeButton = screen.getByTestId("remove-logo-button"); - await user.click(removeButton); - - expect(removeOrganizationEmailLogoUrlAction).toHaveBeenCalledTimes(1); - expect(removeOrganizationEmailLogoUrlAction).toHaveBeenCalledWith({ - organizationId: "org-123", - }); - }); - - test("calls updateOrganizationEmailLogoUrlAction after uploading and clicking save", async () => { - vi.mocked(handleFileUpload).mockResolvedValueOnce({ - url: "https://example.com/new-uploaded-logo.png", - }); - vi.mocked(updateOrganizationEmailLogoUrlAction).mockResolvedValue({ - data: true, - }); - - render(); - - const user = userEvent.setup(); - - // 1. Replace the logo by uploading a new file - const fileInput = screen.getAllByTestId("upload-file-input"); - const testFile = new File(["dummy content"], "test-image.png", { type: "image/png" }); - await user.upload(fileInput[0], testFile); - - // 2. Click “Save” - const saveButton = screen.getAllByRole("button", { name: /save/i }); - await user.click(saveButton[0]); - - expect(handleFileUpload).toHaveBeenCalledWith(testFile, "env-123", ["jpeg", "png", "jpg", "webp"]); - expect(updateOrganizationEmailLogoUrlAction).toHaveBeenCalledWith({ - organizationId: "org-123", - logoUrl: "https://example.com/new-uploaded-logo.png", - }); - }); - - test("sends test email if a logo is saved and the user clicks 'Send Test Email'", async () => { - vi.mocked(sendTestEmailAction).mockResolvedValue({ - data: { success: true }, - }); - - render(); - - const user = userEvent.setup(); - const testEmailButton = screen.getByTestId("send-test-email-button"); - await user.click(testEmailButton); - - expect(sendTestEmailAction).toHaveBeenCalledWith({ - organizationId: "org-123", - }); - }); - - test("displays upgrade prompt if hasWhiteLabelPermission is false", () => { - render(); - // Check for text about upgrading - expect(screen.getByText(/customize_email_with_a_higher_plan/i)).toBeInTheDocument(); - }); - - test("shows read-only warning if isReadOnly is true", () => { - render(); - - expect( - screen.getByText(/only_owners_managers_and_manage_access_members_can_perform_this_action/i) - ).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/email/components/email-template.test.tsx b/apps/web/modules/email/components/email-template.test.tsx deleted file mode 100644 index 9bd0f58a3d..0000000000 --- a/apps/web/modules/email/components/email-template.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { TFnType } from "@tolgee/react"; -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { EmailTemplate } from "./email-template"; - -const mockTranslate: TFnType = (key) => key; - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - FB_LOGO_URL: "https://example.com/mock-logo.png", - IMPRINT_URL: "https://example.com/imprint", - PRIVACY_URL: "https://example.com/privacy", - IMPRINT_ADDRESS: "Imprint Address", -})); - -const defaultProps = { - children:
    Test Content
    , - logoUrl: "https://example.com/custom-logo.png", - t: mockTranslate, -}; - -describe("EmailTemplate", () => { - beforeEach(() => { - cleanup(); - }); - - test("renders the default logo if no custom logo is provided", async () => { - const emailTemplateElement = await EmailTemplate({ - children:
    Test Content
    , - logoUrl: undefined, - t: mockTranslate, - }); - - render(emailTemplateElement); - - const logoImage = screen.getByTestId("default-logo-image"); - expect(logoImage).toBeInTheDocument(); - expect(logoImage).toHaveAttribute("src", "https://example.com/mock-logo.png"); - }); - - test("renders the custom logo if provided", async () => { - const emailTemplateElement = await EmailTemplate({ - ...defaultProps, - }); - - render(emailTemplateElement); - - const logoImage = screen.getByTestId("logo-image"); - expect(logoImage).toBeInTheDocument(); - expect(logoImage).toHaveAttribute("src", "https://example.com/custom-logo.png"); - }); - - test("renders the children content", async () => { - const emailTemplateElement = await EmailTemplate({ - ...defaultProps, - }); - - render(emailTemplateElement); - - expect(screen.getByTestId("child-text")).toBeInTheDocument(); - }); - - test("renders the imprint and privacy policy links if provided", async () => { - const emailTemplateElement = await EmailTemplate({ - ...defaultProps, - }); - - render(emailTemplateElement); - - expect(screen.getByText("emails.imprint")).toBeInTheDocument(); - expect(screen.getByText("emails.privacy_policy")).toBeInTheDocument(); - }); - - test("renders the imprint address if provided", async () => { - const emailTemplateElement = await EmailTemplate({ - ...defaultProps, - }); - - render(emailTemplateElement); - - expect(screen.getByText("emails.email_template_text_1")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/email/emails/lib/tests/utils.test.tsx b/apps/web/modules/email/emails/lib/tests/utils.test.tsx deleted file mode 100644 index ddc69bb719..0000000000 --- a/apps/web/modules/email/emails/lib/tests/utils.test.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { TFnType, TranslationKey } from "@tolgee/react"; -import { describe, expect, test, vi } from "vitest"; -import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { renderEmailResponseValue } from "../utils"; - -// Mock the components from @react-email/components to avoid dependency issues -vi.mock("@react-email/components", () => ({ - Text: ({ children, className }) =>

    {children}

    , - Container: ({ children }) =>
    {children}
    , - Row: ({ children, className }) =>
    {children}
    , - Column: ({ children, className }) =>
    {children}
    , - Link: ({ children, href }) => {children}, - Img: ({ src, alt, className }) => {alt}, -})); - -// Mock dependencies -vi.mock("@/modules/storage/utils", () => ({ - getOriginalFileNameFromUrl: (url: string) => { - // Extract filename from the URL for testing purposes - const parts = url.split("/"); - return parts[parts.length - 1]; - }, -})); - -// Mock translation function -const mockTranslate = (key: TranslationKey) => key; - -describe("renderEmailResponseValue", () => { - describe("FileUpload question type", () => { - test("renders clickable file upload links with file icons and truncated file names when overrideFileUploadResponse is false", async () => { - // Arrange - const fileUrls = [ - "https://example.com/files/file1.pdf", - "https://example.com/files/very-long-filename-that-should-be-truncated.docx", - ]; - - // Act - const result = await renderEmailResponseValue( - fileUrls, - TSurveyQuestionTypeEnum.FileUpload, - mockTranslate as unknown as TFnType, - false - ); - - render(result); - - // Assert - // Check if we have the correct number of links - const links = screen.getAllByRole("link"); - expect(links).toHaveLength(2); - - // Check if links have correct hrefs - expect(links[0]).toHaveAttribute("href", fileUrls[0]); - expect(links[1]).toHaveAttribute("href", fileUrls[1]); - - // Check if file names are displayed - expect(screen.getByText("file1.pdf")).toBeInTheDocument(); - expect(screen.getByText("very-long-filename-that-should-be-truncated.docx")).toBeInTheDocument(); - - // Check for SVG icons (file icons) - const svgElements = document.querySelectorAll("svg"); - expect(svgElements.length).toBeGreaterThanOrEqual(2); - }); - - test("renders a message when overrideFileUploadResponse is true", async () => { - // Arrange - const fileUrls = ["https://example.com/files/file1.pdf"]; - const expectedMessage = "emails.render_email_response_value_file_upload_response_link_not_included"; - - // Act - const result = await renderEmailResponseValue( - fileUrls, - TSurveyQuestionTypeEnum.FileUpload, - mockTranslate as unknown as TFnType, - true - ); - - render(result); - - // Assert - // Check that the override message is displayed - expect(screen.getByText(expectedMessage)).toBeInTheDocument(); - expect(screen.getByText(expectedMessage)).toHaveClass( - "mt-0", - "break-words", - "whitespace-pre-wrap", - "italic", - "text-sm" - ); - }); - }); - - describe("PictureSelection question type", () => { - test("renders images with appropriate alt text and styling", async () => { - // Arrange - const imageUrls = [ - "https://example.com/images/sunset.jpg", - "https://example.com/images/mountain.png", - "https://example.com/images/beach.webp", - ]; - - // Act - const result = await renderEmailResponseValue( - imageUrls, - TSurveyQuestionTypeEnum.PictureSelection, - mockTranslate as unknown as TFnType - ); - - render(result); - - // Assert - // Check if we have the correct number of images - const images = screen.getAllByRole("img"); - expect(images).toHaveLength(3); - - // Check if images have correct src attributes - expect(images[0]).toHaveAttribute("src", imageUrls[0]); - expect(images[1]).toHaveAttribute("src", imageUrls[1]); - expect(images[2]).toHaveAttribute("src", imageUrls[2]); - - // Check if images have correct alt text (extracted from URL) - expect(images[0]).toHaveAttribute("alt", "sunset.jpg"); - expect(images[1]).toHaveAttribute("alt", "mountain.png"); - expect(images[2]).toHaveAttribute("alt", "beach.webp"); - - // Check if images have the expected styling class - expect(images[0]).toHaveAttribute("class", "m-2 h-28"); - expect(images[1]).toHaveAttribute("class", "m-2 h-28"); - expect(images[2]).toHaveAttribute("class", "m-2 h-28"); - }); - }); - - describe("Ranking question type", () => { - test("renders ranking responses with proper numbering and styling", async () => { - // Arrange - const rankingItems = ["First Choice", "Second Choice", "Third Choice"]; - - // Act - const result = await renderEmailResponseValue( - rankingItems, - TSurveyQuestionTypeEnum.Ranking, - mockTranslate as unknown as TFnType - ); - - render(result); - - // Assert - // Check if we have the correct number of ranking items - const rankingElements = document.querySelectorAll(".mb-1"); - expect(rankingElements).toHaveLength(3); - - // Check if each item has the correct number and styling - rankingItems.forEach((item, index) => { - const itemElement = screen.getByText(item); - expect(itemElement).toBeInTheDocument(); - expect(itemElement).toHaveClass("rounded", "bg-slate-100", "px-2", "py-1"); - - // Check if the ranking number is present - const rankNumber = screen.getByText(`#${index + 1}`); - expect(rankNumber).toBeInTheDocument(); - expect(rankNumber).toHaveClass("text-slate-400"); - }); - }); - }); - - describe("handling long text responses", () => { - test("properly formats extremely long text responses with line breaks", async () => { - // Arrange - // Create a very long text response with multiple paragraphs and long words - const longTextResponse = `This is the first paragraph of a very long response that might be submitted by a user in an open text question. It contains detailed information and feedback. - -This is the second paragraph with an extremely long word: ${"supercalifragilisticexpialidocious".repeat(5)} - -And here's a third paragraph with more text and some line -breaks within the paragraph itself to test if they are preserved properly. - -${"This is a very long sentence that should wrap properly within the email layout and not break the formatting. ".repeat(10)}`; - - // Act - const result = await renderEmailResponseValue( - longTextResponse, - TSurveyQuestionTypeEnum.OpenText, - mockTranslate as unknown as TFnType - ); - - render(result); - - // Assert - // Check if the text is rendered - const textElement = screen.getByText(/This is the first paragraph/); - expect(textElement).toBeInTheDocument(); - - // Check if the extremely long word is rendered without breaking the layout - expect(screen.getByText(/supercalifragilisticexpialidocious/)).toBeInTheDocument(); - - // Verify the text element has the proper CSS classes for handling long text - expect(textElement).toHaveClass("break-words"); - expect(textElement).toHaveClass("whitespace-pre-wrap"); - - // Verify the content is preserved exactly as provided - expect(textElement.textContent).toBe(longTextResponse); - }); - }); - - describe("Default case (unmatched question type)", () => { - test("renders the response as plain text when the question type does not match any specific case", async () => { - // Arrange - const response = "This is a plain text response"; - // Using a question type that doesn't match any specific case in the switch statement - const questionType = "CustomQuestionType" as any; - - // Act - const result = await renderEmailResponseValue( - response, - questionType, - mockTranslate as unknown as TFnType - ); - - render(result); - - // Assert - // Check if the response text is rendered - expect(screen.getByText(response)).toBeInTheDocument(); - - // Check if the text has the expected styling classes - const textElement = screen.getByText(response); - expect(textElement).toHaveClass("mt-0", "break-words", "whitespace-pre-wrap", "text-sm"); - }); - - test("handles array responses in the default case by rendering them as text", async () => { - // Arrange - const response = ["Item 1", "Item 2", "Item 3"]; - const questionType = "AnotherCustomType" as any; - - // Act - const result = await renderEmailResponseValue( - response, - questionType, - mockTranslate as unknown as TFnType - ); - - // Create a fresh container for this test to avoid conflicts with previous renders - const container = document.createElement("div"); - render(result, { container }); - - // Assert - // Check if the text element contains all items from the response array - const textElement = container.querySelector("p"); - expect(textElement).not.toBeNull(); - expect(textElement).toHaveClass("mt-0", "break-words", "whitespace-pre-wrap", "text-sm"); - - // Verify each item is present in the text content - response.forEach((item) => { - expect(textElement?.textContent).toContain(item); - }); - }); - }); -}); diff --git a/apps/web/modules/integrations/webhooks/components/add-webhook-modal.test.tsx b/apps/web/modules/integrations/webhooks/components/add-webhook-modal.test.tsx deleted file mode 100644 index 4884efd4da..0000000000 --- a/apps/web/modules/integrations/webhooks/components/add-webhook-modal.test.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { AddWebhookModal } from "./add-webhook-modal"; - -// Mock the Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ - children, - open, - onOpenChange, - }: { - children: React.ReactNode; - open: boolean; - onOpenChange: (open: boolean) => void; - }) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -

    - {children} -

    - ), - DialogDescription: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -

    - {children} -

    - ), - DialogBody: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogFooter: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -// Mock the child components -vi.mock("./survey-checkbox-group", () => ({ - SurveyCheckboxGroup: ({ - surveys, - selectedSurveys, - selectedAllSurveys, - onSelectAllSurveys, - onSelectedSurveyChange, - allowChanges, - }: any) => ( -
    - - {surveys.map((survey: any) => ( - - ))} -
    - ), -})); - -vi.mock("./trigger-checkbox-group", () => ({ - TriggerCheckboxGroup: ({ selectedTriggers, onCheckboxChange, allowChanges }: any) => ( -
    - - -
    - ), -})); - -// Mock actions -vi.mock("../actions", () => ({ - createWebhookAction: vi.fn(), - testEndpointAction: vi.fn(), -})); - -// Mock other dependencies -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - refresh: vi.fn(), - }), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -describe("AddWebhookModal", () => { - const mockProps = { - environmentId: "env-123", - open: true, - surveys: [ - { id: "survey-1", name: "Test Survey 1" }, - { id: "survey-2", name: "Test Survey 2" }, - ], - setOpen: vi.fn(), - }; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders dialog with correct title and description", () => { - render(); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-content")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent( - "environments.integrations.webhooks.add_webhook" - ); - expect( - screen.getByText("environments.integrations.webhooks.add_webhook_description") - ).toBeInTheDocument(); - }); - - test("does not render when closed", () => { - render(); - - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); - - test("renders form fields", () => { - render(); - - expect(screen.getByLabelText("common.name")).toBeInTheDocument(); - expect(screen.getByLabelText("common.url")).toBeInTheDocument(); - expect(screen.getByTestId("trigger-checkbox-group")).toBeInTheDocument(); - expect(screen.getByTestId("survey-checkbox-group")).toBeInTheDocument(); - }); - - test("renders footer buttons", () => { - render(); - - expect(screen.getByTestId("dialog-footer")).toBeInTheDocument(); - expect(screen.getByText("common.cancel")).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "environments.integrations.webhooks.add_webhook" }) - ).toBeInTheDocument(); - }); - - test("calls setOpen when cancel button is clicked", async () => { - const user = userEvent.setup(); - render(); - - await user.click(screen.getByText("common.cancel")); - - expect(mockProps.setOpen).toHaveBeenCalledWith(false); - }); - - test("renders webhook icon in header", () => { - render(); - - expect(screen.getByTestId("dialog-header")).toBeInTheDocument(); - // The Webhook icon should be rendered within the header - const header = screen.getByTestId("dialog-header"); - expect(header).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/integrations/webhooks/components/webhook-detail-modal.test.tsx b/apps/web/modules/integrations/webhooks/components/webhook-detail-modal.test.tsx deleted file mode 100644 index b197cb545c..0000000000 --- a/apps/web/modules/integrations/webhooks/components/webhook-detail-modal.test.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { Webhook } from "@prisma/client"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { WebhookModal } from "@/modules/integrations/webhooks/components/webhook-detail-modal"; - -// Mock the Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ - open, - onOpenChange, - children, - }: { - open: boolean; - onOpenChange: (open: boolean) => void; - children: React.ReactNode; - }) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ - children, - disableCloseOnOutsideClick, - }: { - children: React.ReactNode; - disableCloseOnOutsideClick?: boolean; - }) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -// Mock the tab components -vi.mock("@/modules/integrations/webhooks/components/webhook-overview-tab", () => ({ - WebhookOverviewTab: ({ webhook }: { webhook: Webhook }) => ( -
    Overview for {webhook.name}
    - ), -})); - -vi.mock("@/modules/integrations/webhooks/components/webhook-settings-tab", () => ({ - WebhookSettingsTab: ({ webhook, setOpen }: { webhook: Webhook; setOpen: (v: boolean) => void }) => ( -
    - Settings for {webhook.name} - -
    - ), -})); - -// Mock useTranslate -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - const translations = { - "common.overview": "Overview", - "common.settings": "Settings", - "common.webhook": "Webhook", - }; - return translations[key] || key; - }, - }), -})); - -// Mock lucide-react -vi.mock("lucide-react", () => ({ - WebhookIcon: () => 🪝, -})); - -const mockWebhook: Webhook = { - id: "webhook-1", - name: "Test Webhook", - url: "https://example.com/webhook", - source: "user", - triggers: [], - surveyIds: [], - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "env-1", -}; - -const mockSurveys: TSurvey[] = []; - -describe("WebhookModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders correctly when open", () => { - const setOpen = vi.fn(); - render( - - ); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Test Webhook"); - expect(screen.getByTestId("webhook-icon")).toBeInTheDocument(); - expect(screen.getByText("Overview")).toBeInTheDocument(); - expect(screen.getByText("Settings")).toBeInTheDocument(); - expect(screen.getByTestId("webhook-overview-tab")).toBeInTheDocument(); - }); - - test("does not render when closed", () => { - const setOpen = vi.fn(); - render( - - ); - - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); - - test("switches tabs correctly", async () => { - const setOpen = vi.fn(); - const user = userEvent.setup(); - - render( - - ); - - // Initially shows overview tab - expect(screen.getByTestId("webhook-overview-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("webhook-settings-tab")).not.toBeInTheDocument(); - - // Click settings tab - const settingsTab = screen.getByText("Settings"); - await user.click(settingsTab); - - // Now shows settings tab - expect(screen.queryByTestId("webhook-overview-tab")).not.toBeInTheDocument(); - expect(screen.getByTestId("webhook-settings-tab")).toBeInTheDocument(); - - // Click overview tab again - const overviewTab = screen.getByText("Overview"); - await user.click(overviewTab); - - // Back to overview tab - expect(screen.getByTestId("webhook-overview-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("webhook-settings-tab")).not.toBeInTheDocument(); - }); - - test("uses webhook as title when name is not provided", () => { - const setOpen = vi.fn(); - const webhookWithoutName = { ...mockWebhook, name: "" }; - - render( - - ); - - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Webhook"); - }); - - test("resets to first tab when modal is reopened", async () => { - const setOpen = vi.fn(); - const user = userEvent.setup(); - - const { rerender } = render( - - ); - - // Switch to settings tab - const settingsTab = screen.getByText("Settings"); - await user.click(settingsTab); - expect(screen.getByTestId("webhook-settings-tab")).toBeInTheDocument(); - - // Close modal - rerender( - - ); - - // Reopen modal - rerender( - - ); - - // Should be back to overview tab - expect(screen.getByTestId("webhook-overview-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("webhook-settings-tab")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/organization/components/CreateOrganizationModal/index.test.tsx b/apps/web/modules/organization/components/CreateOrganizationModal/index.test.tsx deleted file mode 100644 index c00fe47b4f..0000000000 --- a/apps/web/modules/organization/components/CreateOrganizationModal/index.test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { createOrganizationAction } from "@/modules/organization/actions"; -import { CreateOrganizationModal } from "./index"; - -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) => - open ?
    {children}
    : null, - DialogContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogFooter: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("lucide-react", () => ({ - PlusCircleIcon: () => , -})); -const mockPush = vi.fn(); -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(() => ({ - push: mockPush, - })), -})); -vi.mock("@/modules/organization/actions", () => ({ - createOrganizationAction: vi.fn(), -})); -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(() => "Formatted error"), -})); -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ t: (k) => k }), -})); - -describe("CreateOrganizationModal", () => { - afterEach(() => { - cleanup(); - }); - - test("renders dialog and form fields", () => { - render(); - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-header")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-description")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-body")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-footer")).toBeInTheDocument(); - expect( - screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder") - ).toBeInTheDocument(); - expect(screen.getByText("common.cancel")).toBeInTheDocument(); - }); - - test("disables submit button if organization name is empty", () => { - render(); - const submitBtn = screen.getByText("environments.settings.general.create_new_organization", { - selector: "button[type='submit']", - }); - expect(submitBtn).toBeDisabled(); - }); - - test("enables submit button when organization name is entered", async () => { - render(); - const input = screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder"); - const submitBtn = screen.getByText("environments.settings.general.create_new_organization", { - selector: "button[type='submit']", - }); - await userEvent.type(input, "Formbricks Org"); - expect(submitBtn).not.toBeDisabled(); - }); - - test("calls createOrganizationAction and closes dialog on success", async () => { - const setOpen = vi.fn(); - vi.mocked(createOrganizationAction).mockResolvedValue({ data: { id: "org-1" } } as any); - render(); - const input = screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder"); - await userEvent.type(input, "Formbricks Org"); - const submitBtn = screen.getByText("environments.settings.general.create_new_organization", { - selector: "button[type='submit']", - }); - await userEvent.click(submitBtn); - await waitFor(() => { - expect(createOrganizationAction).toHaveBeenCalledWith({ organizationName: "Formbricks Org" }); - expect(setOpen).toHaveBeenCalledWith(false); - expect(mockPush).toHaveBeenCalledWith("/organizations/org-1"); - }); - }); - - test("shows error toast on failure", async () => { - const setOpen = vi.fn(); - vi.mocked(createOrganizationAction).mockResolvedValue({}); - render(); - const input = screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder"); - await userEvent.type(input, "Fail Org"); - const submitBtn = screen.getByText("environments.settings.general.create_new_organization", { - selector: "button[type='submit']", - }); - await userEvent.click(submitBtn); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Formatted error"); - }); - }); - - test("does not submit if name is only whitespace", async () => { - const setOpen = vi.fn(); - render(); - const input = screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder"); - await userEvent.type(input, " "); - const submitBtn = screen.getByText("environments.settings.general.create_new_organization", { - selector: "button[type='submit']", - }); - await userEvent.click(submitBtn); - expect(createOrganizationAction).not.toHaveBeenCalled(); - }); - - test("calls setOpen(false) when cancel is clicked", async () => { - const setOpen = vi.fn(); - render(); - const cancelBtn = screen.getByText("common.cancel"); - await userEvent.click(cancelBtn); - expect(setOpen).toHaveBeenCalledWith(false); - }); -}); diff --git a/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.test.tsx b/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.test.tsx deleted file mode 100644 index 2849a61a48..0000000000 --- a/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.test.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TProject } from "@formbricks/types/project"; -import { AddApiKeyModal } from "./add-api-key-modal"; - -// Mock the Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ - open, - onOpenChange, - children, - }: { - open: boolean; - onOpenChange: (open: boolean) => void; - children: React.ReactNode; - }) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogFooter: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -// Mock the translate hook -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, // Return the key as is for testing - }), -})); - -// Base project definition (customize as needed) -const baseProject = { - id: "project1", - name: "Project 1", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "org1", - styling: { - allowStyleOverwrite: true, - brandColor: { light: "#000000" }, - }, - recontactDays: 0, - inAppSurveyBranding: false, - linkSurveyBranding: false, - config: { - channel: "link" as const, - industry: "saas" as const, - }, - placement: "bottomLeft" as const, - clickOutsideClose: true, - darkOverlay: false, - languages: [], -}; - -const mockProjects: TProject[] = [ - { - ...baseProject, - environments: [ - { - id: "env1", - type: "production", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project1", - appSetupCompleted: true, - }, - { - id: "env2", - type: "development", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project1", - appSetupCompleted: true, - }, - ], - } as TProject, - { - ...baseProject, - id: "project2", - name: "Project 2", - environments: [ - { - id: "env3", - type: "production", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project2", - appSetupCompleted: true, - }, - { - id: "env4", - type: "development", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project2", - appSetupCompleted: true, - }, - ], - } as TProject, -]; - -describe("AddApiKeyModal", () => { - const mockSetOpen = vi.fn(); - const mockOnSubmit = vi.fn(); - - const defaultProps = { - open: true, - setOpen: mockSetOpen, - onSubmit: mockOnSubmit, - projects: mockProjects, - isCreatingAPIKey: false, - }; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders the modal with initial state", () => { - render(); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("environments.project.api_keys.add_api_key"); - expect(screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack")).toBeInTheDocument(); - expect(screen.getByText("environments.project.api_keys.project_access")).toBeInTheDocument(); - }); - - test("handles label input", async () => { - render(); - const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack") as HTMLInputElement; - - await userEvent.type(labelInput, "Test API Key"); - expect(labelInput.value).toBe("Test API Key"); - }); - - test("handles permission changes", async () => { - render(); - - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // Open project dropdown for the first permission row - const projectDropdowns = screen.getAllByRole("button", { name: /Project 1/i }); - await userEvent.click(projectDropdowns[0]); - - // Wait for dropdown content and select 'Project 2' - const project2Option = await screen.findByRole("menuitem", { name: "Project 2" }); - await userEvent.click(project2Option); - - // Verify project selection by checking the updated button text - const updatedButton = await screen.findByRole("button", { name: "Project 2" }); - expect(updatedButton).toBeInTheDocument(); - }); - - test("adds and removes permissions", async () => { - render(); - - // Add new permission - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - await userEvent.click(addButton); - - // Verify new permission row is added - const deleteButtons = screen.getAllByRole("button", { name: "" }); // Trash icons - expect(deleteButtons).toHaveLength(2); - - // Remove the new permission - await userEvent.click(deleteButtons[1]); - - // Check that only the original permission row remains - expect(screen.getAllByRole("button", { name: "" })).toHaveLength(1); - }); - - test("submits form with correct data", async () => { - render(); - - // Fill in label - const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack") as HTMLInputElement; - await userEvent.type(labelInput, "Test API Key"); - - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // Click submit - const submitButton = screen.getByRole("button", { - name: "environments.project.api_keys.add_api_key", - }); - await userEvent.click(submitButton); - - expect(mockOnSubmit).toHaveBeenCalledWith({ - label: "Test API Key", - environmentPermissions: [ - { - environmentId: "env1", - permission: "read", - }, - ], - organizationAccess: { - accessControl: { - read: false, - write: false, - }, - }, - }); - }); - - test("submits form with correct data including organization access toggles", async () => { - render(); - - // Fill in label - const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack"); - await userEvent.type(labelInput, "Test API Key"); - - // Toggle the first switch (read) under organizationAccess - const readSwitch = screen.getByTestId("organization-access-accessControl-read"); // first is read, second is write - await userEvent.click(readSwitch); // toggle 'read' to true - - // Submit form - const submitButton = screen.getByRole("button", { - name: "environments.project.api_keys.add_api_key", - }); - await userEvent.click(submitButton); - - expect(mockOnSubmit).toHaveBeenCalledWith({ - label: "Test API Key", - environmentPermissions: [], - organizationAccess: { - accessControl: { - read: true, - write: false, - }, - }, - }); - }); - - test("disables submit button when label is empty and there are not environment permissions", async () => { - render(); - const submitButton = screen.getByRole("button", { - name: "environments.project.api_keys.add_api_key", - }); - const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack"); - - // Initially disabled - expect(submitButton).toBeDisabled(); - - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // After typing, it should be enabled - await userEvent.type(labelInput, "Test"); - expect(submitButton).not.toBeDisabled(); - }); - - test("closes modal and resets form on cancel", async () => { - render(); - - // Type something into the label - const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack") as HTMLInputElement; - await userEvent.type(labelInput, "Test API Key"); - - // Click the cancel button - const cancelButton = screen.getByRole("button", { name: "common.cancel" }); - await userEvent.click(cancelButton); - - // Verify modal is closed and form is reset - expect(mockSetOpen).toHaveBeenCalledWith(false); - expect(labelInput.value).toBe(""); - }); -}); diff --git a/apps/web/modules/organization/settings/api-keys/components/api-key-list.test.tsx b/apps/web/modules/organization/settings/api-keys/components/api-key-list.test.tsx deleted file mode 100644 index c772d3af92..0000000000 --- a/apps/web/modules/organization/settings/api-keys/components/api-key-list.test.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { render } from "@testing-library/react"; -import { describe, expect, test, vi } from "vitest"; -import { TProject } from "@formbricks/types/project"; -import { getApiKeysWithEnvironmentPermissions } from "../lib/api-key"; -import { ApiKeyList } from "./api-key-list"; - -// Mock the getApiKeysWithEnvironmentPermissions function -vi.mock("../lib/api-key", () => ({ - getApiKeysWithEnvironmentPermissions: vi.fn(), -})); - -// Mock @/lib/constants -vi.mock("@/lib/constants", () => ({ - INTERCOM_SECRET_KEY: "test-secret-key", - IS_INTERCOM_CONFIGURED: true, - INTERCOM_APP_ID: "test-app-id", - ENCRYPTION_KEY: "test-encryption-key", - ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key", - GITHUB_ID: "test-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - SESSION_MAX_AGE: 1000, - AUDIT_LOG_ENABLED: 1, - REDIS_URL: undefined, -})); - -// Mock @/lib/env -vi.mock("@/lib/env", () => ({ - env: { - IS_FORMBRICKS_CLOUD: "0", - }, -})); - -const baseProject = { - id: "project1", - name: "Project 1", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "org1", - styling: { - allowStyleOverwrite: true, - brandColor: { light: "#000000" }, - }, - recontactDays: 0, - inAppSurveyBranding: false, - linkSurveyBranding: false, - config: { - channel: "link" as const, - industry: "saas" as const, - }, - placement: "bottomLeft" as const, - clickOutsideClose: true, - darkOverlay: false, - languages: [], -}; - -const mockProjects: TProject[] = [ - { - ...baseProject, - environments: [ - { - id: "env1", - type: "production", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project1", - appSetupCompleted: true, - }, - { - id: "env2", - type: "development", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project1", - appSetupCompleted: true, - }, - ], - }, -]; - -const mockApiKeys = [ - { - id: "key1", - hashedKey: "hashed1", - label: "Test Key 1", - createdAt: new Date(), - lastUsedAt: null, - organizationId: "org1", - createdBy: "user1", - }, - { - id: "key2", - hashedKey: "hashed2", - label: "Test Key 2", - createdAt: new Date(), - lastUsedAt: null, - organizationId: "org1", - createdBy: "user1", - }, -]; - -describe("ApiKeyList", () => { - test("renders EditAPIKeys with correct props", async () => { - // Mock the getApiKeysWithEnvironmentPermissions function to return our mock data - (getApiKeysWithEnvironmentPermissions as unknown as ReturnType).mockResolvedValue( - mockApiKeys - ); - - const props = { - organizationId: "org1", - locale: "en-US" as const, - isReadOnly: false, - projects: mockProjects, - }; - - const { container } = render(await ApiKeyList(props)); - - // Verify that EditAPIKeys is rendered with the correct props - expect(getApiKeysWithEnvironmentPermissions).toHaveBeenCalledWith("org1"); - expect(container).toBeInTheDocument(); - }); - - test("handles empty api keys", async () => { - // Mock the getApiKeysWithEnvironmentPermissions function to return empty array - (getApiKeysWithEnvironmentPermissions as unknown as ReturnType).mockResolvedValue([]); - - const props = { - organizationId: "org1", - locale: "en-US" as const, - isReadOnly: false, - projects: mockProjects, - }; - - const { container } = render(await ApiKeyList(props)); - - // Verify that EditAPIKeys is rendered even with empty api keys - expect(getApiKeysWithEnvironmentPermissions).toHaveBeenCalledWith("org1"); - expect(container).toBeInTheDocument(); - }); - - test("passes isReadOnly prop correctly", async () => { - (getApiKeysWithEnvironmentPermissions as unknown as ReturnType).mockResolvedValue( - mockApiKeys - ); - - const props = { - organizationId: "org1", - locale: "en-US" as const, - isReadOnly: true, - projects: mockProjects, - }; - - const { container } = render(await ApiKeyList(props)); - - expect(getApiKeysWithEnvironmentPermissions).toHaveBeenCalledWith("org1"); - expect(container).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.test.tsx b/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.test.tsx deleted file mode 100644 index 5e88883096..0000000000 --- a/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.test.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import { ApiKeyPermission } from "@prisma/client"; -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TProject } from "@formbricks/types/project"; -import { createApiKeyAction, deleteApiKeyAction, updateApiKeyAction } from "../actions"; -import { TApiKeyWithEnvironmentPermission } from "../types/api-keys"; -import { EditAPIKeys } from "./edit-api-keys"; - -// Mock the actions -vi.mock("../actions", () => ({ - createApiKeyAction: vi.fn(), - updateApiKeyAction: vi.fn(), - deleteApiKeyAction: vi.fn(), -})); - -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -// Mock the translate hook from @tolgee/react -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, // simply return the key - }), -})); - -// Mock the timeSince function -vi.mock("@/lib/time", () => ({ - timeSince: vi.fn(() => "2 days ago"), -})); - -// Mock the Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open, onOpenChange }: any) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children, ...props }: any) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: any) =>
    {children}
    , - DialogTitle: ({ children, className }: any) => ( -

    - {children} -

    - ), - DialogDescription: ({ children }: any) =>

    {children}

    , - DialogBody: ({ children }: any) =>
    {children}
    , - DialogFooter: ({ children }: any) =>
    {children}
    , -})); - -// Base project setup -const baseProject = {}; - -// Example project data -const mockProjects: TProject[] = [ - { - ...baseProject, - id: "project1", - name: "Project 1", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "org1", - styling: { - allowStyleOverwrite: true, - brandColor: { light: "#000000" }, - }, - config: { - channel: "link" as const, - industry: "saas" as const, - }, - environments: [ - { - id: "env1", - type: "production", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project1", - appSetupCompleted: true, - }, - { - id: "env2", - type: "development", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project1", - appSetupCompleted: true, - }, - ], - } as TProject, -]; - -// Example API keys -const mockApiKeys: TApiKeyWithEnvironmentPermission[] = [ - { - id: "key1", - label: "Test Key 1", - createdAt: new Date(), - organizationAccess: { - accessControl: { - read: true, - write: false, - }, - }, - apiKeyEnvironments: [ - { - environmentId: "env1", - permission: ApiKeyPermission.read, - }, - ], - }, - { - id: "key2", - label: "Test Key 2", - createdAt: new Date(), - organizationAccess: { - accessControl: { - read: true, - write: false, - }, - }, - apiKeyEnvironments: [ - { - environmentId: "env2", - permission: ApiKeyPermission.read, - }, - ], - }, -]; - -describe("EditAPIKeys", () => { - // Reset environment after each test - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const defaultProps = { - organizationId: "org1", - apiKeys: mockApiKeys, - locale: "en-US" as const, - isReadOnly: false, - projects: mockProjects, - }; - - test("renders the API keys list", () => { - render(); - expect(screen.getByText("common.label")).toBeInTheDocument(); - expect(screen.getByText("Test Key 1")).toBeInTheDocument(); - expect(screen.getByText("Test Key 2")).toBeInTheDocument(); - }); - - test("renders empty state when no API keys", () => { - render(); - expect(screen.getByText("environments.project.api_keys.no_api_keys_yet")).toBeInTheDocument(); - }); - - test("shows add API key button when not readonly", () => { - render(); - expect( - screen.getByRole("button", { name: "environments.settings.api_keys.add_api_key" }) - ).toBeInTheDocument(); - }); - - test("hides add API key button when readonly", () => { - render(); - expect( - screen.queryByRole("button", { name: "environments.settings.api_keys.add_api_key" }) - ).not.toBeInTheDocument(); - }); - - test("opens add API key modal when clicking add button", async () => { - render(); - const addButton = screen.getByRole("button", { name: "environments.settings.api_keys.add_api_key" }); - await userEvent.click(addButton); - - // Look for the modal title using the correct test id - const modalTitle = screen.getByTestId("dialog-title"); - expect(modalTitle).toBeInTheDocument(); - expect(modalTitle).toHaveTextContent("environments.project.api_keys.add_api_key"); - }); - - test("handles API key deletion", async () => { - (deleteApiKeyAction as unknown as ReturnType).mockResolvedValue({ data: true }); - - render(); - const deleteButtons = screen.getAllByRole("button", { name: "" }); // Trash icons - - // Click delete button for first API key - await userEvent.click(deleteButtons[0]); - const confirmDeleteButton = screen.getByRole("button", { name: "common.delete" }); - await userEvent.click(confirmDeleteButton); - - expect(deleteApiKeyAction).toHaveBeenCalledWith({ id: "key1" }); - expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_deleted"); - }); - - test("handles API key updation", async () => { - const updatedApiKey: TApiKeyWithEnvironmentPermission = { - id: "key1", - label: "Updated Key", - createdAt: new Date(), - organizationAccess: { - accessControl: { - read: true, - write: false, - }, - }, - apiKeyEnvironments: [ - { - environmentId: "env1", - permission: ApiKeyPermission.read, - }, - ], - }; - (updateApiKeyAction as unknown as ReturnType).mockResolvedValue({ data: updatedApiKey }); - render(); - - // Open view permission modal - const apiKeyRows = screen.getAllByTestId("api-key-row"); - - // click on the first row - await userEvent.click(apiKeyRows[0]); - - const labelInput = screen.getByTestId("api-key-label"); - await userEvent.clear(labelInput); - await userEvent.type(labelInput, "Updated Key"); - - const submitButton = screen.getByRole("button", { name: "common.update" }); - await userEvent.click(submitButton); - - expect(updateApiKeyAction).toHaveBeenCalledWith({ - apiKeyId: "key1", - apiKeyData: { - label: "Updated Key", - }, - }); - - expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_updated"); - }); - - test("handles API key creation", async () => { - const newApiKey: TApiKeyWithEnvironmentPermission = { - id: "key3", - label: "New Key", - createdAt: new Date(), - organizationAccess: { - accessControl: { - read: true, - write: false, - }, - }, - apiKeyEnvironments: [ - { - environmentId: "env2", - permission: ApiKeyPermission.read, - }, - ], - }; - - (createApiKeyAction as unknown as ReturnType).mockResolvedValue({ data: newApiKey }); - - render(); - - // Open add modal - const addButton = screen.getByRole("button", { name: "environments.settings.api_keys.add_api_key" }); - await userEvent.click(addButton); - - // Fill in form - const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack"); - await userEvent.type(labelInput, "New Key"); - - // Optionally toggle the read switch - const readSwitch = screen.getByTestId("organization-access-accessControl-read"); // first is read, second is write - await userEvent.click(readSwitch); // toggle 'read' to true - - // Submit form - const submitButton = screen.getByRole("button", { name: "environments.project.api_keys.add_api_key" }); - await userEvent.click(submitButton); - - expect(createApiKeyAction).toHaveBeenCalledWith({ - organizationId: "org1", - apiKeyData: { - label: "New Key", - environmentPermissions: [], - organizationAccess: { - accessControl: { read: true, write: false }, - }, - }, - }); - - expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_created"); - }); - - test("handles copy to clipboard", async () => { - // Mock the clipboard writeText method - const writeText = vi.fn(); - Object.assign(navigator, { - clipboard: { - writeText, - }, - }); - - // Provide an API key that has an actualKey - const apiKeyWithActual = { - ...mockApiKeys[0], - actualKey: "test-api-key-123", - } as TApiKeyWithEnvironmentPermission & { actualKey: string }; - - render(); - - // Find the copy icon button by testid - const copyButton = screen.getByTestId("copy-button"); - await userEvent.click(copyButton); - - expect(writeText).toHaveBeenCalledWith("test-api-key-123"); - expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_copied_to_clipboard"); - }); - - test("displays 'secret' when no actualKey is provided", () => { - render(); - - // The API keys in mockApiKeys don't have actualKey, so they should display "secret" - expect(screen.getAllByText("environments.project.api_keys.secret")).toHaveLength(2); - }); - - test("stops propagation when clicking copy button", async () => { - const writeText = vi.fn(); - Object.assign(navigator, { - clipboard: { - writeText, - }, - }); - - const apiKeyWithActual = { - ...mockApiKeys[0], - actualKey: "test-api-key-123", - } as TApiKeyWithEnvironmentPermission & { actualKey: string }; - - render(); - - const copyButton = screen.getByTestId("copy-button"); - await userEvent.click(copyButton); - - // View permission modal should not open when clicking copy button - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); - - test("displays created at time for each API key", () => { - render(); - - // Should show "2 days ago" for both API keys (mocked) - expect(screen.getAllByText("2 days ago")).toHaveLength(2); - }); -}); diff --git a/apps/web/modules/organization/settings/api-keys/components/view-permission-modal.test.tsx b/apps/web/modules/organization/settings/api-keys/components/view-permission-modal.test.tsx deleted file mode 100644 index 7d84c5ecda..0000000000 --- a/apps/web/modules/organization/settings/api-keys/components/view-permission-modal.test.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { ApiKeyPermission } from "@prisma/client"; -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TProject } from "@formbricks/types/project"; -import { TApiKeyWithEnvironmentPermission } from "../types/api-keys"; -import { ViewPermissionModal } from "./view-permission-modal"; - -// Mock the translate hook -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -// Base project setup -const baseProject = {}; - -// Example project data -const mockProjects: TProject[] = [ - { - ...baseProject, - id: "project1", - name: "Project 1", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "org1", - styling: { - allowStyleOverwrite: true, - brandColor: { light: "#000000" }, - }, - config: { - channel: "link" as const, - industry: "saas" as const, - }, - environments: [ - { - id: "env1", - type: "production", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project1", - appSetupCompleted: true, - }, - { - id: "env2", - type: "development", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project1", - appSetupCompleted: true, - }, - ], - } as TProject, -]; - -// Example API key with permissions -const mockApiKey: TApiKeyWithEnvironmentPermission = { - id: "key1", - label: "Test Key 1", - createdAt: new Date(), - organizationAccess: { - accessControl: { - read: true, - write: false, - }, - }, - apiKeyEnvironments: [ - { - environmentId: "env1", - permission: ApiKeyPermission.read, - }, - { - environmentId: "env2", - permission: ApiKeyPermission.write, - }, - ], -}; - -// API key with additional organization access -const mockApiKeyWithOrgAccess = { - ...mockApiKey, - organizationAccess: { - accessControl: { read: true, write: false }, - otherAccess: { read: false, write: true }, - }, -}; - -// API key with no environment permissions -const apiKeyWithoutPermissions = { - ...mockApiKey, - apiKeyEnvironments: [], -}; - -describe("ViewPermissionModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const defaultProps = { - open: true, - setOpen: vi.fn(), - projects: mockProjects, - apiKey: mockApiKey, - onSubmit: vi.fn(), - isUpdating: false, - }; - - test("renders the modal with correct title", () => { - render(); - // Check the localized text for the modal's title - expect(screen.getByText(mockApiKey.label)).toBeInTheDocument(); - }); - - test("renders all permissions for the API key", () => { - render(); - // The same key has two environment permissions - const projectNames = screen.getAllByText("Project 1"); - expect(projectNames).toHaveLength(2); // once for each permission - expect(screen.getByText("production")).toBeInTheDocument(); - expect(screen.getByText("development")).toBeInTheDocument(); - expect(screen.getByText("read")).toBeInTheDocument(); - expect(screen.getByText("write")).toBeInTheDocument(); - }); - - test("displays correct project and environment names", () => { - render(); - // Check for 'Project 1', 'production', 'development' - const projectNames = screen.getAllByText("Project 1"); - expect(projectNames).toHaveLength(2); - expect(screen.getByText("production")).toBeInTheDocument(); - expect(screen.getByText("development")).toBeInTheDocument(); - }); - - test("displays correct permission levels", () => { - render(); - // Check if permission levels 'read' and 'write' appear - expect(screen.getByText("read")).toBeInTheDocument(); - expect(screen.getByText("write")).toBeInTheDocument(); - }); - - test("handles API key with no permissions", () => { - render(); - // Ensure environment/permission section is empty - expect(screen.queryByText("Project 1")).not.toBeInTheDocument(); - expect(screen.queryByText("production")).not.toBeInTheDocument(); - expect(screen.queryByText("development")).not.toBeInTheDocument(); - }); - - test("displays organizationAccess toggles", () => { - render(); - - expect(screen.getByTestId("organization-access-accessControl-read")).toBeChecked(); - expect(screen.getByTestId("organization-access-accessControl-read")).toBeDisabled(); - expect(screen.getByTestId("organization-access-accessControl-write")).not.toBeChecked(); - expect(screen.getByTestId("organization-access-accessControl-write")).toBeDisabled(); - expect(screen.getByTestId("organization-access-otherAccess-read")).toBeChecked(); - expect(screen.getByTestId("organization-access-otherAccess-write")).toBeChecked(); - }); -}); diff --git a/apps/web/modules/organization/settings/api-keys/loading.test.tsx b/apps/web/modules/organization/settings/api-keys/loading.test.tsx deleted file mode 100644 index 412284318d..0000000000 --- a/apps/web/modules/organization/settings/api-keys/loading.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import Loading from "./loading"; - -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar", - () => ({ - OrganizationSettingsNavbar: () =>
    OrgNavbar
    , - }) -); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }) => ( -
    - {pageTitle} - {children} -
    - ), -})); -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ t: (k) => k }), -})); - -describe("Loading (API Keys)", () => { - afterEach(() => { - cleanup(); - }); - - test("renders loading skeletons and tolgee strings", () => { - render(); - expect(screen.getByTestId("content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("org-navbar")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument(); - expect(screen.getAllByText("common.loading").length).toBeGreaterThan(0); - expect(screen.getByText("environments.project.api_keys.api_key")).toBeInTheDocument(); - expect(screen.getByText("common.label")).toBeInTheDocument(); - expect(screen.getByText("common.created_at")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/organization/settings/api-keys/page.test.tsx b/apps/web/modules/organization/settings/api-keys/page.test.tsx deleted file mode 100644 index 5d7d963e4f..0000000000 --- a/apps/web/modules/organization/settings/api-keys/page.test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { findMatchingLocale } from "@/lib/utils/locale"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { getProjectsByOrganizationId } from "@/modules/organization/settings/api-keys/lib/projects"; -import { TOrganizationProject } from "@/modules/organization/settings/api-keys/types/api-keys"; -import { APIKeysPage } from "./page"; - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }) => ( -
    - {pageTitle} - {children} -
    - ), -})); -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar", - () => ({ - OrganizationSettingsNavbar: () =>
    OrgNavbar
    , - }) -); -vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({ - SettingsCard: ({ title, description, children }) => ( -
    - {title} - {description} - {children} -
    - ), -})); -vi.mock("@/modules/organization/settings/api-keys/lib/projects", () => ({ - getProjectsByOrganizationId: vi.fn(), -})); -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); -vi.mock("@/lib/utils/locale", () => ({ - findMatchingLocale: vi.fn(), -})); -vi.mock("./components/api-key-list", () => ({ - ApiKeyList: ({ organizationId, locale, isReadOnly, projects }) => ( -
    - {organizationId}-{locale}-{isReadOnly ? "readonly" : "editable"}-{projects.length} -
    - ), -})); -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: true, -})); - -// Mock the server-side translation function -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -const mockParams = { environmentId: "env-1" }; -const mockLocale = "en-US"; -const mockOrg = { id: "org-1" }; -const mockMembership = { role: "owner" }; -const mockProjects: TOrganizationProject[] = [ - { id: "p1", environments: [], name: "project1" }, - { id: "p2", environments: [], name: "project2" }, -]; - -describe("APIKeysPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all main components and passes props", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - currentUserMembership: mockMembership, - organization: mockOrg, - isOwner: true, - } as any); - vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale); - vi.mocked(getProjectsByOrganizationId).mockResolvedValue(mockProjects); - - const props = { params: Promise.resolve(mockParams) }; - render(await APIKeysPage(props)); - expect(screen.getByTestId("content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("org-navbar")).toBeInTheDocument(); - expect(screen.getByTestId("settings-card")).toBeInTheDocument(); - expect(screen.getByTestId("api-key-list")).toHaveTextContent("org-1-en-US-editable-2"); - expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument(); - expect(screen.getByText("common.api_keys")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.api_keys.api_keys_description")).toBeInTheDocument(); - }); - - test("throws error if not owner", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - currentUserMembership: { role: "member" }, - organization: mockOrg, - } as any); - const props = { params: Promise.resolve(mockParams) }; - await expect(APIKeysPage(props)).rejects.toThrow("common.not_authorized"); - }); -}); diff --git a/apps/web/modules/organization/settings/teams/components/edit-memberships/edit-memberships.test.tsx b/apps/web/modules/organization/settings/teams/components/edit-memberships/edit-memberships.test.tsx deleted file mode 100644 index 14a74cf933..0000000000 --- a/apps/web/modules/organization/settings/teams/components/edit-memberships/edit-memberships.test.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { EditMemberships } from "./edit-memberships"; - -vi.mock("@/modules/organization/settings/teams/components/edit-memberships/members-info", () => ({ - MembersInfo: (props: any) =>
    , -})); - -vi.mock("@/modules/organization/settings/teams/lib/invite", () => ({ - getInvitesByOrganizationId: vi.fn(async () => [ - { - id: "invite-1", - email: "invite@example.com", - name: "Invitee", - role: "member", - expiresAt: new Date(), - createdAt: new Date(), - }, - ]), -})); - -vi.mock("@/modules/organization/settings/teams/lib/membership", () => ({ - getMembershipByOrganizationId: vi.fn(async () => [ - { - userId: "user-1", - name: "User One", - email: "user1@example.com", - role: "owner", - accepted: true, - isActive: true, - }, - ]), -})); - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: 0, -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -const mockOrg: TOrganization = { - id: "org-1", - name: "Test Org", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - plan: "free", - period: "monthly", - periodStart: new Date(), - stripeCustomerId: null, - limits: { monthly: { responses: 100, miu: 100 }, projects: 1 }, - }, - isAIEnabled: false, -}; - -describe("EditMemberships", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all table headers and MembersInfo when role is present", async () => { - const ui = await EditMemberships({ - organization: mockOrg, - currentUserId: "user-1", - role: "owner", - isAccessControlAllowed: true, - isUserManagementDisabledFromUi: false, - }); - render(ui); - expect(screen.getByText("common.full_name")).toBeInTheDocument(); - expect(screen.getByText("common.email")).toBeInTheDocument(); - expect(screen.getByText("common.role")).toBeInTheDocument(); - expect(screen.getByText("common.status")).toBeInTheDocument(); - expect(screen.getByText("common.actions")).toBeInTheDocument(); - expect(screen.getByTestId("members-info")).toBeInTheDocument(); - const props = JSON.parse(screen.getByTestId("members-info").getAttribute("data-props")!); - expect(props.organization.id).toBe("org-1"); - expect(props.currentUserId).toBe("user-1"); - expect(props.currentUserRole).toBe("owner"); - expect(props.isAccessControlAllowed).toBe(true); - expect(props.isUserManagementDisabledFromUi).toBe(false); - expect(Array.isArray(props.invites)).toBe(true); - expect(Array.isArray(props.members)).toBe(true); - }); - - test("does not render role/actions columns if isAccessControlAllowed or isUserManagementDisabledFromUi is false", async () => { - const ui = await EditMemberships({ - organization: mockOrg, - currentUserId: "user-1", - role: "member", - isAccessControlAllowed: false, - isUserManagementDisabledFromUi: true, - }); - render(ui); - expect(screen.getByText("common.full_name")).toBeInTheDocument(); - expect(screen.getByText("common.email")).toBeInTheDocument(); - expect(screen.queryByText("common.role")).not.toBeInTheDocument(); - expect(screen.getByText("common.status")).toBeInTheDocument(); - expect(screen.queryByText("common.actions")).not.toBeInTheDocument(); - expect(screen.getByTestId("members-info")).toBeInTheDocument(); - }); - - test("does not render MembersInfo if role is falsy", async () => { - const ui = await EditMemberships({ - organization: mockOrg, - currentUserId: "user-1", - role: undefined as any, - isAccessControlAllowed: true, - isUserManagementDisabledFromUi: false, - }); - render(ui); - expect(screen.queryByTestId("members-info")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/organization/settings/teams/components/edit-memberships/members-info.test.tsx b/apps/web/modules/organization/settings/teams/components/edit-memberships/members-info.test.tsx deleted file mode 100644 index 4953da1733..0000000000 --- a/apps/web/modules/organization/settings/teams/components/edit-memberships/members-info.test.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TMember } from "@formbricks/types/memberships"; -import { TOrganization } from "@formbricks/types/organizations"; -import { getAccessFlags } from "@/lib/membership/utils"; -import { isInviteExpired } from "@/modules/organization/settings/teams/lib/utils"; -import { TInvite } from "@/modules/organization/settings/teams/types/invites"; -import { MembersInfo } from "./members-info"; - -vi.mock("@/modules/ee/role-management/components/edit-membership-role", () => ({ - EditMembershipRole: (props: any) => ( -
    - ), -})); - -vi.mock("@/modules/organization/settings/teams/components/edit-memberships/member-actions", () => ({ - MemberActions: (props: any) =>
    , -})); - -vi.mock("@/modules/ui/components/badge", () => ({ - Badge: (props: any) =>
    {props.text}
    , -})); -vi.mock("@/modules/ui/components/tooltip", () => ({ - TooltipRenderer: (props: any) =>
    {props.children}
    , -})); -vi.mock("@/modules/organization/settings/teams/lib/utils", () => ({ - isInviteExpired: vi.fn(() => false), -})); -vi.mock("@/lib/membership/utils", () => ({ - getAccessFlags: vi.fn(() => ({ isOwner: false, isManager: false })), -})); -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ t: (key: string) => key }), -})); - -const org: TOrganization = { - id: "org-1", - name: "Test Org", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - plan: "free", - period: "monthly", - periodStart: new Date(), - stripeCustomerId: null, - limits: { monthly: { responses: 100, miu: 100 }, projects: 1 }, - }, - isAIEnabled: false, -}; -const member: TMember = { - userId: "user-1", - name: "User One", - email: "user1@example.com", - role: "owner", - accepted: true, - isActive: true, -}; -const inactiveMember: TMember = { - ...member, - isActive: false, - role: "member", - userId: "user-2", - email: "user2@example.com", -}; -const invite: TInvite = { - id: "invite-1", - email: "invite@example.com", - name: "Invitee", - role: "member", - expiresAt: new Date(), - createdAt: new Date(), -}; - -describe("MembersInfo", () => { - afterEach(() => { - cleanup(); - }); - - test("renders member info and EditMembershipRole when isAccessControlAllowed", () => { - render( - - ); - expect(screen.getByText("User One")).toBeInTheDocument(); - expect(screen.getByText("user1@example.com")).toBeInTheDocument(); - expect(screen.getByTestId("edit-membership-role")).toBeInTheDocument(); - expect(screen.getByTestId("badge")).toHaveTextContent("Active"); - expect(screen.getByTestId("member-actions")).toBeInTheDocument(); - }); - - test("renders badge as Inactive for inactive member", () => { - render( - - ); - expect(screen.getByTestId("badge")).toHaveTextContent("Inactive"); - }); - - test("renders invite as Pending with tooltip if not expired", () => { - render( - - ); - expect(screen.getByTestId("tooltip")).toBeInTheDocument(); - expect(screen.getByTestId("badge")).toHaveTextContent("Pending"); - }); - - test("renders invite as Expired if isInviteExpired returns true", () => { - vi.mocked(isInviteExpired).mockReturnValueOnce(true); - render( - - ); - expect(screen.getByTestId("expired-badge")).toHaveTextContent("Expired"); - }); - - test("does not render EditMembershipRole if isAccessControlAllowed is false", () => { - render( - - ); - expect(screen.queryByTestId("edit-membership-role")).not.toBeInTheDocument(); - }); - - test("does not render MemberActions if isUserManagementDisabledFromUi is true", () => { - render( - - ); - expect(screen.queryByTestId("member-actions")).not.toBeInTheDocument(); - }); - - test("showDeleteButton returns correct values for different roles and invite/member types", () => { - vi.mocked(getAccessFlags).mockReturnValueOnce({ - isOwner: true, - isManager: false, - isBilling: false, - isMember: false, - }); - render( - - ); - expect(screen.getByTestId("member-actions")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/organization/settings/teams/components/edit-memberships/organization-actions.test.tsx b/apps/web/modules/organization/settings/teams/components/edit-memberships/organization-actions.test.tsx deleted file mode 100644 index 359149332e..0000000000 --- a/apps/web/modules/organization/settings/teams/components/edit-memberships/organization-actions.test.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"; -import { useRouter } from "next/navigation"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage"; -import { inviteUserAction, leaveOrganizationAction } from "@/modules/organization/settings/teams/actions"; -import { InviteMemberModal } from "@/modules/organization/settings/teams/components/invite-member/invite-member-modal"; -import { OrganizationActions } from "./organization-actions"; - -// Mock the next/navigation module -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), -})); - -// Mock the actions -vi.mock("@/modules/organization/settings/teams/actions", () => ({ - inviteUserAction: vi.fn(), - leaveOrganizationAction: vi.fn(), -})); - -// Mock the InviteMemberModal -vi.mock("@/modules/organization/settings/teams/components/invite-member/invite-member-modal", () => ({ - InviteMemberModal: vi.fn(({ open, setOpen, onSubmit }) => { - if (!open) return null; - return ( -
    - - -
    - ); - }), -})); - -// Mock the Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: vi.fn(({ children, open, onOpenChange }) => { - if (!open) return null; - return ( -
    onOpenChange && onOpenChange(false)}> - {children} -
    - ); - }), - DialogContent: vi.fn(({ children }) =>
    {children}
    ), - DialogHeader: vi.fn(({ children }) =>
    {children}
    ), - DialogTitle: vi.fn(({ children }) =>
    {children}
    ), - DialogDescription: vi.fn(({ children }) =>
    {children}
    ), - DialogFooter: vi.fn(({ children }) =>
    {children}
    ), -})); - -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -// Mock localStorage -const localStorageMock = (() => { - let store = {}; - return { - getItem: vi.fn((key) => store[key] || null), - setItem: vi.fn((key, value) => { - store[key] = value.toString(); - }), - removeItem: vi.fn((key) => { - delete store[key]; - }), - clear: vi.fn(() => { - store = {}; - }), - }; -})(); -Object.defineProperty(window, "localStorage", { value: localStorageMock }); - -// Mock tolgee -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key) => key, - }), -})); - -// Mock Button component -vi.mock("@/modules/ui/components/button", () => ({ - Button: vi.fn(({ children, onClick, loading, disabled, ...props }) => ( - - )), -})); - -describe("OrganizationActions Component", () => { - const mockRouter = { - push: vi.fn(), - refresh: vi.fn(), - }; - - const defaultProps = { - role: "member" as const, - membershipRole: "member" as const, - isLeaveOrganizationDisabled: false, - organization: { id: "org-123", name: "Test Org" } as TOrganization, - teams: [{ id: "team-1", name: "Team 1" }], - isInviteDisabled: false, - isAccessControlAllowed: true, - isFormbricksCloud: false, - environmentId: "env-123", - isMultiOrgEnabled: true, - isUserManagementDisabledFromUi: false, - isStorageConfigured: true, - }; - - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(useRouter).mockReturnValue(mockRouter as unknown as AppRouterInstance); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders without crashing", () => { - render(); - expect(screen.getByText("environments.settings.general.leave_organization")).toBeInTheDocument(); - }); - - test("does not show leave organization button when role is owner", () => { - render(); - expect(screen.queryByText("environments.settings.general.leave_organization")).not.toBeInTheDocument(); - }); - - test("does not show leave organization button when multi-org is disabled", () => { - render(); - expect(screen.queryByText("environments.settings.general.leave_organization")).not.toBeInTheDocument(); - }); - - test("does not show invite button when isInviteDisabled is true", () => { - render(); - expect(screen.queryByText("environments.settings.teams.invite_member")).not.toBeInTheDocument(); - }); - - test("does not show invite button when user is not owner or manager", () => { - render(); - expect(screen.queryByText("environments.settings.teams.invite_member")).not.toBeInTheDocument(); - }); - - test("shows invite button when user is owner", () => { - render(); - expect(screen.getByText("environments.settings.teams.invite_member")).toBeInTheDocument(); - }); - - test("shows invite button when user is manager", () => { - render(); - expect(screen.getByText("environments.settings.teams.invite_member")).toBeInTheDocument(); - }); - - test("opens invite member modal when clicking the invite button", () => { - render(); - fireEvent.click(screen.getByText("environments.settings.teams.invite_member")); - expect(screen.getByTestId("invite-member-modal")).toBeInTheDocument(); - }); - - test("opens leave organization modal when clicking the leave button", () => { - render(); - fireEvent.click(screen.getByText("environments.settings.general.leave_organization")); - expect(screen.getByTestId("leave-org-modal")).toBeInTheDocument(); - }); - - test("handles successful member invite", async () => { - vi.mocked(inviteUserAction).mockResolvedValue({ data: "invite-123" }); - - render(); - fireEvent.click(screen.getByText("environments.settings.teams.invite_member")); - fireEvent.click(screen.getByTestId("invite-submit-btn")); - - await waitFor(() => { - expect(inviteUserAction).toHaveBeenCalledWith({ - organizationId: "org-123", - email: "test@example.com", - name: "Test User", - role: "admin", - teamIds: [], - }); - expect(toast.success).toHaveBeenCalledWith("environments.settings.general.member_invited_successfully"); - }); - }); - - test("handles failed member invite", async () => { - vi.mocked(inviteUserAction).mockResolvedValue({ serverError: "Failed to invite user" }); - - render(); - fireEvent.click(screen.getByText("environments.settings.teams.invite_member")); - fireEvent.click(screen.getByTestId("invite-submit-btn")); - - await waitFor(() => { - expect(inviteUserAction).toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalled(); - }); - }); - - test("handles leave organization successfully", async () => { - vi.mocked(leaveOrganizationAction).mockResolvedValue({ - data: [ - { - userId: "123", - role: "admin", - teamId: "team-1", - }, - ], - }); - - render(); - fireEvent.click(screen.getByText("environments.settings.general.leave_organization")); - fireEvent.click(screen.getByTestId("leave-org-confirm-btn")); - - await waitFor(() => { - expect(leaveOrganizationAction).toHaveBeenCalledWith({ organizationId: "org-123" }); - expect(toast.success).toHaveBeenCalledWith("environments.settings.general.member_deleted_successfully"); - expect(localStorage.removeItem).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS); - expect(mockRouter.push).toHaveBeenCalledWith("/"); - expect(mockRouter.refresh).toHaveBeenCalled(); - }); - }); - - test("handles leave organization error", async () => { - const mockError = new Error("Failed to leave organization"); - vi.mocked(leaveOrganizationAction).mockRejectedValue(mockError); - - render(); - fireEvent.click(screen.getByText("environments.settings.general.leave_organization")); - fireEvent.click(screen.getByTestId("leave-org-confirm-btn")); - - await waitFor(() => { - expect(leaveOrganizationAction).toHaveBeenCalledWith({ organizationId: "org-123" }); - expect(toast.error).toHaveBeenCalledWith("Error: Failed to leave organization"); - }); - }); - - test("cannot leave organization when only one organization is present", () => { - render(); - expect(screen.queryByText("environments.settings.general.leave_organization")).not.toBeInTheDocument(); - }); - - test("invite member modal closes on close button click", () => { - render(); - fireEvent.click(screen.getByText("environments.settings.teams.invite_member")); - expect(screen.getByTestId("invite-member-modal")).toBeInTheDocument(); - fireEvent.click(screen.getByTestId("invite-close-btn")); - expect(screen.queryByTestId("invite-member-modal")).not.toBeInTheDocument(); - }); - - test("leave organization modal closes on cancel", () => { - render(); - fireEvent.click(screen.getByText("environments.settings.general.leave_organization")); - expect(screen.getByTestId("leave-org-modal")).toBeInTheDocument(); - fireEvent.click(screen.getByTestId("leave-org-cancel-btn")); - expect(screen.queryByTestId("leave-org-modal")).not.toBeInTheDocument(); - }); - - test("leave organization button is disabled and warning shown when isLeaveOrganizationDisabled is true", () => { - render(); - fireEvent.click(screen.getByText("environments.settings.general.leave_organization")); - expect(screen.getByTestId("leave-org-modal")).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.general.cannot_leave_only_organization") - ).toBeInTheDocument(); - }); - - test("invite button is hidden when isUserManagementDisabledFromUi is true", () => { - render( - - ); - expect(screen.queryByText("environments.settings.teams.invite_member")).not.toBeInTheDocument(); - }); - - test("invite button is hidden when membershipRole is undefined", () => { - render(); - expect(screen.queryByText("environments.settings.teams.invite_member")).not.toBeInTheDocument(); - }); - - test("invite member modal receives correct props", () => { - render(); - fireEvent.click(screen.getByText("environments.settings.teams.invite_member")); - const modal = screen.getByTestId("invite-member-modal"); - expect(modal).toBeInTheDocument(); - - const calls = vi.mocked(InviteMemberModal).mock.calls; - expect( - calls.some((call) => - expect - .objectContaining({ - environmentId: "env-123", - isAccessControlAllowed: true, - isFormbricksCloud: false, - teams: expect.arrayContaining(defaultProps.teams), - membershipRole: "owner", - open: true, - setOpen: expect.any(Function), - onSubmit: expect.any(Function), - }) - .asymmetricMatch(call[0]) - ) - ).toBe(true); - }); -}); diff --git a/apps/web/modules/organization/settings/teams/components/invite-member/bulk-invite-tab.test.tsx b/apps/web/modules/organization/settings/teams/components/invite-member/bulk-invite-tab.test.tsx deleted file mode 100644 index 5fbeae1e30..0000000000 --- a/apps/web/modules/organization/settings/teams/components/invite-member/bulk-invite-tab.test.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor, within } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { BulkInviteTab } from "./bulk-invite-tab"; - -// Hoisted fns for mocks to avoid hoisting pitfalls -const h = vi.hoisted(() => ({ - mockParse: vi.fn(), - mockToastError: vi.fn(), -})); - -// Mocks -vi.mock("papaparse", () => ({ - default: { parse: h.mockParse }, -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ t: (k: string) => k }), -})); - -vi.mock("react-hot-toast", () => ({ - default: { error: h.mockToastError }, -})); - -vi.mock("@/modules/organization/settings/teams/types/invites", () => ({ - ZInvitees: { parse: vi.fn() }, -})); - -let lastUploaderProps: any; -vi.mock("@/modules/ui/components/file-input/components/uploader", () => ({ - Uploader: vi.fn((props: any) => { - lastUploaderProps = props; - return ( -
    - -
    - ); - }), -})); - -describe("BulkInviteTab", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - lastUploaderProps = undefined; - }); - - const baseProps = { - setOpen: vi.fn(), - onSubmit: vi.fn(), - isAccessControlAllowed: true, - isFormbricksCloud: true, - isStorageConfigured: true, - }; - - test("renders Uploader with correct props", () => { - render(); - expect(screen.getByTestId("uploader-mock")).toBeInTheDocument(); - expect(lastUploaderProps).toEqual( - expect.objectContaining({ - allowedFileExtensions: ["csv"], - id: "bulk-invite", - name: "bulk-invite", - multiple: false, - disabled: false, - isStorageConfigured: true, - }) - ); - }); - - test("selecting a CSV shows filename, disables uploader, enables import; removing clears it", async () => { - render(); - const user = userEvent.setup(); - - const file = new File(["name,email,role\nA,a@example.com,manager"], "people.csv", { - type: "text/csv", - }); - - // Simulate upload via mocked Uploader - lastUploaderProps.handleUpload([file]); - // Filename visible - expect(await screen.findByText("people.csv")).toBeInTheDocument(); - - // Uploader should be disabled after selection (component re-renders) - await waitFor(() => expect(lastUploaderProps.disabled).toBe(true)); - - // Import button enabled - const importButton = screen.getByRole("button", { name: /common.import/i }); - expect(importButton).toBeEnabled(); - - // Remove file (icon-only button near filename) - const nameEl = screen.getByText("people.csv"); - const container = nameEl.closest("div") as HTMLElement; - const removeBtn = within(container).getByRole("button"); - await user.click(removeBtn); - expect(screen.queryByText("people.csv")).not.toBeInTheDocument(); - }); - - test("onImport parses CSV and calls onSubmit (access control allowed)", async () => { - render(); - - const file = new File(["dummy"], "people.csv", { type: "text/csv" }); - lastUploaderProps.handleUpload([file]); - - // Mock Papa.parse to synchronously call complete with parsed rows - h.mockParse.mockImplementation((_file: File, opts: any) => { - opts.complete({ - data: [ - { name: " Alice ", email: " alice@example.com ", role: " Manager " }, - { name: "Bob", email: "bob@example.com", role: "member" }, - ], - }); - }); - - const importButton = screen.getByRole("button", { name: /common.import/i }); - await userEvent.click(importButton); - - expect(baseProps.onSubmit).toHaveBeenCalledWith([ - { name: "Alice", email: "alice@example.com", role: "manager", teamIds: [] }, - { name: "Bob", email: "bob@example.com", role: "member", teamIds: [] }, - ]); - expect(baseProps.setOpen).toHaveBeenCalledWith(false); - }); - - test("onImport forces owner when access control not allowed", async () => { - const props = { ...baseProps, isAccessControlAllowed: false }; - render(); - - const file = new File(["dummy"], "people.csv", { type: "text/csv" }); - lastUploaderProps.handleUpload([file]); - - h.mockParse.mockImplementation((_file: File, opts: any) => { - opts.complete({ - data: [{ name: "Carol", email: "carol@example.com", role: "admin" }], - }); - }); - - const importButton = screen.getByRole("button", { name: /common.import/i }); - await userEvent.click(importButton); - - expect(props.onSubmit).toHaveBeenCalledWith([ - { name: "Carol", email: "carol@example.com", role: "owner", teamIds: [] }, - ]); - }); - - test("onImport maps billing to owner when not Formbricks Cloud", async () => { - const props = { ...baseProps, isFormbricksCloud: false }; - render(); - - const file = new File(["dummy"], "people.csv", { type: "text/csv" }); - lastUploaderProps.handleUpload([file]); - - h.mockParse.mockImplementation((_file: File, opts: any) => { - opts.complete({ - data: [{ name: "Dave", email: "dave@example.com", role: "billing" }], - }); - }); - - const importButton = screen.getByRole("button", { name: /common.import/i }); - await userEvent.click(importButton); - - expect(props.onSubmit).toHaveBeenCalledWith([ - { name: "Dave", email: "dave@example.com", role: "owner", teamIds: [] }, - ]); - }); - - test("invalid drop file type shows toast error", async () => { - render(); - - // Call handleDrop with a non-csv file - const evt = { - preventDefault: vi.fn(), - stopPropagation: vi.fn(), - dataTransfer: { files: [new File(["x"], "image.png", { type: "image/png" })] }, - } as any; - - await lastUploaderProps.handleDrop(evt); - expect(h.mockToastError).toHaveBeenCalled(); - }); - - test("remove file button clears selection", async () => { - render(); - const user = userEvent.setup(); - - const file = new File(["x"], "people.csv", { type: "text/csv" }); - lastUploaderProps.handleUpload([file]); - - // Locate the container that shows filename and its button - const nameEl = await screen.findByText("people.csv"); - const container = nameEl.closest("div") as HTMLElement; - const removeButton = within(container).getByRole("button"); - await user.click(removeButton); - - expect(screen.queryByText("people.csv")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/organization/settings/teams/components/invite-member/individual-invite-tab.test.tsx b/apps/web/modules/organization/settings/teams/components/invite-member/individual-invite-tab.test.tsx deleted file mode 100644 index 7d255ffdfd..0000000000 --- a/apps/web/modules/organization/settings/teams/components/invite-member/individual-invite-tab.test.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TOrganizationRole } from "@formbricks/types/memberships"; -import { IndividualInviteTab } from "./individual-invite-tab"; - -const t = (k: string) => k; -vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t }) })); - -vi.mock("@/modules/ee/role-management/components/add-member-role", () => ({ - AddMemberRole: () =>
    AddMemberRole
    , -})); - -vi.mock("@/modules/ui/components/multi-select", () => ({ - MultiSelect: ({ value, options, onChange, disabled }: any) => ( - - ), -})); - -const defaultProps = { - setOpen: vi.fn(), - onSubmit: vi.fn(), - teams: [ - { id: "team-1", name: "Team 1" }, - { id: "team-2", name: "Team 2" }, - ], - isAccessControlAllowed: true, - isFormbricksCloud: true, - environmentId: "env-1", - membershipRole: "owner" as TOrganizationRole, -}; - -describe("IndividualInviteTab", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders form fields and buttons", () => { - render(); - expect(screen.getByLabelText("common.full_name")).toBeInTheDocument(); - expect(screen.getByLabelText("common.email")).toBeInTheDocument(); - expect(screen.getByTestId("add-member-role")).toBeInTheDocument(); - expect(screen.getByText("common.cancel")).toBeInTheDocument(); - expect(screen.getByText("common.invite")).toBeInTheDocument(); - }); - - test("submits valid form and calls onSubmit", async () => { - render(); - await userEvent.type(screen.getByLabelText("common.full_name"), "Test User"); - await userEvent.type(screen.getByLabelText("common.email"), "test@example.com"); - fireEvent.submit(screen.getByRole("button", { name: "common.invite" }).closest("form")!); - await waitFor(() => - expect(defaultProps.onSubmit).toHaveBeenCalledWith([ - expect.objectContaining({ name: "Test User", email: "test@example.com", role: "member" }), - ]) - ); - expect(defaultProps.setOpen).toHaveBeenCalledWith(false); - }); - - test("shows error for empty name", async () => { - render(); - await userEvent.type(screen.getByLabelText("common.email"), "test@example.com"); - fireEvent.submit(screen.getByRole("button", { name: "common.invite" }).closest("form")!); - expect(await screen.findByText("Name should be at least 1 character long")).toBeInTheDocument(); - }); - - test("shows error for invalid email", async () => { - render(); - await userEvent.type(screen.getByLabelText("common.full_name"), "Test User"); - await userEvent.type(screen.getByLabelText("common.email"), "not-an-email"); - fireEvent.submit(screen.getByRole("button", { name: "common.invite" }).closest("form")!); - expect(await screen.findByText(/Invalid email/)).toBeInTheDocument(); - }); - - test("shows member role info alert when role is member", async () => { - render(); - await userEvent.type(screen.getByLabelText("common.full_name"), "Test User"); - await userEvent.type(screen.getByLabelText("common.email"), "test@example.com"); - // Simulate selecting member role - // Not needed as default is member if isAccessControlAllowed is true - expect(screen.getByText("environments.settings.teams.member_role_info_message")).toBeInTheDocument(); - }); - - test("shows team select when isAccessControlAllowed is true", () => { - render(); - expect(screen.getByTestId("multi-select")).toBeInTheDocument(); - }); - - test("shows upgrade alert when isAccessControlAllowed is false", () => { - render(); - expect(screen.getByText("environments.settings.teams.upgrade_plan_notice_message")).toBeInTheDocument(); - expect(screen.getByText("common.start_free_trial")).toBeInTheDocument(); - }); - - test("shows team select placeholder and message when no teams", () => { - render(); - expect(screen.getByText("environments.settings.teams.create_first_team_message")).toBeInTheDocument(); - }); - - test("cancel button closes modal", async () => { - render(); - userEvent.click(screen.getByText("common.cancel")); - await waitFor(() => expect(defaultProps.setOpen).toHaveBeenCalledWith(false)); - }); -}); diff --git a/apps/web/modules/organization/settings/teams/components/invite-member/invite-member-modal.test.tsx b/apps/web/modules/organization/settings/teams/components/invite-member/invite-member-modal.test.tsx deleted file mode 100644 index ad9dce3d57..0000000000 --- a/apps/web/modules/organization/settings/teams/components/invite-member/invite-member-modal.test.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TOrganizationRole } from "@formbricks/types/memberships"; -import { InviteMemberModal } from "./invite-member-modal"; - -const t = (k: string) => k; -vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t }) })); - -vi.mock("./bulk-invite-tab", () => ({ - BulkInviteTab: () =>
    BulkInviteTab
    , -})); -vi.mock("./individual-invite-tab", () => ({ - IndividualInviteTab: () =>
    IndividualInviteTab
    , -})); - -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) => - open ?
    {children}
    : null, - DialogContent: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogBody: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), -})); - -vi.mock("@/modules/ui/components/tab-toggle", () => ({ - TabToggle: ({ options, onChange, defaultSelected }: any) => ( - - ), -})); - -const defaultProps = { - open: true, - setOpen: vi.fn(), - onSubmit: vi.fn(), - teams: [], - isAccessControlAllowed: true, - isFormbricksCloud: true, - environmentId: "env-1", - membershipRole: "owner" as TOrganizationRole, - isStorageConfigured: true, -}; - -describe("InviteMemberModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders dialog and individual tab by default", () => { - render(); - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-content")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-header")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-description")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-body")).toBeInTheDocument(); - expect(screen.getByTestId("individual-invite-tab")).toBeInTheDocument(); - expect(screen.getByTestId("tab-toggle")).toBeInTheDocument(); - }); - - test("renders correct texts", () => { - render(); - expect(screen.getByText("environments.settings.teams.invite_member")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.teams.invite_member_description")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/organization/settings/teams/components/invite-member/share-invite-modal.test.tsx b/apps/web/modules/organization/settings/teams/components/invite-member/share-invite-modal.test.tsx deleted file mode 100644 index f26e58f0d8..0000000000 --- a/apps/web/modules/organization/settings/teams/components/invite-member/share-invite-modal.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ShareInviteModal } from "./share-invite-modal"; - -const t = (k: string) => k; -vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t }) })); - -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) => - open ?
    {children}
    : null, - DialogContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogHeader: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogTitle: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    - {children} -
    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogFooter: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -const defaultProps = { - inviteToken: "test-token", - open: true, - setOpen: vi.fn(), -}; - -describe("ShareInviteModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders dialog and invite link", () => { - render(); - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-content")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-header")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-description")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-body")).toBeInTheDocument(); - - expect( - screen.getByText("environments.settings.general.organization_invite_link_ready") - ).toBeInTheDocument(); - expect( - screen.getByText( - "environments.settings.general.share_this_link_to_let_your_organization_member_join_your_organization" - ) - ).toBeInTheDocument(); - expect(screen.getByText("common.copy_link")).toBeInTheDocument(); - }); - - test("calls setOpen when dialog is closed", () => { - render(); - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/organization/settings/teams/components/members-view.test.tsx b/apps/web/modules/organization/settings/teams/components/members-view.test.tsx deleted file mode 100644 index aac210ba24..0000000000 --- a/apps/web/modules/organization/settings/teams/components/members-view.test.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TOrganizationRole } from "@formbricks/types/memberships"; -import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getTeamsByOrganizationId } from "@/modules/ee/teams/team-list/lib/team"; -import { getMembershipsByUserId } from "@/modules/organization/settings/teams/lib/membership"; -import { MembersLoading, MembersView } from "./members-view"; - -vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({ - SettingsCard: ({ title, description, children }: any) => ( -
    -
    {title}
    -
    {description}
    - {children} -
    - ), -})); - -vi.mock("@/lib/constants", () => ({ - INVITE_DISABLED: false, - IS_FORMBRICKS_CLOUD: true, - IS_STORAGE_CONFIGURED: true, -})); - -vi.mock("@/modules/organization/settings/teams/components/edit-memberships/organization-actions", () => ({ - OrganizationActions: (props: any) =>
    {JSON.stringify(props)}
    , -})); - -vi.mock("@/modules/organization/settings/teams/components/edit-memberships", () => ({ - EditMemberships: (props: any) =>
    {JSON.stringify(props)}
    , -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -vi.mock("@/modules/organization/settings/teams/lib/membership", () => ({ - getMembershipsByUserId: vi.fn(), -})); - -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getIsMultiOrgEnabled: vi.fn(), -})); - -vi.mock("@/modules/ee/teams/team-list/lib/team", () => ({ - getTeamsByOrganizationId: vi.fn(), -})); - -describe("MembersView", () => { - afterEach(() => { - cleanup(); - }); - - const baseProps = { - membershipRole: "owner", - organization: { id: "org-1", name: "Test Org" }, - currentUserId: "user-1", - environmentId: "env-1", - isAccessControlAllowed: true, - isUserManagementDisabledFromUi: false, - } as any; - - const mockMembership = { - organizationId: "org-1", - userId: "user-1", - accepted: true, - role: "owner" as TOrganizationRole, - }; - - test("renders SettingsCard and children with correct props", async () => { - vi.mocked(getMembershipsByUserId).mockResolvedValue([mockMembership]); - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); - vi.mocked(getTeamsByOrganizationId).mockResolvedValue([{ id: "t1", name: "Team 1" }]); - - const ui = await MembersView(baseProps); - render(ui); - expect(screen.getByTestId("SettingsCard")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.general.manage_members")).toBeInTheDocument(); - expect(screen.getByText("environments.settings.general.manage_members_description")).toBeInTheDocument(); - expect(screen.getByTestId("OrganizationActions")).toBeInTheDocument(); - expect(screen.getByTestId("EditMemberships")).toBeInTheDocument(); - }); - - test("disables leave organization if only one membership", async () => { - vi.mocked(getMembershipsByUserId).mockResolvedValue([]); - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); - vi.mocked(getTeamsByOrganizationId).mockResolvedValue([{ id: "t1", name: "Team 1" }]); - - const ui = await MembersView(baseProps); - render(ui); - expect(screen.getByTestId("OrganizationActions").textContent).toContain( - '"isLeaveOrganizationDisabled":true' - ); - }); - - test("does not render OrganizationActions or EditMemberships if no membershipRole", async () => { - vi.mocked(getMembershipsByUserId).mockResolvedValue([mockMembership]); - vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); - vi.mocked(getTeamsByOrganizationId).mockResolvedValue([{ id: "t1", name: "Team 1" }]); - const ui = await MembersView({ ...baseProps, membershipRole: undefined }); - render(ui); - expect(screen.queryByTestId("OrganizationActions")).toBeNull(); - expect(screen.queryByTestId("EditMemberships")).toBeNull(); - }); -}); - -describe("MembersLoading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders two skeleton loaders", () => { - const { container } = render(); - const skeletons = container.querySelectorAll(".animate-pulse"); - expect(skeletons.length).toBe(2); - expect(skeletons[0]).toHaveClass("h-8", "w-80", "rounded-full", "bg-slate-200"); - }); -}); diff --git a/apps/web/modules/organization/settings/teams/page.test.tsx b/apps/web/modules/organization/settings/teams/page.test.tsx deleted file mode 100644 index b63fc9e931..0000000000 --- a/apps/web/modules/organization/settings/teams/page.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TeamsPage } from "./page"; - -vi.mock( - "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar", - () => ({ - OrganizationSettingsNavbar: (props) =>
    OrgNavbar-{props.activeId}
    , - }) -); - -vi.mock("@/lib/constants", () => ({ - USER_MANAGEMENT_MINIMUM_ROLE: "owner", - IS_FORMBRICKS_CLOUD: 1, - ENCRYPTION_KEY: "test-key", - ENTERPRISE_LICENSE_KEY: "test-enterprise-key", -})); - -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getAccessControlPermission: vi.fn(), -})); - -vi.mock("@/modules/ee/teams/team-list/components/teams-view", () => ({ - TeamsView: (props) =>
    TeamsView-{props.organizationId}
    , -})); - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -vi.mock("@/modules/organization/settings/teams/components/members-view", () => ({ - MembersView: (props) =>
    MembersView-{props.membershipRole}
    , -})); - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }) => ( -
    - {pageTitle} - {children} -
    - ), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -const mockParams = { environmentId: "env-1" }; -const mockOrg = { id: "org-1", billing: { plan: "free" } }; -const mockMembership = { role: "owner" }; -const mockSession = { user: { id: "user-1" } }; - -describe("TeamsPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all main components and passes props", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: mockSession, - currentUserMembership: mockMembership, - organization: mockOrg, - } as any); - vi.mocked(getAccessControlPermission).mockResolvedValue(true); - const props = { params: Promise.resolve(mockParams) }; - render(await TeamsPage(props)); - expect(screen.getByTestId("content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("org-navbar")).toHaveTextContent("OrgNavbar-teams"); - expect(screen.getByTestId("members-view")).toHaveTextContent("MembersView-owner"); - expect(screen.getByTestId("teams-view")).toHaveTextContent("TeamsView-org-1"); - expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument(); - }); - - test("passes correct props to role management util", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - session: mockSession, - currentUserMembership: mockMembership, - organization: mockOrg, - } as any); - vi.mocked(getAccessControlPermission).mockResolvedValue(false); - const props = { params: Promise.resolve(mockParams) }; - render(await TeamsPage(props)); - expect(getAccessControlPermission).toHaveBeenCalledWith("free"); - }); -}); diff --git a/apps/web/modules/projects/components/create-project-modal/index.test.tsx b/apps/web/modules/projects/components/create-project-modal/index.test.tsx deleted file mode 100644 index 96a7d26a9f..0000000000 --- a/apps/web/modules/projects/components/create-project-modal/index.test.tsx +++ /dev/null @@ -1,436 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { toast } from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { getTeamsByOrganizationIdAction } from "@/modules/projects/settings/actions"; -import { CreateProjectModal } from "./index"; - -// Mock dependencies -vi.mock("@/app/(app)/environments/[environmentId]/actions", () => ({ - createProjectAction: vi.fn(), -})); - -vi.mock("@/modules/projects/settings/actions", () => ({ - getTeamsByOrganizationIdAction: vi.fn(), -})); - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(), -})); - -const mockPush = vi.fn(); - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - push: mockPush, - }), -})); - -vi.mock("react-hot-toast", () => ({ - toast: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -// Mock UI components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ open, onOpenChange, children }: any) => - open ? ( -
    onOpenChange(false)}> - {children} -
    - ) : null, - DialogContent: ({ children }: any) =>
    {children}
    , - DialogHeader: ({ children }: any) =>
    {children}
    , - DialogTitle: ({ children }: any) =>

    {children}

    , - DialogDescription: ({ children }: any) =>

    {children}

    , - DialogBody: ({ children }: any) =>
    {children}
    , - DialogFooter: ({ children }: any) =>
    {children}
    , -})); - -// Create a mutable form mock that can be modified per test -let currentFormMock: any; - -const createFormMock = (options: { shouldCallOnSubmit?: boolean; isSubmitting?: boolean } = {}) => ({ - handleSubmit: vi.fn((onSubmit) => (e: any) => { - e.preventDefault(); - if (options.shouldCallOnSubmit !== false) { - onSubmit({ name: "Test Project", teamIds: [] }); - } - }), - formState: { isSubmitting: options.isSubmitting || false }, - reset: vi.fn(), - watch: vi.fn(), - getValues: vi.fn(() => ({ teamIds: [] })), - setValue: vi.fn(), - control: {}, -}); - -vi.mock("react-hook-form", () => ({ - useForm: () => currentFormMock, -})); - -vi.mock("@/modules/ui/components/form", () => ({ - FormProvider: ({ children }: any) =>
    {children}
    , - FormField: ({ render, name }: any) => { - const field = { - value: name === "name" ? "Test Project" : [], - onChange: vi.fn(), - }; - return render({ field, fieldState: {} }); - }, - FormItem: ({ children }: any) =>
    {children}
    , - FormLabel: ({ children }: any) => , - FormControl: ({ children }: any) =>
    {children}
    , - FormError: ({ children }: any) => {children}, -})); - -vi.mock("@/modules/ui/components/input", () => ({ - Input: (props: any) => , -})); - -vi.mock("@/modules/ui/components/multi-select", () => ({ - MultiSelect: ({ value, options, onChange, placeholder }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, type, loading, variant, ...props }: any) => ( - - ), -})); - -describe("CreateProjectModal", () => { - const mockOrganizationTeams = [ - { id: "team-1", name: "Development Team" }, - { id: "team-2", name: "Marketing Team" }, - ]; - - const mockProject = { - id: "project-123", - name: "Test Project", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "org-123", - recontactDays: 7, - inAppSurveyBranding: true, - linkSurveyBranding: true, - config: { channel: "website" as const, industry: "saas" as const }, - placement: "bottomRight" as const, - clickOutsideClose: true, - darkOverlay: false, - brandColor: "#000000", - highlightBorderColor: "#000000", - styling: { allowStyleOverwrite: true }, - logo: null, - languages: [], - environments: [ - { - id: "env-123", - type: "production" as const, - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project-123", - appSetupCompleted: false, - }, - ], - }; - - const defaultProps = { - open: true, - setOpen: vi.fn(), - organizationId: "org-123", - isAccessControlAllowed: true, - }; - - beforeEach(() => { - // Reset all mocks - vi.clearAllMocks(); - - // Reset form mock to default - currentFormMock = createFormMock(); - - // Mock successful teams fetch - vi.mocked(getTeamsByOrganizationIdAction).mockResolvedValue({ - data: mockOrganizationTeams, - } as any); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders modal when open is true", async () => { - render(); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("common.create_project"); - expect(screen.getByTestId("dialog-description")).toHaveTextContent("common.project_creation_description"); - expect(screen.getByTestId("input")).toBeInTheDocument(); - }); - - test("does not render when open is false", () => { - render(); - - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); - - test("fetches organization teams on mount", async () => { - render(); - - await waitFor(() => { - expect(getTeamsByOrganizationIdAction).toHaveBeenCalledWith({ - organizationId: "org-123", - }); - }); - }); - - test("shows team selection when isAccessControlAllowed is true and teams exist", async () => { - render(); - - await waitFor(() => { - expect(screen.getByTestId("multi-select")).toBeInTheDocument(); - }); - - const multiSelect = screen.getByTestId("multi-select"); - expect(multiSelect).toHaveAttribute("data-placeholder", "common.select_teams"); - - const options = screen.getAllByRole("option"); - expect(options).toHaveLength(2); - expect(options[0]).toHaveTextContent("Development Team"); - expect(options[1]).toHaveTextContent("Marketing Team"); - }); - - test("hides team selection when isAccessControlAllowed is false", async () => { - render(); - - await waitFor(() => { - expect(screen.queryByTestId("multi-select")).not.toBeInTheDocument(); - }); - }); - - test("hides team selection when no teams exist", async () => { - vi.mocked(getTeamsByOrganizationIdAction).mockResolvedValue({ - data: [], - } as any); - - render(); - - await waitFor(() => { - expect(screen.queryByTestId("multi-select")).not.toBeInTheDocument(); - }); - }); - - test("handles teams fetch error", async () => { - const errorMessage = "Failed to fetch teams"; - vi.mocked(getTeamsByOrganizationIdAction).mockResolvedValue({ - serverError: "Failed to fetch teams", - } as any); - vi.mocked(getFormattedErrorMessage).mockReturnValue(errorMessage); - - render(); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith(errorMessage); - }); - }); - - test("submits form with project name only when no teams selected", async () => { - const user = userEvent.setup(); - - vi.mocked(createProjectAction).mockResolvedValue({ data: mockProject } as any); - - render(); - - await waitFor(() => { - expect(screen.getByTestId("input")).toBeInTheDocument(); - }); - - const submitButton = screen.getByTestId("button-submit"); - await user.click(submitButton); - - await waitFor(() => { - expect(createProjectAction).toHaveBeenCalledWith({ - organizationId: "org-123", - data: { - name: "Test Project", - teamIds: [], - }, - }); - }); - }); - - test("submits form with selected teams", async () => { - const user = userEvent.setup(); - - // Update form mock to return teams - currentFormMock = createFormMock(); - currentFormMock.handleSubmit = vi.fn((onSubmit) => (e: any) => { - e.preventDefault(); - onSubmit({ name: "Test Project", teamIds: ["team-1", "team-2"] }); - }); - - vi.mocked(createProjectAction).mockResolvedValue({ data: mockProject } as any); - - render(); - - await waitFor(() => { - expect(screen.getByTestId("input")).toBeInTheDocument(); - expect(screen.getByTestId("multi-select")).toBeInTheDocument(); - }); - - const submitButton = screen.getByTestId("button-submit"); - await user.click(submitButton); - - await waitFor(() => { - expect(createProjectAction).toHaveBeenCalledWith({ - organizationId: "org-123", - data: { - name: "Test Project", - teamIds: ["team-1", "team-2"], - }, - }); - }); - }); - - test("shows success message and redirects on successful creation", async () => { - const user = userEvent.setup(); - - vi.mocked(createProjectAction).mockResolvedValue({ data: mockProject } as any); - - render(); - - await waitFor(() => { - expect(screen.getByTestId("input")).toBeInTheDocument(); - }); - - const submitButton = screen.getByTestId("button-submit"); - await user.click(submitButton); - - await waitFor(() => { - expect(createProjectAction).toHaveBeenCalled(); - }); - - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith("Project created successfully"); - expect(defaultProps.setOpen).toHaveBeenCalledWith(false); - expect(mockPush).toHaveBeenCalledWith("/environments/env-123/surveys"); - }); - }); - - test("shows error message on creation failure", async () => { - const user = userEvent.setup(); - const errorMessage = "Project creation failed"; - - vi.mocked(createProjectAction).mockResolvedValue({ - serverError: "Creation failed", - } as any); - vi.mocked(getFormattedErrorMessage).mockReturnValue(errorMessage); - - render(); - - await waitFor(() => { - expect(screen.getByTestId("input")).toBeInTheDocument(); - }); - - const submitButton = screen.getByTestId("button-submit"); - await user.click(submitButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith(errorMessage); - }); - }); - - test("closes modal and resets form when cancel button clicked", async () => { - const user = userEvent.setup(); - - render(); - - await waitFor(() => { - expect(screen.getByTestId("input")).toBeInTheDocument(); - }); - - const cancelButton = screen.getByTestId("button-button"); - await user.click(cancelButton); - - expect(defaultProps.setOpen).toHaveBeenCalledWith(false); - }); - - test("shows loading state during form submission", async () => { - // Mock loading state - currentFormMock = createFormMock({ isSubmitting: true }); - - vi.mocked(createProjectAction).mockImplementation( - () => new Promise((resolve) => setTimeout(() => resolve({ data: mockProject }), 100)) - ); - - render(); - - await waitFor(() => { - expect(screen.getByTestId("input")).toBeInTheDocument(); - }); - - // Check that loading state is shown - const submitButton = screen.getByTestId("button-submit"); - expect(submitButton).toHaveAttribute("data-loading", "true"); - }); - - test("handles form validation errors", async () => { - const user = userEvent.setup(); - - // Mock form with validation error - don't call onSubmit - currentFormMock = createFormMock({ shouldCallOnSubmit: false }); - - render(); - - await waitFor(() => { - expect(screen.getByTestId("input")).toBeInTheDocument(); - }); - - const submitButton = screen.getByTestId("button-submit"); - await user.click(submitButton); - - // Form should not submit if validation fails - expect(createProjectAction).not.toHaveBeenCalled(); - }); - - test("closes modal when dialog background is clicked", async () => { - const user = userEvent.setup(); - - render(); - - const dialog = screen.getByTestId("dialog"); - await user.click(dialog); - - expect(defaultProps.setOpen).toHaveBeenCalledWith(false); - }); -}); diff --git a/apps/web/modules/projects/components/project-limit-modal/index.test.tsx b/apps/web/modules/projects/components/project-limit-modal/index.test.tsx deleted file mode 100644 index 479e570299..0000000000 --- a/apps/web/modules/projects/components/project-limit-modal/index.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ModalButton } from "@/modules/ui/components/upgrade-prompt"; -import { ProjectLimitModal } from "./index"; - -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ open, onOpenChange, children }: any) => - open ? ( -
    onOpenChange(false)}> - {children} -
    - ) : null, - DialogContent: ({ children, className }: any) => ( -
    - {children} -
    - ), - DialogTitle: ({ children }: any) =>

    {children}

    , -})); - -vi.mock("@/modules/ui/components/upgrade-prompt", () => ({ - UpgradePrompt: ({ title, description, buttons }: any) => ( -
    -
    {title}
    -
    {description}
    - - -
    - ), -})); - -describe("ProjectLimitModal", () => { - afterEach(() => { - cleanup(); - }); - - const setOpen = vi.fn(); - const buttons: [ModalButton, ModalButton] = [ - { text: "Start Trial", onClick: vi.fn() }, - { text: "Upgrade", onClick: vi.fn() }, - ]; - - test("renders dialog and upgrade prompt with correct props", () => { - render(); - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("upgrade-prompt")).toBeInTheDocument(); - expect(screen.getByText("common.unlock_more_projects_with_a_higher_plan")).toBeInTheDocument(); - expect(screen.getByText("common.you_have_reached_your_limit_of_project_limit")).toBeInTheDocument(); - expect(screen.getByText("Start Trial")).toBeInTheDocument(); - expect(screen.getByText("Upgrade")).toBeInTheDocument(); - }); - - test("calls setOpen(false) when dialog is closed", async () => { - render(); - await userEvent.click(screen.getByTestId("dialog")); - expect(setOpen).toHaveBeenCalledWith(false); - }); - - test("calls button onClick handlers", async () => { - render(); - await userEvent.click(screen.getByText("Start Trial")); - expect(vi.mocked(buttons[0].onClick)).toHaveBeenCalled(); - await userEvent.click(screen.getByText("Upgrade")); - expect(vi.mocked(buttons[1].onClick)).toHaveBeenCalled(); - }); - - test("does not render when open is false", () => { - render(); - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/(setup)/app-connection/loading.test.tsx b/apps/web/modules/projects/settings/(setup)/app-connection/loading.test.tsx deleted file mode 100644 index 38185b7836..0000000000 --- a/apps/web/modules/projects/settings/(setup)/app-connection/loading.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { AppConnectionLoading } from "./loading"; - -vi.mock("@/modules/projects/settings/components/project-config-navigation", () => ({ - ProjectConfigNavigation: ({ activeId, loading }: any) => ( -
    - {activeId} {loading ? "loading" : "not-loading"} -
    - ), -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ pageTitle, children }: any) => ( -
    - {pageTitle} - {children} -
    - ), -})); -vi.mock("@/app/(app)/components/LoadingCard", () => ({ - LoadingCard: (props: any) => ( -
    - {props.title} {props.description} -
    - ), -})); - -describe("AppConnectionLoading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders wrapper, header, navigation, and all loading cards with correct tolgee keys", () => { - render(); - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toHaveTextContent("common.project_configuration"); - expect(screen.getByTestId("project-config-navigation")).toHaveTextContent("app-connection loading"); - const cards = screen.getAllByTestId("loading-card"); - expect(cards.length).toBe(3); - expect(cards[0]).toHaveTextContent("environments.project.app-connection.app_connection"); - expect(cards[0]).toHaveTextContent("environments.project.app-connection.app_connection_description"); - expect(cards[1]).toHaveTextContent("environments.project.app-connection.how_to_setup"); - expect(cards[1]).toHaveTextContent("environments.project.app-connection.how_to_setup_description"); - expect(cards[2]).toHaveTextContent("environments.project.app-connection.environment_id"); - expect(cards[2]).toHaveTextContent("environments.project.app-connection.environment_id_description"); - }); - - test("renders the blue info bar", () => { - render(); - expect(screen.getByText((_, element) => element!.className.includes("bg-blue-50"))).toBeInTheDocument(); - - expect( - screen.getByText((_, element) => element!.className.includes("animate-pulse")) - ).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/(setup)/app-connection/page.test.tsx b/apps/web/modules/projects/settings/(setup)/app-connection/page.test.tsx deleted file mode 100644 index 3d2d26e1f9..0000000000 --- a/apps/web/modules/projects/settings/(setup)/app-connection/page.test.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { cleanup, render } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { AppConnectionPage } from "./page"; - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ pageTitle, children }: any) => ( -
    - {pageTitle} - {children} -
    - ), -})); -vi.mock("@/modules/projects/settings/components/project-config-navigation", () => ({ - ProjectConfigNavigation: ({ environmentId, activeId }: any) => ( -
    - {environmentId} {activeId} -
    - ), -})); -vi.mock("@/modules/ui/components/environment-notice", () => ({ - EnvironmentNotice: ({ environmentId, subPageUrl }: any) => ( -
    - {environmentId} {subPageUrl} -
    - ), -})); -vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({ - SettingsCard: ({ title, description, children }: any) => ( -
    - {title} {description} {children} -
    - ), -})); -vi.mock("@/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator", () => ({ - WidgetStatusIndicator: ({ environment }: any) => ( -
    {environment.id}
    - ), -})); -vi.mock("@/modules/projects/settings/(setup)/components/setup-instructions", () => ({ - SetupInstructions: ({ environmentId, publicDomain }: any) => ( -
    - {environmentId} {publicDomain} -
    - ), -})); - -vi.mock("../components/action-settings-card", () => ({ - ActionSettingsCard: () =>
    action-settings-card
    , -})); - -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children }: any) =>
    {children}
    , - AlertButton: ({ children }: any) =>
    {children}
    , - AlertDescription: ({ children }: any) =>
    {children}
    , - AlertTitle: ({ children }: any) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/id-badge", () => ({ - IdBadge: ({ id }: any) =>
    {id}
    , -})); - -vi.mock("next/link", () => ({ - __esModule: true, - default: ({ children, href, target }: any) => ( - - {children} - - ), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(async (environmentId: string) => ({ - environment: { id: environmentId, projectId: "project-123" }, - isReadOnly: false, - })), -})); - -vi.mock("@/lib/environment/service", () => ({ - getEnvironments: vi.fn(async (projectId: string) => [ - { id: "env-123", projectId }, - { id: "env-456", projectId }, - ]), -})); - -vi.mock("@/lib/actionClass/service", () => ({ - getActionClasses: vi.fn(async () => []), -})); - -vi.mock("@/lib/getPublicUrl", () => ({ - getPublicDomain: vi.fn(() => "https://example.com"), -})); - -vi.mock("@/lib/utils/locale", () => ({ - findMatchingLocale: vi.fn(async () => "en"), -})); - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - SENTRY_RELEASE: "mock-sentry-release", - SENTRY_ENVIRONMENT: "mock-sentry-environment", - SESSION_MAX_AGE: 1000, -})); - -vi.mock("@/lib/env", () => ({ - env: { - PUBLIC_URL: "https://example.com", - }, -})); - -describe("AppConnectionPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all sections and passes correct props", async () => { - const params = { environmentId: "env-123" }; - const props = { params }; - const { findByTestId, findAllByTestId } = render(await AppConnectionPage(props)); - expect(await findByTestId("page-content-wrapper")).toBeInTheDocument(); - expect(await findByTestId("page-header")).toHaveTextContent("common.project_configuration"); - expect(await findByTestId("project-config-navigation")).toHaveTextContent("env-123 app-connection"); - expect(await findByTestId("environment-notice")).toHaveTextContent("env-123 /project/app-connection"); - - // Check that ActionSettingsCard is rendered - expect(await findByTestId("action-settings-card")).toBeInTheDocument(); - expect(await findByTestId("action-settings-card")).toHaveTextContent("action-settings-card"); - - const cards = await findAllByTestId("settings-card"); - expect(cards.length).toBe(2); - expect(cards[0]).toHaveTextContent("environments.project.app-connection.environment_id"); - expect(cards[0]).toHaveTextContent("environments.project.app-connection.environment_id_description"); - expect(cards[0]).toHaveTextContent("env-123"); // IdBadge - expect(cards[1]).toHaveTextContent("environments.project.app-connection.app_connection"); - expect(cards[1]).toHaveTextContent("environments.project.app-connection.app_connection_description"); - expect(cards[1]).toHaveTextContent("env-123"); // WidgetStatusIndicator - }); -}); diff --git a/apps/web/modules/projects/settings/(setup)/app-connection/utils.test.tsx b/apps/web/modules/projects/settings/(setup)/app-connection/utils.test.tsx deleted file mode 100644 index 58624e6351..0000000000 --- a/apps/web/modules/projects/settings/(setup)/app-connection/utils.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { cleanup, render } from "@testing-library/react"; -import { Code2Icon, MousePointerClickIcon } from "lucide-react"; -import React from "react"; -import { afterEach, describe, expect, test } from "vitest"; -import { ACTION_TYPE_ICON_LOOKUP } from "./utils"; - -describe("ACTION_TYPE_ICON_LOOKUP", () => { - afterEach(() => { - cleanup(); - }); - - test("should contain the correct icon for 'code'", () => { - expect(ACTION_TYPE_ICON_LOOKUP).toHaveProperty("code"); - const IconComponent = ACTION_TYPE_ICON_LOOKUP.code; - expect(React.isValidElement(IconComponent)).toBe(true); - - // Render the icon and check if it's the correct Lucide icon - const { container } = render(IconComponent); - const svgElement = container.querySelector("svg"); - expect(svgElement).toBeInTheDocument(); - // Check for a class or attribute specific to Code2Icon if possible, - // or compare the rendered output structure if necessary. - // For simplicity, we check the component type directly (though this is less robust) - expect(IconComponent.type).toBe(Code2Icon); - }); - - test("should contain the correct icon for 'noCode'", () => { - expect(ACTION_TYPE_ICON_LOOKUP).toHaveProperty("noCode"); - const IconComponent = ACTION_TYPE_ICON_LOOKUP.noCode; - expect(React.isValidElement(IconComponent)).toBe(true); - - // Render the icon and check if it's the correct Lucide icon - const { container } = render(IconComponent); - const svgElement = container.querySelector("svg"); - expect(svgElement).toBeInTheDocument(); - // Similar check as above for MousePointerClickIcon - expect(IconComponent.type).toBe(MousePointerClickIcon); - }); -}); diff --git a/apps/web/modules/projects/settings/(setup)/components/ActionActivityTab.test.tsx b/apps/web/modules/projects/settings/(setup)/components/ActionActivityTab.test.tsx deleted file mode 100644 index 2724d1d8f9..0000000000 --- a/apps/web/modules/projects/settings/(setup)/components/ActionActivityTab.test.tsx +++ /dev/null @@ -1,369 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TActionClass } from "@formbricks/types/action-classes"; -import { TEnvironment } from "@formbricks/types/environment"; -import { getActiveInactiveSurveysAction } from "@/modules/projects/settings/(setup)/app-connection/actions"; -import { createActionClassAction } from "@/modules/survey/editor/actions"; -import { ActionActivityTab } from "./ActionActivityTab"; - -// Mock dependencies -vi.mock("@/modules/projects/settings/(setup)/app-connection/utils", () => ({ - ACTION_TYPE_ICON_LOOKUP: { - noCode:
    NoCodeIcon
    , - code:
    CodeIcon
    , - }, -})); - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - SENTRY_RELEASE: "mock-sentry-release", - SENTRY_ENVIRONMENT: "mock-sentry-environment", - SESSION_MAX_AGE: 1000, -})); - -vi.mock("@/lib/time", () => ({ - convertDateTimeStringShort: (dateString: string) => `formatted-${dateString}`, -})); - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: (error: any) => `Formatted error: ${error?.message || "Unknown error"}`, -})); - -vi.mock("@/modules/survey/editor/actions", () => ({ - createActionClassAction: vi.fn(), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, variant, ...props }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/error-component", () => ({ - ErrorComponent: () =>
    ErrorComponent
    , -})); - -vi.mock("@/modules/ui/components/label", () => ({ - Label: ({ children, ...props }: any) => , -})); - -vi.mock("@/modules/ui/components/loading-spinner", () => ({ - LoadingSpinner: () =>
    LoadingSpinner
    , -})); - -vi.mock("@/modules/projects/settings/(setup)/app-connection/actions", () => ({ - getActiveInactiveSurveysAction: vi.fn(), -})); - -vi.mock("react-hot-toast", () => ({ - default: { success: vi.fn(), error: vi.fn() }, -})); - -const mockActionClass = { - id: "action1", - createdAt: new Date("2023-01-01T10:00:00Z"), - updatedAt: new Date("2023-01-10T11:00:00Z"), - name: "Test Action", - description: "Test Description", - type: "noCode", - environmentId: "env1_dev", - noCodeConfig: { - /* ... */ - } as any, -} as unknown as TActionClass; - -const mockEnvironmentDev = { - id: "env1_dev", - createdAt: new Date(), - updatedAt: new Date(), - type: "development", -} as unknown as TEnvironment; - -const mockEnvironmentProd = { - id: "env1_prod", - createdAt: new Date(), - updatedAt: new Date(), - type: "production", -} as unknown as TEnvironment; - -const mockOtherEnvActionClasses: TActionClass[] = [ - { - id: "action2", - createdAt: new Date(), - updatedAt: new Date(), - name: "Existing Action Prod", - type: "noCode", - environmentId: "env1_prod", - } as unknown as TActionClass, - { - id: "action3", - createdAt: new Date(), - updatedAt: new Date(), - name: "Existing Code Action Prod", - type: "code", - key: "existing-key", - environmentId: "env1_prod", - } as unknown as TActionClass, -]; - -describe("ActionActivityTab", () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(getActiveInactiveSurveysAction).mockResolvedValue({ - data: { - activeSurveys: ["Active Survey 1"], - inactiveSurveys: ["Inactive Survey 1", "Inactive Survey 2"], - }, - }); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders loading state initially", () => { - // Don't resolve the promise immediately - vi.mocked(getActiveInactiveSurveysAction).mockReturnValue(new Promise(() => {})); - render( - - ); - expect(screen.getByText("LoadingSpinner")).toBeInTheDocument(); - }); - - test("renders error state if fetching surveys fails", async () => { - vi.mocked(getActiveInactiveSurveysAction).mockResolvedValue({ - data: undefined, - }); - render( - - ); - // Wait for the component to update after the promise resolves - await screen.findByText("ErrorComponent"); - expect(screen.getByText("ErrorComponent")).toBeInTheDocument(); - }); - - test("renders survey lists and action details correctly", async () => { - render( - - ); - - // Wait for loading to finish - await screen.findByText("common.active_surveys"); - - // Check survey lists - expect(screen.getByText("Active Survey 1")).toBeInTheDocument(); - expect(screen.getByText("Inactive Survey 1")).toBeInTheDocument(); - expect(screen.getByText("Inactive Survey 2")).toBeInTheDocument(); - - // Check action details - // Use the actual Date.toString() output that the mock receives - expect(screen.getByText(`formatted-${mockActionClass.createdAt.toString()}`)).toBeInTheDocument(); // Created on - expect(screen.getByText(`formatted-${mockActionClass.updatedAt.toString()}`)).toBeInTheDocument(); // Last updated - expect(screen.getByText("NoCodeIcon")).toBeInTheDocument(); // Type icon - expect(screen.getByText("noCode")).toBeInTheDocument(); // Type text (now lowercase, capitalized via CSS) - expect(screen.getByText("Development")).toBeInTheDocument(); // Environment - expect(screen.getByText("Copy to Production")).toBeInTheDocument(); // Copy button text - }); - - test("calls copyAction with correct data on button click", async () => { - vi.mocked(createActionClassAction).mockResolvedValue({ data: { id: "newAction" } as any }); - render( - - ); - - await screen.findByText("Copy to Production"); - const copyButton = screen.getByText("Copy to Production"); - await userEvent.click(copyButton); - - expect(createActionClassAction).toHaveBeenCalledTimes(1); - // Include the extra properties that the component sends due to spreading mockActionClass - const expectedActionInput = { - ...mockActionClass, // Spread the original object - name: "Test Action", // Keep the original name as it doesn't conflict - environmentId: "env1_prod", // Target environment ID - }; - // Remove properties not expected by the action call itself, even if sent by component - delete (expectedActionInput as any).id; - delete (expectedActionInput as any).createdAt; - delete (expectedActionInput as any).updatedAt; - - // The assertion now checks against the structure sent by the component - expect(createActionClassAction).toHaveBeenCalledWith({ - action: { - ...mockActionClass, // Include id, createdAt, updatedAt etc. - name: "Test Action", - environmentId: "env1_prod", - }, - }); - expect(toast.success).toHaveBeenCalledWith("environments.actions.action_copied_successfully"); - }); - - test("handles name conflict during copy", async () => { - vi.mocked(createActionClassAction).mockResolvedValue({ data: { id: "newAction" } as any }); - const conflictingActionClass = { ...mockActionClass, name: "Existing Action Prod" }; - render( - - ); - - await screen.findByText("Copy to Production"); - const copyButton = screen.getByText("Copy to Production"); - await userEvent.click(copyButton); - - expect(createActionClassAction).toHaveBeenCalledTimes(1); - - // The assertion now checks against the structure sent by the component - expect(createActionClassAction).toHaveBeenCalledWith({ - action: { - ...conflictingActionClass, // Include id, createdAt, updatedAt etc. - name: "Existing Action Prod (copy)", - environmentId: "env1_prod", - }, - }); - expect(toast.success).toHaveBeenCalledWith("environments.actions.action_copied_successfully"); - }); - - test("handles key conflict during copy for 'code' type", async () => { - const codeActionClass: TActionClass = { - ...mockActionClass, - id: "codeAction1", - type: "code", - key: "existing-key", // Conflicting key - noCodeConfig: { - /* ... */ - } as any, - }; - render( - - ); - - await screen.findByText("Copy to Production"); - const copyButton = screen.getByText("Copy to Production"); - await userEvent.click(copyButton); - - expect(createActionClassAction).not.toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalledWith("environments.actions.action_with_key_already_exists"); - }); - - test("shows error if copy action fails server-side", async () => { - vi.mocked(createActionClassAction).mockResolvedValue({ data: undefined }); - render( - - ); - - await screen.findByText("Copy to Production"); - const copyButton = screen.getByText("Copy to Production"); - await userEvent.click(copyButton); - - expect(createActionClassAction).toHaveBeenCalledTimes(1); - expect(toast.error).toHaveBeenCalledWith("environments.actions.action_copy_failed"); - }); - - test("shows error and prevents copy if user is read-only", async () => { - render( - - ); - - await screen.findByText("Copy to Production"); - const copyButton = screen.getByText("Copy to Production"); - await userEvent.click(copyButton); - - expect(createActionClassAction).not.toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalledWith("common.you_are_not_authorised_to_perform_this_action"); - }); - - test("renders correct copy button text for production environment", async () => { - render( - - ); - await screen.findByText("Copy to Development"); - expect(screen.getByText("Copy to Development")).toBeInTheDocument(); - expect(screen.getByText("Production")).toBeInTheDocument(); // Environment text - }); -}); diff --git a/apps/web/modules/projects/settings/(setup)/components/ActionClassesTable.test.tsx b/apps/web/modules/projects/settings/(setup)/components/ActionClassesTable.test.tsx deleted file mode 100644 index 6e8212fe50..0000000000 --- a/apps/web/modules/projects/settings/(setup)/components/ActionClassesTable.test.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TActionClass } from "@formbricks/types/action-classes"; -import { TEnvironment } from "@formbricks/types/environment"; -import { ActionClassesTable } from "./ActionClassesTable"; - -// Mock the ActionDetailModal -vi.mock("./ActionDetailModal", () => ({ - ActionDetailModal: ({ open, actionClass, setOpen }: any) => - open ? ( -
    - Modal for {actionClass.name} - -
    - ) : null, -})); - -const mockActionClasses: TActionClass[] = [ - { id: "1", name: "Action 1", type: "noCode", environmentId: "env1" } as TActionClass, - { id: "2", name: "Action 2", type: "code", environmentId: "env1" } as TActionClass, -]; - -const mockEnvironment: TEnvironment = { - id: "env1", - name: "Test Environment", - type: "development", -} as unknown as TEnvironment; -const mockOtherEnvironment: TEnvironment = { - id: "env2", - name: "Other Environment", - type: "production", -} as unknown as TEnvironment; - -const mockTableHeading =
    Table Heading
    ; -const mockActionRows = mockActionClasses.map((action) => ( -
    - {action.name} Row -
    -)); - -describe("ActionClassesTable", () => { - afterEach(() => { - cleanup(); - }); - - test("renders table heading and action rows when actions exist", () => { - render( - - {[mockTableHeading, mockActionRows]} - - ); - - expect(screen.getByTestId("table-heading")).toBeInTheDocument(); - expect(screen.getByTestId("action-row-1")).toBeInTheDocument(); - expect(screen.getByTestId("action-row-2")).toBeInTheDocument(); - expect(screen.queryByText("No actions found")).not.toBeInTheDocument(); - }); - - test("renders 'No actions found' message when no actions exist", () => { - render( - - {[mockTableHeading, []]} - - ); - - expect(screen.getByTestId("table-heading")).toBeInTheDocument(); - expect(screen.getByText("No actions found")).toBeInTheDocument(); - expect(screen.queryByTestId("action-row-1")).not.toBeInTheDocument(); - }); - - test("opens ActionDetailModal with correct action when a row is clicked", async () => { - render( - - {[mockTableHeading, mockActionRows]} - - ); - - // Modal should not be open initially - expect(screen.queryByTestId("action-detail-modal")).not.toBeInTheDocument(); - - // Find the button wrapping the first action row - const actionButton1 = screen.getByTitle("Action 1"); - await userEvent.click(actionButton1); - - // Modal should now be open with the correct action name - const modal = screen.getByTestId("action-detail-modal"); - expect(modal).toBeInTheDocument(); - expect(modal).toHaveTextContent("Modal for Action 1"); - - // Close the modal - await userEvent.click(screen.getByText("Close Modal")); - expect(screen.queryByTestId("action-detail-modal")).not.toBeInTheDocument(); - - // Click the second action button - const actionButton2 = screen.getByTitle("Action 2"); - await userEvent.click(actionButton2); - - // Modal should open for the second action - const modal2 = screen.getByTestId("action-detail-modal"); - expect(modal2).toBeInTheDocument(); - expect(modal2).toHaveTextContent("Modal for Action 2"); - }); -}); diff --git a/apps/web/modules/projects/settings/(setup)/components/ActionDetailModal.test.tsx b/apps/web/modules/projects/settings/(setup)/components/ActionDetailModal.test.tsx deleted file mode 100644 index 9e5f0b78d8..0000000000 --- a/apps/web/modules/projects/settings/(setup)/components/ActionDetailModal.test.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TActionClass } from "@formbricks/types/action-classes"; -import { TEnvironment } from "@formbricks/types/environment"; -import { ActionActivityTab } from "./ActionActivityTab"; -import { ActionDetailModal } from "./ActionDetailModal"; -// Import mocked components -import { ActionSettingsTab } from "./ActionSettingsTab"; - -// Mock the Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ - open, - onOpenChange, - children, - }: { - open: boolean; - onOpenChange: (open: boolean) => void; - children: React.ReactNode; - }) => - open ? ( -
    - {children} - -
    - ) : null, - DialogContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -

    {children}

    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -vi.mock("./ActionActivityTab", () => ({ - ActionActivityTab: vi.fn(() =>
    ActionActivityTab
    ), -})); - -vi.mock("./ActionSettingsTab", () => ({ - ActionSettingsTab: vi.fn(() =>
    ActionSettingsTab
    ), -})); - -// Mock the utils file to control ACTION_TYPE_ICON_LOOKUP -vi.mock("@/app/(app)/environments/[environmentId]/actions/utils", () => ({ - ACTION_TYPE_ICON_LOOKUP: { - code:
    Code Icon Mock
    , - noCode:
    No Code Icon Mock
    , - // Add other types if needed by other tests or default props - }, -})); - -// Mock useTranslate -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - const translations = { - "common.activity": "Activity", - "common.settings": "Settings", - "common.no_code": "No Code", - "common.action": "Action", - "common.code": "Code", - }; - return translations[key] || key; - }, - }), -})); - -const mockEnvironmentId = "test-env-id"; -const mockSetOpen = vi.fn(); - -const mockEnvironment = { - id: mockEnvironmentId, - createdAt: new Date(), - updatedAt: new Date(), - type: "production", // Use string literal as TEnvironmentType is not exported - appSetupCompleted: false, -} as unknown as TEnvironment; - -const mockActionClass: TActionClass = { - id: "action-class-1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Action", - description: "This is a test action", - type: "code", // Ensure this matches a key in the mocked ACTION_TYPE_ICON_LOOKUP - environmentId: mockEnvironmentId, - noCodeConfig: null, - key: "test-action-key", -}; - -const mockActionClasses: TActionClass[] = [mockActionClass]; -const mockOtherEnvActionClasses: TActionClass[] = []; -const mockOtherEnvironment = { ...mockEnvironment, id: "other-env-id", name: "Other Environment" }; - -const defaultProps = { - environmentId: mockEnvironmentId, - environment: mockEnvironment, - open: true, - setOpen: mockSetOpen, - actionClass: mockActionClass, - actionClasses: mockActionClasses, - isReadOnly: false, - otherEnvironment: mockOtherEnvironment, - otherEnvActionClasses: mockOtherEnvActionClasses, -}; - -describe("ActionDetailModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); // Clear mocks after each test - }); - - test("renders correctly when open", () => { - render(); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Test Action"); - expect(screen.getByTestId("dialog-description")).toHaveTextContent("This is a test action"); - expect(screen.getByTestId("code-icon")).toBeInTheDocument(); - expect(screen.getByText("Activity")).toBeInTheDocument(); - expect(screen.getByText("Settings")).toBeInTheDocument(); - // Only the first tab (Activity) should be active initially - expect(screen.getByTestId("action-activity-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("action-settings-tab")).not.toBeInTheDocument(); - }); - - test("does not render when open is false", () => { - render(); - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); - - test("switches tabs correctly", async () => { - const user = userEvent.setup(); - render(); - - // Initially shows activity tab (first tab is active) - expect(screen.getByTestId("action-activity-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("action-settings-tab")).not.toBeInTheDocument(); - - // Click settings tab - const settingsTab = screen.getByText("Settings"); - await user.click(settingsTab); - - // Now shows settings tab content - expect(screen.queryByTestId("action-activity-tab")).not.toBeInTheDocument(); - expect(screen.getByTestId("action-settings-tab")).toBeInTheDocument(); - - // Click activity tab again - const activityTab = screen.getByText("Activity"); - await user.click(activityTab); - - // Back to activity tab content - expect(screen.getByTestId("action-activity-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("action-settings-tab")).not.toBeInTheDocument(); - }); - - test("resets to first tab when modal is reopened", async () => { - const user = userEvent.setup(); - const { rerender } = render(); - - // Switch to settings tab - const settingsTab = screen.getByText("Settings"); - await user.click(settingsTab); - expect(screen.getByTestId("action-settings-tab")).toBeInTheDocument(); - - // Close modal - rerender(); - - // Reopen modal - rerender(); - - // Should be back to activity tab (first tab) - expect(screen.getByTestId("action-activity-tab")).toBeInTheDocument(); - expect(screen.queryByTestId("action-settings-tab")).not.toBeInTheDocument(); - }); - - test("renders correct icon based on action type", () => { - // Test with 'noCode' type - const noCodeAction: TActionClass = { ...mockActionClass, type: "noCode" } as TActionClass; - render(); - - expect(screen.getByTestId("nocode-icon")).toBeInTheDocument(); - expect(screen.queryByTestId("code-icon")).not.toBeInTheDocument(); - }); - - test("handles action without description", () => { - const actionWithoutDescription = { ...mockActionClass, description: "" }; - render(); - - expect(screen.getByTestId("dialog-title")).toHaveTextContent("Test Action"); - expect(screen.getByTestId("dialog-description")).toHaveTextContent("Code action"); - }); - - test("passes correct props to ActionActivityTab", () => { - render(); - - const mockedActionActivityTab = vi.mocked(ActionActivityTab); - expect(mockedActionActivityTab).toHaveBeenCalledWith( - { - otherEnvActionClasses: mockOtherEnvActionClasses, - otherEnvironment: mockOtherEnvironment, - isReadOnly: false, - environment: mockEnvironment, - actionClass: mockActionClass, - environmentId: mockEnvironmentId, - }, - undefined - ); - }); - - test("passes correct props to ActionSettingsTab when tab is active", async () => { - const user = userEvent.setup(); - render(); - - // ActionSettingsTab should not be called initially since first tab is active - const mockedActionSettingsTab = vi.mocked(ActionSettingsTab); - expect(mockedActionSettingsTab).not.toHaveBeenCalled(); - - // Click the settings tab to activate ActionSettingsTab - const settingsTab = screen.getByText("Settings"); - await user.click(settingsTab); - - // Now ActionSettingsTab should be called with correct props - expect(mockedActionSettingsTab).toHaveBeenCalledWith( - { - actionClass: mockActionClass, - actionClasses: mockActionClasses, - setOpen: mockSetOpen, - isReadOnly: false, - }, - undefined - ); - }); - - test("passes isReadOnly prop correctly", () => { - render(); - - const mockedActionActivityTab = vi.mocked(ActionActivityTab); - expect(mockedActionActivityTab).toHaveBeenCalledWith( - expect.objectContaining({ - isReadOnly: true, - }), - undefined - ); - }); -}); diff --git a/apps/web/modules/projects/settings/(setup)/components/ActionRowData.test.tsx b/apps/web/modules/projects/settings/(setup)/components/ActionRowData.test.tsx deleted file mode 100644 index 6ecef4b19b..0000000000 --- a/apps/web/modules/projects/settings/(setup)/components/ActionRowData.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TActionClass } from "@formbricks/types/action-classes"; -import { timeSince } from "@/lib/time"; -import { ActionClassDataRow } from "./ActionRowData"; - -vi.mock("@/lib/time", () => ({ - timeSince: vi.fn(), -})); - -const mockActionClass: TActionClass = { - id: "testId", - createdAt: new Date(), - updatedAt: new Date(), - name: "Test Action", - description: "This is a test action", - type: "code", - noCodeConfig: null, - environmentId: "envId", - key: null, -}; - -const locale = "en-US"; -const timeSinceOutput = "2 hours ago"; - -describe("ActionClassDataRow", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders code action correctly", () => { - vi.mocked(timeSince).mockReturnValue(timeSinceOutput); - const actionClass = { ...mockActionClass, type: "code" } as TActionClass; - render(); - - expect(screen.getByText(actionClass.name)).toBeInTheDocument(); - expect(screen.getByText(actionClass.description!)).toBeInTheDocument(); - expect(screen.getByText(timeSinceOutput)).toBeInTheDocument(); - expect(timeSince).toHaveBeenCalledWith(actionClass.createdAt.toString(), locale); - }); - - test("renders no-code action correctly", () => { - vi.mocked(timeSince).mockReturnValue(timeSinceOutput); - const actionClass = { ...mockActionClass, type: "noCode" } as TActionClass; - render(); - - expect(screen.getByText(actionClass.name)).toBeInTheDocument(); - expect(screen.getByText(actionClass.description!)).toBeInTheDocument(); - expect(screen.getByText(timeSinceOutput)).toBeInTheDocument(); - expect(timeSince).toHaveBeenCalledWith(actionClass.createdAt.toString(), locale); - }); - - test("renders without description", () => { - vi.mocked(timeSince).mockReturnValue(timeSinceOutput); - const actionClass = { ...mockActionClass, description: undefined } as unknown as TActionClass; - render(); - - expect(screen.getByText(actionClass.name)).toBeInTheDocument(); - expect(screen.queryByText("This is a test action")).not.toBeInTheDocument(); - expect(screen.getByText(timeSinceOutput)).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/(setup)/components/ActionSettingsTab.test.tsx b/apps/web/modules/projects/settings/(setup)/components/ActionSettingsTab.test.tsx deleted file mode 100644 index c5498219db..0000000000 --- a/apps/web/modules/projects/settings/(setup)/components/ActionSettingsTab.test.tsx +++ /dev/null @@ -1,414 +0,0 @@ -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { toast } from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TActionClass, TActionClassNoCodeConfig, TActionClassType } from "@formbricks/types/action-classes"; -import { ActionSettingsTab } from "./ActionSettingsTab"; - -// Mock actions -vi.mock("@/modules/projects/settings/(setup)/app-connection/actions", () => ({ - deleteActionClassAction: vi.fn(), - updateActionClassAction: vi.fn(), -})); - -// Mock action utils -vi.mock("@/modules/survey/editor/lib/action-utils", () => ({ - useActionClassKeys: vi.fn(() => ["existing-key"]), - createActionClassZodResolver: vi.fn(() => vi.fn()), - validatePermissions: vi.fn(), -})); - -// Mock action builder -vi.mock("@/modules/survey/editor/lib/action-builder", () => ({ - buildActionObject: vi.fn((data, environmentId) => ({ - ...data, - environmentId, - })), -})); - -// Mock utils -vi.mock("@/modules/projects/settings/(setup)/app-connection/utils", () => ({ - isValidCssSelector: vi.fn((selector) => selector !== "invalid-selector"), -})); - -// Mock UI components -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, variant, loading, ...props }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/code-action-form", () => ({ - CodeActionForm: ({ isReadOnly }: { isReadOnly: boolean }) => ( -
    - Code Action Form -
    - ), -})); - -vi.mock("@/modules/ui/components/delete-dialog", () => ({ - DeleteDialog: ({ open, setOpen, isDeleting, onDelete }: any) => - open ? ( -
    - Delete Dialog - - -
    - ) : null, -})); - -vi.mock("@/modules/ui/components/action-name-description-fields", () => ({ - ActionNameDescriptionFields: ({ isReadOnly, nameInputId, descriptionInputId }: any) => ( -
    - - -
    - ), -})); - -vi.mock("@/modules/ui/components/no-code-action-form", () => ({ - NoCodeActionForm: ({ isReadOnly }: { isReadOnly: boolean }) => ( -
    - No Code Action Form -
    - ), -})); - -// Mock icons -vi.mock("lucide-react", () => ({ - TrashIcon: () =>
    Trash
    , -})); - -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - toast: { success: vi.fn(), error: vi.fn() }, -})); - -// Mock react-hook-form -const mockHandleSubmit = vi.fn(); -const mockForm = { - handleSubmit: mockHandleSubmit, - control: {}, - formState: { errors: {} }, -}; - -vi.mock("react-hook-form", async () => { - const actual = await vi.importActual("react-hook-form"); - return { - ...actual, - useForm: vi.fn(() => mockForm), - FormProvider: ({ children }: any) =>
    {children}
    , - }; -}); - -const mockSetOpen = vi.fn(); -const mockActionClasses: TActionClass[] = [ - { - id: "action1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Existing Action", - description: "An existing action", - type: "noCode", - environmentId: "env1", - noCodeConfig: { type: "click" } as TActionClassNoCodeConfig, - } as unknown as TActionClass, -]; - -const createMockActionClass = (id: string, type: TActionClassType, name: string): TActionClass => - ({ - id, - createdAt: new Date(), - updatedAt: new Date(), - name, - description: `${name} description`, - type, - environmentId: "env1", - ...(type === "code" && { key: `${name}-key` }), - ...(type === "noCode" && { - noCodeConfig: { type: "url", rule: "exactMatch", value: `http://${name}.com` }, - }), - }) as unknown as TActionClass; - -describe("ActionSettingsTab", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockHandleSubmit.mockImplementation((fn) => fn); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders correctly for 'code' action type", () => { - const actionClass = createMockActionClass("code1", "code", "Code Action"); - render( - - ); - - expect(screen.getByTestId("action-name-description-fields")).toBeInTheDocument(); - expect(screen.getByTestId("name-input-actionNameSettingsInput")).toBeInTheDocument(); - expect(screen.getByTestId("description-input-actionDescriptionSettingsInput")).toBeInTheDocument(); - expect(screen.getByTestId("code-action-form")).toBeInTheDocument(); - expect( - screen.getByText("environments.actions.this_is_a_code_action_please_make_changes_in_your_code_base") - ).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "common.save_changes" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /common.delete/ })).toBeInTheDocument(); - }); - - test("renders correctly for 'noCode' action type", () => { - const actionClass = createMockActionClass("noCode1", "noCode", "No Code Action"); - render( - - ); - - expect(screen.getByTestId("action-name-description-fields")).toBeInTheDocument(); - expect(screen.getByTestId("no-code-action-form")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "common.save_changes" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /common.delete/ })).toBeInTheDocument(); - }); - - test("renders correctly for other action types (fallback)", () => { - const actionClass = { - ...createMockActionClass("auto1", "noCode", "Auto Action"), - type: "automatic" as any, - }; - render( - - ); - - expect(screen.getByTestId("action-name-description-fields")).toBeInTheDocument(); - expect( - screen.getByText( - "environments.actions.this_action_was_created_automatically_you_cannot_make_changes_to_it" - ) - ).toBeInTheDocument(); - }); - - test("calls utility functions on initialization", async () => { - const actionUtilsMock = await import("@/modules/survey/editor/lib/action-utils"); - - const actionClass = createMockActionClass("noCode1", "noCode", "No Code Action"); - render( - - ); - - expect(actionUtilsMock.useActionClassKeys).toHaveBeenCalledWith(mockActionClasses); - expect(actionUtilsMock.createActionClassZodResolver).toHaveBeenCalled(); - }); - - test("handles successful form submission", async () => { - const { updateActionClassAction } = await import( - "@/modules/projects/settings/(setup)/app-connection/actions" - ); - const actionUtilsMock = await import("@/modules/survey/editor/lib/action-utils"); - - vi.mocked(updateActionClassAction).mockResolvedValue({ data: {} } as any); - - const actionClass = createMockActionClass("noCode1", "noCode", "No Code Action"); - render( - - ); - - // Check that utility functions were called during component initialization - expect(actionUtilsMock.useActionClassKeys).toHaveBeenCalledWith(mockActionClasses); - expect(actionUtilsMock.createActionClassZodResolver).toHaveBeenCalled(); - }); - - test("handles permission validation error", async () => { - const actionUtilsMock = await import("@/modules/survey/editor/lib/action-utils"); - vi.mocked(actionUtilsMock.validatePermissions).mockImplementation(() => { - throw new Error("Not authorized"); - }); - - const actionClass = createMockActionClass("noCode1", "noCode", "No Code Action"); - render( - - ); - - const submitButton = screen.getByRole("button", { name: "common.save_changes" }); - - mockHandleSubmit.mockImplementation((fn) => (e) => { - e.preventDefault(); - return fn({ name: "Test", type: "noCode" }); - }); - - await userEvent.click(submitButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Not authorized"); - }); - }); - - test("handles successful deletion", async () => { - const actionClass = createMockActionClass("noCode1", "noCode", "No Code Action"); - const { deleteActionClassAction } = await import( - "@/modules/projects/settings/(setup)/app-connection/actions" - ); - vi.mocked(deleteActionClassAction).mockResolvedValue({ data: actionClass } as any); - - render( - - ); - - const deleteButtonTrigger = screen.getByRole("button", { name: /common.delete/ }); - await userEvent.click(deleteButtonTrigger); - - expect(screen.getByTestId("delete-dialog")).toBeInTheDocument(); - - const confirmDeleteButton = screen.getByRole("button", { name: "Confirm Delete" }); - await userEvent.click(confirmDeleteButton); - - await waitFor(() => { - expect(deleteActionClassAction).toHaveBeenCalledWith({ actionClassId: actionClass.id }); - expect(toast.success).toHaveBeenCalledWith("environments.actions.action_deleted_successfully"); - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - }); - - test("handles deletion failure", async () => { - const actionClass = createMockActionClass("noCode1", "noCode", "No Code Action"); - const { deleteActionClassAction } = await import( - "@/modules/projects/settings/(setup)/app-connection/actions" - ); - vi.mocked(deleteActionClassAction).mockRejectedValue(new Error("Deletion failed")); - - render( - - ); - - const deleteButtonTrigger = screen.getByRole("button", { name: /common.delete/ }); - await userEvent.click(deleteButtonTrigger); - const confirmDeleteButton = screen.getByRole("button", { name: "Confirm Delete" }); - await userEvent.click(confirmDeleteButton); - - await waitFor(() => { - expect(deleteActionClassAction).toHaveBeenCalled(); - expect(toast.error).toHaveBeenCalledWith("common.something_went_wrong_please_try_again"); - }); - expect(mockSetOpen).not.toHaveBeenCalled(); - }); - - test("renders read-only state correctly", () => { - const actionClass = createMockActionClass("noCode1", "noCode", "No Code Action"); - render( - - ); - - expect(screen.getByTestId("name-input-actionNameSettingsInput")).toBeDisabled(); - expect(screen.getByTestId("description-input-actionDescriptionSettingsInput")).toBeDisabled(); - expect(screen.getByTestId("no-code-action-form")).toHaveAttribute("data-readonly", "true"); - expect(screen.queryByRole("button", { name: "common.save_changes" })).not.toBeInTheDocument(); - expect(screen.queryByRole("button", { name: /common.delete/ })).not.toBeInTheDocument(); - expect(screen.getByRole("link", { name: "common.read_docs" })).toBeInTheDocument(); - }); - - test("prevents delete when read-only", async () => { - const actionClass = createMockActionClass("noCode1", "noCode", "No Code Action"); - const { deleteActionClassAction } = await import( - "@/modules/projects/settings/(setup)/app-connection/actions" - ); - - render( - - ); - - expect(screen.queryByRole("button", { name: /common.delete/ })).not.toBeInTheDocument(); - expect(deleteActionClassAction).not.toHaveBeenCalled(); - }); - - test("renders docs link correctly", () => { - const actionClass = createMockActionClass("noCode1", "noCode", "No Code Action"); - render( - - ); - const docsLink = screen.getByRole("link", { name: "common.read_docs" }); - expect(docsLink).toHaveAttribute("href", "https://formbricks.com/docs/actions/no-code"); - expect(docsLink).toHaveAttribute("target", "_blank"); - }); - - test("uses correct input IDs for ActionNameDescriptionFields", () => { - const actionClass = createMockActionClass("noCode1", "noCode", "No Code Action"); - render( - - ); - - expect(screen.getByTestId("name-input-actionNameSettingsInput")).toBeInTheDocument(); - expect(screen.getByTestId("description-input-actionDescriptionSettingsInput")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/(setup)/components/ActionTableHeading.test.tsx b/apps/web/modules/projects/settings/(setup)/components/ActionTableHeading.test.tsx deleted file mode 100644 index f2070498ab..0000000000 --- a/apps/web/modules/projects/settings/(setup)/components/ActionTableHeading.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ActionTableHeading } from "./ActionTableHeading"; - -// Mock the server-side translation function -vi.mock("@/tolgee/server", () => ({ - getTranslate: async () => (key: string) => key, -})); - -describe("ActionTableHeading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders the table heading with correct column names", async () => { - // Render the async component - const ResolvedComponent = await ActionTableHeading(); - render(ResolvedComponent); - - // Check if the translated column headers are present - expect(screen.getByText("environments.actions.user_actions")).toBeInTheDocument(); - expect(screen.getByText("common.created")).toBeInTheDocument(); - // Check for the screen reader only text - expect(screen.getByText("common.edit")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/(setup)/components/AddActionModal.test.tsx b/apps/web/modules/projects/settings/(setup)/components/AddActionModal.test.tsx deleted file mode 100644 index f5d7f27b76..0000000000 --- a/apps/web/modules/projects/settings/(setup)/components/AddActionModal.test.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import React from "react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TActionClass, TActionClassNoCodeConfig } from "@formbricks/types/action-classes"; -import { AddActionModal } from "./AddActionModal"; - -// Mock child components and hooks -vi.mock("@/modules/survey/editor/components/create-new-action-tab", () => ({ - CreateNewActionTab: vi.fn(({ setOpen }) => ( -
    - CreateNewActionTab Content - -
    - )), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, ...props }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/dialog", () => { - const React = require("react"); - const { useState } = React; - - return { - Dialog: ({ children, open: externalOpen, onOpenChange }: any) => { - const [internalOpen, setInternalOpen] = useState(externalOpen); - - // Update internal state when external open prop changes - React.useEffect(() => { - setInternalOpen(externalOpen); - }, [externalOpen]); - - const handleOpenChange = (newOpen: boolean) => { - setInternalOpen(newOpen); - onOpenChange?.(newOpen); - }; - - return internalOpen ? ( - - {children} - - - ) : null; - }, - DialogContent: ({ children, disableCloseOnOutsideClick, ...props }: any) => ( -
    - {children} -
    - ), - DialogHeader: ({ children }: any) =>
    {children}
    , - DialogTitle: ({ children, className }: any) => ( -

    - {children} -

    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogBody: ({ children }: any) =>
    {children}
    , - }; -}); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("lucide-react", () => ({ - MousePointerClickIcon: () =>
    , - PlusIcon: () =>
    , -})); - -const mockActionClasses: TActionClass[] = [ - { - id: "action1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Action 1", - description: "Description 1", - type: "noCode", - environmentId: "env1", - noCodeConfig: { type: "click" } as unknown as TActionClassNoCodeConfig, - } as unknown as TActionClass, -]; - -const environmentId = "env1"; - -describe("AddActionModal", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const createTestWrapper = () => { - const TestWrapper = () => { - const [open, setOpen] = React.useState(true); - - return ( - - ); - }; - return TestWrapper; - }; - - test("passes correct props to CreateNewActionTab", async () => { - const { CreateNewActionTab } = await import("@/modules/survey/editor/components/create-new-action-tab"); - const mockedCreateNewActionTab = vi.mocked(CreateNewActionTab); - - render( - - ); - - expect(mockedCreateNewActionTab).toHaveBeenCalled(); - const props = mockedCreateNewActionTab.mock.calls[0][0]; - expect(props.environmentId).toBe(environmentId); - expect(props.actionClasses).toEqual(mockActionClasses); // Initial state check - expect(props.isReadOnly).toBe(false); - expect(props.setOpen).toBeInstanceOf(Function); - expect(props.setActionClasses).toBeInstanceOf(Function); - }); - - test("closes the dialog when the close button (simulated) is clicked", async () => { - const TestWrapper = createTestWrapper(); - render(); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - - // Simulate closing via the mocked Dialog's close button - const closeDialogButton = screen.getByText("Close Dialog"); - await userEvent.click(closeDialogButton); - - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); - - test("closes the dialog when setOpen is called from CreateNewActionTab", async () => { - const TestWrapper = createTestWrapper(); - render(); - - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - - // Simulate closing via the mocked CreateNewActionTab's button - const closeFromTabButton = screen.getByText("Close from Tab"); - await userEvent.click(closeFromTabButton); - - expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/components/project-config-navigation.test.tsx b/apps/web/modules/projects/settings/components/project-config-navigation.test.tsx deleted file mode 100644 index c57b4aaa31..0000000000 --- a/apps/web/modules/projects/settings/components/project-config-navigation.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { cleanup, render } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation"; -import { ProjectConfigNavigation } from "./project-config-navigation"; - -vi.mock("@/modules/ui/components/secondary-navigation", () => ({ - SecondaryNavigation: vi.fn(() =>
    ), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ t: (key: string) => key }), -})); - -let mockPathname = "/environments/env-1/project/look"; -vi.mock("next/navigation", () => ({ - usePathname: vi.fn(() => mockPathname), -})); - -describe("ProjectConfigNavigation", () => { - afterEach(() => { - cleanup(); - }); - - test("sets current to true for the correct nav item based on pathname", () => { - const cases = [ - { path: "/environments/env-1/project/general", idx: 0 }, - { path: "/environments/env-1/project/look", idx: 1 }, - { path: "/environments/env-1/project/app-connection", idx: 2 }, - { path: "/environments/env-1/project/integrations", idx: 3 }, - { path: "/environments/env-1/project/teams", idx: 4 }, - { path: "/environments/env-1/project/languages", idx: 5 }, - { path: "/environments/env-1/project/tags", idx: 6 }, - ]; - for (const { path, idx } of cases) { - mockPathname = path; - render(); - const navArg = SecondaryNavigation.mock.calls[0][0].navigation; - - navArg.forEach((item: any, i: number) => { - if (i === idx) { - expect(item.current).toBe(true); - } else { - expect(item.current).toBe(false); - } - }); - SecondaryNavigation.mockClear(); - } - }); -}); diff --git a/apps/web/modules/projects/settings/general/components/delete-project-render.test.tsx b/apps/web/modules/projects/settings/general/components/delete-project-render.test.tsx deleted file mode 100644 index 06c14aa218..0000000000 --- a/apps/web/modules/projects/settings/general/components/delete-project-render.test.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TProject } from "@formbricks/types/project"; -import { DeleteProjectRender } from "./delete-project-render"; - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, ...props }: any) => , -})); -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children }: any) =>
    {children}
    , - AlertDescription: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/delete-dialog", () => ({ - DeleteDialog: ({ open, setOpen, onDelete, text, isDeleting }: any) => - open ? ( -
    - {text} - - -
    - ) : null, -})); -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string, params?: any) => (params?.projectName ? `${key} ${params.projectName}` : key), - }), -})); - -const mockPush = vi.fn(); -vi.mock("next/navigation", () => ({ - useRouter: () => ({ push: mockPush }), -})); - -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(() => "error-message"), -})); -vi.mock("@/lib/utils/strings", () => ({ - truncate: (str: string) => str, -})); - -const mockDeleteProjectAction = vi.fn(); -vi.mock("@/modules/projects/settings/general/actions", () => ({ - deleteProjectAction: (...args: any[]) => mockDeleteProjectAction(...args), -})); - -const mockLocalStorage = { - removeItem: vi.fn(), - setItem: vi.fn(), -}; -global.localStorage = mockLocalStorage as any; - -const baseProject: TProject = { - id: "p1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Project 1", - organizationId: "org1", - styling: { allowStyleOverwrite: true }, - recontactDays: 0, - inAppSurveyBranding: false, - linkSurveyBranding: false, - config: { channel: null, industry: null }, - placement: "bottomRight", - clickOutsideClose: false, - darkOverlay: false, - environments: [ - { - id: "env1", - type: "production", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "p1", - appSetupCompleted: false, - }, - ], - languages: [], - logo: null, -}; - -describe("DeleteProjectRender", () => { - afterEach(() => { - cleanup(); - }); - - test("shows delete button and dialog when enabled", async () => { - render( - - ); - expect( - screen.getByText( - "environments.project.general.delete_project_name_includes_surveys_responses_people_and_more Project 1" - ) - ).toBeInTheDocument(); - expect(screen.getByText("environments.project.general.this_action_cannot_be_undone")).toBeInTheDocument(); - const deleteBtn = screen.getByText("common.delete"); - expect(deleteBtn).toBeInTheDocument(); - await userEvent.click(deleteBtn); - expect(screen.getByTestId("delete-dialog")).toBeInTheDocument(); - }); - - test("shows alert if delete is disabled and not owner/manager", () => { - render( - - ); - expect(screen.getByTestId("alert")).toBeInTheDocument(); - expect(screen.getByTestId("alert-description")).toHaveTextContent( - "environments.project.general.only_owners_or_managers_can_delete_projects" - ); - }); - - test("shows alert if delete is disabled and is owner/manager", () => { - render( - - ); - expect(screen.getByTestId("alert-description")).toHaveTextContent( - "environments.project.general.cannot_delete_only_project" - ); - }); - - test("successful delete with one project removes env id and redirects", async () => { - mockDeleteProjectAction.mockResolvedValue({ data: true }); - render( - - ); - await userEvent.click(screen.getByText("common.delete")); - await userEvent.click(screen.getByTestId("confirm-delete")); - expect(mockLocalStorage.removeItem).toHaveBeenCalled(); - expect(toast.success).toHaveBeenCalledWith("environments.project.general.project_deleted_successfully"); - expect(mockPush).toHaveBeenCalledWith("/"); - }); - - test("successful delete with multiple projects sets env id and redirects", async () => { - const otherProject: TProject = { - ...baseProject, - id: "p2", - environments: [{ ...baseProject.environments[0], id: "env2" }], - }; - mockDeleteProjectAction.mockResolvedValue({ data: true }); - render( - - ); - await userEvent.click(screen.getByText("common.delete")); - await userEvent.click(screen.getByTestId("confirm-delete")); - expect(mockLocalStorage.setItem).toHaveBeenCalledWith("formbricks-environment-id", "env2"); - expect(toast.success).toHaveBeenCalledWith("environments.project.general.project_deleted_successfully"); - expect(mockPush).toHaveBeenCalledWith("/"); - }); - - test("delete error shows error toast and closes dialog", async () => { - mockDeleteProjectAction.mockResolvedValue({ data: false }); - render( - - ); - await userEvent.click(screen.getByText("common.delete")); - await userEvent.click(screen.getByTestId("confirm-delete")); - expect(toast.error).toHaveBeenCalledWith("error-message"); - expect(screen.queryByTestId("delete-dialog")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/general/components/delete-project.test.tsx b/apps/web/modules/projects/settings/general/components/delete-project.test.tsx deleted file mode 100644 index ed8d116b9a..0000000000 --- a/apps/web/modules/projects/settings/general/components/delete-project.test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { getServerSession } from "next-auth"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TProject } from "@formbricks/types/project"; -import { getOrganizationByEnvironmentId } from "@/lib/organization/service"; -import { getUserProjects } from "@/lib/project/service"; -import { DeleteProject } from "./delete-project"; - -vi.mock("@/modules/projects/settings/general/components/delete-project-render", () => ({ - DeleteProjectRender: (props: any) => ( -
    -

    isDeleteDisabled: {String(props.isDeleteDisabled)}

    -

    isOwnerOrManager: {String(props.isOwnerOrManager)}

    -
    - ), -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -const mockProject = { - id: "proj-1", - name: "Project 1", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "org-1", - environments: [], -} as any; - -const mockOrganization = { - id: "org-1", - name: "Org 1", - createdAt: new Date(), - updatedAt: new Date(), - billing: { plan: "free" } as any, -} as any; - -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(() => { - // Return a mock translator that just returns the key - return (key: string) => key; - }), -})); -vi.mock("@/modules/auth/lib/authOptions", () => ({ - authOptions: {}, -})); -vi.mock("@/lib/organization/service", () => ({ - getOrganizationByEnvironmentId: vi.fn(), -})); -vi.mock("@/lib/project/service", () => ({ - getUserProjects: vi.fn(), -})); - -describe("/modules/projects/settings/general/components/delete-project.tsx", () => { - beforeEach(() => { - vi.mocked(getServerSession).mockResolvedValue({ - expires: new Date(Date.now() + 3600 * 1000).toISOString(), - user: { id: "user1" }, - }); - vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization); - vi.mocked(getUserProjects).mockResolvedValue([mockProject, { ...mockProject, id: "proj-2" }]); - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders DeleteProjectRender with correct props when delete is enabled", async () => { - const result = await DeleteProject({ - environmentId: "env-1", - currentProject: mockProject, - organizationProjects: [mockProject, { ...mockProject, id: "proj-2" }], - isOwnerOrManager: true, - }); - render(result); - const el = screen.getByTestId("delete-project-render"); - expect(el).toBeInTheDocument(); - expect(screen.getByText("isDeleteDisabled: false")).toBeInTheDocument(); - expect(screen.getByText("isOwnerOrManager: true")).toBeInTheDocument(); - }); - - test("renders DeleteProjectRender with delete disabled if only one project", async () => { - vi.mocked(getUserProjects).mockResolvedValue([mockProject]); - const result = await DeleteProject({ - environmentId: "env-1", - currentProject: mockProject, - organizationProjects: [mockProject], - isOwnerOrManager: true, - }); - render(result); - const el = screen.getByTestId("delete-project-render"); - expect(el).toBeInTheDocument(); - expect(screen.getByText("isDeleteDisabled: true")).toBeInTheDocument(); - }); - - test("renders DeleteProjectRender with delete disabled if not owner or manager", async () => { - vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization); - vi.mocked(getUserProjects).mockResolvedValue([mockProject, { ...mockProject, id: "proj-2" }]); - const result = await DeleteProject({ - environmentId: "env-1", - currentProject: mockProject, - organizationProjects: [mockProject, { ...mockProject, id: "proj-2" }], - isOwnerOrManager: false, - }); - render(result); - const el = screen.getByTestId("delete-project-render"); - expect(el).toBeInTheDocument(); - expect(screen.getByText("isDeleteDisabled: true")).toBeInTheDocument(); - expect(screen.getByText("isOwnerOrManager: false")).toBeInTheDocument(); - }); - - test("throws error if session is missing", async () => { - vi.mocked(getServerSession).mockResolvedValue(null); - await expect( - DeleteProject({ - environmentId: "env-1", - currentProject: mockProject, - organizationProjects: [mockProject], - isOwnerOrManager: true, - }) - ).rejects.toThrow("common.session_not_found"); - }); - - test("throws error if organization is missing", async () => { - vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-1" } }); - vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null); - await expect( - DeleteProject({ - environmentId: "env-1", - currentProject: mockProject, - organizationProjects: [mockProject], - isOwnerOrManager: true, - }) - ).rejects.toThrow("common.organization_not_found"); - }); -}); diff --git a/apps/web/modules/projects/settings/general/components/edit-project-name-form.test.tsx b/apps/web/modules/projects/settings/general/components/edit-project-name-form.test.tsx deleted file mode 100644 index a4bf74bc6c..0000000000 --- a/apps/web/modules/projects/settings/general/components/edit-project-name-form.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { anyString } from "vitest-mock-extended"; -import { TProject } from "@formbricks/types/project"; -import { EditProjectNameForm } from "./edit-project-name-form"; - -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children }: any) =>
    {children}
    , - AlertDescription: ({ children }: any) =>
    {children}
    , -})); - -const mockUpdateProjectAction = vi.fn(); -vi.mock("@/modules/projects/settings/actions", () => ({ - updateProjectAction: (...args: any[]) => mockUpdateProjectAction(...args), -})); -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(() => "error-message"), -})); - -const baseProject: TProject = { - id: "p1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Project 1", - organizationId: "org1", - styling: { allowStyleOverwrite: true }, - recontactDays: 0, - inAppSurveyBranding: false, - linkSurveyBranding: false, - config: { channel: null, industry: null }, - placement: "bottomRight", - clickOutsideClose: false, - darkOverlay: false, - environments: [ - { - id: "env1", - type: "production", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "p1", - appSetupCompleted: false, - }, - ], - languages: [], - logo: null, -}; - -describe("EditProjectNameForm", () => { - afterEach(() => { - cleanup(); - }); - - test("renders form with project name and update button", () => { - render(); - expect( - screen.getByLabelText("environments.project.general.whats_your_project_called") - ).toBeInTheDocument(); - expect(screen.getByPlaceholderText("common.project_name")).toHaveValue("Project 1"); - expect(screen.getByText("common.update")).toBeInTheDocument(); - }); - - test("shows warning alert if isReadOnly", () => { - render(); - expect(screen.getByTestId("alert")).toBeInTheDocument(); - expect(screen.getByTestId("alert-description")).toHaveTextContent( - "common.only_owners_managers_and_manage_access_members_can_perform_this_action" - ); - expect( - screen.getByLabelText("environments.project.general.whats_your_project_called") - ).toBeInTheDocument(); - expect(screen.getByPlaceholderText("common.project_name")).toBeDisabled(); - expect(screen.getByText("common.update")).toBeDisabled(); - }); - - test("calls updateProjectAction and shows success toast on valid submit", async () => { - mockUpdateProjectAction.mockResolvedValue({ data: { name: "New Name" } }); - render(); - const input = screen.getByPlaceholderText("common.project_name"); - await userEvent.clear(input); - await userEvent.type(input, "New Name"); - await userEvent.click(screen.getByText("common.update")); - expect(mockUpdateProjectAction).toHaveBeenCalledWith({ projectId: "p1", data: { name: "New Name" } }); - expect(toast.success).toHaveBeenCalled(); - }); - - test("shows error toast if updateProjectAction returns no data", async () => { - mockUpdateProjectAction.mockResolvedValue({ data: null }); - render(); - const input = screen.getByPlaceholderText("common.project_name"); - await userEvent.clear(input); - await userEvent.type(input, "Another Name"); - await userEvent.click(screen.getByText("common.update")); - expect(toast.error).toHaveBeenCalledWith(anyString()); - }); - - test("shows error toast if updateProjectAction throws", async () => { - mockUpdateProjectAction.mockRejectedValue(new Error("fail")); - render(); - const input = screen.getByPlaceholderText("common.project_name"); - await userEvent.clear(input); - await userEvent.type(input, "Error Name"); - await userEvent.click(screen.getByText("common.update")); - expect(toast.error).toHaveBeenCalledWith("environments.project.general.error_saving_project_information"); - }); -}); diff --git a/apps/web/modules/projects/settings/general/components/edit-waiting-time-form.test.tsx b/apps/web/modules/projects/settings/general/components/edit-waiting-time-form.test.tsx deleted file mode 100644 index 7bbb63bc6e..0000000000 --- a/apps/web/modules/projects/settings/general/components/edit-waiting-time-form.test.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TProject } from "@formbricks/types/project"; -import { EditWaitingTimeForm } from "./edit-waiting-time-form"; - -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children }: any) =>
    {children}
    , - AlertDescription: ({ children }: any) =>
    {children}
    , -})); - -const mockUpdateProjectAction = vi.fn(); -vi.mock("../../actions", () => ({ - updateProjectAction: (...args: any[]) => mockUpdateProjectAction(...args), -})); -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(() => "error-message"), -})); - -const baseProject: TProject = { - id: "p1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Project 1", - organizationId: "org1", - styling: { allowStyleOverwrite: true }, - recontactDays: 7, - inAppSurveyBranding: false, - linkSurveyBranding: false, - config: { channel: null, industry: null }, - placement: "bottomRight", - clickOutsideClose: false, - darkOverlay: false, - environments: [ - { - id: "env1", - type: "production", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "p1", - appSetupCompleted: false, - }, - ], - languages: [], - logo: null, -}; - -describe("EditWaitingTimeForm", () => { - afterEach(() => { - cleanup(); - }); - - test("renders form with current waiting time and update button", () => { - render(); - expect( - screen.getByLabelText("environments.project.general.wait_x_days_before_showing_next_survey") - ).toBeInTheDocument(); - expect(screen.getByDisplayValue("7")).toBeInTheDocument(); - expect(screen.getByText("common.update")).toBeInTheDocument(); - }); - - test("shows warning alert and disables input/button if isReadOnly", () => { - render(); - expect(screen.getByTestId("alert")).toBeInTheDocument(); - expect(screen.getByTestId("alert-description")).toHaveTextContent( - "common.only_owners_managers_and_manage_access_members_can_perform_this_action" - ); - expect( - screen.getByLabelText("environments.project.general.wait_x_days_before_showing_next_survey") - ).toBeInTheDocument(); - expect(screen.getByDisplayValue("7")).toBeDisabled(); - expect(screen.getByText("common.update")).toBeDisabled(); - }); - - test("calls updateProjectAction and shows success toast on valid submit", async () => { - mockUpdateProjectAction.mockResolvedValue({ data: { recontactDays: 10 } }); - render(); - const input = screen.getByLabelText( - "environments.project.general.wait_x_days_before_showing_next_survey" - ); - await userEvent.clear(input); - await userEvent.type(input, "10"); - await userEvent.click(screen.getByText("common.update")); - expect(mockUpdateProjectAction).toHaveBeenCalledWith({ projectId: "p1", data: { recontactDays: 10 } }); - expect(toast.success).toHaveBeenCalledWith( - "environments.project.general.waiting_period_updated_successfully" - ); - }); - - test("shows error toast if updateProjectAction returns no data", async () => { - mockUpdateProjectAction.mockResolvedValue({ data: null }); - render(); - const input = screen.getByLabelText( - "environments.project.general.wait_x_days_before_showing_next_survey" - ); - await userEvent.clear(input); - await userEvent.type(input, "5"); - await userEvent.click(screen.getByText("common.update")); - expect(toast.error).toHaveBeenCalledWith("error-message"); - }); - - test("shows error toast if updateProjectAction throws", async () => { - mockUpdateProjectAction.mockRejectedValue(new Error("fail")); - render(); - const input = screen.getByLabelText( - "environments.project.general.wait_x_days_before_showing_next_survey" - ); - await userEvent.clear(input); - await userEvent.type(input, "3"); - await userEvent.click(screen.getByText("common.update")); - expect(toast.error).toHaveBeenCalledWith("Error: fail"); - }); -}); diff --git a/apps/web/modules/projects/settings/general/loading.test.tsx b/apps/web/modules/projects/settings/general/loading.test.tsx deleted file mode 100644 index deab26f263..0000000000 --- a/apps/web/modules/projects/settings/general/loading.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { GeneralSettingsLoading } from "./loading"; - -vi.mock("@/modules/projects/settings/components/project-config-navigation", () => ({ - ProjectConfigNavigation: (props: any) =>
    , -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }: any) => ( -
    -
    {pageTitle}
    - {children} -
    - ), -})); -vi.mock("@/app/(app)/components/LoadingCard", () => ({ - LoadingCard: (props: any) => ( -
    -

    {props.title}

    -

    {props.description}

    -
    - ), -})); - -describe("GeneralSettingsLoading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all tolgee strings and main UI elements", () => { - render(); - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("project-config-navigation")).toBeInTheDocument(); - expect(screen.getAllByTestId("loading-card").length).toBe(3); - expect(screen.getByText("common.project_configuration")).toBeInTheDocument(); - expect(screen.getByText("common.project_name")).toBeInTheDocument(); - expect( - screen.getByText("environments.project.general.project_name_settings_description") - ).toBeInTheDocument(); - expect(screen.getByText("environments.project.general.recontact_waiting_time")).toBeInTheDocument(); - expect( - screen.getByText("environments.project.general.recontact_waiting_time_settings_description") - ).toBeInTheDocument(); - expect(screen.getByText("environments.project.general.delete_project")).toBeInTheDocument(); - expect( - screen.getByText("environments.project.general.delete_project_settings_description") - ).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/general/page.test.tsx b/apps/web/modules/projects/settings/general/page.test.tsx deleted file mode 100644 index 2af36f6129..0000000000 --- a/apps/web/modules/projects/settings/general/page.test.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { getProjects } from "@/lib/project/service"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { GeneralSettingsPage } from "./page"; - -vi.mock("@/modules/projects/settings/components/project-config-navigation", () => ({ - ProjectConfigNavigation: (props: any) =>
    , -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }: any) => ( -
    -
    {pageTitle}
    - {children} -
    - ), -})); -vi.mock("@/modules/ui/components/id-badge", () => ({ - IdBadge: ({ id, label, variant }: any) => ( -
    - {label} - {id} -
    - ), -})); -vi.mock("./components/edit-project-name-form", () => ({ - EditProjectNameForm: (props: any) =>
    {props.project.id}
    , -})); -vi.mock("./components/edit-waiting-time-form", () => ({ - EditWaitingTimeForm: (props: any) =>
    {props.project.id}
    , -})); -vi.mock("./components/delete-project", () => ({ - DeleteProject: (props: any) =>
    {props.environmentId}
    , -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(() => { - // Return a mock translator that just returns the key - return (key: string) => key; - }), -})); -const mockProject = { - id: "proj-1", - name: "Project 1", - createdAt: new Date(), - updatedAt: new Date(), - organizationId: "org-1", - environments: [], -} as any; - -const mockOrganization: TOrganization = { - id: "org-1", - name: "Org 1", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - plan: "free", - limits: { monthly: { miu: 10, responses: 10 }, projects: 4 }, - period: "monthly", - periodStart: new Date(), - stripeCustomerId: null, - }, - isAIEnabled: false, -}; - -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); -vi.mock("@/lib/project/service", () => ({ - getProjects: vi.fn(), -})); -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_DEVELOPMENT: false, -})); -vi.mock("@/package.json", () => ({ - default: { - version: "1.2.3", - }, -})); - -describe("GeneralSettingsPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all tolgee strings and main UI elements", async () => { - const props = { params: { environmentId: "env1" } } as any; - - vi.mocked(getProjects).mockResolvedValue([mockProject]); - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - isReadOnly: false, - isOwner: true, - isManager: false, - project: mockProject, - organization: mockOrganization, - } as any); - - const Page = await GeneralSettingsPage(props); - render(Page); - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("project-config-navigation")).toBeInTheDocument(); - expect(screen.getAllByTestId("id-badge").length).toBe(2); - expect(screen.getByTestId("edit-project-name-form")).toBeInTheDocument(); - expect(screen.getByTestId("edit-waiting-time-form")).toBeInTheDocument(); - expect(screen.getByTestId("delete-project")).toBeInTheDocument(); - expect(screen.getByText("common.project_configuration")).toBeInTheDocument(); - expect(screen.getByText("common.project_name")).toBeInTheDocument(); - expect( - screen.getByText("environments.project.general.project_name_settings_description") - ).toBeInTheDocument(); - expect(screen.getByText("environments.project.general.recontact_waiting_time")).toBeInTheDocument(); - expect( - screen.getByText("environments.project.general.recontact_waiting_time_settings_description") - ).toBeInTheDocument(); - expect(screen.getByText("environments.project.general.delete_project")).toBeInTheDocument(); - expect( - screen.getByText("environments.project.general.delete_project_settings_description") - ).toBeInTheDocument(); - expect(screen.getByText("common.project_id")).toBeInTheDocument(); - expect(screen.getByText("common.formbricks_version")).toBeInTheDocument(); - expect(screen.getByText("1.2.3")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/layout.test.tsx b/apps/web/modules/projects/settings/layout.test.tsx deleted file mode 100644 index 991f7f284a..0000000000 --- a/apps/web/modules/projects/settings/layout.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { cleanup } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; -import { ProjectSettingsLayout } from "./layout"; - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -describe("ProjectSettingsLayout", () => { - afterEach(() => { - cleanup(); - }); - - test("redirects to billing if isBilling is true", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ isBilling: true } as TEnvironmentAuth); - const props = { params: { environmentId: "env-1" }, children:
    child
    }; - await ProjectSettingsLayout(props); - expect(vi.mocked(redirect)).toHaveBeenCalledWith("/environments/env-1/settings/billing"); - }); - - test("renders children if isBilling is false", async () => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ isBilling: false } as TEnvironmentAuth); - const props = { params: { environmentId: "env-2" }, children:
    child
    }; - const result = await ProjectSettingsLayout(props); - expect(result).toEqual(
    child
    ); - expect(vi.mocked(redirect)).not.toHaveBeenCalled(); - }); - - test("throws error if getEnvironmentAuth throws", async () => { - const error = new Error("fail"); - vi.mocked(getEnvironmentAuth).mockRejectedValue(error); - const props = { params: { environmentId: "env-3" }, children:
    child
    }; - await expect(ProjectSettingsLayout(props)).rejects.toThrow(error); - }); -}); diff --git a/apps/web/modules/projects/settings/look/components/edit-logo.test.tsx b/apps/web/modules/projects/settings/look/components/edit-logo.test.tsx deleted file mode 100644 index 5911874341..0000000000 --- a/apps/web/modules/projects/settings/look/components/edit-logo.test.tsx +++ /dev/null @@ -1,422 +0,0 @@ -import { Project } from "@prisma/client"; -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { EditLogo } from "./edit-logo"; - -// Mock next/image to render a plain img tag with original src -vi.mock("next/image", () => ({ - __esModule: true, - default: ({ src, alt, ...props }: any) => {alt}, -})); - -// Hoisted mocks -const h = vi.hoisted(() => ({ - mockHandleFileUpload: vi.fn(), - mockUpdateProjectAction: vi.fn(), - mockShowStorageNotConfiguredToast: vi.fn(), - mockToastSuccess: vi.fn(), - mockToastError: vi.fn(), -})); - -// Mocks -vi.mock("@/modules/storage/file-upload", () => ({ - handleFileUpload: h.mockHandleFileUpload, -})); - -vi.mock("@/modules/projects/settings/actions", () => ({ - updateProjectAction: h.mockUpdateProjectAction, -})); - -vi.mock("@/modules/ui/components/storage-not-configured-toast/lib/utils", () => ({ - showStorageNotConfiguredToast: h.mockShowStorageNotConfiguredToast, -})); - -vi.mock("react-hot-toast", () => ({ - default: { - success: h.mockToastSuccess, - error: h.mockToastError, - }, -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ t: (k: string) => k }), -})); - -vi.mock("@/modules/ui/components/file-input", () => ({ - FileInput: vi.fn((props: any) => ( -
    - -
    - )), -})); - -vi.mock("@/modules/ui/components/advanced-option-toggle", () => ({ - AdvancedOptionToggle: vi.fn((props: any) => ( -
    - props.onToggle(e.target.checked)} - disabled={props.disabled} - data-testid="background-color-toggle" - /> - {props.isChecked && props.children} -
    - )), -})); - -vi.mock("@/modules/ui/components/color-picker", () => ({ - ColorPicker: vi.fn((props: any) => ( - props.onChange(e.target.value)} - disabled={props.disabled} - data-testid="color-picker" - /> - )), -})); - -vi.mock("@/modules/ui/components/delete-dialog", () => ({ - DeleteDialog: vi.fn((props: any) => ( -
    - - -
    - )), -})); - -describe("EditLogo", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockProject = { - id: "project-123", - logo: null, - } as Project; - - const baseProps = { - project: mockProject, - environmentId: "env-123", - isReadOnly: false, - isStorageConfigured: true, - }; - - test("renders FileInput when no logo exists", () => { - render(); - expect(screen.getByTestId("file-input")).toBeInTheDocument(); - expect(screen.getByTestId("file-input-upload-button")).toBeInTheDocument(); - }); - - test("renders logo image when logo exists", () => { - const projectWithLogo = { - ...mockProject, - logo: { url: "https://example.com/logo.png", bgColor: "#ffffff" }, - } as Project; - - render(); - - const logoImage = screen.getByAltText("Logo"); - expect(logoImage).toBeInTheDocument(); - expect(logoImage.getAttribute("src")).toBe("https://example.com/logo.png"); - }); - - test("shows read-only warning when isReadOnly is true", () => { - render(); - expect( - screen.getByText("common.only_owners_managers_and_manage_access_members_can_perform_this_action") - ).toBeInTheDocument(); - }); - - test("uploads file via FileInput and enters editing mode", async () => { - render(); - const user = userEvent.setup(); - - const uploadButton = screen.getByTestId("file-input-upload-button"); - await user.click(uploadButton); - - // Should show the uploaded logo - await waitFor(() => { - expect(screen.getByAltText("Logo")).toBeInTheDocument(); - }); - - // Should show editing controls - expect(screen.getByText("environments.project.look.replace_logo")).toBeInTheDocument(); - expect(screen.getByText("environments.project.look.remove_logo")).toBeInTheDocument(); - expect(screen.getByText("common.save")).toBeInTheDocument(); - }); - - test("isStorageConfigured=false shows toast on file change", async () => { - render(); - const user = userEvent.setup(); - - // Get the hidden file input - const fileInput = screen.getByDisplayValue("") as HTMLInputElement; - const file = new File(["dummy"], "logo.png", { type: "image/png" }); - - await user.upload(fileInput, file); - - expect(h.mockShowStorageNotConfiguredToast).toHaveBeenCalled(); - expect(h.mockHandleFileUpload).not.toHaveBeenCalled(); - }); - - test("isStorageConfigured=false shows toast on replace logo button click", async () => { - const projectWithLogo = { - ...mockProject, - logo: { url: "https://example.com/logo.png" }, - } as Project; - - render(); - const user = userEvent.setup(); - - // First click edit to get into editing mode - const editButton = screen.getByText("common.edit"); - await user.click(editButton); - - // Click replace logo button - const replaceButton = screen.getByText("environments.project.look.replace_logo"); - await user.click(replaceButton); - - expect(h.mockShowStorageNotConfiguredToast).toHaveBeenCalled(); - }); - - test("uploads file via hidden input when storage is configured", async () => { - h.mockHandleFileUpload.mockResolvedValue({ url: "https://example.com/new-logo.png" }); - - render(); - const user = userEvent.setup(); - - const fileInput = screen.getByDisplayValue("") as HTMLInputElement; - const file = new File(["dummy"], "logo.png", { type: "image/png" }); - - await user.upload(fileInput, file); - - expect(h.mockHandleFileUpload).toHaveBeenCalledWith(file, "env-123"); - await waitFor(() => { - expect(screen.getByAltText("Logo")).toBeInTheDocument(); - }); - }); - - test("handles file upload error", async () => { - h.mockHandleFileUpload.mockResolvedValue({ error: "Upload failed" }); - - render(); - const user = userEvent.setup(); - - const fileInput = screen.getByDisplayValue("") as HTMLInputElement; - const file = new File(["dummy"], "logo.png", { type: "image/png" }); - - await user.upload(fileInput, file); - - expect(h.mockToastError).toHaveBeenCalledWith("Upload failed"); - }); - - test("saves changes successfully", async () => { - h.mockUpdateProjectAction.mockResolvedValue({ data: true }); - - const projectWithLogo = { - ...mockProject, - logo: { url: "https://example.com/logo.png" }, - } as Project; - - render(); - const user = userEvent.setup(); - - // Click edit to enter editing mode - const editButton = screen.getByText("common.edit"); - await user.click(editButton); - - // Click save - const saveButton = screen.getByText("common.save"); - await user.click(saveButton); - - expect(h.mockUpdateProjectAction).toHaveBeenCalledWith({ - projectId: "project-123", - data: { - logo: { url: "https://example.com/logo.png", bgColor: undefined }, - }, - }); - expect(h.mockToastSuccess).toHaveBeenCalledWith("environments.project.look.logo_updated_successfully"); - }); - - test("handles save error", async () => { - h.mockUpdateProjectAction.mockResolvedValue({ error: "Save failed" }); - - const projectWithLogo = { - ...mockProject, - logo: { url: "https://example.com/logo.png" }, - } as Project; - - render(); - const user = userEvent.setup(); - - // Click edit to enter editing mode - const editButton = screen.getByText("common.edit"); - await user.click(editButton); - - // Click save - const saveButton = screen.getByText("common.save"); - await user.click(saveButton); - - expect(h.mockToastError).toHaveBeenCalled(); - }); - - test("removes logo successfully", async () => { - h.mockUpdateProjectAction.mockResolvedValue({ data: true }); - - const projectWithLogo = { - ...mockProject, - logo: { url: "https://example.com/logo.png" }, - } as Project; - - render(); - const user = userEvent.setup(); - - // Click edit to enter editing mode - const editButton = screen.getByText("common.edit"); - await user.click(editButton); - - // Click remove logo - const removeButton = screen.getByText("environments.project.look.remove_logo"); - await user.click(removeButton); - - // Confirm deletion - const confirmButton = screen.getByTestId("confirm-delete-button"); - await user.click(confirmButton); - - expect(h.mockUpdateProjectAction).toHaveBeenCalledWith({ - projectId: "project-123", - data: { - logo: { url: undefined, bgColor: undefined }, - }, - }); - expect(h.mockToastSuccess).toHaveBeenCalledWith("environments.project.look.logo_removed_successfully"); - }); - - test("toggles background color and updates color picker", async () => { - const projectWithLogo = { - ...mockProject, - logo: { url: "https://example.com/logo.png" }, - } as Project; - - render(); - const user = userEvent.setup(); - - // Click edit to enter editing mode - const editButton = screen.getByText("common.edit"); - await user.click(editButton); - - // Toggle background color on - const bgColorToggle = screen.getByTestId("background-color-toggle"); - await user.click(bgColorToggle); - - // Color picker should appear - const colorPicker = screen.getByTestId("color-picker"); - expect(colorPicker).toBeInTheDocument(); - expect(colorPicker).toHaveValue("#f8f8f8"); - - // Change color - await user.click(colorPicker); - await user.keyboard("#ff0000"); - - // Toggle background color off - await user.click(bgColorToggle); - - // Color picker should disappear - expect(screen.queryByTestId("color-picker")).not.toBeInTheDocument(); - }); - - test("disables controls when isReadOnly is true", () => { - const projectWithLogo = { - ...mockProject, - logo: { url: "https://example.com/logo.png" }, - } as Project; - - render(); - - const fileInput = screen.getByDisplayValue("") as HTMLInputElement; - expect(fileInput).toBeDisabled(); - - // Edit button should be disabled - const editButton = screen.getByText("common.edit"); - expect(editButton).toBeDisabled(); - }); - - test("edit button switches to editing mode", async () => { - const projectWithLogo = { - ...mockProject, - logo: { url: "https://example.com/logo.png" }, - } as Project; - - render(); - const user = userEvent.setup(); - - // Initially shows "Edit" button - const editButton = screen.getByText("common.edit"); - await user.click(editButton); - - // Should show editing controls - expect(screen.getByText("environments.project.look.replace_logo")).toBeInTheDocument(); - expect(screen.getByText("environments.project.look.remove_logo")).toBeInTheDocument(); - expect(screen.getByText("common.save")).toBeInTheDocument(); - }); - - test("passes isStorageConfigured to FileInput", () => { - const { rerender } = render(); - - // Check that FileInput is rendered (only when no logo exists) - expect(screen.getByTestId("file-input")).toBeInTheDocument(); - - // Test with isStorageConfigured=false - rerender(); - expect(screen.getByTestId("file-input")).toBeInTheDocument(); - }); - - test("saves logo with background color when enabled", async () => { - h.mockUpdateProjectAction.mockResolvedValue({ data: true }); - - const projectWithLogo = { - ...mockProject, - logo: { url: "https://example.com/logo.png", bgColor: "#ffffff" }, - } as Project; - - render(); - const user = userEvent.setup(); - - // Click edit to enter editing mode - const editButton = screen.getByText("common.edit"); - await user.click(editButton); - - // Background color should already be enabled since project has bgColor - const bgColorToggle = screen.getByTestId("background-color-toggle"); - expect(bgColorToggle).toBeChecked(); - - // Color picker should be visible with existing color - const colorPicker = screen.getByTestId("color-picker"); - expect(colorPicker).toHaveValue("#ffffff"); - - // Save without changing color to test the existing behavior - const saveButton = screen.getByText("common.save"); - await user.click(saveButton); - - expect(h.mockUpdateProjectAction).toHaveBeenCalledWith({ - projectId: "project-123", - data: { - logo: { url: "https://example.com/logo.png", bgColor: "#ffffff" }, - }, - }); - }); -}); diff --git a/apps/web/modules/projects/settings/look/components/edit-placement-form.test.tsx b/apps/web/modules/projects/settings/look/components/edit-placement-form.test.tsx deleted file mode 100644 index 27b2db3cac..0000000000 --- a/apps/web/modules/projects/settings/look/components/edit-placement-form.test.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Project } from "@prisma/client"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { updateProjectAction } from "@/modules/projects/settings/actions"; -import { EditPlacementForm } from "./edit-placement-form"; - -const baseProject: Project = { - id: "p1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Project 1", - organizationId: "org1", - styling: { allowStyleOverwrite: true }, - recontactDays: 0, - inAppSurveyBranding: false, - linkSurveyBranding: false, - config: { channel: null, industry: null }, - placement: "bottomRight", - clickOutsideClose: false, - darkOverlay: false, - environments: [], - languages: [], - logo: null, -} as any; - -vi.mock("@/modules/projects/settings/actions", () => ({ - updateProjectAction: vi.fn(), -})); -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(), -})); - -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children }: any) =>
    {children}
    , - AlertDescription: ({ children }: any) =>
    {children}
    , -})); - -describe("EditPlacementForm", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all placement radio buttons and save button", () => { - render(); - expect(screen.getByText("common.save")).toBeInTheDocument(); - expect(screen.getByLabelText("common.bottom_right")).toBeInTheDocument(); - expect(screen.getByLabelText("common.top_right")).toBeInTheDocument(); - expect(screen.getByLabelText("common.top_left")).toBeInTheDocument(); - expect(screen.getByLabelText("common.bottom_left")).toBeInTheDocument(); - expect(screen.getByLabelText("common.centered_modal")).toBeInTheDocument(); - }); - - test("submits form and shows success toast", async () => { - render(); - await userEvent.click(screen.getByText("common.save")); - expect(updateProjectAction).toHaveBeenCalled(); - }); - - test("shows error toast if updateProjectAction returns no data", async () => { - vi.mocked(updateProjectAction).mockResolvedValueOnce({ data: false } as any); - render(); - await userEvent.click(screen.getByText("common.save")); - expect(getFormattedErrorMessage).toHaveBeenCalled(); - }); - - test("shows error toast if updateProjectAction throws", async () => { - vi.mocked(updateProjectAction).mockResolvedValueOnce({ data: false } as any); - vi.mocked(getFormattedErrorMessage).mockReturnValueOnce("error"); - render(); - await userEvent.click(screen.getByText("common.save")); - expect(toast.error).toHaveBeenCalledWith("error"); - }); - - test("renders overlay and disables save when isReadOnly", () => { - render(); - expect(screen.getByTestId("alert")).toBeInTheDocument(); - expect(screen.getByTestId("alert-description")).toHaveTextContent( - "common.only_owners_managers_and_manage_access_members_can_perform_this_action" - ); - expect(screen.getByText("common.save")).toBeDisabled(); - }); - - test("shows darkOverlay and clickOutsideClose options for centered modal", async () => { - render( - - ); - expect(screen.getByLabelText("common.light_overlay")).toBeInTheDocument(); - expect(screen.getByLabelText("common.dark_overlay")).toBeInTheDocument(); - expect(screen.getByLabelText("common.disallow")).toBeInTheDocument(); - expect(screen.getByLabelText("common.allow")).toBeInTheDocument(); - }); - - test("changing placement to center shows overlay and clickOutsideClose options", async () => { - render(); - await userEvent.click(screen.getByLabelText("common.centered_modal")); - expect(screen.getByLabelText("common.light_overlay")).toBeInTheDocument(); - expect(screen.getByLabelText("common.dark_overlay")).toBeInTheDocument(); - expect(screen.getByLabelText("common.disallow")).toBeInTheDocument(); - expect(screen.getByLabelText("common.allow")).toBeInTheDocument(); - }); - - test("radio buttons are disabled when isReadOnly", () => { - render(); - expect(screen.getByLabelText("common.bottom_right")).toBeDisabled(); - expect(screen.getByLabelText("common.top_right")).toBeDisabled(); - expect(screen.getByLabelText("common.top_left")).toBeDisabled(); - expect(screen.getByLabelText("common.bottom_left")).toBeDisabled(); - expect(screen.getByLabelText("common.centered_modal")).toBeDisabled(); - }); -}); diff --git a/apps/web/modules/projects/settings/look/components/theme-styling.test.tsx b/apps/web/modules/projects/settings/look/components/theme-styling.test.tsx deleted file mode 100644 index 8ce63f62d9..0000000000 --- a/apps/web/modules/projects/settings/look/components/theme-styling.test.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { Project } from "@prisma/client"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { updateProjectAction } from "@/modules/projects/settings/actions"; -import { ThemeStyling } from "./theme-styling"; - -const baseProject: Project = { - id: "p1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Project 1", - organizationId: "org1", - styling: { allowStyleOverwrite: true }, - recontactDays: 0, - inAppSurveyBranding: false, - linkSurveyBranding: false, - config: { channel: null, industry: null }, - placement: "bottomRight", - clickOutsideClose: false, - darkOverlay: false, - environments: [], - languages: [], - logo: null, -} as any; - -const colors = ["#fff", "#000"]; - -const mockGetFormattedErrorMessage = vi.fn(() => "error-message"); -const mockRouter = { refresh: vi.fn() }; - -vi.mock("@/modules/projects/settings/actions", () => ({ - updateProjectAction: vi.fn(), -})); -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: () => mockGetFormattedErrorMessage(), -})); -vi.mock("next/navigation", () => ({ useRouter: () => mockRouter })); -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children }: any) =>
    {children}
    , - AlertDescription: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, ...props }: any) => , -})); - -vi.mock("@/modules/ui/components/switch", () => ({ - Switch: ({ checked, onCheckedChange }: any) => ( - onCheckedChange(e.target.checked)} /> - ), -})); -vi.mock("@/modules/ui/components/alert-dialog", () => ({ - AlertDialog: ({ open, onConfirm, onDecline, headerText, mainText, confirmBtnLabel }: any) => - open ? ( -
    -
    {headerText}
    -
    {mainText}
    - - -
    - ) : null, -})); -vi.mock("@/modules/ui/components/background-styling-card", () => ({ - BackgroundStylingCard: () =>
    , -})); -vi.mock("@/modules/ui/components/card-styling-settings", () => ({ - CardStylingSettings: () =>
    , -})); -vi.mock("@/modules/survey/editor/components/form-styling-settings", () => ({ - FormStylingSettings: () =>
    , -})); -vi.mock("@/modules/ui/components/theme-styling-preview-survey", () => ({ - ThemeStylingPreviewSurvey: () =>
    , -})); -vi.mock("@/app/lib/templates", () => ({ previewSurvey: () => ({}) })); -vi.mock("@/lib/styling/constants", () => ({ defaultStyling: { allowStyleOverwrite: false } })); - -describe("ThemeStyling", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all main sections and save/reset buttons", () => { - render( - - ); - expect(screen.getByTestId("form-styling-settings")).toBeInTheDocument(); - expect(screen.getByTestId("card-styling-settings")).toBeInTheDocument(); - expect(screen.getByTestId("background-styling-card")).toBeInTheDocument(); - expect(screen.getByTestId("theme-styling-preview-survey")).toBeInTheDocument(); - expect(screen.getByText("common.save")).toBeInTheDocument(); - expect(screen.getByText("common.reset_to_default")).toBeInTheDocument(); - }); - - test("submits form and shows success toast", async () => { - render( - - ); - await userEvent.click(screen.getByText("common.save")); - expect(updateProjectAction).toHaveBeenCalled(); - }); - - test("shows error toast if updateProjectAction returns no data on submit", async () => { - vi.mocked(updateProjectAction).mockResolvedValueOnce({}); - render( - - ); - await userEvent.click(screen.getByText("common.save")); - expect(mockGetFormattedErrorMessage).toHaveBeenCalled(); - }); - - test("shows error toast if updateProjectAction throws on submit", async () => { - vi.mocked(updateProjectAction).mockResolvedValueOnce({}); - render( - - ); - await userEvent.click(screen.getByText("common.save")); - expect(toast.error).toHaveBeenCalled(); - }); - - test("opens and confirms reset styling modal", async () => { - render( - - ); - await userEvent.click(screen.getByText("common.reset_to_default")); - expect(screen.getByTestId("alert-dialog")).toBeInTheDocument(); - await userEvent.click(screen.getByText("common.confirm")); - expect(updateProjectAction).toHaveBeenCalled(); - }); - - test("opens and cancels reset styling modal", async () => { - render( - - ); - await userEvent.click(screen.getByText("common.reset_to_default")); - expect(screen.getByTestId("alert-dialog")).toBeInTheDocument(); - await userEvent.click(screen.getByText("Cancel")); - expect(screen.queryByTestId("alert-dialog")).not.toBeInTheDocument(); - }); - - test("shows error toast if updateProjectAction returns no data on reset", async () => { - vi.mocked(updateProjectAction).mockResolvedValueOnce({}); - render( - - ); - await userEvent.click(screen.getByText("common.reset_to_default")); - await userEvent.click(screen.getByText("common.confirm")); - expect(mockGetFormattedErrorMessage).toHaveBeenCalled(); - }); - - test("renders alert if isReadOnly", () => { - render( - - ); - expect(screen.getByTestId("alert")).toBeInTheDocument(); - expect(screen.getByTestId("alert-description")).toHaveTextContent( - "common.only_owners_managers_and_manage_access_members_can_perform_this_action" - ); - }); -}); diff --git a/apps/web/modules/projects/settings/look/loading.test.tsx b/apps/web/modules/projects/settings/look/loading.test.tsx deleted file mode 100644 index f754f76f85..0000000000 --- a/apps/web/modules/projects/settings/look/loading.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ProjectLookSettingsLoading } from "./loading"; - -vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({ - SettingsCard: ({ children, ...props }: any) => ( -
    - {children} -
    - ), -})); -vi.mock("@/modules/projects/settings/components/project-config-navigation", () => ({ - ProjectConfigNavigation: (props: any) =>
    , -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }: any) => ( -
    -
    {pageTitle}
    - {children} -
    - ), -})); - -// Badge, Button, Label, RadioGroup, RadioGroupItem, Switch are simple enough, no need to mock - -describe("ProjectLookSettingsLoading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all tolgee strings and main UI elements", () => { - render(); - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("project-config-navigation")).toBeInTheDocument(); - expect(screen.getAllByTestId("settings-card").length).toBe(4); - expect(screen.getByText("common.project_configuration")).toBeInTheDocument(); - expect(screen.getByText("environments.project.look.enable_custom_styling")).toBeInTheDocument(); - expect( - screen.getByText("environments.project.look.enable_custom_styling_description") - ).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.edit.form_styling")).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.edit.style_the_question_texts_descriptions_and_input_fields") - ).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.edit.card_styling")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.edit.style_the_survey_card")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.edit.background_styling")).toBeInTheDocument(); - expect(screen.getByText("common.link_surveys")).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.edit.change_the_background_to_a_color_image_or_animation") - ).toBeInTheDocument(); - expect(screen.getAllByText("common.loading").length).toBeGreaterThanOrEqual(3); - expect(screen.getByText("common.preview")).toBeInTheDocument(); - expect(screen.getByText("common.restart")).toBeInTheDocument(); - expect(screen.getByText("environments.project.look.show_powered_by_formbricks")).toBeInTheDocument(); - expect(screen.getByText("common.bottom_right")).toBeInTheDocument(); - expect(screen.getByText("common.top_right")).toBeInTheDocument(); - expect(screen.getByText("common.top_left")).toBeInTheDocument(); - expect(screen.getByText("common.bottom_left")).toBeInTheDocument(); - expect(screen.getByText("common.centered_modal")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/look/page.test.tsx b/apps/web/modules/projects/settings/look/page.test.tsx deleted file mode 100644 index 9fa3bcd6b7..0000000000 --- a/apps/web/modules/projects/settings/look/page.test.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TOrganization } from "@formbricks/types/organizations"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { getProjectByEnvironmentId } from "@/modules/projects/settings/look/lib/project"; -import { EditLogo } from "./components/edit-logo"; -import { ThemeStyling } from "./components/theme-styling"; -import { ProjectLookSettingsPage } from "./page"; - -vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({ - SettingsCard: ({ children, ...props }: any) => ( -
    - {children} -
    - ), -})); - -vi.mock("@/lib/constants", () => ({ - SURVEY_BG_COLORS: ["#fff", "#000"], - IS_FORMBRICKS_CLOUD: 1, - IS_STORAGE_CONFIGURED: true, - UNSPLASH_ACCESS_KEY: "unsplash-key", -})); - -vi.mock("@/lib/cn", () => ({ - cn: (...classes: (string | boolean | undefined)[]) => classes.filter(Boolean).join(" "), -})); - -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: any) =>
    {children}
    , -})); - -vi.mock("@/modules/ee/license-check/lib/utils", async () => ({ - getWhiteLabelPermission: vi.fn(), -})); - -vi.mock("@/modules/ee/whitelabel/remove-branding/components/branding-settings-card", () => ({ - BrandingSettingsCard: () =>
    , -})); -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); - -vi.mock("@/modules/projects/settings/components/project-config-navigation", () => ({ - ProjectConfigNavigation: (props: any) =>
    , -})); - -vi.mock("./components/edit-logo", () => ({ - EditLogo: vi.fn(() =>
    ), -})); -vi.mock("@/modules/projects/settings/look/lib/project", async () => ({ - getProjectByEnvironmentId: vi.fn(), -})); - -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }: any) => ( -
    -
    {pageTitle}
    - {children} -
    - ), -})); - -vi.mock("@/modules/ui/components/alert", () => ({ - Alert: ({ children, variant }: any) => ( -
    - {children} -
    - ), - AlertDescription: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(() => { - // Return a mock translator that just returns the key - return (key: string) => key; - }), -})); -vi.mock("./components/edit-placement-form", () => ({ - EditPlacementForm: () =>
    , -})); -vi.mock("./components/theme-styling", () => ({ - ThemeStyling: vi.fn(() =>
    ), -})); - -describe("ProjectLookSettingsPage", () => { - const props = { params: Promise.resolve({ environmentId: "env1" }) }; - const mockOrg = { - id: "org1", - name: "Test Org", - createdAt: new Date(), - updatedAt: new Date(), - billing: { plan: "pro" } as any, - } as TOrganization; - - beforeEach(() => { - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - isReadOnly: false, - organization: mockOrg, - } as any); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders all tolgee strings and main UI elements", async () => { - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ - id: "project1", - name: "Test Project", - createdAt: new Date(), - updatedAt: new Date(), - environments: [], - } as any); - - const Page = await ProjectLookSettingsPage(props); - render(Page); - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("project-config-navigation")).toBeInTheDocument(); - expect(screen.getAllByTestId("settings-card").length).toBe(3); - expect(screen.getByTestId("theme-styling")).toBeInTheDocument(); - expect(screen.getByTestId("edit-logo")).toBeInTheDocument(); - expect(screen.getByTestId("edit-placement-form")).toBeInTheDocument(); - expect(screen.getByTestId("branding-settings-card")).toBeInTheDocument(); - expect(screen.getByText("common.project_configuration")).toBeInTheDocument(); - }); - - test("throws error if project is not found", async () => { - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(null); - const props = { params: Promise.resolve({ environmentId: "env1" }) }; - await expect(ProjectLookSettingsPage(props)).rejects.toThrow("Project not found"); - }); - - test("does not show storage warning when IS_STORAGE_CONFIGURED is true", async () => { - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ - id: "project1", - name: "Test Project", - createdAt: new Date(), - updatedAt: new Date(), - environments: [], - } as any); - - const Page = await ProjectLookSettingsPage(props); - render(Page); - - expect(screen.queryByTestId("alert")).not.toBeInTheDocument(); - }); - - test("shows storage warning when IS_STORAGE_CONFIGURED is false", async () => { - // Mock IS_STORAGE_CONFIGURED as false - vi.doMock("@/lib/constants", () => ({ - SURVEY_BG_COLORS: ["#fff", "#000"], - IS_FORMBRICKS_CLOUD: 1, - IS_STORAGE_CONFIGURED: false, - UNSPLASH_ACCESS_KEY: "unsplash-key", - })); - - // Re-import the module to get the updated mock - const { ProjectLookSettingsPage: PageWithStorageDisabled } = await import("./page"); - - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ - id: "project1", - name: "Test Project", - createdAt: new Date(), - updatedAt: new Date(), - environments: [], - } as any); - - const Page = await PageWithStorageDisabled(props); - render(Page); - - expect(screen.getByTestId("alert")).toBeInTheDocument(); - expect(screen.getByTestId("alert")).toHaveAttribute("data-variant", "warning"); - expect(screen.getByTestId("alert-description")).toHaveTextContent("common.storage_not_configured"); - }); - - test("passes isStorageConfigured=true to ThemeStyling and EditLogo components", async () => { - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ - id: "project1", - name: "Test Project", - createdAt: new Date(), - updatedAt: new Date(), - environments: [], - } as any); - - const Page = await ProjectLookSettingsPage(props); - render(Page); - - expect(vi.mocked(ThemeStyling)).toHaveBeenCalledWith( - expect.objectContaining({ - isStorageConfigured: true, - }), - undefined - ); - - expect(vi.mocked(EditLogo)).toHaveBeenCalledWith( - expect.objectContaining({ - isStorageConfigured: true, - }), - undefined - ); - }); - - test("passes isStorageConfigured=false to ThemeStyling and EditLogo components when storage is not configured", async () => { - // Mock IS_STORAGE_CONFIGURED as false - vi.doMock("@/lib/constants", () => ({ - SURVEY_BG_COLORS: ["#fff", "#000"], - IS_FORMBRICKS_CLOUD: 1, - IS_STORAGE_CONFIGURED: false, - UNSPLASH_ACCESS_KEY: "unsplash-key", - })); - - // Re-import the module to get the updated mock - const { ProjectLookSettingsPage: PageWithStorageDisabled } = await import("./page"); - - vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ - id: "project1", - name: "Test Project", - createdAt: new Date(), - updatedAt: new Date(), - environments: [], - } as any); - - const Page = await PageWithStorageDisabled(props); - render(Page); - - expect(vi.mocked(ThemeStyling)).toHaveBeenCalledWith( - expect.objectContaining({ - isStorageConfigured: false, - }), - undefined - ); - - expect(vi.mocked(EditLogo)).toHaveBeenCalledWith( - expect.objectContaining({ - isStorageConfigured: false, - }), - undefined - ); - }); -}); diff --git a/apps/web/modules/projects/settings/page.test.tsx b/apps/web/modules/projects/settings/page.test.tsx deleted file mode 100644 index ce3df3e750..0000000000 --- a/apps/web/modules/projects/settings/page.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { cleanup } from "@testing-library/react"; -import { redirect } from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { ProjectSettingsPage } from "./page"; - -vi.mock("next/navigation", () => ({ - redirect: vi.fn(), -})); - -describe("ProjectSettingsPage", () => { - afterEach(() => { - cleanup(); - }); - - test("redirects to the general project settings page", async () => { - const params = { environmentId: "env-123" }; - await ProjectSettingsPage({ params: Promise.resolve(params) }); - expect(vi.mocked(redirect)).toHaveBeenCalledWith("/environments/env-123/project/general"); - }); -}); diff --git a/apps/web/modules/projects/settings/tags/components/edit-tags-wrapper.test.tsx b/apps/web/modules/projects/settings/tags/components/edit-tags-wrapper.test.tsx deleted file mode 100644 index 16f14779fb..0000000000 --- a/apps/web/modules/projects/settings/tags/components/edit-tags-wrapper.test.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TTag, TTagsCount } from "@formbricks/types/tags"; -import { EditTagsWrapper } from "./edit-tags-wrapper"; - -vi.mock("@/modules/projects/settings/tags/components/single-tag", () => ({ - SingleTag: (props: any) =>
    {props.tagName}
    , -})); -vi.mock("@/modules/ui/components/empty-space-filler", () => ({ - EmptySpaceFiller: () =>
    , -})); - -describe("EditTagsWrapper", () => { - afterEach(() => { - cleanup(); - }); - - const environment: TEnvironment = { - id: "env1", - createdAt: new Date(), - updatedAt: new Date(), - type: "production", - projectId: "p1", - appSetupCompleted: true, - }; - - const tags: TTag[] = [ - { id: "tag1", createdAt: new Date(), updatedAt: new Date(), name: "Tag 1", environmentId: "env1" }, - { id: "tag2", createdAt: new Date(), updatedAt: new Date(), name: "Tag 2", environmentId: "env1" }, - ]; - - const tagsCount: TTagsCount = [ - { tagId: "tag1", count: 5 }, - { tagId: "tag2", count: 0 }, - ]; - - test("renders table headers and actions column if not readOnly", () => { - render( - - ); - expect(screen.getByText("environments.project.tags.tag")).toBeInTheDocument(); - expect(screen.getByText("environments.project.tags.count")).toBeInTheDocument(); - expect(screen.getByText("common.actions")).toBeInTheDocument(); - }); - - test("does not render actions column if readOnly", () => { - render( - - ); - expect(screen.queryByText("common.actions")).not.toBeInTheDocument(); - }); - - test("renders EmptySpaceFiller if no tags", () => { - render( - - ); - expect(screen.getByTestId("empty-space-filler")).toBeInTheDocument(); - }); - - test("renders SingleTag for each tag", () => { - render( - - ); - expect(screen.getByTestId("single-tag-tag1")).toHaveTextContent("Tag 1"); - expect(screen.getByTestId("single-tag-tag2")).toHaveTextContent("Tag 2"); - }); -}); diff --git a/apps/web/modules/projects/settings/tags/components/merge-tags-combobox.test.tsx b/apps/web/modules/projects/settings/tags/components/merge-tags-combobox.test.tsx deleted file mode 100644 index c97d1fcbb3..0000000000 --- a/apps/web/modules/projects/settings/tags/components/merge-tags-combobox.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { MergeTagsCombobox } from "./merge-tags-combobox"; - -vi.mock("@/modules/ui/components/command", () => ({ - Command: ({ children }: any) =>
    {children}
    , - CommandEmpty: ({ children }: any) =>
    {children}
    , - CommandGroup: ({ children }: any) =>
    {children}
    , - CommandInput: (props: any) => , - CommandItem: ({ children, onSelect, ...props }: any) => ( -
    onSelect && onSelect(children)} {...props}> - {children} -
    - ), - CommandList: ({ children }: any) =>
    {children}
    , -})); - -vi.mock("@/modules/ui/components/popover", () => ({ - Popover: ({ children }: any) =>
    {children}
    , - PopoverContent: ({ children }: any) =>
    {children}
    , - PopoverTrigger: ({ children }: any) =>
    {children}
    , -})); - -describe("MergeTagsCombobox", () => { - afterEach(() => { - cleanup(); - }); - - const tags = [ - { label: "Tag 1", value: "tag1" }, - { label: "Tag 2", value: "tag2" }, - ]; - - test("renders button with tolgee string", () => { - render(); - expect(screen.getByText("environments.project.tags.merge")).toBeInTheDocument(); - }); - - test("shows popover and all tag items when button is clicked", async () => { - render(); - await userEvent.click(screen.getByText("environments.project.tags.merge")); - expect(screen.getByTestId("popover-content")).toBeInTheDocument(); - expect(screen.getAllByTestId("command-item").length).toBe(2); - expect(screen.getByText("Tag 1")).toBeInTheDocument(); - expect(screen.getByText("Tag 2")).toBeInTheDocument(); - }); - - test("calls onSelect with tag value and closes popover", async () => { - const onSelect = vi.fn(); - render(); - await userEvent.click(screen.getByText("environments.project.tags.merge")); - await userEvent.click(screen.getByText("Tag 1")); - expect(onSelect).toHaveBeenCalledWith("tag1"); - }); - - test("shows no tag found if tags is empty", async () => { - render(); - await userEvent.click(screen.getByText("environments.project.tags.merge")); - expect(screen.getByTestId("command-empty")).toBeInTheDocument(); - }); - - test("filters tags using input", async () => { - render(); - await userEvent.click(screen.getByText("environments.project.tags.merge")); - const input = screen.getByTestId("command-input"); - await userEvent.type(input, "Tag 2"); - expect(screen.getByText("Tag 2")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/tags/components/single-tag.test.tsx b/apps/web/modules/projects/settings/tags/components/single-tag.test.tsx deleted file mode 100644 index 17a15151f4..0000000000 --- a/apps/web/modules/projects/settings/tags/components/single-tag.test.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TTag } from "@formbricks/types/tags"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { - deleteTagAction, - mergeTagsAction, - updateTagNameAction, -} from "@/modules/projects/settings/tags/actions"; -import { SingleTag } from "./single-tag"; - -vi.mock("@/modules/ui/components/delete-dialog", () => ({ - DeleteDialog: ({ open, setOpen, onDelete }: any) => - open ? ( -
    - -
    - ) : null, -})); - -vi.mock("@/modules/ui/components/loading-spinner", () => ({ - LoadingSpinner: () =>
    , -})); - -vi.mock("@/modules/projects/settings/tags/components/merge-tags-combobox", () => ({ - MergeTagsCombobox: ({ tags, onSelect }: any) => ( -
    - {tags.map((t: any) => ( - - ))} -
    - ), -})); - -const mockRouter = { refresh: vi.fn() }; - -vi.mock("@/modules/projects/settings/tags/actions", () => ({ - updateTagNameAction: vi.fn(() => Promise.resolve({ data: {} })), - deleteTagAction: vi.fn(() => Promise.resolve({ data: {} })), - mergeTagsAction: vi.fn(() => Promise.resolve({ data: {} })), -})); -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: vi.fn(), -})); -vi.mock("next/navigation", () => ({ useRouter: () => mockRouter })); - -const baseTag: TTag = { - id: "tag1", - createdAt: new Date(), - updatedAt: new Date(), - name: "Tag 1", - environmentId: "env1", -}; - -const environmentTags: TTag[] = [ - baseTag, - { id: "tag2", createdAt: new Date(), updatedAt: new Date(), name: "Tag 2", environmentId: "env1" }, -]; - -describe("SingleTag", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - afterEach(() => { - cleanup(); - }); - - test("renders tag name and count", () => { - render( - - ); - expect(screen.getByDisplayValue("Tag 1")).toBeInTheDocument(); - expect(screen.getByText("5")).toBeInTheDocument(); - }); - - test("shows loading spinner if tagCountLoading", () => { - render( - - ); - expect(screen.getByTestId("loading-spinner")).toBeInTheDocument(); - }); - - test("calls updateTagNameAction and shows success toast on blur", async () => { - render(); - const input = screen.getByDisplayValue("Tag 1"); - await userEvent.clear(input); - await userEvent.type(input, "Tag 1 Updated"); - fireEvent.blur(input); - expect(updateTagNameAction).toHaveBeenCalledWith({ tagId: baseTag.id, name: "Tag 1 Updated" }); - }); - - test("shows error toast and sets error state if updateTagNameAction fails", async () => { - vi.mocked(updateTagNameAction).mockResolvedValueOnce({ serverError: "Error occurred" }); - render(); - const input = screen.getByDisplayValue("Tag 1"); - fireEvent.blur(input); - }); - - test("shows merge tags combobox and calls mergeTagsAction", async () => { - vi.mocked(mergeTagsAction).mockImplementationOnce(() => Promise.resolve({ data: undefined })); - vi.mocked(getFormattedErrorMessage).mockReturnValue("Error occurred"); - render(); - const mergeBtn = screen.getByText("Tag 2"); - await userEvent.click(mergeBtn); - expect(mergeTagsAction).toHaveBeenCalledWith({ originalTagId: baseTag.id, newTagId: "tag2" }); - expect(getFormattedErrorMessage).toHaveBeenCalled(); - }); - - test("shows error toast if mergeTagsAction fails", async () => { - vi.mocked(mergeTagsAction).mockResolvedValueOnce({}); - render(); - const mergeBtn = screen.getByText("Tag 2"); - await userEvent.click(mergeBtn); - expect(getFormattedErrorMessage).toHaveBeenCalled(); - }); - - test("shows delete dialog and calls deleteTagAction on confirm", async () => { - render(); - await userEvent.click(screen.getByText("common.delete")); - expect(screen.getByTestId("delete-dialog")).toBeInTheDocument(); - await userEvent.click(screen.getByTestId("confirm-delete")); - expect(deleteTagAction).toHaveBeenCalledWith({ tagId: baseTag.id }); - }); - - test("shows error toast if deleteTagAction fails", async () => { - vi.mocked(deleteTagAction).mockResolvedValueOnce({}); - render(); - await userEvent.click(screen.getByText("common.delete")); - await userEvent.click(screen.getByTestId("confirm-delete")); - expect(getFormattedErrorMessage).toHaveBeenCalled(); - }); - - test("does not render actions if isReadOnly", () => { - render( - - ); - expect(screen.queryByText("common.delete")).not.toBeInTheDocument(); - expect(screen.queryByTestId("merge-tags-combobox")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/projects/settings/tags/loading.test.tsx b/apps/web/modules/projects/settings/tags/loading.test.tsx deleted file mode 100644 index 70035f789b..0000000000 --- a/apps/web/modules/projects/settings/tags/loading.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TagsLoading } from "./loading"; - -vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({ - SettingsCard: ({ children, title, description }: any) => ( -
    -
    {title}
    -
    {description}
    - {children} -
    - ), -})); -vi.mock("@/modules/projects/settings/components/project-config-navigation", () => ({ - ProjectConfigNavigation: ({ activeId }: any) => ( -
    {activeId}
    - ), -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }: any) => ( -
    -
    {pageTitle}
    - {children} -
    - ), -})); - -describe("TagsLoading", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all tolgee strings and skeletons", () => { - render(); - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("settings-card")).toBeInTheDocument(); - expect(screen.getByText("common.project_configuration")).toBeInTheDocument(); - expect(screen.getByText("environments.project.tags.manage_tags")).toBeInTheDocument(); - expect(screen.getByText("environments.project.tags.manage_tags_description")).toBeInTheDocument(); - expect(screen.getByText("environments.project.tags.tag")).toBeInTheDocument(); - expect(screen.getByText("environments.project.tags.count")).toBeInTheDocument(); - expect(screen.getByText("common.actions")).toBeInTheDocument(); - expect( - screen.getAllByText((_, node) => node!.className?.includes("animate-pulse")).length - ).toBeGreaterThan(0); - }); -}); diff --git a/apps/web/modules/projects/settings/tags/page.test.tsx b/apps/web/modules/projects/settings/tags/page.test.tsx deleted file mode 100644 index 8dee8e3a65..0000000000 --- a/apps/web/modules/projects/settings/tags/page.test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { TagsPage } from "./page"; - -vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({ - SettingsCard: ({ children, title, description }: any) => ( -
    -
    {title}
    -
    {description}
    - {children} -
    - ), -})); -vi.mock("@/modules/projects/settings/components/project-config-navigation", () => ({ - ProjectConfigNavigation: ({ environmentId, activeId }: any) => ( -
    - {environmentId}-{activeId} -
    - ), -})); -vi.mock("@/modules/ui/components/page-content-wrapper", () => ({ - PageContentWrapper: ({ children }: any) =>
    {children}
    , -})); -vi.mock("@/modules/ui/components/page-header", () => ({ - PageHeader: ({ children, pageTitle }: any) => ( -
    -
    {pageTitle}
    - {children} -
    - ), -})); -vi.mock("./components/edit-tags-wrapper", () => ({ - EditTagsWrapper: () =>
    edit-tags-wrapper
    , -})); - -const mockGetTranslate = vi.fn(async () => (key: string) => key); - -vi.mock("@/tolgee/server", () => ({ getTranslate: () => mockGetTranslate() })); -vi.mock("@/modules/environments/lib/utils", () => ({ - getEnvironmentAuth: vi.fn(), -})); -vi.mock("@/lib/tag/service", () => ({ - getTagsByEnvironmentId: vi.fn(), -})); -vi.mock("@/lib/tagOnResponse/service", () => ({ - getTagsOnResponsesCount: vi.fn(), -})); - -describe("TagsPage", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all tolgee strings and main components", async () => { - const props = { params: { environmentId: "env1" } }; - vi.mocked(getEnvironmentAuth).mockResolvedValue({ - isReadOnly: false, - environment: { - id: "env1", - appSetupCompleted: true, - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project1", - type: "development", - }, - } as any); - - const Page = await TagsPage(props); - render(Page); - expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("page-header")).toBeInTheDocument(); - expect(screen.getByTestId("settings-card")).toBeInTheDocument(); - expect(screen.getByTestId("edit-tags-wrapper")).toBeInTheDocument(); - expect(screen.getByTestId("project-config-navigation")).toHaveTextContent("env1-tags"); - expect(screen.getByText("common.project_configuration")).toBeInTheDocument(); - expect(screen.getByText("environments.project.tags.manage_tags")).toBeInTheDocument(); - expect(screen.getByText("environments.project.tags.manage_tags_description")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/setup/(fresh-instance)/signup/page.test.tsx b/apps/web/modules/setup/(fresh-instance)/signup/page.test.tsx deleted file mode 100644 index b4aff3a86f..0000000000 --- a/apps/web/modules/setup/(fresh-instance)/signup/page.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { render, screen } from "@testing-library/react"; -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { findMatchingLocale } from "@/lib/utils/locale"; -import { getIsSamlSsoEnabled, getIsSsoEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getTranslate } from "@/tolgee/server"; -import { SignupPage } from "./page"; - -// Mock dependencies - -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - FB_LOGO_URL: "mock-fb-logo-url", - SMTP_HOST: "smtp.example.com", - SMTP_PORT: 587, - SMTP_USER: "smtp-user", - SAML_AUDIENCE: "test-saml-audience", - SAML_PATH: "test-saml-path", - SAML_DATABASE_URL: "test-saml-database-url", - TERMS_URL: "test-terms-url", - SIGNUP_ENABLED: true, - PRIVACY_URL: "test-privacy-url", - EMAIL_VERIFICATION_DISABLED: false, - EMAIL_AUTH_ENABLED: true, - GOOGLE_OAUTH_ENABLED: true, - GITHUB_OAUTH_ENABLED: true, - AZURE_OAUTH_ENABLED: true, - OIDC_OAUTH_ENABLED: true, - DEFAULT_ORGANIZATION_ID: "test-default-organization-id", - IS_TURNSTILE_CONFIGURED: true, - SAML_TENANT: "test-saml-tenant", - SAML_PRODUCT: "test-saml-product", - TURNSTILE_SITE_KEY: "test-turnstile-site-key", -})); - -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getIsSsoEnabled: vi.fn(), - getIsSamlSsoEnabled: vi.fn(), -})); - -vi.mock("@/lib/utils/locale", () => ({ - findMatchingLocale: vi.fn(), -})); - -vi.mock("@/tolgee/server", () => ({ - getTranslate: vi.fn(), -})); - -// Mock the SignupForm component to simplify our test assertions -vi.mock("@/modules/auth/signup/components/signup-form", () => ({ - SignupForm: (props) => ( -
    - SignupForm -
    - ), -})); - -describe("SignupPage", () => { - beforeEach(() => { - vi.mocked(getIsSsoEnabled).mockResolvedValue(true); - vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(false); - vi.mocked(findMatchingLocale).mockResolvedValue("en-US"); - vi.mocked(getTranslate).mockResolvedValue((key) => key); - }); - - test("renders the signup page correctly", async () => { - const page = await SignupPage(); - render(page); - - expect(screen.getByTestId("signup-form")).toBeInTheDocument(); - expect(screen.getByTestId("signup-form")).toHaveAttribute( - "data-turnstile-key", - "test-turnstile-site-key" - ); - }); -}); diff --git a/apps/web/modules/setup/organization/[organizationId]/invite/components/invite-members.test.tsx b/apps/web/modules/setup/organization/[organizationId]/invite/components/invite-members.test.tsx deleted file mode 100644 index 762c4d420e..0000000000 --- a/apps/web/modules/setup/organization/[organizationId]/invite/components/invite-members.test.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { useRouter } from "next/navigation"; -import { toast } from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { inviteOrganizationMemberAction } from "@/modules/setup/organization/[organizationId]/invite/actions"; -import { InviteMembers } from "./invite-members"; - -// Mock next/navigation -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), -})); - -// Mock the translation hook -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -// Mock the invite action -vi.mock("@/modules/setup/organization/[organizationId]/invite/actions", () => ({ - inviteOrganizationMemberAction: vi.fn(), -})); - -// Mock toast -vi.mock("react-hot-toast", () => ({ - toast: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -// Mock helper -vi.mock("@/lib/utils/helper", () => ({ - getFormattedErrorMessage: (result) => result?.error || "Invalid email", -})); - -describe("InviteMembers", () => { - const mockInvitedUserId = "a7z22q8y6o1c3hxgmbwlqod5"; - - const mockRouter = { - push: vi.fn(), - } as unknown as ReturnType; - - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(useRouter).mockReturnValue(mockRouter); - }); - - afterEach(() => { - cleanup(); - }); - - test("renders the component with initial state", () => { - render(); - - expect(screen.getByText("setup.invite.invite_your_organization_members")).toBeInTheDocument(); - expect(screen.getByText("setup.invite.life_s_no_fun_alone")).toBeInTheDocument(); - expect(screen.getByPlaceholderText("user@example.com")).toBeInTheDocument(); - expect(screen.getByPlaceholderText("Full Name (optional)")).toBeInTheDocument(); - expect(screen.getByText("setup.invite.add_another_member")).toBeInTheDocument(); - expect(screen.getByText("setup.invite.continue")).toBeInTheDocument(); - expect(screen.getByText("setup.invite.skip")).toBeInTheDocument(); - }); - - test("shows SMTP warning when SMTP is not configured", () => { - render(); - - expect(screen.getByText("setup.invite.smtp_not_configured")).toBeInTheDocument(); - expect(screen.getByText("setup.invite.smtp_not_configured_description")).toBeInTheDocument(); - }); - - test("adds another member field when clicking add member button", () => { - render(); - - const addButton = screen.getByText("setup.invite.add_another_member"); - fireEvent.click(addButton); - - const emailInputs = screen.getAllByPlaceholderText("user@example.com"); - const nameInputs = screen.getAllByPlaceholderText("Full Name (optional)"); - - expect(emailInputs).toHaveLength(2); - expect(nameInputs).toHaveLength(2); - }); - - test("handles skip button click", () => { - render(); - - const skipButton = screen.getByText("setup.invite.skip"); - fireEvent.click(skipButton); - - expect(mockRouter.push).toHaveBeenCalledWith("/"); - }); - - test("handles successful member invitation", async () => { - vi.mocked(inviteOrganizationMemberAction).mockResolvedValueOnce({ data: mockInvitedUserId }); - - render(); - - const emailInput = screen.getByPlaceholderText("user@example.com"); - const nameInput = screen.getByPlaceholderText("Full Name (optional)"); - const continueButton = screen.getByText("setup.invite.continue"); - - fireEvent.change(emailInput, { target: { value: "test@example.com" } }); - fireEvent.change(nameInput, { target: { value: "Test User" } }); - fireEvent.click(continueButton); - - await waitFor(() => { - expect(inviteOrganizationMemberAction).toHaveBeenCalledWith({ - email: "test@example.com", - name: "Test User", - organizationId: "org-123", - }); - expect(toast.success).toHaveBeenCalledWith("setup.invite.invitation_sent_to test@example.com!"); - expect(mockRouter.push).toHaveBeenCalledWith("/"); - }); - }); - - test("handles failed member invitation", async () => { - // @ts-expect-error -- Mocking the error response - vi.mocked(inviteOrganizationMemberAction).mockResolvedValueOnce({ error: "Invalid email" }); - - render(); - - const emailInput = screen.getByPlaceholderText("user@example.com"); - const nameInput = screen.getByPlaceholderText("Full Name (optional)"); - const continueButton = screen.getByText("setup.invite.continue"); - - fireEvent.change(emailInput, { target: { value: "test@example.com" } }); - fireEvent.change(nameInput, { target: { value: "Test User" } }); - fireEvent.click(continueButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("Invalid email"); - }); - }); - - test("handles invitation error", async () => { - vi.mocked(inviteOrganizationMemberAction).mockRejectedValueOnce(new Error("Network error")); - - render(); - - const emailInput = screen.getByPlaceholderText("user@example.com"); - const nameInput = screen.getByPlaceholderText("Full Name (optional)"); - const continueButton = screen.getByText("setup.invite.continue"); - - fireEvent.change(emailInput, { target: { value: "test@example.com" } }); - fireEvent.change(nameInput, { target: { value: "Test User" } }); - fireEvent.click(continueButton); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith("setup.invite.failed_to_invite test@example.com."); - }); - }); - - test("validates email format", async () => { - render(); - - const emailInput = screen.getByPlaceholderText("user@example.com"); - const continueButton = screen.getByText("setup.invite.continue"); - - fireEvent.change(emailInput, { target: { value: "invalid-email" } }); - fireEvent.click(continueButton); - - await waitFor(() => { - expect(screen.getByText(/invalid email/i)).toBeInTheDocument(); - }); - }); -}); diff --git a/apps/web/modules/setup/organization/[organizationId]/invite/page.test.tsx b/apps/web/modules/setup/organization/[organizationId]/invite/page.test.tsx deleted file mode 100644 index 70403707ee..0000000000 --- a/apps/web/modules/setup/organization/[organizationId]/invite/page.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import * as nextAuth from "next-auth"; -import * as nextNavigation from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { AuthenticationError } from "@formbricks/types/errors"; -import * as constants from "@/lib/constants"; -import * as roleAccess from "@/lib/organization/auth"; -import { InvitePage } from "./page"; - -// Mock environment variables -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - FB_LOGO_URL: "mock-fb-logo-url", - SMTP_HOST: "smtp.example.com", - SMTP_PORT: 587, - SMTP_USER: "smtp-user", - SAML_AUDIENCE: "test-saml-audience", - SAML_PATH: "test-saml-path", - SAML_DATABASE_URL: "test-saml-database-url", - TERMS_URL: "test-terms-url", - SIGNUP_ENABLED: true, - PRIVACY_URL: "test-privacy-url", - EMAIL_VERIFICATION_DISABLED: false, - EMAIL_AUTH_ENABLED: true, - GOOGLE_OAUTH_ENABLED: true, - GITHUB_OAUTH_ENABLED: true, - AZURE_OAUTH_ENABLED: true, - OIDC_OAUTH_ENABLED: true, - DEFAULT_ORGANIZATION_ID: "test-default-organization-id", - IS_TURNSTILE_CONFIGURED: true, - SAML_TENANT: "test-saml-tenant", - SAML_PRODUCT: "test-saml-product", - TURNSTILE_SITE_KEY: "test-turnstile-site-key", - SAML_OAUTH_ENABLED: true, - SMTP_PASSWORD: "smtp-password", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -// Mock the InviteMembers component -vi.mock("@/modules/setup/organization/[organizationId]/invite/components/invite-members", () => ({ - InviteMembers: vi.fn(({ IS_SMTP_CONFIGURED, organizationId }) => ( -
    -
    {IS_SMTP_CONFIGURED.toString()}
    -
    {organizationId}
    -
    - )), -})); - -// Mock getServerSession from next-auth -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -// Mock next/navigation -vi.mock("next/navigation", () => ({ - notFound: vi.fn(), - redirect: vi.fn(), -})); - -// Mock verifyUserRoleAccess -vi.mock("@/lib/organization/auth", () => ({ - verifyUserRoleAccess: vi.fn(), -})); - -// Mock getTranslate -vi.mock("@/tolgee/server", () => ({ - getTranslate: () => vi.fn(), -})); - -describe("InvitePage", () => { - const organizationId = "org-123"; - const mockParams = Promise.resolve({ organizationId }); - const mockSession = { - user: { - id: "user-123", - name: "Test User", - email: "test@example.com", - }, - }; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders InviteMembers component when user has access", async () => { - // Mock SMTP configuration values - vi.spyOn(constants, "SMTP_HOST", "get").mockReturnValue("smtp.example.com"); - vi.spyOn(constants, "SMTP_PORT", "get").mockReturnValue("587"); - vi.spyOn(constants, "SMTP_USER", "get").mockReturnValue("user@example.com"); - vi.spyOn(constants, "SMTP_PASSWORD", "get").mockReturnValue("password"); - - // Mock session and role access - vi.mocked(nextAuth.getServerSession).mockResolvedValue(mockSession); - vi.mocked(roleAccess.verifyUserRoleAccess).mockResolvedValue({ - hasCreateOrUpdateMembersAccess: true, - } as unknown as any); - - // Render the page - const page = await InvitePage({ params: mockParams }); - render(page); - - // Verify the component was rendered with correct props - expect(screen.getByTestId("invite-members-component")).toBeInTheDocument(); - expect(screen.getByTestId("smtp-configured").textContent).toBe("true"); - expect(screen.getByTestId("organization-id").textContent).toBe(organizationId); - }); - - test("shows notFound when user lacks permissions", async () => { - // Mock session and role access - vi.mocked(nextAuth.getServerSession).mockResolvedValue(mockSession); - vi.mocked(roleAccess.verifyUserRoleAccess).mockResolvedValue({ - hasCreateOrUpdateMembersAccess: false, - } as unknown as any); - - const notFoundMock = vi.fn(); - vi.mocked(nextNavigation.notFound).mockImplementation(notFoundMock as unknown as any); - - // Render the page - await InvitePage({ params: mockParams }); - - // Verify notFound was called - expect(notFoundMock).toHaveBeenCalled(); - }); - - test("passes false to IS_SMTP_CONFIGURED when SMTP is not fully configured", async () => { - // Mock partial SMTP configuration (missing password) - vi.spyOn(constants, "SMTP_HOST", "get").mockReturnValue("smtp.example.com"); - vi.spyOn(constants, "SMTP_PORT", "get").mockReturnValue("587"); - vi.spyOn(constants, "SMTP_USER", "get").mockReturnValue("user@example.com"); - vi.spyOn(constants, "SMTP_PASSWORD", "get").mockReturnValue(""); - - // Mock session and role access - vi.mocked(nextAuth.getServerSession).mockResolvedValue(mockSession); - vi.mocked(roleAccess.verifyUserRoleAccess).mockResolvedValue({ - hasCreateOrUpdateMembersAccess: true, - } as unknown as any); - - // Render the page - const page = await InvitePage({ params: mockParams }); - render(page); - - // Verify IS_SMTP_CONFIGURED is false - expect(screen.getByTestId("smtp-configured").textContent).toBe("false"); - }); - - test("throws AuthenticationError when session is not available", async () => { - // Mock session as null - vi.mocked(nextAuth.getServerSession).mockResolvedValue(null); - - // Expect an error when rendering the page - await expect(InvitePage({ params: mockParams })).rejects.toThrow(AuthenticationError); - }); -}); diff --git a/apps/web/modules/setup/organization/create/components/create-organization.test.tsx b/apps/web/modules/setup/organization/create/components/create-organization.test.tsx deleted file mode 100644 index 8ff4f1006d..0000000000 --- a/apps/web/modules/setup/organization/create/components/create-organization.test.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { useRouter } from "next/navigation"; -import { toast } from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { createOrganizationAction } from "@/app/setup/organization/create/actions"; -import { CreateOrganization } from "./create-organization"; - -// Mock dependencies -vi.mock("@/app/setup/organization/create/actions", () => ({ - createOrganizationAction: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: vi.fn(() => ({ - t: (key: string) => key, - })), -})); - -vi.mock("react-hot-toast", () => ({ - toast: { - error: vi.fn(), - success: vi.fn(), - }, -})); - -const mockRouter = { - push: vi.fn(), -}; - -describe("CreateOrganization", () => { - beforeEach(() => { - vi.mocked(useRouter).mockReturnValue(mockRouter as any); - vi.mocked(createOrganizationAction).mockReset(); - }); - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders the component correctly", () => { - render(); - - expect(screen.getByText("setup.organization.create.title")).toBeInTheDocument(); - expect(screen.getByText("setup.organization.create.description")).toBeInTheDocument(); - expect(screen.getByPlaceholderText("e.g., Acme Inc")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "setup.organization.create.continue" })).toBeInTheDocument(); - }); - - test("input field updates organization name and button state", async () => { - const user = userEvent.setup(); - render(); - - const input = screen.getByPlaceholderText("e.g., Acme Inc"); - const button = screen.getByRole("button", { name: "setup.organization.create.continue" }); - - expect(button).toBeDisabled(); - - await user.type(input, "Test Organization"); - expect(input).toHaveValue("Test Organization"); - expect(button).toBeEnabled(); - - await user.clear(input); - expect(input).toHaveValue(""); - expect(button).toBeDisabled(); - - await user.type(input, " "); - expect(input).toHaveValue(" "); - expect(button).toBeDisabled(); - }); - - test("calls createOrganizationAction and redirects on successful submission", async () => { - const user = userEvent.setup(); - const mockOrganizationId = "org_123test"; - vi.mocked(createOrganizationAction).mockResolvedValue({ - data: { id: mockOrganizationId, name: "Test Org" }, - error: null, - } as any); - - render(); - - const input = screen.getByPlaceholderText("e.g., Acme Inc"); - const button = screen.getByRole("button", { name: "setup.organization.create.continue" }); - - await user.type(input, "Test Organization"); - await user.click(button); - - await waitFor(() => { - expect(createOrganizationAction).toHaveBeenCalledWith({ organizationName: "Test Organization" }); - }); - await waitFor(() => { - expect(mockRouter.push).toHaveBeenCalledWith(`/setup/organization/${mockOrganizationId}/invite`); - }); - }); - - test("shows an error toast if createOrganizationAction throws an error", async () => { - const user = userEvent.setup(); - vi.mocked(createOrganizationAction).mockRejectedValue(new Error("Network error")); - - render(); - - const input = screen.getByPlaceholderText("e.g., Acme Inc"); - const button = screen.getByRole("button", { name: "setup.organization.create.continue" }); - - await user.type(input, "Test Organization"); - await user.click(button); - - await waitFor(() => { - expect(createOrganizationAction).toHaveBeenCalledWith({ organizationName: "Test Organization" }); - }); - await waitFor(() => { - expect(vi.mocked(toast.error)).toHaveBeenCalledWith("Some error occurred while creating organization"); - }); - expect(mockRouter.push).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/modules/setup/organization/create/components/removed-from-organization.test.tsx b/apps/web/modules/setup/organization/create/components/removed-from-organization.test.tsx deleted file mode 100644 index 0a8ce9b3c7..0000000000 --- a/apps/web/modules/setup/organization/create/components/removed-from-organization.test.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TUser } from "@formbricks/types/user"; -import { DeleteAccountModal } from "@/modules/account/components/DeleteAccountModal"; -import { RemovedFromOrganization } from "./removed-from-organization"; - -// Mock DeleteAccountModal -vi.mock("@/modules/account/components/DeleteAccountModal", () => ({ - DeleteAccountModal: vi.fn(({ open, setOpen, user, isFormbricksCloud, organizationsWithSingleOwner }) => { - if (!open) return null; - return ( -
    -

    User: {user.email}

    -

    IsFormbricksCloud: {isFormbricksCloud.toString()}

    -

    OrgsWithSingleOwner: {organizationsWithSingleOwner.length}

    - -
    - ); - }), -})); - -// Mock Alert components -vi.mock("@/modules/ui/components/alert", async () => { - const actual = await vi.importActual("@/modules/ui/components/alert"); - return { - ...actual, - Alert: ({ children, variant }) => ( -
    - {children} -
    - ), - AlertTitle: ({ children }) =>
    {children}
    , - AlertDescription: ({ children }) =>
    {children}
    , - }; -}); - -// Mock Button component -vi.mock("@/modules/ui/components/button", () => ({ - Button: vi.fn(({ children, onClick }) => ( - - )), -})); - -// Mock useTranslate from @tolgee/react -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -const mockUser = { - id: "user-123", - name: "Test User", - email: "test@example.com", - twoFactorEnabled: false, - identityProvider: "email", - createdAt: new Date(), - updatedAt: new Date(), - notificationSettings: { - alert: {}, - }, - role: "other", -} as TUser; - -describe("RemovedFromOrganization", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders correctly with initial content", () => { - render(); - expect(screen.getByText("setup.organization.create.no_membership_found")).toBeInTheDocument(); - expect(screen.getByText("setup.organization.create.no_membership_found_description")).toBeInTheDocument(); - expect(screen.getByText("setup.organization.create.delete_account_description")).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "setup.organization.create.delete_account" }) - ).toBeInTheDocument(); - expect(screen.queryByTestId("delete-account-modal")).not.toBeInTheDocument(); - }); - - test("opens DeleteAccountModal when 'Delete Account' button is clicked", async () => { - render(); - const deleteButton = screen.getByRole("button", { name: "setup.organization.create.delete_account" }); - await userEvent.click(deleteButton); - const modal = screen.getByTestId("delete-account-modal"); - expect(modal).toBeInTheDocument(); - expect(modal).toHaveTextContent(`User: ${mockUser.email}`); - expect(modal).toHaveTextContent("IsFormbricksCloud: false"); - expect(modal).toHaveTextContent("OrgsWithSingleOwner: 0"); - // Only check the last call, which is the open=true call - const lastCall = vi.mocked(DeleteAccountModal).mock.calls.at(-1)?.[0]; - expect(lastCall).toMatchObject({ - open: true, - user: mockUser, - isFormbricksCloud: false, - organizationsWithSingleOwner: [], - }); - }); - - test("passes isFormbricksCloud prop correctly to DeleteAccountModal", async () => { - render(); - const deleteButton = screen.getByRole("button", { name: "setup.organization.create.delete_account" }); - await userEvent.click(deleteButton); - const modal = screen.getByTestId("delete-account-modal"); - expect(modal).toBeInTheDocument(); - expect(modal).toHaveTextContent("IsFormbricksCloud: true"); - const lastCall = vi.mocked(DeleteAccountModal).mock.calls.at(-1)?.[0]; - expect(lastCall).toMatchObject({ - open: true, - user: mockUser, - isFormbricksCloud: true, - organizationsWithSingleOwner: [], - }); - }); -}); diff --git a/apps/web/modules/setup/organization/create/page.test.tsx b/apps/web/modules/setup/organization/create/page.test.tsx deleted file mode 100644 index 80f7922a4d..0000000000 --- a/apps/web/modules/setup/organization/create/page.test.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import * as nextAuth from "next-auth"; -import * as nextNavigation from "next/navigation"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { AuthenticationError } from "@formbricks/types/errors"; -import * as instanceService from "@/lib/instance/service"; -import * as organizationService from "@/lib/organization/service"; -import * as userService from "@/lib/user/service"; -import * as licenseCheck from "@/modules/ee/license-check/lib/utils"; -import { CreateOrganizationPage } from "./page"; - -// Mock environment variables -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - ENCRYPTION_KEY: "mock-encryption-key", - ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", - GITHUB_ID: "mock-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_PRODUCTION: false, - SENTRY_DSN: "mock-sentry-dsn", - FB_LOGO_URL: "mock-fb-logo-url", - SMTP_HOST: "smtp.example.com", - SMTP_PORT: 587, - SMTP_USER: "smtp-user", - SAML_AUDIENCE: "test-saml-audience", - SAML_PATH: "test-saml-path", - SAML_DATABASE_URL: "test-saml-database-url", - TERMS_URL: "test-terms-url", - SIGNUP_ENABLED: true, - PRIVACY_URL: "test-privacy-url", - EMAIL_VERIFICATION_DISABLED: false, - EMAIL_AUTH_ENABLED: true, - GOOGLE_OAUTH_ENABLED: true, - GITHUB_OAUTH_ENABLED: true, - AZURE_OAUTH_ENABLED: true, - OIDC_OAUTH_ENABLED: true, - DEFAULT_ORGANIZATION_ID: "test-default-organization-id", - IS_TURNSTILE_CONFIGURED: true, - SAML_TENANT: "test-saml-tenant", - SAML_PRODUCT: "test-saml-product", - TURNSTILE_SITE_KEY: "test-turnstile-site-key", - SAML_OAUTH_ENABLED: true, - SMTP_PASSWORD: "smtp-password", - SESSION_MAX_AGE: 1000, - REDIS_URL: undefined, - AUDIT_LOG_ENABLED: true, -})); - -// Mock the CreateOrganization component -vi.mock("./components/create-organization", () => ({ - CreateOrganization: vi.fn(() =>
    ), -})); - -// Mock the RemovedFromOrganization component -vi.mock("./components/removed-from-organization", () => ({ - RemovedFromOrganization: vi.fn(({ user, isFormbricksCloud }) => ( -
    -
    {user.id}
    -
    {isFormbricksCloud.toString()}
    -
    - )), -})); - -// Mock the ClientLogout component -vi.mock("@/modules/ui/components/client-logout", () => ({ - ClientLogout: vi.fn(() =>
    ), -})); - -// Mock getServerSession from next-auth -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -// Mock next/navigation -vi.mock("next/navigation", () => ({ - notFound: vi.fn(), -})); - -// Mock services -vi.mock("@/lib/instance/service", () => ({ - gethasNoOrganizations: vi.fn(), -})); - -vi.mock("@/lib/organization/service", () => ({ - getOrganizationsByUserId: vi.fn(), -})); - -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); - -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getIsMultiOrgEnabled: vi.fn(), -})); - -// Mock getTranslate -vi.mock("@/tolgee/server", async () => { - const actual = await vi.importActual("@/tolgee/server"); - return { - ...actual, - getTranslate: () => (key: string) => key, - }; -}); - -describe("CreateOrganizationPage", () => { - const mockSession = { - user: { - id: "user-123", - name: "Test User", - email: "test@example.com", - }, - }; - - const mockUser = { - id: "user-123", - name: "Test User", - email: "test@example.com", - emailVerified: null, - twoFactorEnabled: false, - identityProvider: "email" as const, - createdAt: new Date(), - updatedAt: new Date(), - isActive: true, - onboardingCompleted: false, - role: "founder" as const, - organizationId: "org-123", - organizationRole: "owner", - organizationName: "Test Org", - organizationSlug: "test-org", - organizationPlan: "free", - organizationPeriod: "monthly", - organizationPeriodStart: new Date(), - organizationStripeCustomerId: null, - organizationIsAIEnabled: false, - objective: null, - notificationSettings: { - alert: { - surveyInvite: true, - surveyResponse: true, - surveyClosed: true, - surveyPaused: true, - surveyCompleted: true, - surveyDeleted: true, - surveyUpdated: true, - surveyCreated: true, - }, - unsubscribedOrganizationIds: [], - }, - locale: "en-US" as const, - lastLoginAt: new Date(), - }; - - const mockOrganization = { - id: "org-1", - name: "Test Organization", - createdAt: new Date(), - updatedAt: new Date(), - billing: { - stripeCustomerId: null, - plan: "free" as const, - period: "monthly" as const, - limits: { - monthly: { - responses: 1000, - miu: 100, - }, - projects: 5, - }, - periodStart: new Date(), - }, - isAIEnabled: false, - }; - - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders CreateOrganization when hasNoOrganizations is true", async () => { - // Mock session and services - vi.mocked(nextAuth.getServerSession).mockResolvedValue(mockSession); - vi.mocked(userService.getUser).mockResolvedValue(mockUser); - vi.mocked(instanceService.gethasNoOrganizations).mockResolvedValue(true); - vi.mocked(licenseCheck.getIsMultiOrgEnabled).mockResolvedValue(false); - vi.mocked(organizationService.getOrganizationsByUserId).mockResolvedValue([]); - - // Render the page - const page = await CreateOrganizationPage(); - render(page); - - // Verify the component was rendered - expect(screen.getByTestId("create-organization-component")).toBeInTheDocument(); - }); - - test("renders CreateOrganization when isMultiOrgEnabled is true", async () => { - // Mock session and services - vi.mocked(nextAuth.getServerSession).mockResolvedValue(mockSession); - vi.mocked(userService.getUser).mockResolvedValue(mockUser); - vi.mocked(instanceService.gethasNoOrganizations).mockResolvedValue(false); - vi.mocked(licenseCheck.getIsMultiOrgEnabled).mockResolvedValue(true); - vi.mocked(organizationService.getOrganizationsByUserId).mockResolvedValue([]); - - // Render the page - const page = await CreateOrganizationPage(); - render(page); - - // Verify the component was rendered - expect(screen.getByTestId("create-organization-component")).toBeInTheDocument(); - }); - - test("renders RemovedFromOrganization when user has no organizations", async () => { - // Mock session and services - vi.mocked(nextAuth.getServerSession).mockResolvedValue(mockSession); - vi.mocked(userService.getUser).mockResolvedValue(mockUser); - vi.mocked(instanceService.gethasNoOrganizations).mockResolvedValue(false); - vi.mocked(licenseCheck.getIsMultiOrgEnabled).mockResolvedValue(false); - vi.mocked(organizationService.getOrganizationsByUserId).mockResolvedValue([]); - - // Render the page - const page = await CreateOrganizationPage(); - render(page); - - // Verify the component was rendered with correct props - expect(screen.getByTestId("removed-from-organization-component")).toBeInTheDocument(); - expect(screen.getByTestId("user-id").textContent).toBe(mockUser.id); - expect(screen.getByTestId("is-formbricks-cloud").textContent).toBe("false"); - }); - - test("shows notFound when user has organizations", async () => { - // Mock session and services - vi.mocked(nextAuth.getServerSession).mockResolvedValue(mockSession); - vi.mocked(userService.getUser).mockResolvedValue(mockUser); - vi.mocked(instanceService.gethasNoOrganizations).mockResolvedValue(false); - vi.mocked(licenseCheck.getIsMultiOrgEnabled).mockResolvedValue(false); - vi.mocked(organizationService.getOrganizationsByUserId).mockResolvedValue([mockOrganization]); - - const notFoundMock = vi.fn(); - vi.mocked(nextNavigation.notFound).mockImplementation(notFoundMock as unknown as any); - - // Render the page - await CreateOrganizationPage(); - - // Verify notFound was called - expect(notFoundMock).toHaveBeenCalled(); - }); - - test("renders ClientLogout when user is not found", async () => { - // Mock session and services - vi.mocked(nextAuth.getServerSession).mockResolvedValue(mockSession); - vi.mocked(userService.getUser).mockResolvedValue(null); - - // Render the page - const page = await CreateOrganizationPage(); - render(page); - - // Verify the component was rendered - expect(screen.getByTestId("client-logout-component")).toBeInTheDocument(); - }); - - test("throws AuthenticationError when session is not available", async () => { - // Mock session as null - vi.mocked(nextAuth.getServerSession).mockResolvedValue(null); - - // Expect an error when rendering the page - await expect(CreateOrganizationPage()).rejects.toThrow(AuthenticationError); - }); -}); diff --git a/apps/web/modules/survey/components/edit-public-survey-alert-dialog/index.test.tsx b/apps/web/modules/survey/components/edit-public-survey-alert-dialog/index.test.tsx deleted file mode 100644 index 8cd5e9c841..0000000000 --- a/apps/web/modules/survey/components/edit-public-survey-alert-dialog/index.test.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { EditPublicSurveyAlertDialog } from "./index"; - -// Mock translation to return keys as text -vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t: (key: string) => key }) })); - -// Mock Dialog components -vi.mock("@/modules/ui/components/dialog", () => ({ - Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) => - open ?
    {children}
    : null, - DialogContent: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogHeader: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogDescription: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogBody: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), - DialogFooter: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - -describe("EditPublicSurveyAlertDialog", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders with all expected content", () => { - const setOpen = vi.fn(); - render(); - - // Check dialog structure - expect(screen.getByTestId("dialog")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-content")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-header")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-title")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-body")).toBeInTheDocument(); - expect(screen.getByTestId("dialog-footer")).toBeInTheDocument(); - - // Title - expect(screen.getByText("environments.surveys.edit.caution_edit_published_survey")).toBeInTheDocument(); - - // Paragraphs - expect(screen.getByText("environments.surveys.edit.caution_recommendation")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.edit.caution_explanation_intro")).toBeInTheDocument(); - - // List items - expect( - screen.getByText("environments.surveys.edit.caution_explanation_responses_are_safe") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.edit.caution_explanation_new_responses_separated") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.edit.caution_explanation_only_new_responses_in_summary") - ).toBeInTheDocument(); - }); - - test("renders default close button and calls setOpen when clicked", () => { - const setOpen = vi.fn(); - render(); - - // Find the close button in the UI - const closeButton = screen.getByRole("button", { name: "common.close" }); - expect(closeButton).toBeInTheDocument(); - - fireEvent.click(closeButton); - expect(setOpen).toHaveBeenCalledWith(false); - }); - - test("renders primary button and calls action when clicked", async () => { - const setOpen = vi.fn(); - const primaryAction = vi.fn().mockResolvedValue(undefined); - - render( - - ); - - const primaryButton = screen.getByRole("button", { name: "Primary Text" }); - expect(primaryButton).toBeInTheDocument(); - - fireEvent.click(primaryButton); - - await waitFor(() => { - expect(primaryAction).toHaveBeenCalledTimes(1); - }); - }); - - test("renders secondary button and calls action when clicked", () => { - const setOpen = vi.fn(); - const secondaryAction = vi.fn(); - - render( - - ); - - const secondaryButton = screen.getByRole("button", { name: "Secondary Text" }); - expect(secondaryButton).toBeInTheDocument(); - - fireEvent.click(secondaryButton); - expect(secondaryAction).toHaveBeenCalledTimes(1); - expect(setOpen).not.toHaveBeenCalled(); - }); - - test("renders both buttons when both actions are provided", async () => { - const setOpen = vi.fn(); - const primaryAction = vi.fn().mockResolvedValue(undefined); - const secondaryAction = vi.fn(); - - render( - - ); - - // Dialog has our two action buttons - const allButtons = screen.getAllByRole("button"); - // Verify our two action buttons by their text content - const actionButtons = allButtons.filter( - (button) => button.textContent === "Secondary Text" || button.textContent === "Primary Text" - ); - expect(actionButtons).toHaveLength(2); - expect(actionButtons[0]).toHaveTextContent("Secondary Text"); - expect(actionButtons[1]).toHaveTextContent("Primary Text"); - - fireEvent.click(actionButtons[0]); - expect(secondaryAction).toHaveBeenCalledTimes(1); - - fireEvent.click(actionButtons[1]); - await waitFor(() => { - expect(primaryAction).toHaveBeenCalledTimes(1); - }); - }); - - test("shows loading state in primary button when isLoading is true", () => { - const setOpen = vi.fn(); - const primaryAction = vi.fn().mockResolvedValue(undefined); - - render( - - ); - - const primaryButton = screen.getByRole("button", { name: "Primary Text" }); - // Check if button has loading class or attribute - expect( - primaryButton.classList.contains("loading") || - primaryButton.innerHTML.includes("loader") || - primaryButton.getAttribute("aria-busy") === "true" - ).toBeTruthy(); - }); -}); diff --git a/apps/web/modules/survey/components/question-form-input/components/fallback-input.test.tsx b/apps/web/modules/survey/components/question-form-input/components/fallback-input.test.tsx deleted file mode 100644 index cffd196c2b..0000000000 --- a/apps/web/modules/survey/components/question-form-input/components/fallback-input.test.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { toast } from "react-hot-toast"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurveyRecallItem } from "@formbricks/types/surveys/types"; -import { FallbackInput } from "./fallback-input"; - -vi.mock("react-hot-toast", () => ({ - toast: { - error: vi.fn(), - }, -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - const translations: { [key: string]: string } = { - "environments.surveys.edit.add_fallback_placeholder": - "Add a placeholder to show if the question gets skipped:", - "environments.surveys.edit.fallback_for": "Fallback for", - "environments.surveys.edit.fallback_missing": "Fallback missing", - "environments.surveys.edit.add_fallback": "Add", - }; - return translations[key] || key; - }, - }), -})); - -describe("FallbackInput", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockFilteredRecallItems: (TSurveyRecallItem | undefined)[] = [ - { id: "item1", label: "Item 1", type: "question" }, - { id: "item2", label: "Item 2", type: "question" }, - ]; - - const mockSetFallbacks = vi.fn(); - const mockAddFallback = vi.fn(); - const mockSetOpen = vi.fn(); - const mockInputRef = { current: null } as any; - - const defaultProps = { - filteredRecallItems: mockFilteredRecallItems, - fallbacks: {}, - setFallbacks: mockSetFallbacks, - fallbackInputRef: mockInputRef, - addFallback: mockAddFallback, - open: true, - setOpen: mockSetOpen, - }; - - test("renders fallback input component correctly", () => { - render(); - - expect(screen.getByText("Add a placeholder to show if the question gets skipped:")).toBeInTheDocument(); - expect(screen.getByLabelText("Item 1")).toBeInTheDocument(); - expect(screen.getByLabelText("Item 2")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "common.save" })).toBeDisabled(); - }); - - test("enables Save button when fallbacks are provided for all items", () => { - render(); - - expect(screen.getByRole("button", { name: "common.save" })).toBeEnabled(); - }); - - test("updates fallbacks when input changes", async () => { - const user = userEvent.setup(); - - render(); - - const input1 = screen.getByLabelText("Item 1"); - await user.type(input1, "test"); - - // Check that setFallbacks was called (at least once) - expect(mockSetFallbacks).toHaveBeenCalled(); - }); - - test("handles Enter key press correctly when input is valid", async () => { - const user = userEvent.setup(); - - render(); - - const input = screen.getByLabelText("Item 1"); - await user.type(input, "{Enter}"); - - expect(mockAddFallback).toHaveBeenCalled(); - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - - test("shows error toast and doesn't call addFallback when Enter is pressed with empty fallbacks", async () => { - const user = userEvent.setup(); - - render(); - - const input = screen.getByLabelText("Item 1"); - await user.type(input, "{Enter}"); - - expect(toast.error).toHaveBeenCalledWith("Fallback missing"); - expect(mockAddFallback).not.toHaveBeenCalled(); - expect(mockSetOpen).not.toHaveBeenCalled(); - }); - - test("calls addFallback when Save button is clicked", async () => { - const user = userEvent.setup(); - - render(); - - const saveButton = screen.getByRole("button", { name: "common.save" }); - await user.click(saveButton); - - expect(mockAddFallback).toHaveBeenCalled(); - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - - test("handles undefined recall items gracefully", () => { - const mixedRecallItems: (TSurveyRecallItem | undefined)[] = [ - { id: "item1", label: "Item 1", type: "question" }, - undefined, - ]; - - render(); - - expect(screen.getByLabelText("Item 1")).toBeInTheDocument(); - expect(screen.queryByText("undefined")).not.toBeInTheDocument(); - }); - - test("replaces 'nbsp' with space in fallback value", () => { - render(); - - const input = screen.getByLabelText("Item 1"); - expect(input).toHaveValue("fallback text"); - }); - - test("does not render when open is false", () => { - render(); - - expect( - screen.queryByText("Add a placeholder to show if the question gets skipped:") - ).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/survey/components/question-form-input/components/multi-lang-wrapper.test.tsx b/apps/web/modules/survey/components/question-form-input/components/multi-lang-wrapper.test.tsx deleted file mode 100644 index ca0e2f9fc0..0000000000 --- a/apps/web/modules/survey/components/question-form-input/components/multi-lang-wrapper.test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TLanguage } from "@formbricks/types/project"; -import { TI18nString, TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types"; -import { getEnabledLanguages } from "@/lib/i18n/utils"; -import { headlineToRecall } from "@/lib/utils/recall"; -import { MultiLangWrapper } from "./multi-lang-wrapper"; - -vi.mock("@/lib/i18n/utils", () => ({ - getEnabledLanguages: vi.fn(), -})); - -vi.mock("@/lib/utils/recall", () => ({ - headlineToRecall: vi.fn((value) => value), - recallToHeadline: vi.fn(() => ({ default: "Default translation text" })), -})); - -vi.mock("@/modules/ee/multi-language-surveys/components/language-indicator", () => ({ - LanguageIndicator: vi.fn(() =>
    Language Indicator
    ), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => - key === "environments.project.languages.translate" - ? "Translate from" - : key === "environments.project.languages.incomplete_translations" - ? "Some languages are missing translations" - : key, // NOSONAR - }), -})); - -describe("MultiLangWrapper", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - const mockRender = vi.fn(({ onChange, children }) => ( -
    -
    Content
    - {children} - -
    - )); - - const mockProps = { - isTranslationIncomplete: false, - value: { default: "Test value" } as TI18nString, - onChange: vi.fn(), - localSurvey: { - languages: [ - { language: { code: "en", name: "English" }, default: true }, - { language: { code: "fr", name: "French" }, default: false }, - ], - } as unknown as TSurvey, - selectedLanguageCode: "en", - setSelectedLanguageCode: vi.fn(), - locale: { language: "en-US" } as const, - render: mockRender, - } as any; - - test("renders correctly with single language", () => { - vi.mocked(getEnabledLanguages).mockReturnValue([ - { language: { code: "en-US" } as unknown as TLanguage } as unknown as TSurveyLanguage, - ]); - - render(); - - expect(screen.getByTestId("rendered-content")).toBeInTheDocument(); - expect(screen.queryByTestId("language-indicator")).not.toBeInTheDocument(); - }); - - test("renders language indicator when multiple languages are enabled", () => { - vi.mocked(getEnabledLanguages).mockReturnValue([ - { language: { code: "en-US" } as unknown as TLanguage } as unknown as TSurveyLanguage, - { language: { code: "fr-FR" } as unknown as TLanguage } as unknown as TSurveyLanguage, - ]); - - render(); - - expect(screen.getByTestId("language-indicator")).toBeInTheDocument(); - }); - - test("calls onChange when value changes", async () => { - vi.mocked(getEnabledLanguages).mockReturnValue([ - { language: { code: "en-US" } as unknown as TLanguage } as unknown as TSurveyLanguage, - ]); - - render(); - - await userEvent.click(screen.getByTestId("change-button")); - - expect(mockProps.onChange).toHaveBeenCalledWith({ - default: "new value", - }); - }); - - test("shows translation text when non-default language is selected", () => { - vi.mocked(getEnabledLanguages).mockReturnValue([ - { language: { code: "en" } as unknown as TLanguage } as unknown as TSurveyLanguage, - { language: { code: "fr" } as unknown as TLanguage } as unknown as TSurveyLanguage, - ]); - - render(); - - expect(screen.getByText(/Translate from/)).toBeInTheDocument(); - }); - - test("shows incomplete translation warning when applicable", () => { - vi.mocked(getEnabledLanguages).mockReturnValue([ - { language: { code: "en" } as unknown as TLanguage } as unknown as TSurveyLanguage, - { language: { code: "fr" } as unknown as TLanguage } as unknown as TSurveyLanguage, - ]); - - render(); - - expect(screen.getByText("Some languages are missing translations")).toBeInTheDocument(); - }); - - test("uses headlineToRecall when recall items and fallbacks are provided", async () => { - vi.mocked(getEnabledLanguages).mockReturnValue([ - { language: { code: "en" } as unknown as TLanguage } as unknown as TSurveyLanguage, - ]); - const mockRenderWithRecall = vi.fn(({ onChange }) => ( -
    - -
    - )); - - render(); - - await userEvent.click(screen.getByTestId("recall-button")); - - expect(mockProps.onChange).toHaveBeenCalled(); - expect(headlineToRecall).toHaveBeenCalled(); - }); -}); diff --git a/apps/web/modules/survey/components/question-form-input/components/recall-item-select.test.tsx b/apps/web/modules/survey/components/question-form-input/components/recall-item-select.test.tsx deleted file mode 100644 index 7c19ba90b1..0000000000 --- a/apps/web/modules/survey/components/question-form-input/components/recall-item-select.test.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { - TSurvey, - TSurveyQuestion, - TSurveyQuestionTypeEnum, - TSurveyRecallItem, -} from "@formbricks/types/surveys/types"; -import { RecallItemSelect } from "./recall-item-select"; - -vi.mock("@/lib/utils/recall", () => ({ - getTextContentWithRecallTruncated: vi.fn((text: string, maxLength: number = 25) => { - // Remove all HTML tags by repeatedly applying the regex - let cleaned = text; - let prev; - do { - prev = cleaned; - cleaned = cleaned.replace(/<[^>]*>/g, ""); - } while (cleaned !== prev); - cleaned = cleaned.replace(/\s+/g, " ").trim(); - - const withRecallReplaced = cleaned.replace(/#recall:[^#]+#/g, "___"); - - if (withRecallReplaced.length <= maxLength) { - return withRecallReplaced; - } - - const start = withRecallReplaced.slice(0, 10); - const end = withRecallReplaced.slice(-10); - return `${start}...${end}`; - }), -})); - -describe("RecallItemSelect", () => { - afterEach(() => { - cleanup(); - }); - - const mockAddRecallItem = vi.fn(); - const mockSetShowRecallItemSelect = vi.fn(); - - const mockSurvey = { - id: "survey-1", - name: "Test Survey", - createdAt: new Date("2023-01-01T00:00:00Z"), - updatedAt: new Date("2023-01-01T00:00:00Z"), - questions: [ - { - id: "q1", - type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { en: "Question 1" }, - } as unknown as TSurveyQuestion, - { - id: "q2", - type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { en: "Question 2" }, - } as unknown as TSurveyQuestion, - { - id: "current-q", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { en: "Current Question" }, - } as unknown as TSurveyQuestion, - { - id: "q4", - type: TSurveyQuestionTypeEnum.FileUpload, - headline: { en: "File Upload Question" }, - } as unknown as TSurveyQuestion, - ], - hiddenFields: { - enabled: true, - fieldIds: ["hidden1", "hidden2"], - }, - variables: [ - { id: "var1", name: "Variable 1", type: "text" } as unknown as TSurvey["variables"][0], - { id: "var2", name: "Variable 2", type: "number" } as unknown as TSurvey["variables"][1], - ], - welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"], - status: "draft", - environmentId: "env-1", - type: "app", - } as unknown as TSurvey; - - const mockRecallItems: TSurveyRecallItem[] = []; - - test("renders recall items from questions, hidden fields, and variables", async () => { - render( - - ); - - expect(screen.getByText("Question 1")).toBeInTheDocument(); - expect(screen.getByText("Question 2")).toBeInTheDocument(); - expect(screen.getByText("hidden1")).toBeInTheDocument(); - expect(screen.getByText("hidden2")).toBeInTheDocument(); - expect(screen.getByText("Variable 1")).toBeInTheDocument(); - expect(screen.getByText("Variable 2")).toBeInTheDocument(); - - expect(screen.queryByText("Current Question")).not.toBeInTheDocument(); - expect(screen.queryByText("File Upload Question")).not.toBeInTheDocument(); - }); - - test("do not render questions if questionId is 'start' (welcome card)", async () => { - render( - - ); - - expect(screen.queryByText("Question 1")).not.toBeInTheDocument(); - expect(screen.queryByText("Question 2")).not.toBeInTheDocument(); - - expect(screen.getByText("hidden1")).toBeInTheDocument(); - expect(screen.getByText("hidden2")).toBeInTheDocument(); - expect(screen.getByText("Variable 1")).toBeInTheDocument(); - expect(screen.getByText("Variable 2")).toBeInTheDocument(); - - expect(screen.queryByText("Current Question")).not.toBeInTheDocument(); - expect(screen.queryByText("File Upload Question")).not.toBeInTheDocument(); - }); - - test("filters recall items based on search input", async () => { - const user = userEvent.setup(); - render( - - ); - - const searchInput = screen.getByPlaceholderText("Search options"); - await user.type(searchInput, "Variable"); - - expect(screen.getByText("Variable 1")).toBeInTheDocument(); - expect(screen.getByText("Variable 2")).toBeInTheDocument(); - expect(screen.queryByText("Question 1")).not.toBeInTheDocument(); - }); - - test("calls addRecallItem and setShowRecallItemSelect when item is selected", async () => { - const user = userEvent.setup(); - render( - - ); - - const firstItem = screen.getByText("Question 1"); - await user.click(firstItem); - - expect(mockAddRecallItem).toHaveBeenCalledWith({ - id: "q1", - label: "Question 1", - type: "question", - }); - expect(mockSetShowRecallItemSelect).toHaveBeenCalledWith(false); - }); - - test("doesn't show already selected recall items", async () => { - const selectedRecallItems: TSurveyRecallItem[] = [{ id: "q1", label: "Question 1", type: "question" }]; - - render( - - ); - - expect(screen.queryByText("Question 1")).not.toBeInTheDocument(); - expect(screen.getByText("Question 2")).toBeInTheDocument(); - }); - - test("shows 'No recall items found' when search has no results", async () => { - const user = userEvent.setup(); - render( - - ); - - const searchInput = screen.getByPlaceholderText("Search options"); - await user.type(searchInput, "nonexistent"); - - expect(screen.getByText("environments.surveys.edit.no_recall_items_found")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/survey/components/question-form-input/components/recall-wrapper.test.tsx b/apps/web/modules/survey/components/question-form-input/components/recall-wrapper.test.tsx deleted file mode 100644 index f78b496bde..0000000000 --- a/apps/web/modules/survey/components/question-form-input/components/recall-wrapper.test.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { toast } from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyRecallItem } from "@formbricks/types/surveys/types"; -import * as recallUtils from "@/lib/utils/recall"; -import { RecallItemSelect } from "@/modules/survey/components/question-form-input/components/recall-item-select"; -import { RecallWrapper } from "./recall-wrapper"; - -vi.mock("react-hot-toast", () => ({ - toast: { - error: vi.fn(), - }, -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => { - const translations: { [key: string]: string } = { - "environments.surveys.edit.edit_recall": "Edit Recall", - "environments.surveys.edit.add_fallback_placeholder": "Add fallback value...", - }; - return translations[key] || key; - }, - }), -})); - -vi.mock("@/lib/utils/recall", async () => { - const actual = await vi.importActual("@/lib/utils/recall"); - return { - ...actual, - getRecallItems: vi.fn(), - getFallbackValues: vi.fn().mockReturnValue({}), - headlineToRecall: vi.fn().mockImplementation((val) => val), - recallToHeadline: vi.fn().mockImplementation((val) => val), - findRecallInfoById: vi.fn(), - extractRecallInfo: vi.fn(), - extractId: vi.fn(), - replaceRecallInfoWithUnderline: vi.fn().mockImplementation((val) => val), - }; -}); - -// Mock structuredClone if it's not available -global.structuredClone = global.structuredClone || ((obj: any) => JSON.parse(JSON.stringify(obj))); - -vi.mock("@/modules/survey/components/question-form-input/components/fallback-input", () => ({ - FallbackInput: vi - .fn() - .mockImplementation(({ addFallback, open, filteredRecallItems, fallbacks, setFallbacks }) => - open ? ( -
    - {filteredRecallItems.map((item: any) => ( - setFallbacks({ ...fallbacks, [item.id]: e.target.value })} - /> - ))} - -
    - ) : null - ), -})); - -vi.mock("@/modules/survey/components/question-form-input/components/recall-item-select", () => ({ - RecallItemSelect: vi - .fn() - .mockImplementation(() =>
    Recall Item Select
    ), -})); - -describe("RecallWrapper", () => { - const defaultProps = { - value: "Test value", - onChange: vi.fn(), - localSurvey: { - id: "testSurveyId", - questions: [], - hiddenFields: { enabled: false }, - } as unknown as TSurvey, - questionId: "testQuestionId", - render: ({ value, onChange, highlightedJSX, children, isRecallSelectVisible }: any) => ( -
    -
    {highlightedJSX}
    - onChange(e.target.value)} /> - {children} - {isRecallSelectVisible.toString()} -
    - ), - usedLanguageCode: "en", - isRecallAllowed: true, - onAddFallback: vi.fn(), - }; - - afterEach(() => { - cleanup(); - }); - - // Ensure headlineToRecall always returns a string, even with null input - beforeEach(() => { - vi.mocked(recallUtils.headlineToRecall).mockImplementation((val) => val || ""); - vi.mocked(recallUtils.recallToHeadline).mockImplementation((val) => val || { en: "" }); - // Reset all mocks to default state - vi.mocked(recallUtils.getRecallItems).mockReturnValue([]); - vi.mocked(recallUtils.findRecallInfoById).mockReturnValue(null); - }); - - test("renders correctly with no recall items", () => { - render(); - - expect(screen.getByTestId("test-input")).toBeInTheDocument(); - expect(screen.getByTestId("rendered-text")).toBeInTheDocument(); - }); - - test("renders correctly with recall items", () => { - const recallItems = [{ id: "testRecallId", label: "testLabel", type: "question" }] as TSurveyRecallItem[]; - vi.mocked(recallUtils.getRecallItems).mockReturnValue(recallItems); - - render(); - - expect(screen.getByTestId("test-input")).toBeInTheDocument(); - expect(screen.getByTestId("rendered-text")).toBeInTheDocument(); - }); - - test("shows recall item select when @ is typed", async () => { - render(); - - const input = screen.getByTestId("test-input"); - await userEvent.type(input, "@"); - - expect(screen.getByTestId("recall-select-visible").textContent).toBe("true"); - }); - - test("adds recall item when selected", async () => { - render(); - - const input = screen.getByTestId("test-input"); - await userEvent.type(input, "@"); - - expect(RecallItemSelect).toHaveBeenCalled(); - }); - - test("detects recall items when value contains recall syntax", () => { - const valueWithRecall = "Test with #recall:testId/fallback:# inside"; - const recallItems = [{ id: "testId", label: "testLabel", type: "question" }] as TSurveyRecallItem[]; - - vi.mocked(recallUtils.getRecallItems).mockReturnValue(recallItems); - - render(); - - // Verify that recall items are detected - expect(recallUtils.getRecallItems).toHaveBeenCalledWith(valueWithRecall, expect.any(Object), "en"); - }); - - test("displays error when trying to add empty recall item", async () => { - render(); - - const input = screen.getByTestId("test-input"); - await userEvent.type(input, "@"); - - const mockedRecallItemSelect = vi.mocked(RecallItemSelect); - const addRecallItemFunction = mockedRecallItemSelect.mock.calls[0][0].addRecallItem; - - // Add an item with empty label - addRecallItemFunction({ id: "testRecallId", label: "", type: "question" }); - - expect(toast.error).toHaveBeenCalledWith("Recall item label cannot be empty"); - }); - - test("handles input changes correctly", async () => { - render(); - - const input = screen.getByTestId("test-input"); - await userEvent.type(input, "New text"); - - expect(defaultProps.onChange).toHaveBeenCalled(); - }); - - test("updates internal value when props value changes", () => { - const { rerender } = render(); - - rerender(); - - expect(screen.getByTestId("test-input")).toHaveValue("Updated value"); - }); - - test("handles recall disable", () => { - render(); - - const input = screen.getByTestId("test-input"); - fireEvent.change(input, { target: { value: "test@" } }); - - expect(screen.getByTestId("recall-select-visible").textContent).toBe("false"); - }); - - test("renders recall items when value contains recall syntax", () => { - const valueWithRecall = "Test with #recall:testId/fallback:# inside"; - const recallItems = [{ id: "testId", label: "testLabel", type: "question" }] as TSurveyRecallItem[]; - - vi.mocked(recallUtils.getRecallItems).mockReturnValue(recallItems); - - render(); - - // Verify that recall items are detected and rendered - expect(recallUtils.getRecallItems).toHaveBeenCalledWith(valueWithRecall, expect.any(Object), "en"); - }); - - test("handles recall item state changes", () => { - const valueWithRecall = "Test with #recall:testId/fallback:# inside"; - const recallItems = [{ id: "testId", label: "testLabel", type: "question" }] as TSurveyRecallItem[]; - - vi.mocked(recallUtils.getRecallItems).mockReturnValue(recallItems); - - render(); - - // Verify that recall items are detected - expect(recallUtils.getRecallItems).toHaveBeenCalledWith(valueWithRecall, expect.any(Object), "en"); - }); -}); diff --git a/apps/web/modules/survey/components/question-form-input/index.test.tsx b/apps/web/modules/survey/components/question-form-input/index.test.tsx deleted file mode 100644 index 93b50ace24..0000000000 --- a/apps/web/modules/survey/components/question-form-input/index.test.tsx +++ /dev/null @@ -1,837 +0,0 @@ -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { createI18nString } from "@/lib/i18n/utils"; -// Import FileInput to get the mocked version -import { FileInput } from "@/modules/ui/components/file-input"; -import { QuestionFormInput } from "./index"; - -// Mock all the modules that might cause server-side environment variable access issues -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - ENCRYPTION_KEY: "test-encryption-key", - WEBAPP_URL: "http://localhost:3000", - DEFAULT_BRAND_COLOR: "#64748b", - AVAILABLE_LOCALES: ["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW", "pt-PT"], - DEFAULT_LOCALE: "en-US", - IS_PRODUCTION: false, - PASSWORD_RESET_DISABLED: false, - EMAIL_VERIFICATION_DISABLED: false, - DEBUG: false, - E2E_TESTING: false, - RATE_LIMITING_DISABLED: true, - ENTERPRISE_LICENSE_KEY: "test-license-key", - GITHUB_ID: "test-github-id", - GITHUB_SECRET: "test-github-secret", - POSTHOG_API_KEY: "mock-posthog-api-key", - POSTHOG_API_HOST: "mock-posthog-host", - IS_POSTHOG_CONFIGURED: true, - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - SENTRY_DSN: "mock-sentry-dsn", -})); - -// Mock env module -vi.mock("@/lib/env", () => ({ - env: { - IS_FORMBRICKS_CLOUD: "0", - ENCRYPTION_KEY: "test-encryption-key", - NODE_ENV: "test", - ENTERPRISE_LICENSE_KEY: "test-license-key", - }, -})); - -// Mock server-only module to prevent error -vi.mock("server-only", () => ({})); - -// Mock crypto for hashString -vi.mock("crypto", () => ({ - default: { - createHash: () => ({ - update: () => ({ - digest: () => "mocked-hash", - }), - }), - createCipheriv: () => ({ - update: () => "encrypted-", - final: () => "data", - }), - createDecipheriv: () => ({ - update: () => "decrypted-", - final: () => "data", - }), - randomBytes: () => Buffer.from("random-bytes"), - }, - createHash: () => ({ - update: () => ({ - digest: () => "mocked-hash", - }), - }), - randomBytes: () => Buffer.from("random-bytes"), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("@/lib/utils/hooks/useSyncScroll", () => ({ - useSyncScroll: vi.fn(), -})); - -vi.mock("@formkit/auto-animate/react", () => ({ - useAutoAnimate: () => [null], -})); - -vi.mock("lodash", () => ({ - debounce: (fn: (...args: any[]) => unknown) => fn, -})); - -// Mock hashString function -vi.mock("@/lib/hashString", () => ({ - hashString: (str: string) => "hashed_" + str, -})); - -// Mock recallToHeadline to return test values for language switching test -vi.mock("@/lib/utils/recall", () => ({ - recallToHeadline: (value: any, _survey: any, _useOnlyNumbers = false) => { - // For the language switching test, return different values based on language - if (value && typeof value === "object") { - return { - default: "Test Headline", - fr: "Test Headline FR", - ...value, - }; - } - return value; - }, -})); - -// Mock UI components -vi.mock("@/modules/ui/components/input", () => ({ - Input: ({ - id, - value, - className, - placeholder, - onChange, - "aria-label": ariaLabel, - isInvalid, - ...rest - }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, "aria-label": ariaLabel, variant, size, ...rest }: any) => ( - - ), -})); - -vi.mock("@/modules/ui/components/tooltip", () => ({ - TooltipRenderer: ({ children, tooltipContent }: any) => ( - {children} - ), - TooltipProvider: ({ children }: any) =>
    {children}
    , - Tooltip: ({ children }: any) =>
    {children}
    , - TooltipTrigger: ({ children, asChild }: any) => (asChild ? children :
    {children}
    ), - TooltipContent: ({ children }: any) =>
    {children}
    , -})); - -// Mock LocalizedEditor to render as a simple input for testing -vi.mock("@/modules/ee/multi-language-surveys/components/localized-editor", () => ({ - LocalizedEditor: ({ id, value, updateQuestion, questionIdx }: any) => ( - { - if (updateQuestion) { - updateQuestion(questionIdx, { [id]: { default: e.target.value } }); - } - }} - /> - ), -})); - -// Mock component imports to avoid rendering real components that might access server-side resources -vi.mock("@/modules/survey/components/question-form-input/components/multi-lang-wrapper", () => ({ - MultiLangWrapper: ({ render, value, onChange }: any) => { - return render({ - value, - onChange: (val: any) => onChange({ default: val }), - children: null, - }); - }, -})); - -vi.mock("@/modules/survey/components/question-form-input/components/recall-wrapper", () => ({ - RecallWrapper: ({ render, value, onChange }: any) => { - return render({ - value, - onChange, - highlightedJSX: <>, - children: null, - isRecallSelectVisible: false, - }); - }, -})); - -// Mock file input component -vi.mock("@/modules/ui/components/file-input", () => ({ - FileInput: vi.fn(() =>
    environments.surveys.edit.add_photo_or_video
    ), -})); - -// Mock license-check module -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - verifyLicense: () => ({ verified: true }), - isRestricted: () => false, -})); - -const mockUpdateQuestion = vi.fn(); -const mockUpdateSurvey = vi.fn(); -const mockUpdateChoice = vi.fn(); -const mockSetSelectedLanguageCode = vi.fn(); - -const defaultLanguages = [ - { - id: "lan_123", - default: true, - enabled: true, - language: { - id: "en", - code: "en", - name: "English", - createdAt: new Date(), - updatedAt: new Date(), - alias: null, - projectId: "project_123", - }, - }, - { - id: "lan_456", - default: false, - enabled: true, - language: { - id: "fr", - code: "fr", - name: "French", - createdAt: new Date(), - updatedAt: new Date(), - alias: null, - projectId: "project_123", - }, - }, -]; - -const mockSurvey = { - id: "survey_123", - name: "Test Survey", - type: "link", - createdAt: new Date(), - updatedAt: new Date(), - environmentId: "env_123", - status: "draft", - questions: [ - { - id: "question_1", - type: TSurveyQuestionTypeEnum.OpenText, - headline: createI18nString("First Question", ["en", "fr"]), - subheader: createI18nString("Subheader text", ["en", "fr"]), - required: true, - inputType: "text", - charLimit: { - enabled: false, - }, - }, - { - id: "question_2", - type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: createI18nString("Second Question", ["en", "fr"]), - required: false, - choices: [ - { id: "choice_1", label: createI18nString("Choice 1", ["en", "fr"]) }, - { id: "choice_2", label: createI18nString("Choice 2", ["en", "fr"]) }, - ], - }, - { - id: "question_3", - type: TSurveyQuestionTypeEnum.Rating, - headline: createI18nString("Rating Question", ["en", "fr"]), - required: true, - scale: "number", - range: 5, - lowerLabel: createI18nString("Low", ["en", "fr"]), - upperLabel: createI18nString("High", ["en", "fr"]), - isColorCodingEnabled: false, - }, - ], - recontactDays: null, - welcomeCard: { - enabled: true, - headline: createI18nString("Welcome", ["en", "fr"]), - subheader: createI18nString("

    Welcome to our survey

    ", ["en", "fr"]), - buttonLabel: createI18nString("Start", ["en", "fr"]), - fileUrl: "", - videoUrl: "", - timeToFinish: false, - showResponseCount: false, - }, - languages: defaultLanguages, - autoClose: null, - projectOverwrites: {}, - styling: {}, - singleUse: { - enabled: false, - isEncrypted: false, - }, - endings: [ - { - id: "ending_1", - type: "endScreen", - headline: createI18nString("Thank you", ["en", "fr"]), - subheader: createI18nString("Feedback submitted", ["en", "fr"]), - imageUrl: "", - }, - ], - delay: 0, - autoComplete: null, - triggers: [], - segment: null, - hiddenFields: { enabled: false, fieldIds: [] }, - variables: [], - followUps: [], -} as unknown as TSurvey; - -describe("QuestionFormInput", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); // Clean up the DOM after each test - vi.clearAllMocks(); - vi.resetModules(); - }); - - test("renders with headline input", async () => { - render( - - ); - - expect(screen.getByLabelText("Headline")).toBeInTheDocument(); - expect(screen.getByTestId("headline")).toBeInTheDocument(); - }); - - test("handles input changes correctly", async () => { - const user = userEvent.setup(); - - render( - - ); - - const input = screen.getByTestId("headline-test"); - await user.clear(input); - await user.type(input, "New Headline"); - - expect(mockUpdateQuestion).toHaveBeenCalled(); - }); - - test("handles choice updates correctly", async () => { - // Mock the updateChoice function implementation for this test - mockUpdateChoice.mockImplementation((_) => { - // Implementation does nothing, but records that the function was called - return; - }); - - if (mockSurvey.questions[1].type !== TSurveyQuestionTypeEnum.MultipleChoiceSingle) { - throw new Error("Question type is not MultipleChoiceSingle"); - } - - render( - - ); - - // Find the input and trigger a change event - const input = screen.getByTestId("choice.0"); - - // Simulate a more complete change event that should trigger the updateChoice callback - await fireEvent.change(input, { target: { value: "Updated Choice" } }); - - // Force the updateChoice to be called directly since the mocked component may not call it - mockUpdateChoice(0, { label: { default: "Updated Choice" } }); - - // Verify that updateChoice was called - expect(mockUpdateChoice).toHaveBeenCalled(); - }); - - test("handles welcome card updates correctly", async () => { - const user = userEvent.setup(); - - render( - - ); - - const input = screen.getByTestId("headline-welcome"); - await user.clear(input); - await user.type(input, "New Welcome"); - - expect(mockUpdateSurvey).toHaveBeenCalled(); - }); - - test("handles end screen card updates correctly", async () => { - const user = userEvent.setup(); - const endScreenHeadline = - mockSurvey.endings[0].type === "endScreen" ? mockSurvey.endings[0].headline : undefined; - - render( - - ); - - const input = screen.getByTestId("headline-ending"); - await user.clear(input); - await user.type(input, "New Thank You"); - - expect(mockUpdateSurvey).toHaveBeenCalled(); - }); - - test("handles nested property updates correctly", async () => { - const user = userEvent.setup(); - - if (mockSurvey.questions[2].type !== TSurveyQuestionTypeEnum.Rating) { - throw new Error("Question type is not Rating"); - } - - render( - - ); - - const input = screen.getByTestId("lowerLabel"); - await user.clear(input); - await user.type(input, "New Lower Label"); - - expect(mockUpdateQuestion).toHaveBeenCalled(); - }); - - test("toggles image uploader when button is clicked", async () => { - const user = userEvent.setup(); - - render( - - ); - - // The button should have data-testid="toggle-image-uploader-button" - const toggleButton = screen.getByTestId("toggle-image-uploader-button"); - await user.click(toggleButton); - - expect(screen.getByTestId("file-input")).toBeInTheDocument(); - }); - - test("removes subheader when remove button is clicked", async () => { - const user = userEvent.setup(); - - render( - - ); - - const removeButton = screen.getByTestId("Remove description"); - await user.click(removeButton); - - expect(mockUpdateQuestion).toHaveBeenCalledWith(0, { subheader: undefined }); - }); - - test("handles language switching", async () => { - // In this test, we won't check the value directly because our mocked components - // don't actually render with real values, but we'll just make sure the component renders - render( - - ); - - expect(screen.getByTestId("headline-lang")).toBeInTheDocument(); - }); - - test("handles max length constraint", async () => { - render( - - ); - - const input = screen.getByTestId("headline-maxlength"); - expect(input).toHaveAttribute("maxLength", "10"); - }); - - test("uses custom placeholder when provided", () => { - render( - - ); - - const input = screen.getByTestId("headline-placeholder"); - expect(input).toHaveAttribute("placeholder", "Custom placeholder"); - }); - - test("handles onBlur callback", async () => { - const onBlurMock = vi.fn(); - const user = userEvent.setup(); - - render( - - ); - - const input = screen.getByTestId("headline-blur"); - await user.click(input); - fireEvent.blur(input); - - expect(onBlurMock).toHaveBeenCalled(); - }); - - describe("isStorageConfigured functionality", () => { - test("passes isStorageConfigured=true to FileInput when image uploader is shown for headline", async () => { - const user = userEvent.setup(); - - render( - - ); - - // Click the image uploader toggle button - const toggleButton = screen.getByTestId("toggle-image-uploader-button"); - await user.click(toggleButton); - - // Verify FileInput receives isStorageConfigured=true - expect(vi.mocked(FileInput)).toHaveBeenCalledWith( - expect.objectContaining({ - isStorageConfigured: true, - }), - undefined - ); - }); - - test("passes isStorageConfigured=false to FileInput when image uploader is shown for headline", async () => { - const user = userEvent.setup(); - - render( - - ); - - // Click the image uploader toggle button - const toggleButton = screen.getByTestId("toggle-image-uploader-button"); - await user.click(toggleButton); - - // Verify FileInput receives isStorageConfigured=false - expect(vi.mocked(FileInput)).toHaveBeenCalledWith( - expect.objectContaining({ - isStorageConfigured: false, - }), - undefined - ); - }); - - test("does not render FileInput when id is not headline", async () => { - render( - - ); - - // Image uploader toggle button should not be present for non-headline fields - expect(screen.queryByTestId("toggle-image-uploader-button")).not.toBeInTheDocument(); - - // FileInput should not be called - expect(vi.mocked(FileInput)).not.toHaveBeenCalled(); - }); - - test("FileInput is only rendered when image uploader is toggled on for headline", async () => { - const user = userEvent.setup(); - - render( - - ); - - // Initially, FileInput should not be rendered (uploader is closed by default) - expect(vi.mocked(FileInput)).not.toHaveBeenCalled(); - - // Click the image uploader toggle button - const toggleButton = screen.getByTestId("toggle-image-uploader-button"); - await user.click(toggleButton); - - // Now FileInput should be rendered with correct props - expect(vi.mocked(FileInput)).toHaveBeenCalledWith( - expect.objectContaining({ - id: "question-image", - allowedFileExtensions: ["png", "jpeg", "jpg", "webp", "heic"], - environmentId: mockSurvey.environmentId, - isVideoAllowed: true, - isStorageConfigured: true, - }), - undefined - ); - }); - - test("FileInput receives correct file upload callback", async () => { - const user = userEvent.setup(); - - render( - - ); - - // Click the image uploader toggle button - const toggleButton = screen.getByTestId("toggle-image-uploader-button"); - await user.click(toggleButton); - - // Get the onFileUpload callback that was passed to FileInput - const fileInputCall = vi.mocked(FileInput).mock.calls[0][0]; - const onFileUpload = fileInputCall.onFileUpload; - - // Simulate an image upload - onFileUpload(["https://example.com/image.jpg"], "image"); - - // Verify updateQuestion was called with the correct image data - expect(mockUpdateQuestion).toHaveBeenCalledWith(0, { - imageUrl: "https://example.com/image.jpg", - videoUrl: "", - }); - - // Simulate a video upload - onFileUpload(["https://example.com/video.mp4"], "video"); - - // Verify updateQuestion was called with the correct video data - expect(mockUpdateQuestion).toHaveBeenCalledWith(0, { - videoUrl: "https://example.com/video.mp4", - imageUrl: "", - }); - }); - }); -}); diff --git a/apps/web/modules/survey/components/template-list/components/start-from-scratch-template.test.tsx b/apps/web/modules/survey/components/template-list/components/start-from-scratch-template.test.tsx deleted file mode 100644 index c0b052a847..0000000000 --- a/apps/web/modules/survey/components/template-list/components/start-from-scratch-template.test.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TTemplate } from "@formbricks/types/templates"; -import { customSurveyTemplate } from "@/app/lib/templates"; -import { replacePresetPlaceholders } from "../lib/utils"; -import { StartFromScratchTemplate } from "./start-from-scratch-template"; - -vi.mock("@/app/lib/templates", () => ({ - customSurveyTemplate: vi.fn(), -})); - -vi.mock("../lib/utils", () => ({ - replacePresetPlaceholders: vi.fn(), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -vi.mock("@/lib/cn", () => ({ - cn: (...args: any[]) => args.filter(Boolean).join(" "), -})); - -describe("StartFromScratchTemplate", () => { - afterEach(() => { - cleanup(); - }); - - const mockTemplate = { - name: "Custom Survey", - description: "Create a survey from scratch", - icon: "PlusCircleIcon", - } as unknown as TTemplate; - - const mockProject = { - id: "project-1", - name: "Test Project", - } as any; - - test("renders with correct content", () => { - vi.mocked(customSurveyTemplate).mockReturnValue(mockTemplate); - - const setActiveTemplateMock = vi.fn(); - const onTemplateClickMock = vi.fn(); - const createSurveyMock = vi.fn(); - - render( - - ); - - expect(screen.getByText(mockTemplate.name)).toBeInTheDocument(); - expect(screen.getByText(mockTemplate.description)).toBeInTheDocument(); - }); - - test("handles click correctly without preview", async () => { - vi.mocked(customSurveyTemplate).mockReturnValue(mockTemplate); - const user = userEvent.setup(); - - const setActiveTemplateMock = vi.fn(); - const onTemplateClickMock = vi.fn(); - const createSurveyMock = vi.fn(); - - render( - - ); - - const cardButton = screen.getByRole("button", { - name: `${mockTemplate.name} ${mockTemplate.description}`, - }); - await user.click(cardButton); - - expect(createSurveyMock).toHaveBeenCalledWith(mockTemplate); - expect(onTemplateClickMock).not.toHaveBeenCalled(); - expect(setActiveTemplateMock).not.toHaveBeenCalled(); - }); - - test("handles click correctly with preview", async () => { - vi.mocked(customSurveyTemplate).mockReturnValue(mockTemplate); - const replacedTemplate = { ...mockTemplate, name: "Replaced Template" }; - vi.mocked(replacePresetPlaceholders).mockReturnValue(replacedTemplate); - - const user = userEvent.setup(); - const setActiveTemplateMock = vi.fn(); - const onTemplateClickMock = vi.fn(); - const createSurveyMock = vi.fn(); - - render( - - ); - - const cardButton = screen.getByRole("button", { - name: `${mockTemplate.name} ${mockTemplate.description}`, - }); - await user.click(cardButton); - - expect(replacePresetPlaceholders).toHaveBeenCalledWith(mockTemplate, mockProject); - expect(onTemplateClickMock).toHaveBeenCalledWith(replacedTemplate); - expect(setActiveTemplateMock).toHaveBeenCalledWith(replacedTemplate); - }); - - test("shows create button when template is active", () => { - vi.mocked(customSurveyTemplate).mockReturnValue(mockTemplate); - - const setActiveTemplateMock = vi.fn(); - const onTemplateClickMock = vi.fn(); - const createSurveyMock = vi.fn(); - - render( - - ); - - expect(screen.getByText("common.create_survey")).toBeInTheDocument(); - }); - - test("create button calls createSurvey with active template", async () => { - vi.mocked(customSurveyTemplate).mockReturnValue(mockTemplate); - const user = userEvent.setup(); - - const setActiveTemplateMock = vi.fn(); - const onTemplateClickMock = vi.fn(); - const createSurveyMock = vi.fn(); - - render( - - ); - - const createButton = screen.getByText("common.create_survey"); - await user.click(createButton); - - expect(createSurveyMock).toHaveBeenCalledWith(mockTemplate); - }); - - test("button is disabled when loading is true", () => { - vi.mocked(customSurveyTemplate).mockReturnValue(mockTemplate); - - const setActiveTemplateMock = vi.fn(); - const onTemplateClickMock = vi.fn(); - const createSurveyMock = vi.fn(); - - render( - - ); - - const createButton = screen.getByText("common.create_survey").closest("button"); - - // Check for the visual indicators that button is disabled - expect(createButton).toBeInTheDocument(); - expect(createButton?.className).toContain("opacity-50"); - expect(createButton?.className).toContain("cursor-not-allowed"); - }); -}); diff --git a/apps/web/modules/survey/components/template-list/components/template-filters.test.tsx b/apps/web/modules/survey/components/template-list/components/template-filters.test.tsx deleted file mode 100644 index 035ba9a088..0000000000 --- a/apps/web/modules/survey/components/template-list/components/template-filters.test.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TemplateFilters } from "./template-filters"; - -vi.mock("../lib/utils", () => ({ - getChannelMapping: vi.fn(() => [ - { value: "channel1", label: "environments.surveys.templates.channel1" }, - { value: "channel2", label: "environments.surveys.templates.channel2" }, - ]), - getIndustryMapping: vi.fn(() => [ - { value: "industry1", label: "environments.surveys.templates.industry1" }, - { value: "industry2", label: "environments.surveys.templates.industry2" }, - ]), - getRoleMapping: vi.fn(() => [ - { value: "role1", label: "environments.surveys.templates.role1" }, - { value: "role2", label: "environments.surveys.templates.role2" }, - ]), -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - -describe("TemplateFilters", () => { - afterEach(() => { - cleanup(); - }); - - test("renders all filter categories and options", () => { - const setSelectedFilter = vi.fn(); - render(); - - expect(screen.getByText("environments.surveys.templates.all_channels")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.templates.all_industries")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.templates.all_roles")).toBeInTheDocument(); - - expect(screen.getByText("environments.surveys.templates.channel1")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.templates.channel2")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.templates.industry1")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.templates.role1")).toBeInTheDocument(); - }); - - test("clicking a filter button calls setSelectedFilter with correct parameters", async () => { - const setSelectedFilter = vi.fn(); - const user = userEvent.setup(); - - render(); - - await user.click(screen.getByText("environments.surveys.templates.channel1")); - expect(setSelectedFilter).toHaveBeenCalledWith(["channel1", null, null]); - - await user.click(screen.getByText("environments.surveys.templates.industry1")); - expect(setSelectedFilter).toHaveBeenCalledWith([null, "industry1", null]); - }); - - test("clicking 'All' button calls setSelectedFilter with null for that category", async () => { - const setSelectedFilter = vi.fn(); - const user = userEvent.setup(); - - render( - - ); - - await user.click(screen.getByText("environments.surveys.templates.all_channels")); - expect(setSelectedFilter).toHaveBeenCalledWith([null, "app", "website"]); - }); - - test("filter buttons are disabled when templateSearch has a value", () => { - const setSelectedFilter = vi.fn(); - - render( - - ); - - const buttons = screen.getAllByRole("button"); - buttons.forEach((button) => { - expect(button).toBeDisabled(); - }); - }); -}); diff --git a/apps/web/modules/survey/components/template-list/components/template-tags.test.tsx b/apps/web/modules/survey/components/template-list/components/template-tags.test.tsx deleted file mode 100644 index f2637befd6..0000000000 --- a/apps/web/modules/survey/components/template-list/components/template-tags.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import type { TTemplate, TTemplateFilter } from "@formbricks/types/templates"; -import { TemplateTags, getRoleBasedStyling } from "./template-tags"; - -vi.mock("../lib/utils", () => ({ - getRoleMapping: () => [{ value: "marketing", label: "Marketing" }], - getChannelMapping: () => [ - { value: "email", label: "Email Survey" }, - { value: "chat", label: "Chat Survey" }, - { value: "sms", label: "SMS Survey" }, - ], - getIndustryMapping: () => [ - { value: "indA", label: "Industry A" }, - { value: "indB", label: "Industry B" }, - ], -})); - -const baseTemplate = { - role: "marketing", - channels: ["email"], - industries: ["indA"], - preset: { questions: [] }, -} as unknown as TTemplate; - -const noFilter: TTemplateFilter[] = [null, null]; - -describe("TemplateTags", () => { - afterEach(() => { - cleanup(); - }); - - test("getRoleBasedStyling for productManager", () => { - expect(getRoleBasedStyling("productManager")).toBe("border-blue-300 bg-blue-50 text-blue-500"); - }); - - test("getRoleBasedStyling for sales", () => { - expect(getRoleBasedStyling("sales")).toBe("border-emerald-300 bg-emerald-50 text-emerald-500"); - }); - - test("getRoleBasedStyling for customerSuccess", () => { - expect(getRoleBasedStyling("customerSuccess")).toBe("border-violet-300 bg-violet-50 text-violet-500"); - }); - - test("getRoleBasedStyling for peopleManager", () => { - expect(getRoleBasedStyling("peopleManager")).toBe("border-pink-300 bg-pink-50 text-pink-500"); - }); - - test("getRoleBasedStyling default case", () => { - expect(getRoleBasedStyling(undefined)).toBe("border-slate-300 bg-slate-50 text-slate-500"); - }); - - test("renders role tag with correct styling and label", () => { - render(); - const role = screen.getByText("Marketing"); - expect(role).toHaveClass("border-orange-300", "bg-orange-50", "text-orange-500"); - }); - - test("single channel shows label without suffix", () => { - render(); - expect(screen.getByText("Email Survey")).toBeInTheDocument(); - }); - - test("two channels concatenated with 'common.or'", () => { - const tpl = { ...baseTemplate, channels: ["email", "chat"] } as unknown as TTemplate; - render(); - expect(screen.getByText("Chat common.or Email")).toBeInTheDocument(); - }); - - test("three channels shows 'environments.surveys.templates.all_channels'", () => { - const tpl = { ...baseTemplate, channels: ["email", "chat", "sms"] } as unknown as TTemplate; - render(); - expect(screen.getByText("environments.surveys.templates.all_channels")).toBeInTheDocument(); - }); - - test("more than three channels hides channel tag", () => { - const tpl = { ...baseTemplate, channels: ["email", "chat", "sms", "email"] } as unknown as TTemplate; - render(); - expect(screen.queryByText(/Survey|common\.or|all_channels/)).toBeNull(); - }); - - test("single industry shows mapped label", () => { - render(); - expect(screen.getByText("Industry A")).toBeInTheDocument(); - }); - - test("multiple industries shows 'multiple_industries'", () => { - const tpl = { ...baseTemplate, industries: ["indA", "indB"] } as unknown as TTemplate; - render(); - expect(screen.getByText("environments.surveys.templates.multiple_industries")).toBeInTheDocument(); - }); - - test("selectedFilter[1] overrides industry tag", () => { - render(); - expect(screen.getByText("Marketing")).toBeInTheDocument(); - }); - - test("renders branching logic icon when questions have logic", () => { - const tpl = { ...baseTemplate, preset: { questions: [{ logic: [1] }] } } as unknown as TTemplate; - render(); - expect(document.querySelector("svg")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/modules/survey/components/template-list/components/template.test.tsx b/apps/web/modules/survey/components/template-list/components/template.test.tsx deleted file mode 100644 index 961b2966e4..0000000000 --- a/apps/web/modules/survey/components/template-list/components/template.test.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Project } from "@prisma/client"; -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TTemplate, TTemplateFilter } from "@formbricks/types/templates"; -import { replacePresetPlaceholders } from "../lib/utils"; -import { Template } from "./template"; - -vi.mock("../lib/utils", () => ({ - replacePresetPlaceholders: vi.fn((template) => template), -})); - -vi.mock("./template-tags", () => ({ - TemplateTags: () =>
    , -})); - -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ t: (key: string) => key }), -})); - -describe("Template Component", () => { - afterEach(() => { - cleanup(); - }); - - const mockTemplate: TTemplate = { - name: "Test Template", - description: "Test Description", - preset: {} as any, - }; - - const mockProject = { id: "project-id", name: "Test Project" } as Project; - const mockSelectedFilter: TTemplateFilter[] = []; - - const defaultProps = { - template: mockTemplate, - activeTemplate: null, - setActiveTemplate: vi.fn(), - onTemplateClick: vi.fn(), - project: mockProject, - createSurvey: vi.fn(), - loading: false, - selectedFilter: mockSelectedFilter, - }; - - test("renders template correctly", () => { - render(