mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-27 08:50:38 -06:00
Compare commits
27 Commits
v3.8.2
...
chore/upgr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fafcd7e2df | ||
|
|
9c47da0a04 | ||
|
|
9b4f839410 | ||
|
|
81d717ccff | ||
|
|
2e979c7323 | ||
|
|
4dfd15d6dd | ||
|
|
5b9bf3ff43 | ||
|
|
d2f7485098 | ||
|
|
f8fee1fba7 | ||
|
|
19249ca00f | ||
|
|
01e5700340 | ||
|
|
ff2f7660a6 | ||
|
|
2bc05e2b4a | ||
|
|
137c6447b7 | ||
|
|
ebc8f0c917 | ||
|
|
5a8d10b5b4 | ||
|
|
875815fb62 | ||
|
|
cdf526e130 | ||
|
|
b685032b34 | ||
|
|
a171f9cb00 | ||
|
|
c452f05ec2 | ||
|
|
93d91f80f2 | ||
|
|
7b764c8427 | ||
|
|
016289c8cb | ||
|
|
93a9575389 | ||
|
|
9e265adf14 | ||
|
|
eb08a0ed14 |
44
.github/dependabot.yml
vendored
44
.github/dependabot.yml
vendored
@@ -7,76 +7,56 @@ version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # For pnpm monorepos, use npm ecosystem
|
||||
directory: "/" # Root package.json
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
versioning-strategy: increase
|
||||
|
||||
# Apps directory packages
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/apps/demo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/apps/demo-react-native"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/apps/storybook"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/apps/web"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
# Packages directory
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/database"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/lib"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/types"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/config-eslint"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/config-prettier"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/config-typescript"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/js-core"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/surveys"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/logger"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/js"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/react-native"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/vite-plugins"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"github.copilot.chat.codeGeneration.instructions": [
|
||||
{
|
||||
"text": "When generating tests, always use vitest and use the `test` function instead of `it`."
|
||||
}
|
||||
],
|
||||
"sonarlint.connectedMode.project": {
|
||||
"connectionId": "formbricks",
|
||||
"projectKey": "formbricks_formbricks"
|
||||
|
||||
@@ -35,6 +35,6 @@
|
||||
"prop-types": "15.8.1",
|
||||
"storybook": "8.6.12",
|
||||
"tsup": "8.4.0",
|
||||
"vite": "6.2.5"
|
||||
"vite": "6.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,13 +81,14 @@ RUN corepack enable
|
||||
RUN apk add --no-cache curl \
|
||||
&& apk add --no-cache supercronic \
|
||||
# && addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 nextjs
|
||||
&& addgroup -S nextjs \
|
||||
&& adduser -S -u 1001 -G nextjs nextjs
|
||||
|
||||
WORKDIR /home/nextjs
|
||||
|
||||
# Ensure no write permissions are assigned to the copied resources
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./
|
||||
RUN chmod -R 755 ./
|
||||
COPY --from=installer /app/apps/web/.next/standalone ./
|
||||
RUN chown -R nextjs:nextjs ./ && chmod -R 755 ./
|
||||
|
||||
COPY --from=installer /app/apps/web/next.config.mjs .
|
||||
RUN chmod 644 ./next.config.mjs
|
||||
@@ -95,38 +96,38 @@ RUN chmod 644 ./next.config.mjs
|
||||
COPY --from=installer /app/apps/web/package.json .
|
||||
RUN chmod 644 ./package.json
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
|
||||
RUN chmod -R 755 ./apps/web/.next/static
|
||||
COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static
|
||||
RUN chown -R nextjs:nextjs ./apps/web/.next/static && chmod -R 755 ./apps/web/.next/static
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
||||
RUN chmod -R 755 ./apps/web/public
|
||||
COPY --from=installer /app/apps/web/public ./apps/web/public
|
||||
RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||
RUN chmod 644 ./packages/database/schema.prisma
|
||||
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
|
||||
RUN chmod 644 ./packages/database/package.json
|
||||
COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
|
||||
RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
|
||||
RUN chmod -R 755 ./packages/database/migration
|
||||
COPY --from=installer /app/packages/database/migration ./packages/database/migration
|
||||
RUN chown -R nextjs:nextjs ./packages/database/migration && chmod -R 755 ./packages/database/migration
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
|
||||
RUN chmod -R 755 ./packages/database/src
|
||||
COPY --from=installer /app/packages/database/src ./packages/database/src
|
||||
RUN chown -R nextjs:nextjs ./packages/database/src && chmod -R 755 ./packages/database/src
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
|
||||
RUN chmod -R 755 ./packages/database/node_modules
|
||||
COPY --from=installer /app/packages/database/node_modules ./packages/database/node_modules
|
||||
RUN chown -R nextjs:nextjs ./packages/database/node_modules && chmod -R 755 ./packages/database/node_modules
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
||||
RUN chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
||||
COPY --from=installer /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
||||
RUN chown -R nextjs:nextjs ./packages/database/node_modules/@formbricks/logger/dist && chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||
RUN chmod -R 755 ./node_modules/@prisma/client
|
||||
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma
|
||||
RUN chmod -R 755 ./node_modules/.prisma
|
||||
COPY --from=installer /app/node_modules/.prisma ./node_modules/.prisma
|
||||
RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules/.prisma
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
|
||||
RUN chmod 644 ./prisma_version.txt
|
||||
COPY --from=installer /prisma_version.txt .
|
||||
RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
|
||||
|
||||
COPY /docker/cronjobs /app/docker/cronjobs
|
||||
RUN chmod -R 755 /app/docker/cronjobs
|
||||
|
||||
@@ -112,7 +112,7 @@ export const LandingSidebar = ({
|
||||
{/* Dropdown Items */}
|
||||
|
||||
{dropdownNavigation.map((link) => (
|
||||
<Link href={link.href} target={link.target} className="flex w-full items-center">
|
||||
<Link id={link.href} href={link.href} target={link.target} className="flex w-full items-center">
|
||||
<DropdownMenuItem>
|
||||
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||
{link.label}
|
||||
|
||||
@@ -3,7 +3,7 @@ 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, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
@@ -71,7 +71,7 @@ describe("ProjectOnboardingLayout", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("redirects to /auth/login if there is no session", async () => {
|
||||
test("redirects to /auth/login if there is no session", async () => {
|
||||
// Mock no session
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
||||
|
||||
@@ -85,7 +85,7 @@ describe("ProjectOnboardingLayout", () => {
|
||||
expect(layoutElement).toBeUndefined();
|
||||
});
|
||||
|
||||
it("throws an error if user does not exist", async () => {
|
||||
test("throws an error if user does not exist", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
||||
user: { id: "user-123" },
|
||||
});
|
||||
@@ -99,7 +99,7 @@ describe("ProjectOnboardingLayout", () => {
|
||||
).rejects.toThrow("common.user_not_found");
|
||||
});
|
||||
|
||||
it("throws AuthorizationError if user cannot access organization", async () => {
|
||||
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);
|
||||
@@ -112,7 +112,7 @@ describe("ProjectOnboardingLayout", () => {
|
||||
).rejects.toThrow("common.not_authorized");
|
||||
});
|
||||
|
||||
it("throws an error if organization does not exist", async () => {
|
||||
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);
|
||||
@@ -126,7 +126,7 @@ describe("ProjectOnboardingLayout", () => {
|
||||
).rejects.toThrow("common.organization_not_found");
|
||||
});
|
||||
|
||||
it("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
|
||||
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);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { Session } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
@@ -41,7 +41,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders successfully when environment is found", async () => {
|
||||
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,
|
||||
@@ -62,7 +62,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
|
||||
expect(screen.getByTestId("child")).toHaveTextContent("Survey Editor Content");
|
||||
});
|
||||
|
||||
it("throws an error when environment is not found", async () => {
|
||||
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,
|
||||
@@ -79,7 +79,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
|
||||
).rejects.toThrow("common.environment_not_found");
|
||||
});
|
||||
|
||||
it("calls redirect when session is null", async () => {
|
||||
test("calls redirect when session is null", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any,
|
||||
session: undefined as unknown as Session,
|
||||
@@ -98,7 +98,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
|
||||
).rejects.toThrow("Redirect called");
|
||||
});
|
||||
|
||||
it("throws error if user is null", async () => {
|
||||
test("throws error if user is null", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any,
|
||||
session: { user: { id: "user1" } } as Session,
|
||||
|
||||
@@ -2,7 +2,7 @@ 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, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { PosthogIdentify } from "./PosthogIdentify";
|
||||
@@ -18,7 +18,7 @@ describe("PosthogIdentify", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("identifies the user and sets groups when isPosthogEnabled is true", () => {
|
||||
test("identifies the user and sets groups when isPosthogEnabled is true", () => {
|
||||
const mockIdentify = vi.fn();
|
||||
const mockGroup = vi.fn();
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("PosthogIdentify", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does nothing if isPosthogEnabled is false", () => {
|
||||
test("does nothing if isPosthogEnabled is false", () => {
|
||||
const mockIdentify = vi.fn();
|
||||
const mockGroup = vi.fn();
|
||||
|
||||
@@ -95,7 +95,7 @@ describe("PosthogIdentify", () => {
|
||||
expect(mockGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if session user is missing", () => {
|
||||
test("does nothing if session user is missing", () => {
|
||||
const mockIdentify = vi.fn();
|
||||
const mockGroup = vi.fn();
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("PosthogIdentify", () => {
|
||||
expect(mockGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("identifies user but does not group if environmentId/organizationId not provided", () => {
|
||||
test("identifies user but does not group if environmentId/organizationId not provided", () => {
|
||||
const mockIdentify = vi.fn();
|
||||
const mockGroup = vi.fn();
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ export const TYPE_MAPPING = {
|
||||
[TSurveyQuestionTypeEnum.Address]: ["rich_text"],
|
||||
[TSurveyQuestionTypeEnum.Matrix]: ["rich_text"],
|
||||
[TSurveyQuestionTypeEnum.Cal]: ["checkbox"],
|
||||
[TSurveyQuestionTypeEnum.ContactInfo]: ["rich_text"],
|
||||
[TSurveyQuestionTypeEnum.Ranking]: ["rich_text"],
|
||||
};
|
||||
|
||||
export const UNSUPPORTED_TYPES_BY_NOTION = [
|
||||
|
||||
@@ -2,7 +2,7 @@ import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { Session } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { TMembership } from "@formbricks/types/memberships";
|
||||
@@ -53,7 +53,7 @@ describe("EnvLayout", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders successfully when all dependencies return valid data", async () => {
|
||||
test("renders successfully when all dependencies return valid data", 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,
|
||||
@@ -77,7 +77,7 @@ describe("EnvLayout", () => {
|
||||
expect(screen.getByTestId("child")).toHaveTextContent("Content");
|
||||
});
|
||||
|
||||
it("throws error if project is not found", async () => {
|
||||
test("throws error if project is not found", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any,
|
||||
session: { user: { id: "user1" } } as Session,
|
||||
@@ -97,7 +97,7 @@ describe("EnvLayout", () => {
|
||||
).rejects.toThrow("common.project_not_found");
|
||||
});
|
||||
|
||||
it("throws error if membership is not found", async () => {
|
||||
test("throws error if membership is not found", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any,
|
||||
session: { user: { id: "user1" } } as Session,
|
||||
@@ -115,7 +115,7 @@ describe("EnvLayout", () => {
|
||||
).rejects.toThrow("common.membership_not_found");
|
||||
});
|
||||
|
||||
it("calls redirect when session is null", async () => {
|
||||
test("calls redirect when session is null", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any,
|
||||
session: undefined as unknown as Session,
|
||||
@@ -134,7 +134,7 @@ describe("EnvLayout", () => {
|
||||
).rejects.toThrow("Redirect called");
|
||||
});
|
||||
|
||||
it("throws error if user is null", async () => {
|
||||
test("throws error if user is null", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any,
|
||||
session: { user: { id: "user1" } } as Session,
|
||||
|
||||
@@ -22,7 +22,7 @@ export const OrganizationSettingsNavbar = ({
|
||||
loading,
|
||||
}: OrganizationSettingsNavbarProps) => {
|
||||
const pathname = usePathname();
|
||||
const { isMember } = getAccessFlags(membershipRole);
|
||||
const { isMember, isOwner } = getAccessFlags(membershipRole);
|
||||
const isPricingDisabled = isMember;
|
||||
const { t } = useTranslate();
|
||||
|
||||
@@ -59,6 +59,7 @@ export const OrganizationSettingsNavbar = ({
|
||||
label: t("common.api_keys"),
|
||||
href: `/environments/${environmentId}/settings/api-keys`,
|
||||
current: pathname?.includes("/api-keys"),
|
||||
hidden: !isOwner,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import Page from "./page";
|
||||
@@ -84,7 +84,7 @@ describe("Page", () => {
|
||||
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("renders the page with organization settings", async () => {
|
||||
test("renders the page with organization settings", async () => {
|
||||
const props = {
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
};
|
||||
@@ -94,7 +94,7 @@ describe("Page", () => {
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders if session user id empty", async () => {
|
||||
test("renders if session user id empty", async () => {
|
||||
mockEnvironmentAuth.session.user.id = "";
|
||||
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
||||
@@ -108,7 +108,7 @@ describe("Page", () => {
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("handles getEnvironmentAuth error", async () => {
|
||||
test("handles getEnvironmentAuth error", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockRejectedValue(new Error("Authentication error"));
|
||||
|
||||
const props = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import type { JSX } from "react";
|
||||
@@ -42,7 +43,7 @@ export const QuestionSummaryHeader = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<div className="space-y-2 px-4 pt-6 pb-5 md:px-6">
|
||||
<div className={"align-center flex justify-between gap-4"}>
|
||||
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">
|
||||
{formatTextWithSlashes(
|
||||
@@ -69,6 +70,7 @@ export const QuestionSummaryHeader = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SettingsId title={t("common.question_id")} id={questionSummary.question.id}></SettingsId>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, 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";
|
||||
@@ -85,7 +85,7 @@ describe("SurveyAnalysisCTA - handleCopyLink", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("calls copySurveyLink and clipboard.writeText on success", async () => {
|
||||
test("calls copySurveyLink and clipboard.writeText on success", async () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
@@ -108,7 +108,7 @@ describe("SurveyAnalysisCTA - handleCopyLink", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error toast on failure", async () => {
|
||||
test("shows error toast on failure", async () => {
|
||||
refreshSingleUseIdSpy.mockImplementationOnce(() => Promise.reject(new Error("fail")));
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
|
||||
@@ -390,7 +390,7 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
|
||||
value && handleDatePickerClose();
|
||||
}}>
|
||||
<DropdownMenuTrigger asChild className="focus:bg-muted cursor-pointer outline-none">
|
||||
<div className="min-w-auto h-auto rounded-md border border-slate-200 bg-white p-3 hover:border-slate-300 sm:flex sm:px-6 sm:py-3">
|
||||
<div className="h-auto min-w-auto rounded-md border border-slate-200 bg-white p-3 hover:border-slate-300 sm:flex sm:px-6 sm:py-3">
|
||||
<div className="hidden w-full items-center justify-between sm:flex">
|
||||
<span className="text-sm text-slate-700">{t("common.download")}</span>
|
||||
<ArrowDownToLineIcon className="ml-2 h-4 w-4" />
|
||||
@@ -416,14 +416,14 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
|
||||
onClick={() => {
|
||||
handleDowndloadResponses(FilterDownload.FILTER, "csv");
|
||||
}}>
|
||||
<p className="text-slate-700">{t("environments.surveys.summary.current_selection_csv")}</p>
|
||||
<p className="text-slate-700">{t("environments.surveys.summary.filtered_responses_csv")}</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleDowndloadResponses(FilterDownload.FILTER, "xlsx");
|
||||
}}>
|
||||
<p className="text-slate-700">
|
||||
{t("environments.surveys.summary.current_selection_excel")}
|
||||
{t("environments.surveys.summary.filtered_responses_excel")}
|
||||
</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import AppLayout from "./layout";
|
||||
@@ -59,7 +59,7 @@ describe("(app) AppLayout", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders child content and all sub-components when user exists", async () => {
|
||||
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);
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("(app) AppLayout", () => {
|
||||
expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("skips FormbricksClient if no user is present", async () => {
|
||||
test("skips FormbricksClient if no user is present", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
||||
|
||||
const element = await AppLayout({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import AppLayout from "../(auth)/layout";
|
||||
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
@@ -18,7 +18,7 @@ vi.mock("@/modules/ui/components/no-mobile-overlay", () => ({
|
||||
}));
|
||||
|
||||
describe("(auth) AppLayout", () => {
|
||||
it("renders the NoMobileOverlay and IntercomClient, plus children", async () => {
|
||||
test("renders the NoMobileOverlay and IntercomClient, plus children", async () => {
|
||||
const appLayoutElement = await AppLayout({
|
||||
children: <div data-testid="child-content">Hello from children!</div>,
|
||||
});
|
||||
|
||||
@@ -392,6 +392,19 @@ const getValue = (colType: string, value: string | string[] | Date | number | Re
|
||||
},
|
||||
];
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const content = value.join("\n");
|
||||
return [
|
||||
{
|
||||
text: {
|
||||
content:
|
||||
content.length > NOTION_RICH_TEXT_LIMIT
|
||||
? truncateText(content, NOTION_RICH_TEXT_LIMIT)
|
||||
: content,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
text: {
|
||||
|
||||
@@ -12,9 +12,10 @@ type FollowUpResult = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const evaluateFollowUp = async (
|
||||
export const evaluateFollowUp = async (
|
||||
followUpId: string,
|
||||
followUpAction: TSurveyFollowUpAction,
|
||||
survey: TSurvey,
|
||||
response: TResponse,
|
||||
organization: TOrganization
|
||||
): Promise<void> => {
|
||||
@@ -22,6 +23,25 @@ const evaluateFollowUp = async (
|
||||
const { to, subject, body, replyTo } = properties;
|
||||
const toValueFromResponse = response.data[to];
|
||||
const logoUrl = organization.whitelabel?.logoUrl || "";
|
||||
|
||||
// Check if 'to' is a direct email address (team member or user email)
|
||||
const parsedEmailTo = z.string().email().safeParse(to);
|
||||
if (parsedEmailTo.success) {
|
||||
// 'to' is a valid email address, send email directly
|
||||
await sendFollowUpEmail({
|
||||
html: body,
|
||||
subject,
|
||||
to: parsedEmailTo.data,
|
||||
replyTo,
|
||||
survey,
|
||||
response,
|
||||
attachResponseData: properties.attachResponseData,
|
||||
logoUrl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If not a direct email, check if it's a question ID or hidden field ID
|
||||
if (!toValueFromResponse) {
|
||||
throw new Error(`"To" value not found in response data for followup: ${followUpId}`);
|
||||
}
|
||||
@@ -31,7 +51,16 @@ const evaluateFollowUp = async (
|
||||
const parsedResult = z.string().email().safeParse(toValueFromResponse);
|
||||
if (parsedResult.data) {
|
||||
// send email to this email address
|
||||
await sendFollowUpEmail(body, subject, parsedResult.data, replyTo, logoUrl);
|
||||
await sendFollowUpEmail({
|
||||
html: body,
|
||||
subject,
|
||||
to: parsedResult.data,
|
||||
replyTo,
|
||||
logoUrl,
|
||||
survey,
|
||||
response,
|
||||
attachResponseData: properties.attachResponseData,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Email address is not valid for followup: ${followUpId}`);
|
||||
}
|
||||
@@ -42,7 +71,16 @@ const evaluateFollowUp = async (
|
||||
}
|
||||
const parsedResult = z.string().email().safeParse(emailAddress);
|
||||
if (parsedResult.data) {
|
||||
await sendFollowUpEmail(body, subject, parsedResult.data, replyTo, logoUrl);
|
||||
await sendFollowUpEmail({
|
||||
html: body,
|
||||
subject,
|
||||
to: parsedResult.data,
|
||||
replyTo,
|
||||
logoUrl,
|
||||
survey,
|
||||
response,
|
||||
attachResponseData: properties.attachResponseData,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Email address is not valid for followup: ${followUpId}`);
|
||||
}
|
||||
@@ -53,7 +91,7 @@ export const sendSurveyFollowUps = async (
|
||||
survey: TSurvey,
|
||||
response: TResponse,
|
||||
organization: TOrganization
|
||||
) => {
|
||||
): Promise<FollowUpResult[]> => {
|
||||
const followUpPromises = survey.followUps.map(async (followUp): Promise<FollowUpResult> => {
|
||||
const { trigger } = followUp;
|
||||
|
||||
@@ -70,7 +108,7 @@ export const sendSurveyFollowUps = async (
|
||||
}
|
||||
}
|
||||
|
||||
return evaluateFollowUp(followUp.id, followUp.action, response, organization)
|
||||
return evaluateFollowUp(followUp.id, followUp.action, survey, response, organization)
|
||||
.then(() => ({
|
||||
followUpId: followUp.id,
|
||||
status: "success" as const,
|
||||
@@ -92,4 +130,6 @@ export const sendSurveyFollowUps = async (
|
||||
if (errors.length > 0) {
|
||||
logger.error(errors, "Follow-up processing errors");
|
||||
}
|
||||
|
||||
return followUpResults;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyContactInfoQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
|
||||
export const mockEndingId1 = "mpkt4n5krsv2ulqetle7b9e7";
|
||||
export const mockEndingId2 = "ge0h63htnmgq6kwx1suh9cyi";
|
||||
|
||||
export const mockResponseEmailFollowUp: TSurvey["followUps"][number] = {
|
||||
id: "cm9gpuazd0002192z67olbfdt",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "cm9gptbhg0000192zceq9ayuc",
|
||||
name: "nice follow up",
|
||||
trigger: {
|
||||
type: "response",
|
||||
properties: null,
|
||||
},
|
||||
action: {
|
||||
type: "send-email",
|
||||
properties: {
|
||||
to: "vjniuob08ggl8dewl0hwed41",
|
||||
body: '<p class="fb-editor-paragraph"><span>Hey 👋</span><br><br><span>Thanks for taking the time to respond, we will be in touch shortly.</span><br><br><span>Have a great day!</span></p>',
|
||||
from: "noreply@example.com",
|
||||
replyTo: ["test@user.com"],
|
||||
subject: "Thanks for your answers!",
|
||||
attachResponseData: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockEndingFollowUp: TSurvey["followUps"][number] = {
|
||||
id: "j0g23cue6eih6xs5m0m4cj50",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "cm9gptbhg0000192zceq9ayuc",
|
||||
name: "nice follow up",
|
||||
trigger: {
|
||||
type: "endings",
|
||||
properties: {
|
||||
endingIds: [mockEndingId1],
|
||||
},
|
||||
},
|
||||
action: {
|
||||
type: "send-email",
|
||||
properties: {
|
||||
to: "vjniuob08ggl8dewl0hwed41",
|
||||
body: '<p class="fb-editor-paragraph"><span>Hey 👋</span><br><br><span>Thanks for taking the time to respond, we will be in touch shortly.</span><br><br><span>Have a great day!</span></p>',
|
||||
from: "noreply@example.com",
|
||||
replyTo: ["test@user.com"],
|
||||
subject: "Thanks for your answers!",
|
||||
attachResponseData: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockDirectEmailFollowUp: TSurvey["followUps"][number] = {
|
||||
id: "yyc5sq1fqofrsyw4viuypeku",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "cm9gptbhg0000192zceq9ayuc",
|
||||
name: "nice follow up 1",
|
||||
trigger: {
|
||||
type: "response",
|
||||
properties: null,
|
||||
},
|
||||
action: {
|
||||
type: "send-email",
|
||||
properties: {
|
||||
to: "direct@email.com",
|
||||
body: '<p class="fb-editor-paragraph"><span>Hey 👋</span><br><br><span>Thanks for taking the time to respond, we will be in touch shortly.</span><br><br><span>Have a great day!</span></p>',
|
||||
from: "noreply@example.com",
|
||||
replyTo: ["test@user.com"],
|
||||
subject: "Thanks for your answers!",
|
||||
attachResponseData: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockFollowUps: TSurvey["followUps"] = [mockDirectEmailFollowUp, mockResponseEmailFollowUp];
|
||||
|
||||
export const mockSurvey: TSurvey = {
|
||||
id: "cm9gptbhg0000192zceq9ayuc",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Start from scratch",
|
||||
type: "link",
|
||||
environmentId: "cm98djl8e000919hpzi6a80zp",
|
||||
createdBy: "cm98dg3xm000019hpubj39vfi",
|
||||
status: "inProgress",
|
||||
welcomeCard: {
|
||||
html: {
|
||||
default: "Thanks for providing your feedback - let's go!",
|
||||
},
|
||||
enabled: false,
|
||||
headline: {
|
||||
default: "Welcome!",
|
||||
},
|
||||
buttonLabel: {
|
||||
default: "Next",
|
||||
},
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
},
|
||||
questions: [
|
||||
{
|
||||
id: "vjniuob08ggl8dewl0hwed41",
|
||||
type: "openText" as TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: {
|
||||
default: "What would you like to know?",
|
||||
},
|
||||
required: true,
|
||||
charLimit: {},
|
||||
inputType: "email",
|
||||
longAnswer: false,
|
||||
buttonLabel: {
|
||||
default: "Next",
|
||||
},
|
||||
placeholder: {
|
||||
default: "example@email.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
endings: [
|
||||
{
|
||||
id: "gt1yoaeb5a3istszxqbl08mk",
|
||||
type: "endScreen",
|
||||
headline: {
|
||||
default: "Thank you!",
|
||||
},
|
||||
subheader: {
|
||||
default: "We appreciate your feedback.",
|
||||
},
|
||||
buttonLink: "https://formbricks.com",
|
||||
buttonLabel: {
|
||||
default: "Create your own Survey",
|
||||
},
|
||||
},
|
||||
],
|
||||
hiddenFields: {
|
||||
enabled: true,
|
||||
fieldIds: [],
|
||||
},
|
||||
variables: [],
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
autoClose: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
isVerifyEmailEnabled: false,
|
||||
isSingleResponsePerEmailEnabled: false,
|
||||
isBackButtonHidden: false,
|
||||
projectOverwrites: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: {
|
||||
enabled: false,
|
||||
isEncrypted: true,
|
||||
},
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
showLanguageSwitch: null,
|
||||
languages: [],
|
||||
triggers: [],
|
||||
segment: null,
|
||||
followUps: mockFollowUps,
|
||||
};
|
||||
|
||||
export const mockContactQuestion: TSurveyContactInfoQuestion = {
|
||||
id: "zyoobxyolyqj17bt1i4ofr37",
|
||||
type: TSurveyQuestionTypeEnum.ContactInfo,
|
||||
email: {
|
||||
show: true,
|
||||
required: true,
|
||||
placeholder: {
|
||||
default: "Email",
|
||||
},
|
||||
},
|
||||
phone: {
|
||||
show: true,
|
||||
required: true,
|
||||
placeholder: {
|
||||
default: "Phone",
|
||||
},
|
||||
},
|
||||
company: {
|
||||
show: true,
|
||||
required: true,
|
||||
placeholder: {
|
||||
default: "Company",
|
||||
},
|
||||
},
|
||||
headline: {
|
||||
default: "Contact Question",
|
||||
},
|
||||
lastName: {
|
||||
show: true,
|
||||
required: true,
|
||||
placeholder: {
|
||||
default: "Last Name",
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
firstName: {
|
||||
show: true,
|
||||
required: true,
|
||||
placeholder: {
|
||||
default: "First Name",
|
||||
},
|
||||
},
|
||||
buttonLabel: {
|
||||
default: "Next",
|
||||
},
|
||||
backButtonLabel: {
|
||||
default: "Back",
|
||||
},
|
||||
};
|
||||
|
||||
export const mockContactEmailFollowUp: TSurvey["followUps"][number] = {
|
||||
...mockResponseEmailFollowUp,
|
||||
action: {
|
||||
...mockResponseEmailFollowUp.action,
|
||||
properties: {
|
||||
...mockResponseEmailFollowUp.action.properties,
|
||||
to: mockContactQuestion.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockSurveyWithContactQuestion: TSurvey = {
|
||||
...mockSurvey,
|
||||
questions: [mockContactQuestion],
|
||||
followUps: [mockContactEmailFollowUp],
|
||||
};
|
||||
|
||||
export const mockResponse: TResponse = {
|
||||
id: "response1",
|
||||
surveyId: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
variables: {},
|
||||
language: "en",
|
||||
data: {
|
||||
["vjniuob08ggl8dewl0hwed41"]: "test@example.com",
|
||||
},
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
meta: {},
|
||||
finished: true,
|
||||
notes: [],
|
||||
singleUseId: null,
|
||||
tags: [],
|
||||
displayId: null,
|
||||
};
|
||||
|
||||
export const mockResponseWithContactQuestion: TResponse = {
|
||||
...mockResponse,
|
||||
data: {
|
||||
zyoobxyolyqj17bt1i4ofr37: ["test", "user1", "test@user1.com", "99999999999", "sampleCompany"],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
import {
|
||||
mockContactEmailFollowUp,
|
||||
mockDirectEmailFollowUp,
|
||||
mockEndingFollowUp,
|
||||
mockEndingId2,
|
||||
mockResponse,
|
||||
mockResponseEmailFollowUp,
|
||||
mockResponseWithContactQuestion,
|
||||
mockSurvey,
|
||||
mockSurveyWithContactQuestion,
|
||||
} from "@/app/api/(internal)/pipeline/lib/tests/__mocks__/survey-follow-up.mock";
|
||||
import { sendFollowUpEmail } from "@/modules/email";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { evaluateFollowUp, sendSurveyFollowUps } from "../survey-follow-up";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/modules/email", () => ({
|
||||
sendFollowUpEmail: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Survey Follow Up", () => {
|
||||
const mockOrganization: Partial<TOrganization> = {
|
||||
id: "org1",
|
||||
name: "Test Org",
|
||||
whitelabel: {
|
||||
logoUrl: "https://example.com/logo.png",
|
||||
},
|
||||
};
|
||||
|
||||
describe("evaluateFollowUp", () => {
|
||||
test("sends email when to is a direct email address", async () => {
|
||||
const followUpId = mockDirectEmailFollowUp.id;
|
||||
const followUpAction = mockDirectEmailFollowUp.action;
|
||||
|
||||
await evaluateFollowUp(
|
||||
followUpId,
|
||||
followUpAction,
|
||||
mockSurvey,
|
||||
mockResponse,
|
||||
mockOrganization as TOrganization
|
||||
);
|
||||
|
||||
expect(sendFollowUpEmail).toHaveBeenCalledWith({
|
||||
html: mockDirectEmailFollowUp.action.properties.body,
|
||||
subject: mockDirectEmailFollowUp.action.properties.subject,
|
||||
to: mockDirectEmailFollowUp.action.properties.to,
|
||||
replyTo: mockDirectEmailFollowUp.action.properties.replyTo,
|
||||
survey: mockSurvey,
|
||||
response: mockResponse,
|
||||
attachResponseData: true,
|
||||
logoUrl: "https://example.com/logo.png",
|
||||
});
|
||||
});
|
||||
|
||||
test("sends email when to is a question ID with valid email", async () => {
|
||||
const followUpId = mockResponseEmailFollowUp.id;
|
||||
const followUpAction = mockResponseEmailFollowUp.action;
|
||||
|
||||
await evaluateFollowUp(
|
||||
followUpId,
|
||||
followUpAction,
|
||||
mockSurvey as TSurvey,
|
||||
mockResponse as TResponse,
|
||||
mockOrganization as TOrganization
|
||||
);
|
||||
|
||||
expect(sendFollowUpEmail).toHaveBeenCalledWith({
|
||||
html: mockResponseEmailFollowUp.action.properties.body,
|
||||
subject: mockResponseEmailFollowUp.action.properties.subject,
|
||||
to: mockResponse.data[mockResponseEmailFollowUp.action.properties.to],
|
||||
replyTo: mockResponseEmailFollowUp.action.properties.replyTo,
|
||||
survey: mockSurvey,
|
||||
response: mockResponse,
|
||||
attachResponseData: true,
|
||||
logoUrl: "https://example.com/logo.png",
|
||||
});
|
||||
});
|
||||
|
||||
test("sends email when to is a question ID with valid email in array", async () => {
|
||||
const followUpId = mockContactEmailFollowUp.id;
|
||||
const followUpAction = mockContactEmailFollowUp.action;
|
||||
|
||||
await evaluateFollowUp(
|
||||
followUpId,
|
||||
followUpAction,
|
||||
mockSurveyWithContactQuestion,
|
||||
mockResponseWithContactQuestion,
|
||||
mockOrganization as TOrganization
|
||||
);
|
||||
|
||||
expect(sendFollowUpEmail).toHaveBeenCalledWith({
|
||||
html: mockContactEmailFollowUp.action.properties.body,
|
||||
subject: mockContactEmailFollowUp.action.properties.subject,
|
||||
to: mockResponseWithContactQuestion.data[mockContactEmailFollowUp.action.properties.to][2],
|
||||
replyTo: mockContactEmailFollowUp.action.properties.replyTo,
|
||||
survey: mockSurveyWithContactQuestion,
|
||||
response: mockResponseWithContactQuestion,
|
||||
attachResponseData: true,
|
||||
logoUrl: "https://example.com/logo.png",
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error when to value is not found in response data", async () => {
|
||||
const followUpId = "followup1";
|
||||
const followUpAction = {
|
||||
...mockSurvey.followUps![0].action,
|
||||
properties: {
|
||||
...mockSurvey.followUps![0].action.properties,
|
||||
to: "nonExistentField",
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
evaluateFollowUp(
|
||||
followUpId,
|
||||
followUpAction,
|
||||
mockSurvey as TSurvey,
|
||||
mockResponse as TResponse,
|
||||
mockOrganization as TOrganization
|
||||
)
|
||||
).rejects.toThrow(`"To" value not found in response data for followup: ${followUpId}`);
|
||||
});
|
||||
|
||||
test("throws error when email address is invalid", async () => {
|
||||
const followUpId = mockResponseEmailFollowUp.id;
|
||||
const followUpAction = mockResponseEmailFollowUp.action;
|
||||
|
||||
const invalidResponse = {
|
||||
...mockResponse,
|
||||
data: {
|
||||
[mockResponseEmailFollowUp.action.properties.to]: "invalid-email",
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
evaluateFollowUp(
|
||||
followUpId,
|
||||
followUpAction,
|
||||
mockSurvey,
|
||||
invalidResponse,
|
||||
mockOrganization as TOrganization
|
||||
)
|
||||
).rejects.toThrow(`Email address is not valid for followup: ${followUpId}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendSurveyFollowUps", () => {
|
||||
test("skips follow-up when ending Id doesn't match", async () => {
|
||||
const responseWithDifferentEnding = {
|
||||
...mockResponse,
|
||||
endingId: mockEndingId2,
|
||||
};
|
||||
|
||||
const mockSurveyWithEndingFollowUp: TSurvey = {
|
||||
...mockSurvey,
|
||||
followUps: [mockEndingFollowUp],
|
||||
};
|
||||
|
||||
const results = await sendSurveyFollowUps(
|
||||
mockSurveyWithEndingFollowUp,
|
||||
responseWithDifferentEnding as TResponse,
|
||||
mockOrganization as TOrganization
|
||||
);
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
followUpId: mockEndingFollowUp.id,
|
||||
status: "skipped",
|
||||
},
|
||||
]);
|
||||
expect(sendFollowUpEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("processes follow-ups and log errors", async () => {
|
||||
const error = new Error("Test error");
|
||||
vi.mocked(sendFollowUpEmail).mockRejectedValueOnce(error);
|
||||
|
||||
const mockSurveyWithFollowUps: TSurvey = {
|
||||
...mockSurvey,
|
||||
followUps: [mockResponseEmailFollowUp],
|
||||
};
|
||||
|
||||
const results = await sendSurveyFollowUps(
|
||||
mockSurveyWithFollowUps,
|
||||
mockResponse,
|
||||
mockOrganization as TOrganization
|
||||
);
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
followUpId: mockResponseEmailFollowUp.id,
|
||||
status: "error",
|
||||
error: "Test error",
|
||||
},
|
||||
]);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
[`FollowUp ${mockResponseEmailFollowUp.id} failed: Test error`],
|
||||
"Follow-up processing errors"
|
||||
);
|
||||
});
|
||||
|
||||
test("successfully processes follow-ups", async () => {
|
||||
vi.mocked(sendFollowUpEmail).mockResolvedValueOnce(undefined);
|
||||
|
||||
const mockSurveyWithFollowUp: TSurvey = {
|
||||
...mockSurvey,
|
||||
followUps: [mockDirectEmailFollowUp],
|
||||
};
|
||||
|
||||
const results = await sendSurveyFollowUps(
|
||||
mockSurveyWithFollowUp,
|
||||
mockResponse,
|
||||
mockOrganization as TOrganization
|
||||
);
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
followUpId: mockDirectEmailFollowUp.id,
|
||||
status: "success",
|
||||
},
|
||||
]);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,7 @@ import { convertDatesInObject } from "@formbricks/lib/time";
|
||||
import { getPromptText } from "@formbricks/lib/utils/ai";
|
||||
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { handleIntegrations } from "./lib/handleIntegrations";
|
||||
|
||||
export const POST = async (request: Request) => {
|
||||
@@ -50,7 +51,7 @@ export const POST = async (request: Request) => {
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
throw new ResourceNotFoundError("Organization", "Organization not found");
|
||||
}
|
||||
|
||||
// Fetch webhooks
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { hashApiKey } from "@/modules/api/v2/management/lib/utils";
|
||||
import { getApiKeyWithPermissions } from "@/modules/organization/settings/api-keys/lib/api-key";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TAPIKeyEnvironmentPermission } from "@formbricks/types/auth";
|
||||
import { authenticateRequest } from "./auth";
|
||||
@@ -20,7 +20,7 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({
|
||||
}));
|
||||
|
||||
describe("getApiKeyWithPermissions", () => {
|
||||
it("should return API key data with permissions when valid key is provided", async () => {
|
||||
test("returns API key data with permissions when valid key is provided", async () => {
|
||||
const mockApiKeyData = {
|
||||
id: "api-key-id",
|
||||
organizationId: "org-id",
|
||||
@@ -51,7 +51,7 @@ describe("getApiKeyWithPermissions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null when API key is not found", async () => {
|
||||
test("returns null when API key is not found", async () => {
|
||||
vi.mocked(prisma.apiKey.findUnique).mockResolvedValue(null);
|
||||
|
||||
const result = await getApiKeyWithPermissions("invalid-key");
|
||||
@@ -85,31 +85,31 @@ describe("hasPermission", () => {
|
||||
},
|
||||
];
|
||||
|
||||
it("should return true for manage permission with any method", () => {
|
||||
test("returns true for manage permission with any method", () => {
|
||||
expect(hasPermission(permissions, "env-1", "GET")).toBe(true);
|
||||
expect(hasPermission(permissions, "env-1", "POST")).toBe(true);
|
||||
expect(hasPermission(permissions, "env-1", "DELETE")).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle write permission correctly", () => {
|
||||
test("handles write permission correctly", () => {
|
||||
expect(hasPermission(permissions, "env-2", "GET")).toBe(true);
|
||||
expect(hasPermission(permissions, "env-2", "POST")).toBe(true);
|
||||
expect(hasPermission(permissions, "env-2", "DELETE")).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle read permission correctly", () => {
|
||||
test("handles read permission correctly", () => {
|
||||
expect(hasPermission(permissions, "env-3", "GET")).toBe(true);
|
||||
expect(hasPermission(permissions, "env-3", "POST")).toBe(false);
|
||||
expect(hasPermission(permissions, "env-3", "DELETE")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for non-existent environment", () => {
|
||||
test("returns false for non-existent environment", () => {
|
||||
expect(hasPermission(permissions, "env-4", "GET")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authenticateRequest", () => {
|
||||
it("should return authentication data for valid API key", async () => {
|
||||
test("should return authentication data for valid API key", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "valid-api-key" },
|
||||
});
|
||||
@@ -159,13 +159,13 @@ describe("authenticateRequest", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null when no API key is provided", async () => {
|
||||
test("returns null when no API key is provided", async () => {
|
||||
const request = new Request("http://localhost");
|
||||
const result = await authenticateRequest(request);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when API key is invalid", async () => {
|
||||
test("returns null when API key is invalid", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "invalid-api-key" },
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { getActionClasses } from "./action-classes";
|
||||
@@ -43,7 +43,7 @@ describe("getActionClasses", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should successfully fetch action classes for given environment IDs", async () => {
|
||||
test("successfully fetches action classes for given environment IDs", async () => {
|
||||
// Mock the prisma findMany response
|
||||
vi.mocked(prisma.actionClass.findMany).mockResolvedValue(mockActionClasses);
|
||||
|
||||
@@ -61,14 +61,14 @@ describe("getActionClasses", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw DatabaseError when prisma query fails", async () => {
|
||||
test("throws DatabaseError when prisma query fails", async () => {
|
||||
// Mock the prisma findMany to throw an error
|
||||
vi.mocked(prisma.actionClass.findMany).mockRejectedValue(new Error("Database error"));
|
||||
|
||||
await expect(getActionClasses(mockEnvironmentIds)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
it("should handle empty environment IDs array", async () => {
|
||||
test("handles empty environment IDs array", async () => {
|
||||
// Mock the prisma findMany response
|
||||
vi.mocked(prisma.actionClass.findMany).mockResolvedValue([]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { IntercomClient } from "./IntercomClient";
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("IntercomClient", () => {
|
||||
global.window.Intercom = originalWindowIntercom;
|
||||
});
|
||||
|
||||
it("calls Intercom with user data when isIntercomConfigured is true and user is provided", () => {
|
||||
test("calls Intercom with user data when isIntercomConfigured is true and user is provided", () => {
|
||||
const testUser = {
|
||||
id: "test-id",
|
||||
name: "Test User",
|
||||
@@ -55,7 +55,7 @@ describe("IntercomClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("calls Intercom with user data without createdAt", () => {
|
||||
test("calls Intercom with user data without createdAt", () => {
|
||||
const testUser = {
|
||||
id: "test-id",
|
||||
name: "Test User",
|
||||
@@ -83,7 +83,7 @@ describe("IntercomClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("calls Intercom with minimal params if user is not provided", () => {
|
||||
test("calls Intercom with minimal params if user is not provided", () => {
|
||||
render(
|
||||
<IntercomClient isIntercomConfigured={true} intercomAppId="my-app-id" intercomUserHash="my-user-hash" />
|
||||
);
|
||||
@@ -94,7 +94,7 @@ describe("IntercomClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call Intercom if isIntercomConfigured is false", () => {
|
||||
test("does not call Intercom if isIntercomConfigured is false", () => {
|
||||
render(
|
||||
<IntercomClient
|
||||
isIntercomConfigured={false}
|
||||
@@ -106,7 +106,7 @@ describe("IntercomClient", () => {
|
||||
expect(Intercom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shuts down Intercom on unmount", () => {
|
||||
test("shuts down Intercom on unmount", () => {
|
||||
const { unmount } = render(
|
||||
<IntercomClient isIntercomConfigured={true} intercomAppId="my-app-id" intercomUserHash="my-user-hash" />
|
||||
);
|
||||
@@ -120,7 +120,7 @@ describe("IntercomClient", () => {
|
||||
expect(mockWindowIntercom).toHaveBeenCalledWith("shutdown");
|
||||
});
|
||||
|
||||
it("logs an error if Intercom initialization fails", () => {
|
||||
test("logs an error if Intercom initialization fails", () => {
|
||||
// Spy on console.error
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
@@ -141,7 +141,7 @@ describe("IntercomClient", () => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("logs an error if isIntercomConfigured is true but no intercomAppId is provided", () => {
|
||||
test("logs an error if isIntercomConfigured is true but no intercomAppId is provided", () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
render(
|
||||
@@ -159,7 +159,7 @@ describe("IntercomClient", () => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("logs an error if isIntercomConfigured is true but no intercomUserHash is provided", () => {
|
||||
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",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { IntercomClientWrapper } from "./IntercomClientWrapper";
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("IntercomClientWrapper", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders IntercomClient with computed user hash when user is provided", () => {
|
||||
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(<IntercomClientWrapper user={testUser} />);
|
||||
@@ -48,7 +48,7 @@ describe("IntercomClientWrapper", () => {
|
||||
expect(props.user).toEqual(testUser);
|
||||
});
|
||||
|
||||
it("renders IntercomClient without computing a hash when no user is provided", () => {
|
||||
test("renders IntercomClient without computing a hash when no user is provided", () => {
|
||||
render(<IntercomClientWrapper user={null} />);
|
||||
|
||||
const intercomClientEl = screen.getByTestId("mock-intercom-client");
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getTolgee } from "@/tolgee/server";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { TolgeeInstance } from "@tolgee/react";
|
||||
import React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import RootLayout from "./layout";
|
||||
|
||||
// Mock dependencies for the layout
|
||||
@@ -81,7 +81,7 @@ describe("RootLayout", () => {
|
||||
process.env.VERCEL = "1";
|
||||
});
|
||||
|
||||
it("renders the layout with the correct structure and providers", async () => {
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import * as crypto from "@formbricks/lib/crypto";
|
||||
import { generateSurveySingleUseId, validateSurveySingleUseId } from "./singleUseSurveys";
|
||||
|
||||
@@ -45,21 +45,21 @@ describe("generateSurveySingleUseId", () => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("returns unencrypted cuid when isEncrypted is false", () => {
|
||||
test("returns unencrypted cuid when isEncrypted is false", () => {
|
||||
const result = generateSurveySingleUseId(false);
|
||||
|
||||
expect(result).toBe(mockCuid);
|
||||
expect(crypto.symmetricEncrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns encrypted cuid when isEncrypted is true", () => {
|
||||
test("returns encrypted cuid when isEncrypted is true", () => {
|
||||
const result = generateSurveySingleUseId(true);
|
||||
|
||||
expect(result).toBe(mockEncryptedCuid);
|
||||
expect(crypto.symmetricEncrypt).toHaveBeenCalledWith(mockCuid, "test-encryption-key");
|
||||
});
|
||||
|
||||
it("returns undefined when cuid is not valid", () => {
|
||||
test("returns undefined when cuid is not valid", () => {
|
||||
vi.mocked(cuid2.isCuid).mockReturnValue(false);
|
||||
|
||||
const result = validateSurveySingleUseId(mockEncryptedCuid);
|
||||
@@ -67,7 +67,7 @@ describe("generateSurveySingleUseId", () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when decryption fails", () => {
|
||||
test("returns undefined when decryption fails", () => {
|
||||
vi.mocked(crypto.symmetricDecrypt).mockImplementation(() => {
|
||||
throw new Error("Decryption failed");
|
||||
});
|
||||
@@ -77,7 +77,7 @@ describe("generateSurveySingleUseId", () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("throws error when ENCRYPTION_KEY is not set in generateSurveySingleUseId", async () => {
|
||||
test("throws error when ENCRYPTION_KEY is not set in generateSurveySingleUseId", async () => {
|
||||
// Temporarily mock ENCRYPTION_KEY as undefined
|
||||
vi.doMock("@formbricks/lib/constants", () => ({
|
||||
ENCRYPTION_KEY: undefined,
|
||||
@@ -90,7 +90,7 @@ describe("generateSurveySingleUseId", () => {
|
||||
expect(() => generateSurveySingleUseIdNoKey(true)).toThrow("ENCRYPTION_KEY is not set");
|
||||
});
|
||||
|
||||
it("throws error when ENCRYPTION_KEY is not set in validateSurveySingleUseId for symmetric encryption", async () => {
|
||||
test("throws error when ENCRYPTION_KEY is not set in validateSurveySingleUseId for symmetric encryption", async () => {
|
||||
// Temporarily mock ENCRYPTION_KEY as undefined
|
||||
vi.doMock("@formbricks/lib/constants", () => ({
|
||||
ENCRYPTION_KEY: undefined,
|
||||
@@ -103,7 +103,7 @@ describe("generateSurveySingleUseId", () => {
|
||||
expect(() => validateSurveySingleUseIdNoKey(mockEncryptedCuid)).toThrow("ENCRYPTION_KEY is not set");
|
||||
});
|
||||
|
||||
it("throws error when FORMBRICKS_ENCRYPTION_KEY is not set in validateSurveySingleUseId for AES128", async () => {
|
||||
test("throws error when FORMBRICKS_ENCRYPTION_KEY is not set in validateSurveySingleUseId for AES128", async () => {
|
||||
// Temporarily mock FORMBRICKS_ENCRYPTION_KEY as undefined
|
||||
vi.doMock("@formbricks/lib/constants", () => ({
|
||||
ENCRYPTION_KEY: "test-encryption-key",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { SentryProvider } from "./SentryProvider";
|
||||
|
||||
vi.mock("@sentry/nextjs", async () => {
|
||||
@@ -22,7 +22,7 @@ describe("SentryProvider", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("calls Sentry.init when sentryDsn is provided", () => {
|
||||
test("calls Sentry.init when sentryDsn is provided", () => {
|
||||
const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0";
|
||||
const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined);
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("SentryProvider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not call Sentry.init when sentryDsn is not provided", () => {
|
||||
test("does not call Sentry.init when sentryDsn is not provided", () => {
|
||||
const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined);
|
||||
|
||||
render(
|
||||
@@ -59,7 +59,7 @@ describe("SentryProvider", () => {
|
||||
expect(initSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders children", () => {
|
||||
test("renders children", () => {
|
||||
const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0";
|
||||
render(
|
||||
<SentryProvider sentryDsn={sentryDsn}>
|
||||
@@ -69,7 +69,7 @@ describe("SentryProvider", () => {
|
||||
expect(screen.getByTestId("child")).toHaveTextContent("Test Content");
|
||||
});
|
||||
|
||||
it("processes beforeSend correctly", () => {
|
||||
test("processes beforeSend correctly", () => {
|
||||
const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0";
|
||||
const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ const enforceHttps = (request: NextRequest): Response | null => {
|
||||
details: [
|
||||
{
|
||||
field: "",
|
||||
issue: "Only HTTPS connections are allowed on the management and contacts bulk endpoints.",
|
||||
issue: "Only HTTPS connections are allowed on the management endpoints.",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -54,18 +54,22 @@ const enforceHttps = (request: NextRequest): Response | null => {
|
||||
|
||||
const handleAuth = async (request: NextRequest): Promise<Response | null> => {
|
||||
const token = await getToken({ req: request as any });
|
||||
|
||||
if (isAuthProtectedRoute(request.nextUrl.pathname) && !token) {
|
||||
const loginUrl = `${WEBAPP_URL}/auth/login?callbackUrl=${encodeURIComponent(WEBAPP_URL + request.nextUrl.pathname + request.nextUrl.search)}`;
|
||||
return NextResponse.redirect(loginUrl);
|
||||
}
|
||||
|
||||
const callbackUrl = request.nextUrl.searchParams.get("callbackUrl");
|
||||
|
||||
if (callbackUrl && !isValidCallbackUrl(callbackUrl, WEBAPP_URL)) {
|
||||
return NextResponse.json({ error: "Invalid callback URL" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (token && callbackUrl) {
|
||||
return NextResponse.redirect(WEBAPP_URL + callbackUrl);
|
||||
return NextResponse.redirect(callbackUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { getOrganizationsWhereUserIsSingleOwner } from "@formbricks/lib/organization/service";
|
||||
import { deleteUser } from "@formbricks/lib/user/service";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { deleteUserAction } from "./actions";
|
||||
|
||||
// Mock all dependencies
|
||||
vi.mock("@formbricks/lib/user/service", () => ({
|
||||
deleteUser: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/lib/organization/service", () => ({
|
||||
getOrganizationsWhereUserIsSingleOwner: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getIsMultiOrgEnabled: vi.fn(),
|
||||
}));
|
||||
|
||||
// add a mock to authenticatedActionClient.action
|
||||
vi.mock("@/lib/utils/action-client", () => ({
|
||||
authenticatedActionClient: {
|
||||
action: (fn: any) => {
|
||||
return fn;
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("deleteUserAction", () => {
|
||||
test("deletes user successfully when multi-org is enabled", async () => {
|
||||
const ctx = { user: { id: "test-user" } };
|
||||
vi.mocked(deleteUser).mockResolvedValueOnce({ id: "test-user" } as TUser);
|
||||
vi.mocked(getOrganizationsWhereUserIsSingleOwner).mockResolvedValueOnce([]);
|
||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValueOnce(true);
|
||||
|
||||
const result = await deleteUserAction({ ctx } as any);
|
||||
|
||||
expect(result).toStrictEqual({ id: "test-user" } as TUser);
|
||||
expect(deleteUser).toHaveBeenCalledWith("test-user");
|
||||
expect(getOrganizationsWhereUserIsSingleOwner).toHaveBeenCalledWith("test-user");
|
||||
expect(getIsMultiOrgEnabled).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("deletes user successfully when multi-org is disabled but user is not sole owner of any org", async () => {
|
||||
const ctx = { user: { id: "another-user" } };
|
||||
vi.mocked(deleteUser).mockResolvedValueOnce({ id: "another-user" } as TUser);
|
||||
vi.mocked(getOrganizationsWhereUserIsSingleOwner).mockResolvedValueOnce([]);
|
||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValueOnce(false);
|
||||
|
||||
const result = await deleteUserAction({ ctx } as any);
|
||||
|
||||
expect(result).toStrictEqual({ id: "another-user" } as TUser);
|
||||
expect(deleteUser).toHaveBeenCalledWith("another-user");
|
||||
expect(getOrganizationsWhereUserIsSingleOwner).toHaveBeenCalledWith("another-user");
|
||||
expect(getIsMultiOrgEnabled).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("throws OperationNotAllowedError when user is sole owner in at least one org and multi-org is disabled", async () => {
|
||||
const ctx = { user: { id: "sole-owner-user" } };
|
||||
vi.mocked(deleteUser).mockResolvedValueOnce({ id: "test-user" } as TUser);
|
||||
vi.mocked(getOrganizationsWhereUserIsSingleOwner).mockResolvedValueOnce([
|
||||
{ id: "org-1" } as TOrganization,
|
||||
]);
|
||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValueOnce(false);
|
||||
|
||||
await expect(() => deleteUserAction({ ctx } as any)).rejects.toThrow(OperationNotAllowedError);
|
||||
expect(deleteUser).not.toHaveBeenCalled();
|
||||
expect(getOrganizationsWhereUserIsSingleOwner).toHaveBeenCalledWith("sole-owner-user");
|
||||
expect(getIsMultiOrgEnabled).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import * as nextAuth from "next-auth/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";
|
||||
|
||||
vi.mock("next-auth/react", async () => {
|
||||
const actual = await vi.importActual("next-auth/react");
|
||||
return {
|
||||
...actual,
|
||||
signOut: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
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();
|
||||
const mockLogout = vi.fn();
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders modal with correct props", () => {
|
||||
render(
|
||||
<DeleteAccountModal
|
||||
open={true}
|
||||
setOpen={mockSetOpen}
|
||||
user={mockUser}
|
||||
isFormbricksCloud={false}
|
||||
organizationsWithSingleOwner={mockOrgs}
|
||||
formbricksLogout={mockLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Org1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Org2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("disables delete button when email does not match", () => {
|
||||
render(
|
||||
<DeleteAccountModal
|
||||
open={true}
|
||||
setOpen={mockSetOpen}
|
||||
user={mockUser}
|
||||
isFormbricksCloud={false}
|
||||
organizationsWithSingleOwner={[]}
|
||||
formbricksLogout={mockLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
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
|
||||
const signOut = vi.spyOn(nextAuth, "signOut").mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<DeleteAccountModal
|
||||
open={true}
|
||||
setOpen={mockSetOpen}
|
||||
user={mockUser}
|
||||
isFormbricksCloud={false}
|
||||
organizationsWithSingleOwner={[]}
|
||||
formbricksLogout={mockLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
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(mockLogout).toHaveBeenCalled();
|
||||
expect(signOut).toHaveBeenCalledWith({ callbackUrl: "/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
|
||||
const signOut = vi.spyOn(nextAuth, "signOut").mockResolvedValue(undefined);
|
||||
|
||||
Object.defineProperty(window, "location", {
|
||||
writable: true,
|
||||
value: { replace: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<DeleteAccountModal
|
||||
open={true}
|
||||
setOpen={mockSetOpen}
|
||||
user={mockUser}
|
||||
isFormbricksCloud={true}
|
||||
organizationsWithSingleOwner={[]}
|
||||
formbricksLogout={mockLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
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(mockLogout).toHaveBeenCalled();
|
||||
expect(signOut).toHaveBeenCalledWith({ redirect: true });
|
||||
expect(window.location.replace).toHaveBeenCalled();
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("handles deletion errors", async () => {
|
||||
const deleteUserAction = vi.spyOn(actions, "deleteUserAction").mockRejectedValue(new Error("fail"));
|
||||
|
||||
render(
|
||||
<DeleteAccountModal
|
||||
open={true}
|
||||
setOpen={mockSetOpen}
|
||||
user={mockUser}
|
||||
isFormbricksCloud={false}
|
||||
organizationsWithSingleOwner={[]}
|
||||
formbricksLogout={mockLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { T } from "@tolgee/react";
|
||||
import { T, useTranslate } from "@tolgee/react";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
@@ -88,6 +87,7 @@ export const DeleteAccountModal = ({
|
||||
<li>{t("environments.settings.profile.warning_cannot_undo")}</li>
|
||||
</ul>
|
||||
<form
|
||||
data-testid="deleteAccountForm"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await deleteAccount();
|
||||
@@ -98,6 +98,7 @@ export const DeleteAccountModal = ({
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
data-testid="deleteAccountConfirmation"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
placeholder={user.email}
|
||||
|
||||
117
apps/web/modules/analysis/components/RatingSmiley/index.test.tsx
Normal file
117
apps/web/modules/analysis/components/RatingSmiley/index.test.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
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) => (
|
||||
<span data-testid="TiredFace" className={props.className}>
|
||||
TiredFace
|
||||
</span>
|
||||
),
|
||||
WearyFace: (props: any) => (
|
||||
<span data-testid="WearyFace" className={props.className}>
|
||||
WearyFace
|
||||
</span>
|
||||
),
|
||||
PerseveringFace: (props: any) => (
|
||||
<span data-testid="PerseveringFace" className={props.className}>
|
||||
PerseveringFace
|
||||
</span>
|
||||
),
|
||||
FrowningFace: (props: any) => (
|
||||
<span data-testid="FrowningFace" className={props.className}>
|
||||
FrowningFace
|
||||
</span>
|
||||
),
|
||||
ConfusedFace: (props: any) => (
|
||||
<span data-testid="ConfusedFace" className={props.className}>
|
||||
ConfusedFace
|
||||
</span>
|
||||
),
|
||||
NeutralFace: (props: any) => (
|
||||
<span data-testid="NeutralFace" className={props.className}>
|
||||
NeutralFace
|
||||
</span>
|
||||
),
|
||||
SlightlySmilingFace: (props: any) => (
|
||||
<span data-testid="SlightlySmilingFace" className={props.className}>
|
||||
SlightlySmilingFace
|
||||
</span>
|
||||
),
|
||||
SmilingFaceWithSmilingEyes: (props: any) => (
|
||||
<span data-testid="SmilingFaceWithSmilingEyes" className={props.className}>
|
||||
SmilingFaceWithSmilingEyes
|
||||
</span>
|
||||
),
|
||||
GrinningFaceWithSmilingEyes: (props: any) => (
|
||||
<span data-testid="GrinningFaceWithSmilingEyes" className={props.className}>
|
||||
GrinningFaceWithSmilingEyes
|
||||
</span>
|
||||
),
|
||||
GrinningSquintingFace: (props: any) => (
|
||||
<span data-testid="GrinningSquintingFace" className={props.className}>
|
||||
GrinningSquintingFace
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("RatingSmiley", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const activeClass = "fill-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(<RatingSmiley active={true} idx={0} range={10} addColors={true} />);
|
||||
const icon = getByTestId("TiredFace");
|
||||
expect(icon).toBeDefined();
|
||||
expect(icon.className).toContain(activeClass);
|
||||
});
|
||||
|
||||
test("renders correct icon for range 10 when inactive", () => {
|
||||
const { getByTestId } = render(<RatingSmiley active={false} idx={0} range={10} />);
|
||||
const icon = getByTestId("TiredFace");
|
||||
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(<RatingSmiley active={true} idx={0} range={7} addColors={true} />);
|
||||
const icon = getByTestId("WearyFace");
|
||||
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(<RatingSmiley active={true} idx={0} range={5} addColors={true} />);
|
||||
const icon = getByTestId("FrowningFace");
|
||||
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(<RatingSmiley active={true} idx={0} range={4} addColors={true} />);
|
||||
const icon = getByTestId("ConfusedFace");
|
||||
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(<RatingSmiley active={true} idx={0} range={3} addColors={true} />);
|
||||
const icon = getByTestId("ConfusedFace");
|
||||
expect(icon).toBeDefined();
|
||||
expect(icon.className).toContain(activeClass);
|
||||
});
|
||||
});
|
||||
@@ -40,16 +40,28 @@ const getSmiley = (iconIdx: number, idx: number, range: number, active: boolean,
|
||||
const inactiveColor = addColors ? getSmileyColor(range, idx) : "fill-none";
|
||||
|
||||
const icons = [
|
||||
<TiredFace className={active ? activeColor : inactiveColor} />,
|
||||
<WearyFace className={active ? activeColor : inactiveColor} />,
|
||||
<PerseveringFace className={active ? activeColor : inactiveColor} />,
|
||||
<FrowningFace className={active ? activeColor : inactiveColor} />,
|
||||
<ConfusedFace className={active ? activeColor : inactiveColor} />,
|
||||
<NeutralFace className={active ? activeColor : inactiveColor} />,
|
||||
<SlightlySmilingFace className={active ? activeColor : inactiveColor} />,
|
||||
<SmilingFaceWithSmilingEyes className={active ? activeColor : inactiveColor} />,
|
||||
<GrinningFaceWithSmilingEyes className={active ? activeColor : inactiveColor} />,
|
||||
<GrinningSquintingFace className={active ? activeColor : inactiveColor} />,
|
||||
<TiredFace className={active ? activeColor : inactiveColor} data-testid="TiredFace" />,
|
||||
<WearyFace className={active ? activeColor : inactiveColor} data-testid="WearyFace" />,
|
||||
<PerseveringFace className={active ? activeColor : inactiveColor} data-testid="PerseveringFace" />,
|
||||
<FrowningFace className={active ? activeColor : inactiveColor} data-testid="FrowningFace" />,
|
||||
<ConfusedFace className={active ? activeColor : inactiveColor} data-testid="ConfusedFace" />,
|
||||
<NeutralFace className={active ? activeColor : inactiveColor} data-testid="NeutralFace" />,
|
||||
<SlightlySmilingFace
|
||||
className={active ? activeColor : inactiveColor}
|
||||
data-testid="SlightlySmilingFace"
|
||||
/>,
|
||||
<SmilingFaceWithSmilingEyes
|
||||
className={active ? activeColor : inactiveColor}
|
||||
data-testid="SmilingFaceWithSmilingEyes"
|
||||
/>,
|
||||
<GrinningFaceWithSmilingEyes
|
||||
className={active ? activeColor : inactiveColor}
|
||||
data-testid="GrinningFaceWithSmilingEyes"
|
||||
/>,
|
||||
<GrinningSquintingFace
|
||||
className={active ? activeColor : inactiveColor}
|
||||
data-testid="GrinningSquintingFace"
|
||||
/>,
|
||||
];
|
||||
|
||||
return icons[iconIdx];
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
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 { getEnabledLanguages, getLanguageLabel } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { LanguageDropdown } from "./LanguageDropdown";
|
||||
|
||||
vi.mock("@formbricks/lib/i18n/utils", () => ({
|
||||
getEnabledLanguages: vi.fn(),
|
||||
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(
|
||||
<LanguageDropdown survey={dummySurveySingle} setLanguage={setLanguageMock} locale={dummyLocale} />
|
||||
);
|
||||
// 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(
|
||||
<LanguageDropdown survey={dummySurveyMultiple} setLanguage={setLanguageMock} locale={dummyLocale} />
|
||||
);
|
||||
|
||||
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(
|
||||
<LanguageDropdown survey={dummySurveyMultiple} setLanguage={setLanguageMock} locale={dummyLocale} />
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Languages } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { getEnabledLanguages } from "@formbricks/lib/i18n/utils";
|
||||
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
|
||||
import { getEnabledLanguages, getLanguageLabel } from "@formbricks/lib/i18n/utils";
|
||||
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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(<SurveyLinkDisplay surveyUrl={surveyUrl} />);
|
||||
const input = screen.getByTestId("survey-url-input");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders loading state when surveyUrl is empty", () => {
|
||||
render(<SurveyLinkDisplay surveyUrl="" />);
|
||||
const loadingDiv = screen.getByTestId("loading-div");
|
||||
expect(loadingDiv).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -9,13 +9,16 @@ export const SurveyLinkDisplay = ({ surveyUrl }: SurveyLinkDisplayProps) => {
|
||||
<>
|
||||
{surveyUrl ? (
|
||||
<Input
|
||||
data-testid="survey-url-input"
|
||||
autoFocus={true}
|
||||
className="mt-2 w-full min-w-96 text-ellipsis rounded-lg border bg-white px-4 py-2 text-slate-800 caret-transparent"
|
||||
className="mt-2 w-full min-w-96 rounded-lg border bg-white px-4 py-2 text-ellipsis text-slate-800 caret-transparent"
|
||||
value={surveyUrl}
|
||||
/>
|
||||
) : (
|
||||
//loading state
|
||||
<div className="mt-2 h-10 w-full min-w-96 animate-pulse rounded-lg bg-slate-100 px-4 py-2 text-slate-800 caret-transparent"></div>
|
||||
<div
|
||||
data-testid="loading-div"
|
||||
className="mt-2 h-10 w-full min-w-96 animate-pulse rounded-lg bg-slate-100 px-4 py-2 text-slate-800 caret-transparent"></div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import { useSurveyQRCode } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey-qr-code";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { copySurveyLink } from "@/modules/survey/lib/client-utils";
|
||||
import { generateSingleUseIdAction } from "@/modules/survey/list/actions";
|
||||
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 { ShareSurveyLink } from "./index";
|
||||
|
||||
const dummySurvey = {
|
||||
id: "survey123",
|
||||
singleUse: { enabled: true, isEncrypted: false },
|
||||
type: "link",
|
||||
status: "completed",
|
||||
} as any;
|
||||
const dummySurveyDomain = "http://dummy.com";
|
||||
const dummyLocale = "en-US";
|
||||
|
||||
vi.mock("@formbricks/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,
|
||||
AI_AZURE_LLM_RESSOURCE_NAME: "mock-ai-azure-llm-ressource-name",
|
||||
AI_AZURE_LLM_API_KEY: "mock-ai",
|
||||
AI_AZURE_LLM_DEPLOYMENT_ID: "mock-ai-azure-llm-deployment-id",
|
||||
AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-ai-azure-embeddings-ressource-name",
|
||||
AI_AZURE_EMBEDDINGS_API_KEY: "mock-ai-azure-embeddings-api-key",
|
||||
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-ai-azure-embeddings-deployment-id",
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/list/actions", () => ({
|
||||
generateSingleUseIdAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/lib/client-utils", () => ({
|
||||
copySurveyLink: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey-qr-code",
|
||||
() => ({
|
||||
useSurveyQRCode: vi.fn(() => ({
|
||||
downloadQRCode: vi.fn(),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/lib/utils/helper", () => ({
|
||||
getFormattedErrorMessage: vi.fn((error: any) => error.message),
|
||||
}));
|
||||
|
||||
vi.mock("./components/LanguageDropdown", () => {
|
||||
const React = require("react");
|
||||
return {
|
||||
LanguageDropdown: (props: { setLanguage: (lang: string) => void }) => {
|
||||
// Call setLanguage("fr-FR") when the component mounts to simulate a language change.
|
||||
React.useEffect(() => {
|
||||
props.setLanguage("fr-FR");
|
||||
}, [props.setLanguage]);
|
||||
return <div>Mocked LanguageDropdown</div>;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("ShareSurveyLink", () => {
|
||||
beforeEach(() => {
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText: vi.fn().mockResolvedValue(undefined) },
|
||||
});
|
||||
window.open = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("calls getUrl on mount and sets surveyUrl accordingly with singleUse enabled and default language", async () => {
|
||||
// Inline mocks for this test
|
||||
vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: "dummySuId" });
|
||||
|
||||
const setSurveyUrl = vi.fn();
|
||||
render(
|
||||
<ShareSurveyLink
|
||||
survey={dummySurvey}
|
||||
surveyDomain={dummySurveyDomain}
|
||||
surveyUrl=""
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(setSurveyUrl).toHaveBeenCalled();
|
||||
});
|
||||
const url = setSurveyUrl.mock.calls[0][0];
|
||||
expect(url).toContain(`${dummySurveyDomain}/s/${dummySurvey.id}?suId=dummySuId`);
|
||||
expect(url).not.toContain("lang=");
|
||||
});
|
||||
|
||||
test("appends language query when language is changed from default", async () => {
|
||||
vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: "dummySuId" });
|
||||
|
||||
const setSurveyUrl = vi.fn();
|
||||
const DummyWrapper = () => (
|
||||
<ShareSurveyLink
|
||||
survey={dummySurvey}
|
||||
surveyDomain={dummySurveyDomain}
|
||||
surveyUrl="initial"
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale="fr-FR"
|
||||
/>
|
||||
);
|
||||
render(<DummyWrapper />);
|
||||
await waitFor(() => {
|
||||
const generatedUrl = setSurveyUrl.mock.calls[1][0];
|
||||
expect(generatedUrl).toContain("lang=fr-FR");
|
||||
});
|
||||
});
|
||||
|
||||
test("preview button opens new window with preview query", async () => {
|
||||
vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: "dummySuId" });
|
||||
|
||||
const setSurveyUrl = vi.fn().mockReturnValue(`${dummySurveyDomain}/s/${dummySurvey.id}?suId=dummySuId`);
|
||||
render(
|
||||
<ShareSurveyLink
|
||||
survey={dummySurvey}
|
||||
surveyDomain={dummySurveyDomain}
|
||||
surveyUrl={`${dummySurveyDomain}/s/${dummySurvey.id}?suId=dummySuId`}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
const previewButton = await screen.findByRole("button", {
|
||||
name: /environments.surveys.preview_survey_in_a_new_tab/i,
|
||||
});
|
||||
fireEvent.click(previewButton);
|
||||
await waitFor(() => {
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
const previewUrl = vi.mocked(window.open).mock.calls[0][0];
|
||||
expect(previewUrl).toMatch(/\?suId=dummySuId(&|\\?)preview=true/);
|
||||
});
|
||||
});
|
||||
|
||||
test("copy button writes surveyUrl to clipboard and shows toast", async () => {
|
||||
vi.mocked(getFormattedErrorMessage).mockReturnValue("common.copied_to_clipboard");
|
||||
vi.mocked(copySurveyLink).mockImplementation((url: string, newId: string) => `${url}?suId=${newId}`);
|
||||
|
||||
const setSurveyUrl = vi.fn();
|
||||
const surveyUrl = `${dummySurveyDomain}/s/${dummySurvey.id}?suId=dummySuId`;
|
||||
render(
|
||||
<ShareSurveyLink
|
||||
survey={dummySurvey}
|
||||
surveyDomain={dummySurveyDomain}
|
||||
surveyUrl={surveyUrl}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
const copyButton = await screen.findByRole("button", {
|
||||
name: /environments.surveys.copy_survey_link_to_clipboard/i,
|
||||
});
|
||||
fireEvent.click(copyButton);
|
||||
await waitFor(() => {
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(surveyUrl);
|
||||
expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard");
|
||||
});
|
||||
});
|
||||
|
||||
test("download QR code button calls downloadQRCode", async () => {
|
||||
const dummyDownloadQRCode = vi.fn();
|
||||
vi.mocked(getFormattedErrorMessage).mockReturnValue("common.copied_to_clipboard");
|
||||
vi.mocked(useSurveyQRCode).mockReturnValue({ downloadQRCode: dummyDownloadQRCode } as any);
|
||||
|
||||
const setSurveyUrl = vi.fn();
|
||||
render(
|
||||
<ShareSurveyLink
|
||||
survey={dummySurvey}
|
||||
surveyDomain={dummySurveyDomain}
|
||||
surveyUrl={`${dummySurveyDomain}/s/${dummySurvey.id}?suId=dummySuId`}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
const downloadButton = await screen.findByRole("button", {
|
||||
name: /environments.surveys.summary.download_qr_code/i,
|
||||
});
|
||||
fireEvent.click(downloadButton);
|
||||
expect(dummyDownloadQRCode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders regenerate button when survey.singleUse.enabled is true and calls generateNewSingleUseLink", async () => {
|
||||
vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: "dummySuId" });
|
||||
|
||||
const setSurveyUrl = vi.fn();
|
||||
render(
|
||||
<ShareSurveyLink
|
||||
survey={dummySurvey}
|
||||
surveyDomain={dummySurveyDomain}
|
||||
surveyUrl={`${dummySurveyDomain}/s/${dummySurvey.id}?suId=dummySuId`}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
const regenButton = await screen.findByRole("button", { name: /Regenerate single use survey link/i });
|
||||
fireEvent.click(regenButton);
|
||||
await waitFor(() => {
|
||||
expect(generateSingleUseIdAction).toHaveBeenCalled();
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.surveys.new_single_use_link_generated");
|
||||
});
|
||||
});
|
||||
|
||||
test("handles error when generating single-use link fails", async () => {
|
||||
vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: undefined });
|
||||
vi.mocked(getFormattedErrorMessage).mockReturnValue("Failed to generate link");
|
||||
|
||||
const setSurveyUrl = vi.fn();
|
||||
render(
|
||||
<ShareSurveyLink
|
||||
survey={dummySurvey}
|
||||
surveyDomain={dummySurveyDomain}
|
||||
surveyUrl=""
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Failed to generate link");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import {
|
||||
getEnvironmentIdFromResponseId,
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromResponseId,
|
||||
getOrganizationIdFromResponseNoteId,
|
||||
getProjectIdFromEnvironmentId,
|
||||
getProjectIdFromResponseId,
|
||||
getProjectIdFromResponseNoteId,
|
||||
} from "@/lib/utils/helper";
|
||||
import { getTag } from "@/lib/utils/services";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { deleteResponse, getResponse } from "@formbricks/lib/response/service";
|
||||
import {
|
||||
createResponseNote,
|
||||
resolveResponseNote,
|
||||
updateResponseNote,
|
||||
} from "@formbricks/lib/responseNote/service";
|
||||
import { createTag } from "@formbricks/lib/tag/service";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/tagOnResponse/service";
|
||||
import {
|
||||
createResponseNoteAction,
|
||||
createTagAction,
|
||||
createTagToResponseAction,
|
||||
deleteResponseAction,
|
||||
deleteTagOnResponseAction,
|
||||
getResponseAction,
|
||||
resolveResponseNoteAction,
|
||||
updateResponseNoteAction,
|
||||
} from "./actions";
|
||||
|
||||
// Dummy inputs and context
|
||||
const dummyCtx = { user: { id: "user1" } };
|
||||
const dummyTagInput = { environmentId: "env1", tagName: "tag1" };
|
||||
const dummyTagToResponseInput = { responseId: "resp1", tagId: "tag1" };
|
||||
const dummyResponseIdInput = { responseId: "resp1" };
|
||||
const dummyResponseNoteInput = { responseNoteId: "note1", text: "Updated note" };
|
||||
const dummyCreateNoteInput = { responseId: "resp1", text: "New note" };
|
||||
const dummyGetResponseInput = { responseId: "resp1" };
|
||||
|
||||
// Mocks for external dependencies
|
||||
vi.mock("@/lib/utils/action-client-middleware", () => ({
|
||||
checkAuthorizationUpdated: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/utils/helper", () => ({
|
||||
getOrganizationIdFromEnvironmentId: vi.fn(),
|
||||
getProjectIdFromEnvironmentId: vi.fn().mockResolvedValue("proj-env"),
|
||||
getOrganizationIdFromResponseId: vi.fn().mockResolvedValue("org-resp"),
|
||||
getOrganizationIdFromResponseNoteId: vi.fn().mockResolvedValue("org-resp-note"),
|
||||
getProjectIdFromResponseId: vi.fn().mockResolvedValue("proj-resp"),
|
||||
getProjectIdFromResponseNoteId: vi.fn().mockResolvedValue("proj-resp-note"),
|
||||
getEnvironmentIdFromResponseId: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/utils/services", () => ({
|
||||
getTag: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/response/service", () => ({
|
||||
deleteResponse: vi.fn().mockResolvedValue("deletedResponse"),
|
||||
getResponse: vi.fn().mockResolvedValue({ data: "responseData" }),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/responseNote/service", () => ({
|
||||
createResponseNote: vi.fn().mockResolvedValue("createdNote"),
|
||||
updateResponseNote: vi.fn().mockResolvedValue("updatedNote"),
|
||||
resolveResponseNote: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/tag/service", () => ({
|
||||
createTag: vi.fn().mockResolvedValue("createdTag"),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/tagOnResponse/service", () => ({
|
||||
addTagToRespone: vi.fn().mockResolvedValue("tagAdded"),
|
||||
deleteTagOnResponse: vi.fn().mockResolvedValue("tagDeleted"),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/action-client", () => ({
|
||||
authenticatedActionClient: {
|
||||
schema: () => ({
|
||||
action: (fn: any) => async (input: any) => {
|
||||
const { user, ...rest } = input;
|
||||
return fn({
|
||||
parsedInput: rest,
|
||||
ctx: { user },
|
||||
});
|
||||
},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("createTagAction", () => {
|
||||
test("successfully creates a tag", async () => {
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
|
||||
vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValueOnce("org1");
|
||||
await createTagAction({ ...dummyTagInput, ...dummyCtx });
|
||||
expect(checkAuthorizationUpdated).toHaveBeenCalled();
|
||||
expect(getOrganizationIdFromEnvironmentId).toHaveBeenCalledWith(dummyTagInput.environmentId);
|
||||
expect(getProjectIdFromEnvironmentId).toHaveBeenCalledWith(dummyTagInput.environmentId);
|
||||
expect(createTag).toHaveBeenCalledWith(dummyTagInput.environmentId, dummyTagInput.tagName);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTagToResponseAction", () => {
|
||||
test("adds tag to response when environments match", async () => {
|
||||
vi.mocked(getEnvironmentIdFromResponseId).mockResolvedValueOnce("env1");
|
||||
vi.mocked(getTag).mockResolvedValueOnce({ environmentId: "env1" });
|
||||
await createTagToResponseAction({ ...dummyTagToResponseInput, ...dummyCtx });
|
||||
expect(getEnvironmentIdFromResponseId).toHaveBeenCalledWith(dummyTagToResponseInput.responseId);
|
||||
expect(getTag).toHaveBeenCalledWith(dummyTagToResponseInput.tagId);
|
||||
expect(checkAuthorizationUpdated).toHaveBeenCalled();
|
||||
expect(addTagToRespone).toHaveBeenCalledWith(
|
||||
dummyTagToResponseInput.responseId,
|
||||
dummyTagToResponseInput.tagId
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error when environments do not match", async () => {
|
||||
vi.mocked(getEnvironmentIdFromResponseId).mockResolvedValueOnce("env1");
|
||||
vi.mocked(getTag).mockResolvedValueOnce({ environmentId: "differentEnv" });
|
||||
await expect(createTagToResponseAction({ ...dummyTagToResponseInput, ...dummyCtx })).rejects.toThrow(
|
||||
"Response and tag are not in the same environment"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteTagOnResponseAction", () => {
|
||||
test("deletes tag on response when environments match", async () => {
|
||||
vi.mocked(getEnvironmentIdFromResponseId).mockResolvedValueOnce("env1");
|
||||
vi.mocked(getTag).mockResolvedValueOnce({ environmentId: "env1" });
|
||||
await deleteTagOnResponseAction({ ...dummyTagToResponseInput, ...dummyCtx });
|
||||
expect(getOrganizationIdFromResponseId).toHaveBeenCalledWith(dummyTagToResponseInput.responseId);
|
||||
expect(getTag).toHaveBeenCalledWith(dummyTagToResponseInput.tagId);
|
||||
expect(checkAuthorizationUpdated).toHaveBeenCalled();
|
||||
expect(deleteTagOnResponse).toHaveBeenCalledWith(
|
||||
dummyTagToResponseInput.responseId,
|
||||
dummyTagToResponseInput.tagId
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error when environments do not match", async () => {
|
||||
vi.mocked(getEnvironmentIdFromResponseId).mockResolvedValueOnce("env1");
|
||||
vi.mocked(getTag).mockResolvedValueOnce({ environmentId: "differentEnv" });
|
||||
await expect(deleteTagOnResponseAction({ ...dummyTagToResponseInput, ...dummyCtx })).rejects.toThrow(
|
||||
"Response and tag are not in the same environment"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteResponseAction", () => {
|
||||
test("deletes response successfully", async () => {
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
|
||||
await deleteResponseAction({ ...dummyResponseIdInput, ...dummyCtx });
|
||||
expect(checkAuthorizationUpdated).toHaveBeenCalled();
|
||||
expect(getOrganizationIdFromResponseId).toHaveBeenCalledWith(dummyResponseIdInput.responseId);
|
||||
expect(getProjectIdFromResponseId).toHaveBeenCalledWith(dummyResponseIdInput.responseId);
|
||||
expect(deleteResponse).toHaveBeenCalledWith(dummyResponseIdInput.responseId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateResponseNoteAction", () => {
|
||||
test("updates response note successfully", async () => {
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
|
||||
await updateResponseNoteAction({ ...dummyResponseNoteInput, ...dummyCtx });
|
||||
expect(checkAuthorizationUpdated).toHaveBeenCalled();
|
||||
expect(getOrganizationIdFromResponseNoteId).toHaveBeenCalledWith(dummyResponseNoteInput.responseNoteId);
|
||||
expect(getProjectIdFromResponseNoteId).toHaveBeenCalledWith(dummyResponseNoteInput.responseNoteId);
|
||||
expect(updateResponseNote).toHaveBeenCalledWith(
|
||||
dummyResponseNoteInput.responseNoteId,
|
||||
dummyResponseNoteInput.text
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveResponseNoteAction", () => {
|
||||
test("resolves response note successfully", async () => {
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
|
||||
await resolveResponseNoteAction({ responseNoteId: "note1", ...dummyCtx });
|
||||
expect(checkAuthorizationUpdated).toHaveBeenCalled();
|
||||
expect(getOrganizationIdFromResponseNoteId).toHaveBeenCalledWith("note1");
|
||||
expect(getProjectIdFromResponseNoteId).toHaveBeenCalledWith("note1");
|
||||
expect(resolveResponseNote).toHaveBeenCalledWith("note1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createResponseNoteAction", () => {
|
||||
test("creates a response note successfully", async () => {
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
|
||||
await createResponseNoteAction({ ...dummyCreateNoteInput, ...dummyCtx });
|
||||
expect(checkAuthorizationUpdated).toHaveBeenCalled();
|
||||
expect(getOrganizationIdFromResponseId).toHaveBeenCalledWith(dummyCreateNoteInput.responseId);
|
||||
expect(getProjectIdFromResponseId).toHaveBeenCalledWith(dummyCreateNoteInput.responseId);
|
||||
expect(createResponseNote).toHaveBeenCalledWith(
|
||||
dummyCreateNoteInput.responseId,
|
||||
dummyCtx.user.id,
|
||||
dummyCreateNoteInput.text
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getResponseAction", () => {
|
||||
test("retrieves response successfully", async () => {
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
|
||||
await getResponseAction({ ...dummyGetResponseInput, ...dummyCtx });
|
||||
expect(checkAuthorizationUpdated).toHaveBeenCalled();
|
||||
expect(getOrganizationIdFromResponseId).toHaveBeenCalledWith(dummyGetResponseInput.responseId);
|
||||
expect(getProjectIdFromResponseId).toHaveBeenCalledWith(dummyGetResponseInput.responseId);
|
||||
expect(getResponse).toHaveBeenCalledWith(dummyGetResponseInput.responseId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
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 }) => <div>{children}</div>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
TooltipProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
describe("HiddenFields", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders empty container when no fieldIds are provided", () => {
|
||||
render(
|
||||
<HiddenFields hiddenFields={{ fieldIds: [] } as unknown as TSurveyHiddenFields} responseData={{}} />
|
||||
);
|
||||
const container = screen.getByTestId("main-hidden-fields-div");
|
||||
expect(container).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders nothing for fieldIds with no corresponding response data", () => {
|
||||
render(
|
||||
<HiddenFields
|
||||
hiddenFields={{ fieldIds: ["field1"] } as unknown as TSurveyHiddenFields}
|
||||
responseData={{}}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByText("field1")).toBeNull();
|
||||
});
|
||||
|
||||
test("renders field and value when responseData exists and is a string", async () => {
|
||||
render(
|
||||
<HiddenFields
|
||||
hiddenFields={{ fieldIds: ["field1", "field2"] } as unknown as TSurveyHiddenFields}
|
||||
responseData={{ field1: "Value 1", field2: "" }}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<HiddenFields
|
||||
hiddenFields={{ fieldIds: ["field1"] } as unknown as TSurveyHiddenFields}
|
||||
responseData={{ field1: { any: "object" } }}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<HiddenFields
|
||||
hiddenFields={{ fieldIds: ["field1"] } as unknown as TSurveyHiddenFields}
|
||||
responseData={{ field1: "Value 1" }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("common.hidden_field")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ export const HiddenFields = ({ hiddenFields, responseData }: HiddenFieldsProps)
|
||||
const { t } = useTranslate();
|
||||
const fieldIds = hiddenFields.fieldIds ?? [];
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-6">
|
||||
<div data-testid="main-hidden-fields-div" className="mt-6 flex flex-col gap-6">
|
||||
{fieldIds.map((field) => {
|
||||
if (!responseData[field]) return;
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { QuestionSkip } from "./QuestionSkip";
|
||||
|
||||
vi.mock("@/modules/ui/components/tooltip", () => ({
|
||||
Tooltip: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipContent: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipProvider: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipTrigger: ({ children }: any) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/i18n/utils", () => ({
|
||||
getLocalizedValue: vi.fn((value, _) => value),
|
||||
}));
|
||||
|
||||
// Mock recall utils
|
||||
vi.mock("@formbricks/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(
|
||||
<QuestionSkip
|
||||
skippedQuestions={undefined}
|
||||
status="skipped"
|
||||
questions={dummyQuestions}
|
||||
responseData={dummyResponseData}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByText("headline1")).toBeNull();
|
||||
expect(screen.queryByText("headline2")).toBeNull();
|
||||
});
|
||||
|
||||
test("renders welcomeCard branch", () => {
|
||||
render(
|
||||
<QuestionSkip
|
||||
skippedQuestions={["f1"]}
|
||||
status="welcomeCard"
|
||||
questions={dummyQuestions}
|
||||
responseData={{ f1: "Answer 1" }}
|
||||
isFirstQuestionAnswered={false}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<QuestionSkip
|
||||
skippedQuestions={["f1", "f2"]}
|
||||
status="skipped"
|
||||
questions={dummyQuestions}
|
||||
responseData={dummyResponseData}
|
||||
/>
|
||||
);
|
||||
// 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(
|
||||
<QuestionSkip
|
||||
skippedQuestions={["f1", "f2"]}
|
||||
status="aborted"
|
||||
questions={dummyQuestions}
|
||||
responseData={dummyResponseData}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("tooltip-survey_closed")).toBeInTheDocument();
|
||||
expect(screen.getByText("parsed: headline1")).toBeInTheDocument();
|
||||
expect(screen.getByText("parsed: headline2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,7 @@ export const QuestionSkip = ({
|
||||
background:
|
||||
"repeating-linear-gradient(rgb(148, 163, 184), rgb(148, 163, 184) 5px, transparent 5px, transparent 8px)", // adjust the values to fit your design
|
||||
}}>
|
||||
<CheckCircle2Icon className="p-0.25 absolute top-0 w-[1.5rem] min-w-[1.5rem] rounded-full bg-white text-slate-400" />
|
||||
<CheckCircle2Icon className="absolute top-0 w-[1.5rem] min-w-[1.5rem] rounded-full bg-white p-0.25 text-slate-400" />
|
||||
</div>
|
||||
}
|
||||
<div className="ml-6 flex flex-col text-slate-700">{t("common.welcome_card")}</div>
|
||||
@@ -60,27 +60,28 @@ export const QuestionSkip = ({
|
||||
<ChevronsDownIcon className="w-[1.25rem] min-w-[1.25rem] rounded-full bg-slate-400 p-0.5 text-white" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t("environments.surveys.responses.respondent_skipped_questions")}</p>
|
||||
<p data-testid="tooltip-respondent_skipped_questions">
|
||||
{t("environments.surveys.responses.respondent_skipped_questions")}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-6 flex flex-col">
|
||||
{skippedQuestions &&
|
||||
skippedQuestions.map((questionId) => {
|
||||
return (
|
||||
<p className="my-2" key={questionId}>
|
||||
{parseRecallInfo(
|
||||
getLocalizedValue(
|
||||
questions.find((question) => question.id === questionId)!.headline,
|
||||
"default"
|
||||
),
|
||||
responseData
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
{skippedQuestions?.map((questionId) => {
|
||||
return (
|
||||
<p className="my-2" key={questionId}>
|
||||
{parseRecallInfo(
|
||||
getLocalizedValue(
|
||||
questions.find((question) => question.id === questionId)!.headline,
|
||||
"default"
|
||||
),
|
||||
responseData
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -97,7 +98,9 @@ export const QuestionSkip = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 ml-4 flex flex-col">
|
||||
<p className="mb-2 w-fit rounded-lg bg-slate-100 px-2 font-medium text-slate-700">
|
||||
<p
|
||||
data-testid="tooltip-survey_closed"
|
||||
className="mb-2 w-fit rounded-lg bg-slate-100 px-2 font-medium text-slate-700">
|
||||
{t("environments.surveys.responses.survey_closed")}
|
||||
</p>
|
||||
{skippedQuestions &&
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
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) => <div data-testid="RatingResponse">Rating: {answer}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/file-upload-response", () => ({
|
||||
FileUploadResponse: ({ selected }: any) => (
|
||||
<div data-testid="FileUploadResponse">FileUpload: {selected.join(",")}</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/picture-selection-response", () => ({
|
||||
PictureSelectionResponse: ({ selected, isExpanded }: any) => (
|
||||
<div data-testid="PictureSelectionResponse">
|
||||
PictureSelection: {selected.join(",")} ({isExpanded ? "expanded" : "collapsed"})
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/array-response", () => ({
|
||||
ArrayResponse: ({ value }: any) => <div data-testid="ArrayResponse">{value.join(",")}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/response-badges", () => ({
|
||||
ResponseBadges: ({ items }: any) => <div data-testid="ResponseBadges">{items.join(",")}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/ranking-response", () => ({
|
||||
RankingRespone: ({ value }: any) => <div data-testid="RankingRespone">{value.join(",")}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/analysis/utils", () => ({
|
||||
renderHyperlinkedContent: vi.fn((text: string) => "hyper:" + text),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/responses", () => ({
|
||||
processResponseData: (val: any) => "processed:" + val,
|
||||
}));
|
||||
vi.mock("@formbricks/lib/utils/datetime", () => ({
|
||||
formatDateWithOrdinal: (d: Date) => "formatted_" + d.toISOString(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/cn", () => ({
|
||||
cn: (...classes: (string | boolean | undefined)[]) => classes.filter(Boolean).join(" "),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: vi.fn((val, _) => val),
|
||||
getLanguageCode: vi.fn().mockReturnValue("default"),
|
||||
}));
|
||||
|
||||
describe("RenderResponse", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const defaultSurvey = { languages: [] } as any;
|
||||
const defaultQuestion = { id: "q1", type: "Unknown" } as any;
|
||||
const dummyLanguage = "default";
|
||||
|
||||
test("returns '-' for empty responseData (string)", () => {
|
||||
const { container } = render(
|
||||
<RenderResponse
|
||||
responseData={""}
|
||||
question={defaultQuestion}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(container.textContent).toBe("-");
|
||||
});
|
||||
|
||||
test("returns '-' for empty responseData (array)", () => {
|
||||
const { container } = render(
|
||||
<RenderResponse
|
||||
responseData={[]}
|
||||
question={defaultQuestion}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(container.textContent).toBe("-");
|
||||
});
|
||||
|
||||
test("returns '-' for empty responseData (object)", () => {
|
||||
const { container } = render(
|
||||
<RenderResponse
|
||||
responseData={{}}
|
||||
question={defaultQuestion}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(container.textContent).toBe("-");
|
||||
});
|
||||
|
||||
test("renders RatingResponse for 'Rating' question with number", () => {
|
||||
const question = { ...defaultQuestion, type: "rating", scale: 5, range: [1, 5] };
|
||||
render(
|
||||
<RenderResponse responseData={4} question={question} survey={defaultSurvey} language={dummyLanguage} />
|
||||
);
|
||||
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(
|
||||
<RenderResponse
|
||||
responseData={dateStr}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/formatted_/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders PictureSelectionResponse for 'PictureSelection' question", () => {
|
||||
const question = { ...defaultQuestion, type: "pictureSelection", choices: ["a", "b"] };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["choice1", "choice2"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("PictureSelectionResponse")).toHaveTextContent(
|
||||
"PictureSelection: choice1,choice2"
|
||||
);
|
||||
});
|
||||
|
||||
test("renders FileUploadResponse for 'FileUpload' question", () => {
|
||||
const question = { ...defaultQuestion, type: "fileUpload" };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["file1", "file2"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("FileUploadResponse")).toHaveTextContent("FileUpload: file1,file2");
|
||||
});
|
||||
|
||||
test("renders Matrix response", () => {
|
||||
const question = { id: "q1", type: "matrix", rows: ["row1", "row2"] } as any;
|
||||
// getLocalizedValue returns the row value itself
|
||||
const responseData = { row1: "answer1", row2: "answer2" };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={responseData}
|
||||
question={question}
|
||||
survey={{ languages: [] } as any}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<RenderResponse
|
||||
responseData={["addr1", "addr2"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ArrayResponse")).toHaveTextContent("addr1,addr2");
|
||||
});
|
||||
|
||||
test("renders ResponseBadges for 'Cal' question (string)", () => {
|
||||
const question = { ...defaultQuestion, type: "cal" };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={"value"}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("Value");
|
||||
});
|
||||
|
||||
test("renders ResponseBadges for 'Consent' question (number)", () => {
|
||||
const question = { ...defaultQuestion, type: "consent" };
|
||||
render(
|
||||
<RenderResponse responseData={5} question={question} survey={defaultSurvey} language={dummyLanguage} />
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("5");
|
||||
});
|
||||
|
||||
test("renders ResponseBadges for 'CTA' question (string)", () => {
|
||||
const question = { ...defaultQuestion, type: "cta" };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={"click"}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("Click");
|
||||
});
|
||||
|
||||
test("renders ResponseBadges for 'MultipleChoiceSingle' question (string)", () => {
|
||||
const question = { ...defaultQuestion, type: "multipleChoiceSingle" };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={"option1"}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("option1");
|
||||
});
|
||||
|
||||
test("renders ResponseBadges for 'MultipleChoiceMulti' question (array)", () => {
|
||||
const question = { ...defaultQuestion, type: "multipleChoiceMulti" };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["opt1", "opt2"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("opt1,opt2");
|
||||
});
|
||||
|
||||
test("renders ResponseBadges for 'NPS' question (number)", () => {
|
||||
const question = { ...defaultQuestion, type: "nps" };
|
||||
render(
|
||||
<RenderResponse responseData={9} question={question} survey={defaultSurvey} language={dummyLanguage} />
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("9");
|
||||
});
|
||||
|
||||
test("renders RankingRespone for 'Ranking' question", () => {
|
||||
const question = { ...defaultQuestion, type: "ranking" };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["first", "second"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("RankingRespone")).toHaveTextContent("first,second");
|
||||
});
|
||||
|
||||
test("renders default branch for unknown question type with string", () => {
|
||||
const question = { ...defaultQuestion, type: "unknown" };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={"some text"}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("hyper:some text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders default branch for unknown question type with array", () => {
|
||||
const question = { ...defaultQuestion, type: "unknown" };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["a", "b"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("a, b")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
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 { TResponseNote } from "@formbricks/types/responses";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { createResponseNoteAction, resolveResponseNoteAction, updateResponseNoteAction } from "../actions";
|
||||
import { ResponseNotes } from "./ResponseNote";
|
||||
|
||||
const dummyUser = { id: "user1", name: "User One" } as TUser;
|
||||
const dummyResponseId = "resp1";
|
||||
const dummyLocale = "en-US";
|
||||
const dummyNote = {
|
||||
id: "note1",
|
||||
text: "Initial note",
|
||||
isResolved: true,
|
||||
isEdited: false,
|
||||
updatedAt: new Date(),
|
||||
user: { id: "user1", name: "User One" },
|
||||
} as TResponseNote;
|
||||
const dummyUnresolvedNote = {
|
||||
id: "note1",
|
||||
text: "Initial note",
|
||||
isResolved: false,
|
||||
isEdited: false,
|
||||
updatedAt: new Date(),
|
||||
user: { id: "user1", name: "User One" },
|
||||
} as TResponseNote;
|
||||
const updateFetchedResponses = vi.fn();
|
||||
const setIsOpen = vi.fn();
|
||||
|
||||
vi.mock("../actions", () => ({
|
||||
createResponseNoteAction: vi.fn().mockResolvedValue("createdNote"),
|
||||
updateResponseNoteAction: vi.fn().mockResolvedValue("updatedNote"),
|
||||
resolveResponseNoteAction: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: (props: any) => <button {...props}>{props.children}</button>,
|
||||
}));
|
||||
|
||||
// Mock icons for edit and resolve buttons with test ids
|
||||
vi.mock("lucide-react", () => {
|
||||
const actual = vi.importActual("lucide-react");
|
||||
return {
|
||||
...actual,
|
||||
PencilIcon: (props: any) => (
|
||||
<button data-testid="pencil-button" {...props}>
|
||||
Pencil
|
||||
</button>
|
||||
),
|
||||
CheckIcon: (props: any) => (
|
||||
<button data-testid="check-button" {...props}>
|
||||
Check
|
||||
</button>
|
||||
),
|
||||
PlusIcon: (props: any) => (
|
||||
<span data-testid="plus-icon" {...props}>
|
||||
Plus
|
||||
</span>
|
||||
),
|
||||
Maximize2Icon: (props: any) => (
|
||||
<span data-testid="maximize-icon" {...props}>
|
||||
Maximize
|
||||
</span>
|
||||
),
|
||||
Minimize2Icon: (props: any) => (
|
||||
<button data-testid="minimize-button" {...props}>
|
||||
Minimize
|
||||
</button>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock tooltip components
|
||||
vi.mock("@/modules/ui/components/tooltip", () => ({
|
||||
Tooltip: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipContent: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipProvider: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipTrigger: ({ children }: any) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
describe("ResponseNotes", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders collapsed view when isOpen is false", () => {
|
||||
render(
|
||||
<ResponseNotes
|
||||
user={dummyUser}
|
||||
responseId={dummyResponseId}
|
||||
notes={[dummyNote]}
|
||||
isOpen={false}
|
||||
setIsOpen={setIsOpen}
|
||||
updateFetchedResponses={updateFetchedResponses}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/note/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("opens panel on click when collapsed", async () => {
|
||||
render(
|
||||
<ResponseNotes
|
||||
user={dummyUser}
|
||||
responseId={dummyResponseId}
|
||||
notes={[dummyNote]}
|
||||
isOpen={false}
|
||||
setIsOpen={setIsOpen}
|
||||
updateFetchedResponses={updateFetchedResponses}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByText(/note/i));
|
||||
expect(setIsOpen).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("submits a new note", async () => {
|
||||
vi.mocked(createResponseNoteAction).mockResolvedValueOnce("createdNote" as any);
|
||||
render(
|
||||
<ResponseNotes
|
||||
user={dummyUser}
|
||||
responseId={dummyResponseId}
|
||||
notes={[]}
|
||||
isOpen={true}
|
||||
setIsOpen={setIsOpen}
|
||||
updateFetchedResponses={updateFetchedResponses}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
await userEvent.type(textarea, "New note");
|
||||
await userEvent.type(textarea, "{enter}");
|
||||
await waitFor(() => {
|
||||
expect(createResponseNoteAction).toHaveBeenCalledWith({
|
||||
responseId: dummyResponseId,
|
||||
text: "New note",
|
||||
});
|
||||
expect(updateFetchedResponses).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test("edits an existing note", async () => {
|
||||
vi.mocked(updateResponseNoteAction).mockResolvedValueOnce("updatedNote" as any);
|
||||
render(
|
||||
<ResponseNotes
|
||||
user={dummyUser}
|
||||
responseId={dummyResponseId}
|
||||
notes={[dummyUnresolvedNote]}
|
||||
isOpen={true}
|
||||
setIsOpen={setIsOpen}
|
||||
updateFetchedResponses={updateFetchedResponses}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
const pencilButton = screen.getByTestId("pencil-button");
|
||||
await userEvent.click(pencilButton);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveValue("Initial note");
|
||||
await userEvent.clear(textarea);
|
||||
await userEvent.type(textarea, "Updated note");
|
||||
await userEvent.type(textarea, "{enter}");
|
||||
await waitFor(() => {
|
||||
expect(updateResponseNoteAction).toHaveBeenCalledWith({
|
||||
responseNoteId: dummyNote.id,
|
||||
text: "Updated note",
|
||||
});
|
||||
expect(updateFetchedResponses).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test("resolves a note", async () => {
|
||||
vi.mocked(resolveResponseNoteAction).mockResolvedValueOnce(undefined);
|
||||
render(
|
||||
<ResponseNotes
|
||||
user={dummyUser}
|
||||
responseId={dummyResponseId}
|
||||
notes={[dummyUnresolvedNote]}
|
||||
isOpen={true}
|
||||
setIsOpen={setIsOpen}
|
||||
updateFetchedResponses={updateFetchedResponses}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
const checkButton = screen.getByTestId("check-button");
|
||||
userEvent.click(checkButton);
|
||||
await waitFor(() => {
|
||||
expect(resolveResponseNoteAction).toHaveBeenCalledWith({ responseNoteId: dummyNote.id });
|
||||
expect(updateFetchedResponses).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,8 +4,7 @@ import { Button } from "@/modules/ui/components/button";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import clsx from "clsx";
|
||||
import { CheckIcon, PencilIcon, PlusIcon } from "lucide-react";
|
||||
import { Maximize2Icon, Minimize2Icon } from "lucide-react";
|
||||
import { CheckIcon, Maximize2Icon, Minimize2Icon, PencilIcon, PlusIcon } from "lucide-react";
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
@@ -105,10 +104,10 @@ export const ResponseNotes = ({
|
||||
!isOpen && unresolvedNotes.length && "group/hint cursor-pointer bg-white hover:-right-3",
|
||||
!isOpen && !unresolvedNotes.length && "cursor-pointer bg-slate-50",
|
||||
isOpen
|
||||
? "-right-2 top-0 h-5/6 max-h-[600px] w-1/4 bg-white"
|
||||
? "top-0 -right-2 h-5/6 max-h-[600px] w-1/4 bg-white"
|
||||
: unresolvedNotes.length
|
||||
? "right-0 top-[8.33%] h-5/6 max-h-[600px] w-1/12"
|
||||
: "right-[120px] top-[8.333%] h-5/6 max-h-[600px] w-1/12 group-hover:right-[0]"
|
||||
? "top-[8.33%] right-0 h-5/6 max-h-[600px] w-1/12"
|
||||
: "top-[8.333%] right-[120px] h-5/6 max-h-[600px] w-1/12 group-hover:right-[0]"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
@@ -117,7 +116,7 @@ export const ResponseNotes = ({
|
||||
<div className="flex h-full flex-col">
|
||||
<div
|
||||
className={clsx(
|
||||
"space-y-2 rounded-t-lg px-2 pb-2 pt-2",
|
||||
"space-y-2 rounded-t-lg px-2 pt-2 pb-2",
|
||||
unresolvedNotes.length ? "flex h-12 items-center justify-end bg-amber-50" : "bg-slate-200"
|
||||
)}>
|
||||
{!unresolvedNotes.length ? (
|
||||
@@ -128,7 +127,7 @@ export const ResponseNotes = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className="float-left mr-1.5">
|
||||
<Maximize2Icon className="h-4 w-4 text-amber-500 hover:text-amber-600 group-hover/hint:scale-110" />
|
||||
<Maximize2Icon className="h-4 w-4 text-amber-500 group-hover/hint:scale-110 hover:text-amber-600" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -142,7 +141,7 @@ export const ResponseNotes = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex h-full flex-col">
|
||||
<div className="rounded-t-lg bg-amber-50 px-4 pb-3 pt-4">
|
||||
<div className="rounded-t-lg bg-amber-50 px-4 pt-4 pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="group flex items-center">
|
||||
<h3 className="pb-1 text-sm text-amber-500">{t("common.note")}</h3>
|
||||
@@ -228,9 +227,7 @@ export const ResponseNotes = ({
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && noteText) {
|
||||
e.preventDefault();
|
||||
{
|
||||
isUpdatingNote ? handleNoteUpdate(e) : handleNoteSubmission(e);
|
||||
}
|
||||
isUpdatingNote ? handleNoteUpdate(e) : handleNoteSubmission(e);
|
||||
}
|
||||
}}
|
||||
required></textarea>
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
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 { 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) => <button {...props}>{props.children}</button>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/tag", () => ({
|
||||
Tag: (props: any) => (
|
||||
<div data-testid="tag">
|
||||
{props.tagName}
|
||||
{props.allowDelete && <button onClick={() => props.onDelete(props.tagId)}>Delete</button>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/tags-combobox", () => ({
|
||||
TagsCombobox: (props: any) => (
|
||||
<div data-testid="tags-combobox">
|
||||
<button onClick={() => props.createTag("NewTag")}>CreateTag</button>
|
||||
<button onClick={() => props.addTag("tag3")}>AddTag</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("ResponseTagsWrapper", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders settings button when not readOnly and navigates on click", async () => {
|
||||
render(
|
||||
<ResponseTagsWrapper
|
||||
tags={dummyTags}
|
||||
environmentId={dummyEnvironmentId}
|
||||
responseId={dummyResponseId}
|
||||
environmentTags={dummyEnvironmentTags}
|
||||
updateFetchedResponses={dummyUpdateFetchedResponses}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<ResponseTagsWrapper
|
||||
tags={dummyTags}
|
||||
environmentId={dummyEnvironmentId}
|
||||
responseId={dummyResponseId}
|
||||
environmentTags={dummyEnvironmentTags}
|
||||
updateFetchedResponses={dummyUpdateFetchedResponses}
|
||||
isReadOnly={true}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByRole("button")).toBeNull();
|
||||
});
|
||||
|
||||
test("renders provided tags", () => {
|
||||
render(
|
||||
<ResponseTagsWrapper
|
||||
tags={dummyTags}
|
||||
environmentId={dummyEnvironmentId}
|
||||
responseId={dummyResponseId}
|
||||
environmentTags={dummyEnvironmentTags}
|
||||
updateFetchedResponses={dummyUpdateFetchedResponses}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<ResponseTagsWrapper
|
||||
tags={dummyTags}
|
||||
environmentId={dummyEnvironmentId}
|
||||
responseId={dummyResponseId}
|
||||
environmentTags={dummyEnvironmentTags}
|
||||
updateFetchedResponses={dummyUpdateFetchedResponses}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<ResponseTagsWrapper
|
||||
tags={dummyTags}
|
||||
environmentId={dummyEnvironmentId}
|
||||
responseId={dummyResponseId}
|
||||
environmentTags={dummyEnvironmentTags}
|
||||
updateFetchedResponses={dummyUpdateFetchedResponses}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
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: { id: "newTagId", name: "NewTag" } } as any);
|
||||
vi.mocked(createTagToResponseAction).mockResolvedValueOnce({ data: "tagAdded" } as any);
|
||||
render(
|
||||
<ResponseTagsWrapper
|
||||
tags={dummyTags}
|
||||
environmentId={dummyEnvironmentId}
|
||||
responseId={dummyResponseId}
|
||||
environmentTags={dummyEnvironmentTags}
|
||||
updateFetchedResponses={dummyUpdateFetchedResponses}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
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({
|
||||
error: { details: [{ issue: "Unique constraint failed on the fields" }] },
|
||||
} as any);
|
||||
render(
|
||||
<ResponseTagsWrapper
|
||||
tags={dummyTags}
|
||||
environmentId={dummyEnvironmentId}
|
||||
responseId={dummyResponseId}
|
||||
environmentTags={dummyEnvironmentTags}
|
||||
updateFetchedResponses={dummyUpdateFetchedResponses}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<ResponseTagsWrapper
|
||||
tags={dummyTags}
|
||||
environmentId={dummyEnvironmentId}
|
||||
responseId={dummyResponseId}
|
||||
environmentTags={dummyEnvironmentTags}
|
||||
updateFetchedResponses={dummyUpdateFetchedResponses}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<ResponseTagsWrapper
|
||||
tags={dummyTags}
|
||||
environmentId={dummyEnvironmentId}
|
||||
responseId={dummyResponseId}
|
||||
environmentTags={dummyEnvironmentTags}
|
||||
updateFetchedResponses={dummyUpdateFetchedResponses}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import { useTranslate } from "@tolgee/react";
|
||||
import { AlertCircleIcon, SettingsIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import toast from "react-hot-toast";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { createTagAction, createTagToResponseAction, deleteTagOnResponseAction } from "../actions";
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
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) => <div>{children}</div>,
|
||||
TooltipContent: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipProvider: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipTrigger: ({ children }: any) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
// 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: () => <div data-testid="FileDigitIcon" />,
|
||||
FileType2Icon: () => <div data-testid="FileType2Icon" />,
|
||||
}));
|
||||
|
||||
describe("ResponseVariables", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders nothing when no variable in variablesData meets type check", () => {
|
||||
render(
|
||||
<ResponseVariables
|
||||
variables={dummyVariables}
|
||||
variablesData={{ v3: { not: "valid" } } as unknown as TResponseVariables}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByText("Variable One")).toBeNull();
|
||||
expect(screen.queryByText("Variable Two")).toBeNull();
|
||||
});
|
||||
|
||||
test("renders variables with valid response data", () => {
|
||||
render(<ResponseVariables variables={dummyVariables} variablesData={dummyVariablesData} />);
|
||||
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(<ResponseVariables variables={dummyVariables} variablesData={dummyVariablesData} />);
|
||||
expect(screen.getByTestId("FileDigitIcon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("FileType2Icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays tooltip content with 'common.variable'", () => {
|
||||
render(<ResponseVariables variables={dummyVariables} variablesData={dummyVariablesData} />);
|
||||
// TooltipContent mock always renders its children directly.
|
||||
expect(screen.getAllByText("common.variable")[0]).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
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) => <div data-testid="QuestionSkip">{props.status}</div>,
|
||||
}));
|
||||
vi.mock("./RenderResponse", () => ({
|
||||
RenderResponse: (props: any) => <div data-testid="RenderResponse">{props.responseData.toString()}</div>,
|
||||
}));
|
||||
vi.mock("./ResponseVariables", () => ({
|
||||
ResponseVariables: (props: any) => <div data-testid="ResponseVariables">Variables</div>,
|
||||
}));
|
||||
vi.mock("./HiddenFields", () => ({
|
||||
HiddenFields: (props: any) => <div data-testid="HiddenFields">Hidden</div>,
|
||||
}));
|
||||
vi.mock("./VerifiedEmail", () => ({
|
||||
VerifiedEmail: (props: any) => <div data-testid="VerifiedEmail">VerifiedEmail</div>,
|
||||
}));
|
||||
|
||||
// Mocks for utility functions used inside component
|
||||
vi.mock("@formbricks/lib/utils/recall", () => ({
|
||||
parseRecallInfo: vi.fn((headline, data) => "parsed:" + headline),
|
||||
}));
|
||||
vi.mock("@formbricks/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: () => <div data-testid="CheckCircle2Icon">CheckCircle</div>,
|
||||
}));
|
||||
|
||||
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(<SingleResponseCardBody survey={dummySurvey} response={dummyResponse} skippedQuestions={[]} />);
|
||||
expect(screen.getAllByTestId("QuestionSkip")[0]).toHaveTextContent("welcomeCard");
|
||||
});
|
||||
|
||||
test("renders VerifiedEmail when enabled and response verified", () => {
|
||||
render(<SingleResponseCardBody survey={dummySurvey} response={dummyResponse} skippedQuestions={[]} />);
|
||||
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(<SingleResponseCardBody survey={surveyCopy} response={responseCopy} skippedQuestions={[]} />);
|
||||
// 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(
|
||||
<SingleResponseCardBody survey={surveyCopy} response={responseCopy} skippedQuestions={[["q1"]]} />
|
||||
);
|
||||
// Renders QuestionSkip for q1 or q2 branch
|
||||
expect(screen.getAllByTestId("QuestionSkip")[1]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ResponseVariables when variables exist", () => {
|
||||
render(<SingleResponseCardBody survey={dummySurvey} response={dummyResponse} skippedQuestions={[]} />);
|
||||
expect(screen.getByTestId("ResponseVariables")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders HiddenFields when hiddenFields enabled", () => {
|
||||
render(<SingleResponseCardBody survey={dummySurvey} response={dummyResponse} skippedQuestions={[]} />);
|
||||
expect(screen.getByTestId("HiddenFields")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders completion indicator when response finished", () => {
|
||||
render(<SingleResponseCardBody survey={dummySurvey} response={dummyResponse} skippedQuestions={[]} />);
|
||||
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(
|
||||
<SingleResponseCardBody survey={surveyCopy} response={responseCopy} skippedQuestions={[["q2"]]} />
|
||||
);
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import { isSubmissionTimeMoreThan5Minutes } from "@/modules/analysis/components/SingleResponseCard/util";
|
||||
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 { SingleResponseCardHeader } from "./SingleResponseCardHeader";
|
||||
|
||||
// Mocks
|
||||
vi.mock("@/modules/ui/components/avatars", () => ({
|
||||
PersonAvatar: ({ personId }: any) => <div data-testid="PersonAvatar">Avatar: {personId}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/survey-status-indicator", () => ({
|
||||
SurveyStatusIndicator: ({ status }: any) => <div data-testid="SurveyStatusIndicator">Status: {status}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/tooltip", () => ({
|
||||
Tooltip: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipContent: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipProvider: ({ children }: any) => <div>{children}</div>,
|
||||
TooltipTrigger: ({ children }: any) => <div>{children}</div>,
|
||||
}));
|
||||
vi.mock("@formbricks/lib/i18n/utils", () => ({
|
||||
getLanguageLabel: vi.fn((lang, locale) => lang + "_" + locale),
|
||||
}));
|
||||
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(
|
||||
<SingleResponseCardHeader
|
||||
pageType="response"
|
||||
response={dummyResponse}
|
||||
survey={{ ...dummySurvey }}
|
||||
environment={dummyEnvironment}
|
||||
user={dummyUser}
|
||||
isReadOnly={false}
|
||||
setDeleteDialogOpen={vi.fn()}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
// 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(
|
||||
<SingleResponseCardHeader
|
||||
pageType="response"
|
||||
response={responseNoContact}
|
||||
survey={{ ...dummySurvey }}
|
||||
environment={dummyEnvironment}
|
||||
user={dummyUser}
|
||||
isReadOnly={false}
|
||||
setDeleteDialogOpen={vi.fn()}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("common.anonymous")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders people view", () => {
|
||||
render(
|
||||
<SingleResponseCardHeader
|
||||
pageType="people"
|
||||
response={dummyResponse}
|
||||
survey={{ ...dummySurvey, type: "link" }}
|
||||
environment={dummyEnvironment}
|
||||
user={dummyUser}
|
||||
isReadOnly={false}
|
||||
setDeleteDialogOpen={vi.fn()}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole("link")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Survey")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("SurveyStatusIndicator")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders language label when response.language is not default", () => {
|
||||
const modifiedResponse = { ...dummyResponse, language: "fr" };
|
||||
render(
|
||||
<SingleResponseCardHeader
|
||||
pageType="response"
|
||||
response={modifiedResponse}
|
||||
survey={{ ...dummySurvey }}
|
||||
environment={dummyEnvironment}
|
||||
user={dummyUser}
|
||||
isReadOnly={false}
|
||||
setDeleteDialogOpen={vi.fn()}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("fr_en-US")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders enabled trash icon and handles click", async () => {
|
||||
vi.mocked(isSubmissionTimeMoreThan5Minutes).mockReturnValue(true);
|
||||
const setDeleteDialogOpen = vi.fn();
|
||||
render(
|
||||
<SingleResponseCardHeader
|
||||
pageType="response"
|
||||
response={dummyResponse}
|
||||
survey={{ ...dummySurvey }}
|
||||
environment={dummyEnvironment}
|
||||
user={dummyUser}
|
||||
isReadOnly={false}
|
||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<SingleResponseCardHeader
|
||||
pageType="response"
|
||||
response={dummyResponse}
|
||||
survey={{ ...dummySurvey }}
|
||||
environment={dummyEnvironment}
|
||||
user={dummyUser}
|
||||
isReadOnly={false}
|
||||
setDeleteDialogOpen={vi.fn()}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
const disabledTrash = screen.getByLabelText("Cannot delete response in progress");
|
||||
expect(disabledTrash).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import {
|
||||
ConfusedFace,
|
||||
FrowningFace,
|
||||
GrinningFaceWithSmilingEyes,
|
||||
GrinningSquintingFace,
|
||||
NeutralFace,
|
||||
PerseveringFace,
|
||||
SlightlySmilingFace,
|
||||
SmilingFaceWithSmilingEyes,
|
||||
TiredFace,
|
||||
WearyFace,
|
||||
} from "./Smileys";
|
||||
|
||||
const checkSvg = (Component: React.FC<React.SVGProps<SVGElement>>) => {
|
||||
const { container } = render(<Component />);
|
||||
const svg = container.querySelector("svg");
|
||||
expect(svg).toBeTruthy();
|
||||
expect(svg).toHaveAttribute("viewBox", "0 0 72 72");
|
||||
expect(svg).toHaveAttribute("width", "36");
|
||||
expect(svg).toHaveAttribute("height", "36");
|
||||
};
|
||||
|
||||
describe("Smileys", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders TiredFace", () => {
|
||||
checkSvg(TiredFace);
|
||||
});
|
||||
test("renders WearyFace", () => {
|
||||
checkSvg(WearyFace);
|
||||
});
|
||||
test("renders PerseveringFace", () => {
|
||||
checkSvg(PerseveringFace);
|
||||
});
|
||||
test("renders FrowningFace", () => {
|
||||
checkSvg(FrowningFace);
|
||||
});
|
||||
test("renders ConfusedFace", () => {
|
||||
checkSvg(ConfusedFace);
|
||||
});
|
||||
test("renders NeutralFace", () => {
|
||||
checkSvg(NeutralFace);
|
||||
});
|
||||
test("renders SlightlySmilingFace", () => {
|
||||
checkSvg(SlightlySmilingFace);
|
||||
});
|
||||
test("renders SmilingFaceWithSmilingEyes", () => {
|
||||
checkSvg(SmilingFaceWithSmilingEyes);
|
||||
});
|
||||
test("renders GrinningFaceWithSmilingEyes", () => {
|
||||
checkSvg(GrinningFaceWithSmilingEyes);
|
||||
});
|
||||
test("renders GrinningSquintingFace", () => {
|
||||
checkSvg(GrinningSquintingFace);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
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) => (
|
||||
<div data-testid="MailIcon" {...props}>
|
||||
MailIcon
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("VerifiedEmail", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders verified email text and value when provided", () => {
|
||||
render(<VerifiedEmail responseData={{ verifiedEmail: "test@example.com" }} />);
|
||||
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(<VerifiedEmail responseData={{ verifiedEmail: 123 }} />);
|
||||
expect(screen.getByText("common.verified_email")).toBeInTheDocument();
|
||||
const emptyParagraph = screen.getByText("", { selector: "p.ph-no-capture" });
|
||||
expect(emptyParagraph.textContent).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
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,
|
||||
notes: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as unknown as TSurvey;
|
||||
const dummyResponse = {
|
||||
id: "resp1",
|
||||
finished: true,
|
||||
data: { q1: "answer1", q2: null },
|
||||
notes: [],
|
||||
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 dummyDeleteResponses = vi.fn();
|
||||
const dummyUpdateResponse = vi.fn();
|
||||
const dummySetSelectedResponseId = vi.fn();
|
||||
|
||||
// Mock internal components to return identifiable elements
|
||||
vi.mock("./components/SingleResponseCardHeader", () => ({
|
||||
SingleResponseCardHeader: (props: any) => (
|
||||
<div data-testid="SingleResponseCardHeader">
|
||||
<button onClick={() => props.setDeleteDialogOpen(true)}>Open Delete</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("./components/SingleResponseCardBody", () => ({
|
||||
SingleResponseCardBody: () => <div data-testid="SingleResponseCardBody">Body Content</div>,
|
||||
}));
|
||||
vi.mock("./components/ResponseTagsWrapper", () => ({
|
||||
ResponseTagsWrapper: (props: any) => (
|
||||
<div data-testid="ResponseTagsWrapper">
|
||||
<button onClick={() => props.updateFetchedResponses()}>Update Responses</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/delete-dialog", () => ({
|
||||
DeleteDialog: ({ open, onDelete }: any) =>
|
||||
open ? (
|
||||
<button data-testid="DeleteDialog" onClick={() => onDelete()}>
|
||||
Confirm Delete
|
||||
</button>
|
||||
) : null,
|
||||
}));
|
||||
vi.mock("./components/ResponseNote", () => ({
|
||||
ResponseNotes: (props: any) => <div data-testid="ResponseNotes">Notes ({props.notes.length})</div>,
|
||||
}));
|
||||
|
||||
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(
|
||||
<SingleResponseCard
|
||||
survey={draftSurvey}
|
||||
response={dummyResponse}
|
||||
user={dummyUser}
|
||||
pageType="response"
|
||||
environmentTags={[]}
|
||||
environment={dummyEnvironment}
|
||||
updateResponse={dummyUpdateResponse}
|
||||
deleteResponses={dummyDeleteResponses}
|
||||
isReadOnly={true}
|
||||
setSelectedResponseId={dummySetSelectedResponseId}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("SingleResponseCardHeader")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("link")).toBeNull();
|
||||
});
|
||||
|
||||
test("calls deleteResponseAction and refreshes router on successful deletion", async () => {
|
||||
render(
|
||||
<SingleResponseCard
|
||||
survey={dummySurvey}
|
||||
response={dummyResponse}
|
||||
user={dummyUser}
|
||||
pageType="response"
|
||||
environmentTags={[]}
|
||||
environment={dummyEnvironment}
|
||||
updateResponse={dummyUpdateResponse}
|
||||
deleteResponses={dummyDeleteResponses}
|
||||
isReadOnly={false}
|
||||
setSelectedResponseId={dummySetSelectedResponseId}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByText("Open Delete"));
|
||||
|
||||
const deleteButton = await screen.findByTestId("DeleteDialog");
|
||||
await userEvent.click(deleteButton);
|
||||
await waitFor(() => {
|
||||
expect(deleteResponseAction).toHaveBeenCalledWith({ responseId: dummyResponse.id });
|
||||
});
|
||||
|
||||
expect(dummyDeleteResponses).toHaveBeenCalledWith([dummyResponse.id]);
|
||||
});
|
||||
|
||||
test("calls toast.error when deleteResponseAction throws error", async () => {
|
||||
vi.mocked(deleteResponseAction).mockRejectedValueOnce(new Error("Delete failed"));
|
||||
render(
|
||||
<SingleResponseCard
|
||||
survey={dummySurvey}
|
||||
response={dummyResponse}
|
||||
user={dummyUser}
|
||||
pageType="response"
|
||||
environmentTags={[]}
|
||||
environment={dummyEnvironment}
|
||||
updateResponse={dummyUpdateResponse}
|
||||
deleteResponses={dummyDeleteResponses}
|
||||
isReadOnly={false}
|
||||
setSelectedResponseId={dummySetSelectedResponseId}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<SingleResponseCard
|
||||
survey={dummySurvey}
|
||||
response={dummyResponse}
|
||||
user={dummyUser}
|
||||
pageType="response"
|
||||
environmentTags={[]}
|
||||
environment={dummyEnvironment}
|
||||
updateResponse={dummyUpdateResponse}
|
||||
deleteResponses={dummyDeleteResponses}
|
||||
isReadOnly={false}
|
||||
setSelectedResponseId={dummySetSelectedResponseId}
|
||||
locale={dummyLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,8 +11,7 @@ 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 { TUserLocale } from "@formbricks/types/user";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
import { deleteResponseAction, getResponseAction } from "./actions";
|
||||
import { ResponseNotes } from "./components/ResponseNote";
|
||||
import { ResponseTagsWrapper } from "./components/ResponseTagsWrapper";
|
||||
@@ -61,28 +60,24 @@ export const SingleResponseCard = ({
|
||||
survey.questions.forEach((question) => {
|
||||
if (!isValidValue(response.data[question.id])) {
|
||||
temp.push(question.id);
|
||||
} else {
|
||||
if (temp.length > 0) {
|
||||
skippedQuestions.push([...temp]);
|
||||
temp = [];
|
||||
}
|
||||
} else if (temp.length > 0) {
|
||||
skippedQuestions.push([...temp]);
|
||||
temp = [];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
for (let index = survey.questions.length - 1; index >= 0; index--) {
|
||||
const question = survey.questions[index];
|
||||
if (!response.data[question.id]) {
|
||||
if (skippedQuestions.length === 0) {
|
||||
temp.push(question.id);
|
||||
} else if (skippedQuestions.length > 0 && !isValidValue(response.data[question.id])) {
|
||||
temp.push(question.id);
|
||||
}
|
||||
} else {
|
||||
if (temp.length > 0) {
|
||||
temp.reverse();
|
||||
skippedQuestions.push([...temp]);
|
||||
temp = [];
|
||||
}
|
||||
if (
|
||||
!response.data[question.id] &&
|
||||
(skippedQuestions.length === 0 ||
|
||||
(skippedQuestions.length > 0 && !isValidValue(response.data[question.id])))
|
||||
) {
|
||||
temp.push(question.id);
|
||||
} else if (temp.length > 0) {
|
||||
temp.reverse();
|
||||
skippedQuestions.push([...temp]);
|
||||
temp = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { isSubmissionTimeMoreThan5Minutes, isValidValue } from "./util";
|
||||
|
||||
describe("isValidValue", () => {
|
||||
test("returns false for an empty string", () => {
|
||||
expect(isValidValue("")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for a blank string", () => {
|
||||
expect(isValidValue(" ")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true for a non-empty string", () => {
|
||||
expect(isValidValue("hello")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true for numbers", () => {
|
||||
expect(isValidValue(0)).toBe(true);
|
||||
expect(isValidValue(42)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for an empty array", () => {
|
||||
expect(isValidValue([])).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true for a non-empty array", () => {
|
||||
expect(isValidValue(["item"])).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for an empty object", () => {
|
||||
expect(isValidValue({})).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true for a non-empty object", () => {
|
||||
expect(isValidValue({ key: "value" })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSubmissionTimeMoreThan5Minutes", () => {
|
||||
test("returns true if submission time is more than 5 minutes ago", () => {
|
||||
const currentTime = new Date();
|
||||
const oldTime = new Date(currentTime.getTime() - 6 * 60 * 1000); // 6 minutes ago
|
||||
expect(isSubmissionTimeMoreThan5Minutes(oldTime)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false if submission time is less than or equal to 5 minutes ago", () => {
|
||||
const currentTime = new Date();
|
||||
const recentTime = new Date(currentTime.getTime() - 4 * 60 * 1000); // 4 minutes ago
|
||||
expect(isSubmissionTimeMoreThan5Minutes(recentTime)).toBe(false);
|
||||
|
||||
const exact5Minutes = new Date(currentTime.getTime() - 5 * 60 * 1000); // exactly 5 minutes ago
|
||||
expect(isSubmissionTimeMoreThan5Minutes(exact5Minutes)).toBe(false);
|
||||
});
|
||||
});
|
||||
67
apps/web/modules/analysis/utils.test.tsx
Normal file
67
apps/web/modules/analysis/utils.test.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { isValidElement } from "react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { renderHyperlinkedContent } from "./utils";
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { authenticateRequest } from "@/modules/api/v2/auth/authenticate-request"
|
||||
import { checkRateLimitAndThrowError } from "@/modules/api/v2/lib/rate-limit";
|
||||
import { handleApiError } from "@/modules/api/v2/lib/utils";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { err, ok, okVoid } from "@formbricks/types/error-handlers";
|
||||
|
||||
@@ -25,7 +25,7 @@ vi.mock("@/modules/api/v2/lib/utils", () => ({
|
||||
}));
|
||||
|
||||
describe("apiWrapper", () => {
|
||||
it("should handle request and return response", async () => {
|
||||
test("should handle request and return response", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "valid-api-key" },
|
||||
});
|
||||
@@ -49,7 +49,7 @@ describe("apiWrapper", () => {
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle errors and return error response", async () => {
|
||||
test("should handle errors and return error response", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "invalid-api-key" },
|
||||
});
|
||||
@@ -67,7 +67,7 @@ describe("apiWrapper", () => {
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should parse body schema correctly", async () => {
|
||||
test("should parse body schema correctly", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ key: "value" }),
|
||||
@@ -100,7 +100,7 @@ describe("apiWrapper", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle body schema errors", async () => {
|
||||
test("should handle body schema errors", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ key: 123 }),
|
||||
@@ -131,7 +131,7 @@ describe("apiWrapper", () => {
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should parse query schema correctly", async () => {
|
||||
test("should parse query schema correctly", async () => {
|
||||
const request = new Request("http://localhost?key=value");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
@@ -160,7 +160,7 @@ describe("apiWrapper", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle query schema errors", async () => {
|
||||
test("should handle query schema errors", async () => {
|
||||
const request = new Request("http://localhost?foo%ZZ=abc");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
@@ -187,7 +187,7 @@ describe("apiWrapper", () => {
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should parse params schema correctly", async () => {
|
||||
test("should parse params schema correctly", async () => {
|
||||
const request = new Request("http://localhost");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
@@ -217,7 +217,7 @@ describe("apiWrapper", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle no external params", async () => {
|
||||
test("should handle no external params", async () => {
|
||||
const request = new Request("http://localhost");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
@@ -245,7 +245,7 @@ describe("apiWrapper", () => {
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle params schema errors", async () => {
|
||||
test("should handle params schema errors", async () => {
|
||||
const request = new Request("http://localhost");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
@@ -273,7 +273,7 @@ describe("apiWrapper", () => {
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle rate limit errors", async () => {
|
||||
test("should handle rate limit errors", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "valid-api-key" },
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { hashApiKey } from "@/modules/api/v2/management/lib/utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { authenticateRequest } from "../authenticate-request";
|
||||
|
||||
@@ -17,7 +17,7 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({
|
||||
}));
|
||||
|
||||
describe("authenticateRequest", () => {
|
||||
it("should return authentication data if apiKey is valid", async () => {
|
||||
test("should return authentication data if apiKey is valid", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "valid-api-key" },
|
||||
});
|
||||
@@ -87,7 +87,7 @@ describe("authenticateRequest", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should return unauthorized error if apiKey is not found", async () => {
|
||||
test("should return unauthorized error if apiKey is not found", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "invalid-api-key" },
|
||||
});
|
||||
@@ -101,7 +101,7 @@ describe("authenticateRequest", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should return unauthorized error if apiKey is missing", async () => {
|
||||
test("should return unauthorized error if apiKey is missing", async () => {
|
||||
const request = new Request("http://localhost");
|
||||
|
||||
const result = await authenticateRequest(request);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { logApiRequest } from "@/modules/api/v2/lib/utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { apiWrapper } from "../api-wrapper";
|
||||
import { authenticatedApiClient } from "../authenticated-api-client";
|
||||
|
||||
@@ -12,7 +12,7 @@ vi.mock("@/modules/api/v2/lib/utils", () => ({
|
||||
}));
|
||||
|
||||
describe("authenticatedApiClient", () => {
|
||||
it("should log request and return response", async () => {
|
||||
test("should log request and return response", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "valid-api-key" },
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fetchEnvironmentIdFromSurveyIds } from "@/modules/api/v2/management/lib/services";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { err, ok } from "@formbricks/types/error-handlers";
|
||||
import { getEnvironmentId, getEnvironmentIdFromSurveyIds } from "../helper";
|
||||
import { fetchEnvironmentId } from "../services";
|
||||
@@ -12,7 +12,7 @@ vi.mock("../services", () => ({
|
||||
}));
|
||||
|
||||
describe("Tests for getEnvironmentId", () => {
|
||||
it("should return environmentId for surveyId", async () => {
|
||||
test("should return environmentId for surveyId", async () => {
|
||||
vi.mocked(fetchEnvironmentId).mockResolvedValue(ok({ environmentId: "env-id" }));
|
||||
|
||||
const result = await getEnvironmentId("survey-id", false);
|
||||
@@ -22,7 +22,7 @@ describe("Tests for getEnvironmentId", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should return environmentId for responseId", async () => {
|
||||
test("should return environmentId for responseId", async () => {
|
||||
vi.mocked(fetchEnvironmentId).mockResolvedValue(ok({ environmentId: "env-id" }));
|
||||
|
||||
const result = await getEnvironmentId("response-id", true);
|
||||
@@ -32,7 +32,7 @@ describe("Tests for getEnvironmentId", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should return error if getSurveyAndEnvironmentId fails", async () => {
|
||||
test("should return error if getSurveyAndEnvironmentId fails", async () => {
|
||||
vi.mocked(fetchEnvironmentId).mockResolvedValue(
|
||||
err({ type: "not_found" } as unknown as ApiErrorResponseV2)
|
||||
);
|
||||
@@ -49,7 +49,7 @@ describe("getEnvironmentIdFromSurveyIds", () => {
|
||||
const envId1 = createId();
|
||||
const envId2 = createId();
|
||||
|
||||
it("returns the common environment id when all survey ids are in the same environment", async () => {
|
||||
test("returns the common environment id when all survey ids are in the same environment", async () => {
|
||||
vi.mocked(fetchEnvironmentIdFromSurveyIds).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
data: [envId1, envId1],
|
||||
@@ -58,7 +58,7 @@ describe("getEnvironmentIdFromSurveyIds", () => {
|
||||
expect(result).toEqual(ok(envId1));
|
||||
});
|
||||
|
||||
it("returns error when surveys are not in the same environment", async () => {
|
||||
test("returns error when surveys are not in the same environment", async () => {
|
||||
vi.mocked(fetchEnvironmentIdFromSurveyIds).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
data: [envId1, envId2],
|
||||
@@ -73,7 +73,7 @@ describe("getEnvironmentIdFromSurveyIds", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns error when API call fails", async () => {
|
||||
test("returns error when API call fails", async () => {
|
||||
const apiError = {
|
||||
type: "server_error",
|
||||
details: [{ field: "api", issue: "failed" }],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils";
|
||||
import { TGetResponsesFilter } from "@/modules/api/v2/management/responses/types/responses";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { getResponsesQuery } from "../utils";
|
||||
|
||||
vi.mock("@/modules/api/v2/management/lib/utils", () => ({
|
||||
@@ -10,17 +10,17 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({
|
||||
}));
|
||||
|
||||
describe("getResponsesQuery", () => {
|
||||
it("adds surveyId to where clause if provided", () => {
|
||||
test("adds surveyId to where clause if provided", () => {
|
||||
const result = getResponsesQuery(["env-id"], { surveyId: "survey123" } as TGetResponsesFilter);
|
||||
expect(result?.where?.surveyId).toBe("survey123");
|
||||
});
|
||||
|
||||
it("adds contactId to where clause if provided", () => {
|
||||
test("adds contactId to where clause if provided", () => {
|
||||
const result = getResponsesQuery(["env-id"], { contactId: "contact123" } as TGetResponsesFilter);
|
||||
expect(result?.where?.contactId).toBe("contact123");
|
||||
});
|
||||
|
||||
it("calls pickCommonFilter & buildCommonFilterQuery with correct arguments", () => {
|
||||
test("calls pickCommonFilter & buildCommonFilterQuery with correct arguments", () => {
|
||||
vi.mocked(pickCommonFilter).mockReturnValueOnce({ someFilter: true } as any);
|
||||
vi.mocked(buildCommonFilterQuery).mockReturnValueOnce({ where: { combined: true } as any });
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils";
|
||||
import { TGetWebhooksFilter } from "@/modules/api/v2/management/webhooks/types/webhooks";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { getWebhooksQuery } from "../utils";
|
||||
|
||||
vi.mock("@/modules/api/v2/management/lib/utils", () => ({
|
||||
@@ -11,7 +11,7 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({
|
||||
describe("getWebhooksQuery", () => {
|
||||
const environmentId = "env-123";
|
||||
|
||||
it("adds surveyIds condition when provided", () => {
|
||||
test("adds surveyIds condition when provided", () => {
|
||||
const params = { surveyIds: ["survey1"] } as TGetWebhooksFilter;
|
||||
const result = getWebhooksQuery([environmentId], params);
|
||||
expect(result).toBeDefined();
|
||||
@@ -21,14 +21,14 @@ describe("getWebhooksQuery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("calls pickCommonFilter and buildCommonFilterQuery when baseFilter is present", () => {
|
||||
test("calls pickCommonFilter and buildCommonFilterQuery when baseFilter is present", () => {
|
||||
vi.mocked(pickCommonFilter).mockReturnValue({ someFilter: "test" } as any);
|
||||
getWebhooksQuery([environmentId], { surveyIds: ["survey1"] } as TGetWebhooksFilter);
|
||||
expect(pickCommonFilter).toHaveBeenCalled();
|
||||
expect(buildCommonFilterQuery).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("buildCommonFilterQuery is not called if no baseFilter is picked", () => {
|
||||
test("buildCommonFilterQuery is not called if no baseFilter is picked", () => {
|
||||
vi.mocked(pickCommonFilter).mockReturnValue(undefined as any);
|
||||
getWebhooksQuery([environmentId], {} as any);
|
||||
expect(buildCommonFilterQuery).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { TGetWebhooksFilter, TWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks";
|
||||
import { WebhookSource } from "@prisma/client";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { captureTelemetry } from "@formbricks/lib/telemetry";
|
||||
import { createWebhook, getWebhooks } from "../webhook";
|
||||
@@ -37,7 +37,7 @@ describe("getWebhooks", () => {
|
||||
];
|
||||
const count = fakeWebhooks.length;
|
||||
|
||||
it("returns ok response with webhooks and meta", async () => {
|
||||
test("returns ok response with webhooks and meta", async () => {
|
||||
vi.mocked(prisma.$transaction).mockResolvedValueOnce([fakeWebhooks, count]);
|
||||
|
||||
const result = await getWebhooks(environmentId, params as TGetWebhooksFilter);
|
||||
@@ -53,7 +53,7 @@ describe("getWebhooks", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns error when prisma.$transaction throws", async () => {
|
||||
test("returns error when prisma.$transaction throws", async () => {
|
||||
vi.mocked(prisma.$transaction).mockRejectedValueOnce(new Error("Test error"));
|
||||
|
||||
const result = await getWebhooks(environmentId, params as TGetWebhooksFilter);
|
||||
@@ -87,7 +87,7 @@ describe("createWebhook", () => {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
it("creates a webhook and revalidates cache", async () => {
|
||||
test("creates a webhook and revalidates cache", async () => {
|
||||
vi.mocked(prisma.webhook.create).mockResolvedValueOnce(createdWebhook);
|
||||
|
||||
const result = await createWebhook(inputWebhook);
|
||||
@@ -104,7 +104,7 @@ describe("createWebhook", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns error when creation fails", async () => {
|
||||
test("returns error when creation fails", async () => {
|
||||
vi.mocked(prisma.webhook.create).mockRejectedValueOnce(new Error("Creation failed"));
|
||||
|
||||
const result = await createWebhook(inputWebhook);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { OrganizationAccessType } from "@formbricks/types/api-key";
|
||||
import { hasOrganizationIdAndAccess } from "./utils";
|
||||
@@ -8,7 +8,7 @@ describe("hasOrganizationIdAndAccess", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should return false and log error if authentication has no organizationId", () => {
|
||||
test("should return false and log error if authentication has no organizationId", () => {
|
||||
const spyError = vi.spyOn(logger, "error").mockImplementation(() => {});
|
||||
const authentication = {
|
||||
organizationAccess: { accessControl: { read: true } },
|
||||
@@ -21,7 +21,7 @@ describe("hasOrganizationIdAndAccess", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false and log error if param organizationId does not match authentication organizationId", () => {
|
||||
test("should return false and log error if param organizationId does not match authentication organizationId", () => {
|
||||
const spyError = vi.spyOn(logger, "error").mockImplementation(() => {});
|
||||
const authentication = {
|
||||
organizationId: "org2",
|
||||
@@ -35,7 +35,7 @@ describe("hasOrganizationIdAndAccess", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false if access type is missing in organizationAccess", () => {
|
||||
test("should return false if access type is missing in organizationAccess", () => {
|
||||
const authentication = {
|
||||
organizationId: "org1",
|
||||
organizationAccess: { accessControl: {} },
|
||||
@@ -45,7 +45,7 @@ describe("hasOrganizationIdAndAccess", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true if organizationId and access type are valid", () => {
|
||||
test("should return true if organizationId and access type are valid", () => {
|
||||
const authentication = {
|
||||
organizationId: "org1",
|
||||
organizationAccess: { accessControl: { read: true } },
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
TProjectTeamInput,
|
||||
ZProjectZTeamUpdateSchema,
|
||||
} from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TypeOf } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { createProjectTeam, deleteProjectTeam, getProjectTeams, updateProjectTeam } from "../project-teams";
|
||||
@@ -27,7 +27,7 @@ describe("ProjectTeams Lib", () => {
|
||||
});
|
||||
|
||||
describe("getProjectTeams", () => {
|
||||
it("returns projectTeams with meta on success", async () => {
|
||||
test("returns projectTeams with meta on success", async () => {
|
||||
const mockTeams = [{ id: "projTeam1", organizationId: "orgx", projectId: "p1", teamId: "t1" }];
|
||||
(prisma.$transaction as any).mockResolvedValueOnce([mockTeams, mockTeams.length]);
|
||||
const result = await getProjectTeams("orgx", { skip: 0, limit: 10 } as TGetProjectTeamsFilter);
|
||||
@@ -41,7 +41,7 @@ describe("ProjectTeams Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns internal_server_error on exception", async () => {
|
||||
test("returns internal_server_error on exception", async () => {
|
||||
(prisma.$transaction as any).mockRejectedValueOnce(new Error("DB error"));
|
||||
const result = await getProjectTeams("orgx", { skip: 0, limit: 10 } as TGetProjectTeamsFilter);
|
||||
expect(result.ok).toBe(false);
|
||||
@@ -52,7 +52,7 @@ describe("ProjectTeams Lib", () => {
|
||||
});
|
||||
|
||||
describe("createProjectTeam", () => {
|
||||
it("creates a projectTeam successfully", async () => {
|
||||
test("creates a projectTeam successfully", async () => {
|
||||
const mockCreated = { id: "ptx", projectId: "p1", teamId: "t1", organizationId: "orgx" };
|
||||
(prisma.projectTeam.create as any).mockResolvedValueOnce(mockCreated);
|
||||
const result = await createProjectTeam({
|
||||
@@ -65,7 +65,7 @@ describe("ProjectTeams Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns internal_server_error on error", async () => {
|
||||
test("returns internal_server_error on error", async () => {
|
||||
(prisma.projectTeam.create as any).mockRejectedValueOnce(new Error("Create error"));
|
||||
const result = await createProjectTeam({
|
||||
projectId: "p1",
|
||||
@@ -79,7 +79,7 @@ describe("ProjectTeams Lib", () => {
|
||||
});
|
||||
|
||||
describe("updateProjectTeam", () => {
|
||||
it("updates a projectTeam successfully", async () => {
|
||||
test("updates a projectTeam successfully", async () => {
|
||||
(prisma.projectTeam.update as any).mockResolvedValueOnce({
|
||||
id: "pt01",
|
||||
projectId: "p1",
|
||||
@@ -95,7 +95,7 @@ describe("ProjectTeams Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns internal_server_error on error", async () => {
|
||||
test("returns internal_server_error on error", async () => {
|
||||
(prisma.projectTeam.update as any).mockRejectedValueOnce(new Error("Update error"));
|
||||
const result = await updateProjectTeam("t1", "p1", { permission: "READ" } as unknown as TypeOf<
|
||||
typeof ZProjectZTeamUpdateSchema
|
||||
@@ -108,7 +108,7 @@ describe("ProjectTeams Lib", () => {
|
||||
});
|
||||
|
||||
describe("deleteProjectTeam", () => {
|
||||
it("deletes a projectTeam successfully", async () => {
|
||||
test("deletes a projectTeam successfully", async () => {
|
||||
(prisma.projectTeam.delete as any).mockResolvedValueOnce({
|
||||
projectId: "p1",
|
||||
teamId: "t1",
|
||||
@@ -122,7 +122,7 @@ describe("ProjectTeams Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns internal_server_error on error", async () => {
|
||||
test("returns internal_server_error on error", async () => {
|
||||
(prisma.projectTeam.delete as any).mockRejectedValueOnce(new Error("Delete error"));
|
||||
const result = await deleteProjectTeam("t1", "p1");
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { teamCache } from "@/lib/cache/team";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { deleteTeam, getTeam, updateTeam } from "../teams";
|
||||
@@ -25,7 +25,7 @@ const mockTeam = {
|
||||
|
||||
describe("Teams Lib", () => {
|
||||
describe("getTeam", () => {
|
||||
it("returns the team when found", async () => {
|
||||
test("returns the team when found", async () => {
|
||||
(prisma.team.findUnique as any).mockResolvedValueOnce(mockTeam);
|
||||
const result = await getTeam("org456", "team123");
|
||||
expect(result.ok).toBe(true);
|
||||
@@ -37,7 +37,7 @@ describe("Teams Lib", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a not_found error when team is missing", async () => {
|
||||
test("returns a not_found error when team is missing", async () => {
|
||||
(prisma.team.findUnique as any).mockResolvedValueOnce(null);
|
||||
const result = await getTeam("org456", "team123");
|
||||
expect(result.ok).toBe(false);
|
||||
@@ -49,7 +49,7 @@ describe("Teams Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns an internal_server_error when prisma throws", async () => {
|
||||
test("returns an internal_server_error when prisma throws", async () => {
|
||||
(prisma.team.findUnique as any).mockRejectedValueOnce(new Error("DB error"));
|
||||
const result = await getTeam("org456", "team123");
|
||||
expect(result.ok).toBe(false);
|
||||
@@ -60,7 +60,7 @@ describe("Teams Lib", () => {
|
||||
});
|
||||
|
||||
describe("deleteTeam", () => {
|
||||
it("deletes the team and revalidates cache", async () => {
|
||||
test("deletes the team and revalidates cache", async () => {
|
||||
(prisma.team.delete as any).mockResolvedValueOnce(mockTeam);
|
||||
// Mock teamCache.revalidate
|
||||
const revalidateMock = vi.spyOn(teamCache, "revalidate").mockImplementation(() => {});
|
||||
@@ -82,7 +82,7 @@ describe("Teams Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns not_found error on known prisma error", async () => {
|
||||
test("returns not_found error on known prisma error", async () => {
|
||||
(prisma.team.delete as any).mockRejectedValueOnce(
|
||||
new PrismaClientKnownRequestError("Not found", {
|
||||
code: PrismaErrorType.RecordDoesNotExist,
|
||||
@@ -100,7 +100,7 @@ describe("Teams Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns internal_server_error on exception", async () => {
|
||||
test("returns internal_server_error on exception", async () => {
|
||||
(prisma.team.delete as any).mockRejectedValueOnce(new Error("Delete failed"));
|
||||
const result = await deleteTeam("org456", "team123");
|
||||
expect(result.ok).toBe(false);
|
||||
@@ -114,7 +114,7 @@ describe("Teams Lib", () => {
|
||||
const updateInput = { name: "Updated Team" };
|
||||
const updatedTeam = { ...mockTeam, ...updateInput };
|
||||
|
||||
it("updates the team successfully and revalidates cache", async () => {
|
||||
test("updates the team successfully and revalidates cache", async () => {
|
||||
(prisma.team.update as any).mockResolvedValueOnce(updatedTeam);
|
||||
const revalidateMock = vi.spyOn(teamCache, "revalidate").mockImplementation(() => {});
|
||||
const result = await updateTeam("org456", "team123", updateInput);
|
||||
@@ -136,7 +136,7 @@ describe("Teams Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns not_found error when update fails due to missing team", async () => {
|
||||
test("returns not_found error when update fails due to missing team", async () => {
|
||||
(prisma.team.update as any).mockRejectedValueOnce(
|
||||
new PrismaClientKnownRequestError("Not found", {
|
||||
code: PrismaErrorType.RecordDoesNotExist,
|
||||
@@ -154,7 +154,7 @@ describe("Teams Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns internal_server_error on generic exception", async () => {
|
||||
test("returns internal_server_error on generic exception", async () => {
|
||||
(prisma.team.update as any).mockRejectedValueOnce(new Error("Update failed"));
|
||||
const result = await updateTeam("org456", "team123", updateInput);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TGetTeamsFilter } from "@/modules/api/v2/organizations/[organizationId]/teams/types/teams";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { organizationCache } from "@formbricks/lib/organization/cache";
|
||||
import { createTeam, getTeams } from "../teams";
|
||||
@@ -32,7 +32,7 @@ vi.spyOn(organizationCache, "revalidate").mockImplementation(() => {});
|
||||
|
||||
describe("Teams Lib", () => {
|
||||
describe("createTeam", () => {
|
||||
it("creates a team successfully and revalidates cache", async () => {
|
||||
test("creates a team successfully and revalidates cache", async () => {
|
||||
(prisma.team.create as any).mockResolvedValueOnce(mockTeam);
|
||||
|
||||
const teamInput = { name: "Test Team" };
|
||||
@@ -49,7 +49,7 @@ describe("Teams Lib", () => {
|
||||
if (result.ok) expect(result.data).toEqual(mockTeam);
|
||||
});
|
||||
|
||||
it("returns internal error when prisma.team.create fails", async () => {
|
||||
test("returns internal error when prisma.team.create fails", async () => {
|
||||
(prisma.team.create as any).mockRejectedValueOnce(new Error("Create error"));
|
||||
const teamInput = { name: "Test Team" };
|
||||
const organizationId = "org456";
|
||||
@@ -63,7 +63,7 @@ describe("Teams Lib", () => {
|
||||
|
||||
describe("getTeams", () => {
|
||||
const filter = { limit: 10, skip: 0 };
|
||||
it("returns teams with meta on success", async () => {
|
||||
test("returns teams with meta on success", async () => {
|
||||
const teamsArray = [mockTeam];
|
||||
// Simulate prisma transaction return [teams, count]
|
||||
(prisma.$transaction as any).mockResolvedValueOnce([teamsArray, teamsArray.length]);
|
||||
@@ -80,7 +80,7 @@ describe("Teams Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns internal_server_error when prisma transaction fails", async () => {
|
||||
test("returns internal_server_error when prisma transaction fails", async () => {
|
||||
(prisma.$transaction as any).mockRejectedValueOnce(new Error("Transaction error"));
|
||||
const organizationId = "org456";
|
||||
const result = await getTeams(organizationId, filter as TGetTeamsFilter);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { getTeamsQuery } from "../utils";
|
||||
|
||||
// Mock the common utils functions
|
||||
@@ -12,12 +12,12 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({
|
||||
describe("getTeamsQuery", () => {
|
||||
const organizationId = "org123";
|
||||
|
||||
it("returns base query when no params provided", () => {
|
||||
test("returns base query when no params provided", () => {
|
||||
const result = getTeamsQuery(organizationId);
|
||||
expect(result.where).toEqual({ organizationId });
|
||||
});
|
||||
|
||||
it("returns unchanged query if pickCommonFilter returns null/undefined", () => {
|
||||
test("returns unchanged query if pickCommonFilter returns null/undefined", () => {
|
||||
vi.mocked(pickCommonFilter).mockReturnValueOnce(null as any);
|
||||
const params: any = { someParam: "test" };
|
||||
const result = getTeamsQuery(organizationId, params);
|
||||
@@ -26,7 +26,7 @@ describe("getTeamsQuery", () => {
|
||||
expect(result.where).toEqual({ organizationId });
|
||||
});
|
||||
|
||||
it("calls buildCommonFilterQuery and returns updated query when base filter exists", () => {
|
||||
test("calls buildCommonFilterQuery and returns updated query when base filter exists", () => {
|
||||
const baseFilter = { key: "value" };
|
||||
vi.mocked(pickCommonFilter).mockReturnValueOnce(baseFilter as any);
|
||||
// Simulate buildCommonFilterQuery to merge base query with baseFilter
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { teamCache } from "@/lib/cache/team";
|
||||
import { TGetUsersFilter } from "@/modules/api/v2/organizations/[organizationId]/users/types/users";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { membershipCache } from "@formbricks/lib/membership/cache";
|
||||
import { userCache } from "@formbricks/lib/user/cache";
|
||||
@@ -45,7 +45,7 @@ vi.spyOn(teamCache, "revalidate").mockImplementation(() => {});
|
||||
|
||||
describe("Users Lib", () => {
|
||||
describe("getUsers", () => {
|
||||
it("returns users with meta on success", async () => {
|
||||
test("returns users with meta on success", async () => {
|
||||
const usersArray = [mockUser];
|
||||
(prisma.$transaction as any).mockResolvedValueOnce([usersArray, usersArray.length]);
|
||||
const result = await getUsers("org456", { limit: 10, skip: 0 } as TGetUsersFilter);
|
||||
@@ -68,7 +68,7 @@ describe("Users Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns internal_server_error if prisma fails", async () => {
|
||||
test("returns internal_server_error if prisma fails", async () => {
|
||||
(prisma.$transaction as any).mockRejectedValueOnce(new Error("Transaction error"));
|
||||
const result = await getUsers("org456", { limit: 10, skip: 0 } as TGetUsersFilter);
|
||||
expect(result.ok).toBe(false);
|
||||
@@ -79,7 +79,7 @@ describe("Users Lib", () => {
|
||||
});
|
||||
|
||||
describe("createUser", () => {
|
||||
it("creates user and revalidates caches", async () => {
|
||||
test("creates user and revalidates caches", async () => {
|
||||
(prisma.user.create as any).mockResolvedValueOnce(mockUser);
|
||||
const result = await createUser(
|
||||
{ name: "Test User", email: "test@example.com", role: "member" },
|
||||
@@ -92,7 +92,7 @@ describe("Users Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns internal_server_error if creation fails", async () => {
|
||||
test("returns internal_server_error if creation fails", async () => {
|
||||
(prisma.user.create as any).mockRejectedValueOnce(new Error("Create error"));
|
||||
const result = await createUser({ name: "fail", email: "fail@example.com", role: "manager" }, "org456");
|
||||
expect(result.ok).toBe(false);
|
||||
@@ -103,7 +103,7 @@ describe("Users Lib", () => {
|
||||
});
|
||||
|
||||
describe("updateUser", () => {
|
||||
it("updates user and revalidates caches", async () => {
|
||||
test("updates user and revalidates caches", async () => {
|
||||
(prisma.user.findUnique as any).mockResolvedValueOnce(mockUser);
|
||||
(prisma.$transaction as any).mockResolvedValueOnce([{ ...mockUser, name: "Updated User" }]);
|
||||
const result = await updateUser({ email: mockUser.email, name: "Updated User" }, "org456");
|
||||
@@ -114,7 +114,7 @@ describe("Users Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns not_found if user doesn't exist", async () => {
|
||||
test("returns not_found if user doesn't exist", async () => {
|
||||
(prisma.user.findUnique as any).mockResolvedValueOnce(null);
|
||||
const result = await updateUser({ email: "unknown@example.com" }, "org456");
|
||||
expect(result.ok).toBe(false);
|
||||
@@ -123,7 +123,7 @@ describe("Users Lib", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns internal_server_error if update fails", async () => {
|
||||
test("returns internal_server_error if update fails", async () => {
|
||||
(prisma.user.findUnique as any).mockResolvedValueOnce(mockUser);
|
||||
(prisma.$transaction as any).mockRejectedValueOnce(new Error("Update error"));
|
||||
const result = await updateUser({ email: mockUser.email }, "org456");
|
||||
@@ -135,7 +135,7 @@ describe("Users Lib", () => {
|
||||
});
|
||||
|
||||
describe("createUser with teams", () => {
|
||||
it("creates user with existing teams", async () => {
|
||||
test("creates user with existing teams", async () => {
|
||||
(prisma.team.findMany as any).mockResolvedValueOnce([
|
||||
{ id: "team123", name: "MyTeam", projectTeams: [{ projectId: "proj789" }] },
|
||||
]);
|
||||
@@ -157,7 +157,7 @@ describe("Users Lib", () => {
|
||||
});
|
||||
|
||||
describe("updateUser with team changes", () => {
|
||||
it("removes a team and adds new team", async () => {
|
||||
test("removes a team and adds new team", async () => {
|
||||
(prisma.user.findUnique as any).mockResolvedValueOnce({
|
||||
...mockUser,
|
||||
teamUsers: [{ team: { id: "team123", name: "OldTeam", projectTeams: [{ projectId: "proj789" }] } }],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils";
|
||||
import { TGetUsersFilter } from "@/modules/api/v2/organizations/[organizationId]/users/types/users";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { getUsersQuery } from "../utils";
|
||||
|
||||
vi.mock("@/modules/api/v2/management/lib/utils", () => ({
|
||||
@@ -9,7 +9,7 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({
|
||||
}));
|
||||
|
||||
describe("getUsersQuery", () => {
|
||||
it("returns default query if no params are provided", () => {
|
||||
test("returns default query if no params are provided", () => {
|
||||
const result = getUsersQuery("org123");
|
||||
expect(result).toEqual({
|
||||
where: {
|
||||
@@ -22,7 +22,7 @@ describe("getUsersQuery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("includes email filter if email param is provided", () => {
|
||||
test("includes email filter if email param is provided", () => {
|
||||
const result = getUsersQuery("org123", { email: "test@example.com" } as TGetUsersFilter);
|
||||
expect(result.where?.email).toEqual({
|
||||
contains: "test@example.com",
|
||||
@@ -30,12 +30,12 @@ describe("getUsersQuery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("includes id filter if id param is provided", () => {
|
||||
test("includes id filter if id param is provided", () => {
|
||||
const result = getUsersQuery("org123", { id: "user123" } as TGetUsersFilter);
|
||||
expect(result.where?.id).toBe("user123");
|
||||
});
|
||||
|
||||
it("applies baseFilter if pickCommonFilter returns something", () => {
|
||||
test("applies baseFilter if pickCommonFilter returns something", () => {
|
||||
vi.mocked(pickCommonFilter).mockReturnValueOnce({ someField: "test" } as unknown as ReturnType<
|
||||
typeof pickCommonFilter
|
||||
>);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Logo } from "@/modules/ui/components/logo";
|
||||
import Link from "next/link";
|
||||
|
||||
interface FormWrapperProps {
|
||||
children: React.ReactNode;
|
||||
@@ -9,7 +10,9 @@ export const FormWrapper = ({ children }: FormWrapperProps) => {
|
||||
<div className="mx-auto flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
|
||||
<div className="mx-auto w-full max-w-sm rounded-xl bg-white p-8 shadow-xl lg:w-96">
|
||||
<div className="mb-8 text-center">
|
||||
<Logo className="mx-auto w-3/4" />
|
||||
<Link target="_blank" href="https://formbricks.com?utm_source=ce" rel="noopener noreferrer">
|
||||
<Logo className="mx-auto w-3/4" />
|
||||
</Link>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomBytes } from "crypto";
|
||||
import { Provider } from "next-auth/providers/index";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { EMAIL_VERIFICATION_DISABLED } from "@formbricks/lib/constants";
|
||||
import { createToken } from "@formbricks/lib/jwt";
|
||||
@@ -40,13 +40,13 @@ describe("authOptions", () => {
|
||||
describe("CredentialsProvider (credentials) - email/password login", () => {
|
||||
const credentialsProvider = getProviderById("credentials");
|
||||
|
||||
it("should throw error if credentials are not provided", async () => {
|
||||
test("should throw error if credentials are not provided", async () => {
|
||||
await expect(credentialsProvider.options.authorize(undefined, {})).rejects.toThrow(
|
||||
"Invalid credentials"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if user not found", async () => {
|
||||
test("should throw error if user not found", async () => {
|
||||
vi.spyOn(prisma.user, "findUnique").mockResolvedValue(null);
|
||||
|
||||
const credentials = { email: mockUser.email, password: mockPassword };
|
||||
@@ -56,7 +56,7 @@ describe("authOptions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if user has no password stored", async () => {
|
||||
test("should throw error if user has no password stored", async () => {
|
||||
vi.spyOn(prisma.user, "findUnique").mockResolvedValue({
|
||||
id: mockUser.id,
|
||||
email: mockUser.email,
|
||||
@@ -70,7 +70,7 @@ describe("authOptions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if password verification fails", async () => {
|
||||
test("should throw error if password verification fails", async () => {
|
||||
vi.spyOn(prisma.user, "findUnique").mockResolvedValue({
|
||||
id: mockUserId,
|
||||
email: mockUser.email,
|
||||
@@ -84,7 +84,7 @@ describe("authOptions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should successfully login when credentials are valid", async () => {
|
||||
test("should successfully login when credentials are valid", async () => {
|
||||
const fakeUser = {
|
||||
id: mockUserId,
|
||||
email: mockUser.email,
|
||||
@@ -108,7 +108,7 @@ describe("authOptions", () => {
|
||||
});
|
||||
|
||||
describe("Two-Factor Backup Code login", () => {
|
||||
it("should throw error if backup codes are missing", async () => {
|
||||
test("should throw error if backup codes are missing", async () => {
|
||||
const mockUser = {
|
||||
id: mockUserId,
|
||||
email: "2fa@example.com",
|
||||
@@ -130,13 +130,13 @@ describe("authOptions", () => {
|
||||
describe("CredentialsProvider (token) - Token-based email verification", () => {
|
||||
const tokenProvider = getProviderById("token");
|
||||
|
||||
it("should throw error if token is not provided", async () => {
|
||||
test("should throw error if token is not provided", async () => {
|
||||
await expect(tokenProvider.options.authorize({}, {})).rejects.toThrow(
|
||||
"Either a user does not match the provided token or the token is invalid"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if token is invalid or user not found", async () => {
|
||||
test("should throw error if token is invalid or user not found", async () => {
|
||||
const credentials = { token: "badtoken" };
|
||||
|
||||
await expect(tokenProvider.options.authorize(credentials, {})).rejects.toThrow(
|
||||
@@ -144,7 +144,7 @@ describe("authOptions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if email is already verified", async () => {
|
||||
test("should throw error if email is already verified", async () => {
|
||||
vi.spyOn(prisma.user, "findUnique").mockResolvedValue(mockUser);
|
||||
|
||||
const credentials = { token: createToken(mockUser.id, mockUser.email) };
|
||||
@@ -154,7 +154,7 @@ describe("authOptions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should update user and verify email when token is valid", async () => {
|
||||
test("should update user and verify email when token is valid", async () => {
|
||||
vi.spyOn(prisma.user, "findUnique").mockResolvedValue({ id: mockUser.id, emailVerified: null });
|
||||
vi.spyOn(prisma.user, "update").mockResolvedValue({
|
||||
...mockUser,
|
||||
@@ -175,7 +175,7 @@ describe("authOptions", () => {
|
||||
|
||||
describe("Callbacks", () => {
|
||||
describe("jwt callback", () => {
|
||||
it("should add profile information to token if user is found", async () => {
|
||||
test("should add profile information to token if user is found", async () => {
|
||||
vi.spyOn(prisma.user, "findFirst").mockResolvedValue({
|
||||
id: mockUser.id,
|
||||
locale: mockUser.locale,
|
||||
@@ -194,7 +194,7 @@ describe("authOptions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return token unchanged if no existing user is found", async () => {
|
||||
test("should return token unchanged if no existing user is found", async () => {
|
||||
vi.spyOn(prisma.user, "findFirst").mockResolvedValue(null);
|
||||
|
||||
const token = { email: "nonexistent@example.com" };
|
||||
@@ -207,7 +207,7 @@ describe("authOptions", () => {
|
||||
});
|
||||
|
||||
describe("session callback", () => {
|
||||
it("should add user profile to session", async () => {
|
||||
test("should add user profile to session", async () => {
|
||||
const token = {
|
||||
id: "user6",
|
||||
profile: { id: "user6", email: "user6@example.com" },
|
||||
@@ -223,7 +223,7 @@ describe("authOptions", () => {
|
||||
});
|
||||
|
||||
describe("signIn callback", () => {
|
||||
it("should throw error if email is not verified and email verification is enabled", async () => {
|
||||
test("should throw error if email is not verified and email verification is enabled", async () => {
|
||||
const user = { ...mockUser, emailVerified: null };
|
||||
const account = { provider: "credentials" } as any;
|
||||
// EMAIL_VERIFICATION_DISABLED is imported from constants.
|
||||
@@ -239,7 +239,7 @@ describe("authOptions", () => {
|
||||
describe("Two-Factor Authentication (TOTP)", () => {
|
||||
const credentialsProvider = getProviderById("credentials");
|
||||
|
||||
it("should throw error if TOTP code is missing when 2FA is enabled", async () => {
|
||||
test("should throw error if TOTP code is missing when 2FA is enabled", async () => {
|
||||
const mockUser = {
|
||||
id: mockUserId,
|
||||
email: "2fa@example.com",
|
||||
@@ -256,7 +256,7 @@ describe("authOptions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if two factor secret is missing", async () => {
|
||||
test("should throw error if two factor secret is missing", async () => {
|
||||
const mockUser = {
|
||||
id: mockUserId,
|
||||
email: "2fa@example.com",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Response } from "node-fetch";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { createBrevoCustomer } from "./brevo";
|
||||
@@ -20,7 +20,7 @@ describe("createBrevoCustomer", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return early if BREVO_API_KEY is not defined", async () => {
|
||||
test("should return early if BREVO_API_KEY is not defined", async () => {
|
||||
vi.doMock("@formbricks/lib/constants", () => ({
|
||||
BREVO_API_KEY: undefined,
|
||||
BREVO_LIST_ID: "123",
|
||||
@@ -35,7 +35,7 @@ describe("createBrevoCustomer", () => {
|
||||
expect(validateInputs).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log an error if fetch fails", async () => {
|
||||
test("should log an error if fetch fails", async () => {
|
||||
const loggerSpy = vi.spyOn(logger, "error");
|
||||
|
||||
vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Fetch failed"));
|
||||
@@ -45,7 +45,7 @@ describe("createBrevoCustomer", () => {
|
||||
expect(loggerSpy).toHaveBeenCalledWith(expect.any(Error), "Error sending user to Brevo");
|
||||
});
|
||||
|
||||
it("should log the error response if fetch status is not 200", async () => {
|
||||
test("should log the error response if fetch status is not 200", async () => {
|
||||
const loggerSpy = vi.spyOn(logger, "error");
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Authenticator } from "@otplib/core";
|
||||
import type { AuthenticatorOptions } from "@otplib/core/authenticator";
|
||||
import { createDigest, createRandomBytes } from "@otplib/plugin-crypto";
|
||||
import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { totpAuthenticatorCheck } from "./totp";
|
||||
|
||||
vi.mock("@otplib/core");
|
||||
@@ -14,7 +14,7 @@ describe("totpAuthenticatorCheck", () => {
|
||||
const secret = "JBSWY3DPEHPK3PXP";
|
||||
const opts: Partial<AuthenticatorOptions> = { window: [1, 0] };
|
||||
|
||||
it("should check a TOTP token with a base32-encoded secret", () => {
|
||||
test("should check a TOTP token with a base32-encoded secret", () => {
|
||||
const checkMock = vi.fn().mockReturnValue(true);
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: checkMock,
|
||||
@@ -33,7 +33,7 @@ describe("totpAuthenticatorCheck", () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should use default window if none is provided", () => {
|
||||
test("should use default window if none is provided", () => {
|
||||
const checkMock = vi.fn().mockReturnValue(true);
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: checkMock,
|
||||
@@ -52,7 +52,7 @@ describe("totpAuthenticatorCheck", () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should throw an error for invalid token format", () => {
|
||||
test("should throw an error for invalid token format", () => {
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: () => {
|
||||
throw new Error("Invalid token format");
|
||||
@@ -64,7 +64,7 @@ describe("totpAuthenticatorCheck", () => {
|
||||
}).toThrow("Invalid token format");
|
||||
});
|
||||
|
||||
it("should throw an error for invalid secret format", () => {
|
||||
test("should throw an error for invalid secret format", () => {
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: () => {
|
||||
throw new Error("Invalid secret format");
|
||||
@@ -76,7 +76,7 @@ describe("totpAuthenticatorCheck", () => {
|
||||
}).toThrow("Invalid secret format");
|
||||
});
|
||||
|
||||
it("should return false if token verification fails", () => {
|
||||
test("should return false if token verification fails", () => {
|
||||
const checkMock = vi.fn().mockReturnValue(false);
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: checkMock,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { userCache } from "@formbricks/lib/user/cache";
|
||||
@@ -43,7 +43,7 @@ describe("User Management", () => {
|
||||
});
|
||||
|
||||
describe("createUser", () => {
|
||||
it("creates a user successfully", async () => {
|
||||
test("creates a user successfully", async () => {
|
||||
vi.mocked(prisma.user.create).mockResolvedValueOnce(mockPrismaUser);
|
||||
|
||||
const result = await createUser({
|
||||
@@ -56,7 +56,7 @@ describe("User Management", () => {
|
||||
expect(userCache.revalidate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws InvalidInputError when email already exists", async () => {
|
||||
test("throws InvalidInputError when email already exists", async () => {
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", {
|
||||
code: PrismaErrorType.UniqueConstraintViolation,
|
||||
clientVersion: "0.0.1",
|
||||
@@ -76,7 +76,7 @@ describe("User Management", () => {
|
||||
describe("updateUser", () => {
|
||||
const mockUpdateData = { name: "Updated Name" };
|
||||
|
||||
it("updates a user successfully", async () => {
|
||||
test("updates a user successfully", async () => {
|
||||
vi.mocked(prisma.user.update).mockResolvedValueOnce({ ...mockPrismaUser, name: mockUpdateData.name });
|
||||
|
||||
const result = await updateUser(mockUser.id, mockUpdateData);
|
||||
@@ -85,7 +85,7 @@ describe("User Management", () => {
|
||||
expect(userCache.revalidate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws ResourceNotFoundError when user doesn't exist", async () => {
|
||||
test("throws ResourceNotFoundError when user doesn't exist", async () => {
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", {
|
||||
code: PrismaErrorType.RecordDoesNotExist,
|
||||
clientVersion: "0.0.1",
|
||||
@@ -99,7 +99,7 @@ describe("User Management", () => {
|
||||
describe("updateUserLastLoginAt", () => {
|
||||
const mockUpdateData = { name: "Updated Name" };
|
||||
|
||||
it("updates a user successfully", async () => {
|
||||
test("updates a user successfully", async () => {
|
||||
vi.mocked(prisma.user.update).mockResolvedValueOnce({ ...mockPrismaUser, name: mockUpdateData.name });
|
||||
|
||||
const result = await updateUserLastLoginAt(mockUser.email);
|
||||
@@ -108,7 +108,7 @@ describe("User Management", () => {
|
||||
expect(userCache.revalidate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws ResourceNotFoundError when user doesn't exist", async () => {
|
||||
test("throws ResourceNotFoundError when user doesn't exist", async () => {
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", {
|
||||
code: PrismaErrorType.RecordDoesNotExist,
|
||||
clientVersion: "0.0.1",
|
||||
@@ -122,7 +122,7 @@ describe("User Management", () => {
|
||||
describe("getUserByEmail", () => {
|
||||
const mockEmail = "test@example.com";
|
||||
|
||||
it("retrieves a user by email successfully", async () => {
|
||||
test("retrieves a user by email successfully", async () => {
|
||||
const mockUser = {
|
||||
id: "user123",
|
||||
email: mockEmail,
|
||||
@@ -136,7 +136,7 @@ describe("User Management", () => {
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it("throws DatabaseError on prisma error", async () => {
|
||||
test("throws DatabaseError on prisma error", async () => {
|
||||
vi.mocked(prisma.user.findFirst).mockRejectedValueOnce(new Error("Database error"));
|
||||
|
||||
await expect(getUserByEmail(mockEmail)).rejects.toThrow();
|
||||
@@ -146,7 +146,7 @@ describe("User Management", () => {
|
||||
describe("getUser", () => {
|
||||
const mockUserId = "cm5xj580r00000cmgdj9ohups";
|
||||
|
||||
it("retrieves a user by id successfully", async () => {
|
||||
test("retrieves a user by id successfully", async () => {
|
||||
const mockUser = {
|
||||
id: mockUserId,
|
||||
};
|
||||
@@ -157,7 +157,7 @@ describe("User Management", () => {
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it("returns null when user doesn't exist", async () => {
|
||||
test("returns null when user doesn't exist", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce(null);
|
||||
|
||||
const result = await getUser(mockUserId);
|
||||
@@ -165,7 +165,7 @@ describe("User Management", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("throws DatabaseError on prisma error", async () => {
|
||||
test("throws DatabaseError on prisma error", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockRejectedValueOnce(new Error("Database error"));
|
||||
|
||||
await expect(getUser(mockUserId)).rejects.toThrow();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { hashPassword, verifyPassword } from "./utils";
|
||||
|
||||
describe("Password Utils", () => {
|
||||
@@ -6,7 +6,7 @@ describe("Password Utils", () => {
|
||||
const hashedPassword = "$2a$12$LZsLq.9nkZlU0YDPx2aLNelnwD/nyavqbewLN.5.Q5h/UxRD8Ymcy";
|
||||
|
||||
describe("hashPassword", () => {
|
||||
it("should hash a password", async () => {
|
||||
test("should hash a password", async () => {
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
expect(typeof hashedPassword).toBe("string");
|
||||
@@ -14,7 +14,7 @@ describe("Password Utils", () => {
|
||||
expect(hashedPassword.length).toBe(60);
|
||||
});
|
||||
|
||||
it("should generate different hashes for the same password", async () => {
|
||||
test("should generate different hashes for the same password", async () => {
|
||||
const hash1 = await hashPassword(password);
|
||||
const hash2 = await hashPassword(password);
|
||||
|
||||
@@ -23,13 +23,13 @@ describe("Password Utils", () => {
|
||||
});
|
||||
|
||||
describe("verifyPassword", () => {
|
||||
it("should verify a correct password", async () => {
|
||||
test("should verify a correct password", async () => {
|
||||
const isValid = await verifyPassword(password, hashedPassword);
|
||||
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject an incorrect password", async () => {
|
||||
test("should reject an incorrect password", async () => {
|
||||
const isValid = await verifyPassword("WrongPassword123!", hashedPassword);
|
||||
|
||||
expect(isValid).toBe(false);
|
||||
|
||||
@@ -4,7 +4,7 @@ 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, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { createEmailTokenAction } from "../../../auth/actions";
|
||||
import { SignupForm } from "./signup-form";
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("SignupForm", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("toggles the signup form on button click", () => {
|
||||
test("toggles the signup form on button click", () => {
|
||||
render(<SignupForm {...defaultProps} />);
|
||||
|
||||
// Initially, the signup form is hidden.
|
||||
@@ -149,7 +149,7 @@ describe("SignupForm", () => {
|
||||
expect(screen.getByTestId("signup-password")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("submits the form successfully", async () => {
|
||||
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" });
|
||||
@@ -194,7 +194,7 @@ describe("SignupForm", () => {
|
||||
expect(pushMock).toHaveBeenCalledWith("/auth/verification-requested?token=token123");
|
||||
});
|
||||
|
||||
it("submits the form successfully when turnstile is configured", async () => {
|
||||
test("submits the form successfully when turnstile is configured", async () => {
|
||||
// Override props to enable Turnstile
|
||||
const props = {
|
||||
...defaultProps,
|
||||
@@ -246,7 +246,7 @@ describe("SignupForm", () => {
|
||||
expect(pushMock).toHaveBeenCalledWith("/auth/signup-without-verification-success");
|
||||
});
|
||||
|
||||
it("submits the form successfully when turnstile is configured, but createEmailTokenAction don't return data", async () => {
|
||||
test("submits the form successfully when turnstile is configured, but createEmailTokenAction don't return data", async () => {
|
||||
// Override props to enable Turnstile
|
||||
const props = {
|
||||
...defaultProps,
|
||||
@@ -298,7 +298,7 @@ describe("SignupForm", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows an error message if turnstile is configured, but no token is received", async () => {
|
||||
test("shows an error message if turnstile is configured, but no token is received", async () => {
|
||||
// Override props to enable Turnstile
|
||||
const props = {
|
||||
...defaultProps,
|
||||
@@ -332,7 +332,7 @@ describe("SignupForm", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Invite token is in the search params", async () => {
|
||||
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" });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inviteCache } from "@/lib/cache/invite";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { logger } from "@formbricks/logger";
|
||||
@@ -63,7 +63,7 @@ describe("Invite Management", () => {
|
||||
});
|
||||
|
||||
describe("deleteInvite", () => {
|
||||
it("deletes an invite successfully and invalidates cache", async () => {
|
||||
test("deletes an invite successfully and invalidates cache", async () => {
|
||||
vi.mocked(prisma.invite.delete).mockResolvedValue(mockInvite);
|
||||
|
||||
const result = await deleteInvite(mockInviteId);
|
||||
@@ -79,7 +79,7 @@ describe("Invite Management", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("throws DatabaseError when invite doesn't exist", async () => {
|
||||
test("throws DatabaseError when invite doesn't exist", async () => {
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Record not found", {
|
||||
code: PrismaErrorType.RecordDoesNotExist,
|
||||
clientVersion: "0.0.1",
|
||||
@@ -89,7 +89,7 @@ describe("Invite Management", () => {
|
||||
await expect(deleteInvite(mockInviteId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
it("throws DatabaseError for other Prisma errors", async () => {
|
||||
test("throws DatabaseError for other Prisma errors", async () => {
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Database error", {
|
||||
code: "P2002",
|
||||
clientVersion: "0.0.1",
|
||||
@@ -99,7 +99,7 @@ describe("Invite Management", () => {
|
||||
await expect(deleteInvite(mockInviteId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
it("throws DatabaseError for generic errors", async () => {
|
||||
test("throws DatabaseError for generic errors", async () => {
|
||||
vi.mocked(prisma.invite.delete).mockRejectedValue(new Error("Generic error"));
|
||||
|
||||
await expect(deleteInvite(mockInviteId)).rejects.toThrow(DatabaseError);
|
||||
@@ -107,7 +107,7 @@ describe("Invite Management", () => {
|
||||
});
|
||||
|
||||
describe("getInvite", () => {
|
||||
it("retrieves an invite with creator details successfully", async () => {
|
||||
test("retrieves an invite with creator details successfully", async () => {
|
||||
vi.mocked(prisma.invite.findUnique).mockResolvedValue(mockInvite);
|
||||
|
||||
const result = await getInvite(mockInviteId);
|
||||
@@ -131,7 +131,7 @@ describe("Invite Management", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when invite doesn't exist", async () => {
|
||||
test("returns null when invite doesn't exist", async () => {
|
||||
vi.mocked(prisma.invite.findUnique).mockResolvedValue(null);
|
||||
|
||||
const result = await getInvite(mockInviteId);
|
||||
@@ -139,7 +139,7 @@ describe("Invite Management", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("throws DatabaseError on prisma error", async () => {
|
||||
test("throws DatabaseError on prisma error", async () => {
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Database error", {
|
||||
code: "P2002",
|
||||
clientVersion: "0.0.1",
|
||||
@@ -149,7 +149,7 @@ describe("Invite Management", () => {
|
||||
await expect(getInvite(mockInviteId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
it("throws DatabaseError for generic errors", async () => {
|
||||
test("throws DatabaseError for generic errors", async () => {
|
||||
vi.mocked(prisma.invite.findUnique).mockRejectedValue(new Error("Generic error"));
|
||||
|
||||
await expect(getInvite(mockInviteId)).rejects.toThrow(DatabaseError);
|
||||
@@ -157,7 +157,7 @@ describe("Invite Management", () => {
|
||||
});
|
||||
|
||||
describe("getIsValidInviteToken", () => {
|
||||
it("returns true for valid invite", async () => {
|
||||
test("returns true for valid invite", async () => {
|
||||
vi.mocked(prisma.invite.findUnique).mockResolvedValue(mockInvite);
|
||||
|
||||
const result = await getIsValidInviteToken(mockInviteId);
|
||||
@@ -168,7 +168,7 @@ describe("Invite Management", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false when invite doesn't exist", async () => {
|
||||
test("returns false when invite doesn't exist", async () => {
|
||||
vi.mocked(prisma.invite.findUnique).mockResolvedValue(null);
|
||||
|
||||
const result = await getIsValidInviteToken(mockInviteId);
|
||||
@@ -176,7 +176,7 @@ describe("Invite Management", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for expired invite", async () => {
|
||||
test("returns false for expired invite", async () => {
|
||||
const expiredInvite = {
|
||||
...mockInvite,
|
||||
expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago
|
||||
@@ -195,7 +195,7 @@ describe("Invite Management", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false and logs error when database error occurs", async () => {
|
||||
test("returns false and logs error when database error occurs", async () => {
|
||||
const error = new Error("Database error");
|
||||
vi.mocked(prisma.invite.findUnique).mockRejectedValue(error);
|
||||
|
||||
@@ -205,7 +205,7 @@ describe("Invite Management", () => {
|
||||
expect(logger.error).toHaveBeenCalledWith(error, "Error getting invite");
|
||||
});
|
||||
|
||||
it("returns false for invite with null expiresAt", async () => {
|
||||
test("returns false for invite with null expiresAt", async () => {
|
||||
const invalidInvite = {
|
||||
...mockInvite,
|
||||
expiresAt: null,
|
||||
@@ -224,7 +224,7 @@ describe("Invite Management", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false for invite with invalid expiresAt", async () => {
|
||||
test("returns false for invite with invalid expiresAt", async () => {
|
||||
const invalidInvite = {
|
||||
...mockInvite,
|
||||
expiresAt: new Date("invalid-date"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import posthog from "posthog-js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { captureFailedSignup, verifyTurnstileToken } from "./utils";
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -16,7 +16,7 @@ describe("verifyTurnstileToken", () => {
|
||||
const secretKey = "test-secret";
|
||||
const token = "test-token";
|
||||
|
||||
it("should return true when verification is successful", async () => {
|
||||
test("should return true when verification is successful", async () => {
|
||||
const mockResponse = { success: true };
|
||||
(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -36,7 +36,7 @@ describe("verifyTurnstileToken", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false when response is not ok", async () => {
|
||||
test("should return false when response is not ok", async () => {
|
||||
(global.fetch as any).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
@@ -46,14 +46,14 @@ describe("verifyTurnstileToken", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when verification fails", async () => {
|
||||
test("should return false when verification fails", async () => {
|
||||
(global.fetch as any).mockRejectedValue(new Error("Network error"));
|
||||
|
||||
const result = await verifyTurnstileToken(secretKey, token);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when request times out", async () => {
|
||||
test("should return false when request times out", async () => {
|
||||
const mockAbortError = new Error("The operation was aborted");
|
||||
mockAbortError.name = "AbortError";
|
||||
(global.fetch as any).mockRejectedValue(mockAbortError);
|
||||
@@ -64,7 +64,7 @@ describe("verifyTurnstileToken", () => {
|
||||
});
|
||||
|
||||
describe("captureFailedSignup", () => {
|
||||
it("should capture TELEMETRY_FAILED_SIGNUP event with email and name", () => {
|
||||
test("should capture TELEMETRY_FAILED_SIGNUP event with email and name", () => {
|
||||
const captureSpy = vi.spyOn(posthog, "capture");
|
||||
const email = "test@example.com";
|
||||
const name = "Test User";
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { SignupPage } from "./page";
|
||||
@@ -111,7 +111,7 @@ describe("SignupPage", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders the signup page with all components when signup is enabled", async () => {
|
||||
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);
|
||||
@@ -132,7 +132,7 @@ describe("SignupPage", () => {
|
||||
expect(screen.getByTestId("signup-form")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls notFound when signup is disabled and no valid invite token is provided", async () => {
|
||||
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(() => {
|
||||
@@ -144,7 +144,7 @@ describe("SignupPage", () => {
|
||||
expect(notFound).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls notFound when invite token is invalid", async () => {
|
||||
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(() => {
|
||||
@@ -156,7 +156,7 @@ describe("SignupPage", () => {
|
||||
expect(notFound).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls notFound when invite token is valid but invite is not found", async () => {
|
||||
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({
|
||||
@@ -170,7 +170,7 @@ describe("SignupPage", () => {
|
||||
expect(notFound).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders the page with email from search params", async () => {
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { ENCRYPTION_KEY, SURVEY_URL } from "@formbricks/lib/constants";
|
||||
import * as crypto from "@formbricks/lib/crypto";
|
||||
import * as contactSurveyLink from "./contact-survey-link";
|
||||
@@ -53,7 +53,7 @@ describe("Contact Survey Link", () => {
|
||||
});
|
||||
|
||||
describe("getContactSurveyLink", () => {
|
||||
it("creates a survey link with encrypted contact and survey IDs", () => {
|
||||
test("creates a survey link with encrypted contact and survey IDs", () => {
|
||||
const result = contactSurveyLink.getContactSurveyLink(mockContactId, mockSurveyId);
|
||||
|
||||
// Verify encryption was called for both IDs
|
||||
@@ -77,7 +77,7 @@ describe("Contact Survey Link", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("adds expiration to the token when expirationDays is provided", () => {
|
||||
test("adds expiration to the token when expirationDays is provided", () => {
|
||||
const expirationDays = 7;
|
||||
contactSurveyLink.getContactSurveyLink(mockContactId, mockSurveyId, expirationDays);
|
||||
|
||||
@@ -92,7 +92,7 @@ describe("Contact Survey Link", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("throws an error when ENCRYPTION_KEY is not available", async () => {
|
||||
test("throws an error when ENCRYPTION_KEY is not available", async () => {
|
||||
// Reset modules so the new mock is used by the module under test
|
||||
vi.resetModules();
|
||||
// Re‑mock constants to simulate missing ENCRYPTION_KEY
|
||||
@@ -115,7 +115,7 @@ describe("Contact Survey Link", () => {
|
||||
});
|
||||
|
||||
describe("verifyContactSurveyToken", () => {
|
||||
it("verifies and decrypts a valid token", () => {
|
||||
test("verifies and decrypts a valid token", () => {
|
||||
const result = contactSurveyLink.verifyContactSurveyToken(mockToken);
|
||||
|
||||
// Verify JWT verify was called
|
||||
@@ -131,7 +131,7 @@ describe("Contact Survey Link", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("throws an error when token verification fails", () => {
|
||||
test("throws an error when token verification fails", () => {
|
||||
vi.mocked(jwt.verify).mockImplementation(() => {
|
||||
throw new Error("Token verification failed");
|
||||
});
|
||||
@@ -147,7 +147,7 @@ describe("Contact Survey Link", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("throws an error when token has invalid format", () => {
|
||||
test("throws an error when token has invalid format", () => {
|
||||
// Mock JWT.verify to return an incomplete payload
|
||||
vi.mocked(jwt.verify).mockReturnValue({
|
||||
// Missing surveyId
|
||||
@@ -168,7 +168,7 @@ describe("Contact Survey Link", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("throws an error when ENCRYPTION_KEY is not available", async () => {
|
||||
test("throws an error when ENCRYPTION_KEY is not available", async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("@formbricks/lib/constants", () => ({
|
||||
ENCRYPTION_KEY: undefined,
|
||||
|
||||
@@ -51,6 +51,12 @@ export const SegmentActivityTab = ({ currentSegment }: SegmentActivityTabProps)
|
||||
{convertDateTimeStringShort(currentSegment.updatedAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">
|
||||
{t("environments.segments.segment_id")}
|
||||
</Label>
|
||||
<p className="text-xs text-slate-700">{currentSegment.id.toString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { AddMemberRole } from "./add-member-role";
|
||||
|
||||
// Mock dependencies
|
||||
@@ -39,7 +39,7 @@ describe("AddMemberRole Component", () => {
|
||||
};
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("renders role selector when user is owner", () => {
|
||||
test("renders role selector when user is owner", () => {
|
||||
render(
|
||||
<FormWrapper
|
||||
defaultValues={defaultValues}
|
||||
@@ -54,7 +54,7 @@ describe("AddMemberRole Component", () => {
|
||||
expect(roleLabel).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render anything when user is member", () => {
|
||||
test("does not render anything when user is member", () => {
|
||||
render(
|
||||
<FormWrapper
|
||||
defaultValues={defaultValues}
|
||||
@@ -69,7 +69,7 @@ describe("AddMemberRole Component", () => {
|
||||
expect(screen.getByTestId("child")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables the role selector when canDoRoleManagement is false", () => {
|
||||
test("disables the role selector when canDoRoleManagement is false", () => {
|
||||
render(
|
||||
<FormWrapper
|
||||
defaultValues={defaultValues}
|
||||
@@ -86,7 +86,7 @@ describe("AddMemberRole Component", () => {
|
||||
});
|
||||
|
||||
describe("Default values", () => {
|
||||
it("displays the default role value", () => {
|
||||
test("displays the default role value", () => {
|
||||
render(
|
||||
<FormWrapper
|
||||
defaultValues={defaultValues}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { type Mock, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { type Mock, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { EditMembershipRole } from "./edit-membership-role";
|
||||
|
||||
// Mock dependencies
|
||||
@@ -52,7 +52,7 @@ describe("EditMembershipRole Component", () => {
|
||||
};
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("renders a dropdown when user is owner", () => {
|
||||
test("renders a dropdown when user is owner", () => {
|
||||
render(<EditMembershipRole {...defaultProps} />);
|
||||
|
||||
const button = screen.queryByRole("button-role");
|
||||
@@ -60,7 +60,7 @@ describe("EditMembershipRole Component", () => {
|
||||
expect(button).toHaveTextContent("Member");
|
||||
});
|
||||
|
||||
it("renders a badge when user is not owner or manager", () => {
|
||||
test("renders a badge when user is not owner or manager", () => {
|
||||
render(<EditMembershipRole {...defaultProps} currentUserRole="member" />);
|
||||
|
||||
const badge = screen.queryByRole("badge-role");
|
||||
@@ -69,21 +69,21 @@ describe("EditMembershipRole Component", () => {
|
||||
expect(button).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables the dropdown when editing own role", () => {
|
||||
test("disables the dropdown when editing own role", () => {
|
||||
render(<EditMembershipRole {...defaultProps} memberId="user-456" userId="user-456" />);
|
||||
|
||||
const button = screen.getByRole("button-role");
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it("disables the dropdown when the user is the only owner", () => {
|
||||
test("disables the dropdown when the user is the only owner", () => {
|
||||
render(<EditMembershipRole {...defaultProps} memberRole="owner" doesOrgHaveMoreThanOneOwner={false} />);
|
||||
|
||||
const button = screen.getByRole("button-role");
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it("disables the dropdown when a manager tries to edit an owner", () => {
|
||||
test("disables the dropdown when a manager tries to edit an owner", () => {
|
||||
render(<EditMembershipRole {...defaultProps} currentUserRole="manager" memberRole="owner" />);
|
||||
|
||||
const button = screen.getByRole("button-role");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
|
||||
import { AzureButton } from "./azure-button";
|
||||
|
||||
@@ -28,18 +28,18 @@ describe("AzureButton", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders correctly with default props", () => {
|
||||
test("renders correctly with default props", () => {
|
||||
render(<AzureButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_azure" });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with last used indicator when lastUsed is true", () => {
|
||||
test("renders with last used indicator when lastUsed is true", () => {
|
||||
render(<AzureButton {...defaultProps} lastUsed={true} />);
|
||||
expect(screen.getByText("auth.last_used")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sets localStorage item and calls signIn on click", async () => {
|
||||
test("sets localStorage item and calls signIn on click", async () => {
|
||||
render(<AzureButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_azure" });
|
||||
fireEvent.click(button);
|
||||
@@ -51,7 +51,7 @@ describe("AzureButton", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses inviteUrl in callbackUrl when provided", async () => {
|
||||
test("uses inviteUrl in callbackUrl when provided", async () => {
|
||||
const inviteUrl = "https://example.com/invite";
|
||||
render(<AzureButton {...defaultProps} inviteUrl={inviteUrl} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_azure" });
|
||||
@@ -63,7 +63,7 @@ describe("AzureButton", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("handles signup source correctly", async () => {
|
||||
test("handles signup source correctly", async () => {
|
||||
render(<AzureButton {...defaultProps} source="signup" />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_azure" });
|
||||
fireEvent.click(button);
|
||||
@@ -74,7 +74,7 @@ describe("AzureButton", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("triggers direct redirect when directRedirect is true", () => {
|
||||
test("triggers direct redirect when directRedirect is true", () => {
|
||||
render(<AzureButton {...defaultProps} directRedirect={true} />);
|
||||
expect(signIn).toHaveBeenCalledWith("azure-ad", {
|
||||
redirect: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
|
||||
import { GithubButton } from "./github-button";
|
||||
|
||||
@@ -28,18 +28,18 @@ describe("GithubButton", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders correctly with default props", () => {
|
||||
test("renders correctly with default props", () => {
|
||||
render(<GithubButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_github" });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with last used indicator when lastUsed is true", () => {
|
||||
test("renders with last used indicator when lastUsed is true", () => {
|
||||
render(<GithubButton {...defaultProps} lastUsed={true} />);
|
||||
expect(screen.getByText("auth.last_used")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sets localStorage item and calls signIn on click", async () => {
|
||||
test("sets localStorage item and calls signIn on click", async () => {
|
||||
render(<GithubButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_github" });
|
||||
fireEvent.click(button);
|
||||
@@ -51,7 +51,7 @@ describe("GithubButton", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses inviteUrl in callbackUrl when provided", async () => {
|
||||
test("uses inviteUrl in callbackUrl when provided", async () => {
|
||||
const inviteUrl = "https://example.com/invite";
|
||||
render(<GithubButton {...defaultProps} inviteUrl={inviteUrl} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_github" });
|
||||
@@ -63,7 +63,7 @@ describe("GithubButton", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("handles signup source correctly", async () => {
|
||||
test("handles signup source correctly", async () => {
|
||||
render(<GithubButton {...defaultProps} source="signup" />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_github" });
|
||||
fireEvent.click(button);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
|
||||
import { GoogleButton } from "./google-button";
|
||||
|
||||
@@ -28,18 +28,18 @@ describe("GoogleButton", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders correctly with default props", () => {
|
||||
test("renders correctly with default props", () => {
|
||||
render(<GoogleButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_google" });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with last used indicator when lastUsed is true", () => {
|
||||
test("renders with last used indicator when lastUsed is true", () => {
|
||||
render(<GoogleButton {...defaultProps} lastUsed={true} />);
|
||||
expect(screen.getByText("auth.last_used")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sets localStorage item and calls signIn on click", async () => {
|
||||
test("sets localStorage item and calls signIn on click", async () => {
|
||||
render(<GoogleButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_google" });
|
||||
fireEvent.click(button);
|
||||
@@ -51,7 +51,7 @@ describe("GoogleButton", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses inviteUrl in callbackUrl when provided", async () => {
|
||||
test("uses inviteUrl in callbackUrl when provided", async () => {
|
||||
const inviteUrl = "https://example.com/invite";
|
||||
render(<GoogleButton {...defaultProps} inviteUrl={inviteUrl} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_google" });
|
||||
@@ -63,7 +63,7 @@ describe("GoogleButton", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("handles signup source correctly", async () => {
|
||||
test("handles signup source correctly", async () => {
|
||||
render(<GoogleButton {...defaultProps} source="signup" />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_google" });
|
||||
fireEvent.click(button);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
|
||||
import { OpenIdButton } from "./open-id-button";
|
||||
|
||||
@@ -28,25 +28,25 @@ describe("OpenIdButton", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders correctly with default props", () => {
|
||||
test("renders correctly with default props", () => {
|
||||
render(<OpenIdButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_openid" });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with custom text when provided", () => {
|
||||
test("renders with custom text when provided", () => {
|
||||
const customText = "Custom OpenID Text";
|
||||
render(<OpenIdButton {...defaultProps} text={customText} />);
|
||||
const button = screen.getByRole("button", { name: customText });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with last used indicator when lastUsed is true", () => {
|
||||
test("renders with last used indicator when lastUsed is true", () => {
|
||||
render(<OpenIdButton {...defaultProps} lastUsed={true} />);
|
||||
expect(screen.getByText("auth.last_used")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sets localStorage item and calls signIn on click", async () => {
|
||||
test("sets localStorage item and calls signIn on click", async () => {
|
||||
render(<OpenIdButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_openid" });
|
||||
fireEvent.click(button);
|
||||
@@ -58,7 +58,7 @@ describe("OpenIdButton", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses inviteUrl in callbackUrl when provided", async () => {
|
||||
test("uses inviteUrl in callbackUrl when provided", async () => {
|
||||
const inviteUrl = "https://example.com/invite";
|
||||
render(<OpenIdButton {...defaultProps} inviteUrl={inviteUrl} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_openid" });
|
||||
@@ -70,7 +70,7 @@ describe("OpenIdButton", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("handles signup source correctly", async () => {
|
||||
test("handles signup source correctly", async () => {
|
||||
render(<OpenIdButton {...defaultProps} source="signup" />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_openid" });
|
||||
fireEvent.click(button);
|
||||
@@ -81,7 +81,7 @@ describe("OpenIdButton", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("triggers direct redirect when directRedirect is true", () => {
|
||||
test("triggers direct redirect when directRedirect is true", () => {
|
||||
render(<OpenIdButton {...defaultProps} directRedirect={true} />);
|
||||
expect(signIn).toHaveBeenCalledWith("openid", {
|
||||
redirect: true,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { doesSamlConnectionExistAction } from "@/modules/ee/sso/actions";
|
||||
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, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
|
||||
import { SamlButton } from "./saml-button";
|
||||
|
||||
@@ -44,18 +44,18 @@ describe("SamlButton", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders correctly with default props", () => {
|
||||
test("renders correctly with default props", () => {
|
||||
render(<SamlButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_saml" });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with last used indicator when lastUsed is true", () => {
|
||||
test("renders with last used indicator when lastUsed is true", () => {
|
||||
render(<SamlButton {...defaultProps} lastUsed={true} />);
|
||||
expect(screen.getByText("auth.last_used")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sets localStorage item and calls signIn on click when SAML connection exists", async () => {
|
||||
test("sets localStorage item and calls signIn on click when SAML connection exists", async () => {
|
||||
vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: true });
|
||||
render(<SamlButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_saml" });
|
||||
@@ -76,7 +76,7 @@ describe("SamlButton", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("shows error toast when SAML connection does not exist", async () => {
|
||||
test("shows error toast when SAML connection does not exist", async () => {
|
||||
vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: false });
|
||||
render(<SamlButton {...defaultProps} />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_saml" });
|
||||
@@ -87,7 +87,7 @@ describe("SamlButton", () => {
|
||||
expect(signIn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses inviteUrl in callbackUrl when provided", async () => {
|
||||
test("uses inviteUrl in callbackUrl when provided", async () => {
|
||||
vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: true });
|
||||
const inviteUrl = "https://example.com/invite";
|
||||
render(<SamlButton {...defaultProps} inviteUrl={inviteUrl} />);
|
||||
@@ -108,7 +108,7 @@ describe("SamlButton", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("handles signup source correctly", async () => {
|
||||
test("handles signup source correctly", async () => {
|
||||
vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: true });
|
||||
render(<SamlButton {...defaultProps} source="signup" />);
|
||||
const button = screen.getByRole("button", { name: "auth.continue_with_saml" });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { SSOOptions } from "./sso-options";
|
||||
|
||||
// Mock environment variables
|
||||
@@ -81,7 +81,7 @@ describe("SSOOptions Component", () => {
|
||||
source: "signin" as const,
|
||||
};
|
||||
|
||||
it("renders all SSO options when all are enabled", () => {
|
||||
test("renders all SSO options when all are enabled", () => {
|
||||
render(<SSOOptions {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("google-button")).toBeInTheDocument();
|
||||
@@ -91,7 +91,7 @@ describe("SSOOptions Component", () => {
|
||||
expect(screen.getByTestId("saml-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("only renders enabled SSO options", () => {
|
||||
test("only renders enabled SSO options", () => {
|
||||
render(
|
||||
<SSOOptions
|
||||
{...defaultProps}
|
||||
@@ -108,7 +108,7 @@ describe("SSOOptions Component", () => {
|
||||
expect(screen.getByTestId("saml-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("passes correct props to OpenID button", () => {
|
||||
test("passes correct props to OpenID button", () => {
|
||||
render(<SSOOptions {...defaultProps} />);
|
||||
const openIdButton = screen.getByTestId("openid-button");
|
||||
|
||||
@@ -116,7 +116,7 @@ describe("SSOOptions Component", () => {
|
||||
expect(openIdButton).toHaveTextContent("auth.continue_with_oidc");
|
||||
});
|
||||
|
||||
it("passes correct props to SAML button", () => {
|
||||
test("passes correct props to SAML button", () => {
|
||||
render(<SSOOptions {...defaultProps} />);
|
||||
const samlButton = screen.getByTestId("saml-button");
|
||||
|
||||
@@ -125,7 +125,7 @@ describe("SSOOptions Component", () => {
|
||||
expect(samlButton).toHaveAttribute("data-product", "test-product");
|
||||
});
|
||||
|
||||
it("passes correct source prop to all buttons", () => {
|
||||
test("passes correct source prop to all buttons", () => {
|
||||
render(<SSOOptions {...defaultProps} source="signup" />);
|
||||
|
||||
expect(screen.getByTestId("google-button")).toHaveAttribute("data-source", "signup");
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createBrevoCustomer } from "@/modules/auth/lib/brevo";
|
||||
import { createUser, getUserByEmail, updateUser } from "@/modules/auth/lib/user";
|
||||
import type { TSamlNameFields } from "@/modules/auth/types/auth";
|
||||
import { getIsSamlSsoEnabled, getisSsoEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { createAccount } from "@formbricks/lib/account/service";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
@@ -91,7 +91,7 @@ describe("handleSsoCallback", () => {
|
||||
});
|
||||
|
||||
describe("Early return conditions", () => {
|
||||
it("should return false if SSO is not enabled", async () => {
|
||||
test("should return false if SSO is not enabled", async () => {
|
||||
vi.mocked(getisSsoEnabled).mockResolvedValue(false);
|
||||
|
||||
const result = await handleSsoCallback({
|
||||
@@ -103,7 +103,7 @@ describe("handleSsoCallback", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if user email is missing", async () => {
|
||||
test("should return false if user email is missing", async () => {
|
||||
const result = await handleSsoCallback({
|
||||
user: { ...mockUser, email: "" },
|
||||
account: mockAccount,
|
||||
@@ -113,7 +113,7 @@ describe("handleSsoCallback", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if account type is not oauth", async () => {
|
||||
test("should return false if account type is not oauth", async () => {
|
||||
const result = await handleSsoCallback({
|
||||
user: mockUser,
|
||||
account: { ...mockAccount, type: "credentials" },
|
||||
@@ -123,7 +123,7 @@ describe("handleSsoCallback", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if provider is SAML and SAML SSO is not enabled", async () => {
|
||||
test("should return false if provider is SAML and SAML SSO is not enabled", async () => {
|
||||
vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(false);
|
||||
|
||||
const result = await handleSsoCallback({
|
||||
@@ -137,7 +137,7 @@ describe("handleSsoCallback", () => {
|
||||
});
|
||||
|
||||
describe("Existing user handling", () => {
|
||||
it("should return true if user with account already exists and email is the same", async () => {
|
||||
test("should return true if user with account already exists and email is the same", async () => {
|
||||
vi.mocked(prisma.user.findFirst).mockResolvedValue({
|
||||
...mockUser,
|
||||
email: mockUser.email,
|
||||
@@ -166,7 +166,7 @@ describe("handleSsoCallback", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should update user email if user with account exists but email changed", async () => {
|
||||
test("should update user email if user with account exists but email changed", async () => {
|
||||
const existingUser = {
|
||||
...mockUser,
|
||||
id: "existing-user-id",
|
||||
@@ -188,7 +188,7 @@ describe("handleSsoCallback", () => {
|
||||
expect(updateUser).toHaveBeenCalledWith(existingUser.id, { email: mockUser.email });
|
||||
});
|
||||
|
||||
it("should throw error if user with account exists, email changed, and another user has the new email", async () => {
|
||||
test("should throw error if user with account exists, email changed, and another user has the new email", async () => {
|
||||
const existingUser = {
|
||||
...mockUser,
|
||||
id: "existing-user-id",
|
||||
@@ -216,7 +216,7 @@ describe("handleSsoCallback", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should return true if user with email already exists", async () => {
|
||||
test("should return true if user with email already exists", async () => {
|
||||
vi.mocked(prisma.user.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(getUserByEmail).mockResolvedValue({
|
||||
id: "existing-user-id",
|
||||
@@ -237,7 +237,7 @@ describe("handleSsoCallback", () => {
|
||||
});
|
||||
|
||||
describe("New user creation", () => {
|
||||
it("should create a new user if no existing user found", async () => {
|
||||
test("should create a new user if no existing user found", async () => {
|
||||
vi.mocked(prisma.user.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(getUserByEmail).mockResolvedValue(null);
|
||||
vi.mocked(createUser).mockResolvedValue(mockCreatedUser());
|
||||
@@ -260,7 +260,7 @@ describe("handleSsoCallback", () => {
|
||||
expect(createBrevoCustomer).toHaveBeenCalledWith({ id: mockUser.id, email: mockUser.email });
|
||||
});
|
||||
|
||||
it("should create organization and membership for new user when DEFAULT_ORGANIZATION_ID is set", async () => {
|
||||
test("should create organization and membership for new user when DEFAULT_ORGANIZATION_ID is set", async () => {
|
||||
vi.mocked(prisma.user.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(getUserByEmail).mockResolvedValue(null);
|
||||
vi.mocked(createUser).mockResolvedValue(mockCreatedUser());
|
||||
@@ -292,7 +292,7 @@ describe("handleSsoCallback", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should use existing organization if it exists", async () => {
|
||||
test("should use existing organization if it exists", async () => {
|
||||
vi.mocked(prisma.user.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(getUserByEmail).mockResolvedValue(null);
|
||||
vi.mocked(createUser).mockResolvedValue(mockCreatedUser());
|
||||
@@ -313,7 +313,7 @@ describe("handleSsoCallback", () => {
|
||||
});
|
||||
|
||||
describe("OpenID Connect name handling", () => {
|
||||
it("should use oidcUser.name when available", async () => {
|
||||
test("should use oidcUser.name when available", async () => {
|
||||
const openIdUser = mockOpenIdUser({
|
||||
name: "Direct Name",
|
||||
given_name: "John",
|
||||
@@ -341,7 +341,7 @@ describe("handleSsoCallback", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should use given_name + family_name when name is not available", async () => {
|
||||
test("should use given_name + family_name when name is not available", async () => {
|
||||
const openIdUser = mockOpenIdUser({
|
||||
name: undefined,
|
||||
given_name: "John",
|
||||
@@ -369,7 +369,7 @@ describe("handleSsoCallback", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should use preferred_username when name and given_name/family_name are not available", async () => {
|
||||
test("should use preferred_username when name and given_name/family_name are not available", async () => {
|
||||
const openIdUser = mockOpenIdUser({
|
||||
name: undefined,
|
||||
given_name: undefined,
|
||||
@@ -398,7 +398,7 @@ describe("handleSsoCallback", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should fallback to email username when no OIDC name fields are available", async () => {
|
||||
test("should fallback to email username when no OIDC name fields are available", async () => {
|
||||
const openIdUser = mockOpenIdUser({
|
||||
name: undefined,
|
||||
given_name: undefined,
|
||||
@@ -430,7 +430,7 @@ describe("handleSsoCallback", () => {
|
||||
});
|
||||
|
||||
describe("SAML name handling", () => {
|
||||
it("should use samlUser.name when available", async () => {
|
||||
test("should use samlUser.name when available", async () => {
|
||||
const samlUser = {
|
||||
...mockUser,
|
||||
name: "Direct Name",
|
||||
@@ -459,7 +459,7 @@ describe("handleSsoCallback", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should use firstName + lastName when name is not available", async () => {
|
||||
test("should use firstName + lastName when name is not available", async () => {
|
||||
const samlUser = {
|
||||
...mockUser,
|
||||
name: "",
|
||||
@@ -490,7 +490,7 @@ describe("handleSsoCallback", () => {
|
||||
});
|
||||
|
||||
describe("Organization handling", () => {
|
||||
it("should handle invalid DEFAULT_ORGANIZATION_ID gracefully", async () => {
|
||||
test("should handle invalid DEFAULT_ORGANIZATION_ID gracefully", async () => {
|
||||
vi.mocked(prisma.user.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(getUserByEmail).mockResolvedValue(null);
|
||||
vi.mocked(createUser).mockResolvedValue(mockCreatedUser());
|
||||
@@ -509,7 +509,7 @@ describe("handleSsoCallback", () => {
|
||||
expect(createMembership).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle membership creation failure gracefully", async () => {
|
||||
test("should handle membership creation failure gracefully", async () => {
|
||||
vi.mocked(prisma.user.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(getUserByEmail).mockResolvedValue(null);
|
||||
vi.mocked(createUser).mockResolvedValue(mockCreatedUser());
|
||||
@@ -528,7 +528,7 @@ describe("handleSsoCallback", () => {
|
||||
});
|
||||
|
||||
describe("Error handling", () => {
|
||||
it("should handle prisma errors gracefully", async () => {
|
||||
test("should handle prisma errors gracefully", async () => {
|
||||
vi.mocked(prisma.user.findFirst).mockRejectedValue(new Error("Database error"));
|
||||
|
||||
await expect(
|
||||
@@ -540,7 +540,7 @@ describe("handleSsoCallback", () => {
|
||||
).rejects.toThrow("Database error");
|
||||
});
|
||||
|
||||
it("should handle locale finding errors gracefully", async () => {
|
||||
test("should handle locale finding errors gracefully", async () => {
|
||||
vi.mocked(findMatchingLocale).mockRejectedValue(new Error("Locale error"));
|
||||
vi.mocked(prisma.user.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(getUserByEmail).mockResolvedValue(null);
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { getCallbackUrl } from "../utils";
|
||||
|
||||
describe("getCallbackUrl", () => {
|
||||
it("should return base URL with source when no inviteUrl is provided", () => {
|
||||
test("should return base URL with source when no inviteUrl is provided", () => {
|
||||
const result = getCallbackUrl(undefined, "test-source");
|
||||
expect(result).toBe("/?source=test-source");
|
||||
});
|
||||
|
||||
it("should append source parameter to inviteUrl with existing query parameters", () => {
|
||||
test("should append source parameter to inviteUrl with existing query parameters", () => {
|
||||
const result = getCallbackUrl("https://example.com/invite?param=value", "test-source");
|
||||
expect(result).toBe("https://example.com/invite?param=value&source=test-source");
|
||||
});
|
||||
|
||||
it("should append source parameter to inviteUrl without existing query parameters", () => {
|
||||
test("should append source parameter to inviteUrl without existing query parameters", () => {
|
||||
const result = getCallbackUrl("https://example.com/invite", "test-source");
|
||||
expect(result).toBe("https://example.com/invite?source=test-source");
|
||||
});
|
||||
|
||||
it("should handle empty source parameter", () => {
|
||||
test("should handle empty source parameter", () => {
|
||||
const result = getCallbackUrl("https://example.com/invite", "");
|
||||
expect(result).toBe("https://example.com/invite?source=");
|
||||
});
|
||||
|
||||
it("should handle undefined source parameter", () => {
|
||||
test("should handle undefined source parameter", () => {
|
||||
const result = getCallbackUrl("https://example.com/invite", undefined);
|
||||
expect(result).toBe("https://example.com/invite?source=undefined");
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { EmailCustomizationSettings } from "./email-customization-settings";
|
||||
@@ -48,7 +48,7 @@ describe("EmailCustomizationSettings", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders the logo if one is set and shows Replace/Remove buttons", () => {
|
||||
test("renders the logo if one is set and shows Replace/Remove buttons", () => {
|
||||
render(<EmailCustomizationSettings {...defaultProps} />);
|
||||
|
||||
const logoImage = screen.getByTestId("email-customization-preview-image");
|
||||
@@ -64,7 +64,7 @@ describe("EmailCustomizationSettings", () => {
|
||||
expect(screen.getByTestId("remove-logo-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls removeOrganizationEmailLogoUrlAction when removing logo", async () => {
|
||||
test("calls removeOrganizationEmailLogoUrlAction when removing logo", async () => {
|
||||
vi.mocked(removeOrganizationEmailLogoUrlAction).mockResolvedValue({
|
||||
data: true,
|
||||
});
|
||||
@@ -81,7 +81,7 @@ describe("EmailCustomizationSettings", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("calls updateOrganizationEmailLogoUrlAction after uploading and clicking save", async () => {
|
||||
test("calls updateOrganizationEmailLogoUrlAction after uploading and clicking save", async () => {
|
||||
vi.mocked(uploadFile).mockResolvedValueOnce({
|
||||
uploaded: true,
|
||||
url: "https://example.com/new-uploaded-logo.png",
|
||||
@@ -111,7 +111,7 @@ describe("EmailCustomizationSettings", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sends test email if a logo is saved and the user clicks 'Send Test Email'", async () => {
|
||||
test("sends test email if a logo is saved and the user clicks 'Send Test Email'", async () => {
|
||||
vi.mocked(sendTestEmailAction).mockResolvedValue({
|
||||
data: { success: true },
|
||||
});
|
||||
@@ -127,13 +127,13 @@ describe("EmailCustomizationSettings", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("displays upgrade prompt if hasWhiteLabelPermission is false", () => {
|
||||
test("displays upgrade prompt if hasWhiteLabelPermission is false", () => {
|
||||
render(<EmailCustomizationSettings {...defaultProps} hasWhiteLabelPermission={false} />);
|
||||
// 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", () => {
|
||||
test("shows read-only warning if isReadOnly is true", () => {
|
||||
render(<EmailCustomizationSettings {...defaultProps} isReadOnly />);
|
||||
|
||||
expect(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { TFnType } from "@tolgee/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { EmailTemplate } from "./email-template";
|
||||
|
||||
const mockTranslate: TFnType = (key) => key;
|
||||
@@ -25,7 +25,7 @@ describe("EmailTemplate", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders the default logo if no custom logo is provided", async () => {
|
||||
test("renders the default logo if no custom logo is provided", async () => {
|
||||
const emailTemplateElement = await EmailTemplate({
|
||||
children: <div>Test Content</div>,
|
||||
logoUrl: undefined,
|
||||
@@ -39,7 +39,7 @@ describe("EmailTemplate", () => {
|
||||
expect(logoImage).toHaveAttribute("src", "https://example.com/mock-logo.png");
|
||||
});
|
||||
|
||||
it("renders the custom logo if provided", async () => {
|
||||
test("renders the custom logo if provided", async () => {
|
||||
const emailTemplateElement = await EmailTemplate({
|
||||
...defaultProps,
|
||||
});
|
||||
@@ -51,7 +51,7 @@ describe("EmailTemplate", () => {
|
||||
expect(logoImage).toHaveAttribute("src", "https://example.com/custom-logo.png");
|
||||
});
|
||||
|
||||
it("renders the children content", async () => {
|
||||
test("renders the children content", async () => {
|
||||
const emailTemplateElement = await EmailTemplate({
|
||||
...defaultProps,
|
||||
});
|
||||
@@ -61,7 +61,7 @@ describe("EmailTemplate", () => {
|
||||
expect(screen.getByTestId("child-text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the imprint and privacy policy links if provided", async () => {
|
||||
test("renders the imprint and privacy policy links if provided", async () => {
|
||||
const emailTemplateElement = await EmailTemplate({
|
||||
...defaultProps,
|
||||
});
|
||||
@@ -72,7 +72,7 @@ describe("EmailTemplate", () => {
|
||||
expect(screen.getByText("emails.privacy_policy")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the imprint address if provided", async () => {
|
||||
test("renders the imprint address if provided", async () => {
|
||||
const emailTemplateElement = await EmailTemplate({
|
||||
...defaultProps,
|
||||
});
|
||||
|
||||
@@ -46,7 +46,13 @@ export async function EmailTemplate({
|
||||
</Container>
|
||||
|
||||
<Section className="mt-4 text-center text-sm">
|
||||
<Text className="m-0 font-normal text-slate-500">{t("emails.email_template_text_1")}</Text>
|
||||
<Link
|
||||
className="m-0 font-normal text-slate-500"
|
||||
href="https://formbricks.com/?utm_source=email_header&utm_medium=email"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{t("emails.email_template_text_1")}
|
||||
</Link>
|
||||
{IMPRINT_ADDRESS && (
|
||||
<Text className="m-0 font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
|
||||
)}
|
||||
@@ -56,7 +62,7 @@ export async function EmailTemplate({
|
||||
{t("emails.imprint")}
|
||||
</Link>
|
||||
)}
|
||||
{IMPRINT_URL && PRIVACY_URL && "•"}
|
||||
{IMPRINT_URL && PRIVACY_URL && " • "}
|
||||
{PRIVACY_URL && (
|
||||
<Link href={PRIVACY_URL} target="_blank" rel="noopener noreferrer" className="text-slate-500">
|
||||
{t("emails.privacy_policy")}
|
||||
|
||||
259
apps/web/modules/email/emails/lib/tests/utils.test.tsx
Normal file
259
apps/web/modules/email/emails/lib/tests/utils.test.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
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 }) => <p className={className}>{children}</p>,
|
||||
Container: ({ children }) => <div>{children}</div>,
|
||||
Row: ({ children, className }) => <div className={className}>{children}</div>,
|
||||
Column: ({ children, className }) => <div className={className}>{children}</div>,
|
||||
Link: ({ children, href }) => <a href={href}>{children}</a>,
|
||||
Img: ({ src, alt, className }) => <img src={src} alt={alt} className={className} />,
|
||||
}));
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@formbricks/lib/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/uploads/file1.pdf",
|
||||
"https://example.com/uploads/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/uploads/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",
|
||||
"font-bold",
|
||||
"break-words",
|
||||
"whitespace-pre-wrap",
|
||||
"italic"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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", "font-bold", "break-words", "whitespace-pre-wrap");
|
||||
});
|
||||
|
||||
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", "font-bold", "break-words", "whitespace-pre-wrap");
|
||||
|
||||
// Verify each item is present in the text content
|
||||
response.forEach((item) => {
|
||||
expect(textElement?.textContent).toContain(item);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
71
apps/web/modules/email/emails/lib/utils.tsx
Normal file
71
apps/web/modules/email/emails/lib/utils.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Column, Container, Img, Link, Row, Text } from "@react-email/components";
|
||||
import { TFnType } from "@tolgee/react";
|
||||
import { FileIcon } from "lucide-react";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { TSurveyQuestionType, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const renderEmailResponseValue = async (
|
||||
response: string | string[],
|
||||
questionType: TSurveyQuestionType,
|
||||
t: TFnType,
|
||||
overrideFileUploadResponse = false
|
||||
): Promise<React.JSX.Element> => {
|
||||
switch (questionType) {
|
||||
case TSurveyQuestionTypeEnum.FileUpload:
|
||||
return (
|
||||
<Container>
|
||||
{overrideFileUploadResponse ? (
|
||||
<Text className="mt-0 font-bold break-words whitespace-pre-wrap italic">
|
||||
{t("emails.render_email_response_value_file_upload_response_link_not_included")}
|
||||
</Text>
|
||||
) : (
|
||||
Array.isArray(response) &&
|
||||
response.map((responseItem) => (
|
||||
<Link
|
||||
className="mt-2 flex flex-col items-center justify-center rounded-lg bg-slate-200 p-2 text-black shadow-sm"
|
||||
href={responseItem}
|
||||
key={responseItem}>
|
||||
<FileIcon />
|
||||
<Text className="mx-auto mb-0 truncate">{getOriginalFileNameFromUrl(responseItem)}</Text>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
|
||||
case TSurveyQuestionTypeEnum.PictureSelection:
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
{Array.isArray(response) &&
|
||||
response.map((responseItem) => (
|
||||
<Column key={responseItem}>
|
||||
<Img alt={responseItem.split("/").pop()} className="m-2 h-28" src={responseItem} />
|
||||
</Column>
|
||||
))}
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
return (
|
||||
<Container>
|
||||
<Row className="my-1 font-semibold text-slate-700" dir="auto">
|
||||
{Array.isArray(response) &&
|
||||
response.map(
|
||||
(item, index) =>
|
||||
item && (
|
||||
<Row key={item} className="mb-1 flex items-center">
|
||||
<Column className="w-6 text-slate-400">#{index + 1}</Column>
|
||||
<Column className="rounded bg-slate-100 px-2 py-1">{item}</Column>
|
||||
</Row>
|
||||
)
|
||||
)}
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
default:
|
||||
return <Text className="mt-0 font-bold break-words whitespace-pre-wrap">{response}</Text>;
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user