Compare commits

..

23 Commits

Author SHA1 Message Date
Matthias Nannt
fafcd7e2df fix depandabot action 2025-04-18 09:30:30 +02:00
Matthias Nannt
9c47da0a04 add missing packages 2025-04-18 09:27:30 +02:00
Matthias Nannt
9b4f839410 chore(deps): update vite and dependabot config 2025-04-18 09:09:25 +02:00
victorvhs017
81d717ccff fix: iOS SDK memory leaks (#5388)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-04-17 18:03:05 +00:00
Dhruwang Jariwala
2e979c7323 fix: managers should not be allowed to create api keys (#5409) 2025-04-17 14:12:55 +00:00
Anshuman Pandey
4dfd15d6dd fix: adds no-cache header when debug mode is ON (#5405) 2025-04-17 09:03:44 +00:00
Matti Nannt
5b9bf3ff43 chore(infra): increase cloudwatch elb alarm limit to 10 (#5407) 2025-04-17 06:41:13 +00:00
Anshuman Pandey
d2f7485098 feat: advanced follow ups (#5340)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2025-04-17 06:39:22 +00:00
Dhruwang Jariwala
f8fee1fba7 fix: refactor end screen card description ux (#5386)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-04-17 06:28:18 +00:00
Matti Nannt
19249ca00f chore: enable performance insights for rds (#5404) 2025-04-17 06:27:34 +00:00
Anshuman Pandey
01e5700340 fix: adds eslint rules for using test and refactors the current tests (#5397) 2025-04-17 03:32:03 +00:00
Johannes
ff2f7660a6 chore: add segment id to modal view (#5391)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-04-17 02:56:32 +00:00
Johannes
2bc05e2b4a docs: tweak quick start guides (#5403) 2025-04-16 19:38:54 -07:00
Johannes
137c6447b7 docs: tweak Data Prefilling docs for clarity (#5402) 2025-04-16 18:43:00 -07:00
Piyush Gupta
ebc8f0c917 fix: docker build test (#5396) 2025-04-16 12:32:38 +00:00
Anshuman Pandey
5a8d10b5b4 fix: removes the onFinished callbacks from the iOS package (#5384) 2025-04-16 11:11:30 +00:00
Anshuman Pandey
875815fb62 fix: fixes cb urls (#5392) 2025-04-16 04:47:14 +00:00
Dhruwang Jariwala
cdf526e130 fix: type issue in notion integration (#5385) 2025-04-16 04:15:27 +00:00
Dhruwang Jariwala
b685032b34 chore: make env permissions optional in api key (#5309)
Co-authored-by: Victor Santos <victor@formbricks.com>
2025-04-15 11:42:48 +00:00
Vijay
a171f9cb00 fix: security hotspots in Dockerfile (#5314) 2025-04-15 04:15:56 -07:00
Dhruwang Jariwala
c452f05ec2 feat: questionid to summary (#5381) 2025-04-15 05:58:10 +00:00
Dhruwang Jariwala
93d91f80f2 fix: progress bar calculation (#5339) 2025-04-15 00:51:47 +00:00
Piyush Gupta
7b764c8427 fix: adds api_key label to the view permission modal (#5326) 2025-04-14 08:53:14 +00:00
401 changed files with 6187 additions and 2546 deletions

View File

@@ -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: "/"

View File

@@ -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"

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -101,17 +101,17 @@ export const OnboardingSetupInstructions = ({
<div>
{activeTab === "npm" ? (
<div className="prose prose-slate w-full">
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="sh">
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
npm install @formbricks/js
</CodeBlock>
<p>{t("common.or")}</p>
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="sh">
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
yarn add @formbricks/js
</CodeBlock>
<p className="text-sm text-slate-700">
{t("environments.connect.import_formbricks_and_initialize_the_widget_in_your_component")}
</p>
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="js">
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
</CodeBlock>
<Button id="onboarding-inapp-connect-read-npm-docs" className="mt-3" variant="secondary" asChild>
@@ -125,11 +125,11 @@ export const OnboardingSetupInstructions = ({
</div>
) : activeTab === "html" ? (
<div className="prose prose-slate">
<p className="mt-6 -mb-1 text-sm text-slate-700">
<p className="-mb-1 mt-6 text-sm text-slate-700">
{t("environments.connect.insert_this_code_into_the_head_tag_of_your_website")}
</p>
<div>
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="js">
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
{channel === "app" ? htmlSnippetForAppSurveys : htmlSnippetForWebsiteSurveys}
</CodeBlock>
</div>

View File

@@ -44,7 +44,7 @@ const Page = async (props: ConnectPageProps) => {
channel={channel}
/>
<Button
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={`/environments/${environment.id}`}>

View File

@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
<XMTemplateList project={project} user={user} environmentId={environment.id} />
{projects.length >= 2 && (
<Button
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={`/environments/${environment.id}/surveys`}>

View File

@@ -80,7 +80,7 @@ export const LandingSidebar = ({
<DropdownMenuTrigger
asChild
id="userDropdownTrigger"
className="w-full rounded-br-xl border-t py-4 pl-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-hidden">
className="w-full rounded-br-xl border-t py-4 pl-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
<div tabIndex={0} className={cn("flex cursor-pointer flex-row items-center space-x-3")}>
<ProfileAvatar userId={user.id} imageUrl={user.imageUrl} />
<>
@@ -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}

View File

@@ -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);

View File

@@ -50,7 +50,7 @@ const Page = async (props: ChannelPageProps) => {
<OnboardingOptionsContainer options={channelOptions} />
{projects.length >= 1 && (
<Button
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>

View File

@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
<OnboardingOptionsContainer options={channelOptions} />
{projects.length >= 1 && (
<Button
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>

View File

@@ -218,14 +218,14 @@ export const ProjectSettings = ({
</FormProvider>
</div>
<div className="relative flex h-[30rem] w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 shadow-sm">
<div className="relative flex h-[30rem] w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 shadow">
{logoUrl && (
<Image
src={logoUrl}
alt="Logo"
width={256}
height={56}
className="absolute top-2 left-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
/>
)}
<p className="text-sm text-slate-400">{t("common.preview")}</p>

View File

@@ -65,7 +65,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
/>
{projects.length >= 1 && (
<Button
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>

View File

@@ -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,

View File

@@ -36,7 +36,7 @@ export const ActionClassesTable = ({
return (
<>
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
{TableHeading}
<div id="actionClassesWrapper" className="flex flex-col">
{actionClasses.length > 0 ? (

View File

@@ -14,14 +14,16 @@ export const ActionClassDataRow = ({
<div className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
<div className="col-span-4 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="h-5 w-5 shrink-0 text-slate-500">{ACTION_TYPE_ICON_LOOKUP[actionClass.type]}</div>
<div className="h-5 w-5 flex-shrink-0 text-slate-500">
{ACTION_TYPE_ICON_LOOKUP[actionClass.type]}
</div>
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">{actionClass.name}</div>
<div className="text-xs text-slate-400">{actionClass.description}</div>
</div>
</div>
</div>
<div className="col-span-2 my-auto text-center text-sm whitespace-nowrap text-slate-500">
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
{timeSince(actionClass.createdAt.toString(), locale)}
</div>
<div className="text-center"></div>

View File

@@ -10,7 +10,7 @@ const Loading = () => {
<>
<PageContentWrapper>
<PageHeader pageTitle={t("common.actions")} />
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
<span className="sr-only">{t("common.edit")}</span>
<div className="col-span-4 pl-6">{t("environments.actions.user_actions")}</div>
@@ -22,7 +22,7 @@ const Loading = () => {
className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
<div className="col-span-4 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="h-6 w-6 shrink-0 animate-pulse rounded-full bg-slate-200 text-slate-500" />
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-slate-200 text-slate-500" />
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
@@ -33,7 +33,7 @@ const Loading = () => {
</div>
</div>
</div>
<div className="col-span-2 my-auto flex justify-center text-center text-sm whitespace-nowrap text-slate-500">
<div className="col-span-2 my-auto flex justify-center whitespace-nowrap text-center text-sm text-slate-500">
<div className="h-4 w-28 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>

View File

@@ -265,7 +265,7 @@ export const MainNavigation = ({
size="icon"
onClick={toggleSidebar}
className={cn(
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-hidden"
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-none"
)}>
{isCollapsed ? (
<PanelLeftOpenIcon strokeWidth={1.5} />
@@ -332,7 +332,7 @@ export const MainNavigation = ({
<DropdownMenuTrigger
asChild
id="userDropdownTrigger"
className="w-full rounded-br-xl border-t py-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-hidden">
className="w-full rounded-br-xl border-t py-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
<div
tabIndex={0}
className={cn(

View File

@@ -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();

View File

@@ -18,7 +18,7 @@ export const TopControlBar = ({
}: SideBarProps) => {
return (
<div className="fixed inset-0 top-0 z-30 flex h-14 w-full items-center justify-end bg-slate-50 px-6">
<div className="z-10 shadow-2xs">
<div className="shadow-xs z-10">
<div className="flex w-fit items-center space-x-2 py-2">
<TopControlButtons
environment={environment}

View File

@@ -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 = [

View File

@@ -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,

View File

@@ -56,7 +56,7 @@ export const EditAlerts = ({
<TooltipTrigger>
<div className="col-span-1 flex cursor-default items-center justify-center space-x-2">
<span>{t("environments.settings.notifications.every_response")}</span>
<HelpCircleIcon className="h-4 w-4 shrink-0 text-slate-500" />
<HelpCircleIcon className="h-4 w-4 flex-shrink-0 text-slate-500" />
</div>
</TooltipTrigger>
<TooltipContent>
@@ -99,7 +99,7 @@ export const EditAlerts = ({
))}
</div>
) : (
<div className="m-2 flex h-16 items-center justify-center rounded-sm bg-slate-50 text-sm text-slate-500">
<div className="m-2 flex h-16 items-center justify-center rounded bg-slate-50 text-sm text-slate-500">
<p>{t("common.no_surveys_found")}</p>
</div>
)}

View File

@@ -11,7 +11,7 @@ export const IntegrationsTip = ({ environmentId }: IntegrationsTipProps) => {
const { t } = useTranslate();
return (
<div>
<div className="flex max-w-4xl items-center space-y-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-xs md:space-y-0 md:text-base">
<div className="flex max-w-4xl items-center space-y-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base">
<SlackIcon className="mr-3 h-4 w-4 text-blue-400" />
<p className="text-sm">
{t("environments.settings.notifications.need_slack_or_discord_notifications")}?

View File

@@ -105,7 +105,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
<div>
<div className="relative h-10 w-10 overflow-hidden rounded-full">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-30">
<svg className="h-7 w-7 animate-spin text-slate-200" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path

View File

@@ -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,
},
];

View File

@@ -97,7 +97,7 @@ const Page = async (props) => {
</PageHeader>
{isEnterpriseEdition ? (
<div>
<div className="mt-8 max-w-4xl rounded-lg border border-slate-300 bg-slate-100 shadow-xs">
<div className="mt-8 max-w-4xl rounded-lg border border-slate-300 bg-slate-100 shadow-sm">
<div className="space-y-4 p-8">
<div className="flex items-center gap-x-2">
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:bg-green-800">
@@ -123,7 +123,7 @@ const Page = async (props) => {
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
<svg
viewBox="0 0 1024 1024"
className="absolute top-1/2 left-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
className="absolute left-1/2 top-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
aria-hidden="true">
<circle
cx={512}
@@ -152,7 +152,7 @@ const Page = async (props) => {
</p>
</div>
</div>
<div className="mt-8 rounded-lg border border-slate-300 bg-slate-100 shadow-xs">
<div className="mt-8 rounded-lg border border-slate-300 bg-slate-100 shadow-sm">
<div className="p-8">
<h2 className="mr-2 inline-flex text-2xl font-bold text-slate-700">
{t("environments.settings.enterprise.enterprise_features")}

View File

@@ -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 = {

View File

@@ -25,13 +25,13 @@ export const SettingsCard = ({
return (
<div
className={cn(
"relative my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-xs",
"relative my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm",
className
)}
id={title}>
<div className="border-b border-slate-200 px-4 pb-4">
<div className="flex">
<h3 className="text-lg leading-6 font-medium text-slate-900 capitalize">{title}</h3>
<h3 className="text-lg font-medium capitalize leading-6 text-slate-900">{title}</h3>
<div className="ml-2">
{beta && <Badge size="normal" type="warning" text="Beta" />}
{soon && (

View File

@@ -36,7 +36,7 @@ export const ResponseTableCell = ({
// Conditional rendering of maximize icon
const renderMaximizeIcon = cell.column.id === "createdAt" && (
<div
className="hidden shrink-0 cursor-pointer items-center rounded-md border border-slate-200 bg-white p-2 group-hover:flex hover:border-slate-300"
className="hidden flex-shrink-0 cursor-pointer items-center rounded-md border border-slate-200 bg-white p-2 hover:border-slate-300 group-hover:flex"
onClick={handleCellClick}>
<Maximize2Icon className="h-4 w-4" />
</div>

View File

@@ -20,7 +20,7 @@ interface AddressSummaryProps {
export const AddressSummary = ({ questionSummary, environmentId, survey, locale }: AddressSummaryProps) => {
const { t } = useTranslate();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<div>
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">

View File

@@ -16,7 +16,7 @@ export const CTASummary = ({ questionSummary, survey }: CTASummaryProps) => {
const { t } = useTranslate();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
survey={survey}
questionSummary={questionSummary}
@@ -40,7 +40,7 @@ export const CTASummary = ({ questionSummary, survey }: CTASummaryProps) => {
</>
}
/>
<div className="space-y-5 px-4 pt-4 pb-6 text-sm md:px-6 md:text-base">
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex space-x-1">
<p className="font-semibold text-slate-700">CTR</p>

View File

@@ -16,9 +16,9 @@ export const CalSummary = ({ questionSummary, survey }: CalSummaryProps) => {
const { t } = useTranslate();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<div className="space-y-5 px-4 pt-4 pb-6 text-sm md:px-6 md:text-base">
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
<div>
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex space-x-1">

View File

@@ -39,9 +39,9 @@ export const ConsentSummary = ({ questionSummary, survey, setFilter }: ConsentSu
},
];
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<div className="space-y-5 px-4 pt-4 pb-6 text-sm md:px-6 md:text-base">
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{summaryItems.map((summaryItem) => {
return (
<div

View File

@@ -25,7 +25,7 @@ export const ContactInfoSummary = ({
}: ContactInfoSummaryProps) => {
const { t } = useTranslate();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<div>
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">

View File

@@ -46,7 +46,7 @@ export const DateQuestionSummary = ({
};
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<div className="">
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">

View File

@@ -36,7 +36,7 @@ export const FileUploadSummary = ({
};
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<div className="">
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
@@ -80,7 +80,7 @@ export const FileUploadSummary = ({
return (
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
<a href={fileUrl} key={index} target="_blank" rel="noopener noreferrer">
<div className="absolute top-0 right-0 m-2">
<div className="absolute right-0 top-0 m-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
<DownloadIcon className="h-6 text-slate-500" />
</div>

View File

@@ -27,8 +27,8 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
);
};
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="space-y-2 px-4 pt-6 pb-5 md:px-6">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="space-y-2 px-4 pb-5 pt-6 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">{questionSummary.id}</h3>
</div>
@@ -76,7 +76,7 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
</div>
)}
</div>
<div className="ph-no-capture col-span-2 pl-6 font-semibold whitespace-pre-wrap">
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold">
{response.value}
</div>
<div className="px-4 text-slate-500 md:px-6">

View File

@@ -45,14 +45,14 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
: [];
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<div className="overflow-x-auto p-6">
{/* Summary Table */}
<table className="mx-auto border-collapse cursor-default text-left">
<thead>
<tr>
<th className="p-4 pt-0 pb-3 font-medium text-slate-400 dark:border-slate-600 dark:text-slate-200"></th>
<th className="p-4 pb-3 pt-0 font-medium text-slate-400 dark:border-slate-600 dark:text-slate-200"></th>
{columns.map((column) => (
<th key={column} className="text-center font-medium">
<TooltipRenderer tooltipContent={getTooltipContent(column)} shouldRender={true}>
@@ -65,7 +65,7 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
<tbody>
{questionSummary.data.map(({ rowLabel, columnPercentages }, rowIndex) => (
<tr key={rowLabel}>
<td className="max-w-60 overflow-hidden p-4 text-ellipsis whitespace-nowrap">
<td className="max-w-60 overflow-hidden text-ellipsis whitespace-nowrap p-4">
<TooltipRenderer tooltipContent={getTooltipContent(rowLabel)} shouldRender={true}>
<p className="max-w-40 overflow-hidden text-ellipsis whitespace-nowrap">{rowLabel}</p>
</TooltipRenderer>
@@ -83,7 +83,7 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
)}>
<div
style={{ backgroundColor: `rgba(0,196,184,${getOpacityLevel(percentage)})` }}
className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded-sm p-4 text-sm text-slate-950 hover:outline"
className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline"
onClick={() =>
setFilter(
questionSummary.question.id,

View File

@@ -65,7 +65,7 @@ export const MultipleChoiceSummary = ({
};
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
@@ -78,7 +78,7 @@ export const MultipleChoiceSummary = ({
) : undefined
}
/>
<div className="space-y-5 px-4 pt-4 pb-6 text-sm md:px-6 md:text-base">
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{results.map((result, resultsIdx) => (
<div
key={result.value}

View File

@@ -60,16 +60,16 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
};
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<div className="space-y-5 px-4 pt-4 pb-6 text-sm md:px-6 md:text-base">
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{["promoters", "passives", "detractors", "dismissed"].map((group) => (
<div className="cursor-pointer hover:opacity-80" key={group} onClick={() => applyFilter(group)}>
<div
className={`mb-2 flex justify-between ${group === "dismissed" ? "mb-2 border-t bg-white pt-4 text-sm md:text-base" : ""}`}>
<div className="mr-8 flex space-x-1">
<p
className={`font-semibold text-slate-700 capitalize ${group === "dismissed" ? "" : "text-slate-700"}`}>
className={`font-semibold capitalize text-slate-700 ${group === "dismissed" ? "" : "text-slate-700"}`}>
{group}
</p>
<div>
@@ -91,7 +91,7 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
))}
</div>
<div className="flex justify-center pt-4 pb-4">
<div className="flex justify-center pb-4 pt-4">
<HalfCircle value={questionSummary.score} />
</div>
</div>

View File

@@ -60,7 +60,7 @@ export const OpenTextSummary = ({
];
return (
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}

View File

@@ -30,7 +30,7 @@ export const PictureChoiceSummary = ({ questionSummary, survey, setFilter }: Pic
const results = questionSummary.choices;
const { t } = useTranslate();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
@@ -43,7 +43,7 @@ export const PictureChoiceSummary = ({ questionSummary, survey, setFilter }: Pic
) : undefined
}
/>
<div className="space-y-5 px-4 pt-4 pb-6 text-sm md:px-6 md:text-base">
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{results.map((result, index) => (
<div
className="cursor-pointer hover:opacity-80"

View File

@@ -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>
);
};

View File

@@ -17,16 +17,16 @@ export const RankingSummary = ({ questionSummary, surveyType, survey }: RankingS
});
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<div className="space-y-5 px-4 pt-4 pb-6 text-sm md:px-6 md:text-base">
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{results.map((result, resultsIdx) => (
<div key={result.value} className="group cursor-pointer">
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
<div className="mr-8 flex w-full justify-between space-x-1 sm:justify-normal">
<div className="flex w-full items-center">
<span className="mr-2 text-slate-400">#{resultsIdx + 1}</span>
<div className="rounded-sm bg-slate-100 px-2 py-1">{result.value}</div>
<div className="rounded bg-slate-100 px-2 py-1">{result.value}</div>
<span className="ml-auto flex items-center space-x-1">
<span className="font-bold text-slate-600">
#{convertFloatToNDecimal(result.avgRanking, 2)}

View File

@@ -37,7 +37,7 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
}, [questionSummary]);
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
@@ -50,7 +50,7 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
</div>
}
/>
<div className="space-y-5 px-4 pt-4 pb-6 text-sm md:px-6 md:text-base">
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{questionSummary.choices.map((result) => (
<div
className="cursor-pointer hover:opacity-80"

View File

@@ -43,7 +43,7 @@ const ScrollToTop: React.FC<ScrollToTopProps> = ({ containerId }) => {
return (
<button
onClick={scrollToTop}
className={`fixed right-4 bottom-4 z-1 flex h-10 w-10 justify-center rounded-md bg-slate-500 p-2 text-white transition-opacity ${
className={`fixed bottom-4 right-4 z-[1] flex h-10 w-10 justify-center rounded-md bg-slate-500 p-2 text-white transition-opacity ${
showButton ? "opacity-80" : "opacity-0"
}`}>

View File

@@ -39,7 +39,7 @@ export const SummaryDropOffs = ({ dropOff, survey }: SummaryDropOffsProps) => {
};
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="">
<div className="grid h-10 grid-cols-6 items-center border-y border-slate-200 bg-slate-100 text-sm font-semibold text-slate-600">
<div className="col-span-3 pl-4 md:pl-6">{t("common.questions")}</div>
@@ -77,10 +77,10 @@ export const SummaryDropOffs = ({ dropOff, survey }: SummaryDropOffsProps) => {
)}
</p>
</div>
<div className="text-center font-semibold whitespace-pre-wrap">
<div className="whitespace-pre-wrap text-center font-semibold">
{quesDropOff.ttc > 0 ? (quesDropOff.ttc / 1000).toFixed(2) + "s" : "N/A"}
</div>
<div className="text-center font-semibold whitespace-pre-wrap">{quesDropOff.impressions}</div>
<div className="whitespace-pre-wrap text-center font-semibold">{quesDropOff.impressions}</div>
<div className="pl-6 text-center md:px-6">
<span className="mr-1.5 font-semibold">{quesDropOff.dropOffCount}</span>
<span>({Math.round(quesDropOff.dropOffPercentage)}%)</span>

View File

@@ -17,7 +17,7 @@ const StatCard = ({ label, percentage, value, tooltipText, isLoading }) => {
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<div className="flex h-full cursor-default flex-col justify-between space-y-2 rounded-xl border border-slate-200 bg-white p-4 text-left shadow-xs">
<div className="flex h-full cursor-default flex-col justify-between space-y-2 rounded-xl border border-slate-200 bg-white p-4 text-left shadow-sm">
<p className="flex items-center gap-1 text-sm text-slate-600">
{label}
{typeof percentage === "number" && !isNaN(percentage) && !isLoading && (
@@ -101,7 +101,7 @@ export const SummaryMetadata = ({
<TooltipTrigger>
<div
onClick={() => setShowDropOffs(!showDropOffs)}
className="group flex h-full w-full cursor-pointer flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-xs">
className="group flex h-full w-full cursor-pointer flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm">
<span className="text-sm text-slate-600">
{t("environments.surveys.summary.drop_offs")}
{`${Math.round(dropOffPercentage)}%` !== "NaN%" && !isLoading && (

View File

@@ -99,7 +99,7 @@ export const EmbedView = ({
className={cn(
"rounded-md px-4 py-2",
tab.id === activeId
? "bg-white text-slate-900 shadow-xs"
? "bg-white text-slate-900 shadow-sm"
: "border-transparent text-slate-700 hover:text-slate-900"
)}>
{tab.label}

View File

@@ -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

View File

@@ -389,7 +389,7 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
onOpenChange={(value) => {
value && handleDatePickerClose();
}}>
<DropdownMenuTrigger asChild className="focus:bg-muted cursor-pointer outline-hidden">
<DropdownMenuTrigger asChild className="focus:bg-muted cursor-pointer outline-none">
<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>

View File

@@ -96,7 +96,7 @@ export const QuestionFilterComboBox = ({
<DropdownMenuTrigger
disabled={disabled}
className={clsx(
"h-9 max-w-fit rounded-md rounded-r-none border-r-[1px] border-slate-300 bg-white p-2 text-sm text-slate-600 focus:ring-0 focus:outline-transparent",
"h-9 max-w-fit rounded-md rounded-r-none border-r-[1px] border-slate-300 bg-white p-2 text-sm text-slate-600 focus:outline-transparent focus:ring-0",
!disabled ? "cursor-pointer" : "opacity-50"
)}>
<div className="flex items-center justify-between">
@@ -146,7 +146,7 @@ export const QuestionFilterComboBox = ({
key={`${o}-${index}`}
type="button"
onClick={() => handleRemoveMultiSelect(filterComboBoxValue.filter((i) => i !== o))}
className="flex w-30 items-center bg-slate-100 px-2 whitespace-nowrap text-slate-600">
className="w-30 flex items-center whitespace-nowrap bg-slate-100 px-2 text-slate-600">
{o}
<X width={14} height={14} className="ml-2" />
</button>
@@ -166,7 +166,7 @@ export const QuestionFilterComboBox = ({
</div>
<div className="relative mt-2 h-full">
{open && (
<div className="animate-in bg-popover absolute top-0 z-10 max-h-52 w-full overflow-auto rounded-md bg-white outline-hidden">
<div className="animate-in bg-popover absolute top-0 z-10 max-h-52 w-full overflow-auto rounded-md bg-white outline-none">
<CommandList>
<div className="p-2">
<Input

View File

@@ -164,7 +164,7 @@ export const QuestionsComboBox = ({ options, selected, onChangeValue }: Question
value={inputValue}
onValueChange={setInputValue}
placeholder={t("common.search") + "..."}
className="h-5 border-none border-transparent p-0 shadow-none ring-offset-transparent outline-0 focus:border-none focus:border-transparent focus:shadow-none focus:ring-offset-transparent focus:outline-0"
className="h-5 border-none border-transparent p-0 shadow-none outline-0 ring-offset-transparent focus:border-none focus:border-transparent focus:shadow-none focus:outline-0 focus:ring-offset-transparent"
/>
)}
<div>
@@ -177,7 +177,7 @@ export const QuestionsComboBox = ({ options, selected, onChangeValue }: Question
</div>
<div className="relative mt-2 h-full">
{open && (
<div className="animate-in bg-popover absolute top-0 z-50 max-h-52 w-full overflow-auto rounded-md bg-white outline-hidden">
<div className="animate-in bg-popover absolute top-0 z-50 max-h-52 w-full overflow-auto rounded-md bg-white outline-none">
<CommandList>
<CommandEmpty>{t("common.no_result_found")}</CommandEmpty>
{options?.map((data) => (

View File

@@ -199,7 +199,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
return (
<Popover open={isOpen} onOpenChange={handleOpenChange}>
<PopoverTrigger className="flex min-w-[8rem] items-center justify-between rounded-sm border border-slate-200 bg-white p-3 text-sm text-slate-600 hover:border-slate-300 sm:min-w-[11rem] sm:px-6 sm:py-3">
<PopoverTrigger className="flex min-w-[8rem] items-center justify-between rounded border border-slate-200 bg-white p-3 text-sm text-slate-600 hover:border-slate-300 sm:min-w-[11rem] sm:px-6 sm:py-3">
<span>
Filter <b>{filterValue.filter.length > 0 && `(${filterValue.filter.length})`}</b>
</span>

View File

@@ -86,8 +86,8 @@ export const ResultsShareButton = ({ survey, webAppUrl }: ResultsShareButtonProp
<DropdownMenu>
<DropdownMenuTrigger
asChild
className="focus:bg-muted cursor-pointer border border-slate-200 outline-hidden hover:border-slate-300">
<div className="h-auto min-w-auto rounded-md border bg-white p-3 sm:flex sm:min-w-[7rem] sm:px-6 sm:py-3">
className="focus:bg-muted cursor-pointer border border-slate-200 outline-none hover:border-slate-300">
<div className="min-w-auto h-auto rounded-md border bg-white p-3 sm:flex sm:min-w-[7rem] 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("environments.surveys.summary.share_results")}

View File

@@ -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({

View File

@@ -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>,
});

View File

@@ -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: {

View File

@@ -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;
};

View File

@@ -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"],
},
};

View File

@@ -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();
});
});
});

View File

@@ -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

View File

@@ -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" },
});

View File

@@ -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([]);

View File

@@ -35,7 +35,7 @@ export const GET = async (req: NextRequest) => {
<div tw="flex rounded-2xl absolute -right-2 mt-2">
<a tw={`rounded-xl border border-transparent bg-[${brandColor}] h-18 w-38 opacity-50`}></a>
</div>
<div tw="flex rounded-2xl shadow-sm ">
<div tw="flex rounded-2xl shadow ">
<a
tw={`flex items-center justify-center rounded-xl border border-transparent bg-[${brandColor}] text-2xl text-white h-18 w-38`}>
Begin!

View File

@@ -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",

View File

@@ -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");

View File

@@ -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);

View File

@@ -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",

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -1,5 +1,5 @@
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
import { describe, expect, it, vi } from "vitest";
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";
@@ -30,7 +30,7 @@ vi.mock("@/lib/utils/action-client", () => ({
}));
describe("deleteUserAction", () => {
it("deletes user successfully when multi-org is enabled", async () => {
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([]);
@@ -44,7 +44,7 @@ describe("deleteUserAction", () => {
expect(getIsMultiOrgEnabled).toHaveBeenCalledTimes(1);
});
it("deletes user successfully when multi-org is disabled but user is not sole owner of any org", async () => {
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([]);
@@ -58,7 +58,7 @@ describe("deleteUserAction", () => {
expect(getIsMultiOrgEnabled).toHaveBeenCalledTimes(1);
});
it("throws OperationNotAllowedError when user is sole owner in at least one org and multi-org is disabled", async () => {
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([

View File

@@ -1,6 +1,6 @@
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import * as nextAuth from "next-auth/react";
import { afterEach, describe, expect, it, vi } from "vitest";
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";
@@ -32,7 +32,7 @@ describe("DeleteAccountModal", () => {
cleanup();
});
it("renders modal with correct props", () => {
test("renders modal with correct props", () => {
render(
<DeleteAccountModal
open={true}
@@ -48,7 +48,7 @@ describe("DeleteAccountModal", () => {
expect(screen.getByText("Org2")).toBeInTheDocument();
});
it("disables delete button when email does not match", () => {
test("disables delete button when email does not match", () => {
render(
<DeleteAccountModal
open={true}
@@ -65,7 +65,7 @@ describe("DeleteAccountModal", () => {
expect(input).toHaveValue("wrong@example.com");
});
it("allows account deletion flow (non-cloud)", async () => {
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
@@ -96,7 +96,7 @@ describe("DeleteAccountModal", () => {
});
});
it("allows account deletion flow (cloud)", async () => {
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
@@ -133,7 +133,7 @@ describe("DeleteAccountModal", () => {
});
});
it("handles deletion errors", async () => {
test("handles deletion errors", async () => {
const deleteUserAction = vi.spyOn(actions, "deleteUserAction").mockRejectedValue(new Error("fail"));
render(

View File

@@ -1,5 +1,5 @@
import { cleanup, render } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, test, vi } from "vitest";
import { RatingSmiley } from "./index";
// Mock the smiley components from ../SingleResponseCard/components/Smileys
@@ -64,7 +64,7 @@ describe("RatingSmiley", () => {
const activeClass = "fill-rating-fill";
// Test branch: range === 10 => iconsIdx = [0,1,2,...,9]
it("renders correct icon for range 10 when active", () => {
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");
@@ -72,7 +72,7 @@ describe("RatingSmiley", () => {
expect(icon.className).toContain(activeClass);
});
it("renders correct icon for range 10 when inactive", () => {
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();
@@ -80,7 +80,7 @@ describe("RatingSmiley", () => {
});
// Test branch: range === 7 => iconsIdx = [1,3,4,5,6,8,9]
it("renders correct icon for range 7 when active", () => {
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");
@@ -89,7 +89,7 @@ describe("RatingSmiley", () => {
});
// Test branch: range === 5 => iconsIdx = [3,4,5,6,7]
it("renders correct icon for range 5 when active", () => {
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");
@@ -98,7 +98,7 @@ describe("RatingSmiley", () => {
});
// Test branch: range === 4 => iconsIdx = [4,5,6,7]
it("renders correct icon for range 4 when active", () => {
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");
@@ -107,7 +107,7 @@ describe("RatingSmiley", () => {
});
// Test branch: range === 3 => iconsIdx = [4,5,7]
it("renders correct icon for range 3 when active", () => {
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");

View File

@@ -1,6 +1,6 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
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";
@@ -27,7 +27,7 @@ describe("LanguageDropdown", () => {
cleanup();
});
it("renders nothing when enabledLanguages length is 1", () => {
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} />
@@ -36,7 +36,7 @@ describe("LanguageDropdown", () => {
expect(screen.queryByRole("button")).toBeNull();
});
it("renders button and toggles dropdown when multiple languages exist", async () => {
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());
@@ -66,7 +66,7 @@ describe("LanguageDropdown", () => {
});
});
it("closes dropdown when clicking outside", async () => {
test("closes dropdown when clicking outside", async () => {
vi.mocked(getEnabledLanguages).mockReturnValue(dummySurveyMultiple.languages);
vi.mocked(getLanguageLabel).mockImplementation((code: string, _locale: string) => code);

View File

@@ -1,5 +1,5 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, test } from "vitest";
import { SurveyLinkDisplay } from "./SurveyLinkDisplay";
describe("SurveyLinkDisplay", () => {
@@ -7,14 +7,14 @@ describe("SurveyLinkDisplay", () => {
cleanup();
});
it("renders the Input when surveyUrl is provided", () => {
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();
});
it("renders loading state when surveyUrl is empty", () => {
test("renders loading state when surveyUrl is empty", () => {
render(<SurveyLinkDisplay surveyUrl="" />);
const loadingDiv = screen.getByTestId("loading-div");
expect(loadingDiv).toBeInTheDocument();

View File

@@ -4,7 +4,7 @@ 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, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { ShareSurveyLink } from "./index";
const dummySurvey = {
@@ -91,7 +91,7 @@ describe("ShareSurveyLink", () => {
cleanup();
});
it("calls getUrl on mount and sets surveyUrl accordingly with singleUse enabled and default language", async () => {
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" });
@@ -113,7 +113,7 @@ describe("ShareSurveyLink", () => {
expect(url).not.toContain("lang=");
});
it("appends language query when language is changed from default", async () => {
test("appends language query when language is changed from default", async () => {
vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: "dummySuId" });
const setSurveyUrl = vi.fn();
@@ -133,7 +133,7 @@ describe("ShareSurveyLink", () => {
});
});
it("preview button opens new window with preview query", async () => {
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`);
@@ -157,7 +157,7 @@ describe("ShareSurveyLink", () => {
});
});
it("copy button writes surveyUrl to clipboard and shows toast", async () => {
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}`);
@@ -182,7 +182,7 @@ describe("ShareSurveyLink", () => {
});
});
it("download QR code button calls downloadQRCode", async () => {
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);
@@ -204,7 +204,7 @@ describe("ShareSurveyLink", () => {
expect(dummyDownloadQRCode).toHaveBeenCalled();
});
it("renders regenerate button when survey.singleUse.enabled is true and calls generateNewSingleUseLink", async () => {
test("renders regenerate button when survey.singleUse.enabled is true and calls generateNewSingleUseLink", async () => {
vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: "dummySuId" });
const setSurveyUrl = vi.fn();
@@ -225,7 +225,7 @@ describe("ShareSurveyLink", () => {
});
});
it("handles error when generating single-use link fails", async () => {
test("handles error when generating single-use link fails", async () => {
vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: undefined });
vi.mocked(getFormattedErrorMessage).mockReturnValue("Failed to generate link");

View File

@@ -9,7 +9,7 @@ import {
getProjectIdFromResponseNoteId,
} from "@/lib/utils/helper";
import { getTag } from "@/lib/utils/services";
import { describe, expect, it, vi } from "vitest";
import { describe, expect, test, vi } from "vitest";
import { deleteResponse, getResponse } from "@formbricks/lib/response/service";
import {
createResponseNote,
@@ -86,7 +86,7 @@ vi.mock("@/lib/utils/action-client", () => ({
}));
describe("createTagAction", () => {
it("successfully creates a tag", async () => {
test("successfully creates a tag", async () => {
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValueOnce("org1");
await createTagAction({ ...dummyTagInput, ...dummyCtx });
@@ -98,7 +98,7 @@ describe("createTagAction", () => {
});
describe("createTagToResponseAction", () => {
it("adds tag to response when environments match", async () => {
test("adds tag to response when environments match", async () => {
vi.mocked(getEnvironmentIdFromResponseId).mockResolvedValueOnce("env1");
vi.mocked(getTag).mockResolvedValueOnce({ environmentId: "env1" });
await createTagToResponseAction({ ...dummyTagToResponseInput, ...dummyCtx });
@@ -111,7 +111,7 @@ describe("createTagToResponseAction", () => {
);
});
it("throws error when environments do not match", async () => {
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(
@@ -121,7 +121,7 @@ describe("createTagToResponseAction", () => {
});
describe("deleteTagOnResponseAction", () => {
it("deletes tag on response when environments match", async () => {
test("deletes tag on response when environments match", async () => {
vi.mocked(getEnvironmentIdFromResponseId).mockResolvedValueOnce("env1");
vi.mocked(getTag).mockResolvedValueOnce({ environmentId: "env1" });
await deleteTagOnResponseAction({ ...dummyTagToResponseInput, ...dummyCtx });
@@ -134,7 +134,7 @@ describe("deleteTagOnResponseAction", () => {
);
});
it("throws error when environments do not match", async () => {
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(
@@ -144,7 +144,7 @@ describe("deleteTagOnResponseAction", () => {
});
describe("deleteResponseAction", () => {
it("deletes response successfully", async () => {
test("deletes response successfully", async () => {
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
await deleteResponseAction({ ...dummyResponseIdInput, ...dummyCtx });
expect(checkAuthorizationUpdated).toHaveBeenCalled();
@@ -155,7 +155,7 @@ describe("deleteResponseAction", () => {
});
describe("updateResponseNoteAction", () => {
it("updates response note successfully", async () => {
test("updates response note successfully", async () => {
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
await updateResponseNoteAction({ ...dummyResponseNoteInput, ...dummyCtx });
expect(checkAuthorizationUpdated).toHaveBeenCalled();
@@ -169,7 +169,7 @@ describe("updateResponseNoteAction", () => {
});
describe("resolveResponseNoteAction", () => {
it("resolves response note successfully", async () => {
test("resolves response note successfully", async () => {
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
await resolveResponseNoteAction({ responseNoteId: "note1", ...dummyCtx });
expect(checkAuthorizationUpdated).toHaveBeenCalled();
@@ -180,7 +180,7 @@ describe("resolveResponseNoteAction", () => {
});
describe("createResponseNoteAction", () => {
it("creates a response note successfully", async () => {
test("creates a response note successfully", async () => {
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
await createResponseNoteAction({ ...dummyCreateNoteInput, ...dummyCtx });
expect(checkAuthorizationUpdated).toHaveBeenCalled();
@@ -195,7 +195,7 @@ describe("createResponseNoteAction", () => {
});
describe("getResponseAction", () => {
it("retrieves response successfully", async () => {
test("retrieves response successfully", async () => {
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true);
await getResponseAction({ ...dummyGetResponseInput, ...dummyCtx });
expect(checkAuthorizationUpdated).toHaveBeenCalled();

View File

@@ -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 { TSurveyHiddenFields } from "@formbricks/types/surveys/types";
import { HiddenFields } from "./HiddenFields";
@@ -16,7 +16,7 @@ describe("HiddenFields", () => {
cleanup();
});
it("renders empty container when no fieldIds are provided", () => {
test("renders empty container when no fieldIds are provided", () => {
render(
<HiddenFields hiddenFields={{ fieldIds: [] } as unknown as TSurveyHiddenFields} responseData={{}} />
);
@@ -24,7 +24,7 @@ describe("HiddenFields", () => {
expect(container).toBeDefined();
});
it("renders nothing for fieldIds with no corresponding response data", () => {
test("renders nothing for fieldIds with no corresponding response data", () => {
render(
<HiddenFields
hiddenFields={{ fieldIds: ["field1"] } as unknown as TSurveyHiddenFields}
@@ -34,7 +34,7 @@ describe("HiddenFields", () => {
expect(screen.queryByText("field1")).toBeNull();
});
it("renders field and value when responseData exists and is a string", async () => {
test("renders field and value when responseData exists and is a string", async () => {
render(
<HiddenFields
hiddenFields={{ fieldIds: ["field1", "field2"] } as unknown as TSurveyHiddenFields}
@@ -46,7 +46,7 @@ describe("HiddenFields", () => {
expect(screen.queryByText("field2")).toBeNull();
});
it("renders empty text when responseData value is not a string", () => {
test("renders empty text when responseData value is not a string", () => {
render(
<HiddenFields
hiddenFields={{ fieldIds: ["field1"] } as unknown as TSurveyHiddenFields}
@@ -58,7 +58,7 @@ describe("HiddenFields", () => {
expect(valueParagraphs.length).toBeGreaterThan(0);
});
it("displays tooltip content for hidden field", async () => {
test("displays tooltip content for hidden field", async () => {
render(
<HiddenFields
hiddenFields={{ fieldIds: ["field1"] } as unknown as TSurveyHiddenFields}

View File

@@ -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 { parseRecallInfo } from "@formbricks/lib/utils/recall";
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
import { QuestionSkip } from "./QuestionSkip";
@@ -34,7 +34,7 @@ describe("QuestionSkip", () => {
cleanup();
});
it("renders nothing when skippedQuestions is falsy", () => {
test("renders nothing when skippedQuestions is falsy", () => {
render(
<QuestionSkip
skippedQuestions={undefined}
@@ -47,7 +47,7 @@ describe("QuestionSkip", () => {
expect(screen.queryByText("headline2")).toBeNull();
});
it("renders welcomeCard branch", () => {
test("renders welcomeCard branch", () => {
render(
<QuestionSkip
skippedQuestions={["f1"]}
@@ -60,7 +60,7 @@ describe("QuestionSkip", () => {
expect(screen.getByText("common.welcome_card")).toBeInTheDocument();
});
it("renders skipped branch with tooltip and parsed headlines", () => {
test("renders skipped branch with tooltip and parsed headlines", () => {
vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline1");
vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline2");
@@ -79,7 +79,7 @@ describe("QuestionSkip", () => {
expect(screen.getByText("parsed: headline2")).toBeInTheDocument();
});
it("renders aborted branch with closed message and parsed headlines", () => {
test("renders aborted branch with closed message and parsed headlines", () => {
vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline1");
vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline2");

View File

@@ -88,7 +88,7 @@ export const QuestionSkip = ({
{status === "aborted" && (
<div className="flex">
<div
className="flex w-0.5 grow items-start justify-center"
className="flex w-0.5 flex-grow items-start justify-center"
style={{
background:
"repeating-linear-gradient(to bottom, rgb(148 163 184), rgb(148 163 184) 2px, transparent 2px, transparent 10px)", // adjust the 2px to change dot size and 10px to change space between dots

View File

@@ -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 { RenderResponse } from "./RenderResponse";
// Mocks for dependencies
@@ -53,7 +53,7 @@ describe("RenderResponse", () => {
const defaultQuestion = { id: "q1", type: "Unknown" } as any;
const dummyLanguage = "default";
it("returns '-' for empty responseData (string)", () => {
test("returns '-' for empty responseData (string)", () => {
const { container } = render(
<RenderResponse
responseData={""}
@@ -65,7 +65,7 @@ describe("RenderResponse", () => {
expect(container.textContent).toBe("-");
});
it("returns '-' for empty responseData (array)", () => {
test("returns '-' for empty responseData (array)", () => {
const { container } = render(
<RenderResponse
responseData={[]}
@@ -77,7 +77,7 @@ describe("RenderResponse", () => {
expect(container.textContent).toBe("-");
});
it("returns '-' for empty responseData (object)", () => {
test("returns '-' for empty responseData (object)", () => {
const { container } = render(
<RenderResponse
responseData={{}}
@@ -89,7 +89,7 @@ describe("RenderResponse", () => {
expect(container.textContent).toBe("-");
});
it("renders RatingResponse for 'Rating' question with number", () => {
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} />
@@ -97,7 +97,7 @@ describe("RenderResponse", () => {
expect(screen.getByTestId("RatingResponse")).toHaveTextContent("Rating: 4");
});
it("renders formatted date for 'Date' question", () => {
test("renders formatted date for 'Date' question", () => {
const question = { ...defaultQuestion, type: "date" };
const dateStr = new Date("2023-01-01T12:00:00Z").toISOString();
render(
@@ -111,7 +111,7 @@ describe("RenderResponse", () => {
expect(screen.getByText(/formatted_/)).toBeInTheDocument();
});
it("renders PictureSelectionResponse for 'PictureSelection' question", () => {
test("renders PictureSelectionResponse for 'PictureSelection' question", () => {
const question = { ...defaultQuestion, type: "pictureSelection", choices: ["a", "b"] };
render(
<RenderResponse
@@ -126,7 +126,7 @@ describe("RenderResponse", () => {
);
});
it("renders FileUploadResponse for 'FileUpload' question", () => {
test("renders FileUploadResponse for 'FileUpload' question", () => {
const question = { ...defaultQuestion, type: "fileUpload" };
render(
<RenderResponse
@@ -139,7 +139,7 @@ describe("RenderResponse", () => {
expect(screen.getByTestId("FileUploadResponse")).toHaveTextContent("FileUpload: file1,file2");
});
it("renders Matrix response", () => {
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" };
@@ -155,7 +155,7 @@ describe("RenderResponse", () => {
expect(screen.getByText("row2:processed:answer2")).toBeInTheDocument();
});
it("renders ArrayResponse for 'Address' question", () => {
test("renders ArrayResponse for 'Address' question", () => {
const question = { ...defaultQuestion, type: "address" };
render(
<RenderResponse
@@ -168,7 +168,7 @@ describe("RenderResponse", () => {
expect(screen.getByTestId("ArrayResponse")).toHaveTextContent("addr1,addr2");
});
it("renders ResponseBadges for 'Cal' question (string)", () => {
test("renders ResponseBadges for 'Cal' question (string)", () => {
const question = { ...defaultQuestion, type: "cal" };
render(
<RenderResponse
@@ -181,7 +181,7 @@ describe("RenderResponse", () => {
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("Value");
});
it("renders ResponseBadges for 'Consent' question (number)", () => {
test("renders ResponseBadges for 'Consent' question (number)", () => {
const question = { ...defaultQuestion, type: "consent" };
render(
<RenderResponse responseData={5} question={question} survey={defaultSurvey} language={dummyLanguage} />
@@ -189,7 +189,7 @@ describe("RenderResponse", () => {
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("5");
});
it("renders ResponseBadges for 'CTA' question (string)", () => {
test("renders ResponseBadges for 'CTA' question (string)", () => {
const question = { ...defaultQuestion, type: "cta" };
render(
<RenderResponse
@@ -202,7 +202,7 @@ describe("RenderResponse", () => {
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("Click");
});
it("renders ResponseBadges for 'MultipleChoiceSingle' question (string)", () => {
test("renders ResponseBadges for 'MultipleChoiceSingle' question (string)", () => {
const question = { ...defaultQuestion, type: "multipleChoiceSingle" };
render(
<RenderResponse
@@ -215,7 +215,7 @@ describe("RenderResponse", () => {
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("option1");
});
it("renders ResponseBadges for 'MultipleChoiceMulti' question (array)", () => {
test("renders ResponseBadges for 'MultipleChoiceMulti' question (array)", () => {
const question = { ...defaultQuestion, type: "multipleChoiceMulti" };
render(
<RenderResponse
@@ -228,7 +228,7 @@ describe("RenderResponse", () => {
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("opt1,opt2");
});
it("renders ResponseBadges for 'NPS' question (number)", () => {
test("renders ResponseBadges for 'NPS' question (number)", () => {
const question = { ...defaultQuestion, type: "nps" };
render(
<RenderResponse responseData={9} question={question} survey={defaultSurvey} language={dummyLanguage} />
@@ -236,7 +236,7 @@ describe("RenderResponse", () => {
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("9");
});
it("renders RankingRespone for 'Ranking' question", () => {
test("renders RankingRespone for 'Ranking' question", () => {
const question = { ...defaultQuestion, type: "ranking" };
render(
<RenderResponse
@@ -249,7 +249,7 @@ describe("RenderResponse", () => {
expect(screen.getByTestId("RankingRespone")).toHaveTextContent("first,second");
});
it("renders default branch for unknown question type with string", () => {
test("renders default branch for unknown question type with string", () => {
const question = { ...defaultQuestion, type: "unknown" };
render(
<RenderResponse
@@ -262,7 +262,7 @@ describe("RenderResponse", () => {
expect(screen.getByText("hyper:some text")).toBeInTheDocument();
});
it("renders default branch for unknown question type with array", () => {
test("renders default branch for unknown question type with array", () => {
const question = { ...defaultQuestion, type: "unknown" };
render(
<RenderResponse

View File

@@ -1,6 +1,6 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
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";
@@ -84,7 +84,7 @@ describe("ResponseNotes", () => {
cleanup();
});
it("renders collapsed view when isOpen is false", () => {
test("renders collapsed view when isOpen is false", () => {
render(
<ResponseNotes
user={dummyUser}
@@ -99,7 +99,7 @@ describe("ResponseNotes", () => {
expect(screen.getByText(/note/i)).toBeInTheDocument();
});
it("opens panel on click when collapsed", async () => {
test("opens panel on click when collapsed", async () => {
render(
<ResponseNotes
user={dummyUser}
@@ -115,7 +115,7 @@ describe("ResponseNotes", () => {
expect(setIsOpen).toHaveBeenCalledWith(true);
});
it("submits a new note", async () => {
test("submits a new note", async () => {
vi.mocked(createResponseNoteAction).mockResolvedValueOnce("createdNote" as any);
render(
<ResponseNotes
@@ -140,7 +140,7 @@ describe("ResponseNotes", () => {
});
});
it("edits an existing note", async () => {
test("edits an existing note", async () => {
vi.mocked(updateResponseNoteAction).mockResolvedValueOnce("updatedNote" as any);
render(
<ResponseNotes
@@ -169,7 +169,7 @@ describe("ResponseNotes", () => {
});
});
it("resolves a note", async () => {
test("resolves a note", async () => {
vi.mocked(resolveResponseNoteAction).mockResolvedValueOnce(undefined);
render(
<ResponseNotes

View File

@@ -100,14 +100,14 @@ export const ResponseNotes = ({
return (
<div
className={clsx(
"absolute w-1/4 rounded-lg border border-slate-200 shadow-xs transition-all",
"absolute w-1/4 rounded-lg border border-slate-200 shadow-sm transition-all",
!isOpen && unresolvedNotes.length && "group/hint cursor-pointer bg-white hover:-right-3",
!isOpen && !unresolvedNotes.length && "cursor-pointer bg-slate-50",
isOpen
? "top-0 -right-2 h-5/6 max-h-[600px] w-1/4 bg-white"
: unresolvedNotes.length
? "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"
: "top-[8.333%] right-[120px] h-5/6 max-h-[600px] w-1/12 group-hover:right-[0]"
)}
onClick={() => {
if (!isOpen) setIsOpen(true);
@@ -215,7 +215,7 @@ export const ResponseNotes = ({
<textarea
rows={2}
className={cn(
"block w-full resize-none rounded-md border border-slate-100 bg-slate-50 p-2 shadow-xs focus:border-slate-500 focus:ring-0 sm:text-sm",
"block w-full resize-none rounded-md border border-slate-100 bg-slate-50 p-2 shadow-sm focus:border-slate-500 focus:ring-0 sm:text-sm",
!isTextAreaOpen && "scale-y-0 transition-all duration-1000",
!isTextAreaOpen && "translate-y-8 transition-all duration-300",
isTextAreaOpen && "scale-y-1 transition-all duration-1000",

View File

@@ -1,7 +1,7 @@
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, it, vi } from "vitest";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TTag } from "@formbricks/types/tags";
import { createTagAction, createTagToResponseAction, deleteTagOnResponseAction } from "../actions";
import { ResponseTagsWrapper } from "./ResponseTagsWrapper";
@@ -62,7 +62,7 @@ describe("ResponseTagsWrapper", () => {
cleanup();
});
it("renders settings button when not readOnly and navigates on click", async () => {
test("renders settings button when not readOnly and navigates on click", async () => {
render(
<ResponseTagsWrapper
tags={dummyTags}
@@ -78,7 +78,7 @@ describe("ResponseTagsWrapper", () => {
expect(dummyRouterPush).toHaveBeenCalledWith(`/environments/${dummyEnvironmentId}/project/tags`);
});
it("does not render settings button when readOnly", () => {
test("does not render settings button when readOnly", () => {
render(
<ResponseTagsWrapper
tags={dummyTags}
@@ -92,7 +92,7 @@ describe("ResponseTagsWrapper", () => {
expect(screen.queryByRole("button")).toBeNull();
});
it("renders provided tags", () => {
test("renders provided tags", () => {
render(
<ResponseTagsWrapper
tags={dummyTags}
@@ -108,7 +108,7 @@ describe("ResponseTagsWrapper", () => {
expect(screen.getByText("Tag Two")).toBeInTheDocument();
});
it("calls deleteTagOnResponseAction on tag delete success", async () => {
test("calls deleteTagOnResponseAction on tag delete success", async () => {
vi.mocked(deleteTagOnResponseAction).mockResolvedValueOnce({ data: "deleted" } as any);
render(
<ResponseTagsWrapper
@@ -128,7 +128,7 @@ describe("ResponseTagsWrapper", () => {
});
});
it("shows toast error on deleteTagOnResponseAction error", async () => {
test("shows toast error on deleteTagOnResponseAction error", async () => {
vi.mocked(deleteTagOnResponseAction).mockRejectedValueOnce(new Error("delete error"));
render(
<ResponseTagsWrapper
@@ -149,7 +149,7 @@ describe("ResponseTagsWrapper", () => {
});
});
it("creates a new tag via TagsCombobox and calls updateFetchedResponses on success", async () => {
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(
@@ -174,7 +174,7 @@ describe("ResponseTagsWrapper", () => {
});
});
it("handles createTagAction failure and shows toast error", async () => {
test("handles createTagAction failure and shows toast error", async () => {
vi.mocked(createTagAction).mockResolvedValueOnce({
error: { details: [{ issue: "Unique constraint failed on the fields" }] },
} as any);
@@ -198,7 +198,7 @@ describe("ResponseTagsWrapper", () => {
});
});
it("calls addTag correctly via TagsCombobox", async () => {
test("calls addTag correctly via TagsCombobox", async () => {
vi.mocked(createTagToResponseAction).mockResolvedValueOnce({ data: "tagAdded" } as any);
render(
<ResponseTagsWrapper
@@ -218,7 +218,7 @@ describe("ResponseTagsWrapper", () => {
});
});
it("clears tagIdToHighlight after timeout", async () => {
test("clears tagIdToHighlight after timeout", async () => {
vi.useFakeTimers();
render(

View File

@@ -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 { TResponseVariables } from "@formbricks/types/responses";
import { TSurveyVariables } from "@formbricks/types/surveys/types";
import { ResponseVariables } from "./ResponseVariables";
@@ -46,7 +46,7 @@ describe("ResponseVariables", () => {
cleanup();
});
it("renders nothing when no variable in variablesData meets type check", () => {
test("renders nothing when no variable in variablesData meets type check", () => {
render(
<ResponseVariables
variables={dummyVariables}
@@ -57,7 +57,7 @@ describe("ResponseVariables", () => {
expect(screen.queryByText("Variable Two")).toBeNull();
});
it("renders variables with valid response data", () => {
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();
@@ -66,13 +66,13 @@ describe("ResponseVariables", () => {
expect(screen.getByText("abc")).toBeInTheDocument();
});
it("renders FileDigitIcon for number type and FileType2Icon for string type", () => {
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();
});
it("displays tooltip content with 'common.variable'", () => {
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();

View File

@@ -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 { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys/types";
import { SingleResponseCardBody } from "./SingleResponseCardBody";
@@ -65,17 +65,17 @@ describe("SingleResponseCardBody", () => {
language: "en",
} as unknown as TResponse;
it("renders welcomeCard branch when enabled", () => {
test("renders welcomeCard branch when enabled", () => {
render(<SingleResponseCardBody survey={dummySurvey} response={dummyResponse} skippedQuestions={[]} />);
expect(screen.getAllByTestId("QuestionSkip")[0]).toHaveTextContent("welcomeCard");
});
it("renders VerifiedEmail when enabled and response verified", () => {
test("renders VerifiedEmail when enabled and response verified", () => {
render(<SingleResponseCardBody survey={dummySurvey} response={dummyResponse} skippedQuestions={[]} />);
expect(screen.getByTestId("VerifiedEmail")).toBeInTheDocument();
});
it("renders RenderResponse for valid answer", () => {
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={[]} />);
@@ -83,7 +83,7 @@ describe("SingleResponseCardBody", () => {
expect(screen.getByTestId("RenderResponse")).toHaveTextContent("answer1");
});
it("renders QuestionSkip for invalid answer", () => {
test("renders QuestionSkip for invalid answer", () => {
const surveyCopy = { ...dummySurvey, welcomeCard: { enabled: false } } as TSurvey;
const responseCopy = { ...dummyResponse, data: { q1: "", q2: "" } };
render(
@@ -93,23 +93,23 @@ describe("SingleResponseCardBody", () => {
expect(screen.getAllByTestId("QuestionSkip")[1]).toBeInTheDocument();
});
it("renders ResponseVariables when variables exist", () => {
test("renders ResponseVariables when variables exist", () => {
render(<SingleResponseCardBody survey={dummySurvey} response={dummyResponse} skippedQuestions={[]} />);
expect(screen.getByTestId("ResponseVariables")).toBeInTheDocument();
});
it("renders HiddenFields when hiddenFields enabled", () => {
test("renders HiddenFields when hiddenFields enabled", () => {
render(<SingleResponseCardBody survey={dummySurvey} response={dummyResponse} skippedQuestions={[]} />);
expect(screen.getByTestId("HiddenFields")).toBeInTheDocument();
});
it("renders completion indicator when response finished", () => {
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();
});
it("processes question mapping correctly with skippedQuestions modification", () => {
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: "" } };

View File

@@ -1,7 +1,7 @@
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, it, vi } from "vitest";
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";
@@ -65,7 +65,7 @@ describe("SingleResponseCardHeader", () => {
const dummyUser = { id: "user1", email: "user1@example.com" } as TUser;
const dummyLocale = "en-US";
it("renders response view with contact (user exists)", () => {
test("renders response view with contact (user exists)", () => {
vi.mocked(isSubmissionTimeMoreThan5Minutes).mockReturnValue(true);
render(
<SingleResponseCardHeader
@@ -84,7 +84,7 @@ describe("SingleResponseCardHeader", () => {
expect(screen.getByRole("link")).toBeInTheDocument();
});
it("renders response view with no contact (anonymous)", () => {
test("renders response view with no contact (anonymous)", () => {
const responseNoContact = { ...dummyResponse, contact: null };
render(
<SingleResponseCardHeader
@@ -101,7 +101,7 @@ describe("SingleResponseCardHeader", () => {
expect(screen.getByText("common.anonymous")).toBeInTheDocument();
});
it("renders people view", () => {
test("renders people view", () => {
render(
<SingleResponseCardHeader
pageType="people"
@@ -119,7 +119,7 @@ describe("SingleResponseCardHeader", () => {
expect(screen.getByTestId("SurveyStatusIndicator")).toBeInTheDocument();
});
it("renders language label when response.language is not default", () => {
test("renders language label when response.language is not default", () => {
const modifiedResponse = { ...dummyResponse, language: "fr" };
render(
<SingleResponseCardHeader
@@ -136,7 +136,7 @@ describe("SingleResponseCardHeader", () => {
expect(screen.getByText("fr_en-US")).toBeInTheDocument();
});
it("renders enabled trash icon and handles click", async () => {
test("renders enabled trash icon and handles click", async () => {
vi.mocked(isSubmissionTimeMoreThan5Minutes).mockReturnValue(true);
const setDeleteDialogOpen = vi.fn();
render(
@@ -156,7 +156,7 @@ describe("SingleResponseCardHeader", () => {
expect(setDeleteDialogOpen).toHaveBeenCalledWith(true);
});
it("renders disabled trash icon when deletion not allowed", async () => {
test("renders disabled trash icon when deletion not allowed", async () => {
vi.mocked(isSubmissionTimeMoreThan5Minutes).mockReturnValue(false);
render(
<SingleResponseCardHeader

View File

@@ -1,5 +1,5 @@
import { cleanup, render } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, test } from "vitest";
import {
ConfusedFace,
FrowningFace,
@@ -27,34 +27,34 @@ describe("Smileys", () => {
cleanup();
});
it("renders TiredFace", () => {
test("renders TiredFace", () => {
checkSvg(TiredFace);
});
it("renders WearyFace", () => {
test("renders WearyFace", () => {
checkSvg(WearyFace);
});
it("renders PerseveringFace", () => {
test("renders PerseveringFace", () => {
checkSvg(PerseveringFace);
});
it("renders FrowningFace", () => {
test("renders FrowningFace", () => {
checkSvg(FrowningFace);
});
it("renders ConfusedFace", () => {
test("renders ConfusedFace", () => {
checkSvg(ConfusedFace);
});
it("renders NeutralFace", () => {
test("renders NeutralFace", () => {
checkSvg(NeutralFace);
});
it("renders SlightlySmilingFace", () => {
test("renders SlightlySmilingFace", () => {
checkSvg(SlightlySmilingFace);
});
it("renders SmilingFaceWithSmilingEyes", () => {
test("renders SmilingFaceWithSmilingEyes", () => {
checkSvg(SmilingFaceWithSmilingEyes);
});
it("renders GrinningFaceWithSmilingEyes", () => {
test("renders GrinningFaceWithSmilingEyes", () => {
checkSvg(GrinningFaceWithSmilingEyes);
});
it("renders GrinningSquintingFace", () => {
test("renders GrinningSquintingFace", () => {
checkSvg(GrinningSquintingFace);
});
});

View File

@@ -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 { VerifiedEmail } from "./VerifiedEmail";
vi.mock("lucide-react", () => ({
@@ -15,14 +15,14 @@ describe("VerifiedEmail", () => {
cleanup();
});
it("renders verified email text and value when provided", () => {
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();
});
it("renders empty value when verifiedEmail is not a string", () => {
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" });

View File

@@ -1,7 +1,7 @@
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, it, vi } from "vitest";
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";
@@ -81,7 +81,7 @@ describe("SingleResponseCard", () => {
cleanup();
});
it("renders as a plain div when survey is draft and isReadOnly", () => {
test("renders as a plain div when survey is draft and isReadOnly", () => {
const draftSurvey = { ...dummySurvey, status: "draft" } as TSurvey;
render(
<SingleResponseCard
@@ -103,7 +103,7 @@ describe("SingleResponseCard", () => {
expect(screen.queryByRole("link")).toBeNull();
});
it("calls deleteResponseAction and refreshes router on successful deletion", async () => {
test("calls deleteResponseAction and refreshes router on successful deletion", async () => {
render(
<SingleResponseCard
survey={dummySurvey}
@@ -131,7 +131,7 @@ describe("SingleResponseCard", () => {
expect(dummyDeleteResponses).toHaveBeenCalledWith([dummyResponse.id]);
});
it("calls toast.error when deleteResponseAction throws error", async () => {
test("calls toast.error when deleteResponseAction throws error", async () => {
vi.mocked(deleteResponseAction).mockRejectedValueOnce(new Error("Delete failed"));
render(
<SingleResponseCard
@@ -157,7 +157,7 @@ describe("SingleResponseCard", () => {
});
});
it("calls updateResponse when getResponseAction returns updated response", async () => {
test("calls updateResponse when getResponseAction returns updated response", async () => {
vi.mocked(getResponseAction).mockResolvedValueOnce({ data: { updated: true } as any });
render(
<SingleResponseCard

View File

@@ -116,7 +116,7 @@ export const SingleResponseCard = ({
<div className={clsx("group relative", isOpen && "min-h-[300px]")}>
<div
className={clsx(
"relative z-20 my-6 rounded-xl border border-slate-200 bg-white shadow-xs transition-all",
"relative z-20 my-6 rounded-xl border border-slate-200 bg-white shadow-sm transition-all",
pageType === "response" &&
(isOpen
? "w-3/4"

View File

@@ -1,49 +1,49 @@
import { describe, expect, it } from "vitest";
import { describe, expect, test } from "vitest";
import { isSubmissionTimeMoreThan5Minutes, isValidValue } from "./util";
describe("isValidValue", () => {
it("returns false for an empty string", () => {
test("returns false for an empty string", () => {
expect(isValidValue("")).toBe(false);
});
it("returns false for a blank string", () => {
test("returns false for a blank string", () => {
expect(isValidValue(" ")).toBe(false);
});
it("returns true for a non-empty string", () => {
test("returns true for a non-empty string", () => {
expect(isValidValue("hello")).toBe(true);
});
it("returns true for numbers", () => {
test("returns true for numbers", () => {
expect(isValidValue(0)).toBe(true);
expect(isValidValue(42)).toBe(true);
});
it("returns false for an empty array", () => {
test("returns false for an empty array", () => {
expect(isValidValue([])).toBe(false);
});
it("returns true for a non-empty array", () => {
test("returns true for a non-empty array", () => {
expect(isValidValue(["item"])).toBe(true);
});
it("returns false for an empty object", () => {
test("returns false for an empty object", () => {
expect(isValidValue({})).toBe(false);
});
it("returns true for a non-empty object", () => {
test("returns true for a non-empty object", () => {
expect(isValidValue({ key: "value" })).toBe(true);
});
});
describe("isSubmissionTimeMoreThan5Minutes", () => {
it("returns true if submission time is more than 5 minutes ago", () => {
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);
});
it("returns false if submission time is less than or equal to 5 minutes ago", () => {
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);

View File

@@ -1,6 +1,6 @@
import { cleanup } from "@testing-library/react";
import { isValidElement } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, test, vi } from "vitest";
import { renderHyperlinkedContent } from "./utils";
describe("renderHyperlinkedContent", () => {
@@ -8,7 +8,7 @@ describe("renderHyperlinkedContent", () => {
cleanup();
});
it("returns a single span element when input has no url", () => {
test("returns a single span element when input has no url", () => {
const input = "Hello world";
const elements = renderHyperlinkedContent(input);
expect(elements).toHaveLength(1);
@@ -19,7 +19,7 @@ describe("renderHyperlinkedContent", () => {
expect(element.props.children).toEqual("Hello world");
});
it("splits input with a valid url into span, anchor, span", () => {
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.
@@ -36,7 +36,7 @@ describe("renderHyperlinkedContent", () => {
expect(elements[2].props.children).toEqual(" for info");
});
it("handles multiple valid urls in the input", () => {
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", ""
@@ -47,7 +47,7 @@ describe("renderHyperlinkedContent", () => {
expect(elements[3].props.href).toEqual("https://vitejs.dev");
});
it("renders a span instead of anchor when URL constructor throws", () => {
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(() => {

View File

@@ -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" },
});

View File

@@ -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);

View File

@@ -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" },
});

View File

@@ -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" }],

View File

@@ -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 });

Some files were not shown because too many files have changed in this diff Show More