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 new file mode 100644 index 0000000000..b7f6303685 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx @@ -0,0 +1,133 @@ +import { + getIsMultiOrgEnabled, + getIsOrganizationAIReady, + getWhiteLabelPermission, +} from "@/modules/ee/license-check/lib/utils"; +import { getTranslate } from "@/tolgee/server"; +import { getServerSession } from "next-auth"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; +import { getAccessFlags } from "@formbricks/lib/membership/utils"; +import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; +import { getUser } from "@formbricks/lib/user/service"; +import { TMembership } from "@formbricks/types/memberships"; +import { TOrganization } from "@formbricks/types/organizations"; +import { TUser } from "@formbricks/types/user"; +import Page from "./page"; + +vi.mock("@formbricks/lib/constants", () => ({ + IS_FORMBRICKS_CLOUD: false, + 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", +})); + +vi.mock("next-auth", () => ({ + getServerSession: vi.fn(), +})); + +vi.mock("@/tolgee/server", () => ({ + getTranslate: vi.fn(), +})); + +vi.mock("@formbricks/lib/user/service", () => ({ + getUser: vi.fn(), +})); + +vi.mock("@formbricks/lib/organization/service", () => ({ + getOrganizationByEnvironmentId: vi.fn(), +})); + +vi.mock("@formbricks/lib/membership/service", () => ({ + getMembershipByUserIdOrganizationId: vi.fn(), +})); + +vi.mock("@formbricks/lib/membership/utils", () => ({ + getAccessFlags: vi.fn(), +})); + +vi.mock("@/modules/ee/license-check/lib/utils", () => ({ + getIsMultiOrgEnabled: vi.fn(), + getIsOrganizationAIReady: vi.fn(), + getWhiteLabelPermission: vi.fn(), +})); + +describe("Page", () => { + const mockParams = { environmentId: "test-environment-id" }; + const mockSession = { user: { id: "test-user-id" } }; + const mockUser = { id: "test-user-id" } as TUser; + const mockOrganization = { id: "test-organization-id", billing: { plan: "free" } } as TOrganization; + const mockMembership = { role: "owner" } as TMembership; + const mockTranslate = vi.fn((key) => key); + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getServerSession).mockResolvedValue(mockSession); + vi.mocked(getTranslate).mockResolvedValue(mockTranslate); + vi.mocked(getUser).mockResolvedValue(mockUser); + vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization); + vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); + vi.mocked(getAccessFlags).mockReturnValue({ + isOwner: true, + isManager: false, + isBilling: false, + isMember: false, + }); + vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); + vi.mocked(getIsOrganizationAIReady).mockResolvedValue(true); + vi.mocked(getWhiteLabelPermission).mockResolvedValue(true); + }); + + it("renders the page with organization settings", async () => { + const props = { + params: Promise.resolve({ environmentId: "env-123" }), + }; + + const result = await Page(props); + + expect(result).toBeTruthy(); + }); + + it("renders if session user id is null", async () => { + vi.mocked(getServerSession).mockResolvedValue({ user: { id: null } }); + + const props = { + params: Promise.resolve({ environmentId: "env-123" }), + }; + + const result = await Page(props); + + expect(result).toBeTruthy(); + }); + + it("throws an error if the session is not found", async () => { + vi.mocked(getServerSession).mockResolvedValue(null); + + await expect(Page({ params: Promise.resolve(mockParams) })).rejects.toThrow("common.session_not_found"); + }); + + it("throws an error if the organization is not found", async () => { + vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null); + + await expect(Page({ params: Promise.resolve(mockParams) })).rejects.toThrow( + "common.organization_not_found" + ); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx index 4f6a177dd7..d7ba202c3c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx @@ -12,7 +12,8 @@ import { PageHeader } from "@/modules/ui/components/page-header"; import { SettingsId } from "@/modules/ui/components/settings-id"; import { getTranslate } from "@/tolgee/server"; import { getServerSession } from "next-auth"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import React from "react"; +import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; @@ -84,6 +85,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => { environmentId={params.environmentId} isReadOnly={!isOwnerOrManager} isFormbricksCloud={IS_FORMBRICKS_CLOUD} + fbLogoUrl={FB_LOGO_URL} user={user} /> {isMultiOrgEnabled && ( diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.tsx index 1ed3bd21bc..e402d9fd59 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.tsx @@ -2,6 +2,7 @@ import { Badge } from "@/modules/ui/components/badge"; import { useTranslate } from "@tolgee/react"; +import React from "react"; import { cn } from "@formbricks/lib/cn"; export const SettingsCard = ({ 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 new file mode 100644 index 0000000000..90469c55d1 --- /dev/null +++ b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.test.tsx @@ -0,0 +1,144 @@ +import { + removeOrganizationEmailLogoUrlAction, + sendTestEmailAction, + updateOrganizationEmailLogoUrlAction, +} from "@/modules/ee/whitelabel/email-customization/actions"; +import { uploadFile } from "@/modules/ui/components/file-input/lib/utils"; +import "@testing-library/jest-dom/vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { TOrganization } from "@formbricks/types/organizations"; +import { TUser } from "@formbricks/types/user"; +import { EmailCustomizationSettings } from "./email-customization-settings"; + +vi.mock("@/modules/ee/whitelabel/email-customization/actions", () => ({ + removeOrganizationEmailLogoUrlAction: vi.fn(), + sendTestEmailAction: vi.fn(), + updateOrganizationEmailLogoUrlAction: vi.fn(), +})); + +vi.mock("@/modules/ui/components/file-input/lib/utils", () => ({ + uploadFile: 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", +}; + +describe("EmailCustomizationSettings", () => { + beforeEach(() => { + cleanup(); + }); + + it("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(); + }); + + it("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", + }); + }); + + it("calls updateOrganizationEmailLogoUrlAction after uploading and clicking save", async () => { + vi.mocked(uploadFile).mockResolvedValueOnce({ + uploaded: true, + 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]); + + // The component calls `uploadFile` then `updateOrganizationEmailLogoUrlAction` + expect(uploadFile).toHaveBeenCalledWith(testFile, ["jpeg", "png", "jpg", "webp"], "env-123"); + expect(updateOrganizationEmailLogoUrlAction).toHaveBeenCalledWith({ + organizationId: "org-123", + logoUrl: "https://example.com/new-uploaded-logo.png", + }); + }); + + it("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", + }); + }); + + it("displays upgrade prompt if hasWhiteLabelPermission is false", () => { + render(); + // Check for text about upgrading + expect(screen.getByText(/customize_email_with_a_higher_plan/i)).toBeInTheDocument(); + }); + + it("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/ee/whitelabel/email-customization/components/email-customization-settings.tsx b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx index ed42d4c1ea..c97f0001a4 100644 --- a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx +++ b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx @@ -17,7 +17,7 @@ import { useTranslate } from "@tolgee/react"; import { RepeatIcon, Trash2Icon } from "lucide-react"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useRef, useState } from "react"; +import React, { useRef, useState } from "react"; import { toast } from "react-hot-toast"; import { cn } from "@formbricks/lib/cn"; import { TAllowedFileExtension } from "@formbricks/types/common"; @@ -25,8 +25,6 @@ import { TOrganization } from "@formbricks/types/organizations"; import { TUser } from "@formbricks/types/user"; const allowedFileExtensions: TAllowedFileExtension[] = ["jpeg", "png", "jpg", "webp"]; -const DEFAULT_LOGO_URL = - "https://s3.eu-central-1.amazonaws.com/listmonk-formbricks/Formbricks-Light-transparent.png"; interface EmailCustomizationSettingsProps { organization: TOrganization; @@ -35,6 +33,7 @@ interface EmailCustomizationSettingsProps { isReadOnly: boolean; isFormbricksCloud: boolean; user: TUser | null; + fbLogoUrl: string; } export const EmailCustomizationSettings = ({ @@ -44,15 +43,16 @@ export const EmailCustomizationSettings = ({ isReadOnly, isFormbricksCloud, user, + fbLogoUrl, }: EmailCustomizationSettingsProps) => { const { t } = useTranslate(); const [logoFile, setLogoFile] = useState(null); - const [logoUrl, setLogoUrl] = useState(organization.whitelabel?.logoUrl || DEFAULT_LOGO_URL); + const [logoUrl, setLogoUrl] = useState(organization.whitelabel?.logoUrl || fbLogoUrl); const [isSaving, setIsSaving] = useState(false); const inputRef = useRef(null) as React.RefObject; - const isDefaultLogo = logoUrl === DEFAULT_LOGO_URL; + const isDefaultLogo = logoUrl === fbLogoUrl; const router = useRouter(); @@ -202,13 +202,18 @@ export const EmailCustomizationSettings = ({
- @@ -233,7 +238,11 @@ export const EmailCustomizationSettings = ({
-
Logo key; + +vi.mock("@formbricks/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(); + }); + + it("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"); + }); + + it("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"); + }); + + it("renders the children content", async () => { + const emailTemplateElement = await EmailTemplate({ + ...defaultProps, + }); + + render(emailTemplateElement); + + expect(screen.getByTestId("child-text")).toBeInTheDocument(); + }); + + it("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(); + }); + + it("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/components/email-template.tsx b/apps/web/modules/email/components/email-template.tsx index 87811b7669..92ad28ab29 100644 --- a/apps/web/modules/email/components/email-template.tsx +++ b/apps/web/modules/email/components/email-template.tsx @@ -1,15 +1,15 @@ import { Body, Container, Html, Img, Link, Section, Tailwind, Text } from "@react-email/components"; import { TFnType } from "@tolgee/react"; -import { IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants"; +import React from "react"; +import { FB_LOGO_URL, IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants"; -const fbLogoUrl = - "https://s3.eu-central-1.amazonaws.com/listmonk-formbricks/Formbricks-Light-transparent.png"; +const fbLogoUrl = FB_LOGO_URL; const logoLink = "https://formbricks.com?utm_source=email_header&utm_medium=email"; interface EmailTemplateProps { - children: React.ReactNode; - logoUrl?: string; - t: TFnType; + readonly children: React.ReactNode; + readonly logoUrl?: string; + readonly t: TFnType; } export async function EmailTemplate({ @@ -30,10 +30,15 @@ export async function EmailTemplate({
{isDefaultLogo ? ( - Logo + Logo ) : ( - Logo + Logo )}
diff --git a/apps/web/modules/email/emails/survey/follow-up.test.tsx b/apps/web/modules/email/emails/survey/follow-up.test.tsx new file mode 100644 index 0000000000..f6b9c62321 --- /dev/null +++ b/apps/web/modules/email/emails/survey/follow-up.test.tsx @@ -0,0 +1,88 @@ +import { getTranslate } from "@/tolgee/server"; +import "@testing-library/jest-dom/vitest"; +import { render, screen } from "@testing-library/react"; +import { DefaultParamType, TFnType, TranslationKey } from "@tolgee/react/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { FollowUpEmail } from "./follow-up"; + +vi.mock("@formbricks/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", +})); + +vi.mock("@/tolgee/server", () => ({ + getTranslate: vi.fn(), +})); + +const defaultProps = { + html: "

Test HTML Content

", + logoUrl: "https://example.com/custom-logo.png", +}; + +describe("FollowUpEmail", () => { + beforeEach(() => { + vi.mocked(getTranslate).mockResolvedValue( + ((key: string) => key) as TFnType + ); + }); + + it("renders the default logo if no custom logo is provided", async () => { + const followUpEmailElement = await FollowUpEmail({ + ...defaultProps, + logoUrl: undefined, + }); + + render(followUpEmailElement); + + const logoImage = screen.getByAltText("Logo"); + expect(logoImage).toBeInTheDocument(); + expect(logoImage).toHaveAttribute("src", "https://example.com/mock-logo.png"); + }); + + it("renders the custom logo if provided", async () => { + const followUpEmailElement = await FollowUpEmail({ + ...defaultProps, + }); + + render(followUpEmailElement); + + const logoImage = screen.getByAltText("Logo"); + expect(logoImage).toBeInTheDocument(); + expect(logoImage).toHaveAttribute("src", "https://example.com/custom-logo.png"); + }); + + it("renders the HTML content", async () => { + const followUpEmailElement = await FollowUpEmail({ + ...defaultProps, + }); + + render(followUpEmailElement); + + expect(screen.getByText("Test HTML Content")).toBeInTheDocument(); + }); + + it("renders the imprint and privacy policy links if provided", async () => { + const followUpEmailElement = await FollowUpEmail({ + ...defaultProps, + }); + + render(followUpEmailElement); + + expect(screen.getByText("emails.imprint")).toBeInTheDocument(); + expect(screen.getByText("emails.privacy_policy")).toBeInTheDocument(); + }); + + it("renders the imprint address if provided", async () => { + const followUpEmailElement = await FollowUpEmail({ + ...defaultProps, + }); + + render(followUpEmailElement); + + expect(screen.getByText("emails.powered_by_formbricks")).toBeInTheDocument(); + expect(screen.getByText("Imprint Address")).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/email/emails/survey/follow-up.tsx b/apps/web/modules/email/emails/survey/follow-up.tsx index 613a2575cf..fed887ea88 100644 --- a/apps/web/modules/email/emails/survey/follow-up.tsx +++ b/apps/web/modules/email/emails/survey/follow-up.tsx @@ -1,15 +1,22 @@ import { getTranslate } from "@/tolgee/server"; import { Body, Container, Html, Img, Link, Section, Tailwind, Text } from "@react-email/components"; import dompurify from "isomorphic-dompurify"; -import { IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants"; +import React from "react"; +import { FB_LOGO_URL, IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants"; + +const fbLogoUrl = FB_LOGO_URL; +const logoLink = "https://formbricks.com?utm_source=email_header&utm_medium=email"; interface FollowUpEmailProps { - html: string; - logoUrl?: string; + readonly html: string; + readonly logoUrl?: string; } export async function FollowUpEmail({ html, logoUrl }: FollowUpEmailProps): Promise { const t = await getTranslate(); + console.log(t("emails.imprint")); + const isDefaultLogo = !logoUrl || logoUrl === fbLogoUrl; + return ( @@ -18,11 +25,15 @@ export async function FollowUpEmail({ html, logoUrl }: FollowUpEmailProps): Prom style={{ fontFamily: "'Jost', 'Helvetica Neue', 'Segoe UI', 'Helvetica', 'sans-serif'", }}> - {logoUrl && ( -
+
+ {isDefaultLogo ? ( + + Logo + + ) : ( Logo -
- )} + )} +
Click or drag to upload files.

({ // mock react cache const testCache = (func: T) => func; -vi.mock("react", () => { - const originalModule = vi.importActual("react"); +vi.mock("react", async () => { + const react = await vi.importActual("react"); + return { - ...originalModule, + ...react, cache: testCache, }; }); +vi.mock("@tolgee/react", () => ({ + useTranslate: () => { + return { + t: (key: string) => key, + }; + }, +})); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + back: vi.fn(), + forward: vi.fn(), + prefetch: vi.fn(), + refresh: vi.fn(), + }), +})); + // mock server-only vi.mock("server-only", () => { return {}; }); +vi.mock("@prisma/client", async () => { + const actual = await vi.importActual("@prisma/client"); + + return { + ...actual, + Prisma: actual.Prisma, + PrismaClient: class { + $connect() { + return Promise.resolve(); + } + $disconnect() { + return Promise.resolve(); + } + $extends() { + return this; + } + }, + }; +}); + +if (typeof URL.revokeObjectURL !== "function") { + URL.revokeObjectURL = () => {}; +} + +if (typeof URL.createObjectURL !== "function") { + URL.createObjectURL = () => "blob://fake-url"; +} + beforeEach(() => { vi.resetModules(); vi.resetAllMocks(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a8c63488b..5dd8164b33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -330,6 +330,9 @@ importers: '@tanstack/react-table': specifier: 8.20.6 version: 8.20.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@testing-library/jest-dom': + specifier: 6.6.3 + version: 6.6.3 '@tolgee/cli': specifier: 2.8.1 version: 2.8.1(jiti@2.4.1)(typescript@5.7.2) @@ -532,6 +535,9 @@ importers: '@neshca/cache-handler': specifier: 1.9.0 version: 1.9.0(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0) + '@testing-library/react': + specifier: 16.2.0 + version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/bcryptjs': specifier: 2.4.6 version: 2.4.6 @@ -553,6 +559,9 @@ importers: '@types/qrcode': specifier: 1.5.5 version: 1.5.5 + '@types/testing-library__react': + specifier: 10.2.0 + version: 10.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@vitest/coverage-v8': specifier: 2.1.8 version: 2.1.8(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) @@ -5575,6 +5584,25 @@ packages: resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.2.0': + resolution: {integrity: sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@testing-library/user-event@14.5.2': resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} engines: {node: '>=12', npm: '>=6'} @@ -5816,6 +5844,10 @@ packages: '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/testing-library__react@10.2.0': + resolution: {integrity: sha512-KbU7qVfEwml8G5KFxM+xEfentAAVj/SOQSjW0+HqzjPE0cXpt0IpSamfX4jGYCImznDHgQcfXBPajS7HjLZduw==} + deprecated: This is a stub types definition. testing-library__react provides its own type definitions, so you do not need this installed. + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -20003,6 +20035,26 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 + '@testing-library/jest-dom@6.6.3': + dependencies: + '@adobe/css-tools': 4.4.1 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + + '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.26.7 + '@testing-library/dom': 10.4.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': dependencies: '@testing-library/dom': 10.4.0 @@ -20263,6 +20315,16 @@ snapshots: dependencies: '@types/node': 22.10.2 + '@types/testing-library__react@10.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@testing-library/react': 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + transitivePeerDependencies: + - '@testing-library/dom' + - '@types/react' + - '@types/react-dom' + - react + - react-dom + '@types/trusted-types@2.0.7': optional: true diff --git a/sonar-project.properties b/sonar-project.properties index 897155f65b..e7a41cb5a7 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -9,8 +9,6 @@ sonar.exclusions=**/node_modules/**,**/.next/**,**/dist/**,**/build/**,**/*.test sonar.tests=apps/web sonar.test.inclusions=**/*.test.*,**/*.spec.* sonar.javascript.lcov.reportPaths=apps/web/coverage/lcov.info -sonar.coverage.exclusions=**/constants.ts,**/route.ts,**/openapi.ts,**/openapi-document.ts,modules/**/types/**,playwright/**,**/*.test.*,**/*.spec.* -sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/constants.ts,**/route.ts,**/openapi.ts,**/openapi-document.ts,modules/**/types/** # TypeScript configuration sonar.typescript.tsconfigPath=apps/web/tsconfig.json @@ -23,5 +21,5 @@ sonar.scm.exclusions.disabled=false sonar.sourceEncoding=UTF-8 # Coverage -sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/constants.ts,**/route.ts,modules/**/types/**,**/*.stories.*,**/mocks/** -sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/constants.ts,**/route.ts,modules/**/types/** \ No newline at end of file +sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/constants.ts,**/route.ts,modules/**/types/**,**/*.stories.*,**/mocks/**,**/openapi.ts,**/openapi-document.ts,playwright/** +sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/constants.ts,**/route.ts,modules/**/types/**,**/openapi.ts,**/openapi-document.ts