mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-01 12:27:31 -05:00
Compare commits
1 Commits
feature/ad
...
increase-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7056466ce9 |
2
.github/actions/cache-build-web/action.yml
vendored
2
.github/actions/cache-build-web/action.yml
vendored
@@ -49,7 +49,7 @@ runs:
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
@@ -11,7 +11,7 @@ When generating test files inside the "/app/web" path, follow these rules:
|
||||
- Follow the same test pattern used for other files in the package where the file is located
|
||||
- All imports should be at the top of the file, not inside individual tests
|
||||
- For mocking inside "test" blocks use "vi.mocked"
|
||||
- If the file is located in the "packages/survey" path, use "@testing-library/preact" instead of "@testing-library/react"
|
||||
- Add the original file path to the "test.coverage.include"array in the "apps/web/vite.config.mts" file. Do this only when the test file is created.
|
||||
- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file
|
||||
- When using "screen.getByText" check for the tolgee string if it is being used in the file.
|
||||
- The types for mocked variables can be found in the "packages/types" path. Be sure that every imported type exists before using it. Don't create types that are not already in the codebase.
|
||||
@@ -28,5 +28,4 @@ afterEach(() => {
|
||||
- The "afterEach" function should only have the "cleanup()" line inside it and should be adde to the "vitest" imports.
|
||||
- For click events, import userEvent from "@testing-library/user-event"
|
||||
- Mock other components that can make the text more complex and but at the same time mocking it wouldn't make the test flaky. It's ok to leave basic and simple components.
|
||||
- You don't need to mock @tolgee/react
|
||||
- Use "import "@testing-library/jest-dom/vitest";"
|
||||
- You don't need to mock @tolgee/react
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
VERSION:
|
||||
description: 'The version of the Docker image to release, full image tag if image tag is v0.0.0 enter v0.0.0.'
|
||||
description: 'The version of the Docker image to release'
|
||||
required: true
|
||||
type: string
|
||||
REPOSITORY:
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
- uses: helmfile/helmfile-action@v2
|
||||
name: Deploy Formbricks Cloud Prod
|
||||
if: inputs.ENVIRONMENT == 'prod'
|
||||
if: (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch') && github.event.inputs.ENVIRONMENT == 'prod'
|
||||
env:
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||
@@ -75,7 +75,6 @@ jobs:
|
||||
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.FORMBRICKS_INGRESS_CERT_ARN }}
|
||||
FORMBRICKS_ROLE_ARN: ${{ secrets.FORMBRICKS_ROLE_ARN }}
|
||||
with:
|
||||
helmfile-version: 'v1.0.0'
|
||||
helm-plugins: >
|
||||
https://github.com/databus23/helm-diff,
|
||||
https://github.com/jkroepke/helm-secrets
|
||||
@@ -85,14 +84,13 @@ jobs:
|
||||
|
||||
- uses: helmfile/helmfile-action@v2
|
||||
name: Deploy Formbricks Cloud Stage
|
||||
if: inputs.ENVIRONMENT == 'stage'
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ENVIRONMENT == 'stage'
|
||||
env:
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.STAGE_FORMBRICKS_INGRESS_CERT_ARN }}
|
||||
FORMBRICKS_ROLE_ARN: ${{ secrets.STAGE_FORMBRICKS_ROLE_ARN }}
|
||||
with:
|
||||
helmfile-version: 'v1.0.0'
|
||||
helm-plugins: >
|
||||
https://github.com/databus23/helm-diff,
|
||||
https://github.com/jkroepke/helm-secrets
|
||||
|
||||
2
.github/workflows/formbricks-release.yml
vendored
2
.github/workflows/formbricks-release.yml
vendored
@@ -30,5 +30,5 @@ jobs:
|
||||
- docker-build
|
||||
- helm-chart-release
|
||||
with:
|
||||
VERSION: v${{ needs.docker-build.outputs.VERSION }}
|
||||
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
||||
ENVIRONMENT: "prod"
|
||||
|
||||
27
.github/workflows/labeler.yml
vendored
Normal file
27
.github/workflows/labeler.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
name: Pull Request Labeler
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||
sync-labels: ""
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
2
.github/workflows/sonarqube.yml
vendored
2
.github/workflows/sonarqube.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
"@storybook/react": "8.6.12",
|
||||
"@storybook/react-vite": "8.6.12",
|
||||
"@storybook/test": "8.6.12",
|
||||
"@typescript-eslint/eslint-plugin": "8.32.0",
|
||||
"@typescript-eslint/parser": "8.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||
"@typescript-eslint/parser": "8.31.1",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"esbuild": "0.25.4",
|
||||
"esbuild": "0.25.2",
|
||||
"eslint-plugin-storybook": "0.12.0",
|
||||
"prop-types": "15.8.1",
|
||||
"storybook": "8.6.12",
|
||||
|
||||
@@ -18,9 +18,8 @@ FROM node:22-alpine3.21 AS base
|
||||
FROM base AS installer
|
||||
|
||||
# Enable corepack and prepare pnpm
|
||||
RUN npm install --ignore-scripts -g corepack@latest
|
||||
RUN npm install -g corepack@latest
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@9.15.9 --activate
|
||||
|
||||
# Install necessary build tools and compilers
|
||||
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
|
||||
@@ -60,7 +59,7 @@ COPY . .
|
||||
RUN touch apps/web/.env
|
||||
|
||||
# Install the dependencies
|
||||
RUN pnpm install --ignore-scripts
|
||||
RUN pnpm install
|
||||
|
||||
# Build the project using our secret reader script
|
||||
# This mounts the secrets only during this build step without storing them in layers
|
||||
@@ -76,7 +75,7 @@ RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_ver
|
||||
#
|
||||
FROM base AS runner
|
||||
|
||||
RUN npm install --ignore-scripts -g corepack@latest
|
||||
RUN npm install -g corepack@latest
|
||||
RUN corepack enable
|
||||
|
||||
RUN apk add --no-cache curl \
|
||||
@@ -142,13 +141,12 @@ RUN chmod -R 755 ./node_modules/@noble/hashes
|
||||
COPY --from=installer /app/node_modules/zod ./node_modules/zod
|
||||
RUN chmod -R 755 ./node_modules/zod
|
||||
|
||||
RUN npm install --ignore-scripts -g tsx typescript pino-pretty
|
||||
RUN npm install -g prisma
|
||||
RUN npm install -g tsx typescript prisma pino-pretty
|
||||
|
||||
EXPOSE 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
ENV NODE_ENV="production"
|
||||
USER nextjs
|
||||
# USER nextjs
|
||||
|
||||
# Prepare volume for uploads
|
||||
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
||||
|
||||
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
asChild>
|
||||
<Link href={"/"}>
|
||||
|
||||
@@ -33,7 +33,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto flex justify-center whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="col-span-2 my-auto flex justify-center text-center text-sm whitespace-nowrap text-slate-500">
|
||||
<div className="h-4 w-28 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -264,7 +264,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:outline-none focus:ring-0 focus:ring-transparent"
|
||||
"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} />
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationAirtable,
|
||||
TIntegrationAirtableConfigData,
|
||||
TIntegrationAirtableCredential,
|
||||
TIntegrationAirtableTables,
|
||||
} from "@formbricks/types/integration/airtable";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { AddIntegrationModal } from "./AddIntegrationModal";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown",
|
||||
() => ({
|
||||
BaseSelectDropdown: ({ control, airtableArray, fetchTable, defaultValue, setValue }) => (
|
||||
<div>
|
||||
<label htmlFor="base">Base</label>
|
||||
<select
|
||||
id="base"
|
||||
defaultValue={defaultValue}
|
||||
onChange={(e) => {
|
||||
control._mockOnChange({ target: { name: "base", value: e.target.value } });
|
||||
setValue("table", ""); // Reset table when base changes
|
||||
fetchTable(e.target.value);
|
||||
}}>
|
||||
<option value="">Select Base</option>
|
||||
{airtableArray.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable", () => ({
|
||||
fetchTables: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: (value, _locale) => value?.default || value || "",
|
||||
}));
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
replaceHeadlineRecall: (survey, _locale) => survey,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/additional-integration-settings", () => ({
|
||||
AdditionalIntegrationSettings: ({
|
||||
includeVariables,
|
||||
setIncludeVariables,
|
||||
includeHiddenFields,
|
||||
setIncludeHiddenFields,
|
||||
includeMetadata,
|
||||
setIncludeMetadata,
|
||||
includeCreatedAt,
|
||||
setIncludeCreatedAt,
|
||||
}) => (
|
||||
<div data-testid="additional-settings">
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid="include-variables"
|
||||
checked={includeVariables}
|
||||
onChange={(e) => setIncludeVariables(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid="include-hidden"
|
||||
checked={includeHiddenFields}
|
||||
onChange={(e) => setIncludeHiddenFields(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid="include-metadata"
|
||||
checked={includeMetadata}
|
||||
onChange={(e) => setIncludeMetadata(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid="include-createdat"
|
||||
checked={includeCreatedAt}
|
||||
onChange={(e) => setIncludeCreatedAt(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: ({ children, open, setOpen }) =>
|
||||
open ? (
|
||||
<div data-testid="modal">
|
||||
{children}
|
||||
<button onClick={() => setOpen(false)}>Close Modal</button>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/alert", () => ({
|
||||
Alert: ({ children }) => <div data-testid="alert">{children}</div>,
|
||||
AlertTitle: ({ children }) => <div data-testid="alert-title">{children}</div>,
|
||||
AlertDescription: ({ children }) => <div data-testid="alert-description">{children}</div>,
|
||||
}));
|
||||
vi.mock("next/image", () => ({
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
default: (props) => <img alt="test" {...props} />,
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: vi.fn(() => ({ refresh: vi.fn() })),
|
||||
}));
|
||||
|
||||
// Mock the Select component used for Table and Survey selections
|
||||
vi.mock("@/modules/ui/components/select", () => ({
|
||||
Select: ({ children }) => (
|
||||
// Render children, assuming Controller passes props to the Trigger/Value
|
||||
// The actual select logic will be handled by the mocked Controller/field
|
||||
// We need to simulate the structure expected by the Controller render prop
|
||||
<div>{children}</div>
|
||||
),
|
||||
SelectTrigger: ({ children, ...props }) => <div {...props}>{children}</div>, // Mock Trigger
|
||||
SelectValue: ({ placeholder }) => <span>{placeholder || "Select..."}</span>, // Mock Value display
|
||||
SelectContent: ({ children }) => <div>{children}</div>, // Mock Content wrapper
|
||||
SelectItem: ({ children, value, ...props }) => (
|
||||
// Mock Item - crucial for userEvent.selectOptions if we were using a real select
|
||||
// For Controller, the value change is handled by field.onChange directly
|
||||
<div data-value={value} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock react-hook-form Controller to render a simple select
|
||||
vi.mock("react-hook-form", async () => {
|
||||
const actual = await vi.importActual("react-hook-form");
|
||||
let fields = {};
|
||||
const mockReset = vi.fn((values) => {
|
||||
fields = values || {}; // Reset fields, optionally with new values
|
||||
});
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useForm: vi.fn((options) => {
|
||||
fields = options?.defaultValues || {};
|
||||
const mockControlOnChange = (event) => {
|
||||
if (event && event.target) {
|
||||
fields[event.target.name] = event.target.value;
|
||||
}
|
||||
};
|
||||
return {
|
||||
handleSubmit: (fn) => (e) => {
|
||||
e?.preventDefault();
|
||||
fn(fields);
|
||||
},
|
||||
control: {
|
||||
_mockOnChange: mockControlOnChange,
|
||||
// Add other necessary control properties if needed
|
||||
register: vi.fn(),
|
||||
unregister: vi.fn(),
|
||||
getFieldState: vi.fn(() => ({ invalid: false, isDirty: false, isTouched: false, error: null })),
|
||||
_names: { mount: new Set(), unMount: new Set(), array: new Set(), watch: new Set() },
|
||||
_options: {},
|
||||
_proxyFormState: {
|
||||
isDirty: false,
|
||||
isValidating: false,
|
||||
dirtyFields: {},
|
||||
touchedFields: {},
|
||||
errors: {},
|
||||
},
|
||||
_formState: { isDirty: false, isValidating: false, dirtyFields: {}, touchedFields: {}, errors: {} },
|
||||
_updateFormState: vi.fn(),
|
||||
_updateFieldArray: vi.fn(),
|
||||
_executeSchema: vi.fn().mockResolvedValue({ errors: {}, values: {} }),
|
||||
_getWatch: vi.fn(),
|
||||
_subjects: {
|
||||
watch: { subscribe: vi.fn() },
|
||||
array: { subscribe: vi.fn() },
|
||||
state: { subscribe: vi.fn() },
|
||||
},
|
||||
_getDirty: vi.fn(),
|
||||
_reset: vi.fn(),
|
||||
_removeUnmounted: vi.fn(),
|
||||
},
|
||||
watch: (name) => fields[name],
|
||||
setValue: (name, value) => {
|
||||
fields[name] = value;
|
||||
},
|
||||
reset: mockReset,
|
||||
formState: { errors: {}, isDirty: false, isValid: true, isSubmitting: false },
|
||||
getValues: (name) => (name ? fields[name] : fields),
|
||||
};
|
||||
}),
|
||||
Controller: ({ name, defaultValue }) => {
|
||||
// Initialize field value if not already set by reset/defaultValues
|
||||
if (fields[name] === undefined && defaultValue !== undefined) {
|
||||
fields[name] = defaultValue;
|
||||
}
|
||||
|
||||
const field = {
|
||||
onChange: (valueOrEvent) => {
|
||||
const value = valueOrEvent?.target ? valueOrEvent.target.value : valueOrEvent;
|
||||
fields[name] = value;
|
||||
// Re-render might be needed here in a real scenario, but testing library handles it
|
||||
},
|
||||
onBlur: vi.fn(),
|
||||
value: fields[name],
|
||||
name: name,
|
||||
ref: vi.fn(),
|
||||
};
|
||||
|
||||
// Find the corresponding label to associate with the select
|
||||
const labelId = name; // Assuming label 'for' matches field name
|
||||
const labelText =
|
||||
name === "table" ? "environments.integrations.airtable.table_name" : "common.select_survey";
|
||||
|
||||
// Render a simple select element instead of the complex component
|
||||
// This makes interaction straightforward with userEvent.selectOptions
|
||||
return (
|
||||
<>
|
||||
{/* The actual label is rendered outside the Controller in the component */}
|
||||
<select
|
||||
id={labelId}
|
||||
aria-label={labelText} // Use aria-label for accessibility in tests
|
||||
{...field} // Spread field props
|
||||
defaultValue={defaultValue} // Pass defaultValue
|
||||
>
|
||||
{/* Need to dynamically get options based on context, simplified here */}
|
||||
{name === "table" &&
|
||||
mockTables.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name}
|
||||
</option>
|
||||
))}
|
||||
{name === "survey" &&
|
||||
mockSurveys.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
},
|
||||
reset: mockReset,
|
||||
};
|
||||
});
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const mockSurveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
name: "Survey 1",
|
||||
questions: [
|
||||
{ id: "q1", headline: { default: "Question 1" } },
|
||||
{ id: "q2", headline: { default: "Question 2" } },
|
||||
],
|
||||
hiddenFields: { enabled: true, fieldIds: ["hf1"] },
|
||||
variables: { enabled: true, fieldIds: ["var1"] },
|
||||
} as any,
|
||||
{
|
||||
id: "survey2",
|
||||
name: "Survey 2",
|
||||
questions: [{ id: "q3", headline: { default: "Question 3" } }],
|
||||
hiddenFields: { enabled: false },
|
||||
variables: { enabled: false },
|
||||
} as any,
|
||||
];
|
||||
const mockAirtableArray: TIntegrationItem[] = [
|
||||
{ id: "base1", name: "Base 1" },
|
||||
{ id: "base2", name: "Base 2" },
|
||||
];
|
||||
const mockAirtableIntegration: TIntegrationAirtable = {
|
||||
id: "integration1",
|
||||
type: "airtable",
|
||||
environmentId,
|
||||
config: {
|
||||
key: { access_token: "abc" } as TIntegrationAirtableCredential,
|
||||
email: "test@test.com",
|
||||
data: [],
|
||||
},
|
||||
};
|
||||
const mockTables: TIntegrationAirtableTables["tables"] = [
|
||||
{ id: "table1", name: "Table 1" },
|
||||
{ id: "table2", name: "Table 2" },
|
||||
];
|
||||
const mockSetOpenWithStates = vi.fn();
|
||||
const mockRouterRefresh = vi.fn();
|
||||
|
||||
describe("AddIntegrationModal", () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(useRouter).mockReturnValue({ refresh: mockRouterRefresh } as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders in add mode correctly", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={mockAirtableArray}
|
||||
surveys={mockSurveys}
|
||||
airtableIntegration={mockAirtableIntegration}
|
||||
isEditMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("environments.integrations.airtable.link_airtable_table")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Base")).toBeInTheDocument();
|
||||
// Use getByLabelText for the mocked selects
|
||||
expect(screen.getByLabelText("environments.integrations.airtable.table_name")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("common.select_survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.save")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("common.delete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows 'No Base Found' error when airtableArray is empty", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={[]}
|
||||
surveys={mockSurveys}
|
||||
airtableIntegration={mockAirtableIntegration}
|
||||
isEditMode={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("alert-title")).toHaveTextContent(
|
||||
"environments.integrations.airtable.no_bases_found"
|
||||
);
|
||||
});
|
||||
|
||||
test("shows 'No Surveys Found' warning when surveys array is empty", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={mockAirtableArray}
|
||||
surveys={[]}
|
||||
airtableIntegration={mockAirtableIntegration}
|
||||
isEditMode={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("environments.integrations.create_survey_warning")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("fetches and displays tables when a base is selected", async () => {
|
||||
vi.mocked(fetchTables).mockResolvedValue({ tables: mockTables });
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={mockAirtableArray}
|
||||
surveys={mockSurveys}
|
||||
airtableIntegration={mockAirtableIntegration}
|
||||
isEditMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const baseSelect = screen.getByLabelText("Base");
|
||||
await userEvent.selectOptions(baseSelect, "base1");
|
||||
|
||||
expect(fetchTables).toHaveBeenCalledWith(environmentId, "base1");
|
||||
await waitFor(() => {
|
||||
// Use getByLabelText (mocked select)
|
||||
const tableSelect = screen.getByLabelText("environments.integrations.airtable.table_name");
|
||||
expect(tableSelect).toBeEnabled();
|
||||
// Check options within the mocked select
|
||||
expect(tableSelect.querySelector("option[value='table1']")).toBeInTheDocument();
|
||||
expect(tableSelect.querySelector("option[value='table2']")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles deletion in edit mode", async () => {
|
||||
const initialData: TIntegrationAirtableConfigData = {
|
||||
baseId: "base1",
|
||||
tableId: "table1",
|
||||
surveyId: "survey1",
|
||||
questionIds: ["q1"],
|
||||
questions: "common.selected_questions",
|
||||
tableName: "Table 1",
|
||||
surveyName: "Survey 1",
|
||||
createdAt: new Date(),
|
||||
includeVariables: false,
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: false,
|
||||
includeCreatedAt: true,
|
||||
};
|
||||
const integrationWithData = {
|
||||
...mockAirtableIntegration,
|
||||
config: { ...mockAirtableIntegration.config, data: [initialData] },
|
||||
};
|
||||
const defaultData = { ...initialData, index: 0 } as any;
|
||||
|
||||
vi.mocked(fetchTables).mockResolvedValue({ tables: mockTables });
|
||||
vi.mocked(createOrUpdateIntegrationAction).mockResolvedValue({ ok: true, data: {} } as any);
|
||||
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={mockAirtableArray}
|
||||
surveys={mockSurveys}
|
||||
airtableIntegration={integrationWithData}
|
||||
isEditMode={true}
|
||||
defaultData={defaultData}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => expect(fetchTables).toHaveBeenCalled()); // Wait for initial load
|
||||
|
||||
// Click delete
|
||||
await userEvent.click(screen.getByText("common.delete"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationAction).toHaveBeenCalledTimes(1);
|
||||
const submittedData = vi.mocked(createOrUpdateIntegrationAction).mock.calls[0][0].integrationData;
|
||||
// Expect data array to be empty after deletion
|
||||
expect(submittedData.config.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully");
|
||||
expect(mockSetOpenWithStates).toHaveBeenCalledWith(false);
|
||||
expect(mockRouterRefresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles cancel button click", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={mockAirtableArray}
|
||||
surveys={mockSurveys}
|
||||
airtableIntegration={mockAirtableIntegration}
|
||||
isEditMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText("common.cancel"));
|
||||
expect(mockSetOpenWithStates).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { AirtableWrapper } from "./AirtableWrapper";
|
||||
|
||||
// Mock child components
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration",
|
||||
() => ({
|
||||
ManageIntegration: ({ setIsConnected }) => (
|
||||
<div data-testid="manage-integration">
|
||||
<button onClick={() => setIsConnected(false)}>Disconnect</button>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
ConnectIntegration: ({ handleAuthorization, isEnabled }) => (
|
||||
<div data-testid="connect-integration">
|
||||
<button onClick={handleAuthorization} disabled={!isEnabled}>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock library function
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable", () => ({
|
||||
authorize: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock image import
|
||||
vi.mock("@/images/airtableLogo.svg", () => ({
|
||||
default: "airtable-logo-path",
|
||||
}));
|
||||
|
||||
// Mock window.location.replace
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
replace: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const webAppUrl = "https://app.formbricks.com";
|
||||
const environment = { id: environmentId } as TEnvironment;
|
||||
const surveys = [];
|
||||
const airtableArray = [];
|
||||
const locale = "en-US" as const;
|
||||
|
||||
const baseProps = {
|
||||
environmentId,
|
||||
airtableArray,
|
||||
surveys,
|
||||
environment,
|
||||
webAppUrl,
|
||||
locale,
|
||||
};
|
||||
|
||||
describe("AirtableWrapper", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when not connected (no integration)", () => {
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={true} airtableIntegration={undefined} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when not connected (integration without key)", () => {
|
||||
const integrationWithoutKey = { config: {} } as TIntegrationAirtable;
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={true} airtableIntegration={integrationWithoutKey} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration disabled when isEnabled is false", () => {
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={false} airtableIntegration={undefined} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeDisabled();
|
||||
});
|
||||
|
||||
test("calls authorize and redirects when Connect button is clicked", async () => {
|
||||
const mockAuthorize = vi.mocked(authorize);
|
||||
const redirectUrl = "https://airtable.com/auth";
|
||||
mockAuthorize.mockResolvedValue(redirectUrl);
|
||||
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={true} airtableIntegration={undefined} />);
|
||||
|
||||
const connectButton = screen.getByRole("button", { name: "Connect" });
|
||||
await userEvent.click(connectButton);
|
||||
|
||||
expect(mockAuthorize).toHaveBeenCalledWith(environmentId, webAppUrl);
|
||||
await vi.waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith(redirectUrl);
|
||||
});
|
||||
});
|
||||
|
||||
test("renders ManageIntegration when connected", () => {
|
||||
const connectedIntegration = {
|
||||
id: "int-1",
|
||||
config: { key: { access_token: "abc" }, email: "test@test.com", data: [] },
|
||||
} as unknown as TIntegrationAirtable;
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={true} airtableIntegration={connectedIntegration} />);
|
||||
expect(screen.getByTestId("manage-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("connect-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("switches from ManageIntegration to ConnectIntegration when disconnected", async () => {
|
||||
const connectedIntegration = {
|
||||
id: "int-1",
|
||||
config: { key: { access_token: "abc" }, email: "test@test.com", data: [] },
|
||||
} as unknown as TIntegrationAirtable;
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={true} airtableIntegration={connectedIntegration} />);
|
||||
|
||||
// Initially, ManageIntegration is shown
|
||||
expect(screen.getByTestId("manage-integration")).toBeInTheDocument();
|
||||
|
||||
// Simulate disconnection via ManageIntegration's button
|
||||
const disconnectButton = screen.getByRole("button", { name: "Disconnect" });
|
||||
await userEvent.click(disconnectButton);
|
||||
|
||||
// Now, ConnectIntegration should be shown
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,125 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { IntegrationModalInputs } from "./AddIntegrationModal";
|
||||
import { BaseSelectDropdown } from "./BaseSelectDropdown";
|
||||
|
||||
// Mock UI components
|
||||
vi.mock("@/modules/ui/components/label", () => ({
|
||||
Label: ({ children, htmlFor }: { children: React.ReactNode; htmlFor: string }) => (
|
||||
<label htmlFor={htmlFor}>{children}</label>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/select", () => ({
|
||||
Select: ({ children, onValueChange, disabled, defaultValue }) => (
|
||||
<select
|
||||
data-testid="base-select"
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
defaultValue={defaultValue}>
|
||||
{children}
|
||||
</select>
|
||||
),
|
||||
SelectTrigger: ({ children }) => <div>{children}</div>,
|
||||
SelectValue: () => <span>SelectValueMock</span>,
|
||||
SelectContent: ({ children }) => <div>{children}</div>,
|
||||
SelectItem: ({ children, value }) => <option value={value}>{children}</option>,
|
||||
}));
|
||||
|
||||
// Mock react-hook-form's Controller specifically
|
||||
vi.mock("react-hook-form", async () => {
|
||||
const actual = await vi.importActual("react-hook-form");
|
||||
// Keep the actual useForm
|
||||
const originalUseForm = actual.useForm;
|
||||
|
||||
// Mock Controller
|
||||
const MockController = ({ name, _, render, defaultValue }) => {
|
||||
// Minimal mock: call render with a basic field object
|
||||
const field = {
|
||||
onChange: vi.fn(), // Simple spy for field.onChange
|
||||
onBlur: vi.fn(),
|
||||
value: defaultValue, // Use defaultValue passed to Controller
|
||||
name: name,
|
||||
ref: vi.fn(),
|
||||
};
|
||||
// The component passes the render prop result to the actual Select component
|
||||
return render({ field });
|
||||
};
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useForm: originalUseForm, // Use the actual useForm
|
||||
Controller: MockController, // Use the mocked Controller
|
||||
};
|
||||
});
|
||||
|
||||
const mockAirtableArray: TIntegrationItem[] = [
|
||||
{ id: "base1", name: "Base One" },
|
||||
{ id: "base2", name: "Base Two" },
|
||||
];
|
||||
|
||||
const mockFetchTable = vi.fn();
|
||||
|
||||
// Use a wrapper component that utilizes the actual useForm
|
||||
const renderComponent = (
|
||||
isLoading = false,
|
||||
defaultValue: string | undefined = undefined,
|
||||
airtableArray = mockAirtableArray
|
||||
) => {
|
||||
const Component = () => {
|
||||
// Now uses the actual useForm because Controller is mocked separately
|
||||
const { control, setValue } = useForm<IntegrationModalInputs>({
|
||||
defaultValues: { base: defaultValue },
|
||||
});
|
||||
return (
|
||||
<BaseSelectDropdown
|
||||
control={control}
|
||||
isLoading={isLoading}
|
||||
fetchTable={mockFetchTable} // The spy
|
||||
airtableArray={airtableArray}
|
||||
setValue={setValue} // Actual RHF setValue
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return render(<Component />);
|
||||
};
|
||||
|
||||
describe("BaseSelectDropdown", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders the label and select trigger", () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText("environments.integrations.airtable.airtable_base")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("base-select")).toBeInTheDocument();
|
||||
expect(screen.getByText("SelectValueMock")).toBeInTheDocument(); // From mocked SelectValue
|
||||
});
|
||||
|
||||
test("renders options from airtableArray", () => {
|
||||
renderComponent();
|
||||
const select = screen.getByTestId("base-select");
|
||||
expect(select.querySelectorAll("option")).toHaveLength(mockAirtableArray.length);
|
||||
expect(screen.getByText("Base One")).toBeInTheDocument();
|
||||
expect(screen.getByText("Base Two")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("disables the select when isLoading is true", () => {
|
||||
renderComponent(true);
|
||||
expect(screen.getByTestId("base-select")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("enables the select when isLoading is false", () => {
|
||||
renderComponent(false);
|
||||
expect(screen.getByTestId("base-select")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("renders correctly with empty airtableArray", () => {
|
||||
renderComponent(false, undefined, []);
|
||||
const select = screen.getByTestId("base-select");
|
||||
expect(select.querySelectorAll("option")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TIntegrationAirtableTables } from "@formbricks/types/integration/airtable";
|
||||
import { authorize, fetchTables } from "./airtable";
|
||||
|
||||
// Mock the logger
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const baseId = "test-base-id";
|
||||
const apiHost = "http://localhost:3000";
|
||||
|
||||
describe("Airtable Library", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchTables", () => {
|
||||
test("should fetch tables successfully", async () => {
|
||||
const mockTables: TIntegrationAirtableTables = {
|
||||
tables: [
|
||||
{ id: "tbl1", name: "Table 1" },
|
||||
{ id: "tbl2", name: "Table 2" },
|
||||
],
|
||||
};
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: async () => ({ data: mockTables }),
|
||||
};
|
||||
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
||||
|
||||
const tables = await fetchTables(environmentId, baseId);
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(`/api/v1/integrations/airtable/tables?baseId=${baseId}`, {
|
||||
method: "GET",
|
||||
headers: { environmentId: environmentId },
|
||||
cache: "no-store",
|
||||
});
|
||||
expect(tables).toEqual(mockTables);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authorize", () => {
|
||||
test("should return authUrl successfully", async () => {
|
||||
const mockAuthUrl = "https://airtable.com/oauth2/v1/authorize?...";
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: async () => ({ data: { authUrl: mockAuthUrl } }),
|
||||
};
|
||||
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
||||
|
||||
const authUrl = await authorize(environmentId, apiHost);
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(`${apiHost}/api/v1/integrations/airtable`, {
|
||||
method: "GET",
|
||||
headers: { environmentId: environmentId },
|
||||
});
|
||||
expect(authUrl).toBe(mockAuthUrl);
|
||||
});
|
||||
|
||||
test("should throw error and log when fetch fails", async () => {
|
||||
const errorText = "Failed to fetch";
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
text: async () => errorText,
|
||||
};
|
||||
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
||||
|
||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(`${apiHost}/api/v1/integrations/airtable`, {
|
||||
method: "GET",
|
||||
headers: { environmentId: environmentId },
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith({ errorText }, "authorize: Could not fetch airtable config");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,217 +0,0 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { getAirtableTables } from "@/lib/airtable/service";
|
||||
import { WEBAPP_URL } from "@/lib/constants";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable, TIntegrationAirtableCredential } from "@formbricks/types/integration/airtable";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import Page from "./page";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper", () => ({
|
||||
AirtableWrapper: vi.fn(() => <div>AirtableWrapper Mock</div>),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys");
|
||||
vi.mock("@/lib/airtable/service");
|
||||
|
||||
let mockAirtableClientId: string | undefined = "test-client-id";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
get AIRTABLE_CLIENT_ID() {
|
||||
return mockAirtableClientId;
|
||||
},
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
IS_PRODUCTION: true,
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/integration/service");
|
||||
vi.mock("@/lib/utils/locale");
|
||||
vi.mock("@/modules/environments/lib/utils");
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: vi.fn(() => <div>GoBackButton Mock</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: vi.fn(({ children }) => <div>{children}</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: vi.fn(({ pageTitle }) => <h1>{pageTitle}</h1>),
|
||||
}));
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
vi.mock("next/navigation");
|
||||
|
||||
const mockEnvironmentId = "test-env-id";
|
||||
const mockEnvironment = {
|
||||
id: mockEnvironmentId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
} as unknown as TEnvironment;
|
||||
const mockSurveys: TSurvey[] = [{ id: "survey1", name: "Survey 1" } as TSurvey];
|
||||
const mockAirtableIntegration: TIntegrationAirtable = {
|
||||
type: "airtable",
|
||||
config: {
|
||||
key: { access_token: "test-token" } as unknown as TIntegrationAirtableCredential,
|
||||
data: [],
|
||||
email: "test@example.com",
|
||||
},
|
||||
environmentId: mockEnvironmentId,
|
||||
id: "int_airtable_123",
|
||||
};
|
||||
const mockAirtableTables: TIntegrationItem[] = [{ id: "table1", name: "Table 1" } as TIntegrationItem];
|
||||
const mockLocale = "en-US";
|
||||
|
||||
const props = {
|
||||
params: {
|
||||
environmentId: mockEnvironmentId,
|
||||
},
|
||||
};
|
||||
|
||||
describe("Airtable Integration Page", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
vi.mocked(getSurveys).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getIntegrations).mockResolvedValue([mockAirtableIntegration]);
|
||||
vi.mocked(getAirtableTables).mockResolvedValue(mockAirtableTables);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("redirects if user is readOnly", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: true,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
await render(await Page(props));
|
||||
expect(redirect).toHaveBeenCalledWith("./");
|
||||
});
|
||||
|
||||
test("renders correctly when integration is configured", async () => {
|
||||
await render(await Page(props));
|
||||
|
||||
expect(screen.getByText("environments.integrations.airtable.airtable_integration")).toBeInTheDocument();
|
||||
expect(screen.getByText("GoBackButton Mock")).toBeInTheDocument();
|
||||
expect(screen.getByText("AirtableWrapper Mock")).toBeInTheDocument();
|
||||
|
||||
expect(vi.mocked(getEnvironmentAuth)).toHaveBeenCalledWith(mockEnvironmentId);
|
||||
expect(vi.mocked(getSurveys)).toHaveBeenCalledWith(mockEnvironmentId);
|
||||
expect(vi.mocked(getIntegrations)).toHaveBeenCalledWith(mockEnvironmentId);
|
||||
expect(vi.mocked(getAirtableTables)).toHaveBeenCalledWith(mockEnvironmentId);
|
||||
expect(vi.mocked(findMatchingLocale)).toHaveBeenCalled();
|
||||
|
||||
const AirtableWrapper = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"
|
||||
)
|
||||
).AirtableWrapper
|
||||
);
|
||||
expect(AirtableWrapper).toHaveBeenCalledWith(
|
||||
{
|
||||
isEnabled: true,
|
||||
airtableIntegration: mockAirtableIntegration,
|
||||
airtableArray: mockAirtableTables,
|
||||
environmentId: mockEnvironmentId,
|
||||
surveys: mockSurveys,
|
||||
environment: mockEnvironment,
|
||||
webAppUrl: WEBAPP_URL,
|
||||
locale: mockLocale,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("renders correctly when integration exists but is not configured (no key)", async () => {
|
||||
const integrationWithoutKey = {
|
||||
...mockAirtableIntegration,
|
||||
config: { ...mockAirtableIntegration.config, key: undefined },
|
||||
} as unknown as TIntegrationAirtable;
|
||||
vi.mocked(getIntegrations).mockResolvedValue([integrationWithoutKey]);
|
||||
|
||||
await render(await Page(props));
|
||||
|
||||
expect(screen.getByText("environments.integrations.airtable.airtable_integration")).toBeInTheDocument();
|
||||
expect(screen.getByText("AirtableWrapper Mock")).toBeInTheDocument();
|
||||
|
||||
expect(vi.mocked(getAirtableTables)).not.toHaveBeenCalled(); // Should not fetch tables if no key
|
||||
|
||||
const AirtableWrapper = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"
|
||||
)
|
||||
).AirtableWrapper
|
||||
);
|
||||
// Update assertion to match the actual call
|
||||
expect(AirtableWrapper).toHaveBeenCalledWith(
|
||||
{
|
||||
isEnabled: true, // isEnabled is true because AIRTABLE_CLIENT_ID is set in beforeEach
|
||||
airtableIntegration: integrationWithoutKey,
|
||||
airtableArray: [], // Should be empty as getAirtableTables is not called
|
||||
environmentId: mockEnvironmentId,
|
||||
surveys: mockSurveys,
|
||||
environment: mockEnvironment,
|
||||
webAppUrl: WEBAPP_URL,
|
||||
locale: mockLocale,
|
||||
},
|
||||
undefined // Change second argument to undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("renders correctly when integration is disabled (no client ID)", async () => {
|
||||
mockAirtableClientId = undefined; // Simulate disabled integration
|
||||
|
||||
await render(await Page(props));
|
||||
|
||||
expect(screen.getByText("environments.integrations.airtable.airtable_integration")).toBeInTheDocument();
|
||||
expect(screen.getByText("AirtableWrapper Mock")).toBeInTheDocument();
|
||||
|
||||
const AirtableWrapper = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"
|
||||
)
|
||||
).AirtableWrapper
|
||||
);
|
||||
expect(AirtableWrapper).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isEnabled: false, // Should be false
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,694 +0,0 @@
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsConfigData,
|
||||
} from "@formbricks/types/integration/google-sheet";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock actions and utilities
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions", () => ({
|
||||
getSpreadsheetNameByIdAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util", () => ({
|
||||
constructGoogleSheetsUrl: (id: string) => `https://docs.google.com/spreadsheets/d/${id}`,
|
||||
extractSpreadsheetIdFromUrl: (url: string) => url.split("/")[5],
|
||||
isValidGoogleSheetsUrl: (url: string) => url.startsWith("https://docs.google.com/spreadsheets/d/"),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: (value: any, _locale: string) => value?.default || "",
|
||||
}));
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
replaceHeadlineRecall: (survey: any) => survey,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/additional-integration-settings", () => ({
|
||||
AdditionalIntegrationSettings: ({
|
||||
includeVariables,
|
||||
setIncludeVariables,
|
||||
includeHiddenFields,
|
||||
setIncludeHiddenFields,
|
||||
includeMetadata,
|
||||
setIncludeMetadata,
|
||||
includeCreatedAt,
|
||||
setIncludeCreatedAt,
|
||||
}: any) => (
|
||||
<div>
|
||||
<span>Additional Settings</span>
|
||||
<input
|
||||
data-testid="include-variables"
|
||||
type="checkbox"
|
||||
checked={includeVariables}
|
||||
onChange={(e) => setIncludeVariables(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-hidden-fields"
|
||||
type="checkbox"
|
||||
checked={includeHiddenFields}
|
||||
onChange={(e) => setIncludeHiddenFields(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-metadata"
|
||||
type="checkbox"
|
||||
checked={includeMetadata}
|
||||
onChange={(e) => setIncludeMetadata(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-created-at"
|
||||
type="checkbox"
|
||||
checked={includeCreatedAt}
|
||||
onChange={(e) => setIncludeCreatedAt(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/dropdown-selector", () => ({
|
||||
DropdownSelector: ({ label, items, selectedItem, setSelectedItem }: any) => (
|
||||
<div>
|
||||
<label>{label}</label>
|
||||
<select
|
||||
data-testid="survey-dropdown"
|
||||
value={selectedItem?.id || ""}
|
||||
onChange={(e) => {
|
||||
const selected = items.find((item: any) => item.id === e.target.value);
|
||||
setSelectedItem(selected);
|
||||
}}>
|
||||
<option value="">Select a survey</option>
|
||||
{items.map((item: any) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: ({ open, children }: { open: boolean; children: React.ReactNode }) =>
|
||||
open ? <div data-testid="modal">{children}</div> : null,
|
||||
}));
|
||||
vi.mock("next/image", () => ({
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
|
||||
}));
|
||||
vi.mock("react-hook-form", () => ({
|
||||
useForm: () => ({
|
||||
handleSubmit: (callback: any) => (event: any) => {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@tolgee/react", async () => {
|
||||
const MockTolgeeProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
const useTranslate = () => ({
|
||||
t: (key: string, _?: any) => {
|
||||
// NOSONAR
|
||||
// Simple mock translation function
|
||||
if (key === "common.all_questions") return "All questions";
|
||||
if (key === "common.selected_questions") return "Selected questions";
|
||||
if (key === "environments.integrations.google_sheets.link_google_sheet") return "Link Google Sheet";
|
||||
if (key === "common.update") return "Update";
|
||||
if (key === "common.delete") return "Delete";
|
||||
if (key === "common.cancel") return "Cancel";
|
||||
if (key === "environments.integrations.google_sheets.spreadsheet_url") return "Spreadsheet URL";
|
||||
if (key === "common.select_survey") return "Select survey";
|
||||
if (key === "common.questions") return "Questions";
|
||||
if (key === "environments.integrations.google_sheets.enter_a_valid_spreadsheet_url_error")
|
||||
return "Please enter a valid Google Sheet URL.";
|
||||
if (key === "environments.integrations.please_select_a_survey_error") return "Please select a survey.";
|
||||
if (key === "environments.integrations.select_at_least_one_question_error")
|
||||
return "Please select at least one question.";
|
||||
if (key === "environments.integrations.integration_updated_successfully")
|
||||
return "Integration updated successfully.";
|
||||
if (key === "environments.integrations.integration_added_successfully")
|
||||
return "Integration added successfully.";
|
||||
if (key === "environments.integrations.integration_removed_successfully")
|
||||
return "Integration removed successfully.";
|
||||
if (key === "environments.integrations.google_sheets.google_sheet_logo") return "Google Sheet logo";
|
||||
if (key === "environments.integrations.google_sheets.google_sheets_integration_description")
|
||||
return "Sync responses with Google Sheets.";
|
||||
if (key === "environments.integrations.create_survey_warning")
|
||||
return "You need to create a survey first.";
|
||||
return key; // Return key if no translation is found
|
||||
},
|
||||
});
|
||||
return { TolgeeProvider: MockTolgeeProvider, useTranslate };
|
||||
});
|
||||
|
||||
// Mock dependencies
|
||||
const createOrUpdateIntegrationAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/integrations/actions"))
|
||||
.createOrUpdateIntegrationAction
|
||||
);
|
||||
const getSpreadsheetNameByIdAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions"))
|
||||
.getSpreadsheetNameByIdAction
|
||||
);
|
||||
const toast = vi.mocked((await import("react-hot-toast")).default);
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const mockSetOpen = vi.fn();
|
||||
|
||||
const surveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 1",
|
||||
type: "app",
|
||||
environmentId: environmentId,
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1?" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 2?" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Choice 1" } },
|
||||
{ id: "c2", label: { default: "Choice 2" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
variables: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
{
|
||||
id: "survey2",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 2",
|
||||
type: "link",
|
||||
environmentId: environmentId,
|
||||
status: "draft",
|
||||
questions: [
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate this?" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
variables: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const mockGoogleSheetIntegration = {
|
||||
id: "integration1",
|
||||
type: "googleSheets",
|
||||
config: {
|
||||
key: {
|
||||
access_token: "mock_access_token",
|
||||
expiry_date: Date.now() + 3600000,
|
||||
refresh_token: "mock_refresh_token",
|
||||
scope: "mock_scope",
|
||||
token_type: "Bearer",
|
||||
},
|
||||
email: "test@example.com",
|
||||
data: [], // Initially empty, will be populated in beforeEach
|
||||
},
|
||||
} as unknown as TIntegrationGoogleSheets;
|
||||
|
||||
const mockSelectedIntegration: TIntegrationGoogleSheetsConfigData & { index: number } = {
|
||||
spreadsheetId: "existing-sheet-id",
|
||||
spreadsheetName: "Existing Sheet",
|
||||
surveyId: surveys[0].id,
|
||||
surveyName: surveys[0].name,
|
||||
questionIds: [surveys[0].questions[0].id],
|
||||
questions: "Selected questions",
|
||||
createdAt: new Date(),
|
||||
includeVariables: true,
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: true,
|
||||
includeCreatedAt: false,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
describe("AddIntegrationModal", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset integration data before each test if needed
|
||||
mockGoogleSheetIntegration.config.data = [
|
||||
{ ...mockSelectedIntegration }, // Simulate existing data for update/delete tests
|
||||
];
|
||||
});
|
||||
|
||||
test("renders correctly when open (create mode)", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Link Google Sheet", { selector: "div.text-xl.font-medium" })
|
||||
).toBeInTheDocument();
|
||||
// Use getByPlaceholderText for the input
|
||||
expect(
|
||||
screen.getByPlaceholderText("https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>")
|
||||
).toBeInTheDocument();
|
||||
// Use getByTestId for the dropdown
|
||||
expect(screen.getByTestId("survey-dropdown")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Link Google Sheet" })).toBeInTheDocument();
|
||||
expect(screen.queryByText("Delete")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Questions")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly when open (update mode)", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Link Google Sheet", { selector: "div.text-xl.font-medium" })
|
||||
).toBeInTheDocument();
|
||||
// Use getByPlaceholderText for the input
|
||||
expect(
|
||||
screen.getByPlaceholderText("https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>")
|
||||
).toHaveValue("https://docs.google.com/spreadsheets/d/existing-sheet-id");
|
||||
expect(screen.getByTestId("survey-dropdown")).toHaveValue(surveys[0].id);
|
||||
expect(screen.getByText("Questions")).toBeInTheDocument();
|
||||
expect(screen.getByText("Delete")).toBeInTheDocument();
|
||||
expect(screen.getByText("Update")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("include-variables")).toBeChecked();
|
||||
expect(screen.getByTestId("include-hidden-fields")).not.toBeChecked();
|
||||
expect(screen.getByTestId("include-metadata")).toBeChecked();
|
||||
expect(screen.getByTestId("include-created-at")).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("selects survey and shows questions", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[1].id);
|
||||
|
||||
expect(screen.getByText("Questions")).toBeInTheDocument();
|
||||
surveys[1].questions.forEach((q) => {
|
||||
expect(screen.getByLabelText(q.headline.default)).toBeInTheDocument();
|
||||
// Initially all questions should be checked when a survey is selected in create mode
|
||||
expect(screen.getByLabelText(q.headline.default)).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles question selection", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
const firstQuestionCheckbox = screen.getByLabelText(surveys[0].questions[0].headline.default);
|
||||
expect(firstQuestionCheckbox).toBeChecked(); // Initially checked
|
||||
|
||||
await userEvent.click(firstQuestionCheckbox);
|
||||
expect(firstQuestionCheckbox).not.toBeChecked(); // Unchecked after click
|
||||
|
||||
await userEvent.click(firstQuestionCheckbox);
|
||||
expect(firstQuestionCheckbox).toBeChecked(); // Checked again
|
||||
});
|
||||
|
||||
test("creates integration successfully", async () => {
|
||||
getSpreadsheetNameByIdAction.mockResolvedValue({ data: "Test Sheet Name" });
|
||||
createOrUpdateIntegrationAction.mockResolvedValue({ data: null as any }); // Mock successful action
|
||||
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={{
|
||||
...mockGoogleSheetIntegration,
|
||||
config: { ...mockGoogleSheetIntegration.config, data: [] },
|
||||
}} // Start with empty data
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Google Sheet" });
|
||||
|
||||
await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/new-sheet-id");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
// Wait for questions to appear and potentially uncheck one
|
||||
const firstQuestionCheckbox = await screen.findByLabelText(surveys[0].questions[0].headline.default);
|
||||
await userEvent.click(firstQuestionCheckbox); // Uncheck first question
|
||||
|
||||
// Check additional settings
|
||||
await userEvent.click(screen.getByTestId("include-variables"));
|
||||
await userEvent.click(screen.getByTestId("include-metadata"));
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getSpreadsheetNameByIdAction).toHaveBeenCalledWith({
|
||||
googleSheetIntegration: expect.any(Object),
|
||||
environmentId,
|
||||
spreadsheetId: "new-sheet-id",
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationAction).toHaveBeenCalledWith({
|
||||
environmentId,
|
||||
integrationData: expect.objectContaining({
|
||||
type: "googleSheets",
|
||||
config: expect.objectContaining({
|
||||
key: mockGoogleSheetIntegration.config.key,
|
||||
email: mockGoogleSheetIntegration.config.email,
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
spreadsheetId: "new-sheet-id",
|
||||
spreadsheetName: "Test Sheet Name",
|
||||
surveyId: surveys[0].id,
|
||||
surveyName: surveys[0].name,
|
||||
questionIds: surveys[0].questions.slice(1).map((q) => q.id), // Excludes the first question
|
||||
questions: "Selected questions",
|
||||
includeVariables: true,
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: true,
|
||||
includeCreatedAt: true, // Default
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Integration added successfully.");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes integration successfully", async () => {
|
||||
createOrUpdateIntegrationAction.mockResolvedValue({ data: null as any });
|
||||
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration} // Contains initial data at index 0
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteButton = screen.getByText("Delete");
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationAction).toHaveBeenCalledWith({
|
||||
environmentId,
|
||||
integrationData: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
data: [], // Data array should be empty after deletion
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Integration removed successfully.");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("shows validation error for invalid URL", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Google Sheet" });
|
||||
|
||||
await userEvent.type(urlInput, "invalid-url");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please enter a valid Google Sheet URL.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationAction).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows validation error if no survey selected", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const submitButton = screen.getByRole("button", { name: "Link Google Sheet" });
|
||||
|
||||
await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/some-id");
|
||||
// No survey selected
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select a survey.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationAction).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows validation error if no questions selected", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Google Sheet" });
|
||||
|
||||
await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/some-id");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
// Uncheck all questions
|
||||
for (const question of surveys[0].questions) {
|
||||
const checkbox = await screen.findByLabelText(question.headline.default);
|
||||
await userEvent.click(checkbox);
|
||||
}
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select at least one question.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationAction).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows error toast if createOrUpdateIntegrationAction fails", async () => {
|
||||
const errorMessage = "Failed to update integration";
|
||||
getSpreadsheetNameByIdAction.mockResolvedValue({ data: "Some Sheet Name" });
|
||||
createOrUpdateIntegrationAction.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Google Sheet" });
|
||||
|
||||
await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/another-id");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getSpreadsheetNameByIdAction).toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationAction).toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls setOpen(false) and resets form on cancel", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
|
||||
// Simulate some interaction
|
||||
await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/temp-id");
|
||||
await userEvent.click(cancelButton);
|
||||
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
// Re-render with open=true to check if state was reset (URL should be empty)
|
||||
cleanup();
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
// Use getByPlaceholderText for the input check after re-render
|
||||
expect(
|
||||
screen.getByPlaceholderText("https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>")
|
||||
).toHaveValue("");
|
||||
});
|
||||
});
|
||||
@@ -1,175 +0,0 @@
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsCredential,
|
||||
} from "@formbricks/types/integration/google-sheet";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock child components and functions
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration",
|
||||
() => ({
|
||||
ManageIntegration: vi.fn(({ setOpenAddIntegrationModal }) => (
|
||||
<div data-testid="manage-integration">
|
||||
<button onClick={() => setOpenAddIntegrationModal(true)}>Open Modal</button>
|
||||
</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
ConnectIntegration: vi.fn(({ handleAuthorization }) => (
|
||||
<div data-testid="connect-integration">
|
||||
<button onClick={handleAuthorization}>Connect</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal",
|
||||
() => ({
|
||||
AddIntegrationModal: vi.fn(({ open }) =>
|
||||
open ? <div data-testid="add-integration-modal">Modal</div> : null
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google", () => ({
|
||||
authorize: vi.fn(() => Promise.resolve("http://google.com/auth")),
|
||||
}));
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
appSetupCompleted: false,
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockSurveys: TSurvey[] = [];
|
||||
const mockWebAppUrl = "http://localhost:3000";
|
||||
const mockLocale = "en-US";
|
||||
|
||||
const mockGoogleSheetIntegration = {
|
||||
id: "test-integration-id",
|
||||
type: "googleSheets",
|
||||
config: {
|
||||
key: { access_token: "test-token" } as unknown as TIntegrationGoogleSheetsCredential,
|
||||
data: [],
|
||||
email: "test@example.com",
|
||||
},
|
||||
} as unknown as TIntegrationGoogleSheets;
|
||||
|
||||
describe("GoogleSheetWrapper", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when not connected", () => {
|
||||
render(
|
||||
<GoogleSheetWrapper
|
||||
isEnabled={true}
|
||||
environment={mockEnvironment}
|
||||
surveys={mockSurveys}
|
||||
webAppUrl={mockWebAppUrl}
|
||||
locale={mockLocale}
|
||||
// No googleSheetIntegration provided initially
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("add-integration-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when integration exists but has no key", () => {
|
||||
const integrationWithoutKey = {
|
||||
...mockGoogleSheetIntegration,
|
||||
config: { data: [], email: "test" },
|
||||
} as unknown as TIntegrationGoogleSheets;
|
||||
render(
|
||||
<GoogleSheetWrapper
|
||||
isEnabled={true}
|
||||
environment={mockEnvironment}
|
||||
surveys={mockSurveys}
|
||||
googleSheetIntegration={integrationWithoutKey}
|
||||
webAppUrl={mockWebAppUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls authorize when connect button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
// Mock window.location.replace
|
||||
const originalLocation = window.location;
|
||||
// @ts-expect-error
|
||||
delete window.location;
|
||||
window.location = { ...originalLocation, replace: vi.fn() } as any;
|
||||
|
||||
render(
|
||||
<GoogleSheetWrapper
|
||||
isEnabled={true}
|
||||
environment={mockEnvironment}
|
||||
surveys={mockSurveys}
|
||||
webAppUrl={mockWebAppUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
const connectButton = screen.getByRole("button", { name: "Connect" });
|
||||
await user.click(connectButton);
|
||||
|
||||
expect(vi.mocked(authorize)).toHaveBeenCalledWith(mockEnvironment.id, mockWebAppUrl);
|
||||
// Need to wait for the promise returned by authorize to resolve
|
||||
await vi.waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith("http://google.com/auth");
|
||||
});
|
||||
|
||||
// Restore window.location
|
||||
window.location = originalLocation as any;
|
||||
});
|
||||
|
||||
test("renders ManageIntegration and AddIntegrationModal when connected", () => {
|
||||
render(
|
||||
<GoogleSheetWrapper
|
||||
isEnabled={true}
|
||||
environment={mockEnvironment}
|
||||
surveys={mockSurveys}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
webAppUrl={mockWebAppUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("manage-integration")).toBeInTheDocument();
|
||||
// Modal is rendered but initially hidden
|
||||
expect(screen.queryByTestId("add-integration-modal")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("connect-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("opens AddIntegrationModal when triggered from ManageIntegration", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<GoogleSheetWrapper
|
||||
isEnabled={true}
|
||||
environment={mockEnvironment}
|
||||
surveys={mockSurveys}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
webAppUrl={mockWebAppUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("add-integration-modal")).not.toBeInTheDocument();
|
||||
const openModalButton = screen.getByRole("button", { name: "Open Modal" }); // Button inside mocked ManageIntegration
|
||||
await user.click(openModalButton);
|
||||
expect(screen.getByTestId("add-integration-modal")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { authorize } from "./google";
|
||||
|
||||
// Mock the logger
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
const mockFetch = vi.fn();
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
describe("authorize", () => {
|
||||
const environmentId = "test-env-id";
|
||||
const apiHost = "http://test.com";
|
||||
const expectedUrl = `${apiHost}/api/google-sheet`;
|
||||
const expectedHeaders = { environmentId: environmentId };
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should return authUrl on successful fetch", async () => {
|
||||
const mockAuthUrl = "https://accounts.google.com/o/oauth2/v2/auth?...";
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { authUrl: mockAuthUrl } }),
|
||||
});
|
||||
|
||||
const authUrl = await authorize(environmentId, apiHost);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: expectedHeaders,
|
||||
});
|
||||
expect(authUrl).toBe(mockAuthUrl);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should throw error and log on failed fetch", async () => {
|
||||
const errorText = "Failed to fetch";
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
text: async () => errorText,
|
||||
});
|
||||
|
||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: expectedHeaders,
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ errorText },
|
||||
"authorize: Could not fetch google sheet config"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { constructGoogleSheetsUrl, extractSpreadsheetIdFromUrl, isValidGoogleSheetsUrl } from "./util";
|
||||
|
||||
describe("Google Sheets Util", () => {
|
||||
describe("extractSpreadsheetIdFromUrl", () => {
|
||||
test("should extract spreadsheet ID from a valid URL", () => {
|
||||
const url =
|
||||
"https://docs.google.com/spreadsheets/d/1aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPq/edit#gid=0";
|
||||
const expectedId = "1aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPq";
|
||||
expect(extractSpreadsheetIdFromUrl(url)).toBe(expectedId);
|
||||
});
|
||||
|
||||
test("should throw an error for an invalid URL", () => {
|
||||
const invalidUrl = "https://not-a-google-sheet-url.com";
|
||||
expect(() => extractSpreadsheetIdFromUrl(invalidUrl)).toThrow("Invalid Google Sheets URL");
|
||||
});
|
||||
|
||||
test("should throw an error for a URL without an ID", () => {
|
||||
const urlWithoutId = "https://docs.google.com/spreadsheets/d/";
|
||||
expect(() => extractSpreadsheetIdFromUrl(urlWithoutId)).toThrow("Invalid Google Sheets URL");
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructGoogleSheetsUrl", () => {
|
||||
test("should construct a valid Google Sheets URL from a spreadsheet ID", () => {
|
||||
const spreadsheetId = "1aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPq";
|
||||
const expectedUrl =
|
||||
"https://docs.google.com/spreadsheets/d/1aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPq";
|
||||
expect(constructGoogleSheetsUrl(spreadsheetId)).toBe(expectedUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidGoogleSheetsUrl", () => {
|
||||
test("should return true for a valid Google Sheets URL", () => {
|
||||
const validUrl =
|
||||
"https://docs.google.com/spreadsheets/d/1aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPq/edit#gid=0";
|
||||
expect(isValidGoogleSheetsUrl(validUrl)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for an invalid URL", () => {
|
||||
const invalidUrl = "https://not-a-google-sheet-url.com";
|
||||
expect(isValidGoogleSheetsUrl(invalidUrl)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true for a base Google Sheets URL", () => {
|
||||
const baseUrl = "https://docs.google.com/spreadsheets/d/";
|
||||
expect(isValidGoogleSheetsUrl(baseUrl)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Loading from "./loading";
|
||||
|
||||
// Mock the GoBackButton component
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: () => <div>GoBackButton</div>,
|
||||
}));
|
||||
|
||||
describe("Loading", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the loading state correctly", () => {
|
||||
render(<Loading />);
|
||||
|
||||
// Check for GoBackButton mock
|
||||
expect(screen.getByText("GoBackButton")).toBeInTheDocument();
|
||||
|
||||
// Check for the disabled button text
|
||||
expect(screen.getByText("environments.integrations.google_sheets.link_new_sheet")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.google_sheets.link_new_sheet").closest("button")
|
||||
).toHaveClass("pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none");
|
||||
|
||||
// Check for table headers
|
||||
expect(screen.getByText("common.survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.google_sheets.google_sheet_name")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.questions")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.updated_at")).toBeInTheDocument();
|
||||
|
||||
// Check for placeholder elements (count based on the loop)
|
||||
const placeholders = screen.getAllByRole("generic", { hidden: true }); // Using generic role as divs don't have implicit roles
|
||||
// Calculate expected placeholders: 3 rows * 5 placeholders per row = 15
|
||||
// Plus the button, header divs (4), and the main containers
|
||||
// It's simpler to check if there are *any* pulse animations
|
||||
expect(placeholders.some((el) => el.classList.contains("animate-pulse"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,228 +0,0 @@
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/page";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsCredential,
|
||||
} from "@formbricks/types/integration/google-sheet";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper",
|
||||
() => ({
|
||||
GoogleSheetWrapper: vi.fn(
|
||||
({ isEnabled, environment, surveys, googleSheetIntegration, webAppUrl, locale }) => (
|
||||
<div>
|
||||
<span>Mocked GoogleSheetWrapper</span>
|
||||
<span data-testid="isEnabled">{isEnabled.toString()}</span>
|
||||
<span data-testid="environmentId">{environment.id}</span>
|
||||
<span data-testid="surveyCount">{surveys?.length ?? 0}</span>
|
||||
<span data-testid="integrationId">{googleSheetIntegration?.id}</span>
|
||||
<span data-testid="webAppUrl">{webAppUrl}</span>
|
||||
<span data-testid="locale">{locale}</span>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
})
|
||||
);
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys", () => ({
|
||||
getSurveys: vi.fn(),
|
||||
}));
|
||||
|
||||
let mockGoogleSheetClientId: string | undefined = "test-client-id";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
get GOOGLE_SHEETS_CLIENT_ID() {
|
||||
return mockGoogleSheetClientId;
|
||||
},
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: "test-client-secret",
|
||||
GOOGLE_SHEETS_REDIRECT_URL: "test-redirect-url",
|
||||
}));
|
||||
vi.mock("@/lib/integration/service", () => ({
|
||||
getIntegrations: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/utils/locale", () => ({
|
||||
findMatchingLocale: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: vi.fn(({ url }) => <div data-testid="go-back">{url}</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: vi.fn(({ children }) => <div>{children}</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: vi.fn(({ pageTitle }) => <h1>{pageTitle}</h1>),
|
||||
}));
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
appSetupCompleted: false,
|
||||
type: "development",
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockSurveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
name: "Survey 1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "test-env-id",
|
||||
status: "inProgress",
|
||||
type: "app",
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
languages: [],
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
segment: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const mockGoogleSheetIntegration = {
|
||||
id: "integration1",
|
||||
type: "googleSheets",
|
||||
config: {
|
||||
data: [],
|
||||
key: {
|
||||
refresh_token: "refresh",
|
||||
access_token: "access",
|
||||
expiry_date: Date.now() + 3600000,
|
||||
} as unknown as TIntegrationGoogleSheetsCredential,
|
||||
email: "test@example.com",
|
||||
},
|
||||
} as unknown as TIntegrationGoogleSheets;
|
||||
|
||||
const mockProps = {
|
||||
params: { environmentId: "test-env-id" },
|
||||
};
|
||||
|
||||
describe("GoogleSheetsIntegrationPage", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
} as TEnvironmentAuth);
|
||||
vi.mocked(getSurveys).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getIntegrations).mockResolvedValue([mockGoogleSheetIntegration]);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
|
||||
});
|
||||
|
||||
test("renders the page with GoogleSheetWrapper when enabled and not read-only", async () => {
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(
|
||||
screen.getByText("environments.integrations.google_sheets.google_sheets_integration")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Mocked GoogleSheetWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("isEnabled")).toHaveTextContent("true");
|
||||
expect(screen.getByTestId("environmentId")).toHaveTextContent(mockEnvironment.id);
|
||||
expect(screen.getByTestId("surveyCount")).toHaveTextContent(mockSurveys.length.toString());
|
||||
expect(screen.getByTestId("integrationId")).toHaveTextContent(mockGoogleSheetIntegration.id);
|
||||
expect(screen.getByTestId("webAppUrl")).toHaveTextContent("test-webapp-url");
|
||||
expect(screen.getByTestId("locale")).toHaveTextContent("en-US");
|
||||
expect(screen.getByTestId("go-back")).toHaveTextContent(
|
||||
`test-webapp-url/environments/${mockProps.params.environmentId}/integrations`
|
||||
);
|
||||
expect(vi.mocked(redirect)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls redirect when user is read-only", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: true,
|
||||
} as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(vi.mocked(redirect)).toHaveBeenCalledWith("./");
|
||||
});
|
||||
|
||||
test("passes isEnabled=false to GoogleSheetWrapper when constants are missing", async () => {
|
||||
mockGoogleSheetClientId = undefined;
|
||||
|
||||
const { default: PageWithMissingConstants } = (await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/page"
|
||||
)) as { default: typeof Page };
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
} as TEnvironmentAuth);
|
||||
vi.mocked(getSurveys).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getIntegrations).mockResolvedValue([mockGoogleSheetIntegration]);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
|
||||
|
||||
const PageComponent = await PageWithMissingConstants(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByTestId("isEnabled")).toHaveTextContent("false");
|
||||
});
|
||||
|
||||
test("handles case where no Google Sheet integration exists", async () => {
|
||||
vi.mocked(getIntegrations).mockResolvedValue([]); // No integrations
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("Mocked GoogleSheetWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("integrationId")).toBeEmptyDOMElement(); // No integration ID passed
|
||||
});
|
||||
});
|
||||
@@ -1,172 +0,0 @@
|
||||
import { cache } from "@/lib/cache";
|
||||
import { surveyCache } from "@/lib/survey/cache";
|
||||
import { selectSurvey } from "@/lib/survey/service";
|
||||
import { transformPrismaSurvey } from "@/lib/survey/utils";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getSurveys } from "./surveys";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/cache");
|
||||
vi.mock("@/lib/survey/cache", () => ({
|
||||
surveyCache: {
|
||||
tag: {
|
||||
byEnvironmentId: vi.fn((environmentId) => `survey_environment_${environmentId}`),
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/survey/service", () => ({
|
||||
selectSurvey: { id: true, name: true, status: true, updatedAt: true }, // Expanded mock based on usage
|
||||
}));
|
||||
vi.mock("@/lib/survey/utils");
|
||||
vi.mock("@/lib/utils/validate");
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
survey: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("react", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("react")>();
|
||||
return {
|
||||
...actual,
|
||||
cache: vi.fn((fn) => fn), // Mock reactCache to just return the function
|
||||
};
|
||||
});
|
||||
|
||||
const environmentId = "test-environment-id";
|
||||
// Ensure mockPrismaSurveys includes all fields used in selectSurvey mock
|
||||
const mockPrismaSurveys = [
|
||||
{ id: "survey1", name: "Survey 1", status: "inProgress", updatedAt: new Date() },
|
||||
{ id: "survey2", name: "Survey 2", status: "draft", updatedAt: new Date() },
|
||||
];
|
||||
const mockTransformedSurveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
name: "Survey 1",
|
||||
status: "inProgress",
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: false },
|
||||
type: "app", // Changed type to web to match original file
|
||||
environmentId: environmentId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
languages: [],
|
||||
styling: null,
|
||||
} as unknown as TSurvey,
|
||||
{
|
||||
id: "survey2",
|
||||
name: "Survey 2",
|
||||
status: "draft",
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: false },
|
||||
type: "app",
|
||||
environmentId: environmentId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
languages: [],
|
||||
styling: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
describe("getSurveys", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(cache).mockImplementation((fn) => async () => {
|
||||
return fn();
|
||||
});
|
||||
});
|
||||
|
||||
test("should fetch and transform surveys successfully", async () => {
|
||||
vi.mocked(prisma.survey.findMany).mockResolvedValue(mockPrismaSurveys);
|
||||
vi.mocked(transformPrismaSurvey).mockImplementation((survey) => {
|
||||
const found = mockTransformedSurveys.find((ts) => ts.id === survey.id);
|
||||
if (!found) throw new Error("Survey not found in mock transformed data");
|
||||
// Ensure the returned object matches the TSurvey structure precisely
|
||||
return { ...found } as TSurvey;
|
||||
});
|
||||
|
||||
const surveys = await getSurveys(environmentId);
|
||||
|
||||
expect(surveys).toEqual(mockTransformedSurveys);
|
||||
// Use expect.any(ZId) for the Zod schema validation check
|
||||
expect(validateInputs).toHaveBeenCalledWith([environmentId, expect.any(Object)]); // Adjusted expectation
|
||||
expect(prisma.survey.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
environmentId,
|
||||
status: {
|
||||
not: "completed",
|
||||
},
|
||||
},
|
||||
select: selectSurvey,
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
});
|
||||
expect(transformPrismaSurvey).toHaveBeenCalledTimes(mockPrismaSurveys.length);
|
||||
expect(transformPrismaSurvey).toHaveBeenCalledWith(mockPrismaSurveys[0]);
|
||||
expect(transformPrismaSurvey).toHaveBeenCalledWith(mockPrismaSurveys[1]);
|
||||
// Check if the inner cache function was called with the correct arguments
|
||||
expect(cache).toHaveBeenCalledWith(
|
||||
expect.any(Function), // The async function passed to cache
|
||||
[`getSurveys-${environmentId}`], // The cache key
|
||||
{
|
||||
tags: [surveyCache.tag.byEnvironmentId(environmentId)], // Cache tags
|
||||
}
|
||||
);
|
||||
// Remove the assertion for reactCache being called within the test execution
|
||||
// expect(reactCache).toHaveBeenCalled(); // Removed this line
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on Prisma known request error", async () => {
|
||||
// No need to mock cache here again as beforeEach handles it
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Test error", {
|
||||
code: "P2025",
|
||||
clientVersion: "5.0.0",
|
||||
meta: {}, // Added meta property
|
||||
});
|
||||
vi.mocked(prisma.survey.findMany).mockRejectedValue(prismaError);
|
||||
|
||||
await expect(getSurveys(environmentId)).rejects.toThrow(DatabaseError);
|
||||
expect(logger.error).toHaveBeenCalledWith({ error: prismaError }, "getSurveys: Could not fetch surveys");
|
||||
expect(cache).toHaveBeenCalled(); // Ensure cache wrapper was still called
|
||||
});
|
||||
|
||||
test("should throw original error on other errors", async () => {
|
||||
// No need to mock cache here again as beforeEach handles it
|
||||
const genericError = new Error("Something went wrong");
|
||||
vi.mocked(prisma.survey.findMany).mockRejectedValue(genericError);
|
||||
|
||||
await expect(getSurveys(environmentId)).rejects.toThrow(genericError);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
expect(cache).toHaveBeenCalled(); // Ensure cache wrapper was still called
|
||||
});
|
||||
});
|
||||
@@ -1,114 +0,0 @@
|
||||
import { cache } from "@/lib/cache";
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { getWebhookCountBySource } from "./webhook";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/cache");
|
||||
vi.mock("@/lib/cache/webhook", () => ({
|
||||
webhookCache: {
|
||||
tag: {
|
||||
byEnvironmentIdAndSource: vi.fn((envId, source) => `webhook_${envId}_${source ?? "all"}`),
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/utils/validate");
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
webhook: {
|
||||
count: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const environmentId = "test-environment-id";
|
||||
const sourceZapier = "zapier";
|
||||
|
||||
describe("getWebhookCountBySource", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(cache).mockImplementation((fn) => async () => {
|
||||
return fn();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("should return webhook count for a specific source", async () => {
|
||||
const mockCount = 5;
|
||||
vi.mocked(prisma.webhook.count).mockResolvedValue(mockCount);
|
||||
|
||||
const count = await getWebhookCountBySource(environmentId, sourceZapier);
|
||||
|
||||
expect(count).toBe(mockCount);
|
||||
expect(validateInputs).toHaveBeenCalledWith(
|
||||
[environmentId, expect.any(Object)],
|
||||
[sourceZapier, expect.any(Object)]
|
||||
);
|
||||
expect(prisma.webhook.count).toHaveBeenCalledWith({
|
||||
where: {
|
||||
environmentId,
|
||||
source: sourceZapier,
|
||||
},
|
||||
});
|
||||
expect(cache).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
[`getWebhookCountBySource-${environmentId}-${sourceZapier}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, sourceZapier)],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("should return total webhook count when source is undefined", async () => {
|
||||
const mockCount = 10;
|
||||
vi.mocked(prisma.webhook.count).mockResolvedValue(mockCount);
|
||||
|
||||
const count = await getWebhookCountBySource(environmentId);
|
||||
|
||||
expect(count).toBe(mockCount);
|
||||
expect(validateInputs).toHaveBeenCalledWith(
|
||||
[environmentId, expect.any(Object)],
|
||||
[undefined, expect.any(Object)]
|
||||
);
|
||||
expect(prisma.webhook.count).toHaveBeenCalledWith({
|
||||
where: {
|
||||
environmentId,
|
||||
source: undefined,
|
||||
},
|
||||
});
|
||||
expect(cache).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
[`getWebhookCountBySource-${environmentId}-undefined`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, undefined)],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on Prisma known request error", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
|
||||
code: "P2025",
|
||||
clientVersion: "5.0.0",
|
||||
});
|
||||
vi.mocked(prisma.webhook.count).mockRejectedValue(prismaError);
|
||||
|
||||
await expect(getWebhookCountBySource(environmentId, sourceZapier)).rejects.toThrow(DatabaseError);
|
||||
expect(prisma.webhook.count).toHaveBeenCalledTimes(1);
|
||||
expect(cache).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should throw original error on other errors", async () => {
|
||||
const genericError = new Error("Something went wrong");
|
||||
vi.mocked(prisma.webhook.count).mockRejectedValue(genericError);
|
||||
|
||||
await expect(getWebhookCountBySource(environmentId)).rejects.toThrow(genericError);
|
||||
expect(prisma.webhook.count).toHaveBeenCalledTimes(1);
|
||||
expect(cache).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,606 +0,0 @@
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/AddIntegrationModal";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
TIntegrationNotion,
|
||||
TIntegrationNotionConfigData,
|
||||
TIntegrationNotionCredential,
|
||||
TIntegrationNotionDatabase,
|
||||
} from "@formbricks/types/integration/notion";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock actions and utilities
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: (value: any, _locale: string) => value?.default || "",
|
||||
}));
|
||||
vi.mock("@/lib/pollyfills/structuredClone", () => ({
|
||||
structuredClone: (obj: any) => JSON.parse(JSON.stringify(obj)),
|
||||
}));
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
replaceHeadlineRecall: (survey: any) => survey,
|
||||
}));
|
||||
vi.mock("@/modules/survey/lib/questions", () => ({
|
||||
getQuestionTypes: () => [
|
||||
{ id: TSurveyQuestionTypeEnum.OpenText, label: "Open Text" },
|
||||
{ id: TSurveyQuestionTypeEnum.MultipleChoiceSingle, label: "Multiple Choice Single" },
|
||||
{ id: TSurveyQuestionTypeEnum.Date, label: "Date" },
|
||||
],
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, onClick, loading, variant, type = "button" }: any) => (
|
||||
<button onClick={onClick} disabled={loading} data-variant={variant} type={type}>
|
||||
{loading ? "Loading..." : children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/dropdown-selector", () => ({
|
||||
DropdownSelector: ({ label, items, selectedItem, setSelectedItem, placeholder, disabled }: any) => {
|
||||
// Ensure the selected item is always available as an option
|
||||
const allOptions = [...items];
|
||||
if (selectedItem && !items.some((item: any) => item.id === selectedItem.id)) {
|
||||
// Use a simple object structure consistent with how options are likely used
|
||||
allOptions.push({ id: selectedItem.id, name: selectedItem.name });
|
||||
}
|
||||
// Remove duplicates just in case
|
||||
const uniqueOptions = Array.from(new Map(allOptions.map((item) => [item.id, item])).values());
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && <label>{label}</label>}
|
||||
<select
|
||||
data-testid={`dropdown-${label?.toLowerCase().replace(/\s+/g, "-") || placeholder?.toLowerCase().replace(/\s+/g, "-")}`}
|
||||
value={selectedItem?.id || ""} // Still set value based on selectedItem prop
|
||||
onChange={(e) => {
|
||||
const selected = uniqueOptions.find((item: any) => item.id === e.target.value);
|
||||
setSelectedItem(selected);
|
||||
}}
|
||||
disabled={disabled}>
|
||||
<option value="">{placeholder || "Select..."}</option>
|
||||
{/* Render options from the potentially augmented list */}
|
||||
{uniqueOptions.map((item: any) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/label", () => ({
|
||||
Label: ({ children }: { children: React.ReactNode }) => <label>{children}</label>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: ({ open, children }: { open: boolean; children: React.ReactNode }) =>
|
||||
open ? <div data-testid="modal">{children}</div> : null,
|
||||
}));
|
||||
vi.mock("lucide-react", () => ({
|
||||
PlusIcon: () => <span data-testid="plus-icon">+</span>,
|
||||
XIcon: () => <span data-testid="x-icon">x</span>,
|
||||
}));
|
||||
vi.mock("next/image", () => ({
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
|
||||
}));
|
||||
vi.mock("react-hook-form", () => ({
|
||||
useForm: () => ({
|
||||
handleSubmit: (callback: any) => (event: any) => {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@tolgee/react", async () => {
|
||||
const MockTolgeeProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
const useTranslate = () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
// NOSONAR
|
||||
// Simple mock translation function
|
||||
if (key === "common.warning") return "Warning";
|
||||
if (key === "common.metadata") return "Metadata";
|
||||
if (key === "common.created_at") return "Created at";
|
||||
if (key === "common.hidden_field") return "Hidden Field";
|
||||
if (key === "environments.integrations.notion.link_notion_database") return "Link Notion Database";
|
||||
if (key === "environments.integrations.notion.sync_responses_with_a_notion_database")
|
||||
return "Sync responses with a Notion database.";
|
||||
if (key === "environments.integrations.notion.select_a_database") return "Select a database";
|
||||
if (key === "common.select_survey") return "Select survey";
|
||||
if (key === "environments.integrations.notion.map_formbricks_fields_to_notion_property")
|
||||
return "Map Formbricks fields to Notion property";
|
||||
if (key === "environments.integrations.notion.select_a_survey_question")
|
||||
return "Select a survey question";
|
||||
if (key === "environments.integrations.notion.select_a_field_to_map") return "Select a field to map";
|
||||
if (key === "common.delete") return "Delete";
|
||||
if (key === "common.cancel") return "Cancel";
|
||||
if (key === "common.update") return "Update";
|
||||
if (key === "environments.integrations.notion.please_select_a_database")
|
||||
return "Please select a database.";
|
||||
if (key === "environments.integrations.please_select_a_survey_error") return "Please select a survey.";
|
||||
if (key === "environments.integrations.notion.please_select_at_least_one_mapping")
|
||||
return "Please select at least one mapping.";
|
||||
if (key === "environments.integrations.notion.please_resolve_mapping_errors")
|
||||
return "Please resolve mapping errors.";
|
||||
if (key === "environments.integrations.notion.please_complete_mapping_fields_with_notion_property")
|
||||
return "Please complete mapping fields.";
|
||||
if (key === "environments.integrations.integration_updated_successfully")
|
||||
return "Integration updated successfully.";
|
||||
if (key === "environments.integrations.integration_added_successfully")
|
||||
return "Integration added successfully.";
|
||||
if (key === "environments.integrations.integration_removed_successfully")
|
||||
return "Integration removed successfully.";
|
||||
if (key === "environments.integrations.notion.notion_logo") return "Notion logo";
|
||||
if (key === "environments.integrations.create_survey_warning")
|
||||
return "You need to create a survey first.";
|
||||
if (key === "environments.integrations.notion.create_at_least_one_database_to_setup_this_integration")
|
||||
return "Create at least one database.";
|
||||
if (key === "environments.integrations.notion.duplicate_connection_warning")
|
||||
return "Duplicate connection warning.";
|
||||
if (key === "environments.integrations.notion.que_name_of_type_cant_be_mapped_to")
|
||||
return `Question ${params.que_name} (${params.question_label}) can't be mapped to ${params.col_name} (${params.col_type}). Allowed types: ${params.mapped_type}`;
|
||||
|
||||
return key; // Return key if no translation is found
|
||||
},
|
||||
});
|
||||
return { TolgeeProvider: MockTolgeeProvider, useTranslate };
|
||||
});
|
||||
|
||||
// Mock dependencies
|
||||
const createOrUpdateIntegrationAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/integrations/actions"))
|
||||
.createOrUpdateIntegrationAction
|
||||
);
|
||||
const toast = vi.mocked((await import("react-hot-toast")).default);
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const mockSetOpen = vi.fn();
|
||||
|
||||
const surveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 1",
|
||||
type: "app",
|
||||
environmentId: environmentId,
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1?" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 2?" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Choice 1" } },
|
||||
{ id: "c2", label: { default: "Choice 2" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
variables: [{ id: "var1", name: "Variable 1" }],
|
||||
hiddenFields: { enabled: true, fieldIds: ["hf1"] },
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
{
|
||||
id: "survey2",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 2",
|
||||
type: "link",
|
||||
environmentId: environmentId,
|
||||
status: "draft",
|
||||
questions: [
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyQuestionTypeEnum.Date,
|
||||
headline: { default: "Date Question?" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
variables: [],
|
||||
hiddenFields: { enabled: false },
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const databases: TIntegrationNotionDatabase[] = [
|
||||
{
|
||||
id: "db1",
|
||||
name: "Database 1 Title",
|
||||
properties: {
|
||||
prop1: { id: "p1", name: "Title Prop", type: "title" },
|
||||
prop2: { id: "p2", name: "Text Prop", type: "rich_text" },
|
||||
prop3: { id: "p3", name: "Number Prop", type: "number" },
|
||||
prop4: { id: "p4", name: "Date Prop", type: "date" },
|
||||
prop5: { id: "p5", name: "Unsupported Prop", type: "formula" }, // Unsupported
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "db2",
|
||||
name: "Database 2 Title",
|
||||
properties: {
|
||||
propA: { id: "pa", name: "Name", type: "title" },
|
||||
propB: { id: "pb", name: "Email", type: "email" },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockNotionIntegration: TIntegrationNotion = {
|
||||
id: "integration1",
|
||||
type: "notion",
|
||||
environmentId: environmentId,
|
||||
config: {
|
||||
key: {
|
||||
access_token: "token",
|
||||
bot_id: "bot",
|
||||
workspace_name: "ws",
|
||||
workspace_icon: "",
|
||||
} as unknown as TIntegrationNotionCredential,
|
||||
data: [], // Initially empty
|
||||
},
|
||||
};
|
||||
|
||||
const mockSelectedIntegration: TIntegrationNotionConfigData & { index: number } = {
|
||||
databaseId: databases[0].id,
|
||||
databaseName: databases[0].name,
|
||||
surveyId: surveys[0].id,
|
||||
surveyName: surveys[0].name,
|
||||
mapping: [
|
||||
{
|
||||
column: { id: "p1", name: "Title Prop", type: "title" },
|
||||
question: { id: "q1", name: "Question 1?", type: TSurveyQuestionTypeEnum.OpenText },
|
||||
},
|
||||
{
|
||||
column: { id: "p2", name: "Text Prop", type: "rich_text" },
|
||||
question: { id: "var1", name: "Variable 1", type: TSurveyQuestionTypeEnum.OpenText },
|
||||
},
|
||||
],
|
||||
createdAt: new Date(),
|
||||
index: 0,
|
||||
};
|
||||
|
||||
describe("AddIntegrationModal (Notion)", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset integration data before each test if needed
|
||||
mockNotionIntegration.config.data = [
|
||||
{ ...mockSelectedIntegration }, // Simulate existing data for update/delete tests
|
||||
];
|
||||
});
|
||||
|
||||
test("renders correctly when open (create mode)", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.notion.link_database")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-select-a-database")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-select-survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "environments.integrations.notion.link_database" })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText("Delete")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Map Formbricks fields to Notion property")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly when open (update mode)", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={mockNotionIntegration}
|
||||
databases={databases}
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-select-a-database")).toHaveValue(databases[0].id);
|
||||
expect(screen.getByTestId("dropdown-select-survey")).toHaveValue(surveys[0].id);
|
||||
expect(screen.getByText("Map Formbricks fields to Notion property")).toBeInTheDocument();
|
||||
|
||||
// Check if mapping rows are rendered
|
||||
await waitFor(() => {
|
||||
const questionDropdowns = screen.getAllByTestId("dropdown-select-a-survey-question");
|
||||
const columnDropdowns = screen.getAllByTestId("dropdown-select-a-field-to-map");
|
||||
|
||||
expect(questionDropdowns).toHaveLength(2); // Expecting two rows based on mockSelectedIntegration
|
||||
expect(columnDropdowns).toHaveLength(2);
|
||||
|
||||
// Assert values for the first row
|
||||
expect(questionDropdowns[0]).toHaveValue("q1");
|
||||
expect(columnDropdowns[0]).toHaveValue("p1");
|
||||
|
||||
// Assert values for the second row
|
||||
expect(questionDropdowns[1]).toHaveValue("var1");
|
||||
expect(columnDropdowns[1]).toHaveValue("p2");
|
||||
|
||||
expect(screen.getAllByTestId("plus-icon").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByTestId("x-icon").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(screen.getByText("Delete")).toBeInTheDocument();
|
||||
expect(screen.getByText("Update")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("selects database and survey, shows mapping", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const dbDropdown = screen.getByTestId("dropdown-select-a-database");
|
||||
const surveyDropdown = screen.getByTestId("dropdown-select-survey");
|
||||
|
||||
await userEvent.selectOptions(dbDropdown, databases[0].id);
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
expect(screen.getByText("Map Formbricks fields to Notion property")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-select-a-survey-question")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-select-a-field-to-map")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("adds and removes mapping rows", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const dbDropdown = screen.getByTestId("dropdown-select-a-database");
|
||||
const surveyDropdown = screen.getByTestId("dropdown-select-survey");
|
||||
|
||||
await userEvent.selectOptions(dbDropdown, databases[0].id);
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
expect(screen.getAllByTestId("dropdown-select-a-survey-question")).toHaveLength(1);
|
||||
|
||||
const plusButton = screen.getByTestId("plus-icon");
|
||||
await userEvent.click(plusButton);
|
||||
|
||||
expect(screen.getAllByTestId("dropdown-select-a-survey-question")).toHaveLength(2);
|
||||
|
||||
const xButton = screen.getAllByTestId("x-icon")[0]; // Get the first X button
|
||||
await userEvent.click(xButton);
|
||||
|
||||
expect(screen.getAllByTestId("dropdown-select-a-survey-question")).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("deletes integration successfully", async () => {
|
||||
createOrUpdateIntegrationAction.mockResolvedValue({ data: null as any });
|
||||
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={mockNotionIntegration} // Contains initial data at index 0
|
||||
databases={databases}
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteButton = screen.getByText("Delete");
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationAction).toHaveBeenCalledWith({
|
||||
environmentId,
|
||||
integrationData: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
data: [], // Data array should be empty after deletion
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Integration removed successfully.");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("shows validation error if no database selected", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
await userEvent.selectOptions(screen.getByTestId("dropdown-select-survey"), surveys[0].id);
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "environments.integrations.notion.link_database" })
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select a database.");
|
||||
});
|
||||
});
|
||||
|
||||
test("shows validation error if no survey selected", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
await userEvent.selectOptions(screen.getByTestId("dropdown-select-a-database"), databases[0].id);
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "environments.integrations.notion.link_database" })
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select a survey.");
|
||||
});
|
||||
});
|
||||
|
||||
test("shows validation error if no mapping defined", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
await userEvent.selectOptions(screen.getByTestId("dropdown-select-a-database"), databases[0].id);
|
||||
await userEvent.selectOptions(screen.getByTestId("dropdown-select-survey"), surveys[0].id);
|
||||
// Default mapping row is empty
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "environments.integrations.notion.link_database" })
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select at least one mapping.");
|
||||
});
|
||||
});
|
||||
|
||||
test("calls setOpen(false) and resets form on cancel", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const dbDropdown = screen.getByTestId("dropdown-select-a-database");
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
|
||||
await userEvent.selectOptions(dbDropdown, databases[0].id); // Simulate interaction
|
||||
await userEvent.click(cancelButton);
|
||||
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
// Re-render with open=true to check if state was reset
|
||||
cleanup();
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("dropdown-select-a-database")).toHaveValue(""); // Should be reset
|
||||
});
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/notion/lib/notion";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationNotion, TIntegrationNotionCredential } from "@formbricks/types/integration/notion";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { NotionWrapper } from "./NotionWrapper";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: "test-client-secret",
|
||||
GOOGLE_SHEETS_REDIRECT_URL: "test-redirect-url",
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration", () => ({
|
||||
ManageIntegration: vi.fn(({ setIsConnected }) => (
|
||||
<div data-testid="manage-integration">
|
||||
<button onClick={() => setIsConnected(false)}>Disconnect</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
ConnectIntegration: vi.fn(
|
||||
(
|
||||
{ handleAuthorization, isEnabled } // Reverted back to isEnabled
|
||||
) => (
|
||||
<div data-testid="connect-integration">
|
||||
<button onClick={handleAuthorization} disabled={!isEnabled}>
|
||||
{" "}
|
||||
{/* Reverted back to isEnabled */}
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock library function
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/notion/lib/notion", () => ({
|
||||
authorize: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock image import
|
||||
vi.mock("@/images/notion-logo.svg", () => ({
|
||||
default: "notion-logo-path",
|
||||
}));
|
||||
|
||||
// Mock window.location.replace
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
replace: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const webAppUrl = "https://app.formbricks.com";
|
||||
const environment = { id: environmentId } as TEnvironment;
|
||||
const surveys: TSurvey[] = [];
|
||||
const databases = [];
|
||||
const locale = "en-US" as const;
|
||||
|
||||
const mockNotionIntegration: TIntegrationNotion = {
|
||||
id: "int-notion-123",
|
||||
type: "notion",
|
||||
environmentId: environmentId,
|
||||
config: {
|
||||
key: { access_token: "test-token" } as TIntegrationNotionCredential,
|
||||
data: [],
|
||||
},
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
environment,
|
||||
surveys,
|
||||
databasesArray: databases, // Renamed databases to databasesArray to match component prop
|
||||
webAppUrl,
|
||||
locale,
|
||||
};
|
||||
|
||||
describe("NotionWrapper", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration disabled when enabled is false", () => {
|
||||
// Changed description slightly
|
||||
render(<NotionWrapper {...baseProps} enabled={false} notionIntegration={undefined} />); // Changed isEnabled to enabled
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeDisabled();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration enabled when enabled is true and not connected (no integration)", () => {
|
||||
// Changed description slightly
|
||||
render(<NotionWrapper {...baseProps} enabled={true} notionIntegration={undefined} />); // Changed isEnabled to enabled
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration enabled when enabled is true and not connected (integration without key)", () => {
|
||||
// Changed description slightly
|
||||
const integrationWithoutKey = {
|
||||
...mockNotionIntegration,
|
||||
config: { data: [] },
|
||||
} as unknown as TIntegrationNotion;
|
||||
render(<NotionWrapper {...baseProps} enabled={true} notionIntegration={integrationWithoutKey} />); // Changed isEnabled to enabled
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls authorize and redirects when Connect button is clicked", async () => {
|
||||
const mockAuthorize = vi.mocked(authorize);
|
||||
const redirectUrl = "https://notion.com/auth";
|
||||
mockAuthorize.mockResolvedValue(redirectUrl);
|
||||
|
||||
render(<NotionWrapper {...baseProps} enabled={true} notionIntegration={undefined} />); // Changed isEnabled to enabled
|
||||
|
||||
const connectButton = screen.getByRole("button", { name: "Connect" });
|
||||
await userEvent.click(connectButton);
|
||||
|
||||
expect(mockAuthorize).toHaveBeenCalledWith(environmentId, webAppUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith(redirectUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { authorize } from "./notion";
|
||||
|
||||
// Mock the logger
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
const mockFetch = vi.fn();
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
describe("authorize", () => {
|
||||
const environmentId = "test-env-id";
|
||||
const apiHost = "http://test.com";
|
||||
const expectedUrl = `${apiHost}/api/v1/integrations/notion`;
|
||||
const expectedHeaders = { environmentId: environmentId };
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should return authUrl on successful fetch", async () => {
|
||||
const mockAuthUrl = "https://api.notion.com/v1/oauth/authorize?...";
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { authUrl: mockAuthUrl } }),
|
||||
});
|
||||
|
||||
const authUrl = await authorize(environmentId, apiHost);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: expectedHeaders,
|
||||
});
|
||||
expect(authUrl).toBe(mockAuthUrl);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should throw error and log on failed fetch", async () => {
|
||||
const errorText = "Failed to fetch";
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
text: async () => errorText,
|
||||
});
|
||||
|
||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: expectedHeaders,
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith({ errorText }, "authorize: Could not fetch notion config");
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Loading from "./loading";
|
||||
|
||||
// Mock child components
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, className }: { children: React.ReactNode; className: string }) => (
|
||||
<button className={className}>{children}</button>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: () => <div data-testid="go-back-button">Go Back</div>,
|
||||
}));
|
||||
|
||||
// Mock @tolgee/react
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key, // Simple mock translation
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("Notion Integration Loading", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders loading state correctly", () => {
|
||||
render(<Loading />);
|
||||
|
||||
// Check for GoBackButton mock
|
||||
expect(screen.getByTestId("go-back-button")).toBeInTheDocument();
|
||||
|
||||
// Check for the disabled button
|
||||
const linkButton = screen.getByText("environments.integrations.notion.link_database");
|
||||
expect(linkButton).toBeInTheDocument();
|
||||
expect(linkButton.closest("button")).toHaveClass(
|
||||
"pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200"
|
||||
);
|
||||
|
||||
// Check for table headers
|
||||
expect(screen.getByText("common.survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.notion.database_name")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.updated_at")).toBeInTheDocument();
|
||||
|
||||
// Check for placeholder elements (skeleton loaders)
|
||||
// There should be 3 rows * 5 pulse divs per row = 15 pulse divs
|
||||
const pulseDivs = screen.getAllByText("", { selector: "div.animate-pulse" });
|
||||
expect(pulseDivs.length).toBeGreaterThanOrEqual(15); // Check if at least 15 pulse divs are rendered
|
||||
});
|
||||
});
|
||||
@@ -1,250 +0,0 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/notion/page";
|
||||
import { getIntegrationByType } from "@/lib/integration/service";
|
||||
import { getNotionDatabases } from "@/lib/notion/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper", () => ({
|
||||
NotionWrapper: vi.fn(
|
||||
({ enabled, environment, surveys, notionIntegration, webAppUrl, databasesArray, locale }) => (
|
||||
<div>
|
||||
<span>Mocked NotionWrapper</span>
|
||||
<span data-testid="enabled">{enabled.toString()}</span>
|
||||
<span data-testid="environmentId">{environment.id}</span>
|
||||
<span data-testid="surveyCount">{surveys?.length ?? 0}</span>
|
||||
<span data-testid="integrationId">{notionIntegration?.id}</span>
|
||||
<span data-testid="webAppUrl">{webAppUrl}</span>
|
||||
<span data-testid="databaseCount">{databasesArray?.length ?? 0}</span>
|
||||
<span data-testid="locale">{locale}</span>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys", () => ({
|
||||
getSurveys: vi.fn(),
|
||||
}));
|
||||
|
||||
let mockNotionClientId: string | undefined = "test-client-id";
|
||||
let mockNotionClientSecret: string | undefined = "test-client-secret";
|
||||
let mockNotionAuthUrl: string | undefined = "https://notion.com/auth";
|
||||
let mockNotionRedirectUri: string | undefined = "https://app.formbricks.com/redirect";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
get NOTION_OAUTH_CLIENT_ID() {
|
||||
return mockNotionClientId;
|
||||
},
|
||||
get NOTION_OAUTH_CLIENT_SECRET() {
|
||||
return mockNotionClientSecret;
|
||||
},
|
||||
get NOTION_AUTH_URL() {
|
||||
return mockNotionAuthUrl;
|
||||
},
|
||||
get NOTION_REDIRECT_URI() {
|
||||
return mockNotionRedirectUri;
|
||||
},
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
vi.mock("@/lib/integration/service", () => ({
|
||||
getIntegrationByType: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/notion/service", () => ({
|
||||
getNotionDatabases: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/utils/locale", () => ({
|
||||
findMatchingLocale: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: vi.fn(({ url }) => <div data-testid="go-back">{url}</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: vi.fn(({ children }) => <div>{children}</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: vi.fn(({ pageTitle }) => <h1>{pageTitle}</h1>),
|
||||
}));
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
appSetupCompleted: false,
|
||||
type: "development",
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockSurveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
name: "Survey 1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "test-env-id",
|
||||
status: "inProgress",
|
||||
type: "app",
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
languages: [],
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
segment: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const mockNotionIntegration = {
|
||||
id: "integration1",
|
||||
type: "notion",
|
||||
config: {
|
||||
data: [],
|
||||
key: { bot_id: "bot-id-123" },
|
||||
email: "test@example.com",
|
||||
},
|
||||
} as unknown as TIntegrationNotion;
|
||||
|
||||
const mockDatabases: TIntegrationNotionDatabase[] = [
|
||||
{ id: "db1", name: "Database 1", properties: {} },
|
||||
{ id: "db2", name: "Database 2", properties: {} },
|
||||
];
|
||||
|
||||
const mockProps = {
|
||||
params: { environmentId: "test-env-id" },
|
||||
};
|
||||
|
||||
describe("NotionIntegrationPage", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
} as TEnvironmentAuth);
|
||||
vi.mocked(getSurveys).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getIntegrationByType).mockResolvedValue(mockNotionIntegration);
|
||||
vi.mocked(getNotionDatabases).mockResolvedValue(mockDatabases);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
|
||||
mockNotionClientId = "test-client-id";
|
||||
mockNotionClientSecret = "test-client-secret";
|
||||
mockNotionAuthUrl = "https://notion.com/auth";
|
||||
mockNotionRedirectUri = "https://app.formbricks.com/redirect";
|
||||
});
|
||||
|
||||
test("renders the page with NotionWrapper when enabled and not read-only", async () => {
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("environments.integrations.notion.notion_integration")).toBeInTheDocument();
|
||||
expect(screen.getByText("Mocked NotionWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("enabled")).toHaveTextContent("true");
|
||||
expect(screen.getByTestId("environmentId")).toHaveTextContent(mockEnvironment.id);
|
||||
expect(screen.getByTestId("surveyCount")).toHaveTextContent(mockSurveys.length.toString());
|
||||
expect(screen.getByTestId("integrationId")).toHaveTextContent(mockNotionIntegration.id);
|
||||
expect(screen.getByTestId("webAppUrl")).toHaveTextContent("test-webapp-url");
|
||||
expect(screen.getByTestId("databaseCount")).toHaveTextContent(mockDatabases.length.toString());
|
||||
expect(screen.getByTestId("locale")).toHaveTextContent("en-US");
|
||||
expect(screen.getByTestId("go-back")).toHaveTextContent(
|
||||
`test-webapp-url/environments/${mockProps.params.environmentId}/integrations`
|
||||
);
|
||||
expect(vi.mocked(redirect)).not.toHaveBeenCalled();
|
||||
expect(vi.mocked(getNotionDatabases)).toHaveBeenCalledWith(mockEnvironment.id);
|
||||
});
|
||||
|
||||
test("calls redirect when user is read-only", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: true,
|
||||
} as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(vi.mocked(redirect)).toHaveBeenCalledWith("./");
|
||||
});
|
||||
|
||||
test("passes enabled=false to NotionWrapper when constants are missing", async () => {
|
||||
mockNotionClientId = undefined; // Simulate missing constant
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByTestId("enabled")).toHaveTextContent("false");
|
||||
});
|
||||
|
||||
test("handles case where no Notion integration exists", async () => {
|
||||
vi.mocked(getIntegrationByType).mockResolvedValue(null); // No integration
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("Mocked NotionWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("integrationId")).toBeEmptyDOMElement(); // No integration ID passed
|
||||
expect(screen.getByTestId("databaseCount")).toHaveTextContent("0"); // No databases fetched
|
||||
expect(vi.mocked(getNotionDatabases)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles case where integration exists but has no key (bot_id)", async () => {
|
||||
const integrationWithoutKey = {
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, key: undefined },
|
||||
} as unknown as TIntegrationNotion;
|
||||
vi.mocked(getIntegrationByType).mockResolvedValue(integrationWithoutKey);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("Mocked NotionWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("integrationId")).toHaveTextContent(integrationWithoutKey.id);
|
||||
expect(screen.getByTestId("databaseCount")).toHaveTextContent("0"); // No databases fetched
|
||||
expect(vi.mocked(getNotionDatabases)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,243 +0,0 @@
|
||||
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/integrations/lib/webhook";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/page";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegration } from "@formbricks/types/integration";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/webhook", () => ({
|
||||
getWebhookCountBySource: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/integration/service", () => ({
|
||||
getIntegrations: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/integration-card", () => ({
|
||||
Card: ({ label, description, statusText, disabled }) => (
|
||||
<div data-testid={`card-${label}`}>
|
||||
<h1>{label}</h1>
|
||||
<p>{description}</p>
|
||||
<span>{statusText}</span>
|
||||
{disabled && <span>Disabled</span>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: ({ children }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: ({ pageTitle }) => <h1>{pageTitle}</h1>,
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("next/image", () => ({
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
default: ({ alt }) => <img alt={alt} />,
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
appSetupCompleted: true,
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockIntegrations: TIntegration[] = [
|
||||
{
|
||||
id: "google-sheets-id",
|
||||
type: "googleSheets",
|
||||
environmentId: "test-env-id",
|
||||
config: { data: [], email: "test@example.com" } as unknown as TIntegration["config"],
|
||||
},
|
||||
{
|
||||
id: "slack-id",
|
||||
type: "slack",
|
||||
environmentId: "test-env-id",
|
||||
config: { data: [] } as unknown as TIntegration["config"],
|
||||
},
|
||||
];
|
||||
|
||||
const mockParams = { environmentId: "test-env-id" };
|
||||
const mockProps = { params: mockParams };
|
||||
|
||||
describe("Integrations Page", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getWebhookCountBySource).mockResolvedValue(0);
|
||||
vi.mocked(getIntegrations).mockResolvedValue([]);
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
isBilling: false,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
});
|
||||
|
||||
test("renders the page header and integration cards", async () => {
|
||||
vi.mocked(getWebhookCountBySource).mockImplementation(async (envId, source) => {
|
||||
if (source === "zapier") return 1;
|
||||
if (source === "user") return 2;
|
||||
return 0;
|
||||
});
|
||||
vi.mocked(getIntegrations).mockResolvedValue(mockIntegrations);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("common.integrations")).toBeInTheDocument(); // Page Header
|
||||
expect(screen.getByTestId("card-Javascript SDK")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.website_or_app_integration_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.connected")[0]).toBeInTheDocument(); // JS SDK status
|
||||
|
||||
expect(screen.getByTestId("card-Zapier")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.zapier_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getByText("1 zap")).toBeInTheDocument(); // Zapier status
|
||||
|
||||
expect(screen.getByTestId("card-Webhooks")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.webhook_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getByText("2 webhooks")).toBeInTheDocument(); // Webhook status
|
||||
|
||||
expect(screen.getByTestId("card-Google Sheets")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.google_sheet_integration_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.connected")[1]).toBeInTheDocument(); // Google Sheets status
|
||||
|
||||
expect(screen.getByTestId("card-Airtable")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.airtable_integration_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.not_connected")[0]).toBeInTheDocument(); // Airtable status
|
||||
|
||||
expect(screen.getByTestId("card-Slack")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.slack_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.connected")[2]).toBeInTheDocument(); // Slack status
|
||||
|
||||
expect(screen.getByTestId("card-n8n")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.n8n_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.not_connected")[1]).toBeInTheDocument(); // n8n status
|
||||
|
||||
expect(screen.getByTestId("card-Make.com")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.make_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.not_connected")[2]).toBeInTheDocument(); // Make status
|
||||
|
||||
expect(screen.getByTestId("card-Notion")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.notion_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.not_connected")[3]).toBeInTheDocument(); // Notion status
|
||||
|
||||
expect(screen.getByTestId("card-Activepieces")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.activepieces_integration_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.not_connected")[4]).toBeInTheDocument(); // Activepieces status
|
||||
});
|
||||
|
||||
test("renders disabled cards when isReadOnly is true", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: true,
|
||||
isBilling: false,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
// JS SDK and Webhooks should not be disabled
|
||||
expect(screen.getByTestId("card-Javascript SDK")).not.toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Webhooks")).not.toHaveTextContent("Disabled");
|
||||
|
||||
// Other cards should be disabled
|
||||
expect(screen.getByTestId("card-Zapier")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Google Sheets")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Airtable")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Slack")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-n8n")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Make.com")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Notion")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("Disabled");
|
||||
});
|
||||
|
||||
test("redirects when isBilling is true", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
isBilling: true,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
await Page(mockProps);
|
||||
|
||||
expect(vi.mocked(redirect)).toHaveBeenCalledWith(
|
||||
`/environments/${mockParams.environmentId}/settings/billing`
|
||||
);
|
||||
});
|
||||
|
||||
test("renders correct status text for single integration", async () => {
|
||||
vi.mocked(getWebhookCountBySource).mockImplementation(async (envId, source) => {
|
||||
if (source === "n8n") return 1;
|
||||
if (source === "make") return 1;
|
||||
if (source === "activepieces") return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByTestId("card-n8n")).toHaveTextContent("1 common.integration");
|
||||
expect(screen.getByTestId("card-Make.com")).toHaveTextContent("1 common.integration");
|
||||
expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("1 common.integration");
|
||||
});
|
||||
|
||||
test("renders correct status text for multiple integrations", async () => {
|
||||
vi.mocked(getWebhookCountBySource).mockImplementation(async (envId, source) => {
|
||||
if (source === "n8n") return 3;
|
||||
if (source === "make") return 4;
|
||||
if (source === "activepieces") return 5;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByTestId("card-n8n")).toHaveTextContent("3 common.integrations");
|
||||
expect(screen.getByTestId("card-Make.com")).toHaveTextContent("4 common.integrations");
|
||||
expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("5 common.integrations");
|
||||
});
|
||||
|
||||
test("renders not connected status when widgetSetupCompleted is false", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: { ...mockEnvironment, appSetupCompleted: false },
|
||||
isReadOnly: false,
|
||||
isBilling: false,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByTestId("card-Javascript SDK")).toHaveTextContent("common.not_connected");
|
||||
});
|
||||
});
|
||||
@@ -1,750 +0,0 @@
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationSlack,
|
||||
TIntegrationSlackConfigData,
|
||||
TIntegrationSlackCredential,
|
||||
} from "@formbricks/types/integration/slack";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { AddChannelMappingModal } from "./AddChannelMappingModal";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: (value: any, _locale: string) => value?.default || "",
|
||||
}));
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
replaceHeadlineRecall: (survey: any) => survey,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/additional-integration-settings", () => ({
|
||||
AdditionalIntegrationSettings: ({
|
||||
includeVariables,
|
||||
setIncludeVariables,
|
||||
includeHiddenFields,
|
||||
setIncludeHiddenFields,
|
||||
includeMetadata,
|
||||
setIncludeMetadata,
|
||||
includeCreatedAt,
|
||||
setIncludeCreatedAt,
|
||||
}: any) => (
|
||||
<div>
|
||||
<span>Additional Settings</span>
|
||||
<input
|
||||
data-testid="include-variables"
|
||||
type="checkbox"
|
||||
checked={includeVariables}
|
||||
onChange={(e) => setIncludeVariables(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-hidden-fields"
|
||||
type="checkbox"
|
||||
checked={includeHiddenFields}
|
||||
onChange={(e) => setIncludeHiddenFields(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-metadata"
|
||||
type="checkbox"
|
||||
checked={includeMetadata}
|
||||
onChange={(e) => setIncludeMetadata(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-created-at"
|
||||
type="checkbox"
|
||||
checked={includeCreatedAt}
|
||||
onChange={(e) => setIncludeCreatedAt(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/dropdown-selector", () => ({
|
||||
DropdownSelector: ({ label, items, selectedItem, setSelectedItem, disabled }: any) => (
|
||||
<div>
|
||||
<label>{label}</label>
|
||||
<select
|
||||
data-testid={label.includes("channel") ? "channel-dropdown" : "survey-dropdown"}
|
||||
value={selectedItem?.id || ""}
|
||||
onChange={(e) => {
|
||||
const selected = items.find((item: any) => item.id === e.target.value);
|
||||
setSelectedItem(selected);
|
||||
}}
|
||||
disabled={disabled}>
|
||||
<option value="">Select...</option>
|
||||
{items.map((item: any) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: ({ open, children }: { open: boolean; children: React.ReactNode }) =>
|
||||
open ? <div data-testid="modal">{children}</div> : null,
|
||||
}));
|
||||
vi.mock("next/image", () => ({
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
|
||||
}));
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
vi.mock("react-hook-form", () => ({
|
||||
useForm: () => ({
|
||||
handleSubmit: (callback: any) => (event: any) => {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@tolgee/react", async () => {
|
||||
const MockTolgeeProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
const useTranslate = () => ({
|
||||
t: (key: string, _?: any) => {
|
||||
// NOSONAR
|
||||
// Simple mock translation function
|
||||
if (key === "common.all_questions") return "All questions";
|
||||
if (key === "common.selected_questions") return "Selected questions";
|
||||
if (key === "environments.integrations.slack.link_slack_channel") return "Link Slack Channel";
|
||||
if (key === "common.update") return "Update";
|
||||
if (key === "common.delete") return "Delete";
|
||||
if (key === "common.cancel") return "Cancel";
|
||||
if (key === "environments.integrations.slack.select_channel") return "Select channel";
|
||||
if (key === "common.select_survey") return "Select survey";
|
||||
if (key === "common.questions") return "Questions";
|
||||
if (key === "environments.integrations.slack.please_select_a_channel")
|
||||
return "Please select a channel.";
|
||||
if (key === "environments.integrations.please_select_a_survey_error") return "Please select a survey.";
|
||||
if (key === "environments.integrations.select_at_least_one_question_error")
|
||||
return "Please select at least one question.";
|
||||
if (key === "environments.integrations.integration_updated_successfully")
|
||||
return "Integration updated successfully.";
|
||||
if (key === "environments.integrations.integration_added_successfully")
|
||||
return "Integration added successfully.";
|
||||
if (key === "environments.integrations.integration_removed_successfully")
|
||||
return "Integration removed successfully.";
|
||||
if (key === "environments.integrations.slack.dont_see_your_channel") return "Don't see your channel?";
|
||||
if (key === "common.note") return "Note";
|
||||
if (key === "environments.integrations.slack.already_connected_another_survey")
|
||||
return "This channel is already connected to another survey.";
|
||||
if (key === "environments.integrations.slack.create_at_least_one_channel_error")
|
||||
return "Please create at least one channel in Slack first.";
|
||||
if (key === "environments.integrations.create_survey_warning")
|
||||
return "You need to create a survey first.";
|
||||
if (key === "environments.integrations.slack.link_channel") return "Link Channel";
|
||||
return key; // Return key if no translation is found
|
||||
},
|
||||
});
|
||||
return { TolgeeProvider: MockTolgeeProvider, useTranslate };
|
||||
});
|
||||
vi.mock("lucide-react", () => ({
|
||||
CircleHelpIcon: () => <div data-testid="circle-help-icon" />,
|
||||
Check: () => <div data-testid="check-icon" />, // Add the Check icon mock
|
||||
Loader2: () => <div data-testid="loader-icon" />, // Add the Loader2 icon mock
|
||||
}));
|
||||
|
||||
// Mock dependencies
|
||||
const createOrUpdateIntegrationActionMock = vi.mocked(createOrUpdateIntegrationAction);
|
||||
const toast = vi.mocked((await import("react-hot-toast")).default);
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const mockSetOpen = vi.fn();
|
||||
|
||||
const surveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 1",
|
||||
type: "app",
|
||||
environmentId: environmentId,
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1?" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 2?" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Choice 1" } },
|
||||
{ id: "c2", label: { default: "Choice 2" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
variables: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
{
|
||||
id: "survey2",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 2",
|
||||
type: "link",
|
||||
environmentId: environmentId,
|
||||
status: "draft",
|
||||
questions: [
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate this?" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
variables: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const channels: TIntegrationItem[] = [
|
||||
{ id: "channel1", name: "#general" },
|
||||
{ id: "channel2", name: "#random" },
|
||||
];
|
||||
|
||||
const mockSlackIntegration: TIntegrationSlack = {
|
||||
id: "integration1",
|
||||
type: "slack",
|
||||
environmentId: environmentId,
|
||||
config: {
|
||||
key: {
|
||||
access_token: "xoxb-test-token",
|
||||
team_name: "Test Team",
|
||||
team_id: "T123",
|
||||
} as unknown as TIntegrationSlackCredential,
|
||||
data: [], // Initially empty
|
||||
},
|
||||
};
|
||||
|
||||
const mockSelectedIntegration: TIntegrationSlackConfigData & { index: number } = {
|
||||
channelId: channels[0].id,
|
||||
channelName: channels[0].name,
|
||||
surveyId: surveys[0].id,
|
||||
surveyName: surveys[0].name,
|
||||
questionIds: [surveys[0].questions[0].id],
|
||||
questions: "Selected questions",
|
||||
createdAt: new Date(),
|
||||
includeVariables: true,
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: true,
|
||||
includeCreatedAt: false,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
describe("AddChannelMappingModal", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset integration data before each test if needed
|
||||
mockSlackIntegration.config.data = [
|
||||
{ ...mockSelectedIntegration }, // Simulate existing data for update/delete tests
|
||||
];
|
||||
});
|
||||
|
||||
test("renders correctly when open (create mode)", () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Link Slack Channel", { selector: "div.text-xl.font-medium" })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("channel-dropdown")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("survey-dropdown")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Link Channel" })).toBeInTheDocument();
|
||||
expect(screen.queryByText("Delete")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Questions")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("circle-help-icon")).toBeInTheDocument();
|
||||
expect(screen.getByText("Don't see your channel?")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly when open (update mode)", () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Link Slack Channel", { selector: "div.text-xl.font-medium" })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("channel-dropdown")).toHaveValue(channels[0].id);
|
||||
expect(screen.getByTestId("survey-dropdown")).toHaveValue(surveys[0].id);
|
||||
expect(screen.getByText("Questions")).toBeInTheDocument();
|
||||
expect(screen.getByText("Delete")).toBeInTheDocument();
|
||||
expect(screen.getByText("Update")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("include-variables")).toBeChecked();
|
||||
expect(screen.getByTestId("include-hidden-fields")).not.toBeChecked();
|
||||
expect(screen.getByTestId("include-metadata")).toBeChecked();
|
||||
expect(screen.getByTestId("include-created-at")).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("selects survey and shows questions", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[1].id);
|
||||
|
||||
expect(screen.getByText("Questions")).toBeInTheDocument();
|
||||
surveys[1].questions.forEach((q) => {
|
||||
expect(screen.getByLabelText(q.headline.default)).toBeInTheDocument();
|
||||
// Initially all questions should be checked when a survey is selected in create mode
|
||||
expect(screen.getByLabelText(q.headline.default)).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles question selection", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
const firstQuestionCheckbox = screen.getByLabelText(surveys[0].questions[0].headline.default);
|
||||
expect(firstQuestionCheckbox).toBeChecked(); // Initially checked
|
||||
|
||||
await userEvent.click(firstQuestionCheckbox);
|
||||
expect(firstQuestionCheckbox).not.toBeChecked(); // Unchecked after click
|
||||
|
||||
await userEvent.click(firstQuestionCheckbox);
|
||||
expect(firstQuestionCheckbox).toBeChecked(); // Checked again
|
||||
});
|
||||
|
||||
test("creates integration successfully", async () => {
|
||||
createOrUpdateIntegrationActionMock.mockResolvedValue({ data: null as any }); // Mock successful action
|
||||
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={{ ...mockSlackIntegration, config: { ...mockSlackIntegration.config, data: [] } }} // Start with empty data
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Channel" });
|
||||
|
||||
await userEvent.selectOptions(channelDropdown, channels[1].id);
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
// Wait for questions to appear and potentially uncheck one
|
||||
const firstQuestionCheckbox = await screen.findByLabelText(surveys[0].questions[0].headline.default);
|
||||
await userEvent.click(firstQuestionCheckbox); // Uncheck first question
|
||||
|
||||
// Check additional settings
|
||||
await userEvent.click(screen.getByTestId("include-variables"));
|
||||
await userEvent.click(screen.getByTestId("include-metadata"));
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationActionMock).toHaveBeenCalledWith({
|
||||
environmentId,
|
||||
integrationData: expect.objectContaining({
|
||||
type: "slack",
|
||||
config: expect.objectContaining({
|
||||
key: mockSlackIntegration.config.key,
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
channelId: channels[1].id,
|
||||
channelName: channels[1].name,
|
||||
surveyId: surveys[0].id,
|
||||
surveyName: surveys[0].name,
|
||||
questionIds: surveys[0].questions.slice(1).map((q) => q.id), // Excludes the first question
|
||||
questions: "Selected questions",
|
||||
includeVariables: true,
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: true,
|
||||
includeCreatedAt: true, // Default
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Integration added successfully.");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes integration successfully", async () => {
|
||||
createOrUpdateIntegrationActionMock.mockResolvedValue({ data: null as any });
|
||||
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration} // Contains initial data at index 0
|
||||
channels={channels}
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteButton = screen.getByText("Delete");
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationActionMock).toHaveBeenCalledWith({
|
||||
environmentId,
|
||||
integrationData: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
data: [], // Data array should be empty after deletion
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Integration removed successfully.");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("shows validation error if no channel selected", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Channel" });
|
||||
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
// No channel selected
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select a channel.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationActionMock).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows validation error if no survey selected", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Channel" });
|
||||
|
||||
await userEvent.selectOptions(channelDropdown, channels[0].id);
|
||||
// No survey selected
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select a survey.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationActionMock).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows validation error if no questions selected", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Channel" });
|
||||
|
||||
await userEvent.selectOptions(channelDropdown, channels[0].id);
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
// Uncheck all questions
|
||||
for (const question of surveys[0].questions) {
|
||||
const checkbox = await screen.findByLabelText(question.headline.default);
|
||||
await userEvent.click(checkbox);
|
||||
}
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select at least one question.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationActionMock).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows error toast if createOrUpdateIntegrationAction fails", async () => {
|
||||
const errorMessage = "Failed to update integration";
|
||||
createOrUpdateIntegrationActionMock.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Channel" });
|
||||
|
||||
await userEvent.selectOptions(channelDropdown, channels[0].id);
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationActionMock).toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls setOpen(false) and resets form on cancel", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
|
||||
// Simulate some interaction
|
||||
await userEvent.selectOptions(channelDropdown, channels[0].id);
|
||||
await userEvent.click(cancelButton);
|
||||
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
// Re-render with open=true to check if state was reset (channel should be unselected)
|
||||
cleanup();
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("channel-dropdown")).toHaveValue("");
|
||||
});
|
||||
|
||||
test("shows warning when selected channel is already connected (add mode)", async () => {
|
||||
// Add an existing connection for channel1
|
||||
const integrationWithExisting = {
|
||||
...mockSlackIntegration,
|
||||
config: {
|
||||
...mockSlackIntegration.config,
|
||||
data: [
|
||||
{
|
||||
channelId: "channel1",
|
||||
channelName: "#general",
|
||||
surveyId: "survey-other",
|
||||
surveyName: "Other Survey",
|
||||
questionIds: ["q-other"],
|
||||
questions: "All questions",
|
||||
createdAt: new Date(),
|
||||
} as TIntegrationSlackConfigData,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={integrationWithExisting}
|
||||
channels={channels}
|
||||
selectedIntegration={null} // Add mode
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
await userEvent.selectOptions(channelDropdown, "channel1");
|
||||
|
||||
expect(screen.getByText("This channel is already connected to another survey.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not show warning when selected channel is the one being edited", async () => {
|
||||
// Edit the existing connection for channel1
|
||||
const integrationToEdit = {
|
||||
...mockSlackIntegration,
|
||||
config: {
|
||||
...mockSlackIntegration.config,
|
||||
data: [
|
||||
{
|
||||
channelId: "channel1",
|
||||
channelName: "#general",
|
||||
surveyId: "survey1",
|
||||
surveyName: "Survey 1",
|
||||
questionIds: ["q1"],
|
||||
questions: "Selected questions",
|
||||
createdAt: new Date(),
|
||||
index: 0,
|
||||
} as TIntegrationSlackConfigData & { index: number },
|
||||
],
|
||||
},
|
||||
};
|
||||
const selectedIntegrationForEdit = integrationToEdit.config.data[0];
|
||||
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={integrationToEdit}
|
||||
channels={channels}
|
||||
selectedIntegration={selectedIntegrationForEdit} // Edit mode
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
// Channel is already selected via selectedIntegration prop
|
||||
expect(channelDropdown).toHaveValue("channel1");
|
||||
|
||||
expect(
|
||||
screen.queryByText("This channel is already connected to another survey.")
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationSlack, TIntegrationSlackCredential } from "@formbricks/types/integration/slack";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { getSlackChannelsAction } from "../actions";
|
||||
import { authorize } from "../lib/slack";
|
||||
import { SlackWrapper } from "./SlackWrapper";
|
||||
|
||||
// Mock child components and actions
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/actions", () => ({
|
||||
getSlackChannelsAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal",
|
||||
() => ({
|
||||
AddChannelMappingModal: vi.fn(({ open }) => (open ? <div data-testid="add-modal">Add Modal</div> : null)),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration", () => ({
|
||||
ManageIntegration: vi.fn(({ setOpenAddIntegrationModal, setIsConnected, handleSlackAuthorization }) => (
|
||||
<div data-testid="manage-integration">
|
||||
<button onClick={() => setOpenAddIntegrationModal(true)}>Open Modal</button>
|
||||
<button onClick={() => setIsConnected(false)}>Disconnect</button>
|
||||
<button onClick={handleSlackAuthorization}>Reconnect</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/lib/slack", () => ({
|
||||
authorize: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/images/slacklogo.png", () => ({
|
||||
default: "slack-logo-path",
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
ConnectIntegration: vi.fn(({ handleAuthorization, isEnabled }) => (
|
||||
<div data-testid="connect-integration">
|
||||
<button onClick={handleAuthorization} disabled={!isEnabled}>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock window.location.replace
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
replace: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const mockEnvironment = { id: "test-env-id" } as TEnvironment;
|
||||
const mockSurveys: TSurvey[] = [];
|
||||
const mockWebAppUrl = "http://localhost:3000";
|
||||
const mockLocale: TUserLocale = "en-US";
|
||||
const mockSlackChannels: TIntegrationItem[] = [{ id: "C123", name: "general" }];
|
||||
|
||||
const mockSlackIntegration: TIntegrationSlack = {
|
||||
id: "slack-int-1",
|
||||
type: "slack",
|
||||
environmentId: "test-env-id",
|
||||
config: {
|
||||
key: { access_token: "xoxb-valid-token" } as unknown as TIntegrationSlackCredential,
|
||||
data: [],
|
||||
},
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
environment: mockEnvironment,
|
||||
surveys: mockSurveys,
|
||||
webAppUrl: mockWebAppUrl,
|
||||
locale: mockLocale,
|
||||
};
|
||||
|
||||
describe("SlackWrapper", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getSlackChannelsAction).mockResolvedValue({ data: mockSlackChannels });
|
||||
vi.mocked(authorize).mockResolvedValue("https://slack.com/auth");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when not connected (no integration)", () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={undefined} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when not connected (integration without key)", () => {
|
||||
const integrationWithoutKey = { ...mockSlackIntegration, config: { data: [], email: "test" } } as any;
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={integrationWithoutKey} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration disabled when isEnabled is false", () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={false} slackIntegration={undefined} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeDisabled();
|
||||
});
|
||||
|
||||
test("calls authorize and redirects when Connect button is clicked", async () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={undefined} />);
|
||||
const connectButton = screen.getByRole("button", { name: "Connect" });
|
||||
await userEvent.click(connectButton);
|
||||
|
||||
expect(authorize).toHaveBeenCalledWith(mockEnvironment.id, mockWebAppUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith("https://slack.com/auth");
|
||||
});
|
||||
});
|
||||
|
||||
test("renders ManageIntegration and AddChannelMappingModal (hidden) when connected", () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={mockSlackIntegration} />);
|
||||
expect(screen.getByTestId("manage-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("connect-integration")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("add-modal")).not.toBeInTheDocument(); // Modal is initially hidden
|
||||
});
|
||||
|
||||
test("calls getSlackChannelsAction on mount", async () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={mockSlackIntegration} />);
|
||||
await waitFor(() => {
|
||||
expect(getSlackChannelsAction).toHaveBeenCalledWith({ environmentId: mockEnvironment.id });
|
||||
});
|
||||
});
|
||||
|
||||
test("switches from ManageIntegration to ConnectIntegration when disconnected", async () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={mockSlackIntegration} />);
|
||||
expect(screen.getByTestId("manage-integration")).toBeInTheDocument();
|
||||
|
||||
const disconnectButton = screen.getByRole("button", { name: "Disconnect" });
|
||||
await userEvent.click(disconnectButton);
|
||||
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("opens AddChannelMappingModal when triggered from ManageIntegration", async () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={mockSlackIntegration} />);
|
||||
expect(screen.queryByTestId("add-modal")).not.toBeInTheDocument();
|
||||
|
||||
const openModalButton = screen.getByRole("button", { name: "Open Modal" });
|
||||
await userEvent.click(openModalButton);
|
||||
|
||||
expect(screen.getByTestId("add-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls handleSlackAuthorization when reconnect button is clicked in ManageIntegration", async () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={mockSlackIntegration} />);
|
||||
const reconnectButton = screen.getByRole("button", { name: "Reconnect" });
|
||||
await userEvent.click(reconnectButton);
|
||||
|
||||
expect(authorize).toHaveBeenCalledWith(mockEnvironment.id, mockWebAppUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith("https://slack.com/auth");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { authorize } from "./slack";
|
||||
|
||||
// Mock the logger
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe("authorize", () => {
|
||||
const environmentId = "test-env-id";
|
||||
const apiHost = "http://test.com";
|
||||
const expectedUrl = `${apiHost}/api/v1/integrations/slack`;
|
||||
const expectedAuthUrl = "http://slack.com/auth";
|
||||
|
||||
test("should return authUrl on successful fetch", async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { authUrl: expectedAuthUrl } }),
|
||||
} as Response);
|
||||
|
||||
const authUrl = await authorize(environmentId, apiHost);
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: { environmentId },
|
||||
});
|
||||
expect(authUrl).toBe(expectedAuthUrl);
|
||||
});
|
||||
|
||||
test("should throw error and log error on failed fetch", async () => {
|
||||
const errorText = "Failed to fetch";
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
text: async () => errorText,
|
||||
} as Response);
|
||||
|
||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: { environmentId },
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith({ errorText }, "authorize: Could not fetch slack config");
|
||||
});
|
||||
});
|
||||
@@ -1,222 +0,0 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/slack/page";
|
||||
import { getIntegrationByType } from "@/lib/integration/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationSlack, TIntegrationSlackCredential } from "@formbricks/types/integration/slack";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys", () => ({
|
||||
getSurveys: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper", () => ({
|
||||
SlackWrapper: vi.fn(({ isEnabled, environment, surveys, slackIntegration, webAppUrl, locale }) => (
|
||||
<div data-testid="slack-wrapper">
|
||||
Mock SlackWrapper: isEnabled={isEnabled.toString()}, envId={environment.id}, surveys=
|
||||
{surveys.length}, integrationId={slackIntegration?.id}, webAppUrl={webAppUrl}, locale={locale}
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_PRODUCTION: true,
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
SLACK_CLIENT_ID: "test-slack-client-id",
|
||||
SLACK_CLIENT_SECRET: "test-slack-client-secret",
|
||||
WEBAPP_URL: "http://test.formbricks.com",
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/integration/service", () => ({
|
||||
getIntegrationByType: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/locale", () => ({
|
||||
findMatchingLocale: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: vi.fn(({ url }) => <div data-testid="go-back-button">Go Back: {url}</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: vi.fn(({ children }) => <div>{children}</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: vi.fn(({ pageTitle }) => <h1 data-testid="page-header">{pageTitle}</h1>),
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock data
|
||||
const environmentId = "test-env-id";
|
||||
const mockEnvironment = {
|
||||
id: environmentId,
|
||||
createdAt: new Date(),
|
||||
type: "development",
|
||||
} as unknown as TEnvironment;
|
||||
const mockSurveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
name: "Survey 1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: environmentId,
|
||||
status: "inProgress",
|
||||
type: "link",
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: false },
|
||||
languages: [],
|
||||
styling: null,
|
||||
segment: null,
|
||||
resultShareKey: null,
|
||||
displayPercentage: null,
|
||||
closeOnDate: null,
|
||||
runOnDate: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
const mockSlackIntegration = {
|
||||
id: "slack-int-id",
|
||||
type: "slack",
|
||||
config: {
|
||||
data: [],
|
||||
key: "test-key" as unknown as TIntegrationSlackCredential,
|
||||
},
|
||||
} as unknown as TIntegrationSlack;
|
||||
const mockLocale = "en-US";
|
||||
const mockParams = { params: { environmentId } };
|
||||
|
||||
describe("SlackIntegrationPage", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getSurveys).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getIntegrationByType).mockResolvedValue(mockSlackIntegration);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale);
|
||||
});
|
||||
|
||||
test("renders correctly when user is not read-only", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
isReadOnly: false,
|
||||
environment: mockEnvironment,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
const tree = await Page(mockParams);
|
||||
render(tree);
|
||||
|
||||
expect(screen.getByTestId("page-header")).toHaveTextContent(
|
||||
"environments.integrations.slack.slack_integration"
|
||||
);
|
||||
expect(screen.getByTestId("go-back-button")).toHaveTextContent(
|
||||
`Go Back: http://test.formbricks.com/environments/${environmentId}/integrations`
|
||||
);
|
||||
expect(screen.getByTestId("slack-wrapper")).toBeInTheDocument();
|
||||
|
||||
// Check props passed to SlackWrapper
|
||||
expect(vi.mocked(SlackWrapper)).toHaveBeenCalledWith(
|
||||
{
|
||||
isEnabled: true, // Since SLACK_CLIENT_ID and SLACK_CLIENT_SECRET are mocked
|
||||
environment: mockEnvironment,
|
||||
surveys: mockSurveys,
|
||||
slackIntegration: mockSlackIntegration,
|
||||
webAppUrl: "http://test.formbricks.com",
|
||||
locale: mockLocale,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(vi.mocked(redirect)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("redirects when user is read-only", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
isReadOnly: true,
|
||||
environment: mockEnvironment,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
// Need to actually call the component function to trigger the redirect logic
|
||||
await Page(mockParams);
|
||||
|
||||
expect(vi.mocked(redirect)).toHaveBeenCalledWith("./");
|
||||
expect(vi.mocked(SlackWrapper)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders correctly when Slack integration is not configured", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
isReadOnly: false,
|
||||
environment: mockEnvironment,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
vi.mocked(getIntegrationByType).mockResolvedValue(null); // Simulate no integration found
|
||||
|
||||
const tree = await Page(mockParams);
|
||||
render(tree);
|
||||
|
||||
expect(screen.getByTestId("page-header")).toHaveTextContent(
|
||||
"environments.integrations.slack.slack_integration"
|
||||
);
|
||||
expect(screen.getByTestId("slack-wrapper")).toBeInTheDocument();
|
||||
|
||||
// Check props passed to SlackWrapper when integration is null
|
||||
expect(vi.mocked(SlackWrapper)).toHaveBeenCalledWith(
|
||||
{
|
||||
isEnabled: true,
|
||||
environment: mockEnvironment,
|
||||
surveys: mockSurveys,
|
||||
slackIntegration: null, // Expecting null here
|
||||
webAppUrl: "http://test.formbricks.com",
|
||||
locale: mockLocale,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(vi.mocked(redirect)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import WebhooksPage from "./page";
|
||||
|
||||
vi.mock("@/modules/integrations/webhooks/page", () => ({
|
||||
WebhooksPage: vi.fn(() => <div>WebhooksPageMock</div>),
|
||||
}));
|
||||
|
||||
describe("WebhooksIntegrationPage", () => {
|
||||
test("renders WebhooksPage component", () => {
|
||||
render(<WebhooksPage params={{ environmentId: "test-env-id" }} />);
|
||||
expect(WebhooksPage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TMembership } from "@formbricks/types/memberships";
|
||||
import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import EnvironmentPage from "./page";
|
||||
|
||||
vi.mock("@/lib/membership/service", () => ({
|
||||
getMembershipByUserIdOrganizationId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/membership/utils", () => ({
|
||||
getAccessFlags: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("EnvironmentPage", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockEnvironmentId = "test-environment-id";
|
||||
const mockUserId = "test-user-id";
|
||||
const mockOrganizationId = "test-organization-id";
|
||||
|
||||
const mockSession = {
|
||||
user: {
|
||||
id: mockUserId,
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
emailVerified: new Date(),
|
||||
role: "user",
|
||||
objective: "other",
|
||||
},
|
||||
expires: new Date(Date.now() + 3600 * 1000).toISOString(), // 1 hour from now
|
||||
} as any;
|
||||
|
||||
const mockOrganization: TOrganization = {
|
||||
id: mockOrganizationId,
|
||||
name: "Test Organization",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
billing: {
|
||||
stripeCustomerId: "cus_123",
|
||||
} as unknown as TOrganizationBilling,
|
||||
} as unknown as TOrganization;
|
||||
|
||||
test("should redirect to billing settings if isBilling is true", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
session: mockSession,
|
||||
organization: mockOrganization,
|
||||
environment: { id: mockEnvironmentId, type: "production", product: { id: "prodId" } },
|
||||
} as any); // Using 'any' for brevity as environment type is complex and not core to this test
|
||||
|
||||
const mockMembership: TMembership = {
|
||||
userId: mockUserId,
|
||||
organizationId: mockOrganizationId,
|
||||
role: "owner" as any,
|
||||
accepted: true,
|
||||
};
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
|
||||
vi.mocked(getAccessFlags).mockReturnValue({ isBilling: true, isOwner: true } as any);
|
||||
|
||||
await EnvironmentPage({ params: { environmentId: mockEnvironmentId } });
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith(`/environments/${mockEnvironmentId}/settings/billing`);
|
||||
});
|
||||
|
||||
test("should redirect to surveys if isBilling is false", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
session: mockSession,
|
||||
organization: mockOrganization,
|
||||
environment: { id: mockEnvironmentId, type: "production", product: { id: "prodId" } },
|
||||
} as any);
|
||||
|
||||
const mockMembership: TMembership = {
|
||||
userId: mockUserId,
|
||||
organizationId: mockOrganizationId,
|
||||
role: "developer" as any, // Role that would result in isBilling: false
|
||||
accepted: true,
|
||||
};
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
|
||||
vi.mocked(getAccessFlags).mockReturnValue({ isBilling: false, isOwner: false } as any);
|
||||
|
||||
await EnvironmentPage({ params: { environmentId: mockEnvironmentId } });
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith(`/environments/${mockEnvironmentId}/surveys`);
|
||||
});
|
||||
|
||||
test("should handle session being null", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
session: null, // Simulate no active session
|
||||
organization: mockOrganization,
|
||||
environment: { id: mockEnvironmentId, type: "production", product: { id: "prodId" } },
|
||||
} as any);
|
||||
|
||||
// Membership fetch might return null or throw, depending on implementation when userId is undefined
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null);
|
||||
// Access flags would likely be all false if membership is null
|
||||
vi.mocked(getAccessFlags).mockReturnValue({ isBilling: false, isOwner: false } as any);
|
||||
|
||||
await EnvironmentPage({ params: { environmentId: mockEnvironmentId } });
|
||||
|
||||
// Expect redirect to surveys as default when isBilling is false
|
||||
expect(redirect).toHaveBeenCalledWith(`/environments/${mockEnvironmentId}/surveys`);
|
||||
});
|
||||
|
||||
test("should handle currentUserMembership being null", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
session: mockSession,
|
||||
organization: mockOrganization,
|
||||
environment: { id: mockEnvironmentId, type: "production", product: { id: "prodId" } },
|
||||
} as any);
|
||||
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null); // Simulate no membership found
|
||||
// Access flags would likely be all false if membership is null
|
||||
vi.mocked(getAccessFlags).mockReturnValue({ isBilling: false, isOwner: false } as any);
|
||||
|
||||
await EnvironmentPage({ params: { environmentId: mockEnvironmentId } });
|
||||
|
||||
// Expect redirect to surveys as default when isBilling is false
|
||||
expect(redirect).toHaveBeenCalledWith(`/environments/${mockEnvironmentId}/surveys`);
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { AppConnectionLoading as OriginalAppConnectionLoading } from "@/modules/projects/settings/(setup)/app-connection/loading";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import AppConnectionLoading from "./loading";
|
||||
|
||||
// Mock the original component to ensure we are testing the re-export
|
||||
vi.mock("@/modules/projects/settings/(setup)/app-connection/loading", () => ({
|
||||
AppConnectionLoading: () => <div data-testid="mock-app-connection-loading">Mock AppConnectionLoading</div>,
|
||||
}));
|
||||
|
||||
describe("AppConnectionLoading Re-export", () => {
|
||||
test("should re-export AppConnectionLoading from the correct module", () => {
|
||||
// Check if the re-exported component is the same as the original (mocked) component
|
||||
expect(AppConnectionLoading).toBe(OriginalAppConnectionLoading);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { AppConnectionPage as OriginalAppConnectionPage } from "@/modules/projects/settings/(setup)/app-connection/page";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import AppConnectionPage from "./page";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
describe("AppConnectionPage Re-export", () => {
|
||||
test("should re-export AppConnectionPage correctly", () => {
|
||||
expect(AppConnectionPage).toBe(OriginalAppConnectionPage);
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { GeneralSettingsLoading as OriginalGeneralSettingsLoading } from "@/modules/projects/settings/general/loading";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import GeneralSettingsLoadingPage from "./loading";
|
||||
|
||||
// Mock the original component to ensure we are testing the re-export
|
||||
vi.mock("@/modules/projects/settings/general/loading", () => ({
|
||||
GeneralSettingsLoading: () => (
|
||||
<div data-testid="mock-general-settings-loading">Mock GeneralSettingsLoading</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("GeneralSettingsLoadingPage Re-export", () => {
|
||||
test("should re-export GeneralSettingsLoading from the correct module", () => {
|
||||
// Check if the re-exported component is the same as the original (mocked) component
|
||||
expect(GeneralSettingsLoadingPage).toBe(OriginalGeneralSettingsLoading);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { GeneralSettingsPage } from "@/modules/projects/settings/general/page";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
describe("GeneralSettingsPage re-export", () => {
|
||||
test("should re-export GeneralSettingsPage component", () => {
|
||||
expect(Page).toBe(GeneralSettingsPage);
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { LanguagesLoading as OriginalLanguagesLoading } from "@/modules/ee/languages/loading";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import LanguagesLoading from "./loading";
|
||||
|
||||
// Mock the original component to ensure we are testing the re-export
|
||||
vi.mock("@/modules/ee/languages/loading", () => ({
|
||||
LanguagesLoading: () => <div data-testid="mock-languages-loading">Mock LanguagesLoading</div>,
|
||||
}));
|
||||
|
||||
describe("LanguagesLoadingPage Re-export", () => {
|
||||
test("should re-export LanguagesLoading from the correct module", () => {
|
||||
// Check if the re-exported component is the same as the original (mocked) component
|
||||
expect(LanguagesLoading).toBe(OriginalLanguagesLoading);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { LanguagesPage } from "@/modules/ee/languages/page";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
describe("LanguagesPage re-export", () => {
|
||||
test("should re-export LanguagesPage component", () => {
|
||||
expect(Page).toBe(LanguagesPage);
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import ProjectLayout, { metadata as layoutMetadata } from "./layout";
|
||||
|
||||
vi.mock("@/modules/projects/settings/layout", () => ({
|
||||
ProjectSettingsLayout: ({ children }) => <div data-testid="project-settings-layout">{children}</div>,
|
||||
metadata: { title: "Mocked Project Settings" },
|
||||
}));
|
||||
|
||||
describe("ProjectLayout", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders ProjectSettingsLayout", () => {
|
||||
const { getByTestId } = render(<ProjectLayout>Child Content</ProjectLayout>);
|
||||
expect(getByTestId("project-settings-layout")).toBeInTheDocument();
|
||||
expect(getByTestId("project-settings-layout")).toHaveTextContent("Child Content");
|
||||
});
|
||||
|
||||
test("exports metadata from @/modules/projects/settings/layout", () => {
|
||||
expect(layoutMetadata).toEqual({ title: "Mocked Project Settings" });
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { ProjectLookSettingsLoading as OriginalProjectLookSettingsLoading } from "@/modules/projects/settings/look/loading";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import ProjectLookSettingsLoading from "./loading";
|
||||
|
||||
// Mock the original component to ensure we are testing the re-export
|
||||
vi.mock("@/modules/projects/settings/look/loading", () => ({
|
||||
ProjectLookSettingsLoading: () => (
|
||||
<div data-testid="mock-project-look-settings-loading">Mock ProjectLookSettingsLoading</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("ProjectLookSettingsLoadingPage Re-export", () => {
|
||||
test("should re-export ProjectLookSettingsLoading from the correct module", () => {
|
||||
// Check if the re-exported component is the same as the original (mocked) component
|
||||
expect(ProjectLookSettingsLoading).toBe(OriginalProjectLookSettingsLoading);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ProjectLookSettingsPage } from "@/modules/projects/settings/look/page";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
describe("ProjectLookSettingsPage re-export", () => {
|
||||
test("should re-export ProjectLookSettingsPage component", () => {
|
||||
expect(Page).toBe(ProjectLookSettingsPage);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ProjectSettingsPage } from "@/modules/projects/settings/page";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
describe("ProjectSettingsPage re-export", () => {
|
||||
test("should re-export ProjectSettingsPage component", () => {
|
||||
expect(Page).toBe(ProjectSettingsPage);
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { TagsLoading as OriginalTagsLoading } from "@/modules/projects/settings/tags/loading";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import TagsLoading from "./loading";
|
||||
|
||||
// Mock the original component to ensure we are testing the re-export
|
||||
vi.mock("@/modules/projects/settings/tags/loading", () => ({
|
||||
TagsLoading: () => <div data-testid="mock-tags-loading">Mock TagsLoading</div>,
|
||||
}));
|
||||
|
||||
describe("TagsLoadingPage Re-export", () => {
|
||||
test("should re-export TagsLoading from the correct module", () => {
|
||||
// Check if the re-exported component is the same as the original (mocked) component
|
||||
expect(TagsLoading).toBe(OriginalTagsLoading);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { TagsPage } from "@/modules/projects/settings/tags/page";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
describe("TagsPage re-export", () => {
|
||||
test("should re-export TagsPage component", () => {
|
||||
expect(Page).toBe(TagsPage);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ProjectTeams } from "@/modules/ee/teams/project-teams/page";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
describe("ProjectTeams re-export", () => {
|
||||
test("should re-export ProjectTeams component", () => {
|
||||
expect(Page).toBe(ProjectTeams);
|
||||
});
|
||||
});
|
||||
@@ -1,148 +0,0 @@
|
||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { AccountSettingsNavbar } from "./AccountSettingsNavbar";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
usePathname: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/secondary-navigation", () => ({
|
||||
SecondaryNavigation: vi.fn(() => <div>SecondaryNavigationMock</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => {
|
||||
if (key === "common.profile") return "Profile";
|
||||
if (key === "common.notifications") return "Notifications";
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("AccountSettingsNavbar", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders correctly and sets profile as current when pathname includes /profile", () => {
|
||||
vi.mocked(usePathname).mockReturnValue("/environments/testEnvId/settings/profile");
|
||||
render(<AccountSettingsNavbar environmentId="testEnvId" activeId="profile" />);
|
||||
|
||||
expect(SecondaryNavigation).toHaveBeenCalledWith(
|
||||
{
|
||||
navigation: [
|
||||
{
|
||||
id: "profile",
|
||||
label: "Profile",
|
||||
href: "/environments/testEnvId/settings/profile",
|
||||
current: true,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
label: "Notifications",
|
||||
href: "/environments/testEnvId/settings/notifications",
|
||||
current: false,
|
||||
},
|
||||
],
|
||||
activeId: "profile",
|
||||
loading: undefined,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("sets notifications as current when pathname includes /notifications", () => {
|
||||
vi.mocked(usePathname).mockReturnValue("/environments/testEnvId/settings/notifications");
|
||||
render(<AccountSettingsNavbar environmentId="testEnvId" activeId="notifications" />);
|
||||
|
||||
expect(SecondaryNavigation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
navigation: [
|
||||
{
|
||||
id: "profile",
|
||||
label: "Profile",
|
||||
href: "/environments/testEnvId/settings/profile",
|
||||
current: false,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
label: "Notifications",
|
||||
href: "/environments/testEnvId/settings/notifications",
|
||||
current: true,
|
||||
},
|
||||
],
|
||||
activeId: "notifications",
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("passes loading prop to SecondaryNavigation", () => {
|
||||
vi.mocked(usePathname).mockReturnValue("/environments/testEnvId/settings/profile");
|
||||
render(<AccountSettingsNavbar environmentId="testEnvId" activeId="profile" loading={true} />);
|
||||
|
||||
expect(SecondaryNavigation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
loading: true,
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("handles undefined environmentId gracefully in hrefs", () => {
|
||||
vi.mocked(usePathname).mockReturnValue("/environments/undefined/settings/profile");
|
||||
render(<AccountSettingsNavbar activeId="profile" />); // environmentId is undefined
|
||||
|
||||
expect(SecondaryNavigation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
navigation: [
|
||||
{
|
||||
id: "profile",
|
||||
label: "Profile",
|
||||
href: "/environments/undefined/settings/profile",
|
||||
current: true,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
label: "Notifications",
|
||||
href: "/environments/undefined/settings/notifications",
|
||||
current: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("handles null pathname gracefully", () => {
|
||||
vi.mocked(usePathname).mockReturnValue("");
|
||||
render(<AccountSettingsNavbar environmentId="testEnvId" activeId="profile" />);
|
||||
|
||||
expect(SecondaryNavigation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
navigation: [
|
||||
{
|
||||
id: "profile",
|
||||
label: "Profile",
|
||||
href: "/environments/testEnvId/settings/profile",
|
||||
current: false,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
label: "Notifications",
|
||||
href: "/environments/testEnvId/settings/notifications",
|
||||
current: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { Session, getServerSession } from "next-auth";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import AccountSettingsLayout from "./layout";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/organization/service");
|
||||
vi.mock("@/lib/project/service");
|
||||
vi.mock("next-auth", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("next-auth")>();
|
||||
return {
|
||||
...actual,
|
||||
getServerSession: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
const mockGetOrganizationByEnvironmentId = vi.mocked(getOrganizationByEnvironmentId);
|
||||
const mockGetProjectByEnvironmentId = vi.mocked(getProjectByEnvironmentId);
|
||||
const mockGetServerSession = vi.mocked(getServerSession);
|
||||
|
||||
const mockOrganization = { id: "org_test_id" } as unknown as TOrganization;
|
||||
const mockProject = { id: "project_test_id" } as unknown as TProject;
|
||||
const mockSession = { user: { id: "user_test_id" } } as unknown as Session;
|
||||
|
||||
const t = (key: any) => key;
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => t,
|
||||
}));
|
||||
|
||||
const mockProps = {
|
||||
params: { environmentId: "env_test_id" },
|
||||
children: <div>Child Content</div>,
|
||||
};
|
||||
|
||||
describe("AccountSettingsLayout", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
mockGetOrganizationByEnvironmentId.mockResolvedValue(mockOrganization);
|
||||
mockGetProjectByEnvironmentId.mockResolvedValue(mockProject);
|
||||
mockGetServerSession.mockResolvedValue(mockSession);
|
||||
});
|
||||
|
||||
test("should render children when all data is fetched successfully", async () => {
|
||||
render(await AccountSettingsLayout(mockProps));
|
||||
expect(screen.getByText("Child Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should throw error if organization is not found", async () => {
|
||||
mockGetOrganizationByEnvironmentId.mockResolvedValue(null);
|
||||
await expect(AccountSettingsLayout(mockProps)).rejects.toThrowError("common.organization_not_found");
|
||||
});
|
||||
|
||||
test("should throw error if project is not found", async () => {
|
||||
mockGetProjectByEnvironmentId.mockResolvedValue(null);
|
||||
await expect(AccountSettingsLayout(mockProps)).rejects.toThrowError("common.project_not_found");
|
||||
});
|
||||
|
||||
test("should throw error if session is not found", async () => {
|
||||
mockGetServerSession.mockResolvedValue(null);
|
||||
await expect(AccountSettingsLayout(mockProps)).rejects.toThrowError("common.session_not_found");
|
||||
});
|
||||
});
|
||||
@@ -1,268 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { Membership } from "../types";
|
||||
import { EditAlerts } from "./EditAlerts";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/modules/ui/components/tooltip", () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <div data-testid="tooltip">{children}</div>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="tooltip-content">{children}</div>
|
||||
),
|
||||
TooltipProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="tooltip-provider">{children}</div>
|
||||
),
|
||||
TooltipTrigger: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="tooltip-trigger">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
HelpCircleIcon: () => <div data-testid="help-circle-icon" />,
|
||||
UsersIcon: () => <div data-testid="users-icon" />,
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href} data-testid="link">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockNotificationSwitch = vi.fn();
|
||||
vi.mock("./NotificationSwitch", () => ({
|
||||
NotificationSwitch: (props: any) => {
|
||||
mockNotificationSwitch(props);
|
||||
return (
|
||||
<div data-testid={`notification-switch-${props.surveyOrProjectOrOrganizationId}`}>
|
||||
NotificationSwitch
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUser = {
|
||||
id: "user1",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
role: "project_manager",
|
||||
objective: "other",
|
||||
emailVerified: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
identityProvider: "email",
|
||||
twoFactorEnabled: false,
|
||||
} as unknown as TUser;
|
||||
|
||||
const mockMemberships: Membership[] = [
|
||||
{
|
||||
organization: {
|
||||
id: "org1",
|
||||
name: "Organization 1",
|
||||
projects: [
|
||||
{
|
||||
id: "proj1",
|
||||
name: "Project 1",
|
||||
environments: [
|
||||
{
|
||||
id: "env1",
|
||||
surveys: [
|
||||
{ id: "survey1", name: "Survey 1 Org 1 Proj 1" },
|
||||
{ id: "survey2", name: "Survey 2 Org 1 Proj 1" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "proj2",
|
||||
name: "Project 2",
|
||||
environments: [
|
||||
{
|
||||
id: "env2",
|
||||
surveys: [{ id: "survey3", name: "Survey 3 Org 1 Proj 2" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
organization: {
|
||||
id: "org2",
|
||||
name: "Organization 2",
|
||||
projects: [
|
||||
{
|
||||
id: "proj3",
|
||||
name: "Project 3",
|
||||
environments: [
|
||||
{
|
||||
id: "env3",
|
||||
surveys: [{ id: "survey4", name: "Survey 4 Org 2 Proj 3" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
organization: {
|
||||
id: "org3",
|
||||
name: "Organization 3 No Surveys",
|
||||
projects: [
|
||||
{
|
||||
id: "proj4",
|
||||
name: "Project 4",
|
||||
environments: [
|
||||
{
|
||||
id: "env4",
|
||||
surveys: [], // No surveys in this environment
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const autoDisableNotificationType = "someType";
|
||||
const autoDisableNotificationElementId = "someElementId";
|
||||
|
||||
describe("EditAlerts", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders correctly with multiple memberships and surveys", () => {
|
||||
render(
|
||||
<EditAlerts
|
||||
memberships={mockMemberships}
|
||||
user={mockUser}
|
||||
environmentId={environmentId}
|
||||
autoDisableNotificationType={autoDisableNotificationType}
|
||||
autoDisableNotificationElementId={autoDisableNotificationElementId}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check organization names
|
||||
expect(screen.getByText("Organization 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Organization 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Organization 3 No Surveys")).toBeInTheDocument();
|
||||
|
||||
// Check survey names and project names as subtext
|
||||
expect(screen.getByText("Survey 1 Org 1 Proj 1")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Project 1")[0]).toBeInTheDocument(); // Project name under survey
|
||||
expect(screen.getByText("Survey 2 Org 1 Proj 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Survey 3 Org 1 Proj 2")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Project 2")[0]).toBeInTheDocument();
|
||||
expect(screen.getByText("Survey 4 Org 2 Proj 3")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Project 3")[0]).toBeInTheDocument();
|
||||
|
||||
// Check "No surveys found" message for org3
|
||||
const org3Heading = screen.getByText("Organization 3 No Surveys");
|
||||
expect(org3Heading.parentElement?.parentElement?.parentElement).toHaveTextContent(
|
||||
"common.no_surveys_found"
|
||||
);
|
||||
|
||||
// Check NotificationSwitch calls
|
||||
// Org 1 auto-subscribe
|
||||
expect(mockNotificationSwitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
surveyOrProjectOrOrganizationId: "org1",
|
||||
notificationType: "unsubscribedOrganizationIds",
|
||||
autoDisableNotificationType,
|
||||
autoDisableNotificationElementId,
|
||||
})
|
||||
);
|
||||
// Survey 1
|
||||
expect(mockNotificationSwitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
surveyOrProjectOrOrganizationId: "survey1",
|
||||
notificationType: "alert",
|
||||
autoDisableNotificationType,
|
||||
autoDisableNotificationElementId,
|
||||
})
|
||||
);
|
||||
// Survey 4
|
||||
expect(mockNotificationSwitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
surveyOrProjectOrOrganizationId: "survey4",
|
||||
notificationType: "alert",
|
||||
autoDisableNotificationType,
|
||||
autoDisableNotificationElementId,
|
||||
})
|
||||
);
|
||||
|
||||
// Check tooltip
|
||||
expect(screen.getAllByTestId("tooltip-provider").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByTestId("tooltip").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByTestId("tooltip-trigger").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByTestId("tooltip-content")[0]).toHaveTextContent(
|
||||
"environments.settings.notifications.every_response_tooltip"
|
||||
);
|
||||
expect(screen.getAllByTestId("help-circle-icon").length).toBeGreaterThan(0);
|
||||
|
||||
// Check invite link
|
||||
const inviteLinks = screen.getAllByTestId("link");
|
||||
const specificInviteLink = inviteLinks.find(
|
||||
(link) => link.getAttribute("href") === `/environments/${environmentId}/settings/general`
|
||||
);
|
||||
expect(specificInviteLink).toBeInTheDocument();
|
||||
expect(specificInviteLink).toHaveTextContent("common.invite_them");
|
||||
|
||||
// Check UsersIcon
|
||||
expect(screen.getAllByTestId("users-icon").length).toBe(mockMemberships.length);
|
||||
});
|
||||
|
||||
test("renders correctly when a membership has no surveys", () => {
|
||||
const singleMembershipNoSurveys: Membership[] = [
|
||||
{
|
||||
organization: {
|
||||
id: "org-no-survey",
|
||||
name: "Org Without Surveys",
|
||||
projects: [
|
||||
{
|
||||
id: "proj-no-survey",
|
||||
name: "Project Without Surveys",
|
||||
environments: [
|
||||
{
|
||||
id: "env-no-survey",
|
||||
surveys: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
render(
|
||||
<EditAlerts
|
||||
memberships={singleMembershipNoSurveys}
|
||||
user={mockUser}
|
||||
environmentId={environmentId}
|
||||
autoDisableNotificationType={autoDisableNotificationType}
|
||||
autoDisableNotificationElementId={autoDisableNotificationElementId}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Org Without Surveys")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.no_surveys_found")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Survey 1 Org 1 Proj 1")).not.toBeInTheDocument(); // Ensure other surveys aren't rendered
|
||||
|
||||
// Check NotificationSwitch for organization auto-subscribe
|
||||
expect(mockNotificationSwitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
surveyOrProjectOrOrganizationId: "org-no-survey",
|
||||
notificationType: "unsubscribedOrganizationIds",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,166 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { Membership } from "../types";
|
||||
import { EditWeeklySummary } from "./EditWeeklySummary";
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
UsersIcon: () => <div data-testid="users-icon" />,
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href} data-testid="link">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockNotificationSwitch = vi.fn();
|
||||
vi.mock("./NotificationSwitch", () => ({
|
||||
NotificationSwitch: (props: any) => {
|
||||
mockNotificationSwitch(props);
|
||||
return (
|
||||
<div data-testid={`notification-switch-${props.surveyOrProjectOrOrganizationId}`}>
|
||||
NotificationSwitch
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
const mockT = vi.fn((key) => key);
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: mockT,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockUser = {
|
||||
id: "user1",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {
|
||||
proj1: true,
|
||||
proj3: false,
|
||||
},
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
role: "project_manager",
|
||||
objective: "other",
|
||||
emailVerified: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
identityProvider: "email",
|
||||
twoFactorEnabled: false,
|
||||
} as unknown as TUser;
|
||||
|
||||
const mockMemberships: Membership[] = [
|
||||
{
|
||||
organization: {
|
||||
id: "org1",
|
||||
name: "Organization 1",
|
||||
projects: [
|
||||
{ id: "proj1", name: "Project 1", environments: [] },
|
||||
{ id: "proj2", name: "Project 2", environments: [] },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
organization: {
|
||||
id: "org2",
|
||||
name: "Organization 2",
|
||||
projects: [{ id: "proj3", name: "Project 3", environments: [] }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
|
||||
describe("EditWeeklySummary", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders correctly with multiple memberships and projects", () => {
|
||||
render(<EditWeeklySummary memberships={mockMemberships} user={mockUser} environmentId={environmentId} />);
|
||||
|
||||
expect(screen.getByText("Organization 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Project 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Project 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Organization 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Project 3")).toBeInTheDocument();
|
||||
|
||||
expect(mockNotificationSwitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
surveyOrProjectOrOrganizationId: "proj1",
|
||||
notificationSettings: mockUser.notificationSettings,
|
||||
notificationType: "weeklySummary",
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId("notification-switch-proj1")).toBeInTheDocument();
|
||||
|
||||
expect(mockNotificationSwitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
surveyOrProjectOrOrganizationId: "proj2",
|
||||
notificationSettings: mockUser.notificationSettings,
|
||||
notificationType: "weeklySummary",
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId("notification-switch-proj2")).toBeInTheDocument();
|
||||
|
||||
expect(mockNotificationSwitch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
surveyOrProjectOrOrganizationId: "proj3",
|
||||
notificationSettings: mockUser.notificationSettings,
|
||||
notificationType: "weeklySummary",
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId("notification-switch-proj3")).toBeInTheDocument();
|
||||
|
||||
const inviteLinks = screen.getAllByTestId("link");
|
||||
expect(inviteLinks.length).toBe(mockMemberships.length);
|
||||
inviteLinks.forEach((link) => {
|
||||
expect(link).toHaveAttribute("href", `/environments/${environmentId}/settings/general`);
|
||||
expect(link).toHaveTextContent("common.invite_them");
|
||||
});
|
||||
|
||||
expect(screen.getAllByTestId("users-icon").length).toBe(mockMemberships.length);
|
||||
|
||||
expect(screen.getAllByText("common.project")[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.weekly_summary")[0]).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getAllByText("environments.settings.notifications.want_to_loop_in_organization_mates?").length
|
||||
).toBe(mockMemberships.length);
|
||||
});
|
||||
|
||||
test("renders correctly with no memberships", () => {
|
||||
render(<EditWeeklySummary memberships={[]} user={mockUser} environmentId={environmentId} />);
|
||||
expect(screen.queryByText("Organization 1")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("users-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly when an organization has no projects", () => {
|
||||
const membershipsWithNoProjects: Membership[] = [
|
||||
{
|
||||
organization: {
|
||||
id: "org3",
|
||||
name: "Organization No Projects",
|
||||
projects: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
render(
|
||||
<EditWeeklySummary
|
||||
memberships={membershipsWithNoProjects}
|
||||
user={mockUser}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Organization No Projects")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Project 1")).not.toBeInTheDocument(); // Check that no projects are listed under it
|
||||
expect(mockNotificationSwitch).not.toHaveBeenCalled(); // No projects, so no switches for projects
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { IntegrationsTip } from "./IntegrationsTip";
|
||||
|
||||
vi.mock("@/modules/ui/components/icons", () => ({
|
||||
SlackIcon: () => <div data-testid="slack-icon" />,
|
||||
}));
|
||||
|
||||
const mockT = vi.fn((key) => key);
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: mockT,
|
||||
}),
|
||||
}));
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
|
||||
describe("IntegrationsTip", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders the component with correct text and link", () => {
|
||||
render(<IntegrationsTip environmentId={environmentId} />);
|
||||
|
||||
expect(screen.getByTestId("slack-icon")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.settings.notifications.need_slack_or_discord_notifications?")
|
||||
).toBeInTheDocument();
|
||||
|
||||
const linkElement = screen.getByText("environments.settings.notifications.use_the_integration");
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
expect(linkElement).toHaveAttribute("href", `/environments/${environmentId}/integrations`);
|
||||
});
|
||||
});
|
||||
@@ -1,249 +0,0 @@
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
import { updateNotificationSettingsAction } from "../actions";
|
||||
import { NotificationSwitch } from "./NotificationSwitch";
|
||||
|
||||
vi.mock("@/modules/ui/components/switch", () => ({
|
||||
Switch: vi.fn(({ checked, disabled, onCheckedChange, id, "aria-label": ariaLabel }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid={id}
|
||||
aria-label={ariaLabel}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={onCheckedChange}
|
||||
/>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("../actions", () => ({
|
||||
updateNotificationSettingsAction: vi.fn(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
const surveyId = "survey1";
|
||||
const projectId = "project1";
|
||||
const organizationId = "org1";
|
||||
|
||||
const baseNotificationSettings: TUserNotificationSettings = {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
unsubscribedOrganizationIds: [],
|
||||
};
|
||||
|
||||
describe("NotificationSwitch", () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const renderSwitch = (props: Partial<React.ComponentProps<typeof NotificationSwitch>>) => {
|
||||
const defaultProps: React.ComponentProps<typeof NotificationSwitch> = {
|
||||
surveyOrProjectOrOrganizationId: surveyId,
|
||||
notificationSettings: JSON.parse(JSON.stringify(baseNotificationSettings)),
|
||||
notificationType: "alert",
|
||||
};
|
||||
return render(<NotificationSwitch {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
test("renders with initial checked state for 'alert' (true)", () => {
|
||||
const settings = { ...baseNotificationSettings, alert: { [surveyId]: true } };
|
||||
renderSwitch({ notificationSettings: settings, notificationType: "alert" });
|
||||
const switchInput = screen.getByLabelText("toggle notification settings for alert") as HTMLInputElement;
|
||||
expect(switchInput.checked).toBe(true);
|
||||
});
|
||||
|
||||
test("renders with initial checked state for 'alert' (false)", () => {
|
||||
const settings = { ...baseNotificationSettings, alert: { [surveyId]: false } };
|
||||
renderSwitch({ notificationSettings: settings, notificationType: "alert" });
|
||||
const switchInput = screen.getByLabelText("toggle notification settings for alert") as HTMLInputElement;
|
||||
expect(switchInput.checked).toBe(false);
|
||||
});
|
||||
|
||||
test("renders with initial checked state for 'weeklySummary' (true)", () => {
|
||||
const settings = { ...baseNotificationSettings, weeklySummary: { [projectId]: true } };
|
||||
renderSwitch({
|
||||
surveyOrProjectOrOrganizationId: projectId,
|
||||
notificationSettings: settings,
|
||||
notificationType: "weeklySummary",
|
||||
});
|
||||
const switchInput = screen.getByLabelText(
|
||||
"toggle notification settings for weeklySummary"
|
||||
) as HTMLInputElement;
|
||||
expect(switchInput.checked).toBe(true);
|
||||
});
|
||||
|
||||
test("renders with initial checked state for 'unsubscribedOrganizationIds' (subscribed initially, so checked is true)", () => {
|
||||
const settings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [] };
|
||||
renderSwitch({
|
||||
surveyOrProjectOrOrganizationId: organizationId,
|
||||
notificationSettings: settings,
|
||||
notificationType: "unsubscribedOrganizationIds",
|
||||
});
|
||||
const switchInput = screen.getByLabelText(
|
||||
"toggle notification settings for unsubscribedOrganizationIds"
|
||||
) as HTMLInputElement;
|
||||
expect(switchInput.checked).toBe(true); // Not in unsubscribed list means subscribed
|
||||
});
|
||||
|
||||
test("renders with initial checked state for 'unsubscribedOrganizationIds' (unsubscribed initially, so checked is false)", () => {
|
||||
const settings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [organizationId] };
|
||||
renderSwitch({
|
||||
surveyOrProjectOrOrganizationId: organizationId,
|
||||
notificationSettings: settings,
|
||||
notificationType: "unsubscribedOrganizationIds",
|
||||
});
|
||||
const switchInput = screen.getByLabelText(
|
||||
"toggle notification settings for unsubscribedOrganizationIds"
|
||||
) as HTMLInputElement;
|
||||
expect(switchInput.checked).toBe(false); // In unsubscribed list means unsubscribed
|
||||
});
|
||||
|
||||
test("handles switch change for 'alert' type", async () => {
|
||||
const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } };
|
||||
renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" });
|
||||
const switchInput = screen.getByLabelText("toggle notification settings for alert");
|
||||
|
||||
await act(async () => {
|
||||
await user.click(switchInput);
|
||||
});
|
||||
|
||||
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||
notificationSettings: { ...initialSettings, alert: { [surveyId]: true } },
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
"environments.settings.notifications.notification_settings_updated",
|
||||
{ id: "notification-switch" }
|
||||
);
|
||||
expect(switchInput).toBeEnabled(); // Check if not disabled after action
|
||||
});
|
||||
|
||||
test("handles switch change for 'unsubscribedOrganizationIds' (subscribe)", async () => {
|
||||
const initialSettings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [organizationId] }; // initially unsubscribed
|
||||
renderSwitch({
|
||||
surveyOrProjectOrOrganizationId: organizationId,
|
||||
notificationSettings: initialSettings,
|
||||
notificationType: "unsubscribedOrganizationIds",
|
||||
});
|
||||
const switchInput = screen.getByLabelText("toggle notification settings for unsubscribedOrganizationIds");
|
||||
|
||||
await act(async () => {
|
||||
await user.click(switchInput);
|
||||
});
|
||||
|
||||
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||
notificationSettings: { ...initialSettings, unsubscribedOrganizationIds: [] }, // should be removed from list
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
"environments.settings.notifications.notification_settings_updated",
|
||||
{ id: "notification-switch" }
|
||||
);
|
||||
});
|
||||
|
||||
test("handles switch change for 'unsubscribedOrganizationIds' (unsubscribe)", async () => {
|
||||
const initialSettings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [] }; // initially subscribed
|
||||
renderSwitch({
|
||||
surveyOrProjectOrOrganizationId: organizationId,
|
||||
notificationSettings: initialSettings,
|
||||
notificationType: "unsubscribedOrganizationIds",
|
||||
});
|
||||
const switchInput = screen.getByLabelText("toggle notification settings for unsubscribedOrganizationIds");
|
||||
|
||||
await act(async () => {
|
||||
await user.click(switchInput);
|
||||
});
|
||||
|
||||
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||
notificationSettings: { ...initialSettings, unsubscribedOrganizationIds: [organizationId] }, // should be added to list
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
"environments.settings.notifications.notification_settings_updated",
|
||||
{ id: "notification-switch" }
|
||||
);
|
||||
});
|
||||
|
||||
test("useEffect: auto-disables 'alert' notification if conditions met", () => {
|
||||
const settings = { ...baseNotificationSettings, alert: { [surveyId]: true } }; // Initially true
|
||||
renderSwitch({
|
||||
surveyOrProjectOrOrganizationId: surveyId,
|
||||
notificationSettings: settings,
|
||||
notificationType: "alert",
|
||||
autoDisableNotificationType: "alert",
|
||||
autoDisableNotificationElementId: surveyId,
|
||||
});
|
||||
|
||||
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||
notificationSettings: { ...settings, alert: { [surveyId]: false } },
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
"environments.settings.notifications.you_will_not_receive_any_more_emails_for_responses_on_this_survey",
|
||||
{ id: "notification-switch" }
|
||||
);
|
||||
});
|
||||
|
||||
test("useEffect: auto-disables 'unsubscribedOrganizationIds' (auto-unsubscribes) if conditions met", () => {
|
||||
const settings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [] }; // Initially subscribed
|
||||
renderSwitch({
|
||||
surveyOrProjectOrOrganizationId: organizationId,
|
||||
notificationSettings: settings,
|
||||
notificationType: "unsubscribedOrganizationIds",
|
||||
autoDisableNotificationType: "someOtherType", // This prop is used to trigger the effect, not directly for type matching in this case
|
||||
autoDisableNotificationElementId: organizationId,
|
||||
});
|
||||
|
||||
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||
notificationSettings: { ...settings, unsubscribedOrganizationIds: [organizationId] },
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
"environments.settings.notifications.you_will_not_be_auto_subscribed_to_this_organizations_surveys_anymore",
|
||||
{ id: "notification-switch" }
|
||||
);
|
||||
});
|
||||
|
||||
test("useEffect: does not auto-disable if 'autoDisableNotificationElementId' does not match", () => {
|
||||
const settings = { ...baseNotificationSettings, alert: { [surveyId]: true } };
|
||||
renderSwitch({
|
||||
surveyOrProjectOrOrganizationId: surveyId,
|
||||
notificationSettings: settings,
|
||||
notificationType: "alert",
|
||||
autoDisableNotificationType: "alert",
|
||||
autoDisableNotificationElementId: "otherId", // Mismatch
|
||||
});
|
||||
expect(updateNotificationSettingsAction).not.toHaveBeenCalled();
|
||||
expect(toast.success).not.toHaveBeenCalledWith(
|
||||
"environments.settings.notifications.you_will_not_receive_any_more_emails_for_responses_on_this_survey"
|
||||
);
|
||||
});
|
||||
|
||||
test("useEffect: does not auto-disable if not checked initially for 'alert'", () => {
|
||||
const settings = { ...baseNotificationSettings, alert: { [surveyId]: false } }; // Initially false
|
||||
renderSwitch({
|
||||
surveyOrProjectOrOrganizationId: surveyId,
|
||||
notificationSettings: settings,
|
||||
notificationType: "alert",
|
||||
autoDisableNotificationType: "alert",
|
||||
autoDisableNotificationElementId: surveyId,
|
||||
});
|
||||
expect(updateNotificationSettingsAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("useEffect: does not auto-disable if not checked initially for 'unsubscribedOrganizationIds' (already unsubscribed)", () => {
|
||||
const settings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [organizationId] }; // Initially unsubscribed
|
||||
renderSwitch({
|
||||
surveyOrProjectOrOrganizationId: organizationId,
|
||||
notificationSettings: settings,
|
||||
notificationType: "unsubscribedOrganizationIds",
|
||||
autoDisableNotificationType: "someType",
|
||||
autoDisableNotificationElementId: organizationId,
|
||||
});
|
||||
expect(updateNotificationSettingsAction).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Loading from "./loading";
|
||||
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="page-content-wrapper">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: ({ pageTitle }: { pageTitle: string }) => <div data-testid="page-header">{pageTitle}</div>,
|
||||
}));
|
||||
|
||||
describe("Loading Notifications Settings", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders loading state correctly", () => {
|
||||
render(<Loading />);
|
||||
|
||||
expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument();
|
||||
const pageHeader = screen.getByTestId("page-header");
|
||||
expect(pageHeader).toBeInTheDocument();
|
||||
expect(pageHeader).toHaveTextContent("common.account_settings");
|
||||
|
||||
// Check for Alerts LoadingCard
|
||||
expect(screen.getByText("environments.settings.notifications.email_alerts_surveys")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.settings.notifications.set_up_an_alert_to_get_an_email_on_new_responses")
|
||||
).toBeInTheDocument();
|
||||
const alertsCard = screen
|
||||
.getByText("environments.settings.notifications.email_alerts_surveys")
|
||||
.closest("div[class*='rounded-xl']"); // Find parent card
|
||||
expect(alertsCard).toBeInTheDocument();
|
||||
|
||||
// Check for Weekly Summary LoadingCard
|
||||
expect(
|
||||
screen.getByText("environments.settings.notifications.weekly_summary_projects")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.settings.notifications.stay_up_to_date_with_a_Weekly_every_Monday")
|
||||
).toBeInTheDocument();
|
||||
const weeklySummaryCard = screen
|
||||
.getByText("environments.settings.notifications.weekly_summary_projects")
|
||||
.closest("div[class*='rounded-xl']"); // Find parent card
|
||||
expect(weeklySummaryCard).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,258 +0,0 @@
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { EditAlerts } from "./components/EditAlerts";
|
||||
import { EditWeeklySummary } from "./components/EditWeeklySummary";
|
||||
import Page from "./page";
|
||||
import { Membership } from "./types";
|
||||
|
||||
// Mock external dependencies
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar",
|
||||
() => ({
|
||||
AccountSettingsNavbar: ({ activeId }) => <div>AccountSettingsNavbar activeId={activeId}</div>,
|
||||
})
|
||||
);
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({
|
||||
SettingsCard: ({ title, description, children }) => (
|
||||
<div>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/lib/user/service", () => ({
|
||||
getUser: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/auth/lib/authOptions", () => ({
|
||||
authOptions: {},
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: ({ children }) => <div>{children}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: ({ pageTitle, children }) => (
|
||||
<div>
|
||||
<h1>{pageTitle}</h1>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
vi.mock("next-auth", () => ({
|
||||
getServerSession: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
membership: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("./components/EditAlerts", () => ({
|
||||
EditAlerts: vi.fn(() => <div>EditAlertsComponent</div>),
|
||||
}));
|
||||
vi.mock("./components/EditWeeklySummary", () => ({
|
||||
EditWeeklySummary: vi.fn(() => <div>EditWeeklySummaryComponent</div>),
|
||||
}));
|
||||
vi.mock("./components/IntegrationsTip", () => ({
|
||||
IntegrationsTip: () => <div>IntegrationsTipComponent</div>,
|
||||
}));
|
||||
|
||||
const mockUser: Partial<TUser> = {
|
||||
id: "user-1",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
notificationSettings: {
|
||||
alert: { "survey-old": true },
|
||||
weeklySummary: { "project-old": true },
|
||||
unsubscribedOrganizationIds: ["org-unsubscribed"],
|
||||
},
|
||||
};
|
||||
|
||||
const mockMemberships: Membership[] = [
|
||||
{
|
||||
organization: {
|
||||
id: "org-1",
|
||||
name: "Org 1",
|
||||
projects: [
|
||||
{
|
||||
id: "project-1",
|
||||
name: "Project 1",
|
||||
environments: [
|
||||
{
|
||||
id: "env-prod-1",
|
||||
surveys: [
|
||||
{ id: "survey-1", name: "Survey 1" },
|
||||
{ id: "survey-2", name: "Survey 2" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockSession = {
|
||||
user: {
|
||||
id: "user-1",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mockParams = { environmentId: "env-1" };
|
||||
const mockSearchParams = {
|
||||
type: "alertTest",
|
||||
elementId: "elementTestId",
|
||||
};
|
||||
|
||||
describe("NotificationsPage", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getServerSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser as TUser);
|
||||
vi.mocked(prisma.membership.findMany).mockResolvedValue(mockMemberships as any); // Prisma types can be complex
|
||||
});
|
||||
|
||||
test("renders correctly with user and memberships, and processes notification settings", async () => {
|
||||
const props = { params: mockParams, searchParams: mockSearchParams };
|
||||
const PageComponent = await Page(props);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("common.account_settings")).toBeInTheDocument();
|
||||
expect(screen.getByText("AccountSettingsNavbar activeId=notifications")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.settings.notifications.email_alerts_surveys")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.settings.notifications.set_up_an_alert_to_get_an_email_on_new_responses")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("EditAlertsComponent")).toBeInTheDocument();
|
||||
expect(screen.getByText("IntegrationsTipComponent")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.settings.notifications.weekly_summary_projects")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.settings.notifications.stay_up_to_date_with_a_Weekly_every_Monday")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("EditWeeklySummaryComponent")).toBeInTheDocument();
|
||||
|
||||
// The actual `user.notificationSettings` passed to EditAlerts will be a new object
|
||||
// after `setCompleteNotificationSettings` processes it.
|
||||
// We verify the structure and defaults.
|
||||
const editAlertsCall = vi.mocked(EditAlerts).mock.calls[0][0];
|
||||
expect(editAlertsCall.user.notificationSettings.alert["survey-1"]).toBe(false);
|
||||
expect(editAlertsCall.user.notificationSettings.alert["survey-2"]).toBe(false);
|
||||
// If "survey-old" was not part of any membership survey, it might be removed or kept depending on exact logic.
|
||||
// The current logic only adds keys from memberships. So "survey-old" would be gone from .alert
|
||||
// Let's adjust expectation based on `setCompleteNotificationSettings`
|
||||
// It iterates memberships, then projects, then environments, then surveys.
|
||||
// `newNotificationSettings.alert[survey.id] = notificationSettings[survey.id]?.responseFinished || (notificationSettings.alert && notificationSettings.alert[survey.id]) || false;`
|
||||
// This means only survey IDs found in memberships will be in the new `alert` object.
|
||||
// `newNotificationSettings.weeklySummary[project.id]` also only adds project IDs from memberships.
|
||||
|
||||
const finalExpectedSettings = {
|
||||
alert: {
|
||||
"survey-1": false,
|
||||
"survey-2": false,
|
||||
},
|
||||
weeklySummary: {
|
||||
"project-1": false,
|
||||
},
|
||||
unsubscribedOrganizationIds: ["org-unsubscribed"],
|
||||
};
|
||||
|
||||
expect(editAlertsCall.user.notificationSettings).toEqual(finalExpectedSettings);
|
||||
expect(editAlertsCall.memberships).toEqual(mockMemberships);
|
||||
expect(editAlertsCall.environmentId).toBe(mockParams.environmentId);
|
||||
expect(editAlertsCall.autoDisableNotificationType).toBe(mockSearchParams.type);
|
||||
expect(editAlertsCall.autoDisableNotificationElementId).toBe(mockSearchParams.elementId);
|
||||
|
||||
const editWeeklySummaryCall = vi.mocked(EditWeeklySummary).mock.calls[0][0];
|
||||
expect(editWeeklySummaryCall.user.notificationSettings).toEqual(finalExpectedSettings);
|
||||
expect(editWeeklySummaryCall.memberships).toEqual(mockMemberships);
|
||||
expect(editWeeklySummaryCall.environmentId).toBe(mockParams.environmentId);
|
||||
});
|
||||
|
||||
test("throws error if session is not found", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue(null);
|
||||
const props = { params: mockParams, searchParams: {} };
|
||||
await expect(Page(props)).rejects.toThrow("common.session_not_found");
|
||||
});
|
||||
|
||||
test("throws error if user is not found", async () => {
|
||||
vi.mocked(getUser).mockResolvedValue(null);
|
||||
const props = { params: mockParams, searchParams: {} };
|
||||
await expect(Page(props)).rejects.toThrow("common.user_not_found");
|
||||
});
|
||||
|
||||
test("renders with empty memberships and default notification settings", async () => {
|
||||
vi.mocked(prisma.membership.findMany).mockResolvedValue([]);
|
||||
const userWithNoSpecificSettings = {
|
||||
...mockUser,
|
||||
notificationSettings: { unsubscribedOrganizationIds: [] }, // Start fresh
|
||||
};
|
||||
vi.mocked(getUser).mockResolvedValue(userWithNoSpecificSettings as unknown as TUser);
|
||||
|
||||
const props = { params: mockParams, searchParams: {} };
|
||||
const PageComponent = await Page(props);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("EditAlertsComponent")).toBeInTheDocument();
|
||||
expect(screen.getByText("EditWeeklySummaryComponent")).toBeInTheDocument();
|
||||
|
||||
const expectedEmptySettings = {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
unsubscribedOrganizationIds: [],
|
||||
};
|
||||
|
||||
const editAlertsCall = vi.mocked(EditAlerts).mock.calls[0][0];
|
||||
expect(editAlertsCall.user.notificationSettings).toEqual(expectedEmptySettings);
|
||||
expect(editAlertsCall.memberships).toEqual([]);
|
||||
|
||||
const editWeeklySummaryCall = vi.mocked(EditWeeklySummary).mock.calls[0][0];
|
||||
expect(editWeeklySummaryCall.user.notificationSettings).toEqual(expectedEmptySettings);
|
||||
expect(editWeeklySummaryCall.memberships).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles legacy notification settings correctly", async () => {
|
||||
const userWithLegacySettings: Partial<TUser> = {
|
||||
id: "user-legacy",
|
||||
notificationSettings: {
|
||||
"survey-1": { responseFinished: true }, // Legacy alert for survey-1
|
||||
weeklySummary: { "project-1": true },
|
||||
unsubscribedOrganizationIds: [],
|
||||
} as any, // To allow legacy structure
|
||||
};
|
||||
vi.mocked(getUser).mockResolvedValue(userWithLegacySettings as TUser);
|
||||
// Memberships define survey-1 and project-1
|
||||
vi.mocked(prisma.membership.findMany).mockResolvedValue(mockMemberships as any);
|
||||
|
||||
const props = { params: mockParams, searchParams: {} };
|
||||
const PageComponent = await Page(props);
|
||||
render(PageComponent);
|
||||
|
||||
const expectedProcessedSettings = {
|
||||
alert: {
|
||||
"survey-1": true, // Should be true due to legacy setting
|
||||
"survey-2": false, // Default for other surveys in membership
|
||||
},
|
||||
weeklySummary: {
|
||||
"project-1": true, // From user's weeklySummary
|
||||
},
|
||||
unsubscribedOrganizationIds: [],
|
||||
};
|
||||
|
||||
const editAlertsCall = vi.mocked(EditAlerts).mock.calls[0][0];
|
||||
expect(editAlertsCall.user.notificationSettings).toEqual(expectedProcessedSettings);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import { ZId } from "@formbricks/types/common";
|
||||
import { ZUserUpdateInput } from "@formbricks/types/user";
|
||||
|
||||
export const updateUserAction = authenticatedActionClient
|
||||
.schema(ZUserUpdateInput.pick({ name: true, locale: true }))
|
||||
.schema(ZUserUpdateInput.partial())
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await updateUser(ctx.user.id, parsedInput);
|
||||
});
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { AccountSecurity } from "./AccountSecurity";
|
||||
|
||||
vi.mock("@/modules/ee/two-factor-auth/components/enable-two-factor-modal", () => ({
|
||||
EnableTwoFactorModal: ({ open }) =>
|
||||
open ? <div data-testid="enable-2fa-modal">EnableTwoFactorModal</div> : null,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/two-factor-auth/components/disable-two-factor-modal", () => ({
|
||||
DisableTwoFactorModal: ({ open }) =>
|
||||
open ? <div data-testid="disable-2fa-modal">DisableTwoFactorModal</div> : null,
|
||||
}));
|
||||
|
||||
const mockUser = {
|
||||
id: "test-user-id",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: "project_manager",
|
||||
objective: "other",
|
||||
} as unknown as TUser;
|
||||
|
||||
describe("AccountSecurity", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("renders correctly with 2FA disabled", () => {
|
||||
render(<AccountSecurity user={{ ...mockUser, twoFactorEnabled: false }} />);
|
||||
expect(screen.getByText("environments.settings.profile.two_factor_authentication")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.settings.profile.two_factor_authentication_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("switch")).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("renders correctly with 2FA enabled", () => {
|
||||
render(<AccountSecurity user={{ ...mockUser, twoFactorEnabled: true }} />);
|
||||
expect(screen.getByRole("switch")).toBeChecked();
|
||||
});
|
||||
|
||||
test("opens EnableTwoFactorModal when switch is turned on", async () => {
|
||||
render(<AccountSecurity user={{ ...mockUser, twoFactorEnabled: false }} />);
|
||||
const switchControl = screen.getByRole("switch");
|
||||
await userEvent.click(switchControl);
|
||||
expect(screen.getByTestId("enable-2fa-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("opens DisableTwoFactorModal when switch is turned off", async () => {
|
||||
render(<AccountSecurity user={{ ...mockUser, twoFactorEnabled: true }} />);
|
||||
const switchControl = screen.getByRole("switch");
|
||||
await userEvent.click(switchControl);
|
||||
expect(screen.getByTestId("disable-2fa-modal")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Session } from "next-auth";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { DeleteAccount } from "./DeleteAccount";
|
||||
|
||||
vi.mock("@/modules/account/components/DeleteAccountModal", () => ({
|
||||
DeleteAccountModal: ({ open }) =>
|
||||
open ? <div data-testid="delete-account-modal">DeleteAccountModal</div> : null,
|
||||
}));
|
||||
|
||||
const mockUser = {
|
||||
id: "user1",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
notificationSettings: { alert: {}, weeklySummary: {}, unsubscribedOrganizationIds: [] },
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: "project_manager",
|
||||
objective: "other",
|
||||
} as unknown as TUser;
|
||||
|
||||
const mockSession: Session = {
|
||||
user: mockUser,
|
||||
expires: new Date(Date.now() + 2 * 86400).toISOString(),
|
||||
};
|
||||
|
||||
const mockOrganizations: TOrganization[] = [
|
||||
{
|
||||
id: "org1",
|
||||
name: "Org 1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
billing: {
|
||||
stripeCustomerId: "cus_123",
|
||||
} as unknown as TOrganization["billing"],
|
||||
} as unknown as TOrganization,
|
||||
];
|
||||
|
||||
describe("DeleteAccount", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("renders correctly and opens modal on click", async () => {
|
||||
render(
|
||||
<DeleteAccount
|
||||
session={mockSession}
|
||||
IS_FORMBRICKS_CLOUD={true}
|
||||
user={mockUser}
|
||||
organizationsWithSingleOwner={[]}
|
||||
isMultiOrgEnabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("environments.settings.profile.warning_cannot_undo")).toBeInTheDocument();
|
||||
const deleteButton = screen.getByText("environments.settings.profile.confirm_delete_my_account");
|
||||
expect(deleteButton).toBeEnabled();
|
||||
await userEvent.click(deleteButton);
|
||||
expect(screen.getByTestId("delete-account-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders null if session is not provided", () => {
|
||||
const { container } = render(
|
||||
<DeleteAccount
|
||||
session={null}
|
||||
IS_FORMBRICKS_CLOUD={true}
|
||||
user={mockUser}
|
||||
organizationsWithSingleOwner={[]}
|
||||
isMultiOrgEnabled={true}
|
||||
/>
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
test("enables delete button if multi-org enabled even if user is single owner", () => {
|
||||
render(
|
||||
<DeleteAccount
|
||||
session={mockSession}
|
||||
IS_FORMBRICKS_CLOUD={false}
|
||||
user={mockUser}
|
||||
organizationsWithSingleOwner={mockOrganizations}
|
||||
isMultiOrgEnabled={true}
|
||||
/>
|
||||
);
|
||||
const deleteButton = screen.getByText("environments.settings.profile.confirm_delete_my_account");
|
||||
expect(deleteButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import * as profileActions from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions";
|
||||
import * as fileUploadHooks from "@/app/lib/fileUpload";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Session } from "next-auth";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { EditProfileAvatarForm } from "./EditProfileAvatarForm";
|
||||
|
||||
vi.mock("@/modules/ui/components/avatars", () => ({
|
||||
ProfileAvatar: ({ imageUrl }) => <div data-testid="profile-avatar">{imageUrl || "No Avatar"}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
refresh: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions", () => ({
|
||||
updateAvatarAction: vi.fn(),
|
||||
removeAvatarAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/lib/fileUpload", () => ({
|
||||
handleFileUpload: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockSession: Session = {
|
||||
user: { id: "user-id" },
|
||||
expires: "session-expires-at",
|
||||
};
|
||||
const environmentId = "test-env-id";
|
||||
|
||||
describe("EditProfileAvatarForm", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(profileActions.updateAvatarAction).mockResolvedValue({});
|
||||
vi.mocked(profileActions.removeAvatarAction).mockResolvedValue({});
|
||||
vi.mocked(fileUploadHooks.handleFileUpload).mockResolvedValue({
|
||||
url: "new-avatar.jpg",
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test("renders correctly without an existing image", () => {
|
||||
render(<EditProfileAvatarForm session={mockSession} environmentId={environmentId} imageUrl={null} />);
|
||||
expect(screen.getByTestId("profile-avatar")).toHaveTextContent("No Avatar");
|
||||
expect(screen.getByText("environments.settings.profile.upload_image")).toBeInTheDocument();
|
||||
expect(screen.queryByText("environments.settings.profile.remove_image")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly with an existing image", () => {
|
||||
render(
|
||||
<EditProfileAvatarForm
|
||||
session={mockSession}
|
||||
environmentId={environmentId}
|
||||
imageUrl="existing-avatar.jpg"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("profile-avatar")).toHaveTextContent("existing-avatar.jpg");
|
||||
expect(screen.getByText("environments.settings.profile.change_image")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.settings.profile.remove_image")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles image removal successfully", async () => {
|
||||
render(
|
||||
<EditProfileAvatarForm
|
||||
session={mockSession}
|
||||
environmentId={environmentId}
|
||||
imageUrl="existing-avatar.jpg"
|
||||
/>
|
||||
);
|
||||
const removeButton = screen.getByText("environments.settings.profile.remove_image");
|
||||
await userEvent.click(removeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(profileActions.removeAvatarAction).toHaveBeenCalledWith({ environmentId });
|
||||
});
|
||||
});
|
||||
|
||||
test("shows error if removeAvatarAction fails", async () => {
|
||||
vi.mocked(profileActions.removeAvatarAction).mockRejectedValue(new Error("API error"));
|
||||
render(
|
||||
<EditProfileAvatarForm
|
||||
session={mockSession}
|
||||
environmentId={environmentId}
|
||||
imageUrl="existing-avatar.jpg"
|
||||
/>
|
||||
);
|
||||
const removeButton = screen.getByText("environments.settings.profile.remove_image");
|
||||
await userEvent.click(removeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(toast.error)).toHaveBeenCalledWith(
|
||||
"environments.settings.profile.avatar_update_failed"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,117 +0,0 @@
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { updateUserAction } from "../actions";
|
||||
import { EditProfileDetailsForm } from "./EditProfileDetailsForm";
|
||||
|
||||
const mockUser = {
|
||||
id: "test-user-id",
|
||||
name: "Old Name",
|
||||
email: "test@example.com",
|
||||
locale: "en-US",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: "project_manager",
|
||||
objective: "other",
|
||||
} as unknown as TUser;
|
||||
|
||||
// Mock window.location.reload
|
||||
const originalLocation = window.location;
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("location", {
|
||||
...originalLocation,
|
||||
reload: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions", () => ({
|
||||
updateUserAction: vi.fn(),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("EditProfileDetailsForm", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders with initial user data and updates successfully", async () => {
|
||||
vi.mocked(updateUserAction).mockResolvedValue({ ...mockUser, name: "New Name" } as any);
|
||||
|
||||
render(<EditProfileDetailsForm user={mockUser} />);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("common.full_name");
|
||||
expect(nameInput).toHaveValue(mockUser.name);
|
||||
expect(screen.getByDisplayValue(mockUser.email)).toBeDisabled();
|
||||
// Check initial language (English)
|
||||
expect(screen.getByText("English (US)")).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, "New Name");
|
||||
|
||||
// Change language
|
||||
const languageDropdownTrigger = screen.getByRole("button", { name: /English/ });
|
||||
await userEvent.click(languageDropdownTrigger);
|
||||
const germanOption = await screen.findByText("German"); // Assuming 'German' is an option
|
||||
await userEvent.click(germanOption);
|
||||
|
||||
const updateButton = screen.getByText("common.update");
|
||||
expect(updateButton).toBeEnabled();
|
||||
await userEvent.click(updateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateUserAction).toHaveBeenCalledWith({ name: "New Name", locale: "de-DE" });
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
"environments.settings.profile.profile_updated_successfully"
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test("shows error toast if update fails", async () => {
|
||||
const errorMessage = "Update failed";
|
||||
vi.mocked(updateUserAction).mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
render(<EditProfileDetailsForm user={mockUser} />);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("common.full_name");
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, "Another Name");
|
||||
|
||||
const updateButton = screen.getByText("common.update");
|
||||
await userEvent.click(updateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateUserAction).toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(`common.error: ${errorMessage}`);
|
||||
});
|
||||
});
|
||||
|
||||
test("update button is disabled initially and enables on change", async () => {
|
||||
render(<EditProfileDetailsForm user={mockUser} />);
|
||||
const updateButton = screen.getByText("common.update");
|
||||
expect(updateButton).toBeDisabled();
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("common.full_name");
|
||||
await userEvent.type(nameInput, " updated");
|
||||
expect(updateButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Loading from "./loading";
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar",
|
||||
() => ({
|
||||
AccountSettingsNavbar: ({ activeId, loading }) => (
|
||||
<div data-testid="account-settings-navbar">
|
||||
AccountSettingsNavbar - active: {activeId}, loading: {loading?.toString()}
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/app/(app)/components/LoadingCard", () => ({
|
||||
LoadingCard: ({ title, description }) => (
|
||||
<div data-testid="loading-card">
|
||||
<div>{title}</div>
|
||||
<div>{description}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: ({ pageTitle, children }) => (
|
||||
<div>
|
||||
<h1>{pageTitle}</h1>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: ({ children }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
describe("Loading", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders loading state correctly", () => {
|
||||
render(<Loading />);
|
||||
|
||||
expect(screen.getByText("common.account_settings")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("account-settings-navbar")).toHaveTextContent(
|
||||
"AccountSettingsNavbar - active: profile, loading: true"
|
||||
);
|
||||
|
||||
const loadingCards = screen.getAllByTestId("loading-card");
|
||||
expect(loadingCards).toHaveLength(3);
|
||||
|
||||
expect(loadingCards[0]).toHaveTextContent("environments.settings.profile.personal_information");
|
||||
expect(loadingCards[0]).toHaveTextContent("environments.settings.profile.update_personal_info");
|
||||
|
||||
expect(loadingCards[1]).toHaveTextContent("common.avatar");
|
||||
expect(loadingCards[1]).toHaveTextContent("environments.settings.profile.organization_identification");
|
||||
|
||||
expect(loadingCards[2]).toHaveTextContent("environments.settings.profile.delete_account");
|
||||
expect(loadingCards[2]).toHaveTextContent("environments.settings.profile.confirm_delete_account");
|
||||
});
|
||||
});
|
||||
@@ -1,188 +0,0 @@
|
||||
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import { Session } from "next-auth";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import Page from "./page";
|
||||
|
||||
// Mock services and utils
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: true,
|
||||
}));
|
||||
vi.mock("@/lib/organization/service", () => ({
|
||||
getOrganizationsWhereUserIsSingleOwner: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/user/service", () => ({
|
||||
getUser: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getIsMultiOrgEnabled: vi.fn(),
|
||||
getIsTwoFactorAuthEnabled: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
const t = (key: any) => key;
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => t,
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar",
|
||||
() => ({
|
||||
AccountSettingsNavbar: ({ environmentId, activeId }) => (
|
||||
<div data-testid="account-settings-navbar">
|
||||
AccountSettingsNavbar: {environmentId} {activeId}
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity",
|
||||
() => ({
|
||||
AccountSecurity: ({ user }) => <div data-testid="account-security">AccountSecurity: {user.id}</div>,
|
||||
})
|
||||
);
|
||||
vi.mock("./components/DeleteAccount", () => ({
|
||||
DeleteAccount: ({ user }) => <div data-testid="delete-account">DeleteAccount: {user.id}</div>,
|
||||
}));
|
||||
vi.mock("./components/EditProfileAvatarForm", () => ({
|
||||
EditProfileAvatarForm: ({ _, environmentId }) => (
|
||||
<div data-testid="edit-profile-avatar-form">EditProfileAvatarForm: {environmentId}</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("./components/EditProfileDetailsForm", () => ({
|
||||
EditProfileDetailsForm: ({ user }) => (
|
||||
<div data-testid="edit-profile-details-form">EditProfileDetailsForm: {user.id}</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/upgrade-prompt", () => ({
|
||||
UpgradePrompt: ({ title }) => <div data-testid="upgrade-prompt">{title}</div>,
|
||||
}));
|
||||
|
||||
const mockUser = {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
imageUrl: "http://example.com/avatar.png",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
notificationSettings: { alert: {}, weeklySummary: {}, unsubscribedOrganizationIds: [] },
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: "project_manager",
|
||||
objective: "other",
|
||||
} as unknown as TUser;
|
||||
|
||||
const mockSession: Session = {
|
||||
user: mockUser,
|
||||
expires: "never",
|
||||
};
|
||||
|
||||
const mockOrganizations: TOrganization[] = [];
|
||||
|
||||
const params = { environmentId: "env-123" };
|
||||
|
||||
describe("ProfilePage", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getOrganizationsWhereUserIsSingleOwner).mockResolvedValue(mockOrganizations);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
session: mockSession,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getIsTwoFactorAuthEnabled).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders profile page with all sections for email user with 2FA license", async () => {
|
||||
render(await Page({ params: Promise.resolve(params) }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("common.account_settings")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("account-settings-navbar")).toHaveTextContent(
|
||||
"AccountSettingsNavbar: env-123 profile"
|
||||
);
|
||||
expect(screen.getByTestId("edit-profile-details-form")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("edit-profile-avatar-form")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("account-security")).toBeInTheDocument(); // Shown because 2FA license is enabled
|
||||
expect(screen.queryByTestId("upgrade-prompt")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("delete-account")).toBeInTheDocument();
|
||||
// Use a regex to match the text content, allowing for variable whitespace
|
||||
expect(screen.getByText(new RegExp(`common\\.profile\\s*:\\s*${mockUser.id}`))).toBeInTheDocument(); // SettingsId
|
||||
});
|
||||
});
|
||||
|
||||
test("renders UpgradePrompt when 2FA license is disabled and user 2FA is off", async () => {
|
||||
vi.mocked(getIsTwoFactorAuthEnabled).mockResolvedValue(false); // License disabled
|
||||
const userWith2FAOff = { ...mockUser, twoFactorEnabled: false };
|
||||
vi.mocked(getUser).mockResolvedValue(userWith2FAOff);
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
session: { ...mockSession, user: userWith2FAOff },
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
render(await Page({ params: Promise.resolve(params) }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("upgrade-prompt")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("upgrade-prompt")).toHaveTextContent(
|
||||
"environments.settings.profile.unlock_two_factor_authentication"
|
||||
);
|
||||
expect(screen.queryByTestId("account-security")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders AccountSecurity when 2FA license is disabled but user 2FA is on", async () => {
|
||||
vi.mocked(getIsTwoFactorAuthEnabled).mockResolvedValue(false); // License disabled
|
||||
const userWith2FAOn = { ...mockUser, twoFactorEnabled: true };
|
||||
vi.mocked(getUser).mockResolvedValue(userWith2FAOn);
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
session: { ...mockSession, user: userWith2FAOn },
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
render(await Page({ params: Promise.resolve(params) }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("account-security")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("upgrade-prompt")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("does not render security card if identityProvider is not email", async () => {
|
||||
const nonEmailUser = { ...mockUser, identityProvider: "google" as "email" | "github" | "google" }; // type assertion
|
||||
vi.mocked(getUser).mockResolvedValue(nonEmailUser);
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
session: { ...mockSession, user: nonEmailUser },
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
render(await Page({ params: Promise.resolve(params) }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("account-security")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("upgrade-prompt")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("common.security")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error if user is not found", async () => {
|
||||
vi.mocked(getUser).mockResolvedValue(null);
|
||||
// Need to catch the promise rejection for async component errors
|
||||
try {
|
||||
// We don't await the render directly, but the component execution
|
||||
await Page({ params: Promise.resolve(params) });
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("common.user_not_found");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import LoadingPage from "./loading";
|
||||
|
||||
// Mock the IS_FORMBRICKS_CLOUD constant
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: true,
|
||||
}));
|
||||
|
||||
// Mock the actual Loading component that is being imported
|
||||
vi.mock("@/modules/organization/settings/api-keys/loading", () => ({
|
||||
default: ({ isFormbricksCloud }: { isFormbricksCloud: boolean }) => (
|
||||
<div data-testid="mocked-loading-component">isFormbricksCloud: {String(isFormbricksCloud)}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("LoadingPage for API Keys", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the underlying Loading component with correct isFormbricksCloud prop", () => {
|
||||
render(<LoadingPage />);
|
||||
const mockedLoadingComponent = screen.getByTestId("mocked-loading-component");
|
||||
expect(mockedLoadingComponent).toBeInTheDocument();
|
||||
// Check if the prop is passed correctly based on the mocked constant value
|
||||
expect(mockedLoadingComponent).toHaveTextContent("isFormbricksCloud: true");
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
|
||||
// Mock the APIKeysPage component
|
||||
vi.mock("@/modules/organization/settings/api-keys/page", () => ({
|
||||
APIKeysPage: () => <div data-testid="mocked-api-keys-page">APIKeysPage Content</div>,
|
||||
}));
|
||||
|
||||
describe("APIKeys Page", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the APIKeysPage component", () => {
|
||||
render(<Page />);
|
||||
const apiKeysPageComponent = screen.getByTestId("mocked-api-keys-page");
|
||||
expect(apiKeysPageComponent).toBeInTheDocument();
|
||||
expect(apiKeysPageComponent).toHaveTextContent("APIKeysPage Content");
|
||||
});
|
||||
});
|
||||
@@ -1,74 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import Loading from "./loading";
|
||||
|
||||
// Mock constants
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: true,
|
||||
}));
|
||||
|
||||
// Mock server-side translation
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="page-content-wrapper">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: ({ pageTitle, children }: { pageTitle: string; children: React.ReactNode }) => (
|
||||
<div data-testid="page-header">
|
||||
<h1>{pageTitle}</h1>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar",
|
||||
() => ({
|
||||
OrganizationSettingsNavbar: ({ activeId, loading }: { activeId: string; loading?: boolean }) => (
|
||||
<div data-testid="org-settings-navbar">
|
||||
Active: {activeId}, Loading: {String(loading)}
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
describe("Billing Loading Page", () => {
|
||||
beforeEach(async () => {
|
||||
const mockTranslate = vi.fn((key) => key);
|
||||
vi.mocked(await import("@/tolgee/server")).getTranslate.mockResolvedValue(mockTranslate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders PageContentWrapper, PageHeader, and OrganizationSettingsNavbar", async () => {
|
||||
render(await Loading());
|
||||
|
||||
expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument();
|
||||
const pageHeader = screen.getByTestId("page-header");
|
||||
expect(pageHeader).toBeInTheDocument();
|
||||
expect(pageHeader).toHaveTextContent("environments.settings.general.organization_settings");
|
||||
|
||||
const navbar = screen.getByTestId("org-settings-navbar");
|
||||
expect(navbar).toBeInTheDocument();
|
||||
expect(navbar).toHaveTextContent("Active: billing");
|
||||
expect(navbar).toHaveTextContent("Loading: true");
|
||||
});
|
||||
|
||||
test("renders placeholder divs", async () => {
|
||||
render(await Loading());
|
||||
// Check for the presence of divs with animate-pulse, assuming they are the placeholders
|
||||
const placeholders = screen.getAllByRole("generic", { hidden: true }); // Using a generic role as divs don't have implicit roles
|
||||
const animatedPlaceholders = placeholders.filter((el) => el.classList.contains("animate-pulse"));
|
||||
expect(animatedPlaceholders.length).toBeGreaterThanOrEqual(2); // Expecting at least two placeholder divs
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
|
||||
// Mock the PricingPage component
|
||||
vi.mock("@/modules/ee/billing/page", () => ({
|
||||
PricingPage: () => <div data-testid="mocked-pricing-page">PricingPage Content</div>,
|
||||
}));
|
||||
|
||||
describe("Billing Page", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the PricingPage component", () => {
|
||||
render(<Page />);
|
||||
const pricingPageComponent = screen.getByTestId("mocked-pricing-page");
|
||||
expect(pricingPageComponent).toBeInTheDocument();
|
||||
expect(pricingPageComponent).toHaveTextContent("PricingPage Content");
|
||||
});
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { OrganizationSettingsNavbar } from "./OrganizationSettingsNavbar";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
usePathname: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/membership/utils", () => ({
|
||||
getAccessFlags: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock SecondaryNavigation to inspect its props
|
||||
let mockSecondaryNavigationProps: any;
|
||||
vi.mock("@/modules/ui/components/secondary-navigation", () => ({
|
||||
SecondaryNavigation: (props: any) => {
|
||||
mockSecondaryNavigationProps = props;
|
||||
return <div data-testid="secondary-navigation">Mocked SecondaryNavigation</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
describe("OrganizationSettingsNavbar", () => {
|
||||
beforeEach(() => {
|
||||
mockSecondaryNavigationProps = null; // Reset before each test
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
environmentId: "env123",
|
||||
isFormbricksCloud: true,
|
||||
membershipRole: "owner" as TOrganizationRole,
|
||||
activeId: "general",
|
||||
loading: false,
|
||||
};
|
||||
|
||||
test.each([
|
||||
{
|
||||
pathname: "/environments/env123/settings/general",
|
||||
role: "owner",
|
||||
isCloud: true,
|
||||
expectedVisibility: { general: true, billing: true, teams: true, enterprise: false, "api-keys": true },
|
||||
},
|
||||
{
|
||||
pathname: "/environments/env123/settings/teams",
|
||||
role: "member",
|
||||
isCloud: false,
|
||||
expectedVisibility: {
|
||||
general: true,
|
||||
billing: false,
|
||||
teams: true,
|
||||
enterprise: false,
|
||||
"api-keys": false,
|
||||
},
|
||||
}, // enterprise hidden if not cloud, api-keys hidden if not owner
|
||||
{
|
||||
pathname: "/environments/env123/settings/api-keys",
|
||||
role: "admin",
|
||||
isCloud: true,
|
||||
expectedVisibility: { general: true, billing: true, teams: true, enterprise: false, "api-keys": false },
|
||||
}, // api-keys hidden if not owner
|
||||
{
|
||||
pathname: "/environments/env123/settings/enterprise",
|
||||
role: "owner",
|
||||
isCloud: false,
|
||||
expectedVisibility: { general: true, billing: false, teams: true, enterprise: true, "api-keys": true },
|
||||
}, // enterprise shown if not cloud and not member
|
||||
])(
|
||||
"renders correct navigation items based on props and path ($pathname, $role, $isCloud)",
|
||||
({ pathname, role, isCloud, expectedVisibility }) => {
|
||||
vi.mocked(usePathname).mockReturnValue(pathname);
|
||||
vi.mocked(getAccessFlags).mockReturnValue({
|
||||
isOwner: role === "owner",
|
||||
isMember: role === "member",
|
||||
} as any);
|
||||
|
||||
render(
|
||||
<OrganizationSettingsNavbar
|
||||
{...defaultProps}
|
||||
membershipRole={role as TOrganizationRole}
|
||||
isFormbricksCloud={isCloud}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("secondary-navigation")).toBeInTheDocument();
|
||||
expect(mockSecondaryNavigationProps).not.toBeNull();
|
||||
|
||||
const visibleNavItems = mockSecondaryNavigationProps.navigation.filter((item: any) => !item.hidden);
|
||||
const visibleIds = visibleNavItems.map((item: any) => item.id);
|
||||
|
||||
Object.entries(expectedVisibility).forEach(([id, shouldBeVisible]) => {
|
||||
if (shouldBeVisible) {
|
||||
expect(visibleIds).toContain(id);
|
||||
} else {
|
||||
expect(visibleIds).not.toContain(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Check current status
|
||||
mockSecondaryNavigationProps.navigation.forEach((item: any) => {
|
||||
if (item.href === pathname) {
|
||||
expect(item.current).toBe(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
test("passes loading prop to SecondaryNavigation", () => {
|
||||
vi.mocked(usePathname).mockReturnValue("/environments/env123/settings/general");
|
||||
vi.mocked(getAccessFlags).mockReturnValue({
|
||||
isOwner: true,
|
||||
isMember: false,
|
||||
} as any);
|
||||
render(<OrganizationSettingsNavbar {...defaultProps} loading={true} />);
|
||||
expect(mockSecondaryNavigationProps.loading).toBe(true);
|
||||
});
|
||||
|
||||
test("hides billing when loading is true", () => {
|
||||
vi.mocked(usePathname).mockReturnValue("/environments/env123/settings/general");
|
||||
vi.mocked(getAccessFlags).mockReturnValue({
|
||||
isOwner: true,
|
||||
isMember: false,
|
||||
} as any);
|
||||
render(<OrganizationSettingsNavbar {...defaultProps} isFormbricksCloud={true} loading={true} />);
|
||||
const billingItem = mockSecondaryNavigationProps.navigation.find((item: any) => item.id === "billing");
|
||||
expect(billingItem.hidden).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Loading from "./loading";
|
||||
|
||||
// Mock constants
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false, // Enterprise page is typically for self-hosted
|
||||
}));
|
||||
|
||||
// Mock server-side translation
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="page-content-wrapper">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: ({ pageTitle, children }: { pageTitle: string; children: React.ReactNode }) => (
|
||||
<div data-testid="page-header">
|
||||
<h1>{pageTitle}</h1>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar",
|
||||
() => ({
|
||||
OrganizationSettingsNavbar: ({ activeId, loading }: { activeId: string; loading?: boolean }) => (
|
||||
<div data-testid="org-settings-navbar">
|
||||
Active: {activeId}, Loading: {String(loading)}
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
describe("Enterprise Loading Page", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders PageContentWrapper, PageHeader, and OrganizationSettingsNavbar", async () => {
|
||||
render(await Loading());
|
||||
|
||||
expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument();
|
||||
const pageHeader = screen.getByTestId("page-header");
|
||||
expect(pageHeader).toBeInTheDocument();
|
||||
expect(pageHeader).toHaveTextContent("environments.settings.general.organization_settings");
|
||||
|
||||
const navbar = screen.getByTestId("org-settings-navbar");
|
||||
expect(navbar).toBeInTheDocument();
|
||||
expect(navbar).toHaveTextContent("Active: enterprise");
|
||||
expect(navbar).toHaveTextContent("Loading: true");
|
||||
});
|
||||
|
||||
test("renders placeholder divs", async () => {
|
||||
render(await Loading());
|
||||
const placeholders = screen.getAllByRole("generic", { hidden: true });
|
||||
const animatedPlaceholders = placeholders.filter((el) => el.classList.contains("animate-pulse"));
|
||||
expect(animatedPlaceholders.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
@@ -1,193 +0,0 @@
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TMembership } from "@formbricks/types/memberships";
|
||||
import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import EnterpriseSettingsPage from "./page";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
membership: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
environment: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
project: {
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("next-auth", () => ({
|
||||
getServerSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
usePathname: vi.fn(),
|
||||
notFound: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/organization/service", () => ({
|
||||
getOrganizationByEnvironmentId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/user/service", () => ({
|
||||
getUser: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/membership/service", () => ({
|
||||
getMembershipByUserIdOrganizationId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/membership/utils", () => ({
|
||||
getAccessFlags: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="page-content-wrapper">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="page-header">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/settings-card", () => ({
|
||||
SettingsCard: ({ title, description, children }: any) => (
|
||||
<div data-testid={`settings-card-${title?.split(".")[0]}`}>
|
||||
<h2>{title}</h2>
|
||||
<p>{description}</p>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
|
||||
let mockIsFormbricksCloud = false;
|
||||
vi.mock("@/lib/constants", async () => ({
|
||||
get IS_FORMBRICKS_CLOUD() {
|
||||
return mockIsFormbricksCloud;
|
||||
},
|
||||
IS_PRODUCTION: false,
|
||||
FB_LOGO_URL: "https://example.com/mock-logo.png",
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "mock-github-secret",
|
||||
GOOGLE_CLIENT_ID: "mock-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "mock-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "mock-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "mock-azure-client-secret",
|
||||
AZUREAD_TENANT_ID: "mock-azuread-tenant-id",
|
||||
OIDC_CLIENT_ID: "mock-oidc-client-id",
|
||||
OIDC_CLIENT_SECRET: "mock-oidc-client-secret",
|
||||
OIDC_ISSUER: "mock-oidc-issuer",
|
||||
OIDC_DISPLAY_NAME: "mock-oidc-display-name",
|
||||
OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm",
|
||||
SAML_DATABASE_URL: "mock-saml-database-url",
|
||||
WEBAPP_URL: "mock-webapp-url",
|
||||
SMTP_HOST: "mock-smtp-host",
|
||||
SMTP_PORT: "mock-smtp-port",
|
||||
E2E_TESTING: "mock-e2e-testing",
|
||||
}));
|
||||
|
||||
const mockEnvironmentId = "c6x2k3vq00000e5twdfh8x9xg";
|
||||
const mockOrganizationId = "test-org-id";
|
||||
const mockUserId = "test-user-id";
|
||||
|
||||
const mockSession = {
|
||||
user: {
|
||||
id: mockUserId,
|
||||
},
|
||||
};
|
||||
|
||||
const mockUser = {
|
||||
id: mockUserId,
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
notificationSettings: { alert: {}, weeklySummary: {} },
|
||||
role: "project_manager",
|
||||
objective: "other",
|
||||
} as unknown as TUser;
|
||||
|
||||
const mockOrganization = {
|
||||
id: mockOrganizationId,
|
||||
name: "Test Organization",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
billing: {
|
||||
stripeCustomerId: null,
|
||||
plan: "free",
|
||||
limits: { monthly: { responses: null, miu: null }, projects: null },
|
||||
features: {
|
||||
isUsageBasedSubscriptionEnabled: false,
|
||||
isSubscriptionUpdateDisabled: false,
|
||||
},
|
||||
} as unknown as TOrganizationBilling,
|
||||
} as unknown as TOrganization;
|
||||
|
||||
const mockMembership: TMembership = {
|
||||
organizationId: mockOrganizationId,
|
||||
userId: mockUserId,
|
||||
accepted: true,
|
||||
role: "owner",
|
||||
};
|
||||
|
||||
describe("EnterpriseSettingsPage", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockIsFormbricksCloud = false;
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environmentId: mockEnvironmentId,
|
||||
organizationId: mockOrganizationId,
|
||||
userId: mockUserId,
|
||||
} as any);
|
||||
vi.mocked(getServerSession).mockResolvedValue(mockSession as any);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
|
||||
vi.mocked(getAccessFlags).mockReturnValue({ isOwner: true, isAdmin: true } as any); // Ensure isAdmin is also covered if relevant
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders correctly for an owner when not on Formbricks Cloud", async () => {
|
||||
const Page = await EnterpriseSettingsPage({ params: { environmentId: mockEnvironmentId } });
|
||||
render(Page);
|
||||
|
||||
expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("page-header")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("environments.settings.enterprise.sso")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.settings.billing.remove_branding")).toBeInTheDocument();
|
||||
|
||||
expect(redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,192 +0,0 @@
|
||||
import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||
import { cleanup, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useRouter } from "next/navigation";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { DeleteOrganization } from "./DeleteOrganization";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions", () => ({
|
||||
deleteOrganizationAction: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockT = (key: string, params?: any) => {
|
||||
if (params && typeof params === "object") {
|
||||
let translation = key;
|
||||
for (const p in params) {
|
||||
translation = translation.replace(`{{${p}}}`, params[p]);
|
||||
}
|
||||
return translation;
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
const organizationMock = {
|
||||
id: "org_123",
|
||||
name: "Test Organization",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
billing: {
|
||||
stripeCustomerId: null,
|
||||
plan: "free",
|
||||
} as unknown as TOrganizationBilling,
|
||||
} as unknown as TOrganization;
|
||||
|
||||
const mockRouterPush = vi.fn();
|
||||
|
||||
const renderComponent = (props: Partial<Parameters<typeof DeleteOrganization>[0]> = {}) => {
|
||||
const defaultProps = {
|
||||
organization: organizationMock,
|
||||
isDeleteDisabled: false,
|
||||
isUserOwner: true,
|
||||
...props,
|
||||
};
|
||||
return render(<DeleteOrganization {...defaultProps} />);
|
||||
};
|
||||
|
||||
describe("DeleteOrganization", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(useRouter).mockReturnValue({ push: mockRouterPush } as any);
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders delete button and info text when delete is not disabled", () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText("environments.settings.general.once_its_gone_its_gone")).toBeInTheDocument();
|
||||
const deleteButton = screen.getByRole("button", { name: "common.delete" });
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
expect(deleteButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test("renders warning and no delete button when delete is disabled and user is owner", () => {
|
||||
renderComponent({ isDeleteDisabled: true, isUserOwner: true });
|
||||
expect(
|
||||
screen.getByText("environments.settings.general.cannot_delete_only_organization")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "common.delete" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders warning and no delete button when delete is disabled and user is not owner", () => {
|
||||
renderComponent({ isDeleteDisabled: true, isUserOwner: false });
|
||||
expect(
|
||||
screen.getByText("environments.settings.general.only_org_owner_can_perform_action")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "common.delete" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("opens delete dialog on button click", async () => {
|
||||
renderComponent();
|
||||
const deleteButton = screen.getByRole("button", { name: "common.delete" });
|
||||
await userEvent.click(deleteButton);
|
||||
expect(screen.getByText("environments.settings.general.delete_organization_warning")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
mockT("environments.settings.general.delete_organization_warning_3", {
|
||||
organizationName: organizationMock.name,
|
||||
})
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("delete button in modal is disabled until correct organization name is typed", async () => {
|
||||
renderComponent();
|
||||
const deleteButton = screen.getByRole("button", { name: "common.delete" });
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
const modalDeleteButton = within(dialog).getByRole("button", { name: "common.delete" });
|
||||
expect(modalDeleteButton).toBeDisabled();
|
||||
|
||||
const inputField = screen.getByPlaceholderText(organizationMock.name);
|
||||
await userEvent.type(inputField, organizationMock.name);
|
||||
expect(modalDeleteButton).not.toBeDisabled();
|
||||
|
||||
await userEvent.clear(inputField);
|
||||
await userEvent.type(inputField, "Wrong Name");
|
||||
expect(modalDeleteButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("calls deleteOrganizationAction on confirm, shows success, clears localStorage, and navigates", async () => {
|
||||
vi.mocked(deleteOrganizationAction).mockResolvedValue({} as any);
|
||||
localStorage.setItem(FORMBRICKS_ENVIRONMENT_ID_LS, "some-env-id");
|
||||
renderComponent();
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: "common.delete" });
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
const inputField = screen.getByPlaceholderText(organizationMock.name);
|
||||
await userEvent.type(inputField, organizationMock.name);
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
const modalDeleteButton = within(dialog).getByRole("button", { name: "common.delete" });
|
||||
await userEvent.click(modalDeleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteOrganizationAction).toHaveBeenCalledWith({ organizationId: organizationMock.id });
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
"environments.settings.general.organization_deleted_successfully"
|
||||
);
|
||||
expect(localStorage.getItem(FORMBRICKS_ENVIRONMENT_ID_LS)).toBeNull();
|
||||
expect(mockRouterPush).toHaveBeenCalledWith("/");
|
||||
expect(
|
||||
screen.queryByText("environments.settings.general.delete_organization_warning")
|
||||
).not.toBeInTheDocument(); // Modal should close
|
||||
});
|
||||
});
|
||||
|
||||
test("shows error toast on deleteOrganizationAction failure", async () => {
|
||||
vi.mocked(deleteOrganizationAction).mockRejectedValue(new Error("Deletion failed"));
|
||||
renderComponent();
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: "common.delete" });
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
const inputField = screen.getByPlaceholderText(organizationMock.name);
|
||||
await userEvent.type(inputField, organizationMock.name);
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
const modalDeleteButton = within(dialog).getByRole("button", { name: "common.delete" });
|
||||
await userEvent.click(modalDeleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteOrganizationAction).toHaveBeenCalledWith({ organizationId: organizationMock.id });
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
"environments.settings.general.error_deleting_organization_please_try_again"
|
||||
);
|
||||
expect(
|
||||
screen.queryByText("environments.settings.general.delete_organization_warning")
|
||||
).not.toBeInTheDocument(); // Modal should close
|
||||
});
|
||||
});
|
||||
|
||||
test("closes modal on cancel click", async () => {
|
||||
renderComponent();
|
||||
const deleteButton = screen.getByRole("button", { name: "common.delete" });
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
expect(screen.getByText("environments.settings.general.delete_organization_warning")).toBeInTheDocument();
|
||||
const cancelButton = screen.getByRole("button", { name: "common.cancel" });
|
||||
await userEvent.click(cancelButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText("environments.settings.general.delete_organization_warning")
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { EditOrganizationNameForm } from "./EditOrganizationNameForm";
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions", () => ({
|
||||
updateOrganizationNameAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
|
||||
const organizationMock = {
|
||||
id: "org_123",
|
||||
name: "Old Organization Name",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
billing: {
|
||||
stripeCustomerId: null,
|
||||
plan: "free",
|
||||
} as unknown as TOrganization["billing"],
|
||||
} as unknown as TOrganization;
|
||||
|
||||
const renderForm = (membershipRole: "owner" | "member") => {
|
||||
return render(
|
||||
<EditOrganizationNameForm
|
||||
environmentId="env_123"
|
||||
organization={organizationMock}
|
||||
membershipRole={membershipRole}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe("EditOrganizationNameForm", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(updateOrganizationNameAction).mockReset();
|
||||
});
|
||||
|
||||
test("renders with initial organization name and allows owner to update", async () => {
|
||||
renderForm("owner");
|
||||
|
||||
const nameInput = screen.getByPlaceholderText(
|
||||
"environments.settings.general.organization_name_placeholder"
|
||||
);
|
||||
expect(nameInput).toHaveValue(organizationMock.name);
|
||||
expect(nameInput).not.toBeDisabled();
|
||||
|
||||
const updateButton = screen.getByText("common.update");
|
||||
expect(updateButton).toBeDisabled(); // Initially disabled as form is not dirty
|
||||
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, "New Organization Name");
|
||||
expect(updateButton).not.toBeDisabled(); // Enabled after change
|
||||
|
||||
vi.mocked(updateOrganizationNameAction).mockResolvedValueOnce({
|
||||
data: { ...organizationMock, name: "New Organization Name" },
|
||||
});
|
||||
|
||||
await userEvent.click(updateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateOrganizationNameAction).toHaveBeenCalledWith({
|
||||
organizationId: organizationMock.id,
|
||||
data: { name: "New Organization Name" },
|
||||
});
|
||||
expect(
|
||||
screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder")
|
||||
).toHaveValue("New Organization Name");
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
"environments.settings.general.organization_name_updated_successfully"
|
||||
);
|
||||
});
|
||||
expect(updateButton).toBeDisabled(); // Disabled after successful submit and reset
|
||||
});
|
||||
|
||||
test("shows error toast on update failure", async () => {
|
||||
renderForm("owner");
|
||||
|
||||
const nameInput = screen.getByPlaceholderText(
|
||||
"environments.settings.general.organization_name_placeholder"
|
||||
);
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, "Another Name");
|
||||
|
||||
const updateButton = screen.getByText("common.update");
|
||||
|
||||
vi.mocked(updateOrganizationNameAction).mockResolvedValueOnce({
|
||||
data: null as any,
|
||||
});
|
||||
|
||||
await userEvent.click(updateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateOrganizationNameAction).toHaveBeenCalled();
|
||||
expect(toast.error).toHaveBeenCalledWith("");
|
||||
});
|
||||
expect(nameInput).toHaveValue("Another Name"); // Name should not reset on error
|
||||
});
|
||||
|
||||
test("shows generic error toast on exception during update", async () => {
|
||||
renderForm("owner");
|
||||
|
||||
const nameInput = screen.getByPlaceholderText(
|
||||
"environments.settings.general.organization_name_placeholder"
|
||||
);
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, "Exception Name");
|
||||
|
||||
const updateButton = screen.getByText("common.update");
|
||||
|
||||
vi.mocked(updateOrganizationNameAction).mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
await userEvent.click(updateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateOrganizationNameAction).toHaveBeenCalled();
|
||||
expect(toast.error).toHaveBeenCalledWith("Error: Network error");
|
||||
});
|
||||
});
|
||||
|
||||
test("disables input and button for non-owner roles and shows warning", async () => {
|
||||
const roles: "member"[] = ["member"];
|
||||
for (const role of roles) {
|
||||
renderForm(role);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText(
|
||||
"environments.settings.general.organization_name_placeholder"
|
||||
);
|
||||
expect(nameInput).toBeDisabled();
|
||||
|
||||
const updateButton = screen.getByText("common.update");
|
||||
expect(updateButton).toBeDisabled();
|
||||
|
||||
expect(
|
||||
screen.getByText("environments.settings.general.only_org_owner_can_perform_action")
|
||||
).toBeInTheDocument();
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import Loading from "./loading";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar",
|
||||
() => ({
|
||||
OrganizationSettingsNavbar: vi.fn(() => <div>OrganizationSettingsNavbar</div>),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/app/(app)/components/LoadingCard", () => ({
|
||||
LoadingCard: vi.fn(({ title, description }) => (
|
||||
<div>
|
||||
<div>{title}</div>
|
||||
<div>{description}</div>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
describe("Loading", () => {
|
||||
const mockTranslate = vi.fn((key) => key);
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
|
||||
});
|
||||
|
||||
test("renders loading state correctly", async () => {
|
||||
const LoadingComponent = await Loading();
|
||||
render(LoadingComponent);
|
||||
|
||||
expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument();
|
||||
expect(OrganizationSettingsNavbar).toHaveBeenCalledWith(
|
||||
{
|
||||
isFormbricksCloud: IS_FORMBRICKS_CLOUD,
|
||||
activeId: "general",
|
||||
loading: true,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(screen.getByText("environments.settings.general.organization_name")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.settings.general.organization_name_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.settings.general.delete_organization")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.settings.general.delete_organization_description")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,10 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { DeleteOrganization } from "./components/DeleteOrganization";
|
||||
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
||||
import Page from "./page";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
@@ -59,34 +52,7 @@ vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getWhiteLabelPermission: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar",
|
||||
() => ({
|
||||
OrganizationSettingsNavbar: vi.fn(() => <div>OrganizationSettingsNavbar</div>),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("./components/EditOrganizationNameForm", () => ({
|
||||
EditOrganizationNameForm: vi.fn(() => <div>EditOrganizationNameForm</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/whitelabel/email-customization/components/email-customization-settings", () => ({
|
||||
EmailCustomizationSettings: vi.fn(() => <div>EmailCustomizationSettings</div>),
|
||||
}));
|
||||
|
||||
vi.mock("./components/DeleteOrganization", () => ({
|
||||
DeleteOrganization: vi.fn(() => <div>DeleteOrganization</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/settings-id", () => ({
|
||||
SettingsId: vi.fn(() => <div>SettingsId</div>),
|
||||
}));
|
||||
|
||||
describe("Page", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
let mockEnvironmentAuth = {
|
||||
session: { user: { id: "test-user-id" } },
|
||||
currentUserMembership: { role: "owner" },
|
||||
@@ -97,10 +63,8 @@ describe("Page", () => {
|
||||
|
||||
const mockUser = { id: "test-user-id" } as TUser;
|
||||
const mockTranslate = vi.fn((key) => key);
|
||||
const mockParams = { environmentId: "env-123" };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
||||
@@ -108,163 +72,28 @@ describe("Page", () => {
|
||||
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
test("renders the page with organization settings for owner", async () => {
|
||||
test("renders the page with organization settings", async () => {
|
||||
const props = {
|
||||
params: Promise.resolve(mockParams),
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
};
|
||||
|
||||
const PageComponent = await Page(props);
|
||||
render(PageComponent);
|
||||
const result = await Page(props);
|
||||
|
||||
expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument();
|
||||
expect(OrganizationSettingsNavbar).toHaveBeenCalledWith(
|
||||
{
|
||||
environmentId: mockParams.environmentId,
|
||||
isFormbricksCloud: IS_FORMBRICKS_CLOUD,
|
||||
membershipRole: "owner",
|
||||
activeId: "general",
|
||||
},
|
||||
undefined
|
||||
);
|
||||
expect(screen.getByText("environments.settings.general.organization_name")).toBeInTheDocument();
|
||||
expect(EditOrganizationNameForm).toHaveBeenCalledWith(
|
||||
{
|
||||
organization: mockEnvironmentAuth.organization,
|
||||
environmentId: mockParams.environmentId,
|
||||
membershipRole: "owner",
|
||||
},
|
||||
undefined
|
||||
);
|
||||
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
|
||||
{
|
||||
organization: mockEnvironmentAuth.organization,
|
||||
hasWhiteLabelPermission: true,
|
||||
environmentId: mockParams.environmentId,
|
||||
isReadOnly: false,
|
||||
isFormbricksCloud: IS_FORMBRICKS_CLOUD,
|
||||
fbLogoUrl: FB_LOGO_URL,
|
||||
user: mockUser,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
expect(screen.getByText("environments.settings.general.delete_organization")).toBeInTheDocument();
|
||||
expect(DeleteOrganization).toHaveBeenCalledWith(
|
||||
{
|
||||
organization: mockEnvironmentAuth.organization,
|
||||
isDeleteDisabled: false,
|
||||
isUserOwner: true,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
expect(SettingsId).toHaveBeenCalledWith(
|
||||
{
|
||||
title: "common.organization_id",
|
||||
id: mockEnvironmentAuth.organization.id,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test("renders correctly when user is manager", async () => {
|
||||
const managerAuth = {
|
||||
...mockEnvironmentAuth,
|
||||
currentUserMembership: { role: "manager" },
|
||||
isOwner: false,
|
||||
isManager: true,
|
||||
} as unknown as TEnvironmentAuth;
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(managerAuth);
|
||||
test("renders if session user id empty", async () => {
|
||||
mockEnvironmentAuth.session.user.id = "";
|
||||
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
||||
|
||||
const props = {
|
||||
params: Promise.resolve(mockParams),
|
||||
};
|
||||
const PageComponent = await Page(props);
|
||||
render(PageComponent);
|
||||
|
||||
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isReadOnly: false, // owner or manager can edit
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
expect(DeleteOrganization).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isDeleteDisabled: true, // only owner can delete
|
||||
isUserOwner: false,
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("renders correctly when multi-org is disabled", async () => {
|
||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false);
|
||||
const props = {
|
||||
params: Promise.resolve(mockParams),
|
||||
};
|
||||
const PageComponent = await Page(props);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.queryByText("environments.settings.general.delete_organization")).not.toBeInTheDocument();
|
||||
expect(DeleteOrganization).not.toHaveBeenCalled();
|
||||
// isDeleteDisabled should be true because multiOrg is disabled, even for owner
|
||||
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isReadOnly: false,
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("renders correctly when user is not owner or manager (e.g., admin)", async () => {
|
||||
const adminAuth = {
|
||||
...mockEnvironmentAuth,
|
||||
currentUserMembership: { role: "admin" },
|
||||
isOwner: false,
|
||||
isManager: false,
|
||||
} as unknown as TEnvironmentAuth;
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(adminAuth);
|
||||
|
||||
const props = {
|
||||
params: Promise.resolve(mockParams),
|
||||
};
|
||||
const PageComponent = await Page(props);
|
||||
render(PageComponent);
|
||||
|
||||
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isReadOnly: true,
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
expect(DeleteOrganization).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isDeleteDisabled: true,
|
||||
isUserOwner: false,
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("renders if session user id empty, user is null", async () => {
|
||||
const noUserSessionAuth = {
|
||||
...mockEnvironmentAuth,
|
||||
session: { ...mockEnvironmentAuth.session, user: { ...mockEnvironmentAuth.session.user, id: "" } },
|
||||
};
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(noUserSessionAuth);
|
||||
vi.mocked(getUser).mockResolvedValue(null);
|
||||
|
||||
const props = {
|
||||
params: Promise.resolve(mockParams),
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
};
|
||||
|
||||
const PageComponent = await Page(props);
|
||||
render(PageComponent);
|
||||
expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument();
|
||||
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
user: null,
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
const result = await Page(props);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test("handles getEnvironmentAuth error", async () => {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { Session, getServerSession } from "next-auth";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import OrganizationSettingsLayout from "./layout";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/organization/service");
|
||||
vi.mock("@/lib/project/service");
|
||||
vi.mock("next-auth", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("next-auth")>();
|
||||
return {
|
||||
...actual,
|
||||
getServerSession: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock("@/modules/auth/lib/authOptions", () => ({
|
||||
authOptions: {}, // Mock authOptions if it's directly used or causes issues
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
const mockGetOrganizationByEnvironmentId = vi.mocked(getOrganizationByEnvironmentId);
|
||||
const mockGetProjectByEnvironmentId = vi.mocked(getProjectByEnvironmentId);
|
||||
const mockGetServerSession = vi.mocked(getServerSession);
|
||||
|
||||
const mockOrganization = { id: "org_test_id" } as unknown as TOrganization;
|
||||
const mockProject = { id: "project_test_id" } as unknown as TProject;
|
||||
const mockSession = { user: { id: "user_test_id" } } as unknown as Session;
|
||||
|
||||
const t = (key: string) => key;
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => t,
|
||||
}));
|
||||
|
||||
const mockProps = {
|
||||
params: { environmentId: "env_test_id" },
|
||||
children: <div>Child Content for Organization Settings</div>,
|
||||
};
|
||||
|
||||
describe("OrganizationSettingsLayout", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
mockGetOrganizationByEnvironmentId.mockResolvedValue(mockOrganization);
|
||||
mockGetProjectByEnvironmentId.mockResolvedValue(mockProject);
|
||||
mockGetServerSession.mockResolvedValue(mockSession);
|
||||
});
|
||||
|
||||
test("should render children when all data is fetched successfully", async () => {
|
||||
render(await OrganizationSettingsLayout(mockProps));
|
||||
expect(screen.getByText("Child Content for Organization Settings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should throw error if organization is not found", async () => {
|
||||
mockGetOrganizationByEnvironmentId.mockResolvedValue(null);
|
||||
await expect(OrganizationSettingsLayout(mockProps)).rejects.toThrowError("common.organization_not_found");
|
||||
});
|
||||
|
||||
test("should throw error if project is not found", async () => {
|
||||
mockGetProjectByEnvironmentId.mockResolvedValue(null);
|
||||
await expect(OrganizationSettingsLayout(mockProps)).rejects.toThrowError("common.project_not_found");
|
||||
});
|
||||
|
||||
test("should throw error if session is not found", async () => {
|
||||
mockGetServerSession.mockResolvedValue(null);
|
||||
await expect(OrganizationSettingsLayout(mockProps)).rejects.toThrowError("common.session_not_found");
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import { TeamsPage } from "@/modules/organization/settings/teams/page";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
FB_LOGO_URL: "mock-fb-logo-url",
|
||||
SMTP_HOST: "mock-smtp-host",
|
||||
SMTP_PORT: 587,
|
||||
SMTP_USER: "mock-smtp-user",
|
||||
SMTP_PASSWORD: "mock-smtp-password",
|
||||
}));
|
||||
|
||||
describe("TeamsPage re-export", () => {
|
||||
test("should re-export TeamsPage component", () => {
|
||||
expect(Page).toBe(TeamsPage);
|
||||
});
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
vi.mock("@/modules/ui/components/badge", () => ({
|
||||
Badge: ({ text }) => <div data-testid="mock-badge">{text}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key) => key, // Mock t function to return the key
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("SettingsCard", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
title: "Test Title",
|
||||
description: "Test Description",
|
||||
children: <div data-testid="child-content">Child Content</div>,
|
||||
};
|
||||
|
||||
test("renders title, description, and children", () => {
|
||||
render(<SettingsCard {...defaultProps} />);
|
||||
expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
|
||||
expect(screen.getByText(defaultProps.description)).toBeInTheDocument();
|
||||
expect(screen.getByTestId("child-content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders Beta badge when beta prop is true", () => {
|
||||
render(<SettingsCard {...defaultProps} beta />);
|
||||
const badgeElement = screen.getByTestId("mock-badge");
|
||||
expect(badgeElement).toBeInTheDocument();
|
||||
expect(badgeElement).toHaveTextContent("Beta");
|
||||
});
|
||||
|
||||
test("renders Soon badge when soon prop is true", () => {
|
||||
render(<SettingsCard {...defaultProps} soon />);
|
||||
const badgeElement = screen.getByTestId("mock-badge");
|
||||
expect(badgeElement).toBeInTheDocument();
|
||||
expect(badgeElement).toHaveTextContent("environments.settings.enterprise.coming_soon");
|
||||
});
|
||||
|
||||
test("does not render badges when beta and soon props are false", () => {
|
||||
render(<SettingsCard {...defaultProps} />);
|
||||
expect(screen.queryByTestId("mock-badge")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies default padding when noPadding prop is false", () => {
|
||||
render(<SettingsCard {...defaultProps} />);
|
||||
const childrenContainer = screen.getByTestId("child-content").parentElement;
|
||||
expect(childrenContainer).toHaveClass("px-4 pt-4");
|
||||
});
|
||||
|
||||
test("applies custom className to the root element", () => {
|
||||
const customClass = "my-custom-class";
|
||||
render(<SettingsCard {...defaultProps} className={customClass} />);
|
||||
const cardElement = screen.getByText(defaultProps.title).closest("div.relative");
|
||||
expect(cardElement).toHaveClass(customClass);
|
||||
});
|
||||
|
||||
test("renders with default classes", () => {
|
||||
render(<SettingsCard {...defaultProps} />);
|
||||
const cardElement = screen.getByText(defaultProps.title).closest("div.relative");
|
||||
expect(cardElement).toHaveClass(
|
||||
"relative my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { SettingsTitle } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsTitle";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
|
||||
describe("SettingsTitle", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the title correctly", () => {
|
||||
const titleText = "My Awesome Settings";
|
||||
render(<SettingsTitle title={titleText} />);
|
||||
const headingElement = screen.getByRole("heading", { name: titleText, level: 2 });
|
||||
expect(headingElement).toBeInTheDocument();
|
||||
expect(headingElement).toHaveTextContent(titleText);
|
||||
expect(headingElement).toHaveClass("my-4 text-2xl font-medium leading-6 text-slate-800");
|
||||
});
|
||||
|
||||
test("renders with an empty title", () => {
|
||||
render(<SettingsTitle title="" />);
|
||||
const headingElement = screen.getByRole("heading", { level: 2 });
|
||||
expect(headingElement).toBeInTheDocument();
|
||||
expect(headingElement).toHaveTextContent("");
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("Settings Page", () => {
|
||||
test("should redirect to profile settings page", async () => {
|
||||
const params = { environmentId: "testEnvId" };
|
||||
await Page({ params });
|
||||
expect(redirect).toHaveBeenCalledWith(`/environments/${params.environmentId}/settings/profile`);
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { Unplug } from "lucide-react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { EmptyAppSurveys } from "./EmptyInAppSurveys";
|
||||
|
||||
vi.mock("lucide-react", async () => {
|
||||
const actual = await vi.importActual("lucide-react");
|
||||
return {
|
||||
...actual,
|
||||
Unplug: vi.fn(() => <div data-testid="unplug-icon" />),
|
||||
};
|
||||
});
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "test-env-id",
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
describe("EmptyAppSurveys", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders correctly with translated text and icon", () => {
|
||||
render(<EmptyAppSurveys environment={mockEnvironment} />);
|
||||
|
||||
expect(screen.getByTestId("unplug-icon")).toBeInTheDocument();
|
||||
expect(Unplug).toHaveBeenCalled();
|
||||
|
||||
expect(screen.getByText("environments.surveys.summary.youre_not_plugged_in_yet")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"environments.surveys.summary.connect_your_website_or_app_with_formbricks_to_get_started"
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,243 +0,0 @@
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import {
|
||||
getResponseCountAction,
|
||||
revalidateSurveyIdPath,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
|
||||
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
||||
import { useIntervalWhenFocused } from "@/lib/utils/hooks/useIntervalWhenFocused";
|
||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||
import { act, cleanup, render, waitFor } from "@testing-library/react";
|
||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TLanguage } from "@formbricks/types/project";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLanguage,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
FB_LOGO_URL: "mock-fb-logo-url",
|
||||
SMTP_HOST: "mock-smtp-host",
|
||||
SMTP_PORT: 587,
|
||||
SMTP_USER: "mock-smtp-user",
|
||||
SMTP_PASSWORD: "mock-smtp-password",
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext");
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions");
|
||||
vi.mock("@/app/lib/surveys/surveys");
|
||||
vi.mock("@/app/share/[sharingKey]/actions");
|
||||
vi.mock("@/lib/utils/hooks/useIntervalWhenFocused");
|
||||
vi.mock("@/modules/ui/components/secondary-navigation", () => ({
|
||||
SecondaryNavigation: vi.fn(() => <div data-testid="secondary-navigation" />),
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
usePathname: vi.fn(),
|
||||
useParams: vi.fn(),
|
||||
useSearchParams: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUsePathname = vi.mocked(usePathname);
|
||||
const mockUseParams = vi.mocked(useParams);
|
||||
const mockUseSearchParams = vi.mocked(useSearchParams);
|
||||
const mockUseResponseFilter = vi.mocked(useResponseFilter);
|
||||
const mockGetResponseCountAction = vi.mocked(getResponseCountAction);
|
||||
const mockRevalidateSurveyIdPath = vi.mocked(revalidateSurveyIdPath);
|
||||
const mockGetFormattedFilters = vi.mocked(getFormattedFilters);
|
||||
const mockUseIntervalWhenFocused = vi.mocked(useIntervalWhenFocused);
|
||||
const MockSecondaryNavigation = vi.mocked(SecondaryNavigation);
|
||||
|
||||
const mockSurveyLanguages: TSurveyLanguage[] = [
|
||||
{ language: { code: "en-US" } as unknown as TLanguage, default: true, enabled: true },
|
||||
];
|
||||
|
||||
const mockSurvey = {
|
||||
id: "surveyId123",
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
environmentId: "envId123",
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "question1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: false,
|
||||
logic: [],
|
||||
isDraft: false,
|
||||
imageUrl: "",
|
||||
subheader: { default: "" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
languages: mockSurveyLanguages,
|
||||
variables: [],
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
welcomeCard: { enabled: false, headline: { default: "" } } as unknown as TSurvey["welcomeCard"],
|
||||
segment: null,
|
||||
resultShareKey: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
recontactDays: null,
|
||||
runOnDate: null,
|
||||
displayPercentage: null,
|
||||
createdBy: null,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const defaultProps = {
|
||||
environmentId: "testEnvId",
|
||||
survey: mockSurvey,
|
||||
initialTotalResponseCount: 10,
|
||||
activeId: "summary",
|
||||
};
|
||||
|
||||
describe("SurveyAnalysisNavigation", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("calls revalidateSurveyIdPath on navigation item click", async () => {
|
||||
mockUsePathname.mockReturnValue(
|
||||
`/environments/${defaultProps.environmentId}/surveys/${mockSurvey.id}/summary`
|
||||
);
|
||||
mockUseParams.mockReturnValue({ environmentId: defaultProps.environmentId, surveyId: mockSurvey.id });
|
||||
mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any);
|
||||
mockUseResponseFilter.mockReturnValue({ selectedFilter: "all", dateRange: {} } as any);
|
||||
mockGetFormattedFilters.mockReturnValue([] as any);
|
||||
mockGetResponseCountAction.mockResolvedValue({ data: 5 });
|
||||
|
||||
render(<SurveyAnalysisNavigation {...defaultProps} />);
|
||||
await waitFor(() => expect(MockSecondaryNavigation).toHaveBeenCalled());
|
||||
|
||||
const lastCallArgs = MockSecondaryNavigation.mock.calls[MockSecondaryNavigation.mock.calls.length - 1][0];
|
||||
|
||||
if (!lastCallArgs.navigation || lastCallArgs.navigation.length < 2) {
|
||||
throw new Error("Navigation items not found");
|
||||
}
|
||||
|
||||
act(() => {
|
||||
(lastCallArgs.navigation[0] as any).onClick();
|
||||
});
|
||||
expect(mockRevalidateSurveyIdPath).toHaveBeenCalledWith(
|
||||
defaultProps.environmentId,
|
||||
defaultProps.survey.id
|
||||
);
|
||||
vi.mocked(mockRevalidateSurveyIdPath).mockClear();
|
||||
|
||||
act(() => {
|
||||
(lastCallArgs.navigation[1] as any).onClick();
|
||||
});
|
||||
expect(mockRevalidateSurveyIdPath).toHaveBeenCalledWith(
|
||||
defaultProps.environmentId,
|
||||
defaultProps.survey.id
|
||||
);
|
||||
});
|
||||
|
||||
test("passes correct runWhen flag to useIntervalWhenFocused based on share embed modal", () => {
|
||||
mockUsePathname.mockReturnValue(
|
||||
`/environments/${defaultProps.environmentId}/surveys/${mockSurvey.id}/summary`
|
||||
);
|
||||
mockUseParams.mockReturnValue({ environmentId: defaultProps.environmentId, surveyId: mockSurvey.id });
|
||||
mockUseResponseFilter.mockReturnValue({ selectedFilter: "all", dateRange: {} } as any);
|
||||
mockGetFormattedFilters.mockReturnValue([] as any);
|
||||
mockGetResponseCountAction.mockResolvedValue({ data: 5 });
|
||||
|
||||
mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue("true") } as any);
|
||||
render(<SurveyAnalysisNavigation {...defaultProps} />);
|
||||
expect(mockUseIntervalWhenFocused).toHaveBeenCalledWith(expect.any(Function), 10000, false, false);
|
||||
cleanup();
|
||||
|
||||
mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any);
|
||||
render(<SurveyAnalysisNavigation {...defaultProps} />);
|
||||
expect(mockUseIntervalWhenFocused).toHaveBeenCalledWith(expect.any(Function), 10000, true, false);
|
||||
});
|
||||
|
||||
test("displays correct response count string in label for various scenarios", async () => {
|
||||
mockUsePathname.mockReturnValue(
|
||||
`/environments/${defaultProps.environmentId}/surveys/${mockSurvey.id}/responses`
|
||||
);
|
||||
mockUseParams.mockReturnValue({ environmentId: defaultProps.environmentId, surveyId: mockSurvey.id });
|
||||
mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any);
|
||||
mockUseResponseFilter.mockReturnValue({ selectedFilter: "all", dateRange: {} } as any);
|
||||
mockGetFormattedFilters.mockReturnValue([] as any);
|
||||
|
||||
// Scenario 1: total = 10, filtered = null (initial state)
|
||||
render(<SurveyAnalysisNavigation {...defaultProps} initialTotalResponseCount={10} />);
|
||||
expect(MockSecondaryNavigation.mock.calls[0][0].navigation[1].label).toBe("common.responses (10)");
|
||||
cleanup();
|
||||
vi.resetAllMocks(); // Reset mocks for next case
|
||||
|
||||
// Scenario 2: total = 15, filtered = 15
|
||||
mockUsePathname.mockReturnValue(
|
||||
`/environments/${defaultProps.environmentId}/surveys/${mockSurvey.id}/responses`
|
||||
);
|
||||
mockUseParams.mockReturnValue({ environmentId: defaultProps.environmentId, surveyId: mockSurvey.id });
|
||||
mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any);
|
||||
mockUseResponseFilter.mockReturnValue({ selectedFilter: "all", dateRange: {} } as any);
|
||||
mockGetFormattedFilters.mockReturnValue([] as any);
|
||||
mockGetResponseCountAction.mockImplementation(async (args) => {
|
||||
if (args && "filterCriteria" in args) return { data: 15, error: null, success: true };
|
||||
return { data: 15, error: null, success: true };
|
||||
});
|
||||
render(<SurveyAnalysisNavigation {...defaultProps} initialTotalResponseCount={15} />);
|
||||
await waitFor(() => {
|
||||
const lastCallArgs =
|
||||
MockSecondaryNavigation.mock.calls[MockSecondaryNavigation.mock.calls.length - 1][0];
|
||||
expect(lastCallArgs.navigation[1].label).toBe("common.responses (15)");
|
||||
});
|
||||
cleanup();
|
||||
vi.resetAllMocks();
|
||||
|
||||
// Scenario 3: total = 10, filtered = 15 (filtered > total)
|
||||
mockUsePathname.mockReturnValue(
|
||||
`/environments/${defaultProps.environmentId}/surveys/${mockSurvey.id}/responses`
|
||||
);
|
||||
mockUseParams.mockReturnValue({ environmentId: defaultProps.environmentId, surveyId: mockSurvey.id });
|
||||
mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any);
|
||||
mockUseResponseFilter.mockReturnValue({ selectedFilter: "all", dateRange: {} } as any);
|
||||
mockGetFormattedFilters.mockReturnValue([] as any);
|
||||
mockGetResponseCountAction.mockImplementation(async (args) => {
|
||||
if (args && "filterCriteria" in args) return { data: 15, error: null, success: true };
|
||||
return { data: 10, error: null, success: true };
|
||||
});
|
||||
render(<SurveyAnalysisNavigation {...defaultProps} initialTotalResponseCount={10} />);
|
||||
await waitFor(() => {
|
||||
const lastCallArgs =
|
||||
MockSecondaryNavigation.mock.calls[MockSecondaryNavigation.mock.calls.length - 1][0];
|
||||
expect(lastCallArgs.navigation[1].label).toBe("common.responses (15)");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,124 +0,0 @@
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import SurveyLayout, { generateMetadata } from "./layout";
|
||||
|
||||
vi.mock("@/lib/response/service", () => ({
|
||||
getResponseCountBySurveyId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/survey/service", () => ({
|
||||
getSurvey: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next-auth", () => ({
|
||||
getServerSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/auth/lib/authOptions", () => ({
|
||||
authOptions: {},
|
||||
}));
|
||||
|
||||
const mockSurveyId = "survey_123";
|
||||
const mockEnvironmentId = "env_456";
|
||||
const mockSurveyName = "Test Survey";
|
||||
const mockResponseCount = 10;
|
||||
|
||||
const mockSurvey = {
|
||||
id: mockSurveyId,
|
||||
name: mockSurveyName,
|
||||
questions: [],
|
||||
endings: [],
|
||||
status: "inProgress",
|
||||
type: "app",
|
||||
environmentId: mockEnvironmentId,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
variables: [],
|
||||
triggers: [],
|
||||
styling: null,
|
||||
languages: [],
|
||||
segment: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayLimit: null,
|
||||
displayOption: "displayOnce",
|
||||
isBackButtonHidden: false,
|
||||
pin: null,
|
||||
recontactDays: null,
|
||||
resultShareKey: null,
|
||||
runOnDate: null,
|
||||
showLanguageSwitch: false,
|
||||
singleUse: null,
|
||||
surveyClosedMessage: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
autoComplete: null,
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
describe("SurveyLayout", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("generateMetadata", () => {
|
||||
test("should return correct metadata when session and survey exist", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user_test_id" } });
|
||||
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
|
||||
vi.mocked(getResponseCountBySurveyId).mockResolvedValue(mockResponseCount);
|
||||
|
||||
const metadata = await generateMetadata({
|
||||
params: Promise.resolve({ surveyId: mockSurveyId, environmentId: mockEnvironmentId }),
|
||||
});
|
||||
|
||||
expect(metadata).toEqual({
|
||||
title: `${mockResponseCount} Responses | ${mockSurveyName} Results`,
|
||||
});
|
||||
expect(getServerSession).toHaveBeenCalledWith(authOptions);
|
||||
expect(getSurvey).toHaveBeenCalledWith(mockSurveyId);
|
||||
expect(getResponseCountBySurveyId).toHaveBeenCalledWith(mockSurveyId);
|
||||
});
|
||||
|
||||
test("should return correct metadata when survey is null", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user_test_id" } });
|
||||
vi.mocked(getSurvey).mockResolvedValue(null);
|
||||
vi.mocked(getResponseCountBySurveyId).mockResolvedValue(mockResponseCount);
|
||||
|
||||
const metadata = await generateMetadata({
|
||||
params: Promise.resolve({ surveyId: mockSurveyId, environmentId: mockEnvironmentId }),
|
||||
});
|
||||
|
||||
expect(metadata).toEqual({
|
||||
title: `${mockResponseCount} Responses | undefined Results`,
|
||||
});
|
||||
});
|
||||
|
||||
test("should return empty title when session does not exist", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue(null);
|
||||
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
|
||||
vi.mocked(getResponseCountBySurveyId).mockResolvedValue(mockResponseCount);
|
||||
|
||||
const metadata = await generateMetadata({
|
||||
params: Promise.resolve({ surveyId: mockSurveyId, environmentId: mockEnvironmentId }),
|
||||
});
|
||||
|
||||
expect(metadata).toEqual({
|
||||
title: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("SurveyLayout Component", () => {
|
||||
test("should render children", async () => {
|
||||
const childText = "Test Child Component";
|
||||
render(await SurveyLayout({ children: <div>{childText}</div> }));
|
||||
expect(screen.getByText(childText)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,249 +0,0 @@
|
||||
import { ResponseCardModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal";
|
||||
import { SingleResponseCard } from "@/modules/analysis/components/SingleResponseCard";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
vi.mock("@/modules/analysis/components/SingleResponseCard", () => ({
|
||||
SingleResponseCard: vi.fn(() => <div data-testid="single-response-card">SingleResponseCard</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: vi.fn(({ children, onClick, disabled, variant, className }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
data-variant={variant}
|
||||
className={className}
|
||||
data-testid="mock-button">
|
||||
{children}
|
||||
</button>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: vi.fn(({ children, open }) => (open ? <div data-testid="modal">{children}</div> : null)),
|
||||
}));
|
||||
|
||||
const mockResponses = [
|
||||
{
|
||||
id: "response1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "survey1",
|
||||
finished: true,
|
||||
data: {},
|
||||
meta: {
|
||||
userAgent: { browser: "Chrome", os: "Mac OS", device: "Desktop" },
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
notes: [],
|
||||
tags: [],
|
||||
} as unknown as TResponse,
|
||||
{
|
||||
id: "response2",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "survey1",
|
||||
finished: true,
|
||||
data: {},
|
||||
meta: {
|
||||
userAgent: { browser: "Firefox", os: "Windows", device: "Desktop" },
|
||||
url: "http://localhost:3000/page2",
|
||||
},
|
||||
notes: [],
|
||||
tags: [],
|
||||
} as unknown as TResponse,
|
||||
{
|
||||
id: "response3",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "survey1",
|
||||
finished: false,
|
||||
data: {},
|
||||
meta: {
|
||||
userAgent: { browser: "Safari", os: "iOS", device: "Mobile" },
|
||||
url: "http://localhost:3000/page3",
|
||||
},
|
||||
notes: [],
|
||||
tags: [],
|
||||
} as unknown as TResponse,
|
||||
] as unknown as TResponse[];
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
environmentId: "env1",
|
||||
status: "inProgress",
|
||||
questions: [],
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: 0,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: null,
|
||||
triggers: [],
|
||||
languages: [],
|
||||
resultShareKey: null,
|
||||
displayPercentage: null,
|
||||
welcomeCard: { enabled: false, headline: { default: "Welcome!" } } as unknown as TSurvey["welcomeCard"],
|
||||
styling: null,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "env1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
appSetupCompleted: false,
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockUser = {
|
||||
id: "user1",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: "project_manager",
|
||||
objective: "increase_conversion",
|
||||
notificationSettings: { alert: {}, weeklySummary: {}, unsubscribedOrganizationIds: [] },
|
||||
} as unknown as TUser;
|
||||
|
||||
const mockEnvironmentTags: TTag[] = [
|
||||
{ id: "tag1", createdAt: new Date(), updatedAt: new Date(), name: "Tag 1", environmentId: "env1" },
|
||||
];
|
||||
|
||||
const mockLocale: TUserLocale = "en-US";
|
||||
|
||||
const mockSetSelectedResponseId = vi.fn();
|
||||
const mockUpdateResponse = vi.fn();
|
||||
const mockDeleteResponses = vi.fn();
|
||||
const mockSetOpen = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
responses: mockResponses,
|
||||
selectedResponseId: mockResponses[0].id,
|
||||
setSelectedResponseId: mockSetSelectedResponseId,
|
||||
survey: mockSurvey,
|
||||
environment: mockEnvironment,
|
||||
user: mockUser,
|
||||
environmentTags: mockEnvironmentTags,
|
||||
updateResponse: mockUpdateResponse,
|
||||
deleteResponses: mockDeleteResponses,
|
||||
isReadOnly: false,
|
||||
open: true,
|
||||
setOpen: mockSetOpen,
|
||||
locale: mockLocale,
|
||||
};
|
||||
|
||||
describe("ResponseCardModal", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should not render if selectedResponseId is null", () => {
|
||||
const { container } = render(<ResponseCardModal {...defaultProps} selectedResponseId={null} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should render the modal when a response is selected", () => {
|
||||
render(<ResponseCardModal {...defaultProps} />);
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("single-response-card")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should call setSelectedResponseId with the next response id when next button is clicked", async () => {
|
||||
render(<ResponseCardModal {...defaultProps} selectedResponseId={mockResponses[0].id} />);
|
||||
const buttons = screen.getAllByTestId("mock-button");
|
||||
const nextButton = buttons.find((button) => button.querySelector("svg.lucide-chevron-right"));
|
||||
if (nextButton) await userEvent.click(nextButton);
|
||||
expect(mockSetSelectedResponseId).toHaveBeenCalledWith(mockResponses[1].id);
|
||||
});
|
||||
|
||||
test("should call setSelectedResponseId with the previous response id when back button is clicked", async () => {
|
||||
render(<ResponseCardModal {...defaultProps} selectedResponseId={mockResponses[1].id} />);
|
||||
const buttons = screen.getAllByTestId("mock-button");
|
||||
const backButton = buttons.find((button) => button.querySelector("svg.lucide-chevron-left"));
|
||||
if (backButton) await userEvent.click(backButton);
|
||||
expect(mockSetSelectedResponseId).toHaveBeenCalledWith(mockResponses[0].id);
|
||||
});
|
||||
|
||||
test("should disable back button if current response is the first one", () => {
|
||||
render(<ResponseCardModal {...defaultProps} selectedResponseId={mockResponses[0].id} />);
|
||||
const buttons = screen.getAllByTestId("mock-button");
|
||||
const backButton = buttons.find((button) => button.querySelector("svg.lucide-chevron-left"));
|
||||
expect(backButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("should disable next button if current response is the last one", () => {
|
||||
render(
|
||||
<ResponseCardModal {...defaultProps} selectedResponseId={mockResponses[mockResponses.length - 1].id} />
|
||||
);
|
||||
const buttons = screen.getAllByTestId("mock-button");
|
||||
const nextButton = buttons.find((button) => button.querySelector("svg.lucide-chevron-right"));
|
||||
expect(nextButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("should call setSelectedResponseId with null when close button is clicked", async () => {
|
||||
render(<ResponseCardModal {...defaultProps} />);
|
||||
const buttons = screen.getAllByTestId("mock-button");
|
||||
const closeButton = buttons.find((button) => button.querySelector("svg.lucide-x"));
|
||||
if (closeButton) await userEvent.click(closeButton);
|
||||
expect(mockSetSelectedResponseId).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
test("useEffect should set open to true and currentIndex when selectedResponseId is provided", () => {
|
||||
render(<ResponseCardModal {...defaultProps} selectedResponseId={mockResponses[1].id} />);
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(true);
|
||||
// Current index is internal state, but we can check if the correct response is displayed
|
||||
// by checking the props passed to SingleResponseCard
|
||||
expect(vi.mocked(SingleResponseCard).mock.calls[0][0].response).toEqual(mockResponses[1]);
|
||||
});
|
||||
|
||||
test("useEffect should set open to false when selectedResponseId is null after being open", () => {
|
||||
const { rerender } = render(
|
||||
<ResponseCardModal {...defaultProps} selectedResponseId={mockResponses[0].id} />
|
||||
);
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(true);
|
||||
rerender(<ResponseCardModal {...defaultProps} selectedResponseId={null} />);
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("should render ChevronLeft, ChevronRight, and XIcon", () => {
|
||||
render(<ResponseCardModal {...defaultProps} />);
|
||||
expect(document.querySelector(".lucide-chevron-left")).toBeInTheDocument();
|
||||
expect(document.querySelector(".lucide-chevron-right")).toBeInTheDocument();
|
||||
expect(document.querySelector(".lucide-x")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// Mock Lucide icons for easier querying
|
||||
vi.mock("lucide-react", async () => {
|
||||
const actual = await vi.importActual("lucide-react");
|
||||
return {
|
||||
...actual,
|
||||
ChevronLeft: vi.fn((props) => <svg {...props} className="lucide-chevron-left" />),
|
||||
ChevronRight: vi.fn((props) => <svg {...props} className="lucide-chevron-right" />),
|
||||
XIcon: vi.fn((props) => <svg {...props} className="lucide-x" />),
|
||||
};
|
||||
});
|
||||
@@ -1,388 +0,0 @@
|
||||
import { ResponseTable } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse, TResponseDataValue } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
import {
|
||||
ResponseDataView,
|
||||
extractResponseData,
|
||||
formatAddressData,
|
||||
formatContactInfoData,
|
||||
mapResponsesToTableData,
|
||||
} from "./ResponseDataView";
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable",
|
||||
() => ({
|
||||
ResponseTable: vi.fn(() => <div data-testid="response-table">ResponseTable</div>),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: vi.fn((key) => {
|
||||
if (key === "environments.surveys.responses.completed") return "Completed";
|
||||
if (key === "environments.surveys.responses.not_completed") return "Not Completed";
|
||||
return key;
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 2" },
|
||||
required: false,
|
||||
choices: [{ id: "c1", label: { default: "Choice 1" } }],
|
||||
},
|
||||
{
|
||||
id: "matrix1",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Matrix Question" },
|
||||
required: false,
|
||||
rows: [{ id: "row1", label: "Row 1" }],
|
||||
columns: [{ id: "col1", label: "Col 1" }],
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "address1",
|
||||
type: TSurveyQuestionTypeEnum.Address,
|
||||
headline: { default: "Address Question" },
|
||||
required: false,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "contactInfo1",
|
||||
type: TSurveyQuestionTypeEnum.ContactInfo,
|
||||
headline: { default: "Contact Info Question" },
|
||||
required: false,
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
hiddenFields: { enabled: true, fieldIds: ["hidden1"] },
|
||||
variables: [{ id: "var1", name: "Variable 1", type: "text", value: "default" }],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: null,
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
triggers: [],
|
||||
languages: [],
|
||||
resultShareKey: null,
|
||||
displayPercentage: null,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockResponses: TResponse[] = [
|
||||
{
|
||||
id: "response1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "survey1",
|
||||
finished: true,
|
||||
data: {
|
||||
q1: "Answer 1",
|
||||
q2: "Choice 1",
|
||||
matrix1: { row1: "Col 1" },
|
||||
address1: ["123 Main St", "Apt 4B", "Anytown", "CA", "90210", "USA"] as TResponseDataValue,
|
||||
contactInfo1: [
|
||||
"John",
|
||||
"Doe",
|
||||
"john.doe@example.com",
|
||||
"555-1234",
|
||||
"Formbricks Inc.",
|
||||
] as TResponseDataValue,
|
||||
hidden1: "Hidden Value 1",
|
||||
verifiedEmail: "test@example.com",
|
||||
},
|
||||
meta: { userAgent: { browser: "test-agent" }, url: "http://localhost" },
|
||||
singleUseId: null,
|
||||
ttc: {},
|
||||
tags: [{ id: "tag1", name: "Tag1", environmentId: "env1", createdAt: new Date(), updatedAt: new Date() }],
|
||||
notes: [
|
||||
{
|
||||
id: "note1",
|
||||
text: "Note 1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
isResolved: false,
|
||||
isEdited: false,
|
||||
user: { id: "user1", name: "User 1" },
|
||||
},
|
||||
],
|
||||
variables: { var1: "Response Var Value" },
|
||||
language: "en",
|
||||
contact: null,
|
||||
contactAttributes: null,
|
||||
},
|
||||
{
|
||||
id: "response2",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "survey1",
|
||||
finished: false,
|
||||
data: { q1: "Answer 2" },
|
||||
meta: { userAgent: { browser: "test-agent-2" }, url: "http://localhost" },
|
||||
singleUseId: null,
|
||||
ttc: {},
|
||||
tags: [],
|
||||
notes: [],
|
||||
variables: {},
|
||||
language: "de",
|
||||
contact: null,
|
||||
contactAttributes: null,
|
||||
},
|
||||
];
|
||||
|
||||
const mockUser = {
|
||||
id: "user1",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: "project_manager",
|
||||
objective: "other",
|
||||
} as unknown as TUser;
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "env1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "production",
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockEnvironmentTags: TTag[] = [
|
||||
{ id: "tag1", name: "Tag1", environmentId: "env1", createdAt: new Date(), updatedAt: new Date() },
|
||||
{ id: "tag2", name: "Tag2", environmentId: "env1", createdAt: new Date(), updatedAt: new Date() },
|
||||
];
|
||||
|
||||
const mockLocale: TUserLocale = "en-US";
|
||||
|
||||
const defaultProps = {
|
||||
survey: mockSurvey,
|
||||
responses: mockResponses,
|
||||
user: mockUser,
|
||||
environment: mockEnvironment,
|
||||
environmentTags: mockEnvironmentTags,
|
||||
isReadOnly: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
hasMore: true,
|
||||
deleteResponses: vi.fn(),
|
||||
updateResponse: vi.fn(),
|
||||
isFetchingFirstPage: false,
|
||||
locale: mockLocale,
|
||||
};
|
||||
|
||||
describe("ResponseDataView", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders ResponseTable with correct props", () => {
|
||||
render(<ResponseDataView {...defaultProps} />);
|
||||
expect(screen.getByTestId("response-table")).toBeInTheDocument();
|
||||
|
||||
const responseTableMock = vi.mocked(ResponseTable);
|
||||
expect(responseTableMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
const expectedData = [
|
||||
{
|
||||
responseData: {
|
||||
q1: "Answer 1",
|
||||
q2: "Choice 1",
|
||||
row1: "Col 1", // from matrix question
|
||||
addressLine1: "123 Main St",
|
||||
addressLine2: "Apt 4B",
|
||||
city: "Anytown",
|
||||
state: "CA",
|
||||
zip: "90210",
|
||||
country: "USA",
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
email: "john.doe@example.com",
|
||||
phone: "555-1234",
|
||||
company: "Formbricks Inc.",
|
||||
hidden1: "Hidden Value 1",
|
||||
},
|
||||
createdAt: mockResponses[0].createdAt,
|
||||
status: "Completed",
|
||||
responseId: "response1",
|
||||
tags: mockResponses[0].tags,
|
||||
notes: mockResponses[0].notes,
|
||||
variables: { var1: "Response Var Value" },
|
||||
verifiedEmail: "test@example.com",
|
||||
language: "en",
|
||||
person: null,
|
||||
contactAttributes: null,
|
||||
},
|
||||
{
|
||||
responseData: {
|
||||
q1: "Answer 2",
|
||||
},
|
||||
createdAt: mockResponses[1].createdAt,
|
||||
status: "Not Completed",
|
||||
responseId: "response2",
|
||||
tags: [],
|
||||
notes: [],
|
||||
variables: {},
|
||||
verifiedEmail: "",
|
||||
language: "de",
|
||||
person: null,
|
||||
contactAttributes: null,
|
||||
},
|
||||
];
|
||||
|
||||
expect(responseTableMock.mock.calls[0][0].data).toEqual(expectedData);
|
||||
expect(responseTableMock.mock.calls[0][0].survey).toEqual(mockSurvey);
|
||||
expect(responseTableMock.mock.calls[0][0].responses).toEqual(mockResponses);
|
||||
expect(responseTableMock.mock.calls[0][0].user).toEqual(mockUser);
|
||||
expect(responseTableMock.mock.calls[0][0].environmentTags).toEqual(mockEnvironmentTags);
|
||||
expect(responseTableMock.mock.calls[0][0].isReadOnly).toBe(false);
|
||||
expect(responseTableMock.mock.calls[0][0].environment).toEqual(mockEnvironment);
|
||||
expect(responseTableMock.mock.calls[0][0].fetchNextPage).toBe(defaultProps.fetchNextPage);
|
||||
expect(responseTableMock.mock.calls[0][0].hasMore).toBe(true);
|
||||
expect(responseTableMock.mock.calls[0][0].deleteResponses).toBe(defaultProps.deleteResponses);
|
||||
expect(responseTableMock.mock.calls[0][0].updateResponse).toBe(defaultProps.updateResponse);
|
||||
expect(responseTableMock.mock.calls[0][0].isFetchingFirstPage).toBe(false);
|
||||
expect(responseTableMock.mock.calls[0][0].locale).toBe(mockLocale);
|
||||
});
|
||||
|
||||
test("formatAddressData correctly formats data", () => {
|
||||
const addressData: TResponseDataValue = ["1 Main St", "Apt 1", "CityA", "StateA", "10001", "CountryA"];
|
||||
const formatted = formatAddressData(addressData);
|
||||
expect(formatted).toEqual({
|
||||
addressLine1: "1 Main St",
|
||||
addressLine2: "Apt 1",
|
||||
city: "CityA",
|
||||
state: "StateA",
|
||||
zip: "10001",
|
||||
country: "CountryA",
|
||||
});
|
||||
});
|
||||
|
||||
test("formatAddressData handles undefined values", () => {
|
||||
const addressData: TResponseDataValue = ["1 Main St", "", "CityA", "", "10001", ""]; // Changed undefined to empty string as per function logic
|
||||
const formatted = formatAddressData(addressData);
|
||||
expect(formatted).toEqual({
|
||||
addressLine1: "1 Main St",
|
||||
addressLine2: "",
|
||||
city: "CityA",
|
||||
state: "",
|
||||
zip: "10001",
|
||||
country: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("formatAddressData returns empty object for non-array input", () => {
|
||||
const formatted = formatAddressData("not an array");
|
||||
expect(formatted).toEqual({});
|
||||
});
|
||||
|
||||
test("formatContactInfoData correctly formats data", () => {
|
||||
const contactData: TResponseDataValue = ["Jane", "Doe", "jane@mail.com", "123-456", "Org B"];
|
||||
const formatted = formatContactInfoData(contactData);
|
||||
expect(formatted).toEqual({
|
||||
firstName: "Jane",
|
||||
lastName: "Doe",
|
||||
email: "jane@mail.com",
|
||||
phone: "123-456",
|
||||
company: "Org B",
|
||||
});
|
||||
});
|
||||
|
||||
test("formatContactInfoData handles undefined values", () => {
|
||||
const contactData: TResponseDataValue = ["Jane", "", "jane@mail.com", "", "Org B"]; // Changed undefined to empty string
|
||||
const formatted = formatContactInfoData(contactData);
|
||||
expect(formatted).toEqual({
|
||||
firstName: "Jane",
|
||||
lastName: "",
|
||||
email: "jane@mail.com",
|
||||
phone: "",
|
||||
company: "Org B",
|
||||
});
|
||||
});
|
||||
|
||||
test("formatContactInfoData returns empty object for non-array input", () => {
|
||||
const formatted = formatContactInfoData({});
|
||||
expect(formatted).toEqual({});
|
||||
});
|
||||
|
||||
test("extractResponseData correctly extracts and formats data", () => {
|
||||
const response = mockResponses[0];
|
||||
const survey = mockSurvey;
|
||||
const extracted = extractResponseData(response, survey);
|
||||
expect(extracted).toEqual({
|
||||
q1: "Answer 1",
|
||||
q2: "Choice 1",
|
||||
row1: "Col 1", // from matrix question
|
||||
addressLine1: "123 Main St",
|
||||
addressLine2: "Apt 4B",
|
||||
city: "Anytown",
|
||||
state: "CA",
|
||||
zip: "90210",
|
||||
country: "USA",
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
email: "john.doe@example.com",
|
||||
phone: "555-1234",
|
||||
company: "Formbricks Inc.",
|
||||
hidden1: "Hidden Value 1",
|
||||
});
|
||||
});
|
||||
|
||||
test("extractResponseData handles missing optional data", () => {
|
||||
const response: TResponse = {
|
||||
...mockResponses[1],
|
||||
data: { q1: "Answer 2" },
|
||||
};
|
||||
const survey = mockSurvey;
|
||||
const extracted = extractResponseData(response, survey);
|
||||
expect(extracted).toEqual({
|
||||
q1: "Answer 2",
|
||||
// address and contactInfo will add empty strings if the keys exist but values are not arrays
|
||||
// but here, the keys 'address1' and 'contactInfo1' are not in response.data
|
||||
// hidden1 is also not in response.data
|
||||
});
|
||||
});
|
||||
|
||||
test("mapResponsesToTableData correctly maps responses", () => {
|
||||
const tMock = vi.fn((key) => (key === "environments.surveys.responses.completed" ? "Done" : "Pending"));
|
||||
const tableData = mapResponsesToTableData(mockResponses, mockSurvey, tMock);
|
||||
expect(tableData.length).toBe(2);
|
||||
expect(tableData[0].status).toBe("Done");
|
||||
expect(tableData[1].status).toBe("Pending");
|
||||
expect(tableData[0].responseData.q1).toBe("Answer 1");
|
||||
expect(tableData[0].responseData.hidden1).toBe("Hidden Value 1");
|
||||
expect(tableData[0].variables.var1).toBe("Response Var Value");
|
||||
expect(tableData[1].responseData.q1).toBe("Answer 2");
|
||||
expect(tableData[0].verifiedEmail).toBe("test@example.com");
|
||||
expect(tableData[1].verifiedEmail).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -24,8 +24,7 @@ interface ResponseDataViewProps {
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
// Export for testing
|
||||
export const formatAddressData = (responseValue: TResponseDataValue): Record<string, string> => {
|
||||
const formatAddressData = (responseValue: TResponseDataValue): Record<string, string> => {
|
||||
const addressKeys = ["addressLine1", "addressLine2", "city", "state", "zip", "country"];
|
||||
return Array.isArray(responseValue)
|
||||
? responseValue.reduce((acc, curr, index) => {
|
||||
@@ -35,8 +34,7 @@ export const formatAddressData = (responseValue: TResponseDataValue): Record<str
|
||||
: {};
|
||||
};
|
||||
|
||||
// Export for testing
|
||||
export const formatContactInfoData = (responseValue: TResponseDataValue): Record<string, string> => {
|
||||
const formatContactInfoData = (responseValue: TResponseDataValue): Record<string, string> => {
|
||||
const addressKeys = ["firstName", "lastName", "email", "phone", "company"];
|
||||
return Array.isArray(responseValue)
|
||||
? responseValue.reduce((acc, curr, index) => {
|
||||
@@ -46,8 +44,7 @@ export const formatContactInfoData = (responseValue: TResponseDataValue): Record
|
||||
: {};
|
||||
};
|
||||
|
||||
// Export for testing
|
||||
export const extractResponseData = (response: TResponse, survey: TSurvey): Record<string, any> => {
|
||||
const extractResponseData = (response: TResponse, survey: TSurvey): Record<string, any> => {
|
||||
let responseData: Record<string, any> = {};
|
||||
|
||||
survey.questions.forEach((question) => {
|
||||
@@ -76,8 +73,7 @@ export const extractResponseData = (response: TResponse, survey: TSurvey): Recor
|
||||
return responseData;
|
||||
};
|
||||
|
||||
// Export for testing
|
||||
export const mapResponsesToTableData = (
|
||||
const mapResponsesToTableData = (
|
||||
responses: TResponse[],
|
||||
survey: TSurvey,
|
||||
t: TFnType
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
import { ResponseDataView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView";
|
||||
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||
import { act, cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
|
||||
useResponseFilter: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions", () => ({
|
||||
getResponseCountAction: vi.fn(),
|
||||
getResponsesAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView",
|
||||
() => ({
|
||||
ResponseDataView: vi.fn(() => <div data-testid="response-data-view">ResponseDataView</div>),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter", () => ({
|
||||
CustomFilter: vi.fn(() => <div data-testid="custom-filter">CustomFilter</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResultsShareButton", () => ({
|
||||
ResultsShareButton: vi.fn(() => <div data-testid="results-share-button">ResultsShareButton</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/lib/surveys/surveys", () => ({
|
||||
getFormattedFilters: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/share/[sharingKey]/actions", () => ({
|
||||
getResponseCountBySurveySharingKeyAction: vi.fn(),
|
||||
getResponsesBySurveySharingKeyAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
replaceHeadlineRecall: vi.fn((survey) => survey),
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useParams: vi.fn(),
|
||||
useSearchParams: vi.fn(),
|
||||
useRouter: vi.fn(),
|
||||
usePathname: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUseResponseFilter = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"))
|
||||
.useResponseFilter
|
||||
);
|
||||
const mockGetResponsesAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions"))
|
||||
.getResponsesAction
|
||||
);
|
||||
const mockGetResponseCountAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions"))
|
||||
.getResponseCountAction
|
||||
);
|
||||
const mockGetResponsesBySurveySharingKeyAction = vi.mocked(
|
||||
(await import("@/app/share/[sharingKey]/actions")).getResponsesBySurveySharingKeyAction
|
||||
);
|
||||
const mockGetResponseCountBySurveySharingKeyAction = vi.mocked(
|
||||
(await import("@/app/share/[sharingKey]/actions")).getResponseCountBySurveySharingKeyAction
|
||||
);
|
||||
const mockUseParams = vi.mocked((await import("next/navigation")).useParams);
|
||||
const mockUseSearchParams = vi.mocked((await import("next/navigation")).useSearchParams);
|
||||
const mockGetFormattedFilters = vi.mocked((await import("@/app/lib/surveys/surveys")).getFormattedFilters);
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [],
|
||||
thankYouCard: { enabled: true, headline: "Thank You!" },
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: 0,
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
type: "web",
|
||||
status: "inProgress",
|
||||
languages: [],
|
||||
styling: null,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockEnvironment = { id: "env1", name: "Test Environment" } as unknown as TEnvironment;
|
||||
const mockUser = { id: "user1", name: "Test User" } as TUser;
|
||||
const mockTags: TTag[] = [{ id: "tag1", name: "Tag 1", environmentId: "env1" } as TTag];
|
||||
const mockLocale: TUserLocale = "en-US";
|
||||
|
||||
const defaultProps = {
|
||||
environment: mockEnvironment,
|
||||
survey: mockSurvey,
|
||||
surveyId: "survey1",
|
||||
webAppUrl: "http://localhost:3000",
|
||||
user: mockUser,
|
||||
environmentTags: mockTags,
|
||||
responsesPerPage: 10,
|
||||
locale: mockLocale,
|
||||
isReadOnly: false,
|
||||
};
|
||||
|
||||
const mockResponseFilterState = {
|
||||
selectedFilter: "all",
|
||||
dateRange: { from: undefined, to: undefined },
|
||||
resetState: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const mockResponses: TResponse[] = [
|
||||
{
|
||||
id: "response1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "survey1",
|
||||
finished: true,
|
||||
data: {},
|
||||
meta: { userAgent: {} },
|
||||
notes: [],
|
||||
tags: [],
|
||||
} as unknown as TResponse,
|
||||
{
|
||||
id: "response2",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "survey1",
|
||||
finished: true,
|
||||
data: {},
|
||||
meta: { userAgent: {} },
|
||||
notes: [],
|
||||
tags: [],
|
||||
} as unknown as TResponse,
|
||||
];
|
||||
|
||||
describe("ResponsePage", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseParams.mockReturnValue({ environmentId: "env1", surveyId: "survey1" });
|
||||
mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any);
|
||||
mockUseResponseFilter.mockReturnValue(mockResponseFilterState);
|
||||
mockGetResponsesAction.mockResolvedValue({ data: mockResponses });
|
||||
mockGetResponseCountAction.mockResolvedValue({ data: 20 });
|
||||
mockGetResponsesBySurveySharingKeyAction.mockResolvedValue({ data: mockResponses });
|
||||
mockGetResponseCountBySurveySharingKeyAction.mockResolvedValue({ data: 20 });
|
||||
mockGetFormattedFilters.mockReturnValue({});
|
||||
});
|
||||
|
||||
test("renders correctly with default props", async () => {
|
||||
render(<ResponsePage {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("custom-filter")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("results-share-button")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("response-data-view")).toBeInTheDocument();
|
||||
});
|
||||
expect(mockGetResponseCountAction).toHaveBeenCalled();
|
||||
expect(mockGetResponsesAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not render ResultsShareButton when isReadOnly is true", async () => {
|
||||
render(<ResponsePage {...defaultProps} isReadOnly={true} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("results-share-button")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("does not render ResultsShareButton when on sharing page", async () => {
|
||||
mockUseParams.mockReturnValue({ sharingKey: "share123" });
|
||||
render(<ResponsePage {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("results-share-button")).not.toBeInTheDocument();
|
||||
});
|
||||
expect(mockGetResponseCountBySurveySharingKeyAction).toHaveBeenCalled();
|
||||
expect(mockGetResponsesBySurveySharingKeyAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fetches next page of responses", async () => {
|
||||
const { rerender } = render(<ResponsePage {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(mockGetResponsesAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Simulate calling fetchNextPage (e.g., via ResponseDataView prop)
|
||||
// For this test, we'll directly manipulate state to simulate the effect
|
||||
// In a real scenario, this would be triggered by user interaction with ResponseDataView
|
||||
const responseDataViewProps = vi.mocked(ResponseDataView).mock.calls[0][0];
|
||||
|
||||
await act(async () => {
|
||||
await responseDataViewProps.fetchNextPage();
|
||||
});
|
||||
|
||||
rerender(<ResponsePage {...defaultProps} />); // Rerender to reflect state changes
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetResponsesAction).toHaveBeenCalledTimes(2); // Initial fetch + next page
|
||||
expect(mockGetResponsesAction).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
offset: defaultProps.responsesPerPage, // page 2
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes responses and updates count", async () => {
|
||||
render(<ResponsePage {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(mockGetResponsesAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const responseDataViewProps = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView"
|
||||
)
|
||||
).ResponseDataView
|
||||
).mock.calls[0][0];
|
||||
|
||||
act(() => {
|
||||
responseDataViewProps.deleteResponses(["response1"]);
|
||||
});
|
||||
|
||||
// Check if ResponseDataView is re-rendered with updated responses
|
||||
// This requires checking the props passed to ResponseDataView after deletion
|
||||
// For simplicity, we assume the state update triggers a re-render and ResponseDataView receives new props
|
||||
await waitFor(async () => {
|
||||
const latestCallArgs = vi
|
||||
.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView"
|
||||
)
|
||||
).ResponseDataView
|
||||
)
|
||||
.mock.calls.pop();
|
||||
if (latestCallArgs) {
|
||||
expect(latestCallArgs[0].responses).toHaveLength(mockResponses.length - 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("updates a response", async () => {
|
||||
render(<ResponsePage {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(mockGetResponsesAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const responseDataViewProps = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView"
|
||||
)
|
||||
).ResponseDataView
|
||||
).mock.calls[0][0];
|
||||
|
||||
const updatedResponseData = { ...mockResponses[0], finished: false };
|
||||
act(() => {
|
||||
responseDataViewProps.updateResponse("response1", updatedResponseData);
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
const latestCallArgs = vi
|
||||
.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView"
|
||||
)
|
||||
).ResponseDataView
|
||||
)
|
||||
.mock.calls.pop();
|
||||
if (latestCallArgs) {
|
||||
const updatedResponseInView = latestCallArgs[0].responses.find((r) => r.id === "response1");
|
||||
expect(updatedResponseInView?.finished).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("resets pagination and responses when filters change", async () => {
|
||||
const { rerender } = render(<ResponsePage {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(mockGetResponsesAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Simulate filter change
|
||||
const newFilterState = { ...mockResponseFilterState, selectedFilter: "completed" };
|
||||
mockUseResponseFilter.mockReturnValue(newFilterState);
|
||||
mockGetFormattedFilters.mockReturnValue({ someNewFilter: "value" } as any); // Simulate new formatted filters
|
||||
|
||||
rerender(<ResponsePage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should fetch count and responses again due to filter change
|
||||
expect(mockGetResponseCountAction).toHaveBeenCalledTimes(2);
|
||||
expect(mockGetResponsesAction).toHaveBeenCalledTimes(2);
|
||||
// Check if it fetches with offset 0 (first page)
|
||||
expect(mockGetResponsesAction).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
offset: 0,
|
||||
filterCriteria: { someNewFilter: "value" },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("calls resetState when referer search param is not present", () => {
|
||||
mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue(null) } as any);
|
||||
render(<ResponsePage {...defaultProps} />);
|
||||
expect(mockResponseFilterState.resetState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not call resetState when referer search param is present", () => {
|
||||
mockUseSearchParams.mockReturnValue({ get: vi.fn().mockReturnValue("someReferer") } as any);
|
||||
render(<ResponsePage {...defaultProps} />);
|
||||
expect(mockResponseFilterState.resetState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles empty responses from API", async () => {
|
||||
mockGetResponsesAction.mockResolvedValue({ data: [] });
|
||||
mockGetResponseCountAction.mockResolvedValue({ data: 0 });
|
||||
render(<ResponsePage {...defaultProps} />);
|
||||
await waitFor(async () => {
|
||||
const latestCallArgs = vi
|
||||
.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView"
|
||||
)
|
||||
).ResponseDataView
|
||||
)
|
||||
.mock.calls.pop();
|
||||
if (latestCallArgs) {
|
||||
expect(latestCallArgs[0].responses).toEqual([]);
|
||||
expect(latestCallArgs[0].hasMore).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("handles API errors gracefully for getResponsesAction", async () => {
|
||||
mockGetResponsesAction.mockResolvedValue({ data: null as any });
|
||||
render(<ResponsePage {...defaultProps} />);
|
||||
await waitFor(async () => {
|
||||
const latestCallArgs = vi
|
||||
.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView"
|
||||
)
|
||||
).ResponseDataView
|
||||
)
|
||||
.mock.calls.pop();
|
||||
if (latestCallArgs) {
|
||||
expect(latestCallArgs[0].responses).toEqual([]); // Should default to empty array
|
||||
expect(latestCallArgs[0].isFetchingFirstPage).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("handles API errors gracefully for getResponseCountAction", async () => {
|
||||
mockGetResponseCountAction.mockResolvedValue({ data: null as any });
|
||||
render(<ResponsePage {...defaultProps} />);
|
||||
// No direct visual change, but ensure no crash and component renders
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("response-data-view")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,487 +0,0 @@
|
||||
import { generateResponseTableColumns } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns";
|
||||
import { deleteResponseAction } from "@/modules/analysis/components/SingleResponseCard/actions";
|
||||
import type { DragEndEvent } from "@dnd-kit/core";
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse, TResponseTableData } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
import { ResponseTable } from "./ResponseTable";
|
||||
|
||||
// Hoist variables used in mock factories
|
||||
const { DndContextMock, SortableContextMock, arrayMoveMock } = vi.hoisted(() => {
|
||||
const dndMock = vi.fn(({ children, onDragEnd }) => {
|
||||
// Store the onDragEnd prop to allow triggering it in tests
|
||||
(dndMock as any).lastOnDragEnd = onDragEnd;
|
||||
return <div data-testid="dnd-context">{children}</div>;
|
||||
});
|
||||
const sortableMock = vi.fn(({ children }) => <>{children}</>);
|
||||
const moveMock = vi.fn((array, from, to) => {
|
||||
const newArray = [...array];
|
||||
const [item] = newArray.splice(from, 1);
|
||||
newArray.splice(to, 0, item);
|
||||
return newArray;
|
||||
});
|
||||
return {
|
||||
DndContextMock: dndMock,
|
||||
SortableContextMock: sortableMock,
|
||||
arrayMoveMock: moveMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dnd-kit/core", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@dnd-kit/core")>();
|
||||
return {
|
||||
...actual,
|
||||
DndContext: DndContextMock,
|
||||
useSensor: vi.fn(),
|
||||
useSensors: vi.fn(),
|
||||
closestCenter: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dnd-kit/modifiers", () => ({
|
||||
restrictToHorizontalAxis: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dnd-kit/sortable", () => ({
|
||||
SortableContext: SortableContextMock,
|
||||
arrayMove: arrayMoveMock,
|
||||
horizontalListSortingStrategy: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock child components and hooks
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal",
|
||||
() => ({
|
||||
ResponseCardModal: vi.fn(({ open, setOpen, selectedResponseId }) =>
|
||||
open ? (
|
||||
<div data-testid="response-card-modal">
|
||||
Selected Response ID: {selectedResponseId}
|
||||
<button onClick={() => setOpen(false)}>Close ResponseCardModal</button>
|
||||
</div>
|
||||
) : null
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell",
|
||||
() => ({
|
||||
ResponseTableCell: vi.fn(({ cell, row, setSelectedResponseId }) => (
|
||||
<td data-testid={`cell-${cell.id}`} onClick={() => setSelectedResponseId(row.original.responseId)}>
|
||||
{typeof cell.getValue === "function" ? cell.getValue() : JSON.stringify(cell.getValue())}
|
||||
</td>
|
||||
)),
|
||||
})
|
||||
);
|
||||
|
||||
const mockGeneratedColumns = [
|
||||
{
|
||||
id: "select",
|
||||
header: () => "Select",
|
||||
cell: vi.fn(() => "SelectCell"),
|
||||
enableSorting: false,
|
||||
meta: { type: "select", questionType: null, hidden: false },
|
||||
},
|
||||
{
|
||||
id: "createdAt",
|
||||
header: () => "Created At",
|
||||
cell: vi.fn(({ row }) => new Date(row.original.createdAt).toISOString()),
|
||||
enableSorting: true,
|
||||
meta: { type: "createdAt", questionType: null, hidden: false },
|
||||
},
|
||||
{
|
||||
id: "q1",
|
||||
header: () => "Question 1",
|
||||
cell: vi.fn(({ row }) => row.original.responseData.q1),
|
||||
enableSorting: true,
|
||||
meta: { type: "question", questionType: "openText", hidden: false },
|
||||
},
|
||||
];
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns",
|
||||
() => ({
|
||||
generateResponseTableColumns: vi.fn(() => mockGeneratedColumns),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/modules/analysis/components/SingleResponseCard/actions", () => ({
|
||||
deleteResponseAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/data-table", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/modules/ui/components/data-table")>();
|
||||
return {
|
||||
...actual,
|
||||
DataTableToolbar: vi.fn((props) => (
|
||||
<div data-testid="data-table-toolbar">
|
||||
<button data-testid="toolbar-expand-toggle" onClick={() => props.setIsExpanded(!props.isExpanded)}>
|
||||
Toggle Expand
|
||||
</button>
|
||||
<button data-testid="toolbar-open-settings" onClick={() => props.setIsTableSettingsModalOpen(true)}>
|
||||
Open Settings
|
||||
</button>
|
||||
<button
|
||||
data-testid="toolbar-delete-selected"
|
||||
onClick={() => props.deleteRows(props.table.getSelectedRowModel().rows.map((r) => r.id))}>
|
||||
Delete Selected
|
||||
</button>
|
||||
<button data-testid="toolbar-delete-single" onClick={() => props.deleteAction("single_response_id")}>
|
||||
Delete Single Action
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
DataTableHeader: vi.fn(({ header }) => (
|
||||
<th
|
||||
data-testid={`header-${header.id}`}
|
||||
onClick={() => header.column.getToggleSortingHandler()?.(new MouseEvent("click"))}>
|
||||
{typeof header.column.columnDef.header === "function"
|
||||
? header.column.columnDef.header(header.getContext())
|
||||
: header.column.columnDef.header}
|
||||
<button
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
data-testid={`resize-${header.id}`}>
|
||||
Resize
|
||||
</button>
|
||||
</th>
|
||||
)),
|
||||
DataTableSettingsModal: vi.fn(({ open, setOpen }) =>
|
||||
open ? (
|
||||
<div data-testid="data-table-settings-modal">
|
||||
<button onClick={() => setOpen(false)}>Close Settings</button>
|
||||
</div>
|
||||
) : null
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: vi.fn(() => [vi.fn()]),
|
||||
}));
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: vi.fn((key) => key), // Simple pass-through mock
|
||||
}),
|
||||
}));
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: vi.fn((key: string) => store[key] || null),
|
||||
setItem: vi.fn((key: string, value: string) => {
|
||||
store[key] = value.toString();
|
||||
}),
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
removeItem: vi.fn((key: string) => {
|
||||
delete store[key];
|
||||
}),
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(window, "localStorage", { value: localStorageMock });
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
hiddenFields: { enabled: true, fieldIds: ["hidden1"] },
|
||||
variables: [{ id: "var1", name: "Variable 1", type: "text", value: "default" }],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
headline: { default: "" },
|
||||
html: { default: "" },
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
},
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
closeOnDate: null,
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: null,
|
||||
singleUse: { enabled: false, isEncrypted: true },
|
||||
triggers: [],
|
||||
languages: [],
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
resultShareKey: null,
|
||||
displayPercentage: null,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockResponses: TResponse[] = [
|
||||
{
|
||||
id: "res1",
|
||||
surveyId: "survey1",
|
||||
finished: true,
|
||||
data: { q1: "Response 1 Text" },
|
||||
createdAt: new Date("2023-01-01T10:00:00.000Z"),
|
||||
updatedAt: new Date(),
|
||||
meta: {},
|
||||
singleUseId: null,
|
||||
ttc: {},
|
||||
tags: [],
|
||||
notes: [],
|
||||
variables: {},
|
||||
language: "en",
|
||||
contact: null,
|
||||
contactAttributes: null,
|
||||
},
|
||||
{
|
||||
id: "res2",
|
||||
surveyId: "survey1",
|
||||
finished: false,
|
||||
data: { q1: "Response 2 Text" },
|
||||
createdAt: new Date("2023-01-02T10:00:00.000Z"),
|
||||
updatedAt: new Date(),
|
||||
meta: {},
|
||||
singleUseId: null,
|
||||
ttc: {},
|
||||
tags: [],
|
||||
notes: [],
|
||||
variables: {},
|
||||
language: "en",
|
||||
contact: null,
|
||||
contactAttributes: null,
|
||||
},
|
||||
];
|
||||
|
||||
const mockResponseTableData: TResponseTableData[] = [
|
||||
{
|
||||
responseId: "res1",
|
||||
responseData: { q1: "Response 1 Text" },
|
||||
createdAt: new Date("2023-01-01T10:00:00.000Z"),
|
||||
status: "Completed",
|
||||
tags: [],
|
||||
notes: [],
|
||||
variables: {},
|
||||
verifiedEmail: "",
|
||||
language: "en",
|
||||
person: null,
|
||||
contactAttributes: null,
|
||||
},
|
||||
{
|
||||
responseId: "res2",
|
||||
responseData: { q1: "Response 2 Text" },
|
||||
createdAt: new Date("2023-01-02T10:00:00.000Z"),
|
||||
status: "Not Completed",
|
||||
tags: [],
|
||||
notes: [],
|
||||
variables: {},
|
||||
verifiedEmail: "",
|
||||
language: "en",
|
||||
person: null,
|
||||
contactAttributes: null,
|
||||
},
|
||||
];
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "env1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
appSetupCompleted: false,
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockUser = {
|
||||
id: "user1",
|
||||
name: "Test User",
|
||||
email: "user@test.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: "project_manager",
|
||||
objective: "other",
|
||||
notificationSettings: { alert: {}, weeklySummary: {} },
|
||||
} as unknown as TUser;
|
||||
|
||||
const mockEnvironmentTags: TTag[] = [
|
||||
{ id: "tag1", name: "Tag 1", environmentId: "env1", createdAt: new Date(), updatedAt: new Date() },
|
||||
];
|
||||
const mockLocale: TUserLocale = "en-US";
|
||||
|
||||
const defaultProps = {
|
||||
data: mockResponseTableData,
|
||||
survey: mockSurvey,
|
||||
responses: mockResponses,
|
||||
environment: mockEnvironment,
|
||||
user: mockUser,
|
||||
environmentTags: mockEnvironmentTags,
|
||||
isReadOnly: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
hasMore: true,
|
||||
deleteResponses: vi.fn(),
|
||||
updateResponse: vi.fn(),
|
||||
isFetchingFirstPage: false,
|
||||
locale: mockLocale,
|
||||
};
|
||||
|
||||
describe("ResponseTable", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorageMock.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders skeleton when isFetchingFirstPage is true", () => {
|
||||
render(<ResponseTable {...defaultProps} isFetchingFirstPage={true} />);
|
||||
// Check for skeleton elements (implementation detail, might need adjustment)
|
||||
// For now, check that data is not directly rendered
|
||||
expect(screen.queryByText("Response 1 Text")).not.toBeInTheDocument();
|
||||
// Check if table headers are still there
|
||||
expect(screen.getByText("Created At")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("loads settings from localStorage on mount", () => {
|
||||
const savedOrder = ["q1", "createdAt", "select"];
|
||||
const savedVisibility = { createdAt: false };
|
||||
const savedExpanded = true;
|
||||
localStorageMock.setItem(`${mockSurvey.id}-columnOrder`, JSON.stringify(savedOrder));
|
||||
localStorageMock.setItem(`${mockSurvey.id}-columnVisibility`, JSON.stringify(savedVisibility));
|
||||
localStorageMock.setItem(`${mockSurvey.id}-rowExpand`, JSON.stringify(savedExpanded));
|
||||
|
||||
render(<ResponseTable {...defaultProps} />);
|
||||
|
||||
// Check if generateResponseTableColumns was called with the loaded expanded state
|
||||
expect(vi.mocked(generateResponseTableColumns)).toHaveBeenCalledWith(
|
||||
mockSurvey,
|
||||
savedExpanded,
|
||||
false,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
test("saves settings to localStorage when they change", async () => {
|
||||
const { rerender } = render(<ResponseTable {...defaultProps} />);
|
||||
|
||||
// Simulate column order change via DND
|
||||
const dragEvent: DragEndEvent = {
|
||||
active: { id: "createdAt" },
|
||||
over: { id: "q1" },
|
||||
delta: { x: 0, y: 0 },
|
||||
activators: { x: 0, y: 0 },
|
||||
collisions: null,
|
||||
overNode: null,
|
||||
activeNode: null,
|
||||
} as any;
|
||||
act(() => {
|
||||
(DndContextMock as any).lastOnDragEnd?.(dragEvent);
|
||||
});
|
||||
rerender(<ResponseTable {...defaultProps} />); // Rerender to reflect state change if necessary for useEffect
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
`${mockSurvey.id}-columnOrder`,
|
||||
JSON.stringify(["select", "q1", "createdAt"])
|
||||
);
|
||||
|
||||
// Simulate visibility change (e.g. via settings modal - direct state change for test)
|
||||
// This would typically happen via table.setColumnVisibility, which is internal to useReactTable
|
||||
// For this test, we'll assume a mechanism changes columnVisibility state
|
||||
// This part is hard to test without deeper mocking of useReactTable or exposing setColumnVisibility
|
||||
|
||||
// Simulate row expansion change
|
||||
await userEvent.click(screen.getByTestId("toolbar-expand-toggle")); // Toggle to true
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(`${mockSurvey.id}-rowExpand`, "true");
|
||||
});
|
||||
|
||||
test("handles column drag and drop", () => {
|
||||
render(<ResponseTable {...defaultProps} />);
|
||||
const dragEvent: DragEndEvent = {
|
||||
active: { id: "createdAt" },
|
||||
over: { id: "q1" },
|
||||
delta: { x: 0, y: 0 },
|
||||
activators: { x: 0, y: 0 },
|
||||
collisions: null,
|
||||
overNode: null,
|
||||
activeNode: null,
|
||||
} as any;
|
||||
act(() => {
|
||||
(DndContextMock as any).lastOnDragEnd?.(dragEvent);
|
||||
});
|
||||
expect(arrayMoveMock).toHaveBeenCalledWith(expect.arrayContaining(["createdAt", "q1"]), 1, 2); // Example indices
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
`${mockSurvey.id}-columnOrder`,
|
||||
JSON.stringify(["select", "q1", "createdAt"]) // Based on initial ['select', 'createdAt', 'q1']
|
||||
);
|
||||
});
|
||||
|
||||
test("interacts with DataTableToolbar: toggle expand, open settings, delete", async () => {
|
||||
const deleteResponsesMock = vi.fn();
|
||||
const deleteResponseActionMock = vi.mocked(deleteResponseAction);
|
||||
render(<ResponseTable {...defaultProps} deleteResponses={deleteResponsesMock} />);
|
||||
|
||||
// Toggle expand
|
||||
await userEvent.click(screen.getByTestId("toolbar-expand-toggle"));
|
||||
expect(vi.mocked(generateResponseTableColumns)).toHaveBeenCalledWith(
|
||||
mockSurvey,
|
||||
true,
|
||||
false,
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(`${mockSurvey.id}-rowExpand`, "true");
|
||||
|
||||
// Open settings
|
||||
await userEvent.click(screen.getByTestId("toolbar-open-settings"));
|
||||
expect(screen.getByTestId("data-table-settings-modal")).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText("Close Settings"));
|
||||
expect(screen.queryByTestId("data-table-settings-modal")).not.toBeInTheDocument();
|
||||
|
||||
// Delete selected (mock table selection)
|
||||
// This requires mocking table.getSelectedRowModel().rows
|
||||
// For simplicity, we assume the toolbar button calls deleteRows correctly
|
||||
// The mock for DataTableToolbar calls props.deleteRows with hardcoded IDs for now.
|
||||
// To test properly, we'd need to mock table.getSelectedRowModel
|
||||
// For now, let's assume the mock toolbar calls it.
|
||||
// await userEvent.click(screen.getByTestId("toolbar-delete-selected"));
|
||||
// expect(deleteResponsesMock).toHaveBeenCalledWith(["row1_id", "row2_id"]); // From mock toolbar
|
||||
|
||||
// Delete single action
|
||||
await userEvent.click(screen.getByTestId("toolbar-delete-single"));
|
||||
expect(deleteResponseActionMock).toHaveBeenCalledWith({ responseId: "single_response_id" });
|
||||
});
|
||||
|
||||
test("calls fetchNextPage when 'Load More' is clicked", async () => {
|
||||
const fetchNextPageMock = vi.fn();
|
||||
render(<ResponseTable {...defaultProps} fetchNextPage={fetchNextPageMock} />);
|
||||
await userEvent.click(screen.getByText("common.load_more"));
|
||||
expect(fetchNextPageMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not show 'Load More' if hasMore is false", () => {
|
||||
render(<ResponseTable {...defaultProps} hasMore={false} />);
|
||||
expect(screen.queryByText("common.load_more")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows 'No results' when data is empty", () => {
|
||||
render(<ResponseTable {...defaultProps} data={[]} responses={[]} />);
|
||||
expect(screen.getByText("common.no_results")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("deleteResponse function calls deleteResponseAction", async () => {
|
||||
render(<ResponseTable {...defaultProps} />);
|
||||
// This function is called by DataTableToolbar's deleteAction prop
|
||||
// We can trigger it via the mocked DataTableToolbar
|
||||
await userEvent.click(screen.getByTestId("toolbar-delete-single"));
|
||||
expect(vi.mocked(deleteResponseAction)).toHaveBeenCalledWith({ responseId: "single_response_id" });
|
||||
});
|
||||
});
|
||||
@@ -1,498 +0,0 @@
|
||||
import { processResponseData } from "@/lib/responses";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { getFormattedDateTimeString } from "@/lib/utils/datetime";
|
||||
import { getSelectionColumn } from "@/modules/ui/components/data-table";
|
||||
import { ResponseBadges } from "@/modules/ui/components/response-badges";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { AnyActionArg } from "react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TResponseNote, TResponseNoteUser, TResponseTableData } from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyVariable,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { generateResponseTableColumns } from "./ResponseTableColumns";
|
||||
|
||||
// Mock TFnType
|
||||
const t = vi.fn((key: string, params?: any) => {
|
||||
if (params) {
|
||||
let message = key;
|
||||
for (const p in params) {
|
||||
message = message.replace(`{{${p}}}`, params[p]);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
return key;
|
||||
});
|
||||
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: vi.fn((localizedString, locale) => localizedString[locale] || localizedString.default),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/responses", () => ({
|
||||
processResponseData: vi.fn((data) => (Array.isArray(data) ? data.join(", ") : String(data))),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/contact", () => ({
|
||||
getContactIdentifier: vi.fn((person) => person?.attributes?.email || person?.id || "Anonymous"),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/datetime", () => ({
|
||||
getFormattedDateTimeString: vi.fn((date) => new Date(date).toISOString()),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
recallToHeadline: vi.fn((headline) => headline),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/analysis/components/SingleResponseCard/components/RenderResponse", () => ({
|
||||
RenderResponse: vi.fn(({ responseData, isExpanded }) => (
|
||||
<div>
|
||||
RenderResponse: {JSON.stringify(responseData)} (Expanded: {String(isExpanded)})
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/lib/questions", () => ({
|
||||
getQuestionIconMap: vi.fn(() => ({
|
||||
[TSurveyQuestionTypeEnum.OpenText]: <span>OT</span>,
|
||||
[TSurveyQuestionTypeEnum.MultipleChoiceSingle]: <span>MCS</span>,
|
||||
[TSurveyQuestionTypeEnum.Matrix]: <span>MX</span>,
|
||||
[TSurveyQuestionTypeEnum.Address]: <span>AD</span>,
|
||||
[TSurveyQuestionTypeEnum.ContactInfo]: <span>CI</span>,
|
||||
})),
|
||||
VARIABLES_ICON_MAP: {
|
||||
text: <span>VarT</span>,
|
||||
number: <span>VarN</span>,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/data-table", () => ({
|
||||
getSelectionColumn: vi.fn(() => ({
|
||||
id: "select",
|
||||
header: "Select",
|
||||
cell: "SelectCell",
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/response-badges", () => ({
|
||||
ResponseBadges: vi.fn(({ items, isExpanded }) => (
|
||||
<div>
|
||||
Badges: {items.join(", ")} (Expanded: {String(isExpanded)})
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/tooltip", () => ({
|
||||
Tooltip: ({ children }) => <div>{children}</div>,
|
||||
TooltipContent: ({ children }) => <div>{children}</div>,
|
||||
TooltipProvider: ({ children }) => <div>{children}</div>,
|
||||
TooltipTrigger: ({ children }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ children, href }) => <a href={href}>{children}</a>,
|
||||
}));
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
CircleHelpIcon: () => <span>Help</span>,
|
||||
EyeOffIcon: () => <span>EyeOff</span>,
|
||||
MailIcon: () => <span>Mail</span>,
|
||||
TagIcon: () => <span>Tag</span>,
|
||||
}));
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "q1open",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Open Text Question" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q2matrix",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Matrix Question" },
|
||||
rows: [{ default: "Row1" }, { default: "Row2" }],
|
||||
columns: [{ default: "Col1" }, { default: "Col2" }],
|
||||
required: false,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q3address",
|
||||
type: TSurveyQuestionTypeEnum.Address,
|
||||
headline: { default: "Address Question" },
|
||||
required: false,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q4contact",
|
||||
type: TSurveyQuestionTypeEnum.ContactInfo,
|
||||
headline: { default: "Contact Info Question" },
|
||||
required: false,
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
variables: [
|
||||
{ id: "var1", name: "User Segment", type: "text" } as TSurveyVariable,
|
||||
{ id: "var2", name: "Total Spend", type: "number" } as TSurveyVariable,
|
||||
],
|
||||
hiddenFields: { enabled: true, fieldIds: ["hf1", "hf2"] },
|
||||
endings: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
isVerifyEmailEnabled: false,
|
||||
styling: null,
|
||||
languages: [],
|
||||
segment: null,
|
||||
projectOverwrites: null,
|
||||
singleUse: null,
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
surveyClosedMessage: null,
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
} as TSurvey["welcomeCard"],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockResponseData = {
|
||||
contactAttributes: { country: "USA" },
|
||||
responseData: {
|
||||
q1open: "Open text answer",
|
||||
Row1: "Col1", // For matrix q2matrix
|
||||
Row2: "Col2",
|
||||
addressLine1: "123 Main St",
|
||||
city: "Anytown",
|
||||
firstName: "John",
|
||||
email: "john.doe@example.com",
|
||||
hf1: "Hidden Field 1 Value",
|
||||
},
|
||||
variables: {
|
||||
var1: "Segment A",
|
||||
var2: 100,
|
||||
},
|
||||
notes: [
|
||||
{
|
||||
id: "note1",
|
||||
text: "This is a note",
|
||||
updatedAt: new Date(),
|
||||
user: { name: "User" } as unknown as TResponseNoteUser,
|
||||
} as TResponseNote,
|
||||
],
|
||||
status: "completed",
|
||||
tags: [{ id: "tag1", name: "Important" } as unknown as TTag],
|
||||
language: "default",
|
||||
} as unknown as TResponseTableData;
|
||||
|
||||
describe("generateResponseTableColumns", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
t.mockImplementation((key: string) => key); // Reset t mock for each test
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("should include selection column when not read-only", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, false, t as any);
|
||||
expect(columns[0].id).toBe("select");
|
||||
expect(vi.mocked(getSelectionColumn)).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should not include selection column when read-only", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
expect(columns[0].id).not.toBe("select");
|
||||
expect(vi.mocked(getSelectionColumn)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should include Verified Email column when survey.isVerifyEmailEnabled is true", () => {
|
||||
const surveyWithVerifiedEmail = { ...mockSurvey, isVerifyEmailEnabled: true };
|
||||
const columns = generateResponseTableColumns(surveyWithVerifiedEmail, false, true, t as any);
|
||||
expect(columns.some((col) => (col as any).accessorKey === "verifiedEmail")).toBe(true);
|
||||
});
|
||||
|
||||
test("should not include Verified Email column when survey.isVerifyEmailEnabled is false", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
expect(columns.some((col) => (col as any).accessorKey === "verifiedEmail")).toBe(false);
|
||||
});
|
||||
|
||||
test("should generate columns for variables", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const var1Col = columns.find((col) => (col as any).accessorKey === "var1");
|
||||
expect(var1Col).toBeDefined();
|
||||
const var1Cell = (var1Col?.cell as any)?.({ row: { original: mockResponseData } } as any);
|
||||
expect(var1Cell.props.children).toBe("Segment A");
|
||||
|
||||
const var2Col = columns.find((col) => (col as any).accessorKey === "var2");
|
||||
expect(var2Col).toBeDefined();
|
||||
const var2Cell = (var2Col?.cell as any)?.({ row: { original: mockResponseData } } as any);
|
||||
expect(var2Cell.props.children).toBe(100);
|
||||
});
|
||||
|
||||
test("should generate columns for hidden fields if fieldIds exist", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const hf1Col = columns.find((col) => (col as any).accessorKey === "hf1");
|
||||
expect(hf1Col).toBeDefined();
|
||||
const hf1Cell = (hf1Col?.cell as any)?.({ row: { original: mockResponseData } } as any);
|
||||
expect(hf1Cell.props.children).toBe("Hidden Field 1 Value");
|
||||
});
|
||||
|
||||
test("should not generate columns for hidden fields if fieldIds is undefined", () => {
|
||||
const surveyWithoutHiddenFieldIds = { ...mockSurvey, hiddenFields: { enabled: true } };
|
||||
const columns = generateResponseTableColumns(surveyWithoutHiddenFieldIds, false, true, t as any);
|
||||
const hf1Col = columns.find((col) => (col as any).accessorKey === "hf1");
|
||||
expect(hf1Col).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should generate Notes column", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const notesCol = columns.find((col) => (col as any).accessorKey === "notes");
|
||||
expect(notesCol).toBeDefined();
|
||||
(notesCol?.cell as any)?.({ row: { original: mockResponseData } } as any);
|
||||
expect(vi.mocked(processResponseData)).toHaveBeenCalledWith(["This is a note"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ResponseTableColumns", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("includes verifiedEmailColumn when isVerifyEmailEnabled is true", () => {
|
||||
// Arrange
|
||||
const mockSurvey = {
|
||||
questions: [],
|
||||
variables: [],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
isVerifyEmailEnabled: true,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockT = vi.fn((key) => key);
|
||||
const isExpanded = false;
|
||||
const isReadOnly = false;
|
||||
|
||||
// Act
|
||||
const columns = generateResponseTableColumns(mockSurvey, isExpanded, isReadOnly, mockT);
|
||||
|
||||
// Assert
|
||||
const verifiedEmailColumn: any = columns.find((col: any) => col.accessorKey === "verifiedEmail");
|
||||
expect(verifiedEmailColumn).toBeDefined();
|
||||
expect(verifiedEmailColumn?.accessorKey).toBe("verifiedEmail");
|
||||
|
||||
// Call the header function to trigger the t function call with "common.verified_email"
|
||||
if (verifiedEmailColumn && typeof verifiedEmailColumn.header === "function") {
|
||||
verifiedEmailColumn.header();
|
||||
expect(mockT).toHaveBeenCalledWith("common.verified_email");
|
||||
}
|
||||
});
|
||||
|
||||
test("excludes verifiedEmailColumn when isVerifyEmailEnabled is false", () => {
|
||||
// Arrange
|
||||
const mockSurvey = {
|
||||
questions: [],
|
||||
variables: [],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
isVerifyEmailEnabled: false,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockT = vi.fn((key) => key);
|
||||
const isExpanded = false;
|
||||
const isReadOnly = false;
|
||||
|
||||
// Act
|
||||
const columns = generateResponseTableColumns(mockSurvey, isExpanded, isReadOnly, mockT);
|
||||
|
||||
// Assert
|
||||
const verifiedEmailColumn = columns.find((col: any) => col.accessorKey === "verifiedEmail");
|
||||
expect(verifiedEmailColumn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ResponseTableColumns - Column Implementations", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("dateColumn renders with formatted date", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const dateColumn: any = columns.find((col) => (col as any).accessorKey === "createdAt");
|
||||
expect(dateColumn).toBeDefined();
|
||||
|
||||
// Call the header function to test it returns the expected value
|
||||
expect(dateColumn?.header?.()).toBe("common.date");
|
||||
|
||||
// Mock a response with a date to test the cell function
|
||||
const mockRow = {
|
||||
original: { createdAt: "2023-01-01T12:00:00Z" },
|
||||
} as any;
|
||||
|
||||
// Call the cell function and check the formatted date
|
||||
dateColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(vi.mocked(getFormattedDateTimeString)).toHaveBeenCalledWith(new Date("2023-01-01T12:00:00Z"));
|
||||
});
|
||||
|
||||
test("personColumn renders anonymous when person is null", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const personColumn: any = columns.find((col) => (col as any).accessorKey === "personId");
|
||||
expect(personColumn).toBeDefined();
|
||||
|
||||
// Test header content
|
||||
const headerResult = personColumn?.header?.();
|
||||
expect(headerResult).toBeDefined();
|
||||
|
||||
// Mock a response with no person
|
||||
const mockRow = {
|
||||
original: { person: null },
|
||||
} as any;
|
||||
|
||||
// Mock the t function for this specific call
|
||||
t.mockReturnValueOnce("Anonymous User");
|
||||
|
||||
// Call the cell function and check it returns "Anonymous"
|
||||
const cellResult = personColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(t).toHaveBeenCalledWith("common.anonymous");
|
||||
expect(cellResult?.props?.children).toBe("Anonymous User");
|
||||
});
|
||||
|
||||
test("personColumn renders person identifier when person exists", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const personColumn: any = columns.find((col) => (col as any).accessorKey === "personId");
|
||||
expect(personColumn).toBeDefined();
|
||||
|
||||
// Mock a response with a person
|
||||
const mockRow = {
|
||||
original: {
|
||||
person: { id: "123", attributes: { email: "test@example.com" } },
|
||||
contactAttributes: { name: "John Doe" },
|
||||
},
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
personColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(vi.mocked(getContactIdentifier)).toHaveBeenCalledWith(
|
||||
mockRow.original.person,
|
||||
mockRow.original.contactAttributes
|
||||
);
|
||||
});
|
||||
|
||||
test("tagsColumn returns undefined when tags is not an array", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const tagsColumn: any = columns.find((col) => (col as any).accessorKey === "tags");
|
||||
expect(tagsColumn).toBeDefined();
|
||||
|
||||
// Mock a response with no tags
|
||||
const mockRow = {
|
||||
original: { tags: null },
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
const cellResult = tagsColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(cellResult).toBeUndefined();
|
||||
});
|
||||
|
||||
test("notesColumn renders when notes is an array", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const notesColumn: any = columns.find((col) => (col as any).accessorKey === "notes");
|
||||
expect(notesColumn).toBeDefined();
|
||||
|
||||
// Mock a response with notes
|
||||
const mockRow = {
|
||||
original: { notes: [{ text: "Note 1" }, { text: "Note 2" }] },
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
notesColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(vi.mocked(processResponseData)).toHaveBeenCalledWith(["Note 1", "Note 2"]);
|
||||
});
|
||||
|
||||
test("notesColumn returns undefined when notes is not an array", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
const notesColumn: any = columns.find((col) => (col as any).accessorKey === "notes");
|
||||
expect(notesColumn).toBeDefined();
|
||||
|
||||
// Mock a response with no notes
|
||||
const mockRow = {
|
||||
original: { notes: null },
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
const cellResult = notesColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(cellResult).toBeUndefined();
|
||||
});
|
||||
|
||||
test("variableColumns render variable values correctly", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
|
||||
// Find the variable column for var1
|
||||
const var1Column: any = columns.find((col) => (col as any).accessorKey === "var1");
|
||||
expect(var1Column).toBeDefined();
|
||||
|
||||
// Test the header
|
||||
const headerResult = var1Column?.header?.();
|
||||
expect(headerResult).toBeDefined();
|
||||
|
||||
// Mock a response with a string variable
|
||||
const mockRow = {
|
||||
original: { variables: { var1: "Test Value" } },
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
const cellResult = var1Column?.cell?.({ row: mockRow } as any);
|
||||
expect(cellResult?.props.children).toBe("Test Value");
|
||||
|
||||
// Test with a number variable
|
||||
const var2Column: any = columns.find((col) => (col as any).accessorKey === "var2");
|
||||
expect(var2Column).toBeDefined();
|
||||
|
||||
const mockRowNumber = {
|
||||
original: { variables: { var2: 42 } },
|
||||
} as any;
|
||||
|
||||
const cellResultNumber = var2Column?.cell?.({ row: mockRowNumber } as any);
|
||||
expect(cellResultNumber?.props.children).toBe(42);
|
||||
});
|
||||
|
||||
test("hiddenFieldColumns render when fieldIds exist", () => {
|
||||
const columns = generateResponseTableColumns(mockSurvey, false, true, t as any);
|
||||
|
||||
// Find the hidden field column
|
||||
const hfColumn: any = columns.find((col) => (col as any).accessorKey === "hf1");
|
||||
expect(hfColumn).toBeDefined();
|
||||
|
||||
// Test the header
|
||||
const headerResult = hfColumn?.header?.();
|
||||
expect(headerResult).toBeDefined();
|
||||
|
||||
// Mock a response with a hidden field value
|
||||
const mockRow = {
|
||||
original: { responseData: { hf1: "Hidden Value" } },
|
||||
} as any;
|
||||
|
||||
// Call the cell function
|
||||
const cellResult = hfColumn?.cell?.({ row: mockRow } as any);
|
||||
expect(cellResult?.props.children).toBe("Hidden Value");
|
||||
});
|
||||
|
||||
test("hiddenFieldColumns are empty when fieldIds don't exist", () => {
|
||||
// Create a survey with no hidden field IDs
|
||||
const surveyWithNoHiddenFields = {
|
||||
...mockSurvey,
|
||||
hiddenFields: { enabled: true }, // no fieldIds
|
||||
};
|
||||
|
||||
const columns = generateResponseTableColumns(surveyWithNoHiddenFields, false, true, t as any);
|
||||
|
||||
// Check that no hidden field columns were created
|
||||
const hfColumn = columns.find((col) => (col as any).accessorKey === "hf1");
|
||||
expect(hfColumn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,241 +0,0 @@
|
||||
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
|
||||
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page";
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { getSurveyDomain } from "@/lib/getSurveyUrl";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@/lib/tag/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation",
|
||||
() => ({
|
||||
SurveyAnalysisNavigation: vi.fn(() => <div data-testid="survey-analysis-navigation"></div>),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage",
|
||||
() => ({
|
||||
ResponsePage: vi.fn(() => <div data-testid="response-page"></div>),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA",
|
||||
() => ({
|
||||
SurveyAnalysisCTA: vi.fn(() => <div data-testid="survey-analysis-cta"></div>),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
RESPONSES_PER_PAGE: 10,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/getSurveyUrl", () => ({
|
||||
getSurveyDomain: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/response/service", () => ({
|
||||
getResponseCountBySurveyId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/survey/service", () => ({
|
||||
getSurvey: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/tag/service", () => ({
|
||||
getTagsByEnvironmentId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/user/service", () => ({
|
||||
getUser: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/locale", () => ({
|
||||
findMatchingLocale: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: vi.fn(({ children }) => <div data-testid="page-content-wrapper">{children}</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: vi.fn(({ pageTitle, children, cta }) => (
|
||||
<div data-testid="page-header">
|
||||
<h1 data-testid="page-title">{pageTitle}</h1>
|
||||
{cta}
|
||||
{children}
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
|
||||
const mockEnvironmentId = "test-env-id";
|
||||
const mockSurveyId = "test-survey-id";
|
||||
const mockUserId = "test-user-id";
|
||||
|
||||
const mockSurvey: TSurvey = {
|
||||
id: mockSurveyId,
|
||||
name: "Test Survey",
|
||||
environmentId: mockEnvironmentId,
|
||||
status: "inProgress",
|
||||
type: "web",
|
||||
questions: [],
|
||||
thankYouCard: { enabled: false },
|
||||
endings: [],
|
||||
languages: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
styling: null,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockUser = {
|
||||
id: mockUserId,
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
role: "project_manager",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
locale: "en-US",
|
||||
} as unknown as TUser;
|
||||
|
||||
const mockEnvironment = {
|
||||
id: mockEnvironmentId,
|
||||
type: "production",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
appSetupCompleted: true,
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockTags: TTag[] = [{ id: "tag1", name: "Tag 1", environmentId: mockEnvironmentId } as unknown as TTag];
|
||||
const mockLocale: TUserLocale = "en-US";
|
||||
const mockSurveyDomain = "http://customdomain.com";
|
||||
|
||||
const mockParams = {
|
||||
environmentId: mockEnvironmentId,
|
||||
surveyId: mockSurveyId,
|
||||
};
|
||||
|
||||
describe("ResponsesPage", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
session: { user: { id: mockUserId } } as any,
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
} as TEnvironmentAuth);
|
||||
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getTagsByEnvironmentId).mockResolvedValue(mockTags);
|
||||
vi.mocked(getResponseCountBySurveyId).mockResolvedValue(10);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale);
|
||||
vi.mocked(getSurveyDomain).mockReturnValue(mockSurveyDomain);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("renders correctly with all data", async () => {
|
||||
const props = { params: mockParams };
|
||||
const jsx = await Page(props);
|
||||
render(jsx);
|
||||
|
||||
await screen.findByTestId("page-content-wrapper");
|
||||
expect(screen.getByTestId("page-header")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("page-title")).toHaveTextContent(mockSurvey.name);
|
||||
expect(screen.getByTestId("survey-analysis-cta")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("survey-analysis-navigation")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("response-page")).toBeInTheDocument();
|
||||
|
||||
expect(vi.mocked(SurveyAnalysisCTA)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
environment: mockEnvironment,
|
||||
survey: mockSurvey,
|
||||
isReadOnly: false,
|
||||
user: mockUser,
|
||||
surveyDomain: mockSurveyDomain,
|
||||
responseCount: 10,
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(vi.mocked(SurveyAnalysisNavigation)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
environmentId: mockEnvironmentId,
|
||||
survey: mockSurvey,
|
||||
activeId: "responses",
|
||||
initialTotalResponseCount: 10,
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(vi.mocked(ResponsePage)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
environment: mockEnvironment,
|
||||
survey: mockSurvey,
|
||||
surveyId: mockSurveyId,
|
||||
webAppUrl: "http://localhost:3000",
|
||||
environmentTags: mockTags,
|
||||
user: mockUser,
|
||||
responsesPerPage: 10,
|
||||
locale: mockLocale,
|
||||
isReadOnly: false,
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error if survey not found", async () => {
|
||||
vi.mocked(getSurvey).mockResolvedValue(null);
|
||||
const props = { params: mockParams };
|
||||
await expect(Page(props)).rejects.toThrow("common.survey_not_found");
|
||||
});
|
||||
|
||||
test("throws error if user not found", async () => {
|
||||
vi.mocked(getUser).mockResolvedValue(null);
|
||||
const props = { params: mockParams };
|
||||
await expect(Page(props)).rejects.toThrow("common.user_not_found");
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import ScrollToTop from "./ScrollToTop";
|
||||
|
||||
const containerId = "test-container";
|
||||
|
||||
describe("ScrollToTop", () => {
|
||||
let mockContainer: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContainer = document.createElement("div");
|
||||
mockContainer.id = containerId;
|
||||
mockContainer.scrollTop = 0;
|
||||
mockContainer.scrollTo = vi.fn();
|
||||
mockContainer.addEventListener = vi.fn();
|
||||
mockContainer.removeEventListener = vi.fn();
|
||||
vi.spyOn(document, "getElementById").mockReturnValue(mockContainer);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("renders hidden initially", () => {
|
||||
render(<ScrollToTop containerId={containerId} />);
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("opacity-0");
|
||||
});
|
||||
|
||||
test("calls scrollTo on button click", async () => {
|
||||
render(<ScrollToTop containerId={containerId} />);
|
||||
const button = screen.getByRole("button");
|
||||
|
||||
// Make button visible
|
||||
mockContainer.scrollTop = 301;
|
||||
const scrollEvent = new Event("scroll");
|
||||
mockContainer.dispatchEvent(scrollEvent);
|
||||
|
||||
await userEvent.click(button);
|
||||
expect(mockContainer.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: "smooth" });
|
||||
});
|
||||
|
||||
test("does nothing if container is not found", () => {
|
||||
vi.spyOn(document, "getElementById").mockReturnValue(null);
|
||||
render(<ScrollToTop containerId="non-existent-container" />);
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("opacity-0"); // Stays hidden
|
||||
|
||||
// Try to simulate scroll (though no listener would be attached)
|
||||
fireEvent.scroll(window, { target: { scrollY: 400 } });
|
||||
expect(button).toHaveClass("opacity-0");
|
||||
|
||||
// Try to click
|
||||
userEvent.click(button);
|
||||
// No error should occur, and scrollTo should not be called on a null element
|
||||
});
|
||||
|
||||
test("removes event listener on unmount", () => {
|
||||
const { unmount } = render(<ScrollToTop containerId={containerId} />);
|
||||
expect(mockContainer.addEventListener).toHaveBeenCalledWith("scroll", expect.any(Function));
|
||||
|
||||
unmount();
|
||||
expect(mockContainer.removeEventListener).toHaveBeenCalledWith("scroll", expect.any(Function));
|
||||
});
|
||||
});
|
||||
@@ -1,287 +0,0 @@
|
||||
import { ShareEmbedSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ShareEmbedSurvey";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveySingleUse,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
// Mock data
|
||||
const mockSurveyWeb = {
|
||||
id: "survey1",
|
||||
name: "Web Survey",
|
||||
environmentId: "env1",
|
||||
type: "app",
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Q1" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: 0,
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
singleUse: { enabled: false, isEncrypted: false } as TSurveySingleUse,
|
||||
triggers: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
languages: [],
|
||||
styling: null,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockSurveyLink = {
|
||||
...mockSurveyWeb,
|
||||
id: "survey2",
|
||||
name: "Link Survey",
|
||||
type: "link",
|
||||
singleUse: { enabled: false, isEncrypted: false } as TSurveySingleUse,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockUser = {
|
||||
id: "user1",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
role: "project_manager",
|
||||
objective: "other",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
locale: "en-US",
|
||||
} as unknown as TUser;
|
||||
|
||||
// Mocks
|
||||
const mockRouterRefresh = vi.fn();
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
refresh: mockRouterRefresh,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (str: string) => str,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/analysis/components/ShareSurveyLink", () => ({
|
||||
ShareSurveyLink: vi.fn(() => <div>ShareSurveyLinkMock</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/badge", () => ({
|
||||
Badge: vi.fn(({ text }) => <span data-testid="badge-mock">{text}</span>),
|
||||
}));
|
||||
|
||||
const mockEmbedViewComponent = vi.fn();
|
||||
vi.mock("./shareEmbedModal/EmbedView", () => ({
|
||||
EmbedView: (props: any) => mockEmbedViewComponent(props),
|
||||
}));
|
||||
|
||||
const mockPanelInfoViewComponent = vi.fn();
|
||||
vi.mock("./shareEmbedModal/PanelInfoView", () => ({
|
||||
PanelInfoView: (props: any) => mockPanelInfoViewComponent(props),
|
||||
}));
|
||||
|
||||
let capturedDialogOnOpenChange: ((open: boolean) => void) | undefined;
|
||||
vi.mock("@/modules/ui/components/dialog", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/modules/ui/components/dialog")>(
|
||||
"@/modules/ui/components/dialog"
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
Dialog: (props: React.ComponentProps<typeof actual.Dialog>) => {
|
||||
capturedDialogOnOpenChange = props.onOpenChange;
|
||||
return <actual.Dialog {...props} />;
|
||||
},
|
||||
// DialogTitle, DialogContent, DialogDescription will be the actual components
|
||||
// due to ...actual spread and no specific mock for them here.
|
||||
};
|
||||
});
|
||||
|
||||
describe("ShareEmbedSurvey", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
capturedDialogOnOpenChange = undefined;
|
||||
});
|
||||
|
||||
const mockSetOpen = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
survey: mockSurveyWeb,
|
||||
surveyDomain: "test.com",
|
||||
open: true,
|
||||
modalView: "start" as "start" | "embed" | "panel",
|
||||
setOpen: mockSetOpen,
|
||||
user: mockUser,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockEmbedViewComponent.mockImplementation(
|
||||
({ handleInitialPageButton, tabs, activeId, survey, email, surveyUrl, surveyDomain, locale }) => (
|
||||
<div>
|
||||
<button onClick={() => handleInitialPageButton()}>EmbedViewMockContent</button>
|
||||
<div data-testid="embedview-tabs">{JSON.stringify(tabs)}</div>
|
||||
<div data-testid="embedview-activeid">{activeId}</div>
|
||||
<div data-testid="embedview-survey-id">{survey.id}</div>
|
||||
<div data-testid="embedview-email">{email}</div>
|
||||
<div data-testid="embedview-surveyUrl">{surveyUrl}</div>
|
||||
<div data-testid="embedview-surveyDomain">{surveyDomain}</div>
|
||||
<div data-testid="embedview-locale">{locale}</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
mockPanelInfoViewComponent.mockImplementation(({ handleInitialPageButton }) => (
|
||||
<button onClick={() => handleInitialPageButton()}>PanelInfoViewMockContent</button>
|
||||
));
|
||||
});
|
||||
|
||||
test("renders initial 'start' view correctly when open and modalView is 'start'", () => {
|
||||
render(<ShareEmbedSurvey {...defaultProps} />);
|
||||
expect(screen.getByText("environments.surveys.summary.your_survey_is_public 🎉")).toBeInTheDocument();
|
||||
expect(screen.getByText("ShareSurveyLinkMock")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.summary.whats_next")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.summary.embed_survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.summary.configure_alerts")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.summary.setup_integrations")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.summary.send_to_panel")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("badge-mock")).toHaveTextContent("common.new");
|
||||
});
|
||||
|
||||
test("switches to 'embed' view when 'Embed survey' button is clicked", async () => {
|
||||
render(<ShareEmbedSurvey {...defaultProps} />);
|
||||
const embedButton = screen.getByText("environments.surveys.summary.embed_survey");
|
||||
await userEvent.click(embedButton);
|
||||
expect(mockEmbedViewComponent).toHaveBeenCalled();
|
||||
expect(screen.getByText("EmbedViewMockContent")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("switches to 'panel' view when 'Send to panel' button is clicked", async () => {
|
||||
render(<ShareEmbedSurvey {...defaultProps} />);
|
||||
const panelButton = screen.getByText("environments.surveys.summary.send_to_panel");
|
||||
await userEvent.click(panelButton);
|
||||
expect(mockPanelInfoViewComponent).toHaveBeenCalled();
|
||||
expect(screen.getByText("PanelInfoViewMockContent")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls setOpen(false) when handleInitialPageButton is triggered from EmbedView", async () => {
|
||||
render(<ShareEmbedSurvey {...defaultProps} modalView="embed" />);
|
||||
expect(mockEmbedViewComponent).toHaveBeenCalled();
|
||||
const embedViewButton = screen.getByText("EmbedViewMockContent");
|
||||
await userEvent.click(embedViewButton);
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("calls setOpen(false) when handleInitialPageButton is triggered from PanelInfoView", async () => {
|
||||
render(<ShareEmbedSurvey {...defaultProps} modalView="panel" />);
|
||||
expect(mockPanelInfoViewComponent).toHaveBeenCalled();
|
||||
const panelInfoViewButton = screen.getByText("PanelInfoViewMockContent");
|
||||
await userEvent.click(panelInfoViewButton);
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("handleOpenChange (when Dialog calls its onOpenChange prop)", () => {
|
||||
render(<ShareEmbedSurvey {...defaultProps} open={true} survey={mockSurveyWeb} />);
|
||||
expect(capturedDialogOnOpenChange).toBeDefined();
|
||||
|
||||
// Simulate Dialog closing
|
||||
if (capturedDialogOnOpenChange) capturedDialogOnOpenChange(false);
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
expect(mockRouterRefresh).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Simulate Dialog opening
|
||||
mockRouterRefresh.mockClear();
|
||||
mockSetOpen.mockClear();
|
||||
if (capturedDialogOnOpenChange) capturedDialogOnOpenChange(true);
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(true);
|
||||
expect(mockRouterRefresh).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("correctly configures for 'link' survey type in embed view", () => {
|
||||
render(<ShareEmbedSurvey {...defaultProps} survey={mockSurveyLink} modalView="embed" />);
|
||||
const embedViewProps = vi.mocked(mockEmbedViewComponent).mock.calls[0][0] as {
|
||||
tabs: { id: string; label: string; icon: LucideIcon }[];
|
||||
activeId: string;
|
||||
};
|
||||
expect(embedViewProps.tabs.length).toBe(3);
|
||||
expect(embedViewProps.tabs.find((tab) => tab.id === "app")).toBeUndefined();
|
||||
expect(embedViewProps.tabs[0].id).toBe("email");
|
||||
expect(embedViewProps.activeId).toBe("email");
|
||||
});
|
||||
|
||||
test("correctly configures for 'web' survey type in embed view", () => {
|
||||
render(<ShareEmbedSurvey {...defaultProps} survey={mockSurveyWeb} modalView="embed" />);
|
||||
const embedViewProps = vi.mocked(mockEmbedViewComponent).mock.calls[0][0] as {
|
||||
tabs: { id: string; label: string; icon: LucideIcon }[];
|
||||
activeId: string;
|
||||
};
|
||||
expect(embedViewProps.tabs.length).toBe(1);
|
||||
expect(embedViewProps.tabs[0].id).toBe("app");
|
||||
expect(embedViewProps.activeId).toBe("app");
|
||||
});
|
||||
|
||||
test("useEffect does not change activeId if survey.type changes from web to link (while in embed view)", () => {
|
||||
const { rerender } = render(
|
||||
<ShareEmbedSurvey {...defaultProps} survey={mockSurveyWeb} modalView="embed" />
|
||||
);
|
||||
expect(vi.mocked(mockEmbedViewComponent).mock.calls[0][0].activeId).toBe("app");
|
||||
|
||||
rerender(<ShareEmbedSurvey {...defaultProps} survey={mockSurveyLink} modalView="embed" />);
|
||||
expect(vi.mocked(mockEmbedViewComponent).mock.calls[1][0].activeId).toBe("app"); // Current behavior
|
||||
});
|
||||
|
||||
test("initial showView is set by modalView prop when open is true", () => {
|
||||
render(<ShareEmbedSurvey {...defaultProps} open={true} modalView="embed" />);
|
||||
expect(mockEmbedViewComponent).toHaveBeenCalled();
|
||||
expect(screen.getByText("EmbedViewMockContent")).toBeInTheDocument();
|
||||
cleanup();
|
||||
|
||||
render(<ShareEmbedSurvey {...defaultProps} open={true} modalView="panel" />);
|
||||
expect(mockPanelInfoViewComponent).toHaveBeenCalled();
|
||||
expect(screen.getByText("PanelInfoViewMockContent")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("useEffect sets showView to 'start' when open becomes false", () => {
|
||||
const { rerender } = render(<ShareEmbedSurvey {...defaultProps} open={true} modalView="embed" />);
|
||||
expect(screen.getByText("EmbedViewMockContent")).toBeInTheDocument(); // Starts in embed
|
||||
|
||||
rerender(<ShareEmbedSurvey {...defaultProps} open={false} modalView="embed" />);
|
||||
// Dialog mock returns null when open is false, so EmbedViewMockContent is not found
|
||||
expect(screen.queryByText("EmbedViewMockContent")).not.toBeInTheDocument();
|
||||
// To verify showView is 'start', we'd need to inspect internal state or render start view elements
|
||||
// For now, we trust the useEffect sets showView, and if it were to re-open in 'start' mode, it would show.
|
||||
// The main check is that the previous view ('embed') is gone.
|
||||
});
|
||||
|
||||
test("renders correct label for link tab based on singleUse survey property", () => {
|
||||
render(<ShareEmbedSurvey {...defaultProps} survey={mockSurveyLink} modalView="embed" />);
|
||||
let embedViewProps = vi.mocked(mockEmbedViewComponent).mock.calls[0][0] as {
|
||||
tabs: { id: string; label: string }[];
|
||||
};
|
||||
let linkTab = embedViewProps.tabs.find((tab) => tab.id === "link");
|
||||
expect(linkTab?.label).toBe("environments.surveys.summary.share_the_link");
|
||||
cleanup();
|
||||
vi.mocked(mockEmbedViewComponent).mockClear();
|
||||
|
||||
const mockSurveyLinkSingleUse: TSurvey = {
|
||||
...mockSurveyLink,
|
||||
singleUse: { enabled: true, isEncrypted: true },
|
||||
};
|
||||
render(<ShareEmbedSurvey {...defaultProps} survey={mockSurveyLinkSingleUse} modalView="embed" />);
|
||||
embedViewProps = vi.mocked(mockEmbedViewComponent).mock.calls[0][0] as {
|
||||
tabs: { id: string; label: string }[];
|
||||
};
|
||||
linkTab = embedViewProps.tabs.find((tab) => tab.id === "link");
|
||||
expect(linkTab?.label).toBe("environments.surveys.summary.single_use_links");
|
||||
});
|
||||
});
|
||||
@@ -1,137 +0,0 @@
|
||||
import { ShareSurveyResults } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ShareSurveyResults";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
// Mock Button
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: vi.fn(({ children, onClick, asChild, ...props }: any) => {
|
||||
if (asChild) {
|
||||
// For 'asChild', Button renders its children, potentially passing props via Slot.
|
||||
// Mocking simply renders children inside a div that can receive Button's props.
|
||||
return <div {...props}>{children}</div>;
|
||||
}
|
||||
return (
|
||||
<button onClick={onClick} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock Modal
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: vi.fn(({ children, open }) => (open ? <div data-testid="modal">{children}</div> : null)),
|
||||
}));
|
||||
|
||||
// Mock useTranslate
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: vi.fn(() => ({
|
||||
t: (key: string) => key,
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock Next Link
|
||||
vi.mock("next/link", () => ({
|
||||
default: vi.fn(({ children, href, target, rel, ...props }) => (
|
||||
<a href={href} target={target} rel={rel} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock react-hot-toast
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockSetOpen = vi.fn();
|
||||
const mockHandlePublish = vi.fn();
|
||||
const mockHandleUnpublish = vi.fn();
|
||||
const surveyUrl = "https://app.formbricks.com/s/some-survey-id";
|
||||
|
||||
const defaultProps = {
|
||||
open: true,
|
||||
setOpen: mockSetOpen,
|
||||
handlePublish: mockHandlePublish,
|
||||
handleUnpublish: mockHandleUnpublish,
|
||||
showPublishModal: false,
|
||||
surveyUrl: "",
|
||||
};
|
||||
|
||||
describe("ShareSurveyResults", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Mock navigator.clipboard
|
||||
Object.defineProperty(global.navigator, "clipboard", {
|
||||
value: {
|
||||
writeText: vi.fn(() => Promise.resolve()),
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders publish warning when showPublishModal is false", async () => {
|
||||
render(<ShareSurveyResults {...defaultProps} />);
|
||||
expect(screen.getByText("environments.surveys.summary.publish_to_web_warning")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.publish_to_web_warning_description")
|
||||
).toBeInTheDocument();
|
||||
const publishButton = screen.getByText("environments.surveys.summary.publish_to_web");
|
||||
expect(publishButton).toBeInTheDocument();
|
||||
await userEvent.click(publishButton);
|
||||
expect(mockHandlePublish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("renders survey public info when showPublishModal is true and surveyUrl is provided", async () => {
|
||||
render(<ShareSurveyResults {...defaultProps} showPublishModal={true} surveyUrl={surveyUrl} />);
|
||||
expect(screen.getByText("environments.surveys.summary.survey_results_are_public")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.survey_results_are_shared_with_anyone_who_has_the_link")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(surveyUrl)).toBeInTheDocument();
|
||||
|
||||
const copyButton = screen.getByRole("button", { name: "Copy survey link to clipboard" });
|
||||
expect(copyButton).toBeInTheDocument();
|
||||
await userEvent.click(copyButton);
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(surveyUrl);
|
||||
expect(vi.mocked(toast.success)).toHaveBeenCalledWith("common.link_copied");
|
||||
|
||||
const unpublishButton = screen.getByText("environments.surveys.summary.unpublish_from_web");
|
||||
expect(unpublishButton).toBeInTheDocument();
|
||||
await userEvent.click(unpublishButton);
|
||||
expect(mockHandleUnpublish).toHaveBeenCalledTimes(1);
|
||||
|
||||
const viewSiteLink = screen.getByText("environments.surveys.summary.view_site");
|
||||
expect(viewSiteLink).toBeInTheDocument();
|
||||
const anchor = viewSiteLink.closest("a");
|
||||
expect(anchor).toHaveAttribute("href", surveyUrl);
|
||||
expect(anchor).toHaveAttribute("target", "_blank");
|
||||
expect(anchor).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
test("does not render content when modal is closed (open is false)", () => {
|
||||
render(<ShareSurveyResults {...defaultProps} open={false} />);
|
||||
expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("environments.surveys.summary.publish_to_web_warning")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("environments.surveys.summary.survey_results_are_public")
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders publish warning if surveyUrl is empty even if showPublishModal is true", () => {
|
||||
render(<ShareSurveyResults {...defaultProps} showPublishModal={true} surveyUrl="" />);
|
||||
expect(screen.getByText("environments.surveys.summary.publish_to_web_warning")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("environments.surveys.summary.survey_results_are_public")
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,185 +0,0 @@
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TLanguage } from "@formbricks/types/project";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { SuccessMessage } from "./SuccessMessage";
|
||||
|
||||
// Mock Confetti
|
||||
vi.mock("@/modules/ui/components/confetti", () => ({
|
||||
Confetti: vi.fn(() => <div data-testid="confetti-mock" />),
|
||||
}));
|
||||
|
||||
// Mock useSearchParams from next/navigation
|
||||
vi.mock("next/navigation", () => ({
|
||||
useSearchParams: vi.fn(),
|
||||
usePathname: vi.fn(() => "/"), // Default mock for usePathname if ever needed by underlying logic
|
||||
useRouter: vi.fn(() => ({ push: vi.fn() })), // Default mock for useRouter
|
||||
}));
|
||||
|
||||
// Mock react-hot-toast
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockReplaceState = vi.fn();
|
||||
|
||||
describe("SuccessMessage", () => {
|
||||
let mockUrlSearchParamsGet: ReturnType<typeof vi.fn>;
|
||||
|
||||
const mockEnvironmentBase = {
|
||||
id: "env1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
appSetupCompleted: false,
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockSurveyBase = {
|
||||
id: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
environmentId: "env1",
|
||||
status: "draft",
|
||||
questions: [],
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
headline: { default: "" },
|
||||
html: { default: "" },
|
||||
} as unknown as TSurvey["welcomeCard"],
|
||||
triggers: [],
|
||||
languages: [
|
||||
{
|
||||
default: true,
|
||||
enabled: true,
|
||||
language: { id: "lang1", code: "en", alias: null } as unknown as TLanguage,
|
||||
},
|
||||
],
|
||||
segment: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
variables: [],
|
||||
resultShareKey: null,
|
||||
displayPercentage: null,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks(); // Clears mock calls, instances, contexts and results
|
||||
mockUrlSearchParamsGet = vi.fn();
|
||||
vi.mocked(useSearchParams).mockReturnValue({
|
||||
get: mockUrlSearchParamsGet,
|
||||
} as any);
|
||||
|
||||
Object.defineProperty(window, "location", {
|
||||
value: new URL("http://localhost/somepath"),
|
||||
writable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(window, "history", {
|
||||
value: {
|
||||
replaceState: mockReplaceState,
|
||||
pushState: vi.fn(),
|
||||
go: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
mockReplaceState.mockClear(); // Ensure replaceState mock is clean for each test
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("should show 'almost_there' toast and confetti for app survey with widget not setup when success param is present", async () => {
|
||||
mockUrlSearchParamsGet.mockImplementation((param) => (param === "success" ? "true" : null));
|
||||
const environment: TEnvironment = { ...mockEnvironmentBase, appSetupCompleted: false };
|
||||
const survey: TSurvey = { ...mockSurveyBase, type: "app" };
|
||||
|
||||
render(<SuccessMessage environment={environment} survey={survey} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("confetti-mock")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.surveys.summary.almost_there", {
|
||||
id: "survey-publish-success-toast",
|
||||
icon: "🤏",
|
||||
duration: 5000,
|
||||
position: "bottom-right",
|
||||
});
|
||||
|
||||
expect(mockReplaceState).toHaveBeenCalledWith({}, "", "http://localhost/somepath");
|
||||
});
|
||||
|
||||
test("should show 'congrats' toast and confetti for app survey with widget setup when success param is present", async () => {
|
||||
mockUrlSearchParamsGet.mockImplementation((param) => (param === "success" ? "true" : null));
|
||||
const environment: TEnvironment = { ...mockEnvironmentBase, appSetupCompleted: true };
|
||||
const survey: TSurvey = { ...mockSurveyBase, type: "app" };
|
||||
|
||||
render(<SuccessMessage environment={environment} survey={survey} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("confetti-mock")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.surveys.summary.congrats", {
|
||||
id: "survey-publish-success-toast",
|
||||
icon: "🎉",
|
||||
duration: 5000,
|
||||
position: "bottom-right",
|
||||
});
|
||||
expect(mockReplaceState).toHaveBeenCalledWith({}, "", "http://localhost/somepath");
|
||||
});
|
||||
|
||||
test("should show 'congrats' toast, confetti, and update URL for link survey when success param is present", async () => {
|
||||
mockUrlSearchParamsGet.mockImplementation((param) => (param === "success" ? "true" : null));
|
||||
const environment: TEnvironment = { ...mockEnvironmentBase };
|
||||
const survey: TSurvey = { ...mockSurveyBase, type: "link" };
|
||||
|
||||
Object.defineProperty(window, "location", {
|
||||
value: new URL("http://localhost/somepath?success=true"), // initial URL with success
|
||||
writable: true,
|
||||
});
|
||||
|
||||
render(<SuccessMessage environment={environment} survey={survey} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("confetti-mock")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.surveys.summary.congrats", {
|
||||
id: "survey-publish-success-toast",
|
||||
icon: "🎉",
|
||||
duration: 5000,
|
||||
position: "bottom-right",
|
||||
});
|
||||
expect(mockReplaceState).toHaveBeenCalledWith({}, "", "http://localhost/somepath?share=true");
|
||||
});
|
||||
|
||||
test("should not show confetti or toast if success param is not present", () => {
|
||||
mockUrlSearchParamsGet.mockImplementation((param) => null);
|
||||
const environment: TEnvironment = { ...mockEnvironmentBase };
|
||||
const survey: TSurvey = { ...mockSurveyBase, type: "app" };
|
||||
|
||||
render(<SuccessMessage environment={environment} survey={survey} />);
|
||||
|
||||
expect(screen.queryByTestId("confetti-mock")).not.toBeInTheDocument();
|
||||
expect(toast.success).not.toHaveBeenCalled();
|
||||
expect(mockReplaceState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,468 +0,0 @@
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { MultipleChoiceSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary";
|
||||
import { constructToastMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveySummary,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { SummaryList } from "./SummaryList";
|
||||
|
||||
// Mock child components
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys",
|
||||
() => ({
|
||||
EmptyAppSurveys: vi.fn(() => <div>Mocked EmptyAppSurveys</div>),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary",
|
||||
() => ({
|
||||
CTASummary: vi.fn(({ questionSummary }) => <div>Mocked CTASummary: {questionSummary.question.id}</div>),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary",
|
||||
() => ({
|
||||
CalSummary: vi.fn(({ questionSummary }) => <div>Mocked CalSummary: {questionSummary.question.id}</div>),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary",
|
||||
() => ({
|
||||
ConsentSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked ConsentSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary",
|
||||
() => ({
|
||||
ContactInfoSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked ContactInfoSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary",
|
||||
() => ({
|
||||
DateQuestionSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked DateQuestionSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary",
|
||||
() => ({
|
||||
FileUploadSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked FileUploadSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary",
|
||||
() => ({
|
||||
HiddenFieldsSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked HiddenFieldsSummary: {questionSummary.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary",
|
||||
() => ({
|
||||
MatrixQuestionSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked MatrixQuestionSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary",
|
||||
() => ({
|
||||
MultipleChoiceSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked MultipleChoiceSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary",
|
||||
() => ({
|
||||
NPSSummary: vi.fn(({ questionSummary }) => <div>Mocked NPSSummary: {questionSummary.question.id}</div>),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary",
|
||||
() => ({
|
||||
OpenTextSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked OpenTextSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary",
|
||||
() => ({
|
||||
PictureChoiceSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked PictureChoiceSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RankingSummary",
|
||||
() => ({
|
||||
RankingSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked RankingSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary",
|
||||
() => ({
|
||||
RatingSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked RatingSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
vi.mock("./AddressSummary", () => ({
|
||||
AddressSummary: vi.fn(({ questionSummary }) => (
|
||||
<div>Mocked AddressSummary: {questionSummary.question.id}</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock hooks and utils
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
|
||||
useResponseFilter: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: vi.fn((label, _) => (typeof label === "string" ? label : label.default)),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/empty-space-filler", () => ({
|
||||
EmptySpaceFiller: vi.fn(() => <div>Mocked EmptySpaceFiller</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/skeleton-loader", () => ({
|
||||
SkeletonLoader: vi.fn(() => <div>Mocked SkeletonLoader</div>),
|
||||
}));
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
// This mock setup is for a named export 'toast'
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils", () => ({
|
||||
constructToastMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "env_test_id",
|
||||
type: "production",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
appSetupCompleted: true,
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey_test_id",
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
environmentId: "env_test_id",
|
||||
status: "inProgress",
|
||||
questions: [],
|
||||
hiddenFields: { enabled: false },
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
languages: [],
|
||||
resultShareKey: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayPercentage: null,
|
||||
recontactDays: null,
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
segment: null,
|
||||
variables: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockSelectedFilter = { filter: [], onlyComplete: false };
|
||||
const mockSetSelectedFilter = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
summary: [] as TSurveySummary["summary"],
|
||||
responseCount: 10,
|
||||
environment: mockEnvironment,
|
||||
survey: mockSurvey,
|
||||
totalResponseCount: 20,
|
||||
locale: "en" as TUserLocale,
|
||||
};
|
||||
|
||||
const createMockQuestionSummary = (
|
||||
id: string,
|
||||
type: TSurveyQuestionTypeEnum,
|
||||
headline: string = "Test Question"
|
||||
) =>
|
||||
({
|
||||
question: {
|
||||
id,
|
||||
headline: { default: headline, en: headline },
|
||||
type,
|
||||
required: false,
|
||||
choices:
|
||||
type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
|
||||
? [{ id: "choice1", label: { default: "Choice 1" } }]
|
||||
: undefined,
|
||||
logic: [],
|
||||
},
|
||||
type,
|
||||
responseCount: 5,
|
||||
samples: type === TSurveyQuestionTypeEnum.OpenText ? [{ value: "sample" }] : [],
|
||||
choices:
|
||||
type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
|
||||
? [{ label: { default: "Choice 1" }, count: 5, percentage: 1 }]
|
||||
: [],
|
||||
dismissed:
|
||||
type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
|
||||
? { count: 0, percentage: 0 }
|
||||
: undefined,
|
||||
others:
|
||||
type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
|
||||
? [{ value: "other", count: 0, percentage: 0 }]
|
||||
: [],
|
||||
progress: type === TSurveyQuestionTypeEnum.NPS ? { total: 5, trend: 0.5 } : undefined,
|
||||
average: type === TSurveyQuestionTypeEnum.Rating ? 3.5 : undefined,
|
||||
accepted: type === TSurveyQuestionTypeEnum.Consent ? { count: 5, percentage: 1 } : undefined,
|
||||
results:
|
||||
type === TSurveyQuestionTypeEnum.PictureSelection
|
||||
? [{ imageUrl: "url", count: 5, percentage: 1 }]
|
||||
: undefined,
|
||||
files: type === TSurveyQuestionTypeEnum.FileUpload ? [{ url: "url", name: "file.pdf", size: 100 }] : [],
|
||||
booked: type === TSurveyQuestionTypeEnum.Cal ? { count: 5, percentage: 1 } : undefined,
|
||||
data: type === TSurveyQuestionTypeEnum.Matrix ? [{ rowLabel: "Row1", responses: {} }] : undefined,
|
||||
ranking: type === TSurveyQuestionTypeEnum.Ranking ? [{ rank: 1, choiceLabel: "Choice1", count: 5 }] : [],
|
||||
}) as unknown as TSurveySummary["summary"][number];
|
||||
|
||||
const createMockHiddenFieldSummary = (id: string, label: string = "Hidden Field") =>
|
||||
({
|
||||
id,
|
||||
type: "hiddenField",
|
||||
label,
|
||||
value: "some value",
|
||||
count: 1,
|
||||
samples: [{ personId: "person1", value: "Sample Value", updatedAt: new Date().toISOString() }],
|
||||
responseCount: 1,
|
||||
}) as unknown as TSurveySummary["summary"][number];
|
||||
|
||||
const typeToComponentMockNameMap: Record<TSurveyQuestionTypeEnum, string> = {
|
||||
[TSurveyQuestionTypeEnum.OpenText]: "OpenTextSummary",
|
||||
[TSurveyQuestionTypeEnum.MultipleChoiceSingle]: "MultipleChoiceSummary",
|
||||
[TSurveyQuestionTypeEnum.MultipleChoiceMulti]: "MultipleChoiceSummary",
|
||||
[TSurveyQuestionTypeEnum.NPS]: "NPSSummary",
|
||||
[TSurveyQuestionTypeEnum.CTA]: "CTASummary",
|
||||
[TSurveyQuestionTypeEnum.Rating]: "RatingSummary",
|
||||
[TSurveyQuestionTypeEnum.Consent]: "ConsentSummary",
|
||||
[TSurveyQuestionTypeEnum.PictureSelection]: "PictureChoiceSummary",
|
||||
[TSurveyQuestionTypeEnum.Date]: "DateQuestionSummary",
|
||||
[TSurveyQuestionTypeEnum.FileUpload]: "FileUploadSummary",
|
||||
[TSurveyQuestionTypeEnum.Cal]: "CalSummary",
|
||||
[TSurveyQuestionTypeEnum.Matrix]: "MatrixQuestionSummary",
|
||||
[TSurveyQuestionTypeEnum.Address]: "AddressSummary",
|
||||
[TSurveyQuestionTypeEnum.Ranking]: "RankingSummary",
|
||||
[TSurveyQuestionTypeEnum.ContactInfo]: "ContactInfoSummary",
|
||||
};
|
||||
|
||||
describe("SummaryList", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useResponseFilter).mockReturnValue({
|
||||
selectedFilter: mockSelectedFilter,
|
||||
setSelectedFilter: mockSetSelectedFilter,
|
||||
resetFilter: vi.fn(),
|
||||
} as any);
|
||||
});
|
||||
|
||||
test("renders EmptyAppSurveys when survey type is app, responseCount is 0 and appSetupCompleted is false", () => {
|
||||
const testEnv = { ...mockEnvironment, appSetupCompleted: false };
|
||||
const testSurvey = { ...mockSurvey, type: "app" as const };
|
||||
render(<SummaryList {...defaultProps} survey={testSurvey} responseCount={0} environment={testEnv} />);
|
||||
expect(screen.getByText("Mocked EmptyAppSurveys")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders SkeletonLoader when summary is empty and responseCount is not 0", () => {
|
||||
render(<SummaryList {...defaultProps} summary={[]} responseCount={1} />);
|
||||
expect(screen.getByText("Mocked SkeletonLoader")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders EmptySpaceFiller when responseCount is 0 and summary is not empty (no responses match filter)", () => {
|
||||
const summaryWithItem = [createMockQuestionSummary("q1", TSurveyQuestionTypeEnum.OpenText)];
|
||||
render(
|
||||
<SummaryList {...defaultProps} summary={summaryWithItem} responseCount={0} totalResponseCount={10} />
|
||||
);
|
||||
expect(screen.getByText("Mocked EmptySpaceFiller")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders EmptySpaceFiller when responseCount is 0 and totalResponseCount is 0 (no responses at all)", () => {
|
||||
const summaryWithItem = [createMockQuestionSummary("q1", TSurveyQuestionTypeEnum.OpenText)];
|
||||
render(
|
||||
<SummaryList {...defaultProps} summary={summaryWithItem} responseCount={0} totalResponseCount={0} />
|
||||
);
|
||||
expect(screen.getByText("Mocked EmptySpaceFiller")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const questionTypesToTest: TSurveyQuestionTypeEnum[] = [
|
||||
TSurveyQuestionTypeEnum.OpenText,
|
||||
TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
TSurveyQuestionTypeEnum.NPS,
|
||||
TSurveyQuestionTypeEnum.CTA,
|
||||
TSurveyQuestionTypeEnum.Rating,
|
||||
TSurveyQuestionTypeEnum.Consent,
|
||||
TSurveyQuestionTypeEnum.PictureSelection,
|
||||
TSurveyQuestionTypeEnum.Date,
|
||||
TSurveyQuestionTypeEnum.FileUpload,
|
||||
TSurveyQuestionTypeEnum.Cal,
|
||||
TSurveyQuestionTypeEnum.Matrix,
|
||||
TSurveyQuestionTypeEnum.Address,
|
||||
TSurveyQuestionTypeEnum.Ranking,
|
||||
TSurveyQuestionTypeEnum.ContactInfo,
|
||||
];
|
||||
|
||||
questionTypesToTest.forEach((type) => {
|
||||
test(`renders ${type}Summary component`, () => {
|
||||
const mockSummaryItem = createMockQuestionSummary(`q_${type}`, type);
|
||||
const expectedComponentName = typeToComponentMockNameMap[type];
|
||||
render(<SummaryList {...defaultProps} summary={[mockSummaryItem]} />);
|
||||
expect(
|
||||
screen.getByText(new RegExp(`Mocked ${expectedComponentName}:\\s*q_${type}`))
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders HiddenFieldsSummary component", () => {
|
||||
const mockSummaryItem = createMockHiddenFieldSummary("hf1");
|
||||
render(<SummaryList {...defaultProps} summary={[mockSummaryItem]} />);
|
||||
expect(screen.getByText("Mocked HiddenFieldsSummary: hf1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("setFilter function", () => {
|
||||
const questionId = "q_mc_single";
|
||||
const label: TI18nString = { default: "MC Single Question" };
|
||||
const questionType = TSurveyQuestionTypeEnum.MultipleChoiceSingle;
|
||||
const filterValue = "Choice 1";
|
||||
const filterComboBoxValue = "choice1_id";
|
||||
|
||||
beforeEach(() => {
|
||||
// Render with a component that uses setFilter, e.g., MultipleChoiceSummary
|
||||
const mockSummaryItem = createMockQuestionSummary(questionId, questionType, label.default);
|
||||
render(<SummaryList {...defaultProps} summary={[mockSummaryItem]} />);
|
||||
});
|
||||
|
||||
const getSetFilterFn = () => {
|
||||
const MultipleChoiceSummaryMock = vi.mocked(MultipleChoiceSummary);
|
||||
return MultipleChoiceSummaryMock.mock.calls[0][0].setFilter;
|
||||
};
|
||||
|
||||
test("adds a new filter", () => {
|
||||
const setFilter = getSetFilterFn();
|
||||
vi.mocked(constructToastMessage).mockReturnValue("Custom add message");
|
||||
|
||||
setFilter(questionId, label, questionType, filterValue, filterComboBoxValue);
|
||||
|
||||
expect(mockSetSelectedFilter).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
id: questionId,
|
||||
label: label.default,
|
||||
questionType: questionType,
|
||||
type: OptionsType.QUESTIONS,
|
||||
},
|
||||
filterType: {
|
||||
filterComboBoxValue: filterComboBoxValue,
|
||||
filterValue: filterValue,
|
||||
},
|
||||
},
|
||||
],
|
||||
onlyComplete: false,
|
||||
});
|
||||
// Ensure vi.mocked(toast.success) refers to the spy from the named export
|
||||
expect(vi.mocked(toast).success).toHaveBeenCalledWith("Custom add message", { duration: 5000 });
|
||||
expect(vi.mocked(constructToastMessage)).toHaveBeenCalledWith(
|
||||
questionType,
|
||||
filterValue,
|
||||
mockSurvey,
|
||||
questionId,
|
||||
expect.any(Function), // t function
|
||||
filterComboBoxValue
|
||||
);
|
||||
});
|
||||
|
||||
test("updates an existing filter", () => {
|
||||
const existingFilter = {
|
||||
questionType: {
|
||||
id: questionId,
|
||||
label: label.default,
|
||||
questionType: questionType,
|
||||
type: OptionsType.QUESTIONS,
|
||||
},
|
||||
filterType: {
|
||||
filterComboBoxValue: "old_value_combo",
|
||||
filterValue: "old_value",
|
||||
},
|
||||
};
|
||||
vi.mocked(useResponseFilter).mockReturnValue({
|
||||
selectedFilter: { filter: [existingFilter], onlyComplete: false },
|
||||
setSelectedFilter: mockSetSelectedFilter,
|
||||
resetFilter: vi.fn(),
|
||||
} as any);
|
||||
// Re-render or get setFilter again as selectedFilter changed
|
||||
cleanup();
|
||||
const mockSummaryItem = createMockQuestionSummary(questionId, questionType, label.default);
|
||||
render(<SummaryList {...defaultProps} summary={[mockSummaryItem]} />);
|
||||
const setFilter = getSetFilterFn();
|
||||
|
||||
const newFilterValue = "New Choice";
|
||||
const newFilterComboBoxValue = "new_choice_id";
|
||||
setFilter(questionId, label, questionType, newFilterValue, newFilterComboBoxValue);
|
||||
|
||||
expect(mockSetSelectedFilter).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
id: questionId,
|
||||
label: label.default,
|
||||
questionType: questionType,
|
||||
type: OptionsType.QUESTIONS,
|
||||
},
|
||||
filterType: {
|
||||
filterComboBoxValue: newFilterComboBoxValue,
|
||||
filterValue: newFilterValue,
|
||||
},
|
||||
},
|
||||
],
|
||||
onlyComplete: false,
|
||||
});
|
||||
expect(vi.mocked(toast.success)).toHaveBeenCalledWith(
|
||||
"environments.surveys.summary.filter_updated_successfully",
|
||||
{
|
||||
duration: 5000,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { AppTab } from "./AppTab";
|
||||
|
||||
vi.mock("@/modules/ui/components/options-switch", () => ({
|
||||
OptionsSwitch: (props: {
|
||||
options: Array<{ value: string; label: string }>;
|
||||
handleOptionChange: (value: string) => void;
|
||||
}) => (
|
||||
<div data-testid="options-switch">
|
||||
{props.options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
data-testid={`option-${option.value}`}
|
||||
onClick={() => props.handleOptionChange(option.value)}>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/MobileAppTab",
|
||||
() => ({
|
||||
MobileAppTab: () => <div data-testid="mobile-app-tab">MobileAppTab</div>,
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebAppTab",
|
||||
() => ({
|
||||
WebAppTab: () => <div data-testid="web-app-tab">WebAppTab</div>,
|
||||
})
|
||||
);
|
||||
|
||||
describe("AppTab", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders correctly by default with WebAppTab visible", () => {
|
||||
render(<AppTab />);
|
||||
expect(screen.getByTestId("options-switch")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("option-webapp")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("option-mobile")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId("web-app-tab")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("mobile-app-tab")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("switches to MobileAppTab when mobile option is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AppTab />);
|
||||
|
||||
const mobileOptionButton = screen.getByTestId("option-mobile");
|
||||
await user.click(mobileOptionButton);
|
||||
|
||||
expect(screen.getByTestId("mobile-app-tab")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("web-app-tab")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,233 +0,0 @@
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { AuthenticationError } from "@formbricks/types/errors";
|
||||
import { getEmailHtmlAction, sendEmbedSurveyPreviewEmailAction } from "../../actions";
|
||||
import { EmailTab } from "./EmailTab";
|
||||
|
||||
// Mock actions
|
||||
vi.mock("../../actions", () => ({
|
||||
getEmailHtmlAction: vi.fn(),
|
||||
sendEmbedSurveyPreviewEmailAction: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock helper
|
||||
vi.mock("@/lib/utils/helper", () => ({
|
||||
getFormattedErrorMessage: vi.fn((val) => val?.serverError || "Formatted error message"),
|
||||
}));
|
||||
|
||||
// Mock UI components
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, onClick, variant, title, ...props }: any) => (
|
||||
<button onClick={onClick} data-variant={variant} title={title} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/code-block", () => ({
|
||||
CodeBlock: ({ children, language }: { children: React.ReactNode; language: string }) => (
|
||||
<div data-testid="code-block" data-language={language}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/loading-spinner", () => ({
|
||||
LoadingSpinner: () => <div data-testid="loading-spinner">LoadingSpinner</div>,
|
||||
}));
|
||||
|
||||
// Mock lucide-react icons
|
||||
vi.mock("lucide-react", () => ({
|
||||
Code2Icon: () => <div data-testid="code2-icon" />,
|
||||
CopyIcon: () => <div data-testid="copy-icon" />,
|
||||
MailIcon: () => <div data-testid="mail-icon" />,
|
||||
}));
|
||||
|
||||
// Mock navigator.clipboard
|
||||
const mockWriteText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: {
|
||||
writeText: mockWriteText,
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const surveyId = "test-survey-id";
|
||||
const userEmail = "test@example.com";
|
||||
const mockEmailHtmlPreview = "<p>Hello World ?preview=true&foo=bar</p>";
|
||||
const mockCleanedEmailHtml = "<p>Hello World ?foo=bar</p>";
|
||||
|
||||
describe("EmailTab", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getEmailHtmlAction).mockResolvedValue({ data: mockEmailHtmlPreview });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders initial state correctly and fetches email HTML", async () => {
|
||||
render(<EmailTab surveyId={surveyId} email={userEmail} />);
|
||||
|
||||
expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalledWith({ surveyId });
|
||||
|
||||
// Buttons
|
||||
expect(screen.getByRole("button", { name: "send preview email" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "environments.surveys.summary.view_embed_code_for_email" })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mail-icon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("code2-icon")).toBeInTheDocument();
|
||||
|
||||
// Email preview section
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(`To : ${userEmail}`)).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByText("Subject : environments.surveys.summary.formbricks_email_survey_preview")
|
||||
).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Hello World ?preview=true&foo=bar")).toBeInTheDocument(); // Raw HTML content
|
||||
});
|
||||
expect(screen.queryByTestId("code-block")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("toggles embed code view", async () => {
|
||||
render(<EmailTab surveyId={surveyId} email={userEmail} />);
|
||||
await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled());
|
||||
|
||||
const viewEmbedButton = screen.getByRole("button", {
|
||||
name: "environments.surveys.summary.view_embed_code_for_email",
|
||||
});
|
||||
await userEvent.click(viewEmbedButton);
|
||||
|
||||
// Embed code view
|
||||
expect(screen.getByRole("button", { name: "Embed survey in your website" })).toBeInTheDocument(); // Updated name
|
||||
expect(
|
||||
screen.getByRole("button", { name: "environments.surveys.summary.view_embed_code_for_email" }) // Updated name for hide button
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("copy-icon")).toBeInTheDocument();
|
||||
const codeBlock = screen.getByTestId("code-block");
|
||||
expect(codeBlock).toBeInTheDocument();
|
||||
expect(codeBlock).toHaveTextContent(mockCleanedEmailHtml); // Cleaned HTML
|
||||
expect(screen.queryByText(`To : ${userEmail}`)).not.toBeInTheDocument();
|
||||
|
||||
// Toggle back
|
||||
const hideEmbedButton = screen.getByRole("button", {
|
||||
name: "environments.surveys.summary.view_embed_code_for_email", // Updated name for hide button
|
||||
});
|
||||
await userEvent.click(hideEmbedButton);
|
||||
|
||||
expect(screen.getByRole("button", { name: "send preview email" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "environments.surveys.summary.view_embed_code_for_email" })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(`To : ${userEmail}`)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("code-block")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("copies code to clipboard", async () => {
|
||||
render(<EmailTab surveyId={surveyId} email={userEmail} />);
|
||||
await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled());
|
||||
|
||||
const viewEmbedButton = screen.getByRole("button", {
|
||||
name: "environments.surveys.summary.view_embed_code_for_email",
|
||||
});
|
||||
await userEvent.click(viewEmbedButton);
|
||||
|
||||
// Ensure this line queries by the correct aria-label
|
||||
const copyCodeButton = screen.getByRole("button", { name: "Embed survey in your website" });
|
||||
await userEvent.click(copyCodeButton);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith(mockCleanedEmailHtml);
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.surveys.summary.embed_code_copied_to_clipboard");
|
||||
});
|
||||
|
||||
test("sends preview email successfully", async () => {
|
||||
vi.mocked(sendEmbedSurveyPreviewEmailAction).mockResolvedValue({ data: true });
|
||||
render(<EmailTab surveyId={surveyId} email={userEmail} />);
|
||||
await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled());
|
||||
|
||||
const sendPreviewButton = screen.getByRole("button", { name: "send preview email" });
|
||||
await userEvent.click(sendPreviewButton);
|
||||
|
||||
expect(sendEmbedSurveyPreviewEmailAction).toHaveBeenCalledWith({ surveyId });
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.surveys.summary.email_sent");
|
||||
});
|
||||
|
||||
test("handles send preview email failure (server error)", async () => {
|
||||
const errorResponse = { serverError: "Server issue" };
|
||||
vi.mocked(sendEmbedSurveyPreviewEmailAction).mockResolvedValue(errorResponse as any);
|
||||
render(<EmailTab surveyId={surveyId} email={userEmail} />);
|
||||
await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled());
|
||||
|
||||
const sendPreviewButton = screen.getByRole("button", { name: "send preview email" });
|
||||
await userEvent.click(sendPreviewButton);
|
||||
|
||||
expect(sendEmbedSurveyPreviewEmailAction).toHaveBeenCalledWith({ surveyId });
|
||||
expect(getFormattedErrorMessage).toHaveBeenCalledWith(errorResponse);
|
||||
expect(toast.error).toHaveBeenCalledWith("Server issue");
|
||||
});
|
||||
|
||||
test("handles send preview email failure (authentication error)", async () => {
|
||||
vi.mocked(sendEmbedSurveyPreviewEmailAction).mockRejectedValue(new AuthenticationError("Auth failed"));
|
||||
render(<EmailTab surveyId={surveyId} email={userEmail} />);
|
||||
await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled());
|
||||
|
||||
const sendPreviewButton = screen.getByRole("button", { name: "send preview email" });
|
||||
await userEvent.click(sendPreviewButton);
|
||||
|
||||
expect(sendEmbedSurveyPreviewEmailAction).toHaveBeenCalledWith({ surveyId });
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("common.not_authenticated");
|
||||
});
|
||||
});
|
||||
|
||||
test("handles send preview email failure (generic error)", async () => {
|
||||
vi.mocked(sendEmbedSurveyPreviewEmailAction).mockRejectedValue(new Error("Generic error"));
|
||||
render(<EmailTab surveyId={surveyId} email={userEmail} />);
|
||||
await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled());
|
||||
|
||||
const sendPreviewButton = screen.getByRole("button", { name: "send preview email" });
|
||||
await userEvent.click(sendPreviewButton);
|
||||
|
||||
expect(sendEmbedSurveyPreviewEmailAction).toHaveBeenCalledWith({ surveyId });
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("common.something_went_wrong_please_try_again");
|
||||
});
|
||||
});
|
||||
|
||||
test("renders loading spinner if email HTML is not yet fetched", () => {
|
||||
vi.mocked(getEmailHtmlAction).mockReturnValue(new Promise(() => {})); // Never resolves
|
||||
render(<EmailTab surveyId={surveyId} email={userEmail} />);
|
||||
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders default email if email prop is not provided", async () => {
|
||||
render(<EmailTab surveyId={surveyId} email="" />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("To : user@mail.com")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("emailHtml memo removes various ?preview=true patterns", async () => {
|
||||
const htmlWithVariants =
|
||||
"<p>Test1 ?preview=true</p><p>Test2 ?preview=true&next</p><p>Test3 ?preview=true&;next</p>";
|
||||
// Ensure this line matches the "Received" output from your test error
|
||||
const expectedCleanHtml = "<p>Test1 </p><p>Test2 ?next</p><p>Test3 ?next</p>";
|
||||
vi.mocked(getEmailHtmlAction).mockResolvedValue({ data: htmlWithVariants });
|
||||
|
||||
render(<EmailTab surveyId={surveyId} email={userEmail} />);
|
||||
await waitFor(() => expect(vi.mocked(getEmailHtmlAction)).toHaveBeenCalled());
|
||||
|
||||
const viewEmbedButton = screen.getByRole("button", {
|
||||
name: "environments.surveys.summary.view_embed_code_for_email",
|
||||
});
|
||||
await userEvent.click(viewEmbedButton);
|
||||
|
||||
const codeBlock = screen.getByTestId("code-block");
|
||||
expect(codeBlock).toHaveTextContent(expectedCleanHtml);
|
||||
});
|
||||
});
|
||||
@@ -1,154 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { EmbedView } from "./EmbedView";
|
||||
|
||||
// Mock child components
|
||||
vi.mock("./AppTab", () => ({
|
||||
AppTab: () => <div data-testid="app-tab">AppTab Content</div>,
|
||||
}));
|
||||
vi.mock("./EmailTab", () => ({
|
||||
EmailTab: (props: { surveyId: string; email: string }) => (
|
||||
<div data-testid="email-tab">
|
||||
EmailTab Content for {props.surveyId} with {props.email}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("./LinkTab", () => ({
|
||||
LinkTab: (props: { survey: any; surveyUrl: string }) => (
|
||||
<div data-testid="link-tab">
|
||||
LinkTab Content for {props.survey.id} at {props.surveyUrl}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("./WebsiteTab", () => ({
|
||||
WebsiteTab: (props: { surveyUrl: string; environmentId: string }) => (
|
||||
<div data-testid="website-tab">
|
||||
WebsiteTab Content for {props.surveyUrl} in {props.environmentId}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock @tolgee/react
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock lucide-react
|
||||
vi.mock("lucide-react", () => ({
|
||||
ArrowLeftIcon: () => <div data-testid="arrow-left-icon">ArrowLeftIcon</div>,
|
||||
MailIcon: () => <div data-testid="mail-icon">MailIcon</div>,
|
||||
LinkIcon: () => <div data-testid="link-icon">LinkIcon</div>,
|
||||
GlobeIcon: () => <div data-testid="globe-icon">GlobeIcon</div>,
|
||||
SmartphoneIcon: () => <div data-testid="smartphone-icon">SmartphoneIcon</div>,
|
||||
}));
|
||||
|
||||
const mockTabs = [
|
||||
{ id: "email", label: "Email", icon: () => <div data-testid="email-tab-icon" /> },
|
||||
{ id: "webpage", label: "Web Page", icon: () => <div data-testid="webpage-tab-icon" /> },
|
||||
{ id: "link", label: "Link", icon: () => <div data-testid="link-tab-icon" /> },
|
||||
{ id: "app", label: "App", icon: () => <div data-testid="app-tab-icon" /> },
|
||||
];
|
||||
|
||||
const mockSurveyLink = { id: "survey1", type: "link" };
|
||||
const mockSurveyWeb = { id: "survey2", type: "web" };
|
||||
|
||||
const defaultProps = {
|
||||
handleInitialPageButton: vi.fn(),
|
||||
tabs: mockTabs,
|
||||
activeId: "email",
|
||||
setActiveId: vi.fn(),
|
||||
environmentId: "env1",
|
||||
survey: mockSurveyLink,
|
||||
email: "test@example.com",
|
||||
surveyUrl: "http://example.com/survey1",
|
||||
surveyDomain: "http://example.com",
|
||||
setSurveyUrl: vi.fn(),
|
||||
locale: "en" as any,
|
||||
disableBack: false,
|
||||
};
|
||||
|
||||
describe("EmbedView", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("does not render back button when disableBack is true", () => {
|
||||
render(<EmbedView {...defaultProps} disableBack={true} />);
|
||||
expect(screen.queryByRole("button", { name: "common.back" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not render desktop tabs for non-link survey type", () => {
|
||||
render(<EmbedView {...defaultProps} survey={mockSurveyWeb} />);
|
||||
// Desktop tabs container should not be present or not have lg:flex if it's a common parent
|
||||
const desktopTabsButtons = screen.queryAllByRole("button", { name: /Email|Web Page|Link|App/i });
|
||||
// Check if any of these buttons are part of a container that is only visible on large screens
|
||||
const desktopTabContainer = desktopTabsButtons[0]?.closest("div.lg\\:flex");
|
||||
expect(desktopTabContainer).toBeNull();
|
||||
});
|
||||
|
||||
test("calls setActiveId when a tab is clicked (desktop)", async () => {
|
||||
render(<EmbedView {...defaultProps} survey={mockSurveyLink} activeId="email" />);
|
||||
const webpageTabButton = screen.getAllByRole("button", { name: "Web Page" })[0]; // First one is desktop
|
||||
await userEvent.click(webpageTabButton);
|
||||
expect(defaultProps.setActiveId).toHaveBeenCalledWith("webpage");
|
||||
});
|
||||
|
||||
test("renders EmailTab when activeId is 'email'", () => {
|
||||
render(<EmbedView {...defaultProps} activeId="email" />);
|
||||
expect(screen.getByTestId("email-tab")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(`EmailTab Content for ${defaultProps.survey.id} with ${defaultProps.email}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders WebsiteTab when activeId is 'webpage'", () => {
|
||||
render(<EmbedView {...defaultProps} activeId="webpage" />);
|
||||
expect(screen.getByTestId("website-tab")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(`WebsiteTab Content for ${defaultProps.surveyUrl} in ${defaultProps.environmentId}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders LinkTab when activeId is 'link'", () => {
|
||||
render(<EmbedView {...defaultProps} activeId="link" />);
|
||||
expect(screen.getByTestId("link-tab")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(`LinkTab Content for ${defaultProps.survey.id} at ${defaultProps.surveyUrl}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders AppTab when activeId is 'app'", () => {
|
||||
render(<EmbedView {...defaultProps} activeId="app" />);
|
||||
expect(screen.getByTestId("app-tab")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls setActiveId when a responsive tab is clicked", async () => {
|
||||
render(<EmbedView {...defaultProps} survey={mockSurveyLink} activeId="email" />);
|
||||
// Get the responsive tab button (second instance of the button with this name)
|
||||
const responsiveWebpageTabButton = screen.getAllByRole("button", { name: "Web Page" })[1];
|
||||
await userEvent.click(responsiveWebpageTabButton);
|
||||
expect(defaultProps.setActiveId).toHaveBeenCalledWith("webpage");
|
||||
});
|
||||
|
||||
test("applies active styles to the active tab (desktop)", () => {
|
||||
render(<EmbedView {...defaultProps} survey={mockSurveyLink} activeId="email" />);
|
||||
const emailTabButton = screen.getAllByRole("button", { name: "Email" })[0];
|
||||
expect(emailTabButton).toHaveClass("border-slate-200 bg-slate-100 font-semibold text-slate-900");
|
||||
|
||||
const webpageTabButton = screen.getAllByRole("button", { name: "Web Page" })[0];
|
||||
expect(webpageTabButton).toHaveClass("border-transparent text-slate-500 hover:text-slate-700");
|
||||
});
|
||||
|
||||
test("applies active styles to the active tab (responsive)", () => {
|
||||
render(<EmbedView {...defaultProps} survey={mockSurveyLink} activeId="email" />);
|
||||
const responsiveEmailTabButton = screen.getAllByRole("button", { name: "Email" })[1];
|
||||
expect(responsiveEmailTabButton).toHaveClass("bg-white text-slate-900 shadow-sm");
|
||||
|
||||
const responsiveWebpageTabButton = screen.getAllByRole("button", { name: "Web Page" })[1];
|
||||
expect(responsiveWebpageTabButton).toHaveClass("border-transparent text-slate-700 hover:text-slate-900");
|
||||
});
|
||||
});
|
||||
@@ -1,155 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { LinkTab } from "./LinkTab";
|
||||
|
||||
// Mock ShareSurveyLink
|
||||
vi.mock("@/modules/analysis/components/ShareSurveyLink", () => ({
|
||||
ShareSurveyLink: vi.fn(({ survey, surveyUrl, surveyDomain, locale }) => (
|
||||
<div data-testid="share-survey-link">
|
||||
Mocked ShareSurveyLink
|
||||
<span data-testid="survey-id">{survey.id}</span>
|
||||
<span data-testid="survey-url">{surveyUrl}</span>
|
||||
<span data-testid="survey-domain">{surveyDomain}</span>
|
||||
<span data-testid="locale">{locale}</span>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock useTranslate
|
||||
const mockTranslate = vi.fn((key) => key);
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: mockTranslate,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock next/link
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
status: "inProgress",
|
||||
questions: [],
|
||||
thankYouCard: { enabled: false },
|
||||
endings: [],
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
languages: [],
|
||||
styling: null,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockSurveyUrl = "https://app.formbricks.com/s/survey1";
|
||||
const mockSurveyDomain = "https://app.formbricks.com";
|
||||
const mockSetSurveyUrl = vi.fn();
|
||||
const mockLocale: TUserLocale = "en-US";
|
||||
|
||||
const docsLinksExpected = [
|
||||
{
|
||||
titleKey: "environments.surveys.summary.data_prefilling",
|
||||
descriptionKey: "environments.surveys.summary.data_prefilling_description",
|
||||
link: "https://formbricks.com/docs/link-surveys/data-prefilling",
|
||||
},
|
||||
{
|
||||
titleKey: "environments.surveys.summary.source_tracking",
|
||||
descriptionKey: "environments.surveys.summary.source_tracking_description",
|
||||
link: "https://formbricks.com/docs/link-surveys/source-tracking",
|
||||
},
|
||||
{
|
||||
titleKey: "environments.surveys.summary.create_single_use_links",
|
||||
descriptionKey: "environments.surveys.summary.create_single_use_links_description",
|
||||
link: "https://formbricks.com/docs/link-surveys/single-use-links",
|
||||
},
|
||||
];
|
||||
|
||||
describe("LinkTab", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders the main title", () => {
|
||||
render(
|
||||
<LinkTab
|
||||
survey={mockSurvey}
|
||||
surveyUrl={mockSurveyUrl}
|
||||
surveyDomain={mockSurveyDomain}
|
||||
setSurveyUrl={mockSetSurveyUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.share_the_link_to_get_responses")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ShareSurveyLink with correct props", () => {
|
||||
render(
|
||||
<LinkTab
|
||||
survey={mockSurvey}
|
||||
surveyUrl={mockSurveyUrl}
|
||||
surveyDomain={mockSurveyDomain}
|
||||
setSurveyUrl={mockSetSurveyUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("share-survey-link")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("survey-id")).toHaveTextContent(mockSurvey.id);
|
||||
expect(screen.getByTestId("survey-url")).toHaveTextContent(mockSurveyUrl);
|
||||
expect(screen.getByTestId("survey-domain")).toHaveTextContent(mockSurveyDomain);
|
||||
expect(screen.getByTestId("locale")).toHaveTextContent(mockLocale);
|
||||
});
|
||||
|
||||
test("renders the promotional text for link surveys", () => {
|
||||
render(
|
||||
<LinkTab
|
||||
survey={mockSurvey}
|
||||
surveyUrl={mockSurveyUrl}
|
||||
surveyDomain={mockSurveyDomain}
|
||||
setSurveyUrl={mockSetSurveyUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.you_can_do_a_lot_more_with_links_surveys 💡")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders all documentation links correctly", () => {
|
||||
render(
|
||||
<LinkTab
|
||||
survey={mockSurvey}
|
||||
surveyUrl={mockSurveyUrl}
|
||||
surveyDomain={mockSurveyDomain}
|
||||
setSurveyUrl={mockSetSurveyUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
docsLinksExpected.forEach((doc) => {
|
||||
const linkElement = screen.getByText(doc.titleKey).closest("a");
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
expect(linkElement).toHaveAttribute("href", doc.link);
|
||||
expect(linkElement).toHaveAttribute("target", "_blank");
|
||||
expect(screen.getByText(doc.descriptionKey)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(mockTranslate).toHaveBeenCalledWith("environments.surveys.summary.data_prefilling");
|
||||
expect(mockTranslate).toHaveBeenCalledWith("environments.surveys.summary.data_prefilling_description");
|
||||
expect(mockTranslate).toHaveBeenCalledWith("environments.surveys.summary.source_tracking");
|
||||
expect(mockTranslate).toHaveBeenCalledWith("environments.surveys.summary.source_tracking_description");
|
||||
expect(mockTranslate).toHaveBeenCalledWith("environments.surveys.summary.create_single_use_links");
|
||||
expect(mockTranslate).toHaveBeenCalledWith(
|
||||
"environments.surveys.summary.create_single_use_links_description"
|
||||
);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user