mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 02:55:04 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30feac20f8 | |||
| cb6d1437b7 | |||
| 2302644ab9 |
@@ -49,7 +49,7 @@ runs:
|
|||||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
uses: pnpm/action-setup@v4
|
||||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
VERSION:
|
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
|
required: true
|
||||||
type: string
|
type: string
|
||||||
REPOSITORY:
|
REPOSITORY:
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: helmfile/helmfile-action@v2
|
- uses: helmfile/helmfile-action@v2
|
||||||
name: Deploy Formbricks Cloud Prod
|
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:
|
env:
|
||||||
VERSION: ${{ inputs.VERSION }}
|
VERSION: ${{ inputs.VERSION }}
|
||||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||||
@@ -75,7 +75,6 @@ jobs:
|
|||||||
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.FORMBRICKS_INGRESS_CERT_ARN }}
|
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.FORMBRICKS_INGRESS_CERT_ARN }}
|
||||||
FORMBRICKS_ROLE_ARN: ${{ secrets.FORMBRICKS_ROLE_ARN }}
|
FORMBRICKS_ROLE_ARN: ${{ secrets.FORMBRICKS_ROLE_ARN }}
|
||||||
with:
|
with:
|
||||||
helmfile-version: 'v1.0.0'
|
|
||||||
helm-plugins: >
|
helm-plugins: >
|
||||||
https://github.com/databus23/helm-diff,
|
https://github.com/databus23/helm-diff,
|
||||||
https://github.com/jkroepke/helm-secrets
|
https://github.com/jkroepke/helm-secrets
|
||||||
@@ -85,14 +84,13 @@ jobs:
|
|||||||
|
|
||||||
- uses: helmfile/helmfile-action@v2
|
- uses: helmfile/helmfile-action@v2
|
||||||
name: Deploy Formbricks Cloud Stage
|
name: Deploy Formbricks Cloud Stage
|
||||||
if: inputs.ENVIRONMENT == 'stage'
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ENVIRONMENT == 'stage'
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ inputs.VERSION }}
|
VERSION: ${{ inputs.VERSION }}
|
||||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||||
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.STAGE_FORMBRICKS_INGRESS_CERT_ARN }}
|
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.STAGE_FORMBRICKS_INGRESS_CERT_ARN }}
|
||||||
FORMBRICKS_ROLE_ARN: ${{ secrets.STAGE_FORMBRICKS_ROLE_ARN }}
|
FORMBRICKS_ROLE_ARN: ${{ secrets.STAGE_FORMBRICKS_ROLE_ARN }}
|
||||||
with:
|
with:
|
||||||
helmfile-version: 'v1.0.0'
|
|
||||||
helm-plugins: >
|
helm-plugins: >
|
||||||
https://github.com/databus23/helm-diff,
|
https://github.com/databus23/helm-diff,
|
||||||
https://github.com/jkroepke/helm-secrets
|
https://github.com/jkroepke/helm-secrets
|
||||||
|
|||||||
@@ -30,5 +30,5 @@ jobs:
|
|||||||
- docker-build
|
- docker-build
|
||||||
- helm-chart-release
|
- helm-chart-release
|
||||||
with:
|
with:
|
||||||
VERSION: v${{ needs.docker-build.outputs.VERSION }}
|
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
||||||
ENVIRONMENT: "prod"
|
ENVIRONMENT: "prod"
|
||||||
|
|||||||
@@ -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: ""
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
node-version: 22.x
|
node-version: 22.x
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
"@storybook/react": "8.6.12",
|
"@storybook/react": "8.6.12",
|
||||||
"@storybook/react-vite": "8.6.12",
|
"@storybook/react-vite": "8.6.12",
|
||||||
"@storybook/test": "8.6.12",
|
"@storybook/test": "8.6.12",
|
||||||
"@typescript-eslint/eslint-plugin": "8.32.0",
|
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||||
"@typescript-eslint/parser": "8.32.0",
|
"@typescript-eslint/parser": "8.31.1",
|
||||||
"@vitejs/plugin-react": "4.4.1",
|
"@vitejs/plugin-react": "4.4.1",
|
||||||
"esbuild": "0.25.4",
|
"esbuild": "0.25.2",
|
||||||
"eslint-plugin-storybook": "0.12.0",
|
"eslint-plugin-storybook": "0.12.0",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"storybook": "8.6.12",
|
"storybook": "8.6.12",
|
||||||
|
|||||||
+4
-82
@@ -18,9 +18,8 @@ FROM node:22-alpine3.21 AS base
|
|||||||
FROM base AS installer
|
FROM base AS installer
|
||||||
|
|
||||||
# Enable corepack and prepare pnpm
|
# Enable corepack and prepare pnpm
|
||||||
RUN npm install --ignore-scripts -g corepack@latest
|
RUN npm install -g corepack@latest
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
RUN corepack prepare pnpm@9.15.9 --activate
|
|
||||||
|
|
||||||
# Install necessary build tools and compilers
|
# Install necessary build tools and compilers
|
||||||
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
|
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
|
RUN touch apps/web/.env
|
||||||
|
|
||||||
# Install the dependencies
|
# Install the dependencies
|
||||||
RUN pnpm install --ignore-scripts
|
RUN pnpm install
|
||||||
|
|
||||||
# Build the project using our secret reader script
|
# Build the project using our secret reader script
|
||||||
# This mounts the secrets only during this build step without storing them in layers
|
# 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
|
FROM base AS runner
|
||||||
|
|
||||||
RUN npm install --ignore-scripts -g corepack@latest
|
RUN npm install -g corepack@latest
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
RUN apk add --no-cache curl \
|
RUN apk add --no-cache curl \
|
||||||
@@ -89,81 +88,4 @@ WORKDIR /home/nextjs
|
|||||||
|
|
||||||
# Ensure no write permissions are assigned to the copied resources
|
# Ensure no write permissions are assigned to the copied resources
|
||||||
COPY --from=installer /app/apps/web/.next/standalone ./
|
COPY --from=installer /app/apps/web/.next/standalone ./
|
||||||
RUN chown -R nextjs:nextjs ./ && chmod -R 755 ./
|
RUN chown -R nextjs:nextjs ./ && chmod -R 755 ./
|
||||||
|
|
||||||
COPY --from=installer /app/apps/web/next.config.mjs .
|
|
||||||
RUN chmod 644 ./next.config.mjs
|
|
||||||
|
|
||||||
COPY --from=installer /app/apps/web/package.json .
|
|
||||||
RUN chmod 644 ./package.json
|
|
||||||
|
|
||||||
COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static
|
|
||||||
RUN chown -R nextjs:nextjs ./apps/web/.next/static && chmod -R 755 ./apps/web/.next/static
|
|
||||||
|
|
||||||
COPY --from=installer /app/apps/web/public ./apps/web/public
|
|
||||||
RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public
|
|
||||||
|
|
||||||
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
|
||||||
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
|
|
||||||
|
|
||||||
COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
|
|
||||||
RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
|
|
||||||
|
|
||||||
COPY --from=installer /app/packages/database/migration ./packages/database/migration
|
|
||||||
RUN chown -R nextjs:nextjs ./packages/database/migration && chmod -R 755 ./packages/database/migration
|
|
||||||
|
|
||||||
COPY --from=installer /app/packages/database/src ./packages/database/src
|
|
||||||
RUN chown -R nextjs:nextjs ./packages/database/src && chmod -R 755 ./packages/database/src
|
|
||||||
|
|
||||||
COPY --from=installer /app/packages/database/node_modules ./packages/database/node_modules
|
|
||||||
RUN chown -R nextjs:nextjs ./packages/database/node_modules && chmod -R 755 ./packages/database/node_modules
|
|
||||||
|
|
||||||
COPY --from=installer /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
|
||||||
RUN chown -R nextjs:nextjs ./packages/database/node_modules/@formbricks/logger/dist && chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
|
||||||
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
|
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/.prisma ./node_modules/.prisma
|
|
||||||
RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules/.prisma
|
|
||||||
|
|
||||||
COPY --from=installer /prisma_version.txt .
|
|
||||||
RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
|
|
||||||
|
|
||||||
COPY /docker/cronjobs /app/docker/cronjobs
|
|
||||||
RUN chmod -R 755 /app/docker/cronjobs
|
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
|
|
||||||
RUN chmod -R 755 ./node_modules/@paralleldrive/cuid2
|
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes
|
|
||||||
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
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
ENV HOSTNAME "0.0.0.0"
|
|
||||||
ENV NODE_ENV="production"
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
# Prepare volume for uploads
|
|
||||||
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
|
||||||
VOLUME /home/nextjs/apps/web/uploads/
|
|
||||||
|
|
||||||
# Prepare volume for SAML preloaded connection
|
|
||||||
RUN mkdir -p /home/nextjs/apps/web/saml-connection
|
|
||||||
VOLUME /home/nextjs/apps/web/saml-connection
|
|
||||||
|
|
||||||
CMD if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then \
|
|
||||||
echo "Starting cron jobs..."; \
|
|
||||||
supercronic -quiet /app/docker/cronjobs & \
|
|
||||||
else \
|
|
||||||
echo "Docker cron jobs are disabled via DOCKER_CRON_ENABLED=0"; \
|
|
||||||
fi; \
|
|
||||||
(cd packages/database && npm run db:migrate:deploy) && \
|
|
||||||
(cd packages/database && npm run db:create-saml-database:deploy) && \
|
|
||||||
exec node apps/web/server.js
|
|
||||||
+1
-1
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
|
|||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<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"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const Loading = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 className="h-4 w-28 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ export const MainNavigation = ({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className={cn(
|
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 ? (
|
{isCollapsed ? (
|
||||||
<PanelLeftOpenIcon strokeWidth={1.5} />
|
<PanelLeftOpenIcon strokeWidth={1.5} />
|
||||||
|
|||||||
-456
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-134
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-125
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-85
@@ -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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-694
@@ -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("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-175
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-61
@@ -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"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-50
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-40
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-228
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-606
@@ -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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-152
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-58
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-750
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-171
@@ -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`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-15
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-33
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-148
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-268
@@ -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",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-166
@@ -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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-36
@@ -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`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-249
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-50
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-258
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-70
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-97
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-104
@@ -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"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-117
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-63
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-188
@@ -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");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-29
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-21
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-74
@@ -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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-21
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-134
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-68
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-193
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-192
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-149
@@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-67
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+13
-184
@@ -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 { getUser } from "@/lib/user/service";
|
||||||
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
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 { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import { DeleteOrganization } from "./components/DeleteOrganization";
|
|
||||||
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
|
||||||
import Page from "./page";
|
import Page from "./page";
|
||||||
|
|
||||||
vi.mock("@/lib/constants", () => ({
|
vi.mock("@/lib/constants", () => ({
|
||||||
@@ -59,34 +52,7 @@ vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
|||||||
getWhiteLabelPermission: vi.fn(),
|
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", () => {
|
describe("Page", () => {
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
let mockEnvironmentAuth = {
|
let mockEnvironmentAuth = {
|
||||||
session: { user: { id: "test-user-id" } },
|
session: { user: { id: "test-user-id" } },
|
||||||
currentUserMembership: { role: "owner" },
|
currentUserMembership: { role: "owner" },
|
||||||
@@ -97,10 +63,8 @@ describe("Page", () => {
|
|||||||
|
|
||||||
const mockUser = { id: "test-user-id" } as TUser;
|
const mockUser = { id: "test-user-id" } as TUser;
|
||||||
const mockTranslate = vi.fn((key) => key);
|
const mockTranslate = vi.fn((key) => key);
|
||||||
const mockParams = { environmentId: "env-123" };
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
|
||||||
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
|
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
|
||||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
||||||
@@ -108,163 +72,28 @@ describe("Page", () => {
|
|||||||
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
|
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 = {
|
const props = {
|
||||||
params: Promise.resolve(mockParams),
|
params: Promise.resolve({ environmentId: "env-123" }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageComponent = await Page(props);
|
const result = await Page(props);
|
||||||
render(PageComponent);
|
|
||||||
|
|
||||||
expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument();
|
expect(result).toBeTruthy();
|
||||||
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
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders correctly when user is manager", async () => {
|
test("renders if session user id empty", async () => {
|
||||||
const managerAuth = {
|
mockEnvironmentAuth.session.user.id = "";
|
||||||
...mockEnvironmentAuth,
|
|
||||||
currentUserMembership: { role: "manager" },
|
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
||||||
isOwner: false,
|
|
||||||
isManager: true,
|
|
||||||
} as unknown as TEnvironmentAuth;
|
|
||||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(managerAuth);
|
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
params: Promise.resolve(mockParams),
|
params: Promise.resolve({ environmentId: "env-123" }),
|
||||||
};
|
|
||||||
const PageComponent = await Page(props);
|
|
||||||
render(PageComponent);
|
|
||||||
|
|
||||||
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
isReadOnly: false, // owner or manager can edit
|
|
||||||
}),
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
expect(DeleteOrganization).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
isDeleteDisabled: true, // only owner can delete
|
|
||||||
isUserOwner: false,
|
|
||||||
}),
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders correctly when multi-org is disabled", async () => {
|
|
||||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false);
|
|
||||||
const props = {
|
|
||||||
params: Promise.resolve(mockParams),
|
|
||||||
};
|
|
||||||
const PageComponent = await Page(props);
|
|
||||||
render(PageComponent);
|
|
||||||
|
|
||||||
expect(screen.queryByText("environments.settings.general.delete_organization")).not.toBeInTheDocument();
|
|
||||||
expect(DeleteOrganization).not.toHaveBeenCalled();
|
|
||||||
// isDeleteDisabled should be true because multiOrg is disabled, even for owner
|
|
||||||
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
isReadOnly: false,
|
|
||||||
}),
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders correctly when user is not owner or manager (e.g., admin)", async () => {
|
|
||||||
const adminAuth = {
|
|
||||||
...mockEnvironmentAuth,
|
|
||||||
currentUserMembership: { role: "admin" },
|
|
||||||
isOwner: false,
|
|
||||||
isManager: false,
|
|
||||||
} as unknown as TEnvironmentAuth;
|
|
||||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(adminAuth);
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
params: Promise.resolve(mockParams),
|
|
||||||
};
|
|
||||||
const PageComponent = await Page(props);
|
|
||||||
render(PageComponent);
|
|
||||||
|
|
||||||
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
isReadOnly: true,
|
|
||||||
}),
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
expect(DeleteOrganization).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
isDeleteDisabled: true,
|
|
||||||
isUserOwner: false,
|
|
||||||
}),
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders if session user id empty, user is null", async () => {
|
|
||||||
const noUserSessionAuth = {
|
|
||||||
...mockEnvironmentAuth,
|
|
||||||
session: { ...mockEnvironmentAuth.session, user: { ...mockEnvironmentAuth.session.user, id: "" } },
|
|
||||||
};
|
|
||||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(noUserSessionAuth);
|
|
||||||
vi.mocked(getUser).mockResolvedValue(null);
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
params: Promise.resolve(mockParams),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageComponent = await Page(props);
|
const result = await Page(props);
|
||||||
render(PageComponent);
|
|
||||||
expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument();
|
expect(result).toBeTruthy();
|
||||||
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
user: null,
|
|
||||||
}),
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles getEnvironmentAuth error", async () => {
|
test("handles getEnvironmentAuth error", async () => {
|
||||||
|
|||||||
-98
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-38
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-72
@@ -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"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-25
@@ -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`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-37
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-243
@@ -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)");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-124
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-249
@@ -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" />),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
-388
@@ -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("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+4
-8
@@ -24,8 +24,7 @@ interface ResponseDataViewProps {
|
|||||||
locale: TUserLocale;
|
locale: TUserLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export for testing
|
const formatAddressData = (responseValue: TResponseDataValue): Record<string, string> => {
|
||||||
export const formatAddressData = (responseValue: TResponseDataValue): Record<string, string> => {
|
|
||||||
const addressKeys = ["addressLine1", "addressLine2", "city", "state", "zip", "country"];
|
const addressKeys = ["addressLine1", "addressLine2", "city", "state", "zip", "country"];
|
||||||
return Array.isArray(responseValue)
|
return Array.isArray(responseValue)
|
||||||
? responseValue.reduce((acc, curr, index) => {
|
? responseValue.reduce((acc, curr, index) => {
|
||||||
@@ -35,8 +34,7 @@ export const formatAddressData = (responseValue: TResponseDataValue): Record<str
|
|||||||
: {};
|
: {};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export for testing
|
const formatContactInfoData = (responseValue: TResponseDataValue): Record<string, string> => {
|
||||||
export const formatContactInfoData = (responseValue: TResponseDataValue): Record<string, string> => {
|
|
||||||
const addressKeys = ["firstName", "lastName", "email", "phone", "company"];
|
const addressKeys = ["firstName", "lastName", "email", "phone", "company"];
|
||||||
return Array.isArray(responseValue)
|
return Array.isArray(responseValue)
|
||||||
? responseValue.reduce((acc, curr, index) => {
|
? responseValue.reduce((acc, curr, index) => {
|
||||||
@@ -46,8 +44,7 @@ export const formatContactInfoData = (responseValue: TResponseDataValue): Record
|
|||||||
: {};
|
: {};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export for testing
|
const extractResponseData = (response: TResponse, survey: TSurvey): Record<string, any> => {
|
||||||
export const extractResponseData = (response: TResponse, survey: TSurvey): Record<string, any> => {
|
|
||||||
let responseData: Record<string, any> = {};
|
let responseData: Record<string, any> = {};
|
||||||
|
|
||||||
survey.questions.forEach((question) => {
|
survey.questions.forEach((question) => {
|
||||||
@@ -76,8 +73,7 @@ export const extractResponseData = (response: TResponse, survey: TSurvey): Recor
|
|||||||
return responseData;
|
return responseData;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export for testing
|
const mapResponsesToTableData = (
|
||||||
export const mapResponsesToTableData = (
|
|
||||||
responses: TResponse[],
|
responses: TResponse[],
|
||||||
survey: TSurvey,
|
survey: TSurvey,
|
||||||
t: TFnType
|
t: TFnType
|
||||||
|
|||||||
-374
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-487
@@ -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" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-259
@@ -1,259 +0,0 @@
|
|||||||
import { processResponseData } from "@/lib/responses";
|
|
||||||
import { getSelectionColumn } from "@/modules/ui/components/data-table";
|
|
||||||
import { cleanup } from "@testing-library/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"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-241
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-67
@@ -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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-287
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-137
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-185
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-468
@@ -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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-63
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-233
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-154
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-155
@@ -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"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-69
@@ -1,69 +0,0 @@
|
|||||||
import { cleanup, render, screen } from "@testing-library/react";
|
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
||||||
import { MobileAppTab } from "./MobileAppTab";
|
|
||||||
|
|
||||||
// Mock @tolgee/react
|
|
||||||
vi.mock("@tolgee/react", () => ({
|
|
||||||
useTranslate: () => ({
|
|
||||||
t: (key: string) => key, // Return the key itself for easy assertion
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock UI components
|
|
||||||
vi.mock("@/modules/ui/components/alert", () => ({
|
|
||||||
Alert: ({ children }: { children: React.ReactNode }) => <div data-testid="alert">{children}</div>,
|
|
||||||
AlertTitle: ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<div data-testid="alert-title">{children}</div>
|
|
||||||
),
|
|
||||||
AlertDescription: ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<div data-testid="alert-description">{children}</div>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/modules/ui/components/button", () => ({
|
|
||||||
Button: ({ children, asChild, ...props }: { children: React.ReactNode; asChild?: boolean }) =>
|
|
||||||
asChild ? <div {...props}>{children}</div> : <button {...props}>{children}</button>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock next/link
|
|
||||||
vi.mock("next/link", () => ({
|
|
||||||
default: ({ children, href, target, ...props }: any) => (
|
|
||||||
<a href={href} target={target} {...props}>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("MobileAppTab", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders correctly with title, description, and learn more link", () => {
|
|
||||||
render(<MobileAppTab />);
|
|
||||||
|
|
||||||
// Check for Alert component
|
|
||||||
expect(screen.getByTestId("alert")).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check for AlertTitle with correct Tolgee key
|
|
||||||
const alertTitle = screen.getByTestId("alert-title");
|
|
||||||
expect(alertTitle).toBeInTheDocument();
|
|
||||||
expect(alertTitle).toHaveTextContent("environments.surveys.summary.quickstart_mobile_apps");
|
|
||||||
|
|
||||||
// Check for AlertDescription with correct Tolgee key
|
|
||||||
const alertDescription = screen.getByTestId("alert-description");
|
|
||||||
expect(alertDescription).toBeInTheDocument();
|
|
||||||
expect(alertDescription).toHaveTextContent(
|
|
||||||
"environments.surveys.summary.quickstart_mobile_apps_description"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check for the "Learn more" link
|
|
||||||
const learnMoreLink = screen.getByRole("link", { name: "common.learn_more" });
|
|
||||||
expect(learnMoreLink).toBeInTheDocument();
|
|
||||||
expect(learnMoreLink).toHaveAttribute(
|
|
||||||
"href",
|
|
||||||
"https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides"
|
|
||||||
);
|
|
||||||
expect(learnMoreLink).toHaveAttribute("target", "_blank");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-108
@@ -1,108 +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 { PanelInfoView } from "./PanelInfoView";
|
|
||||||
|
|
||||||
// Mock next/image
|
|
||||||
vi.mock("next/image", () => ({
|
|
||||||
default: ({ src, alt, className }: { src: any; alt: string; className?: string }) => (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img src={src.src} alt={alt} className={className} />
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock next/link
|
|
||||||
vi.mock("next/link", () => ({
|
|
||||||
default: ({ children, href, target }: { children: React.ReactNode; href: string; target?: string }) => (
|
|
||||||
<a href={href} target={target}>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock Button component
|
|
||||||
vi.mock("@/modules/ui/components/button", () => ({
|
|
||||||
Button: ({ children, onClick, variant, asChild }: any) => {
|
|
||||||
if (asChild) {
|
|
||||||
return <div onClick={onClick}>{children}</div>; // NOSONAR
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button onClick={onClick} data-variant={variant}>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock lucide-react
|
|
||||||
vi.mock("lucide-react", () => ({
|
|
||||||
ArrowLeftIcon: vi.fn(() => <div data-testid="arrow-left-icon">ArrowLeftIcon</div>),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockHandleInitialPageButton = vi.fn();
|
|
||||||
|
|
||||||
describe("PanelInfoView", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders correctly with back button and all sections", async () => {
|
|
||||||
render(<PanelInfoView disableBack={false} handleInitialPageButton={mockHandleInitialPageButton} />);
|
|
||||||
|
|
||||||
// Check for back button
|
|
||||||
const backButton = screen.getByText("common.back");
|
|
||||||
expect(backButton).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId("arrow-left-icon")).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check images
|
|
||||||
expect(screen.getAllByAltText("Prolific panel selection UI")[0]).toBeInTheDocument();
|
|
||||||
expect(screen.getAllByAltText("Prolific panel selection UI")[1]).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check text content (Tolgee keys)
|
|
||||||
expect(screen.getByText("environments.surveys.summary.what_is_a_panel")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("environments.surveys.summary.what_is_a_panel_answer")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("environments.surveys.summary.when_do_i_need_it")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("environments.surveys.summary.when_do_i_need_it_answer")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("environments.surveys.summary.what_is_prolific")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("environments.surveys.summary.what_is_prolific_answer")).toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(screen.getByText("environments.surveys.summary.how_to_create_a_panel")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("environments.surveys.summary.how_to_create_a_panel_step_1")).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText("environments.surveys.summary.how_to_create_a_panel_step_1_description")
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("environments.surveys.summary.how_to_create_a_panel_step_2")).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText("environments.surveys.summary.how_to_create_a_panel_step_2_description")
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("environments.surveys.summary.how_to_create_a_panel_step_3")).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText("environments.surveys.summary.how_to_create_a_panel_step_3_description")
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("environments.surveys.summary.how_to_create_a_panel_step_4")).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText("environments.surveys.summary.how_to_create_a_panel_step_4_description")
|
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check "Learn more" link
|
|
||||||
const learnMoreLink = screen.getByRole("link", { name: "common.learn_more" });
|
|
||||||
expect(learnMoreLink).toBeInTheDocument();
|
|
||||||
expect(learnMoreLink).toHaveAttribute(
|
|
||||||
"href",
|
|
||||||
"https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/market-research-panel"
|
|
||||||
);
|
|
||||||
expect(learnMoreLink).toHaveAttribute("target", "_blank");
|
|
||||||
|
|
||||||
// Click back button
|
|
||||||
await userEvent.click(backButton);
|
|
||||||
expect(mockHandleInitialPageButton).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders correctly without back button when disableBack is true", () => {
|
|
||||||
render(<PanelInfoView disableBack={true} handleInitialPageButton={mockHandleInitialPageButton} />);
|
|
||||||
|
|
||||||
expect(screen.queryByRole("button", { name: "common.back" })).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId("arrow-left-icon")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user