chore: exclude TSX files from unit test coverage (#6723)

Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
Matti Nannt
2025-10-22 14:55:44 +02:00
committed by GitHub
parent accb4f461d
commit 19389bfffc
615 changed files with 11 additions and 115477 deletions

View File

@@ -1,322 +0,0 @@
---
description:
globs:
alwaysApply: false
---
# Testing Patterns & Best Practices
## Running Tests
### Test Commands
From the **root directory** (formbricks/):
- `npm test` - Run all tests across all packages (recommended for CI/full testing)
- `npm run test:coverage` - Run all tests with coverage reports
- `npm run test:e2e` - Run end-to-end tests with Playwright
From the **apps/web directory** (apps/web/):
- `npm run test` - Run only web app tests (fastest for development)
- `npm run test:coverage` - Run web app tests with coverage
- `npm run test -- <file-pattern>` - Run specific test files
### Examples
```bash
# Run all tests from root (takes ~3 minutes, runs 790 test files with 5334+ tests)
npm test
# Run specific test file from apps/web (fastest for development)
npm run test -- modules/cache/lib/service.test.ts
# Run tests matching pattern from apps/web
npm run test -- modules/ee/license-check/lib/license.test.ts
# Run with coverage from root
npm run test:coverage
# Run specific test with watch mode from apps/web (for development)
npm run test -- --watch modules/cache/lib/service.test.ts
# Run tests for a specific directory from apps/web
npm run test -- modules/cache/
```
### Performance Tips
- **For development**: Use `apps/web` directory commands to run only web app tests
- **For CI/validation**: Use root directory commands to run all packages
- **For specific features**: Use file patterns to target specific test files
- **For debugging**: Use `--watch` mode for continuous testing during development
### Test File Organization
- Place test files in the **same directory** as the source file
- Use `.test.ts` for utility/service tests (Node environment)
- Use `.test.tsx` for React component tests (jsdom environment)
## Test File Naming & Environment
### File Extensions
- Use `.test.tsx` for React component/hook tests (runs in jsdom environment)
- Use `.test.ts` for utility/service tests (runs in Node environment)
- The vitest config uses `environmentMatchGlobs` to automatically set jsdom for `.tsx` files
### Test Structure
```typescript
// Import the mocked functions first
import { useHook } from "@/path/to/hook";
import { serviceFunction } from "@/path/to/service";
import { renderHook, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, test, vi } from "vitest";
// Mock dependencies
vi.mock("@/path/to/hook", () => ({
useHook: vi.fn(),
}));
describe("ComponentName", () => {
beforeEach(() => {
vi.clearAllMocks();
// Setup default mocks
});
test("descriptive test name", async () => {
// Test implementation
});
});
```
## React Hook Testing
### Context Mocking
When testing hooks that use React Context:
```typescript
vi.mocked(useResponseFilter).mockReturnValue({
selectedFilter: {
filter: [],
responseStatus: "all",
},
setSelectedFilter: vi.fn(),
selectedOptions: {
questionOptions: [],
questionFilterOptions: [],
},
setSelectedOptions: vi.fn(),
dateRange: { from: new Date(), to: new Date() },
setDateRange: vi.fn(),
resetState: vi.fn(),
});
```
### Testing Async Hooks
- Always use `waitFor` for async operations
- Test both loading and completed states
- Verify API calls with correct parameters
```typescript
test("fetches data on mount", async () => {
const { result } = renderHook(() => useHook());
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toBe(expectedData);
expect(vi.mocked(apiCall)).toHaveBeenCalledWith(expectedParams);
});
```
### Testing Hook Dependencies
To test useEffect dependencies, ensure mocks return different values:
```typescript
// First render
mockGetFormattedFilters.mockReturnValue(mockFilters);
// Change dependency and trigger re-render
const newMockFilters = { ...mockFilters, finished: true };
mockGetFormattedFilters.mockReturnValue(newMockFilters);
rerender();
```
## Performance Testing
### Race Condition Testing
Test AbortController implementation:
```typescript
test("cancels previous request when new request is made", async () => {
let resolveFirst: (value: any) => void;
let resolveSecond: (value: any) => void;
const firstPromise = new Promise((resolve) => {
resolveFirst = resolve;
});
const secondPromise = new Promise((resolve) => {
resolveSecond = resolve;
});
vi.mocked(apiCall)
.mockReturnValueOnce(firstPromise as any)
.mockReturnValueOnce(secondPromise as any);
const { result } = renderHook(() => useHook());
// Trigger second request
result.current.refetch();
// Resolve in order - first should be cancelled
resolveFirst!({ data: 100 });
resolveSecond!({ data: 200 });
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Should have result from second request
expect(result.current.data).toBe(200);
});
```
### Cleanup Testing
```typescript
test("cleans up on unmount", () => {
const abortSpy = vi.spyOn(AbortController.prototype, "abort");
const { unmount } = renderHook(() => useHook());
unmount();
expect(abortSpy).toHaveBeenCalled();
abortSpy.mockRestore();
});
```
## Error Handling Testing
### API Error Testing
```typescript
test("handles API errors gracefully", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
vi.mocked(apiCall).mockRejectedValue(new Error("API Error"));
const { result } = renderHook(() => useHook());
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(consoleSpy).toHaveBeenCalledWith("Error message:", expect.any(Error));
expect(result.current.data).toBe(fallbackValue);
consoleSpy.mockRestore();
});
```
### Cancelled Request Testing
```typescript
test("does not update state for cancelled requests", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
let rejectFirst: (error: any) => void;
const firstPromise = new Promise((_, reject) => {
rejectFirst = reject;
});
vi.mocked(apiCall)
.mockReturnValueOnce(firstPromise as any)
.mockResolvedValueOnce({ data: 42 });
const { result } = renderHook(() => useHook());
result.current.refetch();
const abortError = new Error("Request cancelled");
rejectFirst!(abortError);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Should not log error for cancelled request
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
```
## Type Safety in Tests
### Mock Type Assertions
Use type assertions for edge cases:
```typescript
vi.mocked(apiCall).mockResolvedValue({
data: null as any, // For testing null handling
});
vi.mocked(apiCall).mockResolvedValue({
data: undefined as any, // For testing undefined handling
});
```
### Proper Mock Typing
Ensure mocks match the actual interface:
```typescript
const mockSurvey: TSurvey = {
id: "survey-123",
name: "Test Survey",
// ... other required properties
} as unknown as TSurvey; // Use when partial mocking is needed
```
## Common Test Patterns
### Testing State Changes
```typescript
test("updates state correctly", async () => {
const { result } = renderHook(() => useHook());
// Initial state
expect(result.current.value).toBe(initialValue);
// Trigger change
result.current.updateValue(newValue);
// Verify change
expect(result.current.value).toBe(newValue);
});
```
### Testing Multiple Scenarios
```typescript
test("handles different modes", async () => {
// Test regular mode
vi.mocked(useParams).mockReturnValue({ surveyId: "123" });
const { rerender } = renderHook(() => useHook());
await waitFor(() => {
expect(vi.mocked(regularApi)).toHaveBeenCalled();
});
rerender();
await waitFor(() => {
expect(vi.mocked(sharingApi)).toHaveBeenCalled();
});
});
```
## Test Organization
### Comprehensive Test Coverage
For hooks, ensure you test:
- ✅ Initialization (with/without initial values)
- ✅ Data fetching (success/error cases)
- ✅ State updates and refetching
- ✅ Dependency changes triggering effects
- ✅ Manual actions (refetch, reset)
- ✅ Race condition prevention
- ✅ Cleanup on unmount
- ✅ Mode switching (if applicable)
- ✅ Edge cases (null/undefined data)
### Test Naming
Use descriptive test names that explain the scenario:
- ✅ "initializes with initial count"
- ✅ "fetches response count on mount for regular survey"
- ✅ "cancels previous request when new request is made"
- ❌ "test hook"
- ❌ "it works"

View File

@@ -1,7 +0,0 @@
---
description: Whenever the user asks to write or update a test file for .tsx or .ts files.
globs:
alwaysApply: false
---
Use the rules in this file when writing tests [copilot-instructions.md](mdc:.github/copilot-instructions.md).
After writing the tests, run them and check if there's any issue with the tests and if all of them are passing. Fix the issues and rerun the tests until all pass.

View File

@@ -1,32 +0,0 @@
# Testing Instructions
When generating test files inside the "/app/web" path, follow these rules:
- You are an experienced senior software engineer
- Use vitest
- Ensure 100% code coverage
- Add as few comments as possible
- The test file should be located in the same folder as the original file
- Use the `test` function instead of `it`
- Follow the same test pattern used for other files in the package where the file is located
- All imports should be at the top of the file, not inside individual tests
- For mocking inside "test" blocks use "vi.mocked"
- If the file is located in the "packages/survey" path, use "@testing-library/preact" instead of "@testing-library/react"
- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file
- When using "screen.getByText" check for the tolgee string if it is being used in the file.
- The types for mocked variables can be found in the "packages/types" path. Be sure that every imported type exists before using it. Don't create types that are not already in the codebase.
- When mocking data check if the properties added are part of the type of the object being mocked. Only specify known properties, don't use properties that are not part of the type.
If it's a test for a ".tsx" file, follow these extra instructions:
- Add this code inside the "describe" block and before any test:
afterEach(() => {
cleanup();
});
- The "afterEach" function should only have the "cleanup()" line inside it and should be adde to the "vitest" imports.
- For click events, import userEvent from "@testing-library/user-event"
- Mock other components that can make the text more complex and but at the same time mocking it wouldn't make the test flaky. It's ok to leave basic and simple components.
- You don't need to mock @tolgee/react
- Use "import "@testing-library/jest-dom/vitest";"

View File

@@ -2,7 +2,7 @@
## Project Structure & Module Organization
Formbricks runs as a pnpm/turbo monorepo. `apps/web` is the Next.js product surface, with feature modules under `app/` and `modules/`, assets in `public/` and `images/`, and Playwright specs in `apps/web/playwright/`. `apps/storybook` renders reusable UI pieces for review. Shared logic lives in `packages/*`: `database` (Prisma schemas/migrations), `surveys`, `js-core`, `types`, plus linting and TypeScript presets (`config-*`). Deployment collateral is kept in `docs/`, `docker/`, and `helm-chart/`. Tests generally sit next to their source as `*.test.ts(x)` or inside `__tests__`.
Formbricks runs as a pnpm/turbo monorepo. `apps/web` is the Next.js product surface, with feature modules under `app/` and `modules/`, assets in `public/` and `images/`, and Playwright specs in `apps/web/playwright/`. `apps/storybook` renders reusable UI pieces for review. Shared logic lives in `packages/*`: `database` (Prisma schemas/migrations), `surveys`, `js-core`, `types`, plus linting and TypeScript presets (`config-*`). Deployment collateral is kept in `docs/`, `docker/`, and `helm-chart/`. Unit tests sit next to their source as `*.test.ts` or inside `__tests__`.
## Build, Test & Development Commands
@@ -21,7 +21,7 @@ TypeScript, React, and Prisma are the primary languages. Use the shared ESLint p
## Testing Guidelines
Prefer Vitest with Testing Library for logic in `.ts` files, keeping specs colocated with the code they exercise (`utility.test.ts`). Do not write tests for `.tsx` files. Mock network and storage boundaries through helpers from `@formbricks/*`. Run `pnpm test` before opening a PR and `pnpm test:coverage` when touching critical flows; keep coverage from regressing. End-to-end scenarios belong in `apps/web/playwright`, using descriptive filenames (`billing.spec.ts`) and tagging slow suites with `@slow` when necessary.
Prefer Vitest with Testing Library for logic in `.ts` files, keeping specs colocated with the code they exercise (`utility.test.ts`). Do not write tests for `.tsx` files—React components are covered by Playwright E2E tests instead. Mock network and storage boundaries through helpers from `@formbricks/*`. Run `pnpm test` before opening a PR and `pnpm test:coverage` when touching critical flows; keep coverage from regressing. End-to-end scenarios belong in `apps/web/playwright`, using descriptive filenames (`billing.spec.ts`) and tagging slow suites with `@slow` when necessary.
## Commit & Pull Request Guidelines

View File

@@ -1,79 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { ConnectWithFormbricks } from "./ConnectWithFormbricks";
// Mocks before import
const pushMock = vi.fn();
const refreshMock = vi.fn();
vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t: (key: string) => key }) }));
vi.mock("next/navigation", () => ({ useRouter: vi.fn(() => ({ push: pushMock, refresh: refreshMock })) }));
vi.mock("./OnboardingSetupInstructions", () => ({
OnboardingSetupInstructions: () => <div data-testid="instructions" />,
}));
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe("ConnectWithFormbricks", () => {
const environment = { id: "env1" } as any;
const webAppUrl = "http://app";
const channel = {} as any;
test("renders waiting state when appSetupCompleted is false", () => {
render(
<ConnectWithFormbricks
environment={environment}
publicDomain={webAppUrl}
appSetupCompleted={false}
channel={channel}
/>
);
expect(screen.getByTestId("instructions")).toBeInTheDocument();
expect(screen.getByText("environments.connect.waiting_for_your_signal")).toBeInTheDocument();
});
test("renders success state when appSetupCompleted is true", () => {
render(
<ConnectWithFormbricks
environment={environment}
publicDomain={webAppUrl}
appSetupCompleted={true}
channel={channel}
/>
);
expect(screen.getByText("environments.connect.congrats")).toBeInTheDocument();
expect(screen.getByText("environments.connect.connection_successful_message")).toBeInTheDocument();
});
test("clicking finish button navigates to surveys", async () => {
render(
<ConnectWithFormbricks
environment={environment}
publicDomain={webAppUrl}
appSetupCompleted={true}
channel={channel}
/>
);
const button = screen.getByRole("button", { name: "environments.connect.finish_onboarding" });
await userEvent.click(button);
expect(pushMock).toHaveBeenCalledWith(`/environments/${environment.id}/surveys`);
});
test("refresh is called on visibilitychange to visible", () => {
render(
<ConnectWithFormbricks
environment={environment}
publicDomain={webAppUrl}
appSetupCompleted={false}
channel={channel}
/>
);
Object.defineProperty(document, "visibilityState", { value: "visible", configurable: true });
document.dispatchEvent(new Event("visibilitychange"));
expect(refreshMock).toHaveBeenCalled();
});
});

View File

@@ -1,103 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import toast from "react-hot-toast";
import { afterEach, beforeAll, describe, expect, test, vi } from "vitest";
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
// Mock react-hot-toast so we can assert that a success message is shown
vi.mock("react-hot-toast", () => ({
__esModule: true,
default: {
success: vi.fn(),
},
}));
// Set up a spy for navigator.clipboard.writeText so it becomes a ViTest spy.
beforeAll(() => {
Object.defineProperty(navigator, "clipboard", {
configurable: true,
writable: true,
value: {
// Using a mockResolvedValue resolves the promise as writeText is async.
writeText: vi.fn().mockResolvedValue(undefined),
},
});
});
describe("OnboardingSetupInstructions", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
// Provide some default props for testing
const defaultProps = {
environmentId: "env-123",
publicDomain: "https://example.com",
channel: "app" as const, // Assuming channel is either "app" or "website"
appSetupCompleted: false,
};
test("renders HTML tab content by default", () => {
render(<OnboardingSetupInstructions {...defaultProps} />);
// Since the default active tab is "html", we check for a unique text
expect(
screen.getByText(/environments.connect.insert_this_code_into_the_head_tag_of_your_website/i)
).toBeInTheDocument();
// The HTML snippet contains a marker comment
expect(screen.getByText("START")).toBeInTheDocument();
// Verify the "Copy Code" button is present
expect(screen.getByRole("button", { name: /common.copy_code/i })).toBeInTheDocument();
});
test("renders NPM tab content when selected", async () => {
render(<OnboardingSetupInstructions {...defaultProps} />);
const user = userEvent.setup();
// Click on the "NPM" tab to switch views.
const npmTab = screen.getByText("NPM");
await user.click(npmTab);
// Check that the install commands are present
expect(screen.getByText(/npm install @formbricks\/js/)).toBeInTheDocument();
expect(screen.getByText(/yarn add @formbricks\/js/)).toBeInTheDocument();
// Verify the "Read Docs" link has the correct URL (based on channel prop)
const readDocsLink = screen.getByRole("link", { name: /common.read_docs/i });
expect(readDocsLink).toHaveAttribute("href", "https://formbricks.com/docs/app-surveys/framework-guides");
});
test("copies HTML snippet to clipboard and shows success toast when Copy Code button is clicked", async () => {
render(<OnboardingSetupInstructions {...defaultProps} />);
const user = userEvent.setup();
const writeTextSpy = vi.spyOn(navigator.clipboard, "writeText");
// Click the "Copy Code" button
const copyButton = screen.getByRole("button", { name: /common.copy_code/i });
await user.click(copyButton);
// Ensure navigator.clipboard.writeText was called.
expect(writeTextSpy).toHaveBeenCalled();
const writtenText = (navigator.clipboard.writeText as any).mock.calls[0][0] as string;
// Check that the pasted snippet contains the expected environment values
expect(writtenText).toContain('var appUrl = "https://example.com"');
expect(writtenText).toContain('var environmentId = "env-123"');
// Verify that a success toast was shown
expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard");
});
test("renders step-by-step manual link with correct URL in HTML tab", () => {
render(<OnboardingSetupInstructions {...defaultProps} />);
const manualLink = screen.getByRole("link", { name: /common.step_by_step_manual/i });
expect(manualLink).toHaveAttribute(
"href",
"https://formbricks.com/docs/app-surveys/framework-guides#html"
);
});
});

View File

@@ -1,147 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { afterEach, describe, expect, test, vi } from "vitest";
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
import OnboardingLayout from "./layout";
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
IS_PRODUCTION: false,
IS_DEVELOPMENT: true,
E2E_TESTING: false,
WEBAPP_URL: "http://localhost:3000",
PUBLIC_URL: "http://localhost:3000/survey",
ENCRYPTION_KEY: "mock-encryption-key",
CRON_SECRET: "mock-cron-secret",
DEFAULT_BRAND_COLOR: "#64748b",
FB_LOGO_URL: "https://mock-logo-url.com/logo.png",
PRIVACY_URL: "http://localhost:3000/privacy",
TERMS_URL: "http://localhost:3000/terms",
IMPRINT_URL: "http://localhost:3000/imprint",
IMPRINT_ADDRESS: "Mock Address",
PASSWORD_RESET_DISABLED: false,
EMAIL_VERIFICATION_DISABLED: false,
GOOGLE_OAUTH_ENABLED: false,
GITHUB_OAUTH_ENABLED: false,
AZURE_OAUTH_ENABLED: false,
OIDC_OAUTH_ENABLED: false,
SAML_OAUTH_ENABLED: false,
SAML_XML_DIR: "./mock-saml-connection",
SIGNUP_ENABLED: true,
EMAIL_AUTH_ENABLED: true,
INVITE_DISABLED: false,
SLACK_CLIENT_SECRET: "mock-slack-secret",
SLACK_CLIENT_ID: "mock-slack-id",
SLACK_AUTH_URL: "https://mock-slack-auth-url.com",
GOOGLE_SHEETS_CLIENT_ID: "mock-google-sheets-id",
GOOGLE_SHEETS_CLIENT_SECRET: "mock-google-sheets-secret",
GOOGLE_SHEETS_REDIRECT_URL: "http://localhost:3000/google-sheets-redirect",
NOTION_OAUTH_CLIENT_ID: "mock-notion-id",
NOTION_OAUTH_CLIENT_SECRET: "mock-notion-secret",
NOTION_REDIRECT_URI: "http://localhost:3000/notion-redirect",
NOTION_AUTH_URL: "https://mock-notion-auth-url.com",
AIRTABLE_CLIENT_ID: "mock-airtable-id",
SMTP_HOST: "mock-smtp-host",
SMTP_PORT: "587",
SMTP_SECURE_ENABLED: false,
SMTP_USER: "mock-smtp-user",
SMTP_PASSWORD: "mock-smtp-password",
SMTP_AUTHENTICATED: true,
SMTP_REJECT_UNAUTHORIZED_TLS: true,
MAIL_FROM: "mock@mail.com",
MAIL_FROM_NAME: "Mock Mail",
NEXTAUTH_SECRET: "mock-nextauth-secret",
ITEMS_PER_PAGE: 30,
SURVEYS_PER_PAGE: 12,
RESPONSES_PER_PAGE: 25,
TEXT_RESPONSES_PER_PAGE: 5,
INSIGHTS_PER_PAGE: 10,
DOCUMENTS_PER_PAGE: 10,
MAX_RESPONSES_FOR_INSIGHT_GENERATION: 500,
MAX_OTHER_OPTION_LENGTH: 250,
ENTERPRISE_LICENSE_KEY: "ABC",
GITHUB_ID: "mock-github-id",
GITHUB_SECRET: "mock-github-secret",
GITHUB_OAUTH_URL: "https://mock-github-auth-url.com",
AZURE_ID: "mock-azure-id",
AZUREAD_CLIENT_ID: "mock-azure-client-id",
AZUREAD_CLIENT_SECRET: "mock-azure-client-secret",
GOOGLE_CLIENT_ID: "mock-google-client-id",
GOOGLE_CLIENT_SECRET: "mock-google-client-secret",
GOOGLE_OAUTH_URL: "https://mock-google-auth-url.com",
AZURE_OAUTH_URL: "https://mock-azure-auth-url.com",
OIDC_ID: "mock-oidc-id",
OIDC_OAUTH_URL: "https://mock-oidc-auth-url.com",
SAML_ID: "mock-saml-id",
SAML_OAUTH_URL: "https://mock-saml-auth-url.com",
SAML_METADATA_URL: "https://mock-saml-metadata-url.com",
AZUREAD_TENANT_ID: "mock-azure-tenant-id",
AZUREAD_OAUTH_URL: "https://mock-azuread-auth-url.com",
OIDC_DISPLAY_NAME: "Mock OIDC",
OIDC_CLIENT_ID: "mock-oidc-client-id",
OIDC_CLIENT_SECRET: "mock-oidc-client-secret",
OIDC_REDIRECT_URL: "http://localhost:3000/oidc-redirect",
OIDC_AUTH_URL: "https://mock-oidc-auth-url.com",
OIDC_ISSUER: "https://mock-oidc-issuer.com",
OIDC_SIGNING_ALGORITHM: "RS256",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: true,
}));
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
vi.mock("next-auth", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@/lib/environment/auth", () => ({
hasUserEnvironmentAccess: vi.fn(),
}));
describe("OnboardingLayout", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("redirects to login if session is missing", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce(null);
await OnboardingLayout({
params: { environmentId: "env1" },
children: <div>Test Content</div>,
});
expect(redirect).toHaveBeenCalledWith("/auth/login");
});
test("throws AuthorizationError if user lacks access", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user1" } });
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false);
await expect(
OnboardingLayout({
params: { environmentId: "env1" },
children: <div>Test Content</div>,
})
).rejects.toThrow("User is not authorized to access this environment");
});
test("renders children if user has access", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user1" } });
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
const result = await OnboardingLayout({
params: { environmentId: "env1" },
children: <div data-testid="child">Test Content</div>,
});
render(result);
expect(screen.getByTestId("child")).toHaveTextContent("Test Content");
});
});

View File

@@ -1,76 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import toast from "react-hot-toast";
import { afterEach, describe, expect, test, vi } from "vitest";
import { createSurveyAction } from "@/modules/survey/components/template-list/actions";
import { XMTemplateList } from "./XMTemplateList";
// Prepare push mock and module mocks before importing component
const pushMock = vi.fn();
vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t: (key: string) => key }) }));
vi.mock("next/navigation", () => ({ useRouter: vi.fn(() => ({ push: pushMock })) }));
vi.mock("react-hot-toast", () => ({ default: { error: vi.fn() } }));
vi.mock("@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates", () => ({
getXMTemplates: (t: any) => [
{ id: 1, name: "tmpl1" },
{ id: 2, name: "tmpl2" },
],
}));
vi.mock("@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils", () => ({
replacePresetPlaceholders: (template: any, project: any) => ({ ...template, projectId: project.id }),
}));
vi.mock("@/modules/survey/components/template-list/actions", () => ({ createSurveyAction: vi.fn() }));
vi.mock("@/lib/utils/helper", () => ({ getFormattedErrorMessage: () => "formatted-error" }));
vi.mock("@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer", () => ({
OnboardingOptionsContainer: ({ options }: { options: any[] }) => (
<div>
{options.map((opt, idx) => (
<button key={idx} data-testid={`option-${idx}`} onClick={opt.onClick}>
{opt.title}
</button>
))}
</div>
),
}));
// Reset mocks between tests
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe("XMTemplateList component", () => {
const project = { id: "proj1" } as any;
const user = { id: "user1" } as any;
const environmentId = "env1";
test("creates survey and navigates on success", async () => {
// Mock successful survey creation
vi.mocked(createSurveyAction).mockResolvedValue({ data: { id: "survey1" } } as any);
render(<XMTemplateList project={project} user={user} environmentId={environmentId} />);
const option0 = screen.getByTestId("option-0");
await userEvent.click(option0);
expect(createSurveyAction).toHaveBeenCalledWith({
environmentId,
surveyBody: expect.objectContaining({ id: 1, projectId: "proj1", type: "link", createdBy: "user1" }),
});
expect(pushMock).toHaveBeenCalledWith(`/environments/${environmentId}/surveys/survey1/edit?mode=cx`);
});
test("shows error toast on failure", async () => {
// Mock failed survey creation
vi.mocked(createSurveyAction).mockResolvedValue({ error: "err" } as any);
render(<XMTemplateList project={project} user={user} environmentId={environmentId} />);
const option1 = screen.getByTestId("option-1");
await userEvent.click(option1);
expect(createSurveyAction).toHaveBeenCalled();
expect(toast.error).toHaveBeenCalledWith("formatted-error");
});
});

View File

@@ -1,82 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { LandingSidebar } from "./landing-sidebar";
// Mock constants that this test needs
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
WEBAPP_URL: "http://localhost:3000",
}));
// Mock server actions that this test needs
vi.mock("@/modules/auth/actions/sign-out", () => ({
logSignOutAction: vi.fn().mockResolvedValue(undefined),
}));
// Module mocks must be declared before importing the component
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({ t: (key: string) => key, isLoading: false }),
}));
// Mock our useSignOut hook
const mockSignOut = vi.fn();
vi.mock("@/modules/auth/hooks/use-sign-out", () => ({
useSignOut: () => ({
signOut: mockSignOut,
}),
}));
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
vi.mock("@/modules/organization/components/CreateOrganizationModal", () => ({
CreateOrganizationModal: ({ open }: { open: boolean }) => (
<div data-testid={open ? "modal-open" : "modal-closed"} />
),
}));
vi.mock("@/modules/ui/components/avatars", () => ({
ProfileAvatar: ({ userId }: { userId: string }) => <div data-testid="avatar">{userId}</div>,
}));
// Ensure mocks are reset between tests
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe("LandingSidebar component", () => {
const user = { id: "u1", name: "Alice", email: "alice@example.com" } as any;
const organization = { id: "o1", name: "orgOne" } as any;
test("renders logo, avatar, and initial modal closed", () => {
render(<LandingSidebar user={user} organization={organization} />);
// Formbricks logo
expect(screen.getByAltText("environments.formbricks_logo")).toBeInTheDocument();
// Profile avatar
expect(screen.getByTestId("avatar")).toHaveTextContent("u1");
// CreateOrganizationModal should be closed initially
expect(screen.getByTestId("modal-closed")).toBeInTheDocument();
});
test("clicking logout triggers signOut", async () => {
render(<LandingSidebar user={user} organization={organization} />);
// Open user dropdown by clicking on avatar trigger
const trigger = screen.getByTestId("avatar").parentElement;
if (trigger) await userEvent.click(trigger);
// Click logout menu item
const logoutItem = await screen.findByText("common.logout");
await userEvent.click(logoutItem);
expect(mockSignOut).toHaveBeenCalledWith({
reason: "user_initiated",
redirectUrl: "/auth/login",
organizationId: "o1",
redirect: true,
callbackUrl: "/auth/login",
clearEnvironmentId: true,
});
});
});

View File

@@ -1,187 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/preact";
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { afterEach, describe, expect, test, vi } from "vitest";
import { getEnvironments } from "@/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getUserProjects } from "@/lib/project/service";
import LandingLayout from "./layout";
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
IS_PRODUCTION: false,
IS_DEVELOPMENT: true,
E2E_TESTING: false,
WEBAPP_URL: "http://localhost:3000",
PUBLIC_URL: "http://localhost:3000/survey",
ENCRYPTION_KEY: "mock-encryption-key",
CRON_SECRET: "mock-cron-secret",
DEFAULT_BRAND_COLOR: "#64748b",
FB_LOGO_URL: "https://mock-logo-url.com/logo.png",
PRIVACY_URL: "http://localhost:3000/privacy",
TERMS_URL: "http://localhost:3000/terms",
IMPRINT_URL: "http://localhost:3000/imprint",
IMPRINT_ADDRESS: "Mock Address",
PASSWORD_RESET_DISABLED: false,
EMAIL_VERIFICATION_DISABLED: false,
GOOGLE_OAUTH_ENABLED: false,
GITHUB_OAUTH_ENABLED: false,
AZURE_OAUTH_ENABLED: false,
OIDC_OAUTH_ENABLED: false,
SAML_OAUTH_ENABLED: false,
SAML_XML_DIR: "./mock-saml-connection",
SIGNUP_ENABLED: true,
EMAIL_AUTH_ENABLED: true,
INVITE_DISABLED: false,
SLACK_CLIENT_SECRET: "mock-slack-secret",
SLACK_CLIENT_ID: "mock-slack-id",
SLACK_AUTH_URL: "https://mock-slack-auth-url.com",
GOOGLE_SHEETS_CLIENT_ID: "mock-google-sheets-id",
GOOGLE_SHEETS_CLIENT_SECRET: "mock-google-sheets-secret",
GOOGLE_SHEETS_REDIRECT_URL: "http://localhost:3000/google-sheets-redirect",
NOTION_OAUTH_CLIENT_ID: "mock-notion-id",
NOTION_OAUTH_CLIENT_SECRET: "mock-notion-secret",
NOTION_REDIRECT_URI: "http://localhost:3000/notion-redirect",
NOTION_AUTH_URL: "https://mock-notion-auth-url.com",
AIRTABLE_CLIENT_ID: "mock-airtable-id",
SMTP_HOST: "mock-smtp-host",
SMTP_PORT: "587",
SMTP_SECURE_ENABLED: false,
SMTP_USER: "mock-smtp-user",
SMTP_PASSWORD: "mock-smtp-password",
SMTP_AUTHENTICATED: true,
SMTP_REJECT_UNAUTHORIZED_TLS: true,
MAIL_FROM: "mock@mail.com",
MAIL_FROM_NAME: "Mock Mail",
NEXTAUTH_SECRET: "mock-nextauth-secret",
ITEMS_PER_PAGE: 30,
SURVEYS_PER_PAGE: 12,
RESPONSES_PER_PAGE: 25,
TEXT_RESPONSES_PER_PAGE: 5,
INSIGHTS_PER_PAGE: 10,
DOCUMENTS_PER_PAGE: 10,
MAX_RESPONSES_FOR_INSIGHT_GENERATION: 500,
MAX_OTHER_OPTION_LENGTH: 250,
ENTERPRISE_LICENSE_KEY: "ABC",
GITHUB_ID: "mock-github-id",
GITHUB_SECRET: "mock-github-secret",
GITHUB_OAUTH_URL: "https://mock-github-auth-url.com",
AZURE_ID: "mock-azure-id",
AZUREAD_CLIENT_ID: "mock-azure-client-id",
AZUREAD_CLIENT_SECRET: "mock-azure-client-secret",
GOOGLE_CLIENT_ID: "mock-google-client-id",
GOOGLE_CLIENT_SECRET: "mock-google-client-secret",
GOOGLE_OAUTH_URL: "https://mock-google-auth-url.com",
AZURE_OAUTH_URL: "https://mock-azure-auth-url.com",
OIDC_ID: "mock-oidc-id",
OIDC_OAUTH_URL: "https://mock-oidc-auth-url.com",
SAML_ID: "mock-saml-id",
SAML_OAUTH_URL: "https://mock-saml-auth-url.com",
SAML_METADATA_URL: "https://mock-saml-metadata-url.com",
AZUREAD_TENANT_ID: "mock-azure-tenant-id",
AZUREAD_OAUTH_URL: "https://mock-azuread-auth-url.com",
OIDC_DISPLAY_NAME: "Mock OIDC",
OIDC_CLIENT_ID: "mock-oidc-client-id",
OIDC_CLIENT_SECRET: "mock-oidc-client-secret",
OIDC_REDIRECT_URL: "http://localhost:3000/oidc-redirect",
OIDC_AUTH_URL: "https://mock-oidc-auth-url.com",
OIDC_ISSUER: "https://mock-oidc-issuer.com",
OIDC_SIGNING_ALGORITHM: "RS256",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: true,
}));
vi.mock("@/lib/environment/service");
vi.mock("@/lib/membership/service");
vi.mock("@/lib/project/service");
vi.mock("next-auth");
vi.mock("next/navigation");
afterEach(() => {
cleanup();
});
describe("LandingLayout", () => {
test("redirects to login if no session exists", async () => {
vi.mocked(getServerSession).mockResolvedValue(null);
const props = { params: { organizationId: "org-123" }, children: <div>Child Content</div> };
await LandingLayout(props);
expect(vi.mocked(redirect)).toHaveBeenCalledWith("/auth/login");
});
test("returns notFound if no membership is found", async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-123" } });
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null);
const props = { params: { organizationId: "org-123" }, children: <div>Child Content</div> };
await LandingLayout(props);
expect(vi.mocked(notFound)).toHaveBeenCalled();
});
test("redirects to production environment if available", async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-123" } });
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({
organizationId: "org-123",
userId: "user-123",
accepted: true,
role: "owner",
});
vi.mocked(getUserProjects).mockResolvedValue([
{
id: "proj-123",
organizationId: "org-123",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-02"),
name: "Project 1",
styling: { allowStyleOverwrite: true },
recontactDays: 30,
inAppSurveyBranding: true,
linkSurveyBranding: true,
} as any,
]);
vi.mocked(getEnvironments).mockResolvedValue([
{
id: "env-123",
type: "production",
projectId: "proj-123",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-02"),
appSetupCompleted: true,
},
]);
const props = { params: { organizationId: "org-123" }, children: <div>Child Content</div> };
await LandingLayout(props);
expect(vi.mocked(redirect)).toHaveBeenCalledWith("/environments/env-123/");
});
test("renders children if no projects or production environment exist", async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-123" } });
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({
organizationId: "org-123",
userId: "user-123",
accepted: true,
role: "owner",
});
vi.mocked(getUserProjects).mockResolvedValue([]);
const props = { params: { organizationId: "org-123" }, children: <div>Child Content</div> };
const result = await LandingLayout(props);
expect(result).toEqual(
<>
<div>Child Content</div>
</>
);
});
});

View File

@@ -1,227 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { notFound, redirect } from "next/navigation";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getOrganizationsByUserId } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { getTranslate } from "@/tolgee/server";
vi.mock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: true,
features: { isMultiOrgEnabled: true },
lastChecked: new Date(),
isPendingDowngrade: false,
fallbackLevel: "live",
}),
getLicenseFeatures: vi.fn().mockResolvedValue({ isMultiOrgEnabled: true }),
}));
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
IS_PRODUCTION: false,
IS_DEVELOPMENT: true,
E2E_TESTING: false,
WEBAPP_URL: "http://localhost:3000",
ENCRYPTION_KEY: "mock-encryption-key",
CRON_SECRET: "mock-cron-secret",
DEFAULT_BRAND_COLOR: "#64748b",
FB_LOGO_URL: "https://mock-logo-url.com/logo.png",
PRIVACY_URL: "http://localhost:3000/privacy",
TERMS_URL: "http://localhost:3000/terms",
IMPRINT_URL: "http://localhost:3000/imprint",
IMPRINT_ADDRESS: "Mock Address",
PASSWORD_RESET_DISABLED: false,
EMAIL_VERIFICATION_DISABLED: false,
GOOGLE_OAUTH_ENABLED: false,
GITHUB_OAUTH_ENABLED: false,
AZURE_OAUTH_ENABLED: false,
OIDC_OAUTH_ENABLED: false,
SAML_OAUTH_ENABLED: false,
SAML_XML_DIR: "./mock-saml-connection",
SIGNUP_ENABLED: true,
EMAIL_AUTH_ENABLED: true,
INVITE_DISABLED: false,
SLACK_CLIENT_SECRET: "mock-slack-secret",
SLACK_CLIENT_ID: "mock-slack-id",
SLACK_AUTH_URL: "https://mock-slack-auth-url.com",
GOOGLE_SHEETS_CLIENT_ID: "mock-google-sheets-id",
GOOGLE_SHEETS_CLIENT_SECRET: "mock-google-sheets-secret",
GOOGLE_SHEETS_REDIRECT_URL: "http://localhost:3000/google-sheets-redirect",
NOTION_OAUTH_CLIENT_ID: "mock-notion-id",
NOTION_OAUTH_CLIENT_SECRET: "mock-notion-secret",
NOTION_REDIRECT_URI: "http://localhost:3000/notion-redirect",
NOTION_AUTH_URL: "https://mock-notion-auth-url.com",
AIRTABLE_CLIENT_ID: "mock-airtable-id",
SMTP_HOST: "mock-smtp-host",
SMTP_PORT: "587",
SMTP_SECURE_ENABLED: false,
SMTP_USER: "mock-smtp-user",
SMTP_PASSWORD: "mock-smtp-password",
SMTP_AUTHENTICATED: true,
SMTP_REJECT_UNAUTHORIZED_TLS: true,
MAIL_FROM: "mock@mail.com",
MAIL_FROM_NAME: "Mock Mail",
NEXTAUTH_SECRET: "mock-nextauth-secret",
ITEMS_PER_PAGE: 30,
SURVEYS_PER_PAGE: 12,
RESPONSES_PER_PAGE: 25,
TEXT_RESPONSES_PER_PAGE: 5,
INSIGHTS_PER_PAGE: 10,
DOCUMENTS_PER_PAGE: 10,
MAX_RESPONSES_FOR_INSIGHT_GENERATION: 500,
MAX_OTHER_OPTION_LENGTH: 250,
ENTERPRISE_LICENSE_KEY: "ABC",
GITHUB_ID: "mock-github-id",
GITHUB_SECRET: "mock-github-secret",
GITHUB_OAUTH_URL: "https://mock-github-auth-url.com",
AZURE_ID: "mock-azure-id",
AZUREAD_CLIENT_ID: "mock-azure-client-id",
AZUREAD_CLIENT_SECRET: "mock-azure-client-secret",
GOOGLE_CLIENT_ID: "mock-google-client-id",
GOOGLE_CLIENT_SECRET: "mock-google-client-secret",
GOOGLE_OAUTH_URL: "https://mock-google-auth-url.com",
AZURE_OAUTH_URL: "https://mock-azure-auth-url.com",
OIDC_ID: "mock-oidc-id",
OIDC_OAUTH_URL: "https://mock-oidc-auth-url.com",
SAML_ID: "mock-saml-id",
SAML_OAUTH_URL: "https://mock-saml-auth-url.com",
SAML_METADATA_URL: "https://mock-saml-metadata-url.com",
AZUREAD_TENANT_ID: "mock-azure-tenant-id",
AZUREAD_OAUTH_URL: "https://mock-azuread-auth-url.com",
OIDC_DISPLAY_NAME: "Mock OIDC",
OIDC_CLIENT_ID: "mock-oidc-client-id",
OIDC_CLIENT_SECRET: "mock-oidc-client-secret",
OIDC_REDIRECT_URL: "http://localhost:3000/oidc-redirect",
OIDC_AUTH_URL: "https://mock-oidc-auth-url.com",
OIDC_ISSUER: "https://mock-oidc-issuer.com",
OIDC_SIGNING_ALGORITHM: "RS256",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: true,
}));
vi.mock("@/lib/getPublicUrl", () => ({
getPublicDomain: vi.fn().mockReturnValue("http://localhost:3000"),
}));
vi.mock("@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar", () => ({
LandingSidebar: () => <div data-testid="landing-sidebar" />,
}));
vi.mock("@/app/(app)/environments/[environmentId]/components/project-and-org-switch", () => ({
ProjectAndOrgSwitch: () => <div data-testid="project-and-org-switch" />,
}));
vi.mock("@/modules/organization/lib/utils");
vi.mock("@/lib/user/service");
vi.mock("@/lib/organization/service");
vi.mock("@/lib/membership/service");
vi.mock("@/tolgee/server");
vi.mock("next/navigation", () => ({
redirect: vi.fn(() => "REDIRECT_STUB"),
notFound: vi.fn(() => "NOT_FOUND_STUB"),
usePathname: vi.fn(() => "/organizations/org1"),
useRouter: vi.fn(() => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
refresh: vi.fn(),
})),
}));
// Mock the React cache function
vi.mock("react", async () => {
const actual = await vi.importActual("react");
return {
...actual,
cache: (fn: any) => fn,
};
});
describe("Page component", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
beforeEach(() => {
vi.resetModules();
});
test("redirects to login if no user session", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValue({ session: {}, organization: {} } as any);
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: true,
features: { isMultiOrgEnabled: true },
lastChecked: new Date(),
isPendingDowngrade: false,
fallbackLevel: "live",
}),
getLicenseFeatures: vi.fn().mockResolvedValue({ isMultiOrgEnabled: true }),
}));
const { default: Page } = await import("./page");
const result = await Page({ params: { organizationId: "org1" } });
expect(redirect).toHaveBeenCalledWith("/auth/login");
expect(result).toBe("REDIRECT_STUB");
});
test("returns notFound if user does not exist", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValue({
session: { user: { id: "user1" } },
organization: {},
} as any);
vi.mocked(getUser).mockResolvedValue(null);
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: true,
features: { isMultiOrgEnabled: true },
lastChecked: new Date(),
isPendingDowngrade: false,
fallbackLevel: "live",
}),
getLicenseFeatures: vi.fn().mockResolvedValue({ isMultiOrgEnabled: true }),
}));
const { default: Page } = await import("./page");
const result = await Page({ params: { organizationId: "org1" } });
expect(notFound).toHaveBeenCalled();
expect(result).toBe("NOT_FOUND_STUB");
});
test("renders header and sidebar for authenticated user", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValue({
session: { user: { id: "user1" } },
organization: { id: "org1", billing: { plan: "free" } },
} as any);
vi.mocked(getUser).mockResolvedValue({ id: "user1", name: "Test User" } as any);
vi.mocked(getOrganizationsByUserId).mockResolvedValue([{ id: "org1", name: "Org One" } as any]);
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({
organizationId: "org1",
userId: "user1",
accepted: true,
role: "member",
} as any);
vi.mocked(getTranslate).mockResolvedValue((props: any) =>
typeof props === "string" ? props : props.key || ""
);
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: true,
features: { isMultiOrgEnabled: true },
lastChecked: new Date(),
isPendingDowngrade: false,
fallbackLevel: "live",
}),
getLicenseFeatures: vi.fn().mockResolvedValue({ isMultiOrgEnabled: true }),
}));
const { default: Page } = await import("./page");
const element = await Page({ params: { organizationId: "org1" } });
render(element as React.ReactElement);
expect(screen.getByTestId("landing-sidebar")).toBeInTheDocument();
expect(screen.getByTestId("project-and-org-switch")).toBeInTheDocument();
expect(screen.getByText("organizations.landing.no_projects_warning_title")).toBeInTheDocument();
expect(screen.getByText("organizations.landing.no_projects_warning_subtitle")).toBeInTheDocument();
});
});

View File

@@ -1,159 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { act, cleanup, render, screen } from "@testing-library/react";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import React from "react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
import { canUserAccessOrganization } from "@/lib/organization/auth";
import { getOrganization } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import ProjectOnboardingLayout from "./layout";
// Mock all the modules and functions that this layout uses:
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,
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: true,
}));
vi.mock("next-auth", () => ({
getServerSession: vi.fn(),
}));
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
vi.mock("@/lib/organization/auth", () => ({
canUserAccessOrganization: vi.fn(),
}));
vi.mock("@/lib/organization/service", () => ({
getOrganization: vi.fn(),
}));
vi.mock("@/lib/user/service", () => ({
getUser: vi.fn(),
}));
vi.mock("@/tolgee/server", () => ({
getTranslate: vi.fn(() => {
// Return a mock translator that just returns the key
return (key: string) => key;
}),
}));
// mock the child components
vi.mock("@/app/(app)/environments/[environmentId]/components/PosthogIdentify", () => ({
PosthogIdentify: () => <div data-testid="posthog-identify" />,
}));
vi.mock("@/modules/ui/components/toaster-client", () => ({
ToasterClient: () => <div data-testid="toaster-client" />,
}));
describe("ProjectOnboardingLayout", () => {
beforeEach(() => {
cleanup();
});
test("redirects to /auth/login if there is no session", async () => {
// Mock no session
vi.mocked(getServerSession).mockResolvedValueOnce(null);
const layoutElement = await ProjectOnboardingLayout({
params: { organizationId: "org-123" },
children: <div data-testid="child-content">Hello!</div>,
});
expect(redirect).toHaveBeenCalledWith("/auth/login");
// Layout returns nothing after redirect
expect(layoutElement).toBeUndefined();
});
test("throws an error if user does not exist", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({
user: { id: "user-123" },
});
vi.mocked(getUser).mockResolvedValueOnce(null); // no user in DB
await expect(
ProjectOnboardingLayout({
params: { organizationId: "org-123" },
children: <div data-testid="child-content">Hello!</div>,
})
).rejects.toThrow("common.user_not_found");
});
test("throws AuthorizationError if user cannot access organization", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false);
await expect(
ProjectOnboardingLayout({
params: { organizationId: "org-123" },
children: <div data-testid="child-content">Child</div>,
})
).rejects.toThrow("common.not_authorized");
});
test("throws an error if organization does not exist", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
vi.mocked(getOrganization).mockResolvedValueOnce(null);
await expect(
ProjectOnboardingLayout({
params: { organizationId: "org-123" },
children: <div data-testid="child-content">Hello!</div>,
})
).rejects.toThrow("common.organization_not_found");
});
test("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
// Provide valid data
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser);
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
vi.mocked(getOrganization).mockResolvedValueOnce({
id: "org-123",
name: "Test Org",
billing: {
plan: "enterprise",
},
} as TOrganization);
let layoutElement: React.ReactNode;
// Because it's an async server component, do it in an act
await act(async () => {
layoutElement = await ProjectOnboardingLayout({
params: { organizationId: "org-123" },
children: <div data-testid="child-content">Hello!</div>,
});
render(layoutElement);
});
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello!");
expect(screen.getByTestId("posthog-identify")).toBeInTheDocument();
expect(screen.getByTestId("toaster-client")).toBeInTheDocument();
});
});

View File

@@ -1,88 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { redirect } from "next/navigation";
import { afterEach, describe, expect, test, vi } from "vitest";
import { getUserProjects } from "@/lib/project/service";
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { getTranslate } from "@/tolgee/server";
import Page from "./page";
const mockTranslate = vi.fn((key) => key);
// Module mocks must be declared before importing the component
vi.mock("@/lib/project/service", () => ({ getUserProjects: vi.fn() }));
vi.mock("@/modules/organization/lib/utils", () => ({ getOrganizationAuth: vi.fn() }));
vi.mock("@/tolgee/server", () => ({ getTranslate: vi.fn() }));
vi.mock("next/navigation", () => ({ redirect: vi.fn(() => "REDIRECT_STUB") }));
vi.mock("@/modules/ui/components/header", () => ({
Header: ({ title, subtitle }: { title: string; subtitle: string }) => (
<div>
<h1>{title}</h1>
<p>{subtitle}</p>
</div>
),
}));
vi.mock("@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer", () => ({
OnboardingOptionsContainer: ({ options }: { options: any[] }) => (
<div data-testid="options">{options.map((o) => o.title).join(",")}</div>
),
}));
vi.mock("next/link", () => ({
default: ({ href, children }: { href: string; children: React.ReactNode }) => <a href={href}>{children}</a>,
}));
describe("Page component", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
const params = Promise.resolve({ organizationId: "org1" });
test("redirects to login if no user session", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValue({ session: {} } as any);
const result = await Page({ params });
expect(redirect).toHaveBeenCalledWith("/auth/login");
expect(result).toBe("REDIRECT_STUB");
});
test("renders header, options, and close button when projects exist", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValue({ session: { user: { id: "user1" } } } as any);
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
vi.mocked(getUserProjects).mockResolvedValue([{ id: 1 }] as any);
const element = await Page({ params });
render(element as React.ReactElement);
// Header title and subtitle
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
"organizations.projects.new.channel.channel_select_title"
);
expect(
screen.getByText("organizations.projects.new.channel.channel_select_subtitle")
).toBeInTheDocument();
// Options container with correct titles
expect(screen.getByTestId("options")).toHaveTextContent(
"organizations.projects.new.channel.link_and_email_surveys," +
"organizations.projects.new.channel.in_product_surveys"
);
// Close button link rendered when projects >=1
const closeLink = screen.getByRole("link");
expect(closeLink).toHaveAttribute("href", "/");
});
test("does not render close button when no projects", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValue({ session: { user: { id: "user1" } } } as any);
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
vi.mocked(getUserProjects).mockResolvedValue([]);
const element = await Page({ params });
render(element as React.ReactElement);
expect(screen.queryByRole("link")).toBeNull();
});
});

View File

@@ -1,223 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TMembership } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getOrganization } from "@/lib/organization/service";
import { getOrganizationProjectsCount } from "@/lib/project/service";
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
import OnboardingLayout from "./layout";
// Mock environment variables
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,
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: true,
}));
// Mock dependencies
vi.mock("next-auth", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@/lib/membership/service", () => ({
getMembershipByUserIdOrganizationId: vi.fn(),
}));
vi.mock("@/lib/organization/service", () => ({
getOrganization: vi.fn(),
}));
vi.mock("@/lib/project/service", () => ({
getOrganizationProjectsCount: vi.fn(),
}));
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
getOrganizationProjectsLimit: vi.fn(),
}));
vi.mock("@/tolgee/server", () => ({
getTranslate: async () => (key: string) => key,
}));
describe("OnboardingLayout", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("redirects to login if no session", async () => {
vi.mocked(getServerSession).mockResolvedValue(null);
const props = {
params: { organizationId: "test-org-id" },
children: <div>Test Child</div>,
};
await OnboardingLayout(props);
expect(redirect).toHaveBeenCalledWith("/auth/login");
});
test("returns not found if user is member or billing", async () => {
const mockSession = {
user: { id: "test-user-id" },
};
vi.mocked(getServerSession).mockResolvedValue(mockSession as any);
const mockMembership: TMembership = {
organizationId: "test-org-id",
userId: "test-user-id",
accepted: true,
role: "member",
};
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
const props = {
params: { organizationId: "test-org-id" },
children: <div>Test Child</div>,
};
await OnboardingLayout(props);
expect(notFound).toHaveBeenCalled();
});
test("throws error if organization is not found", async () => {
const mockSession = {
user: { id: "test-user-id" },
};
vi.mocked(getServerSession).mockResolvedValue(mockSession as any);
const mockMembership: TMembership = {
organizationId: "test-org-id",
userId: "test-user-id",
accepted: true,
role: "owner",
};
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
vi.mocked(getOrganization).mockResolvedValue(null);
const props = {
params: { organizationId: "test-org-id" },
children: <div>Test Child</div>,
};
await expect(OnboardingLayout(props)).rejects.toThrow("common.organization_not_found");
});
test("redirects to home if project limit is reached", async () => {
const mockSession = {
user: { id: "test-user-id" },
};
vi.mocked(getServerSession).mockResolvedValue(mockSession as any);
const mockMembership: TMembership = {
organizationId: "test-org-id",
userId: "test-user-id",
accepted: true,
role: "owner",
};
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
const mockOrganization: TOrganization = {
id: "test-org-id",
name: "Test Org",
createdAt: new Date(),
updatedAt: new Date(),
isAIEnabled: false,
billing: {
stripeCustomerId: null,
plan: "free",
period: "monthly",
limits: {
projects: 3,
monthly: {
responses: 1500,
miu: 2000,
},
},
periodStart: new Date(),
},
};
vi.mocked(getOrganization).mockResolvedValue(mockOrganization);
vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(3);
vi.mocked(getOrganizationProjectsCount).mockResolvedValue(3);
const props = {
params: { organizationId: "test-org-id" },
children: <div>Test Child</div>,
};
await OnboardingLayout(props);
expect(redirect).toHaveBeenCalledWith("/");
});
test("renders children when all conditions are met", async () => {
const mockSession = {
user: { id: "test-user-id" },
};
vi.mocked(getServerSession).mockResolvedValue(mockSession as any);
const mockMembership: TMembership = {
organizationId: "test-org-id",
userId: "test-user-id",
accepted: true,
role: "owner",
};
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
const mockOrganization: TOrganization = {
id: "test-org-id",
name: "Test Org",
createdAt: new Date(),
updatedAt: new Date(),
isAIEnabled: false,
billing: {
stripeCustomerId: null,
plan: "free",
period: "monthly",
limits: {
projects: 3,
monthly: {
responses: 1500,
miu: 2000,
},
},
periodStart: new Date(),
},
};
vi.mocked(getOrganization).mockResolvedValue(mockOrganization);
vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(3);
vi.mocked(getOrganizationProjectsCount).mockResolvedValue(2);
const props = {
params: { organizationId: "test-org-id" },
children: <div>Test Child</div>,
};
const result = await OnboardingLayout(props);
expect(result).toEqual(<>{props.children}</>);
});
});

View File

@@ -1,72 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { redirect } from "next/navigation";
import { afterEach, describe, expect, test, vi } from "vitest";
import { getUserProjects } from "@/lib/project/service";
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { getTranslate } from "@/tolgee/server";
import Page from "./page";
const mockTranslate = vi.fn((key) => key);
vi.mock("@/modules/organization/lib/utils", () => ({ getOrganizationAuth: vi.fn() }));
vi.mock("@/lib/project/service", () => ({ getUserProjects: vi.fn() }));
vi.mock("@/tolgee/server", () => ({ getTranslate: vi.fn() }));
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
vi.mock("next/link", () => ({
__esModule: true,
default: ({ href, children }: any) => <a href={href}>{children}</a>,
}));
vi.mock("@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer", () => ({
OnboardingOptionsContainer: ({ options }: any) => (
<div data-testid="options">{options.map((o: any) => o.title).join(",")}</div>
),
}));
vi.mock("@/modules/ui/components/header", () => ({ Header: ({ title }: any) => <h1>{title}</h1> }));
vi.mock("@/modules/ui/components/button", () => ({
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
}));
describe("Mode Page", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
const params = Promise.resolve({ organizationId: "org1" });
test("redirects to login if no session user", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ session: {} } as any);
await Page({ params });
expect(redirect).toHaveBeenCalledWith("/auth/login");
});
test("renders header and options without close link when no projects", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ session: { user: { id: "u1" } } } as any);
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
vi.mocked(getUserProjects).mockResolvedValueOnce([] as any);
const element = await Page({ params });
render(element as React.ReactElement);
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
"organizations.projects.new.mode.what_are_you_here_for"
);
expect(screen.getByTestId("options")).toHaveTextContent(
"organizations.projects.new.mode.formbricks_surveys," + "organizations.projects.new.mode.formbricks_cx"
);
expect(screen.queryByRole("link")).toBeNull();
});
test("renders close link when projects exist", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ session: { user: { id: "u1" } } } as any);
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
vi.mocked(getUserProjects).mockResolvedValueOnce([{ id: "p1" } as any]);
const element = await Page({ params });
render(element as React.ReactElement);
const link = screen.getByRole("link");
expect(link).toHaveAttribute("href", "/");
});
});

View File

@@ -1,124 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { toast } from "react-hot-toast";
import { afterEach, describe, expect, test, vi } from "vitest";
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
import { ProjectSettings } from "./ProjectSettings";
// Mocks before imports
const pushMock = vi.fn();
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: pushMock }) }));
vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t: (key: string) => key }) }));
vi.mock("react-hot-toast", () => ({ toast: { error: vi.fn() } }));
vi.mock("@/app/(app)/environments/[environmentId]/actions", () => ({ createProjectAction: vi.fn() }));
vi.mock("@/lib/utils/helper", () => ({ getFormattedErrorMessage: () => "formatted-error" }));
vi.mock("@/modules/ui/components/color-picker", () => ({
ColorPicker: ({ color, onChange }: any) => (
<button data-testid="color-picker" onClick={() => onChange("#000")}>
{color}
</button>
),
}));
vi.mock("@/modules/ui/components/input", () => ({
Input: ({ value, onChange, placeholder }: any) => (
<input placeholder={placeholder} value={value} onChange={(e) => onChange((e.target as any).value)} />
),
}));
vi.mock("@/modules/ui/components/multi-select", () => ({
MultiSelect: ({ value, options, onChange }: any) => (
<select
data-testid="multi-select"
multiple
value={value}
onChange={(e) => onChange(Array.from((e.target as any).selectedOptions).map((o: any) => o.value))}>
{options.map((o: any) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
),
}));
vi.mock("@/modules/ui/components/survey", () => ({
SurveyInline: () => <div data-testid="survey-inline" />,
}));
vi.mock("@/lib/templates", () => ({ previewSurvey: () => ({}) }));
vi.mock("@/modules/ee/teams/team-list/components/create-team-modal", () => ({
CreateTeamModal: ({ open }: any) => <div data-testid={open ? "team-modal-open" : "team-modal-closed"} />,
}));
// Clean up after each test
afterEach(() => {
cleanup();
vi.clearAllMocks();
localStorage.clear();
});
describe("ProjectSettings component", () => {
const baseProps = {
organizationId: "org1",
projectMode: "cx",
industry: "ind",
defaultBrandColor: "#fff",
organizationTeams: [],
isAccessControlAllowed: false,
userProjectsCount: 0,
} as any;
const fillAndSubmit = async () => {
const nameInput = screen.getByPlaceholderText("e.g. Formbricks");
await userEvent.clear(nameInput);
await userEvent.type(nameInput, "TestProject");
const nextButton = screen.getByRole("button", { name: "common.next" });
await userEvent.click(nextButton);
};
test("successful createProject for link channel navigates to surveys and clears localStorage", async () => {
(createProjectAction as any).mockResolvedValue({
data: { environments: [{ id: "env123", type: "production" }] },
});
render(<ProjectSettings {...baseProps} channel="link" projectMode="cx" />);
await fillAndSubmit();
expect(createProjectAction).toHaveBeenCalledWith({
organizationId: "org1",
data: expect.objectContaining({ teamIds: [] }),
});
expect(pushMock).toHaveBeenCalledWith("/environments/env123/surveys");
expect(localStorage.getItem("FORMBRICKS_SURVEYS_FILTERS_KEY_LS")).toBeNull();
});
test("successful createProject for app channel navigates to connect", async () => {
(createProjectAction as any).mockResolvedValue({
data: { environments: [{ id: "env456", type: "production" }] },
});
render(<ProjectSettings {...baseProps} channel="app" projectMode="cx" />);
await fillAndSubmit();
expect(pushMock).toHaveBeenCalledWith("/environments/env456/connect");
});
test("successful createProject for cx mode navigates to xm-templates when channel is neither link nor app", async () => {
(createProjectAction as any).mockResolvedValue({
data: { environments: [{ id: "env789", type: "production" }] },
});
render(<ProjectSettings {...baseProps} channel="unknown" projectMode="cx" />);
await fillAndSubmit();
expect(pushMock).toHaveBeenCalledWith("/environments/env789/xm-templates");
});
test("shows error toast on createProject error response", async () => {
(createProjectAction as any).mockResolvedValue({ error: "err" });
render(<ProjectSettings {...baseProps} channel="link" projectMode="cx" />);
await fillAndSubmit();
expect(toast.error).toHaveBeenCalledWith("formatted-error");
});
test("shows error toast on exception", async () => {
(createProjectAction as any).mockImplementation(() => {
throw new Error("fail");
});
render(<ProjectSettings {...baseProps} channel="link" projectMode="cx" />);
await fillAndSubmit();
expect(toast.error).toHaveBeenCalledWith("organizations.projects.new.settings.project_creation_failed");
});
});

View File

@@ -1,106 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { redirect } from "next/navigation";
import { afterEach, describe, expect, test, vi } from "vitest";
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
import { getUserProjects } from "@/lib/project/service";
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import Page from "./page";
vi.mock("@/lib/constants", () => ({ DEFAULT_BRAND_COLOR: "#fff" }));
// Mocks before component import
vi.mock("@/app/(app)/(onboarding)/lib/onboarding", () => ({ getTeamsByOrganizationId: vi.fn() }));
vi.mock("@/lib/project/service", () => ({ getUserProjects: vi.fn() }));
vi.mock("@/modules/ee/license-check/lib/utils", () => ({ getAccessControlPermission: vi.fn() }));
vi.mock("@/modules/organization/lib/utils", () => ({ getOrganizationAuth: vi.fn() }));
vi.mock("@/tolgee/server", () => ({ getTranslate: () => Promise.resolve((key: string) => key) }));
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
vi.mock("next/link", () => ({
__esModule: true,
default: ({ href, children }: any) => <a href={href}>{children}</a>,
}));
vi.mock("@/modules/ui/components/header", () => ({
Header: ({ title, subtitle }: any) => (
<div>
<h1>{title}</h1>
<p>{subtitle}</p>
</div>
),
}));
vi.mock("@/modules/ui/components/button", () => ({
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
}));
vi.mock(
"@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings",
() => ({
ProjectSettings: (props: any) => <div data-testid="project-settings" data-mode={props.projectMode} />,
})
);
// Cleanup after each test
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe("ProjectSettingsPage", () => {
const params = Promise.resolve({ organizationId: "org1" });
const searchParams = Promise.resolve({ channel: "link", industry: "other", mode: "cx" } as any);
test("redirects to login when no session user", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValueOnce({ session: {} } as any);
await Page({ params, searchParams });
expect(redirect).toHaveBeenCalledWith("/auth/login");
});
test("throws when teams not found", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValueOnce({
session: { user: { id: "u1" } },
organization: { billing: { plan: "basic" } },
} as any);
vi.mocked(getUserProjects).mockResolvedValueOnce([] as any);
vi.mocked(getTeamsByOrganizationId).mockResolvedValueOnce(null as any);
vi.mocked(getAccessControlPermission).mockResolvedValueOnce(false as any);
await expect(Page({ params, searchParams })).rejects.toThrow("common.organization_teams_not_found");
});
test("renders header, settings and close link when projects exist", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValueOnce({
session: { user: { id: "u1" } },
organization: { billing: { plan: "basic" } },
} as any);
vi.mocked(getUserProjects).mockResolvedValueOnce([{ id: "p1" }] as any);
vi.mocked(getTeamsByOrganizationId).mockResolvedValueOnce([{ id: "t1", name: "Team1" }] as any);
vi.mocked(getAccessControlPermission).mockResolvedValueOnce(true as any);
const element = await Page({ params, searchParams });
render(element as React.ReactElement);
// Header
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
"organizations.projects.new.settings.project_settings_title"
);
// ProjectSettings stub receives mode prop
expect(screen.getByTestId("project-settings")).toHaveAttribute("data-mode", "cx");
// Close link for existing projects
const link = screen.getByRole("link");
expect(link).toHaveAttribute("href", "/");
});
test("renders without close link when no projects", async () => {
vi.mocked(getOrganizationAuth).mockResolvedValueOnce({
session: { user: { id: "u1" } },
organization: { billing: { plan: "basic" } },
} as any);
vi.mocked(getUserProjects).mockResolvedValueOnce([] as any);
vi.mocked(getTeamsByOrganizationId).mockResolvedValueOnce([{ id: "t1", name: "Team1" }] as any);
vi.mocked(getAccessControlPermission).mockResolvedValueOnce(true as any);
const element = await Page({ params, searchParams });
render(element as React.ReactElement);
expect(screen.queryByRole("link")).toBeNull();
});
});

View File

@@ -1,106 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Home, Settings } from "lucide-react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { OnboardingOptionsContainer } from "./OnboardingOptionsContainer";
describe("OnboardingOptionsContainer", () => {
afterEach(() => {
cleanup();
});
test("renders options with links", () => {
const options = [
{
title: "Test Option",
description: "Test Description",
icon: Home,
href: "/test",
},
];
render(<OnboardingOptionsContainer options={options} />);
expect(screen.getByText("Test Option")).toBeInTheDocument();
expect(screen.getByText("Test Description")).toBeInTheDocument();
});
test("renders options with onClick handler", () => {
const onClickMock = vi.fn();
const options = [
{
title: "Click Option",
description: "Click Description",
icon: Home,
onClick: onClickMock,
},
];
render(<OnboardingOptionsContainer options={options} />);
expect(screen.getByText("Click Option")).toBeInTheDocument();
expect(screen.getByText("Click Description")).toBeInTheDocument();
});
test("renders options with iconText", () => {
const options = [
{
title: "Icon Text Option",
description: "Icon Text Description",
icon: Home,
iconText: "Custom Icon Text",
},
];
render(<OnboardingOptionsContainer options={options} />);
expect(screen.getByText("Custom Icon Text")).toBeInTheDocument();
});
test("renders options with loading state", () => {
const options = [
{
title: "Loading Option",
description: "Loading Description",
icon: Home,
isLoading: true,
},
];
render(<OnboardingOptionsContainer options={options} />);
expect(screen.getByText("Loading Option")).toBeInTheDocument();
});
test("renders multiple options", () => {
const options = [
{
title: "First Option",
description: "First Description",
icon: Home,
},
{
title: "Second Option",
description: "Second Description",
icon: Settings,
},
];
render(<OnboardingOptionsContainer options={options} />);
expect(screen.getByText("First Option")).toBeInTheDocument();
expect(screen.getByText("Second Option")).toBeInTheDocument();
});
test("calls onClick handler when clicking an option", async () => {
const onClickMock = vi.fn();
const options = [
{
title: "Click Option",
description: "Click Description",
icon: Home,
onClick: onClickMock,
},
];
render(<OnboardingOptionsContainer options={options} />);
await userEvent.click(screen.getByText("Click Option"));
expect(onClickMock).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,114 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import { Session } from "next-auth";
import { redirect } from "next/navigation";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
import { getEnvironment } from "@/lib/environment/service";
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
import SurveyEditorEnvironmentLayout from "./layout";
// Mock sub-components to render identifiable elements
vi.mock("@/modules/ui/components/environmentId-base-layout", () => ({
EnvironmentIdBaseLayout: ({ children, environmentId }: any) => (
<div data-testid="EnvironmentIdBaseLayout">
{environmentId}
{children}
</div>
),
}));
// Mocks for dependencies
vi.mock("@/modules/environments/lib/utils", () => ({
environmentIdLayoutChecks: vi.fn(),
}));
vi.mock("@/lib/environment/service", () => ({
getEnvironment: vi.fn(),
}));
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
describe("SurveyEditorEnvironmentLayout", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders successfully when environment is found", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test
session: { user: { id: "user1" } } as Session,
user: { id: "user1", email: "user1@example.com" } as TUser,
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
});
vi.mocked(getEnvironment).mockResolvedValueOnce({ id: "env1" } as TEnvironment);
const result = await SurveyEditorEnvironmentLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div data-testid="child">Survey Editor Content</div>,
});
render(result);
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveTextContent("env1");
expect(screen.getByTestId("child")).toHaveTextContent("Survey Editor Content");
});
test("throws an error when environment is not found", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: { user: { id: "user1" } } as Session,
user: { id: "user1", email: "user1@example.com" } as TUser,
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
});
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
await expect(
SurveyEditorEnvironmentLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div>Content</div>,
})
).rejects.toThrow("common.environment_not_found");
});
test("calls redirect when session is null", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: undefined as unknown as Session,
user: undefined as unknown as TUser,
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
});
vi.mocked(redirect).mockImplementationOnce(() => {
throw new Error("Redirect called");
});
await expect(
SurveyEditorEnvironmentLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div>Content</div>,
})
).rejects.toThrow("Redirect called");
});
test("throws error if user is null", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: { user: { id: "user1" } } as Session,
user: undefined as unknown as TUser,
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
});
vi.mocked(redirect).mockImplementationOnce(() => {
throw new Error("Redirect called");
});
await expect(
SurveyEditorEnvironmentLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div>Content</div>,
})
).rejects.toThrow("common.user_not_found");
});
});

View File

@@ -1,43 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { SingleContactPage } from "@/modules/ee/contacts/[contactId]/page";
import Page from "./page";
// mock constants
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
ENCRYPTION_KEY: "test",
ENTERPRISE_LICENSE_KEY: "test",
GITHUB_ID: "test",
GITHUB_SECRET: "test",
GOOGLE_CLIENT_ID: "test",
GOOGLE_CLIENT_SECRET: "test",
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",
WEBAPP_URL: "mock-webapp-url",
IS_PRODUCTION: true,
FB_LOGO_URL: "https://example.com/mock-logo.png",
SMTP_HOST: "mock-smtp-host",
SMTP_PORT: "mock-smtp-port",
IS_POSTHOG_CONFIGURED: true,
SESSION_MAX_AGE: 1000,
AUDIT_LOG_ENABLED: 1,
REDIS_URL: undefined,
}));
vi.mock("@/lib/env", () => ({
env: {
PUBLIC_URL: "https://public-domain.com",
},
}));
describe("Contact Page Re-export", () => {
test("should re-export SingleContactPage", () => {
expect(Page).toBe(SingleContactPage);
});
});

View File

@@ -1,15 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { ContactsPage } from "@/modules/ee/contacts/page";
import Page from "./page";
// Mock the actual ContactsPage component
vi.mock("@/modules/ee/contacts/page", () => ({
ContactsPage: () => <div data-testid="contacts-page">Mock Contacts Page</div>,
}));
describe("Contacts Page Re-export", () => {
test("should re-export ContactsPage from the EE module", () => {
// Assert that the default export 'Page' is the same as the mocked 'ContactsPage'
expect(Page).toBe(ContactsPage);
});
});

View File

@@ -1,18 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import SegmentsPageWrapper from "./page";
vi.mock("@/modules/ee/contacts/segments/page", () => ({
SegmentsPage: vi.fn(() => <div>SegmentsPageMock</div>),
}));
describe("SegmentsPageWrapper", () => {
afterEach(() => {
cleanup();
});
test("renders the SegmentsPage component", () => {
render(<SegmentsPageWrapper params={{ environmentId: "test-env" } as any} />);
expect(screen.getByText("SegmentsPageMock")).toBeInTheDocument();
});
});

View File

@@ -1,472 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import type { Session } from "next-auth";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TMembership } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { TProject } from "@formbricks/types/project";
import { TUser } from "@formbricks/types/user";
import { getOrganizationsByUserId } from "@/app/(app)/environments/[environmentId]/lib/organization";
import { getProjectsByUserId } from "@/app/(app)/environments/[environmentId]/lib/project";
import { getEnvironment, getEnvironments } from "@/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import {
getMonthlyActiveOrganizationPeopleCount,
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import {
getAccessControlPermission,
getOrganizationProjectsLimit,
} from "@/modules/ee/license-check/lib/utils";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamsByOrganizationId } from "@/modules/ee/teams/team-list/lib/team";
// Mock services and utils
vi.mock("@/lib/environment/service", () => ({
getEnvironment: vi.fn(),
getEnvironments: vi.fn(),
}));
vi.mock("@/lib/organization/service", () => ({
getOrganizationByEnvironmentId: vi.fn(),
getMonthlyActiveOrganizationPeopleCount: vi.fn(),
getMonthlyOrganizationResponseCount: 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(() => ({ isMember: true })), // Default to member for simplicity
}));
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
getOrganizationProjectsLimit: vi.fn(),
getAccessControlPermission: vi.fn(),
}));
vi.mock("@/modules/ee/teams/lib/roles", () => ({
getProjectPermissionByUserId: vi.fn(),
}));
vi.mock("@/modules/ee/teams/team-list/lib/team", () => ({
getTeamsByOrganizationId: vi.fn(),
}));
vi.mock("@/tolgee/server", () => ({
getTranslate: async () => (key: string) => key,
}));
vi.mock("@/app/(app)/environments/[environmentId]/lib/organization", () => ({
getOrganizationsByUserId: vi.fn(),
}));
vi.mock("@/app/(app)/environments/[environmentId]/lib/project", () => ({
getProjectsByUserId: vi.fn(),
}));
vi.mock("@formbricks/database", () => ({
prisma: {
project: {
findMany: vi.fn(),
},
organization: {
findMany: vi.fn(),
},
},
}));
let mockIsFormbricksCloud = false;
let mockIsDevelopment = false;
vi.mock("@/lib/constants", () => ({
get IS_FORMBRICKS_CLOUD() {
return mockIsFormbricksCloud;
},
get IS_DEVELOPMENT() {
return mockIsDevelopment;
},
}));
// Mock components
vi.mock("@/app/(app)/environments/[environmentId]/components/MainNavigation", () => ({
MainNavigation: ({ organizationTeams, isAccessControlAllowed }: any) => (
<div data-testid="main-navigation">
MainNavigation
<div data-testid="organization-teams">{JSON.stringify(organizationTeams || [])}</div>
<div data-testid="is-access-control-allowed">{isAccessControlAllowed?.toString() || "false"}</div>
</div>
),
}));
vi.mock("@/app/(app)/environments/[environmentId]/components/TopControlBar", () => ({
TopControlBar: () => <div data-testid="top-control-bar">TopControlBar</div>,
}));
vi.mock("@/modules/ui/components/limits-reached-banner", () => ({
LimitsReachedBanner: () => <div data-testid="limits-banner">LimitsReachedBanner</div>,
}));
vi.mock("@/modules/ui/components/pending-downgrade-banner", () => ({
PendingDowngradeBanner: ({
isPendingDowngrade,
active,
}: {
isPendingDowngrade: boolean;
active: boolean;
}) =>
isPendingDowngrade && active ? <div data-testid="downgrade-banner">PendingDowngradeBanner</div> : null,
}));
const mockUser = {
id: "user-1",
name: "Test User",
email: "test@example.com",
emailVerified: new Date(),
twoFactorEnabled: false,
identityProvider: "email",
createdAt: new Date(),
updatedAt: new Date(),
notificationSettings: { alert: {} },
} as unknown as TUser;
const mockOrganization = {
id: "org-1",
name: "Test Org",
billing: {
plan: "free",
limits: {},
},
} as unknown as TOrganization;
const mockEnvironment: TEnvironment = {
id: "env-1",
createdAt: new Date(),
updatedAt: new Date(),
type: "production",
projectId: "proj-1",
appSetupCompleted: true,
};
const mockProject: TProject = {
id: "proj-1",
name: "Test Project",
createdAt: new Date(),
updatedAt: new Date(),
organizationId: "org-1",
environments: [mockEnvironment],
} as unknown as TProject;
const mockMembership: TMembership = {
organizationId: "org-1",
userId: "user-1",
accepted: true,
role: "owner",
};
const mockLicense = {
plan: "free",
active: false,
lastChecked: new Date(),
features: { isMultiOrgEnabled: false },
} as any;
const mockProjectPermission = {
userId: "user-1",
projectId: "proj-1",
role: "admin",
} as any;
const mockOrganizationTeams = [
{
id: "team-1",
name: "Development Team",
},
{
id: "team-2",
name: "Marketing Team",
},
];
const mockSession: Session = {
user: {
id: "user-1",
},
expires: new Date(Date.now() + 3600 * 1000).toISOString(),
};
describe("EnvironmentLayout", () => {
beforeEach(() => {
vi.mocked(getUser).mockResolvedValue(mockUser);
vi.mocked(getEnvironment).mockResolvedValue(mockEnvironment);
vi.mocked(getOrganizationsByUserId).mockResolvedValue([
{ id: mockOrganization.id, name: mockOrganization.name },
]);
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization);
vi.mocked(getProjectsByUserId).mockResolvedValue([{ id: mockProject.id, name: mockProject.name }]);
vi.mocked(getEnvironments).mockResolvedValue([mockEnvironment]);
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
vi.mocked(getMonthlyActiveOrganizationPeopleCount).mockResolvedValue(100);
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(500);
vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(null as any);
vi.mocked(getProjectPermissionByUserId).mockResolvedValue(mockProjectPermission);
vi.mocked(getTeamsByOrganizationId).mockResolvedValue(mockOrganizationTeams);
vi.mocked(getAccessControlPermission).mockResolvedValue(true);
mockIsDevelopment = false;
mockIsFormbricksCloud = false;
});
afterEach(() => {
cleanup();
vi.resetAllMocks();
});
test("renders correctly with default props", async () => {
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
render(
await EnvironmentLayout({
environmentId: "env-1",
session: mockSession,
children: <div>Child Content</div>,
})
);
expect(screen.getByTestId("main-navigation")).toBeInTheDocument();
expect(screen.getByTestId("top-control-bar")).toBeInTheDocument();
expect(screen.getByText("Child Content")).toBeInTheDocument();
expect(screen.queryByTestId("dev-banner")).not.toBeInTheDocument();
expect(screen.queryByTestId("limits-banner")).not.toBeInTheDocument();
expect(screen.queryByTestId("downgrade-banner")).not.toBeInTheDocument();
});
test("renders LimitsReachedBanner in Formbricks Cloud", async () => {
mockIsFormbricksCloud = true;
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
render(
await EnvironmentLayout({
environmentId: "env-1",
session: mockSession,
children: <div>Child Content</div>,
})
);
expect(screen.getByTestId("limits-banner")).toBeInTheDocument();
expect(vi.mocked(getMonthlyActiveOrganizationPeopleCount)).toHaveBeenCalledWith(mockOrganization.id);
expect(vi.mocked(getMonthlyOrganizationResponseCount)).toHaveBeenCalledWith(mockOrganization.id);
});
test("renders PendingDowngradeBanner when pending downgrade", async () => {
const pendingLicense = { ...mockLicense, isPendingDowngrade: true, active: true };
vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(null as any);
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue(pendingLicense),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
render(
await EnvironmentLayout({
environmentId: "env-1",
session: mockSession,
children: <div>Child Content</div>,
})
);
expect(screen.getByTestId("downgrade-banner")).toBeInTheDocument();
});
test("handles empty organizationTeams array", async () => {
vi.mocked(getTeamsByOrganizationId).mockResolvedValue([]);
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
render(
await EnvironmentLayout({
environmentId: "env-1",
session: mockSession,
children: <div>Child Content</div>,
})
);
expect(screen.getByTestId("organization-teams")).toHaveTextContent("[]");
});
test("handles null organizationTeams", async () => {
vi.mocked(getTeamsByOrganizationId).mockResolvedValue(null);
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
render(
await EnvironmentLayout({
environmentId: "env-1",
session: mockSession,
children: <div>Child Content</div>,
})
);
expect(screen.getByTestId("organization-teams")).toHaveTextContent("[]");
});
test("handles isAccessControlAllowed false", async () => {
vi.mocked(getAccessControlPermission).mockResolvedValue(false);
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
render(
await EnvironmentLayout({
environmentId: "env-1",
session: mockSession,
children: <div>Child Content</div>,
})
);
expect(screen.getByTestId("is-access-control-allowed")).toHaveTextContent("false");
});
test("throws error if user not found", async () => {
vi.mocked(getUser).mockResolvedValue(null);
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow(
"common.user_not_found"
);
});
test("throws error if organization not found", async () => {
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow(
"common.organization_not_found"
);
});
test("throws error if environment not found", async () => {
vi.mocked(getEnvironment).mockResolvedValue(null);
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow(
"common.environment_not_found"
);
});
test("throws error if projects, environments or organizations not found", async () => {
vi.mocked(getProjectsByUserId).mockResolvedValue(null as any);
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow(
"environments.projects_environments_organizations_not_found"
);
});
test("throws error if member has no project permission", async () => {
vi.mocked(getAccessFlags).mockReturnValue({ isMember: true } as any);
vi.mocked(getProjectPermissionByUserId).mockResolvedValue(null);
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { EnvironmentLayout } = await import(
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
);
await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow(
"common.project_permission_not_found"
);
});
});

View File

@@ -1,33 +0,0 @@
import { render } from "@testing-library/react";
import { describe, expect, test, vi } from "vitest";
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
import EnvironmentStorageHandler from "./EnvironmentStorageHandler";
describe("EnvironmentStorageHandler", () => {
test("sets environmentId in localStorage on mount", () => {
const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
const testEnvironmentId = "test-env-123";
render(<EnvironmentStorageHandler environmentId={testEnvironmentId} />);
expect(setItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS, testEnvironmentId);
setItemSpy.mockRestore();
});
test("updates environmentId in localStorage when prop changes", () => {
const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
const initialEnvironmentId = "test-env-initial";
const updatedEnvironmentId = "test-env-updated";
const { rerender } = render(<EnvironmentStorageHandler environmentId={initialEnvironmentId} />);
expect(setItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS, initialEnvironmentId);
rerender(<EnvironmentStorageHandler environmentId={updatedEnvironmentId} />);
expect(setItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS, updatedEnvironmentId);
expect(setItemSpy).toHaveBeenCalledTimes(2); // Called on mount and on rerender with new prop
setItemSpy.mockRestore();
});
});

View File

@@ -1,149 +0,0 @@
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 { EnvironmentSwitch } from "./EnvironmentSwitch";
// Mock next/navigation
const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: vi.fn(() => ({
push: mockPush,
})),
}));
// Mock @tolgee/react
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key: string) => key,
}),
}));
const mockEnvironmentDev: TEnvironment = {
id: "dev-env-id",
createdAt: new Date(),
updatedAt: new Date(),
type: "development",
projectId: "project-id",
appSetupCompleted: true,
};
const mockEnvironmentProd: TEnvironment = {
id: "prod-env-id",
createdAt: new Date(),
updatedAt: new Date(),
type: "production",
projectId: "project-id",
appSetupCompleted: true,
};
const mockEnvironments = [mockEnvironmentDev, mockEnvironmentProd];
describe("EnvironmentSwitch", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders checked when environment is development", () => {
render(<EnvironmentSwitch environment={mockEnvironmentDev} environments={mockEnvironments} />);
const switchElement = screen.getByRole("switch");
expect(switchElement).toBeChecked();
expect(screen.getByText("common.dev_env")).toHaveClass("text-orange-800");
});
test("renders unchecked when environment is production", () => {
render(<EnvironmentSwitch environment={mockEnvironmentProd} environments={mockEnvironments} />);
const switchElement = screen.getByRole("switch");
expect(switchElement).not.toBeChecked();
expect(screen.getByText("common.dev_env")).not.toHaveClass("text-orange-800");
});
test("calls router.push with development environment ID when toggled from production", async () => {
render(<EnvironmentSwitch environment={mockEnvironmentProd} environments={mockEnvironments} />);
const switchElement = screen.getByRole("switch");
expect(switchElement).not.toBeChecked();
await userEvent.click(switchElement);
// Check loading state (switch disabled)
expect(switchElement).toBeDisabled();
// Check router push call
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith(`/environments/${mockEnvironmentDev.id}/`);
});
// Check visual state change (though state update happens before navigation)
// In a real scenario, the component would re-render with the new environment prop after navigation.
// Here, we simulate the state change directly for testing the toggle logic.
await waitFor(() => {
// Re-render or check internal state if possible, otherwise check mock calls
// Since the component manages its own state, we can check the visual state after click
expect(switchElement).toBeChecked(); // State updates immediately
});
});
test("calls router.push with production environment ID when toggled from development", async () => {
render(<EnvironmentSwitch environment={mockEnvironmentDev} environments={mockEnvironments} />);
const switchElement = screen.getByRole("switch");
expect(switchElement).toBeChecked();
await userEvent.click(switchElement);
// Check loading state (switch disabled)
expect(switchElement).toBeDisabled();
// Check router push call
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith(`/environments/${mockEnvironmentProd.id}/`);
});
// Check visual state change
await waitFor(() => {
expect(switchElement).not.toBeChecked(); // State updates immediately
});
});
test("does not call router.push if target environment is not found", async () => {
const incompleteEnvironments = [mockEnvironmentProd]; // Only production exists
render(<EnvironmentSwitch environment={mockEnvironmentProd} environments={incompleteEnvironments} />);
const switchElement = screen.getByRole("switch");
await userEvent.click(switchElement); // Try to toggle to development
await waitFor(() => {
expect(switchElement).toBeDisabled(); // Loading state still set
});
// router.push should not be called because dev env is missing
expect(mockPush).not.toHaveBeenCalled();
// State still updates visually
await waitFor(() => {
expect(switchElement).toBeChecked();
});
});
test("toggles using the label click", async () => {
render(<EnvironmentSwitch environment={mockEnvironmentProd} environments={mockEnvironments} />);
const labelElement = screen.getByText("common.dev_env");
const switchElement = screen.getByRole("switch");
expect(switchElement).not.toBeChecked();
await userEvent.click(labelElement); // Click the label
// Check loading state (switch disabled)
expect(switchElement).toBeDisabled();
// Check router push call
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith(`/environments/${mockEnvironmentDev.id}/`);
});
// Check visual state change
await waitFor(() => {
expect(switchElement).toBeChecked();
});
});
});

View File

@@ -1,287 +0,0 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { usePathname, useRouter } from "next/navigation";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganization } from "@formbricks/types/organizations";
import { TProject } from "@formbricks/types/project";
import { TUser } from "@formbricks/types/user";
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
import { getLatestStableFbReleaseAction } from "@/modules/projects/settings/(setup)/app-connection/actions";
import { MainNavigation } from "./MainNavigation";
// Mock constants that this test needs
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
WEBAPP_URL: "http://localhost:3000",
}));
// Mock server actions that this test needs
vi.mock("@/modules/auth/actions/sign-out", () => ({
logSignOutAction: vi.fn().mockResolvedValue(undefined),
}));
// Mock dependencies
vi.mock("next/navigation", () => ({
useRouter: vi.fn(() => ({ push: vi.fn() })),
usePathname: vi.fn(() => "/environments/env1/surveys"),
}));
vi.mock("next-auth/react", () => ({
signOut: vi.fn(),
}));
vi.mock("@/modules/auth/hooks/use-sign-out", () => ({
useSignOut: vi.fn(() => ({ signOut: vi.fn() })),
}));
vi.mock("@/modules/projects/settings/(setup)/app-connection/actions", () => ({
getLatestStableFbReleaseAction: vi.fn(),
}));
vi.mock("@/app/lib/formbricks", () => ({
formbricksLogout: vi.fn(),
}));
vi.mock("@/lib/membership/utils", () => ({
getAccessFlags: (role?: string) => ({
isAdmin: role === "admin",
isOwner: role === "owner",
isManager: role === "manager",
isMember: role === "member",
isBilling: role === "billing",
}),
}));
vi.mock("@/modules/organization/components/CreateOrganizationModal", () => ({
CreateOrganizationModal: ({ open }: { open: boolean }) =>
open ? <div data-testid="create-org-modal">Create Org Modal</div> : null,
}));
vi.mock("@/modules/ui/components/avatars", () => ({
ProfileAvatar: () => <div data-testid="profile-avatar">Avatar</div>,
}));
vi.mock("next/image", () => ({
// eslint-disable-next-line @next/next/no-img-element
default: (props: any) => <img alt="test" {...props} />,
}));
vi.mock("../../../../../package.json", () => ({
version: "1.0.0",
}));
// Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {};
return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value.toString();
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
};
})();
Object.defineProperty(window, "localStorage", { value: localStorageMock });
// Mock data
const mockEnvironment: TEnvironment = {
id: "env1",
createdAt: new Date(),
updatedAt: new Date(),
type: "production",
projectId: "proj1",
appSetupCompleted: true,
};
const mockUser = {
id: "user1",
name: "Test User",
email: "test@example.com",
emailVerified: new Date(),
twoFactorEnabled: false,
identityProvider: "email",
createdAt: new Date(),
updatedAt: new Date(),
notificationSettings: { alert: {} },
role: "project_manager",
objective: "other",
} as unknown as TUser;
const mockOrganization = {
id: "org1",
name: "Test Org",
createdAt: new Date(),
updatedAt: new Date(),
billing: { stripeCustomerId: null, plan: "free", limits: { monthly: { responses: null } } } as any,
} as unknown as TOrganization;
const mockOrganizations: TOrganization[] = [
mockOrganization,
{ ...mockOrganization, id: "org2", name: "Another Org" },
];
const mockProject: TProject = {
id: "proj1",
name: "Test Project",
createdAt: new Date(),
updatedAt: new Date(),
organizationId: "org1",
environments: [mockEnvironment],
config: { channel: "website" },
} as unknown as TProject;
const mockProjects: TProject[] = [mockProject];
const defaultProps = {
environment: mockEnvironment,
organizations: mockOrganizations,
user: mockUser,
organization: mockOrganization,
projects: mockProjects,
isMultiOrgEnabled: true,
isFormbricksCloud: false,
isDevelopment: false,
membershipRole: "owner" as const,
organizationProjectsLimit: 5,
isLicenseActive: true,
isAccessControlAllowed: true,
};
describe("MainNavigation", () => {
let mockRouterPush: ReturnType<typeof vi.fn>;
beforeEach(() => {
mockRouterPush = vi.fn();
vi.mocked(useRouter).mockReturnValue({ push: mockRouterPush } as any);
vi.mocked(usePathname).mockReturnValue("/environments/env1/surveys");
vi.mocked(getLatestStableFbReleaseAction).mockResolvedValue({ data: null }); // Default: no new version
localStorage.clear();
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders expanded by default and collapses on toggle", async () => {
render(<MainNavigation {...defaultProps} />);
// Assuming the toggle button is the only one initially without an accessible name
// A more specific selector like data-testid would be better if available.
const toggleButton = screen.getByRole("button", { name: "" });
// Check initial state (expanded)
expect(screen.getByAltText("environments.formbricks_logo")).toBeInTheDocument();
// Check localStorage is not set initially after clear()
expect(localStorage.getItem("isMainNavCollapsed")).toBeNull();
// Click to collapse
await userEvent.click(toggleButton);
// Check state after first toggle (collapsed)
await waitFor(() => {
// Check that the attribute eventually becomes true
// Check that localStorage is updated
expect(localStorage.getItem("isMainNavCollapsed")).toBe("true");
});
// Check that the logo is eventually hidden
await waitFor(() => {
expect(screen.queryByAltText("environments.formbricks_logo")).not.toBeInTheDocument();
});
// Click to expand
await userEvent.click(toggleButton);
// Check state after second toggle (expanded)
await waitFor(() => {
// Check that the attribute eventually becomes false
// Check that localStorage is updated
expect(localStorage.getItem("isMainNavCollapsed")).toBe("false");
});
// Check that the logo is eventually visible
await waitFor(() => {
expect(screen.getByAltText("environments.formbricks_logo")).toBeInTheDocument();
});
});
test("renders user dropdown and handles logout", async () => {
const mockSignOut = vi.fn().mockResolvedValue({ url: "/auth/login" });
vi.mocked(useSignOut).mockReturnValue({ signOut: mockSignOut });
// Set up localStorage spy on the mocked localStorage
render(<MainNavigation {...defaultProps} />);
// Find the avatar and get its parent div which acts as the trigger
const userTrigger = screen.getByTestId("profile-avatar").parentElement!;
expect(userTrigger).toBeInTheDocument(); // Ensure the trigger element is found
await userEvent.click(userTrigger);
// Wait for the dropdown content to appear - using getAllByText to handle multiple instances
await waitFor(() => {
const accountElements = screen.getAllByText("common.account");
expect(accountElements).toHaveLength(2);
});
expect(screen.getByText("common.documentation")).toBeInTheDocument();
expect(screen.getByText("common.logout")).toBeInTheDocument();
const logoutButton = screen.getByText("common.logout");
await userEvent.click(logoutButton);
expect(mockSignOut).toHaveBeenCalledWith({
reason: "user_initiated",
redirectUrl: "/auth/login",
organizationId: "org1",
redirect: false,
callbackUrl: "/auth/login",
clearEnvironmentId: true,
});
await waitFor(() => {
expect(mockRouterPush).toHaveBeenCalledWith("/auth/login");
});
});
test("hides new version banner for members or if no new version", async () => {
// Test for member
vi.mocked(getLatestStableFbReleaseAction).mockResolvedValue({ data: "v1.1.0" });
render(<MainNavigation {...defaultProps} membershipRole="member" />);
let toggleButton = screen.getByRole("button", { name: "" });
await userEvent.click(toggleButton);
await waitFor(() => {
expect(screen.queryByText("common.new_version_available", { exact: false })).not.toBeInTheDocument();
});
cleanup(); // Clean up before next render
// Test for no new version
vi.mocked(getLatestStableFbReleaseAction).mockResolvedValue({ data: null });
render(<MainNavigation {...defaultProps} membershipRole="owner" />);
toggleButton = screen.getByRole("button", { name: "" });
await userEvent.click(toggleButton);
await waitFor(() => {
expect(screen.queryByText("common.new_version_available", { exact: false })).not.toBeInTheDocument();
});
});
test("hides main nav and project switcher if user role is billing", () => {
render(<MainNavigation {...defaultProps} membershipRole="billing" />);
expect(screen.queryByRole("link", { name: /common.surveys/ })).not.toBeInTheDocument();
expect(screen.queryByTestId("project-switcher")).not.toBeInTheDocument();
});
test("passes isAccessControlAllowed props to ProjectSwitcher", () => {
render(<MainNavigation {...defaultProps} />);
// Test basic navigation structure is rendered (aside element with complementary role)
expect(screen.getByRole("complementary")).toBeInTheDocument();
expect(screen.getByTestId("profile-avatar")).toBeInTheDocument();
});
test("handles no organizationTeams", () => {
render(<MainNavigation {...defaultProps} />);
// Test that navigation renders correctly with no teams
expect(screen.getByRole("complementary")).toBeInTheDocument();
});
test("handles isAccessControlAllowed false", () => {
render(<MainNavigation {...defaultProps} />);
// Test that navigation renders correctly with access control disabled
expect(screen.getByRole("complementary")).toBeInTheDocument();
});
});

View File

@@ -1,21 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test } from "vitest";
import { NavbarLoading } from "./NavbarLoading";
describe("NavbarLoading", () => {
afterEach(() => {
cleanup();
});
test("renders the correct number of skeleton elements", () => {
render(<NavbarLoading />);
// Find all divs with the animate-pulse class
const skeletonElements = screen.getAllByText((content, element) => {
return element?.tagName.toLowerCase() === "div" && element.classList.contains("animate-pulse");
});
// There are 8 skeleton divs in the component
expect(skeletonElements).toHaveLength(8);
});
});

View File

@@ -1,105 +0,0 @@
import { cleanup, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { NavigationLink } from "./NavigationLink";
// Mock next/link
vi.mock("next/link", () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) => <a href={href}>{children}</a>,
}));
// Mock tooltip components
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>
),
}));
const defaultProps = {
href: "/test-link",
isActive: false,
isCollapsed: false,
children: <svg data-testid="icon" />,
linkText: "Test Link Text",
isTextVisible: true,
};
describe("NavigationLink", () => {
afterEach(() => {
cleanup();
});
test("renders expanded link correctly (inactive, text visible)", () => {
render(<NavigationLink {...defaultProps} />);
const linkElement = screen.getByRole("link");
const listItem = linkElement.closest("li");
const textSpan = screen.getByText(defaultProps.linkText);
expect(linkElement).toHaveAttribute("href", defaultProps.href);
expect(screen.getByTestId("icon")).toBeInTheDocument();
expect(textSpan).toBeInTheDocument();
expect(textSpan).toHaveClass("opacity-0");
expect(listItem).not.toHaveClass("bg-slate-50"); // inactiveClass check
expect(listItem).toHaveClass("hover:bg-slate-50"); // inactiveClass check
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
});
test("renders expanded link correctly (active, text hidden)", () => {
render(<NavigationLink {...defaultProps} isActive={true} isTextVisible={false} />);
const linkElement = screen.getByRole("link");
const listItem = linkElement.closest("li");
const textSpan = screen.getByText(defaultProps.linkText);
expect(linkElement).toHaveAttribute("href", defaultProps.href);
expect(screen.getByTestId("icon")).toBeInTheDocument();
expect(textSpan).toBeInTheDocument();
expect(textSpan).toHaveClass("opacity-100");
expect(listItem).toHaveClass("bg-slate-50"); // activeClass check
expect(listItem).toHaveClass("border-brand-dark"); // activeClass check
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
});
test("renders collapsed link correctly (inactive)", () => {
render(<NavigationLink {...defaultProps} isCollapsed={true} />);
const linkElement = screen.getByRole("link");
const listItem = linkElement.closest("li");
expect(linkElement).toHaveAttribute("href", defaultProps.href);
expect(screen.getByTestId("icon")).toBeInTheDocument();
// Check text is NOT directly within the list item
expect(within(listItem!).queryByText(defaultProps.linkText)).not.toBeInTheDocument();
expect(listItem).not.toHaveClass("bg-slate-50"); // inactiveClass check
expect(listItem).toHaveClass("hover:bg-slate-50"); // inactiveClass check
// Check tooltip elements
expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument();
expect(screen.getByTestId("tooltip")).toBeInTheDocument();
expect(screen.getByTestId("tooltip-trigger")).toBeInTheDocument();
// Check text IS within the tooltip content mock
expect(screen.getByTestId("tooltip-content")).toHaveTextContent(defaultProps.linkText);
});
test("renders collapsed link correctly (active)", () => {
render(<NavigationLink {...defaultProps} isCollapsed={true} isActive={true} />);
const linkElement = screen.getByRole("link");
const listItem = linkElement.closest("li");
expect(linkElement).toHaveAttribute("href", defaultProps.href);
expect(screen.getByTestId("icon")).toBeInTheDocument();
// Check text is NOT directly within the list item
expect(within(listItem!).queryByText(defaultProps.linkText)).not.toBeInTheDocument();
expect(listItem).toHaveClass("bg-slate-50"); // activeClass check
expect(listItem).toHaveClass("border-brand-dark"); // activeClass check
// Check tooltip elements
expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument();
// Check text IS within the tooltip content mock
expect(screen.getByTestId("tooltip-content")).toHaveTextContent(defaultProps.linkText);
});
});

View File

@@ -1,145 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render } from "@testing-library/react";
import { Session } from "next-auth";
import { usePostHog } from "posthog-js/react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { TOrganizationBilling } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
import { PosthogIdentify } from "./PosthogIdentify";
type PartialPostHog = Partial<ReturnType<typeof usePostHog>>;
vi.mock("posthog-js/react", () => ({
usePostHog: vi.fn(),
}));
describe("PosthogIdentify", () => {
beforeEach(() => {
cleanup();
});
test("identifies the user and sets groups when isPosthogEnabled is true", () => {
const mockIdentify = vi.fn();
const mockGroup = vi.fn();
const mockPostHog: PartialPostHog = {
identify: mockIdentify,
group: mockGroup,
};
vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType<typeof usePostHog>);
render(
<PosthogIdentify
session={{ user: { id: "user-123" } } as Session}
user={
{
name: "Test User",
email: "test@example.com",
} as TUser
}
environmentId="env-456"
organizationId="org-789"
organizationName="Test Org"
organizationBilling={
{
plan: "enterprise",
limits: { monthly: { responses: 1000, miu: 5000 }, projects: 10 },
} as TOrganizationBilling
}
isPosthogEnabled
/>
);
// verify that identify is called with the session user id + extra info
expect(mockIdentify).toHaveBeenCalledWith("user-123", {
name: "Test User",
email: "test@example.com",
});
// environment + organization groups
expect(mockGroup).toHaveBeenCalledTimes(2);
expect(mockGroup).toHaveBeenCalledWith("environment", "env-456", { name: "env-456" });
expect(mockGroup).toHaveBeenCalledWith("organization", "org-789", {
name: "Test Org",
plan: "enterprise",
responseLimit: 1000,
miuLimit: 5000,
});
});
test("does nothing if isPosthogEnabled is false", () => {
const mockIdentify = vi.fn();
const mockGroup = vi.fn();
const mockPostHog: PartialPostHog = {
identify: mockIdentify,
group: mockGroup,
};
vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType<typeof usePostHog>);
render(
<PosthogIdentify
session={{ user: { id: "user-123" } } as Session}
user={{ name: "Test User", email: "test@example.com" } as TUser}
isPosthogEnabled={false}
/>
);
expect(mockIdentify).not.toHaveBeenCalled();
expect(mockGroup).not.toHaveBeenCalled();
});
test("does nothing if session user is missing", () => {
const mockIdentify = vi.fn();
const mockGroup = vi.fn();
const mockPostHog: PartialPostHog = {
identify: mockIdentify,
group: mockGroup,
};
vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType<typeof usePostHog>);
render(
<PosthogIdentify
// no user in session
session={{} as any}
user={{ name: "Test User", email: "test@example.com" } as TUser}
isPosthogEnabled
/>
);
// Because there's no session.user, we skip identify
expect(mockIdentify).not.toHaveBeenCalled();
expect(mockGroup).not.toHaveBeenCalled();
});
test("identifies user but does not group if environmentId/organizationId not provided", () => {
const mockIdentify = vi.fn();
const mockGroup = vi.fn();
const mockPostHog: PartialPostHog = {
identify: mockIdentify,
group: mockGroup,
};
vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType<typeof usePostHog>);
render(
<PosthogIdentify
session={{ user: { id: "user-123" } } as Session}
user={{ name: "Test User", email: "test@example.com" } as TUser}
isPosthogEnabled
/>
);
expect(mockIdentify).toHaveBeenCalledWith("user-123", {
name: "Test User",
email: "test@example.com",
});
// No environmentId or organizationId => no group calls
expect(mockGroup).not.toHaveBeenCalled();
});
});

View File

@@ -1,40 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { ProjectNavItem } from "./ProjectNavItem";
describe("ProjectNavItem", () => {
afterEach(() => {
cleanup();
});
const defaultProps = {
href: "/test-path",
children: <span>Test Child</span>,
};
test("renders correctly when active", () => {
render(<ProjectNavItem {...defaultProps} isActive={true} />);
const linkElement = screen.getByRole("link");
const listItem = linkElement.closest("li");
expect(linkElement).toHaveAttribute("href", "/test-path");
expect(screen.getByText("Test Child")).toBeInTheDocument();
expect(listItem).toHaveClass("bg-slate-50");
expect(listItem).toHaveClass("font-semibold");
expect(listItem).not.toHaveClass("hover:bg-slate-50");
});
test("renders correctly when inactive", () => {
render(<ProjectNavItem {...defaultProps} isActive={false} />);
const linkElement = screen.getByRole("link");
const listItem = linkElement.closest("li");
expect(linkElement).toHaveAttribute("href", "/test-path");
expect(screen.getByText("Test Child")).toBeInTheDocument();
expect(listItem).not.toHaveClass("bg-slate-50");
expect(listItem).not.toHaveClass("font-semibold");
expect(listItem).toHaveClass("hover:bg-slate-50");
});
});

View File

@@ -1,140 +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 { QuestionOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
import { getTodayDate } from "@/app/lib/surveys/surveys";
import { ResponseFilterProvider, useResponseFilter } from "./ResponseFilterContext";
// Mock the getTodayDate function
vi.mock("@/app/lib/surveys/surveys", () => ({
getTodayDate: vi.fn(),
}));
const mockToday = new Date("2024-01-15T00:00:00.000Z");
const mockFromDate = new Date("2024-01-01T00:00:00.000Z");
// Test component to use the hook
const TestComponent = () => {
const {
selectedFilter,
setSelectedFilter,
selectedOptions,
setSelectedOptions,
dateRange,
setDateRange,
resetState,
} = useResponseFilter();
return (
<div>
<div data-testid="responseStatus">{selectedFilter.responseStatus}</div>
<div data-testid="filterLength">{selectedFilter.filter.length}</div>
<div data-testid="questionOptionsLength">{selectedOptions.questionOptions.length}</div>
<div data-testid="questionFilterOptionsLength">{selectedOptions.questionFilterOptions.length}</div>
<div data-testid="dateFrom">{dateRange.from?.toISOString()}</div>
<div data-testid="dateTo">{dateRange.to?.toISOString()}</div>
<button
onClick={() =>
setSelectedFilter({
filter: [
{
questionType: { id: "q1", label: "Question 1" },
filterType: { filterValue: "value1", filterComboBoxValue: "option1" },
},
],
responseStatus: "complete",
})
}>
Update Filter
</button>
<button
onClick={() =>
setSelectedOptions({
questionOptions: [{ header: "q1" } as unknown as QuestionOptions],
questionFilterOptions: [{ id: "qFilterOpt1" } as unknown as QuestionFilterOptions],
})
}>
Update Options
</button>
<button onClick={() => setDateRange({ from: mockFromDate, to: mockToday })}>Update Date Range</button>
<button onClick={resetState}>Reset State</button>
</div>
);
};
describe("ResponseFilterContext", () => {
beforeEach(() => {
vi.mocked(getTodayDate).mockReturnValue(mockToday);
});
afterEach(() => {
cleanup();
vi.resetAllMocks();
});
test("should provide initial state values", () => {
render(
<ResponseFilterProvider>
<TestComponent />
</ResponseFilterProvider>
);
expect(screen.getByTestId("responseStatus").textContent).toBe("all");
expect(screen.getByTestId("filterLength").textContent).toBe("0");
expect(screen.getByTestId("questionOptionsLength").textContent).toBe("0");
expect(screen.getByTestId("questionFilterOptionsLength").textContent).toBe("0");
expect(screen.getByTestId("dateFrom").textContent).toBe("");
expect(screen.getByTestId("dateTo").textContent).toBe(mockToday.toISOString());
});
test("should update selectedFilter state", async () => {
render(
<ResponseFilterProvider>
<TestComponent />
</ResponseFilterProvider>
);
const updateButton = screen.getByText("Update Filter");
await userEvent.click(updateButton);
expect(screen.getByTestId("responseStatus").textContent).toBe("complete");
expect(screen.getByTestId("filterLength").textContent).toBe("1");
});
test("should update selectedOptions state", async () => {
render(
<ResponseFilterProvider>
<TestComponent />
</ResponseFilterProvider>
);
const updateButton = screen.getByText("Update Options");
await userEvent.click(updateButton);
expect(screen.getByTestId("questionOptionsLength").textContent).toBe("1");
expect(screen.getByTestId("questionFilterOptionsLength").textContent).toBe("1");
});
test("should update dateRange state", async () => {
render(
<ResponseFilterProvider>
<TestComponent />
</ResponseFilterProvider>
);
const updateButton = screen.getByText("Update Date Range");
await userEvent.click(updateButton);
expect(screen.getByTestId("dateFrom").textContent).toBe(mockFromDate.toISOString());
expect(screen.getByTestId("dateTo").textContent).toBe(mockToday.toISOString());
});
test("should throw error when useResponseFilter is used outside of Provider", () => {
// Hide console error temporarily
const consoleErrorMock = vi.spyOn(console, "error").mockImplementation(() => {});
expect(() => render(<TestComponent />)).toThrow("useFilterDate must be used within a FilterDateProvider");
consoleErrorMock.mockRestore();
});
});

View File

@@ -1,104 +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 { TEnvironment } from "@formbricks/types/environment";
import { WidgetStatusIndicator } from "./WidgetStatusIndicator";
// Mock next/navigation
const mockRefresh = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({
refresh: mockRefresh,
}),
}));
// Mock lucide-react icons
vi.mock("lucide-react", () => ({
AlertTriangleIcon: () => <div data-testid="alert-icon">AlertTriangleIcon</div>,
CheckIcon: () => <div data-testid="check-icon">CheckIcon</div>,
RotateCcwIcon: () => <div data-testid="refresh-icon">RotateCcwIcon</div>,
}));
// Mock Button component
vi.mock("@/modules/ui/components/button", () => ({
Button: ({ children, onClick, ...props }: any) => (
<button onClick={onClick} {...props}>
{children}
</button>
),
}));
const mockEnvironmentNotImplemented: TEnvironment = {
id: "env-not-implemented",
createdAt: new Date(),
updatedAt: new Date(),
type: "development",
projectId: "proj1",
appSetupCompleted: false, // Not implemented state
};
const mockEnvironmentRunning: TEnvironment = {
id: "env-running",
createdAt: new Date(),
updatedAt: new Date(),
type: "production",
projectId: "proj1",
appSetupCompleted: true, // Running state
};
describe("WidgetStatusIndicator", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders correctly for 'notImplemented' state", () => {
render(<WidgetStatusIndicator environment={mockEnvironmentNotImplemented} />);
// Check icon
expect(screen.getByTestId("alert-icon")).toBeInTheDocument();
expect(screen.queryByTestId("check-icon")).not.toBeInTheDocument();
// Check texts
expect(
screen.getByText("environments.project.app-connection.formbricks_sdk_not_connected")
).toBeInTheDocument();
expect(
screen.getByText("environments.project.app-connection.formbricks_sdk_not_connected_description")
).toBeInTheDocument();
// Check button
const recheckButton = screen.getByRole("button", { name: /environments.project.app-connection.recheck/ });
expect(recheckButton).toBeInTheDocument();
expect(screen.getByTestId("refresh-icon")).toBeInTheDocument();
});
test("renders correctly for 'running' state", () => {
render(<WidgetStatusIndicator environment={mockEnvironmentRunning} />);
// Check icon
expect(screen.getByTestId("check-icon")).toBeInTheDocument();
expect(screen.queryByTestId("alert-icon")).not.toBeInTheDocument();
// Check texts
expect(screen.getByText("environments.project.app-connection.receiving_data")).toBeInTheDocument();
expect(
screen.getByText("environments.project.app-connection.formbricks_sdk_connected")
).toBeInTheDocument();
// Check button absence
expect(
screen.queryByRole("button", { name: /environments.project.app-connection.recheck/ })
).not.toBeInTheDocument();
expect(screen.queryByTestId("refresh-icon")).not.toBeInTheDocument();
});
test("calls router.refresh when 'Recheck' button is clicked", async () => {
render(<WidgetStatusIndicator environment={mockEnvironmentNotImplemented} />);
const recheckButton = screen.getByRole("button", { name: /environments.project.app-connection.recheck/ });
await userEvent.click(recheckButton);
expect(mockRefresh).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,329 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useRouter } from "next/navigation";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { EnvironmentBreadcrumb } from "./environment-breadcrumb";
// Mock the dependencies
vi.mock("next/navigation", () => ({
useRouter: vi.fn(),
}));
// Mock the UI components
vi.mock("@/modules/ui/components/breadcrumb", () => ({
BreadcrumbItem: ({ children, isActive, isHighlighted, ...props }: any) => (
<li data-testid="breadcrumb-item" data-active={isActive} data-highlighted={isHighlighted} {...props}>
{children}
</li>
),
}));
vi.mock("@/modules/ui/components/dropdown-menu", () => ({
DropdownMenu: ({ children, onOpenChange }: any) => (
<button
type="button"
data-testid="dropdown-menu"
onClick={() => onOpenChange?.(true)}
onKeyDown={(e: any) => e.key === "Enter" && onOpenChange?.(true)}>
{children}
</button>
),
DropdownMenuContent: ({ children, ...props }: any) => (
<div data-testid="dropdown-content" {...props}>
{children}
</div>
),
DropdownMenuCheckboxItem: ({ children, onClick, checked, ...props }: any) => (
<div
data-testid="dropdown-checkbox-item"
data-checked={checked}
onClick={onClick}
onKeyDown={(e: any) => e.key === "Enter" && onClick?.()}
role="menuitemcheckbox"
aria-checked={checked}
tabIndex={0}
{...props}>
{children}
</div>
),
DropdownMenuTrigger: ({ children, ...props }: any) => (
<button data-testid="dropdown-trigger" {...props}>
{children}
</button>
),
DropdownMenuGroup: ({ children }: any) => <div data-testid="dropdown-group">{children}</div>,
}));
vi.mock("@/modules/ui/components/tooltip", () => ({
TooltipProvider: ({ children }: any) => <div data-testid="tooltip-provider">{children}</div>,
Tooltip: ({ children }: any) => <div data-testid="tooltip">{children}</div>,
TooltipTrigger: ({ children, asChild }: any) => (
<div data-testid="tooltip-trigger" data-as-child={asChild}>
{children}
</div>
),
TooltipContent: ({ children, className }: any) => (
<div data-testid="tooltip-content" className={className}>
{children}
</div>
),
}));
// Mock Lucide React icons
vi.mock("lucide-react", () => ({
Code2Icon: ({ className, strokeWidth }: any) => {
const isHeader = className?.includes("mr-2");
return (
<svg
data-testid={isHeader ? "code2-header-icon" : "code2-icon"}
className={className}
strokeWidth={strokeWidth}>
<title>Code2 Icon</title>
</svg>
);
},
ChevronDownIcon: ({ className, strokeWidth }: any) => (
<svg data-testid="chevron-down-icon" className={className} strokeWidth={strokeWidth}>
<title>ChevronDown Icon</title>
</svg>
),
CircleHelpIcon: ({ className }: any) => (
<svg data-testid="circle-help-icon" className={className}>
<title>CircleHelp Icon</title>
</svg>
),
Loader2: ({ className }: any) => (
<svg data-testid="loader-2-icon" className={className}>
<title>Loader2 Icon</title>
</svg>
),
}));
describe("EnvironmentBreadcrumb", () => {
const mockPush = vi.fn();
const mockRouter = {
push: mockPush,
replace: vi.fn(),
refresh: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
prefetch: vi.fn(),
};
const mockProductionEnvironment: TEnvironment = {
id: "env-prod-1",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-01"),
type: "production",
projectId: "project-1",
appSetupCompleted: true,
};
const mockDevelopmentEnvironment: TEnvironment = {
id: "env-dev-1",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-01"),
type: "development",
projectId: "project-1",
appSetupCompleted: true,
};
const mockEnvironments: TEnvironment[] = [mockProductionEnvironment, mockDevelopmentEnvironment];
beforeEach(() => {
vi.mocked(useRouter).mockReturnValue(mockRouter as any);
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders environment breadcrumb with production environment", () => {
render(
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
);
expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument();
expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument();
expect(screen.getByTestId("code2-icon")).toBeInTheDocument();
expect(screen.getAllByText("production")).toHaveLength(2); // trigger + dropdown option
});
test("renders environment breadcrumb with development environment and shows tooltip", () => {
render(
<EnvironmentBreadcrumb
environments={mockEnvironments}
currentEnvironment={mockDevelopmentEnvironment}
/>
);
expect(screen.getAllByText("development")).toHaveLength(2); // trigger + dropdown option
expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument();
expect(screen.getByTestId("circle-help-icon")).toBeInTheDocument();
});
test("highlights breadcrumb item for development environment", () => {
render(
<EnvironmentBreadcrumb
environments={mockEnvironments}
currentEnvironment={mockDevelopmentEnvironment}
/>
);
const breadcrumbItem = screen.getByTestId("breadcrumb-item");
expect(breadcrumbItem).toHaveAttribute("data-highlighted", "true");
});
test("does not highlight breadcrumb item for production environment", () => {
render(
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
);
const breadcrumbItem = screen.getByTestId("breadcrumb-item");
expect(breadcrumbItem).toHaveAttribute("data-highlighted", "false");
});
test("shows chevron down icon when dropdown is open", async () => {
const user = userEvent.setup();
render(
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
await waitFor(() => {
expect(screen.getAllByTestId("chevron-down-icon")).toHaveLength(1);
});
});
test("renders dropdown content with environment options", async () => {
const user = userEvent.setup();
render(
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
expect(screen.getByTestId("dropdown-content")).toBeInTheDocument();
expect(screen.getByText("common.choose_environment")).toBeInTheDocument();
expect(screen.getByTestId("dropdown-group")).toBeInTheDocument();
});
test("renders all environment options in dropdown", async () => {
const user = userEvent.setup();
render(
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item");
expect(checkboxItems).toHaveLength(2);
// Check production environment option
const productionOption = checkboxItems.find((item) => item.textContent?.includes("production"));
expect(productionOption).toBeInTheDocument();
expect(productionOption).toHaveAttribute("data-checked", "true");
// Check development environment option
const developmentOption = checkboxItems.find((item) => item.textContent?.includes("development"));
expect(developmentOption).toBeInTheDocument();
expect(developmentOption).toHaveAttribute("data-checked", "false");
});
test("handles environment change when clicking dropdown option", async () => {
const user = userEvent.setup();
render(
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item");
const developmentOption = checkboxItems.find((item) => item.textContent?.includes("development"));
expect(developmentOption).toBeInTheDocument();
await user.click(developmentOption!);
expect(mockPush).toHaveBeenCalledWith("/environments/env-dev-1/");
});
test("capitalizes environment type in display", () => {
render(
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
);
const environmentSpans = screen.getAllByText("production");
const triggerSpan = environmentSpans.find((span) => span.className.includes("capitalize"));
expect(triggerSpan).toHaveClass("capitalize");
});
test("tooltip shows correct content for development environment", () => {
render(
<EnvironmentBreadcrumb
environments={mockEnvironments}
currentEnvironment={mockDevelopmentEnvironment}
/>
);
const tooltipContent = screen.getByTestId("tooltip-content");
expect(tooltipContent).toHaveClass("text-white bg-red-800 border-none mt-2");
expect(tooltipContent).toHaveTextContent("common.development_environment_banner");
});
test("renders without tooltip for production environment", () => {
render(
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
);
expect(screen.queryByTestId("circle-help-icon")).not.toBeInTheDocument();
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
});
test("sets breadcrumb item as active when dropdown is open", async () => {
const user = userEvent.setup();
render(
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
);
// Initially not active
let breadcrumbItem = screen.getByTestId("breadcrumb-item");
expect(breadcrumbItem).toHaveAttribute("data-active", "false");
// Open dropdown
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
// Should be active when dropdown is open
breadcrumbItem = screen.getByTestId("breadcrumb-item");
expect(breadcrumbItem).toHaveAttribute("data-active", "true");
});
test("handles single environment scenario", () => {
const singleEnvironment = [mockProductionEnvironment];
render(
<EnvironmentBreadcrumb
environments={singleEnvironment}
currentEnvironment={mockProductionEnvironment}
/>
);
expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument();
expect(screen.getAllByText("production")).toHaveLength(2); // trigger + dropdown option
});
test("handles empty environments array gracefully", () => {
render(<EnvironmentBreadcrumb environments={[]} currentEnvironment={mockProductionEnvironment} />);
expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument();
expect(screen.getByText("production")).toBeInTheDocument();
});
});

View File

@@ -1,560 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { usePathname, useRouter } from "next/navigation";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations";
import { OrganizationBreadcrumb } from "./organization-breadcrumb";
// Mock the dependencies
vi.mock("next/navigation", () => ({
useRouter: vi.fn(),
usePathname: vi.fn(),
}));
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key: string) => key,
}),
}));
vi.mock("@/modules/organization/components/CreateOrganizationModal", () => ({
CreateOrganizationModal: ({ open, setOpen }: any) =>
open ? (
<div data-testid="create-organization-modal">
<button type="button" onClick={() => setOpen(false)}>
Close Modal
</button>
Create Organization Modal
</div>
) : null,
}));
// Mock the UI components
vi.mock("@/modules/ui/components/breadcrumb", () => ({
BreadcrumbItem: ({ children, isActive, ...props }: any) => (
<li data-testid="breadcrumb-item" data-active={isActive} {...props}>
{children}
</li>
),
}));
vi.mock("@/modules/ui/components/dropdown-menu", () => ({
DropdownMenu: ({ children, onOpenChange }: any) => (
<div
data-testid="dropdown-menu"
onClick={() => onOpenChange?.(true)}
onKeyDown={(e: any) => e.key === "Enter" && onOpenChange?.(true)}
role="button"
tabIndex={0}>
{children}
</div>
),
DropdownMenuContent: ({ children, ...props }: any) => (
<div data-testid="dropdown-content" {...props}>
{children}
</div>
),
DropdownMenuCheckboxItem: ({ children, onClick, checked, ...props }: any) => (
<div
data-testid="dropdown-checkbox-item"
data-checked={checked}
onClick={onClick}
onKeyDown={(e: any) => e.key === "Enter" && onClick?.()}
role="menuitemcheckbox"
aria-checked={checked}
tabIndex={0}
{...props}>
{children}
</div>
),
DropdownMenuTrigger: ({ children, ...props }: any) => (
<button data-testid="dropdown-trigger" {...props}>
{children}
</button>
),
DropdownMenuGroup: ({ children }: any) => <div data-testid="dropdown-group">{children}</div>,
DropdownMenuSeparator: () => <div data-testid="dropdown-separator" />,
}));
// Mock Lucide React icons
vi.mock("lucide-react", () => ({
BuildingIcon: ({ className, strokeWidth }: any) => {
const isHeader = className?.includes("mr-2");
return (
<svg
data-testid={isHeader ? "building-header-icon" : "building-icon"}
className={className}
strokeWidth={strokeWidth}>
<title>Building Icon</title>
</svg>
);
},
ChevronDownIcon: ({ className, strokeWidth }: any) => (
<svg data-testid="chevron-down-icon" className={className} strokeWidth={strokeWidth}>
<title>ChevronDown Icon</title>
</svg>
),
ChevronRightIcon: ({ className, strokeWidth }: any) => (
<svg data-testid="chevron-right-icon" className={className} strokeWidth={strokeWidth}>
<title>ChevronRight Icon</title>
</svg>
),
PlusIcon: ({ className }: any) => (
<svg data-testid="plus-icon" className={className}>
<title>Plus Icon</title>
</svg>
),
SettingsIcon: ({ className }: any) => (
<svg data-testid="settings-icon" className={className}>
<title>Settings Icon</title>
</svg>
),
Loader2: ({ className }: any) => (
<svg data-testid="loader-2-icon" className={className}>
<title>Loader2 Icon</title>
</svg>
),
}));
describe("OrganizationBreadcrumb", () => {
const mockPush = vi.fn();
const mockRouter = {
push: mockPush,
replace: vi.fn(),
refresh: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
prefetch: vi.fn(),
};
const mockOrganization1: TOrganization = {
id: "org-1",
name: "Test Organization 1",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-01"),
billing: {
plan: "free",
stripeCustomerId: null,
} as unknown as TOrganizationBilling,
isAIEnabled: false,
};
const mockOrganization2: TOrganization = {
id: "org-2",
name: "Test Organization 2",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-01"),
billing: {
plan: "startup",
stripeCustomerId: null,
} as unknown as TOrganizationBilling,
isAIEnabled: true,
};
const mockOrganizations = [mockOrganization1, mockOrganization2];
const currentEnvironmentId = "env-123";
beforeEach(() => {
vi.mocked(useRouter).mockReturnValue(mockRouter as any);
vi.mocked(usePathname).mockReturnValue("/environments/env-123/");
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe("Single Organization Setup", () => {
test("renders organization breadcrumb without dropdown for single org", () => {
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={[mockOrganization1]}
isMultiOrgEnabled={false}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument();
expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument();
expect(screen.getByTestId("building-icon")).toBeInTheDocument();
expect(screen.getByText("Test Organization 1")).toBeInTheDocument();
});
test("shows organization settings without organization switcher", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={[mockOrganization1]}
isMultiOrgEnabled={false}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
expect(screen.getByTestId("dropdown-content")).toBeInTheDocument();
expect(screen.getByText("common.organization_settings")).toBeInTheDocument();
expect(screen.queryByText("common.choose_organization")).not.toBeInTheDocument();
});
});
describe("Multi Organization Setup", () => {
test("renders organization breadcrumb with dropdown for multi org", () => {
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument();
expect(screen.getByTestId("building-icon")).toBeInTheDocument();
expect(screen.getAllByText("Test Organization 1")).toHaveLength(2); // trigger + dropdown option
});
test("shows chevron icons correctly", () => {
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
// Should show chevron right when closed
expect(screen.getByTestId("chevron-right-icon")).toBeInTheDocument();
expect(screen.queryByTestId("chevron-down-icon")).not.toBeInTheDocument();
});
test("shows chevron down when dropdown is open", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
await waitFor(() => {
expect(screen.getByTestId("chevron-down-icon")).toBeInTheDocument();
});
});
test("renders organization selector in dropdown", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
expect(screen.getByText("common.choose_organization")).toBeInTheDocument();
expect(screen.getByTestId("dropdown-group")).toBeInTheDocument();
const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item");
expect(checkboxItems.length).toBeGreaterThanOrEqual(2); // Organizations + create new option + settings
});
test("handles organization change when clicking dropdown option", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item");
const org2Option = checkboxItems.find((item) => item.textContent?.includes("Test Organization 2"));
expect(org2Option).toBeInTheDocument();
await user.click(org2Option!);
expect(mockPush).toHaveBeenCalledWith("/organizations/org-2/");
});
test("shows create new organization option when multi org is enabled", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const createOrgOption = screen.getByText("common.create_new_organization");
expect(createOrgOption).toBeInTheDocument();
expect(screen.getByTestId("plus-icon")).toBeInTheDocument();
});
test("opens create organization modal when clicking create new option", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const createOrgOption = screen.getByText("common.create_new_organization");
await user.click(createOrgOption);
expect(screen.getByTestId("create-organization-modal")).toBeInTheDocument();
});
test("hides create new organization option when multi org is disabled", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={false}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
expect(screen.queryByText("common.create_new_organization")).not.toBeInTheDocument();
});
});
describe("Organization Settings", () => {
test("renders all organization settings options", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
isFormbricksCloud={true}
isMember={false}
currentEnvironmentId={currentEnvironmentId}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
expect(screen.getByText("common.organization_settings")).toBeInTheDocument();
expect(screen.getByTestId("settings-icon")).toBeInTheDocument();
expect(screen.getByText("common.general")).toBeInTheDocument();
expect(screen.getByText("common.teams")).toBeInTheDocument();
expect(screen.getByText("common.api_keys")).toBeInTheDocument();
expect(screen.getByText("common.billing")).toBeInTheDocument();
});
test("handles navigation to organization settings", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const generalOption = screen.getByText("common.general");
await user.click(generalOption);
expect(mockPush).toHaveBeenCalledWith(`/environments/${currentEnvironmentId}/settings/general`);
});
test("marks current settings page as checked", async () => {
vi.mocked(usePathname).mockReturnValue("/environments/env-123/settings/teams");
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item");
const teamsOption = checkboxItems.find((item) => item.textContent?.includes("common.teams"));
expect(teamsOption).toBeInTheDocument();
expect(teamsOption).toHaveAttribute("data-checked", "true");
});
});
describe("Edge Cases", () => {
test("handles single organization with multi org enabled", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={[mockOrganization1]}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
// Should still show organization selector since multi org is enabled
expect(screen.getByText("common.choose_organization")).toBeInTheDocument();
expect(screen.getByText("common.create_new_organization")).toBeInTheDocument();
});
test("shows separator between organization switcher and settings", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
expect(screen.getByTestId("dropdown-separator")).toBeInTheDocument();
});
test("sets breadcrumb item as active when dropdown is open", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
// Initially not active
let breadcrumbItem = screen.getByTestId("breadcrumb-item");
expect(breadcrumbItem).toHaveAttribute("data-active", "false");
// Open dropdown
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
// Should be active when dropdown is open
breadcrumbItem = screen.getByTestId("breadcrumb-item");
expect(breadcrumbItem).toHaveAttribute("data-active", "true");
});
test("closes create organization modal correctly", async () => {
const user = userEvent.setup();
render(
<OrganizationBreadcrumb
currentOrganizationId={mockOrganization1.id}
organizations={mockOrganizations}
isMultiOrgEnabled={true}
currentEnvironmentId={currentEnvironmentId}
isFormbricksCloud={true}
isMember={false}
isOwnerOrManager={true}
/>
);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const createOrgOption = screen.getByText("common.create_new_organization");
await user.click(createOrgOption);
expect(screen.getByTestId("create-organization-modal")).toBeInTheDocument();
const closeButton = screen.getByText("Close Modal");
await user.click(closeButton);
expect(screen.queryByTestId("create-organization-modal")).not.toBeInTheDocument();
});
});
});

View File

@@ -1,340 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { ProjectAndOrgSwitch } from "./project-and-org-switch";
// Mock the individual breadcrumb components
vi.mock("@/app/(app)/environments/[environmentId]/components/organization-breadcrumb", () => ({
OrganizationBreadcrumb: ({
currentOrganizationId,
organizations,
isMultiOrgEnabled,
currentEnvironmentId,
}: any) => {
const currentOrganization = organizations.find((org: any) => org.id === currentOrganizationId);
return (
<div data-testid="organization-breadcrumb">
<div>Organization: {currentOrganization?.name}</div>
<div>Organizations Count: {organizations.length}</div>
<div>Multi Org: {isMultiOrgEnabled ? "Enabled" : "Disabled"}</div>
<div>Environment ID: {currentEnvironmentId}</div>
</div>
);
},
}));
vi.mock("@/app/(app)/environments/[environmentId]/components/project-breadcrumb", () => ({
ProjectBreadcrumb: ({
currentProjectId,
projects,
isOwnerOrManager,
organizationProjectsLimit,
isFormbricksCloud,
isLicenseActive,
currentOrganizationId,
currentEnvironmentId,
isAccessControlAllowed,
}: any) => {
const currentProject = projects.find((project: any) => project.id === currentProjectId);
return (
<div data-testid="project-breadcrumb">
<div>Project: {currentProject?.name}</div>
<div>Projects Count: {projects.length}</div>
<div>Owner/Manager: {isOwnerOrManager ? "Yes" : "No"}</div>
<div>Project Limit: {organizationProjectsLimit}</div>
<div>Formbricks Cloud: {isFormbricksCloud ? "Yes" : "No"}</div>
<div>License Active: {isLicenseActive ? "Yes" : "No"}</div>
<div>Organization ID: {currentOrganizationId}</div>
<div>Environment ID: {currentEnvironmentId}</div>
<div>Access Control: {isAccessControlAllowed ? "Allowed" : "Not Allowed"}</div>
</div>
);
},
}));
vi.mock("@/app/(app)/environments/[environmentId]/components/environment-breadcrumb", () => ({
EnvironmentBreadcrumb: ({ environments, currentEnvironmentId }: any) => {
const currentEnvironment = environments.find((env: any) => env.id === currentEnvironmentId);
return (
<div data-testid="environment-breadcrumb">
<div>Environment: {currentEnvironment?.type}</div>
<div>Environments Count: {environments.length}</div>
<div>Environment ID: {currentEnvironment?.id}</div>
</div>
);
},
}));
// Mock the UI components
vi.mock("@/modules/ui/components/breadcrumb", () => ({
Breadcrumb: ({ children }: any) => (
<nav data-testid="breadcrumb" aria-label="breadcrumb">
{children}
</nav>
),
BreadcrumbList: ({ children, className }: any) => (
<ol data-testid="breadcrumb-list" className={className}>
{children}
</ol>
),
}));
describe("ProjectAndOrgSwitch", () => {
const mockOrganization1 = {
id: "org-1",
name: "Test Organization 1",
};
const mockOrganization2 = {
id: "org-2",
name: "Test Organization 2",
};
const mockProject1 = {
id: "proj-1",
name: "Test Project 1",
};
const mockProject2 = {
id: "proj-2",
name: "Test Project 2",
};
const mockEnvironment1: TEnvironment = {
id: "env-1",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-01"),
type: "development",
projectId: "proj-1",
appSetupCompleted: true,
};
const mockEnvironment2: TEnvironment = {
id: "env-2",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-01"),
type: "development",
projectId: "proj-1",
appSetupCompleted: true,
};
const defaultProps = {
currentOrganizationId: "org-1",
organizations: [mockOrganization1, mockOrganization2],
currentProjectId: "proj-1",
projects: [mockProject1, mockProject2],
currentEnvironmentId: "env-1",
environments: [mockEnvironment1, mockEnvironment2],
isMultiOrgEnabled: true,
organizationProjectsLimit: 5,
isFormbricksCloud: true,
isLicenseActive: false,
isOwnerOrManager: true,
isAccessControlAllowed: true,
isMember: true,
};
afterEach(() => {
cleanup();
});
describe("Basic Rendering", () => {
test("renders main breadcrumb structure", () => {
render(<ProjectAndOrgSwitch {...defaultProps} />);
expect(screen.getByTestId("breadcrumb")).toBeInTheDocument();
expect(screen.getByTestId("breadcrumb-list")).toBeInTheDocument();
expect(screen.getByTestId("breadcrumb")).toHaveAttribute("aria-label", "breadcrumb");
});
test("applies correct CSS classes to breadcrumb list", () => {
render(<ProjectAndOrgSwitch {...defaultProps} />);
const breadcrumbList = screen.getByTestId("breadcrumb-list");
expect(breadcrumbList).toHaveClass("gap-0");
});
test("renders all three breadcrumb components", () => {
render(<ProjectAndOrgSwitch {...defaultProps} />);
expect(screen.getByTestId("organization-breadcrumb")).toBeInTheDocument();
expect(screen.getByTestId("project-breadcrumb")).toBeInTheDocument();
});
});
describe("Organization Breadcrumb Integration", () => {
test("passes correct props to organization breadcrumb", () => {
render(<ProjectAndOrgSwitch {...defaultProps} />);
const orgBreadcrumb = screen.getByTestId("organization-breadcrumb");
expect(orgBreadcrumb).toHaveTextContent("Organization: Test Organization 1");
expect(orgBreadcrumb).toHaveTextContent("Organizations Count: 2");
expect(orgBreadcrumb).toHaveTextContent("Multi Org: Enabled");
expect(orgBreadcrumb).toHaveTextContent("Environment ID: env-1");
});
test("handles single organization setup", () => {
render(
<ProjectAndOrgSwitch
{...defaultProps}
organizations={[mockOrganization1]}
isMultiOrgEnabled={false}
/>
);
const orgBreadcrumb = screen.getByTestId("organization-breadcrumb");
expect(orgBreadcrumb).toHaveTextContent("Organizations Count: 1");
expect(orgBreadcrumb).toHaveTextContent("Multi Org: Disabled");
});
});
describe("Project Breadcrumb Integration", () => {
test("passes correct props to project breadcrumb", () => {
render(<ProjectAndOrgSwitch {...defaultProps} />);
const projectBreadcrumb = screen.getByTestId("project-breadcrumb");
expect(projectBreadcrumb).toHaveTextContent("Project: Test Project 1");
expect(projectBreadcrumb).toHaveTextContent("Projects Count: 2");
expect(projectBreadcrumb).toHaveTextContent("Owner/Manager: Yes");
expect(projectBreadcrumb).toHaveTextContent("Project Limit: 5");
expect(projectBreadcrumb).toHaveTextContent("Formbricks Cloud: Yes");
expect(projectBreadcrumb).toHaveTextContent("License Active: No");
expect(projectBreadcrumb).toHaveTextContent("Organization ID: org-1");
expect(projectBreadcrumb).toHaveTextContent("Environment ID: env-1");
expect(projectBreadcrumb).toHaveTextContent("Access Control: Allowed");
});
test("handles non-owner/manager user", () => {
render(<ProjectAndOrgSwitch {...defaultProps} isOwnerOrManager={false} />);
const projectBreadcrumb = screen.getByTestId("project-breadcrumb");
expect(projectBreadcrumb).toHaveTextContent("Owner/Manager: No");
});
test("handles self-hosted setup", () => {
render(<ProjectAndOrgSwitch {...defaultProps} isFormbricksCloud={false} isLicenseActive={true} />);
const projectBreadcrumb = screen.getByTestId("project-breadcrumb");
expect(projectBreadcrumb).toHaveTextContent("Formbricks Cloud: No");
expect(projectBreadcrumb).toHaveTextContent("License Active: Yes");
});
test("handles access control restrictions", () => {
render(<ProjectAndOrgSwitch {...defaultProps} isAccessControlAllowed={false} />);
const projectBreadcrumb = screen.getByTestId("project-breadcrumb");
expect(projectBreadcrumb).toHaveTextContent("Access Control: Not Allowed");
});
});
describe("Environment Breadcrumb Integration", () => {
test("passes correct props to environment breadcrumb", () => {
render(<ProjectAndOrgSwitch {...defaultProps} />);
const envBreadcrumb = screen.getByTestId("environment-breadcrumb");
expect(envBreadcrumb).toHaveTextContent("Environments Count: 2");
});
test("handles single environment", () => {
render(<ProjectAndOrgSwitch {...defaultProps} environments={[mockEnvironment1]} />);
const envBreadcrumb = screen.getByTestId("environment-breadcrumb");
expect(envBreadcrumb).toHaveTextContent("Environments Count: 1");
});
});
describe("Props Propagation", () => {
test("correctly propagates organization limits", () => {
render(<ProjectAndOrgSwitch {...defaultProps} organizationProjectsLimit={10} />);
const projectBreadcrumb = screen.getByTestId("project-breadcrumb");
expect(projectBreadcrumb).toHaveTextContent("Project Limit: 10");
});
test("correctly propagates current organization to project breadcrumb", () => {
render(<ProjectAndOrgSwitch {...defaultProps} currentOrganizationId="org-2" />);
const orgBreadcrumb = screen.getByTestId("organization-breadcrumb");
const projectBreadcrumb = screen.getByTestId("project-breadcrumb");
expect(orgBreadcrumb).toHaveTextContent("Organization: Test Organization 2");
expect(projectBreadcrumb).toHaveTextContent("Organization ID: org-2");
});
});
describe("Edge Cases", () => {
test("handles zero project limit", () => {
render(<ProjectAndOrgSwitch {...defaultProps} organizationProjectsLimit={0} />);
const projectBreadcrumb = screen.getByTestId("project-breadcrumb");
expect(projectBreadcrumb).toHaveTextContent("Project Limit: 0");
});
test("handles all boolean props as false", () => {
render(
<ProjectAndOrgSwitch
{...defaultProps}
isMultiOrgEnabled={false}
isFormbricksCloud={false}
isLicenseActive={false}
isOwnerOrManager={false}
isAccessControlAllowed={false}
/>
);
const orgBreadcrumb = screen.getByTestId("organization-breadcrumb");
const projectBreadcrumb = screen.getByTestId("project-breadcrumb");
expect(orgBreadcrumb).toHaveTextContent("Multi Org: Disabled");
expect(projectBreadcrumb).toHaveTextContent("Owner/Manager: No");
expect(projectBreadcrumb).toHaveTextContent("Formbricks Cloud: No");
expect(projectBreadcrumb).toHaveTextContent("License Active: No");
expect(projectBreadcrumb).toHaveTextContent("Access Control: Not Allowed");
});
test("maintains component order in DOM", () => {
render(<ProjectAndOrgSwitch {...defaultProps} />);
const breadcrumbList = screen.getByTestId("breadcrumb-list");
const children = Array.from(breadcrumbList.children);
expect(children[0]).toHaveAttribute("data-testid", "organization-breadcrumb");
expect(children[1]).toHaveAttribute("data-testid", "project-breadcrumb");
expect(children[2]).toHaveAttribute("data-testid", "environment-breadcrumb");
});
});
describe("TypeScript Props Interface", () => {
test("accepts all required props without error", () => {
// This test ensures the component accepts the full interface
expect(() => {
render(<ProjectAndOrgSwitch {...defaultProps} />);
}).not.toThrow();
});
test("works with minimal valid props", () => {
const minimalProps = {
currentOrganizationId: "org-1",
organizations: [mockOrganization1],
currentProjectId: "proj-1",
projects: [mockProject1],
currentEnvironmentId: "env-1",
environments: [mockEnvironment1],
isMultiOrgEnabled: false,
organizationProjectsLimit: 1,
isFormbricksCloud: false,
isLicenseActive: false,
isOwnerOrManager: false,
isAccessControlAllowed: false,
isMember: true,
};
expect(() => {
render(<ProjectAndOrgSwitch {...minimalProps} />);
}).not.toThrow();
expect(screen.getByTestId("breadcrumb")).toBeInTheDocument();
});
});
});

View File

@@ -1,512 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useRouter } from "next/navigation";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations";
import { TProject } from "@formbricks/types/project";
import { ProjectBreadcrumb } from "./project-breadcrumb";
// Mock the dependencies
vi.mock("next/navigation", () => ({
useRouter: vi.fn(),
usePathname: vi.fn(() => "/environments/env-123/project/general"),
}));
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key: string) => key,
}),
}));
vi.mock("@/modules/projects/components/project-limit-modal", () => ({
ProjectLimitModal: ({ open, setOpen, buttons, projectLimit }: any) =>
open ? (
<div data-testid="project-limit-modal">
<div>Project Limit: {projectLimit}</div>
<button onClick={() => setOpen(false)}>Close Limit Modal</button>
{buttons.map((button: any) => (
<button key={button.text} type="button" onClick={() => button.href && window.open(button.href)}>
{button.text}
</button>
))}
</div>
) : null,
}));
vi.mock("@/modules/projects/components/create-project-modal", () => ({
CreateProjectModal: ({ open, setOpen, organizationId, isAccessControlAllowed }: any) =>
open ? (
<div data-testid="create-project-modal">
<div>Organization: {organizationId}</div>
<div>Access Control: {isAccessControlAllowed ? "Allowed" : "Not Allowed"}</div>
<button onClick={() => setOpen(false)}>Close Create Modal</button>
</div>
) : null,
}));
// Mock the UI components
vi.mock("@/modules/ui/components/breadcrumb", () => ({
BreadcrumbItem: ({ children, isActive, ...props }: any) => (
<li data-testid="breadcrumb-item" data-active={isActive} {...props}>
{children}
</li>
),
}));
vi.mock("@/modules/ui/components/dropdown-menu", () => ({
DropdownMenu: ({ children, onOpenChange }: any) => (
<button
type="button"
data-testid="dropdown-menu"
onClick={() => onOpenChange?.(true)}
onKeyDown={(e: any) => e.key === "Enter" && onOpenChange?.(true)}>
{children}
</button>
),
DropdownMenuContent: ({ children, ...props }: any) => (
<div data-testid="dropdown-content" {...props}>
{children}
</div>
),
DropdownMenuCheckboxItem: ({ children, onClick, checked, ...props }: any) => (
<div
data-testid="dropdown-checkbox-item"
data-checked={checked}
onClick={onClick}
onKeyDown={(e: any) => e.key === "Enter" && onClick?.()}
role="menuitemcheckbox"
aria-checked={checked}
tabIndex={0}
{...props}>
{children}
</div>
),
DropdownMenuTrigger: ({ children, ...props }: any) => (
<button data-testid="dropdown-trigger" {...props}>
{children}
</button>
),
DropdownMenuGroup: ({ children }: any) => <div data-testid="dropdown-group">{children}</div>,
DropdownMenuSeparator: () => <div data-testid="dropdown-separator" />,
}));
// Mock Lucide React icons
vi.mock("lucide-react", () => ({
FolderOpenIcon: ({ className, strokeWidth }: any) => {
const isHeader = className?.includes("mr-2");
return (
<svg
data-testid={isHeader ? "folder-open-header-icon" : "folder-open-icon"}
className={className}
strokeWidth={strokeWidth}>
<title>FolderOpen Icon</title>
</svg>
);
},
ChevronDownIcon: ({ className, strokeWidth }: any) => (
<svg data-testid="chevron-down-icon" className={className} strokeWidth={strokeWidth}>
<title>ChevronDown Icon</title>
</svg>
),
ChevronRightIcon: ({ className, strokeWidth }: any) => (
<svg data-testid="chevron-right-icon" className={className} strokeWidth={strokeWidth}>
<title>ChevronRight Icon</title>
</svg>
),
PlusIcon: ({ className }: any) => (
<svg data-testid="plus-icon" className={className}>
<title>Plus Icon</title>
</svg>
),
Loader2: ({ className }: any) => (
<svg data-testid="loader-2-icon" className={className}>
<title>Loader2 Icon</title>
</svg>
),
CogIcon: ({ className }: any) => (
<svg data-testid="cog-icon" className={className}>
<title>Cog Icon</title>
</svg>
),
SettingsIcon: ({ className }: any) => (
<svg data-testid="settings-icon" className={className}>
<title>Settings Icon</title>
</svg>
),
}));
describe("ProjectBreadcrumb", () => {
const mockPush = vi.fn();
const mockRouter = {
push: mockPush,
replace: vi.fn(),
refresh: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
prefetch: vi.fn(),
};
const mockProject1 = {
id: "proj-1",
name: "Test Project 1",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-01"),
organizationId: "org-1",
languages: [],
} as unknown as TProject;
const mockProject2 = {
id: "proj-2",
name: "Test Project 2",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-01"),
organizationId: "org-1",
languages: [],
} as unknown as TProject;
const mockProjects = [mockProject1, mockProject2];
const mockOrganization: TOrganization = {
id: "org-1",
name: "Test Organization",
createdAt: new Date("2023-01-01"),
updatedAt: new Date("2023-01-01"),
billing: {
plan: "free",
stripeCustomerId: null,
} as unknown as TOrganizationBilling,
isAIEnabled: false,
};
const defaultProps = {
currentProjectId: "proj-1",
currentOrganizationId: "org-1",
projects: mockProjects,
isOwnerOrManager: true,
organizationProjectsLimit: 3,
isFormbricksCloud: true,
isLicenseActive: false,
currentEnvironmentId: "env-123",
isAccessControlAllowed: true,
isEnvironmentBreadcrumbVisible: true,
};
beforeEach(() => {
vi.mocked(useRouter).mockReturnValue(mockRouter as any);
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe("Basic Rendering", () => {
test("renders project breadcrumb correctly", () => {
render(<ProjectBreadcrumb {...defaultProps} />);
expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument();
expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument();
expect(screen.getByTestId("folder-open-icon")).toBeInTheDocument();
expect(screen.getAllByText("Test Project 1")).toHaveLength(2); // trigger + dropdown option
});
test("shows chevron icons correctly", () => {
render(<ProjectBreadcrumb {...defaultProps} />);
// Should show chevron right when closed
expect(screen.getByTestId("chevron-right-icon")).toBeInTheDocument();
expect(screen.queryByTestId("chevron-down-icon")).not.toBeInTheDocument();
});
test("shows chevron down when dropdown is open", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
await waitFor(() => {
expect(screen.getByTestId("chevron-down-icon")).toBeInTheDocument();
});
});
});
describe("Project Selection", () => {
test("renders dropdown content with project options", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
expect(screen.getByTestId("dropdown-content")).toBeInTheDocument();
expect(screen.getByText("common.choose_project")).toBeInTheDocument();
expect(screen.getAllByTestId("dropdown-group")).toHaveLength(2); // Projects group and settings group
});
test("renders all project options in dropdown", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item");
// Find project options (excluding the add new project option)
const projectOptions = checkboxItems.filter((item) => item.textContent?.includes("Test Project"));
expect(projectOptions).toHaveLength(2);
// Check current project is marked as selected
const currentProjectOption = checkboxItems.find((item) => item.textContent?.includes("Test Project 1"));
expect(currentProjectOption).toHaveAttribute("data-checked", "true");
// Check other project is not selected
const otherProjectOption = checkboxItems.find((item) => item.textContent?.includes("Test Project 2"));
expect(otherProjectOption).toHaveAttribute("data-checked", "false");
});
test("handles project change when clicking dropdown option", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const checkboxItems = screen.getAllByTestId("dropdown-checkbox-item");
const project2Option = checkboxItems.find((item) => item.textContent?.includes("Test Project 2"));
expect(project2Option).toBeInTheDocument();
await user.click(project2Option!);
expect(mockPush).toHaveBeenCalledWith("/projects/proj-2/");
});
});
describe("Add New Project", () => {
test("shows add new project option when user is owner or manager", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
expect(screen.getByText("common.add_new_project")).toBeInTheDocument();
expect(screen.getByTestId("plus-icon")).toBeInTheDocument();
});
test("hides add new project option when user is not owner or manager", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} isOwnerOrManager={false} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
expect(screen.queryByText("common.add_new_project")).not.toBeInTheDocument();
});
test("opens create project modal when within project limit", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const addProjectOption = screen.getByText("common.add_new_project");
await user.click(addProjectOption);
expect(screen.getByTestId("create-project-modal")).toBeInTheDocument();
expect(screen.getByText("Organization: org-1")).toBeInTheDocument();
expect(screen.getByText("Access Control: Allowed")).toBeInTheDocument();
});
test("opens limit modal when exceeding project limit", async () => {
const user = userEvent.setup();
const props = {
...defaultProps,
projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }],
organizationProjectsLimit: 3,
};
render(<ProjectBreadcrumb {...props} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const addProjectOption = screen.getByText("common.add_new_project");
await user.click(addProjectOption);
expect(screen.getByTestId("project-limit-modal")).toBeInTheDocument();
expect(screen.getByText("Project Limit: 3")).toBeInTheDocument();
});
});
describe("Project Limit Modal", () => {
test("shows correct buttons for Formbricks Cloud with non-enterprise plan", async () => {
const user = userEvent.setup();
const props = {
...defaultProps,
projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }],
organizationProjectsLimit: 3,
isFormbricksCloud: true,
isEnvironmentBreadcrumbVisible: true,
currentOrganization: {
...mockOrganization,
billing: { ...mockOrganization.billing, plan: "startup" } as unknown as TOrganizationBilling,
},
};
render(<ProjectBreadcrumb {...props} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const addProjectOption = screen.getByText("common.add_new_project");
await user.click(addProjectOption);
expect(screen.getByText("environments.settings.billing.upgrade")).toBeInTheDocument();
expect(screen.getByText("common.cancel")).toBeInTheDocument();
});
test("shows correct buttons for self-hosted with active license", async () => {
const user = userEvent.setup();
const props = {
...defaultProps,
projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }],
organizationProjectsLimit: 3,
isFormbricksCloud: false,
isLicenseActive: true,
isEnvironmentBreadcrumbVisible: true,
};
render(<ProjectBreadcrumb {...props} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const addProjectOption = screen.getByText("common.add_new_project");
await user.click(addProjectOption);
expect(screen.getByText("environments.settings.billing.upgrade")).toBeInTheDocument();
expect(screen.getByText("common.cancel")).toBeInTheDocument();
});
test("closes limit modal correctly", async () => {
const user = userEvent.setup();
const props = {
...defaultProps,
projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }],
organizationProjectsLimit: 3,
};
render(<ProjectBreadcrumb {...props} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const addProjectOption = screen.getByText("common.add_new_project");
await user.click(addProjectOption);
expect(screen.getByTestId("project-limit-modal")).toBeInTheDocument();
const closeButton = screen.getByText("Close Limit Modal");
await user.click(closeButton);
expect(screen.queryByTestId("project-limit-modal")).not.toBeInTheDocument();
});
});
describe("Create Project Modal", () => {
test("closes create project modal correctly", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const addProjectOption = screen.getByText("common.add_new_project");
await user.click(addProjectOption);
expect(screen.getByTestId("create-project-modal")).toBeInTheDocument();
const closeButton = screen.getByText("Close Create Modal");
await user.click(closeButton);
expect(screen.queryByTestId("create-project-modal")).not.toBeInTheDocument();
});
test("passes correct props to create project modal", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} isAccessControlAllowed={false} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const addProjectOption = screen.getByText("common.add_new_project");
await user.click(addProjectOption);
expect(screen.getByText("Access Control: Not Allowed")).toBeInTheDocument();
});
});
describe("Edge Cases", () => {
test("handles single project scenario", () => {
render(<ProjectBreadcrumb {...defaultProps} projects={[mockProject1]} />);
expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument();
expect(screen.getAllByText("Test Project 1")).toHaveLength(2); // trigger + dropdown option
});
test("sets breadcrumb item as active when dropdown is open", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} />);
// Initially not active
let breadcrumbItem = screen.getByTestId("breadcrumb-item");
expect(breadcrumbItem).toHaveAttribute("data-active", "false");
// Open dropdown
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
// Should be active when dropdown is open
breadcrumbItem = screen.getByTestId("breadcrumb-item");
expect(breadcrumbItem).toHaveAttribute("data-active", "true");
});
test("handles project limit of zero", async () => {
const user = userEvent.setup();
render(<ProjectBreadcrumb {...defaultProps} organizationProjectsLimit={0} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const addProjectOption = screen.getByText("common.add_new_project");
await user.click(addProjectOption);
// Should show limit modal even with 0 projects when limit is 0
expect(screen.getByTestId("project-limit-modal")).toBeInTheDocument();
expect(screen.getByText("Project Limit: 0")).toBeInTheDocument();
});
test("handles enterprise plan on Formbricks Cloud", async () => {
const user = userEvent.setup();
const props = {
...defaultProps,
projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }],
organizationProjectsLimit: 3,
currentOrganization: {
...mockOrganization,
billing: { ...mockOrganization.billing, plan: "enterprise" } as unknown as TOrganizationBilling,
},
};
render(<ProjectBreadcrumb {...props} />);
const dropdownMenu = screen.getByTestId("dropdown-menu");
await user.click(dropdownMenu);
const addProjectOption = screen.getByText("common.add_new_project");
await user.click(addProjectOption);
// Should show self-hosted style buttons for enterprise plan
expect(screen.getByTestId("project-limit-modal")).toBeInTheDocument();
});
});
});

View File

@@ -1,157 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TProject } from "@formbricks/types/project";
import { EnvironmentContextWrapper, useEnvironment } from "./environment-context";
// Mock environment data
const mockEnvironment: TEnvironment = {
id: "test-env-id",
createdAt: new Date(),
updatedAt: new Date(),
type: "development",
projectId: "test-project-id",
appSetupCompleted: true,
};
// Mock project data
const mockProject = {
id: "test-project-id",
createdAt: new Date(),
updatedAt: new Date(),
organizationId: "test-org-id",
config: {
channel: "app",
industry: "saas",
},
linkSurveyBranding: true,
styling: {
allowStyleOverwrite: true,
brandColor: {
light: "#ffffff",
dark: "#000000",
},
questionColor: {
light: "#000000",
dark: "#ffffff",
},
inputColor: {
light: "#000000",
dark: "#ffffff",
},
inputBorderColor: {
light: "#cccccc",
dark: "#444444",
},
cardBackgroundColor: {
light: "#ffffff",
dark: "#000000",
},
cardBorderColor: {
light: "#cccccc",
dark: "#444444",
},
isDarkModeEnabled: false,
isLogoHidden: false,
hideProgressBar: false,
roundness: 8,
cardArrangement: {
linkSurveys: "casual",
appSurveys: "casual",
},
},
recontactDays: 30,
inAppSurveyBranding: true,
logo: {
url: "test-logo.png",
bgColor: "#ffffff",
},
placement: "bottomRight",
clickOutsideClose: true,
} as TProject;
// Test component that uses the hook
const TestComponent = () => {
const { environment, project } = useEnvironment();
return (
<div>
<div data-testid="environment-id">{environment.id}</div>
<div data-testid="environment-type">{environment.type}</div>
<div data-testid="project-id">{project.id}</div>
<div data-testid="project-organization-id">{project.organizationId}</div>
</div>
);
};
describe("EnvironmentContext", () => {
afterEach(() => {
cleanup();
});
test("provides environment and project data to child components", () => {
render(
<EnvironmentContextWrapper environment={mockEnvironment} project={mockProject}>
<TestComponent />
</EnvironmentContextWrapper>
);
expect(screen.getByTestId("environment-id")).toHaveTextContent("test-env-id");
expect(screen.getByTestId("environment-type")).toHaveTextContent("development");
expect(screen.getByTestId("project-id")).toHaveTextContent("test-project-id");
expect(screen.getByTestId("project-organization-id")).toHaveTextContent("test-org-id");
});
test("throws error when useEnvironment is used outside of provider", () => {
const TestComponentWithoutProvider = () => {
useEnvironment();
return <div>Should not render</div>;
};
expect(() => {
render(<TestComponentWithoutProvider />);
}).toThrow("useEnvironment must be used within an EnvironmentProvider");
});
test("updates context value when environment or project changes", () => {
const { rerender } = render(
<EnvironmentContextWrapper environment={mockEnvironment} project={mockProject}>
<TestComponent />
</EnvironmentContextWrapper>
);
expect(screen.getByTestId("environment-type")).toHaveTextContent("development");
const updatedEnvironment = {
...mockEnvironment,
type: "production" as const,
};
rerender(
<EnvironmentContextWrapper environment={updatedEnvironment} project={mockProject}>
<TestComponent />
</EnvironmentContextWrapper>
);
expect(screen.getByTestId("environment-type")).toHaveTextContent("production");
});
test("memoizes context value correctly", () => {
const { rerender } = render(
<EnvironmentContextWrapper environment={mockEnvironment} project={mockProject}>
<TestComponent />
</EnvironmentContextWrapper>
);
// Re-render with same props
rerender(
<EnvironmentContextWrapper environment={mockEnvironment} project={mockProject}>
<TestComponent />
</EnvironmentContextWrapper>
);
// Should still work correctly
expect(screen.getByTestId("environment-id")).toHaveTextContent("test-env-id");
expect(screen.getByTestId("project-id")).toHaveTextContent("test-project-id");
});
});

View File

@@ -1,327 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import { Session } from "next-auth";
import { redirect } from "next/navigation";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TMembership } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { TProject } from "@formbricks/types/project";
import { TUser } from "@formbricks/types/user";
import { getEnvironment } from "@/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
import EnvLayout from "./layout";
// Mock sub-components to render identifiable elements
vi.mock("@/app/(app)/environments/[environmentId]/components/EnvironmentLayout", () => ({
EnvironmentLayout: ({ children, environmentId, session }: any) => (
<div data-testid="EnvironmentLayout" data-environment-id={environmentId} data-session={session?.user?.id}>
{children}
</div>
),
}));
vi.mock("@/modules/ui/components/environmentId-base-layout", () => ({
EnvironmentIdBaseLayout: ({ children, environmentId, session, user, organization }: any) => (
<div
data-testid="EnvironmentIdBaseLayout"
data-environment-id={environmentId}
data-session={session?.user?.id}
data-user={user?.id}
data-organization={organization?.id}>
{children}
</div>
),
}));
vi.mock("@/modules/ui/components/toaster-client", () => ({
ToasterClient: () => <div data-testid="ToasterClient" />,
}));
vi.mock("./components/EnvironmentStorageHandler", () => ({
default: ({ environmentId }: any) => (
<div data-testid="EnvironmentStorageHandler" data-environment-id={environmentId} />
),
}));
vi.mock("@/app/(app)/environments/[environmentId]/context/environment-context", () => ({
EnvironmentContextWrapper: ({ children, environment, project }: any) => (
<div
data-testid="EnvironmentContextWrapper"
data-environment-id={environment?.id}
data-project-id={project?.id}>
{children}
</div>
),
}));
// Mock navigation
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
// Mocks for dependencies
vi.mock("@/modules/environments/lib/utils", () => ({
environmentIdLayoutChecks: vi.fn(),
}));
vi.mock("@/lib/project/service", () => ({
getProjectByEnvironmentId: vi.fn(),
}));
vi.mock("@/lib/environment/service", () => ({
getEnvironment: vi.fn(),
}));
vi.mock("@/lib/membership/service", () => ({
getMembershipByUserIdOrganizationId: vi.fn(),
}));
describe("EnvLayout", () => {
const mockSession = { user: { id: "user1" } } as Session;
const mockUser = { id: "user1", email: "user1@example.com" } as TUser;
const mockOrganization = { id: "org1", name: "Org1", billing: {} } as TOrganization;
const mockProject = { id: "proj1", name: "Test Project" } as TProject;
const mockEnvironment = { id: "env1", type: "production" } as TEnvironment;
const mockMembership = {
id: "member1",
role: "owner",
organizationId: "org1",
userId: "user1",
accepted: true,
} as TMembership;
const mockTranslation = ((key: string) => key) as any;
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders successfully when all dependencies return valid data", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: mockTranslation,
session: mockSession,
user: mockUser,
organization: mockOrganization,
});
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment);
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(mockMembership);
const result = await EnvLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div data-testid="child">Content</div>,
});
render(result);
// Verify main layout structure
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toBeInTheDocument();
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-environment-id", "env1");
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-session", "user1");
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-user", "user1");
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-organization", "org1");
// Verify environment storage handler
expect(screen.getByTestId("EnvironmentStorageHandler")).toBeInTheDocument();
expect(screen.getByTestId("EnvironmentStorageHandler")).toHaveAttribute("data-environment-id", "env1");
// Verify context wrapper
expect(screen.getByTestId("EnvironmentContextWrapper")).toBeInTheDocument();
expect(screen.getByTestId("EnvironmentContextWrapper")).toHaveAttribute("data-environment-id", "env1");
expect(screen.getByTestId("EnvironmentContextWrapper")).toHaveAttribute("data-project-id", "proj1");
// Verify environment layout
expect(screen.getByTestId("EnvironmentLayout")).toBeInTheDocument();
expect(screen.getByTestId("EnvironmentLayout")).toHaveAttribute("data-environment-id", "env1");
expect(screen.getByTestId("EnvironmentLayout")).toHaveAttribute("data-session", "user1");
// Verify children are rendered
expect(screen.getByTestId("child")).toHaveTextContent("Content");
// Verify all services were called with correct parameters
expect(environmentIdLayoutChecks).toHaveBeenCalledWith("env1");
expect(getProjectByEnvironmentId).toHaveBeenCalledWith("env1");
expect(getEnvironment).toHaveBeenCalledWith("env1");
expect(getMembershipByUserIdOrganizationId).toHaveBeenCalledWith("user1", "org1");
});
test("redirects when session is null", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: mockTranslation,
session: null as unknown as Session,
user: mockUser,
organization: mockOrganization,
});
vi.mocked(redirect).mockImplementationOnce(() => {
throw new Error("Redirect called");
});
await expect(
EnvLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div>Content</div>,
})
).rejects.toThrow("Redirect called");
expect(redirect).toHaveBeenCalledWith("/auth/login");
});
test("throws error if user is null", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: mockTranslation,
session: mockSession,
user: null as unknown as TUser,
organization: mockOrganization,
});
await expect(
EnvLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div>Content</div>,
})
).rejects.toThrow("common.user_not_found");
// Verify redirect was not called
expect(redirect).not.toHaveBeenCalled();
});
test("throws error if project is not found", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: mockTranslation,
session: mockSession,
user: mockUser,
organization: mockOrganization,
});
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(null);
vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment);
await expect(
EnvLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div>Content</div>,
})
).rejects.toThrow("common.project_not_found");
// Verify both project and environment were called in Promise.all
expect(getProjectByEnvironmentId).toHaveBeenCalledWith("env1");
expect(getEnvironment).toHaveBeenCalledWith("env1");
});
test("throws error if environment is not found", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: mockTranslation,
session: mockSession,
user: mockUser,
organization: mockOrganization,
});
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
await expect(
EnvLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div>Content</div>,
})
).rejects.toThrow("common.environment_not_found");
// Verify both project and environment were called in Promise.all
expect(getProjectByEnvironmentId).toHaveBeenCalledWith("env1");
expect(getEnvironment).toHaveBeenCalledWith("env1");
});
test("throws error if membership is not found", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: mockTranslation,
session: mockSession,
user: mockUser,
organization: mockOrganization,
});
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment);
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(null);
await expect(
EnvLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div>Content</div>,
})
).rejects.toThrow("common.membership_not_found");
expect(getMembershipByUserIdOrganizationId).toHaveBeenCalledWith("user1", "org1");
});
test("handles Promise.all correctly for project and environment", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: mockTranslation,
session: mockSession,
user: mockUser,
organization: mockOrganization,
});
// Mock Promise.all to verify it's called correctly
const getProjectSpy = vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
const getEnvironmentSpy = vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment);
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(mockMembership);
const result = await EnvLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div data-testid="child">Content</div>,
});
render(result);
// Verify both calls were made
expect(getProjectSpy).toHaveBeenCalledWith("env1");
expect(getEnvironmentSpy).toHaveBeenCalledWith("env1");
// Verify successful rendering
expect(screen.getByTestId("child")).toBeInTheDocument();
});
test("handles different environment types correctly", async () => {
const developmentEnvironment = { id: "env1", type: "development" } as TEnvironment;
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: mockTranslation,
session: mockSession,
user: mockUser,
organization: mockOrganization,
});
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
vi.mocked(getEnvironment).mockResolvedValueOnce(developmentEnvironment);
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(mockMembership);
const result = await EnvLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div data-testid="child">Content</div>,
});
render(result);
// Verify context wrapper receives the development environment
expect(screen.getByTestId("EnvironmentContextWrapper")).toHaveAttribute("data-environment-id", "env1");
expect(screen.getByTestId("child")).toBeInTheDocument();
});
test("handles different user roles correctly", async () => {
const memberMembership = {
id: "member1",
role: "member",
organizationId: "org1",
userId: "user1",
accepted: true,
} as TMembership;
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: mockTranslation,
session: mockSession,
user: mockUser,
organization: mockOrganization,
});
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment);
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(memberMembership);
const result = await EnvLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div data-testid="child">Content</div>,
});
render(result);
// Verify successful rendering with member role
expect(screen.getByTestId("child")).toBeInTheDocument();
expect(getMembershipByUserIdOrganizationId).toHaveBeenCalledWith("user1", "org1");
});
});

View File

@@ -1,141 +0,0 @@
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 { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
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(),
}));
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: true,
}));
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",
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`);
});
});

View File

@@ -1,15 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { AppConnectionLoading as OriginalAppConnectionLoading } from "@/modules/projects/settings/(setup)/app-connection/loading";
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);
});
});

View File

@@ -1,42 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { AppConnectionPage as OriginalAppConnectionPage } from "@/modules/projects/settings/(setup)/app-connection/page";
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",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: true,
}));
vi.mock("@/lib/env", () => ({
env: {
PUBLIC_URL: "https://example.com",
},
}));
describe("AppConnectionPage Re-export", () => {
test("should re-export AppConnectionPage correctly", () => {
expect(AppConnectionPage).toBe(OriginalAppConnectionPage);
});
});

View File

@@ -1,17 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { GeneralSettingsLoading as OriginalGeneralSettingsLoading } from "@/modules/projects/settings/general/loading";
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);
});
});

View File

@@ -1,42 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { GeneralSettingsPage } from "@/modules/projects/settings/general/page";
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",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: 1,
}));
vi.mock("@/lib/env", () => ({
env: {
PUBLIC_URL: "https://public-domain.com",
},
}));
describe("GeneralSettingsPage re-export", () => {
test("should re-export GeneralSettingsPage component", () => {
expect(Page).toBe(GeneralSettingsPage);
});
});

View File

@@ -1,466 +0,0 @@
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 { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
import { fetchTables } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable";
import { AddIntegrationModal } from "./AddIntegrationModal";
// Mock dependencies
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
createOrUpdateIntegrationAction: vi.fn(),
}));
vi.mock(
"@/app/(app)/environments/[environmentId]/project/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]/project/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/dialog", () => ({
Dialog: ({ children, open, onOpenChange }: any) =>
open ? (
<div data-testid="dialog" role="dialog">
{children}
<button onClick={() => onOpenChange(false)}>Close Dialog</button>
</div>
) : null,
DialogContent: ({ children, ...props }: any) => (
<div data-testid="dialog-content" {...props}>
{children}
</div>
),
DialogHeader: ({ children }: any) => <div data-testid="dialog-header">{children}</div>,
DialogTitle: ({ children }: any) => <h2 data-testid="dialog-title">{children}</h2>,
DialogDescription: ({ children }: any) => <p data-testid="dialog-description">{children}</p>,
DialogBody: ({ children }: any) => <div data-testid="dialog-body">{children}</div>,
DialogFooter: ({ children }: any) => <div data-testid="dialog-footer">{children}</div>,
}));
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);
});
});

View File

@@ -1,134 +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 { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable";
import { AirtableWrapper } from "./AirtableWrapper";
// Mock child components
vi.mock(
"@/app/(app)/environments/[environmentId]/project/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]/project/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();
});
});

View File

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

View File

@@ -1,151 +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 { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationAirtable, TIntegrationAirtableConfig } from "@formbricks/types/integration/airtable";
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
import { ManageIntegration } from "./ManageIntegration";
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
deleteIntegrationAction: vi.fn(),
}));
vi.mock(
"@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AddIntegrationModal",
() => ({
AddIntegrationModal: ({ open, setOpenWithStates }) =>
open ? (
<div data-testid="add-modal">
<button onClick={() => setOpenWithStates(false)}>close</button>
</div>
) : null,
})
);
vi.mock("@/modules/ui/components/delete-dialog", () => ({
DeleteDialog: ({ open, setOpen, onDelete }) =>
open ? (
<div data-testid="delete-dialog">
<button onClick={onDelete}>confirm</button>
<button onClick={() => setOpen(false)}>cancel</button>
</div>
) : null,
}));
vi.mock("react-hot-toast", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
const baseProps = {
environment: { id: "env1" } as TEnvironment,
environmentId: "env1",
setIsConnected: vi.fn(),
surveys: [],
airtableArray: [],
locale: "en-US" as const,
};
describe("ManageIntegration", () => {
afterEach(() => {
cleanup();
});
test("empty state", () => {
render(
<ManageIntegration
{...baseProps}
airtableIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
} as TIntegrationAirtable
}
/>
);
expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument();
expect(screen.getByText(/link_new_table/)).toBeInTheDocument();
});
test("open add modal", async () => {
render(
<ManageIntegration
{...baseProps}
airtableIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
} as TIntegrationAirtable
}
/>
);
await userEvent.click(screen.getByText(/link_new_table/));
expect(screen.getByTestId("add-modal")).toBeInTheDocument();
});
test("list integrations and open edit modal", async () => {
const item = {
baseId: "b",
tableId: "t",
surveyId: "s",
surveyName: "S",
tableName: "T",
questions: "Q",
questionIds: ["x"],
createdAt: new Date(),
includeVariables: false,
includeHiddenFields: false,
includeMetadata: false,
includeCreatedAt: false,
};
render(
<ManageIntegration
{...baseProps}
airtableIntegration={
{
id: "1",
config: { email: "a@b.com", data: [item] } as unknown as TIntegrationAirtableConfig,
} as TIntegrationAirtable
}
/>
);
expect(screen.getByText("S")).toBeInTheDocument();
await userEvent.click(screen.getByText("S"));
expect(screen.getByTestId("add-modal")).toBeInTheDocument();
});
test("delete integration success", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
render(
<ManageIntegration
{...baseProps}
airtableIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
} as TIntegrationAirtable
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
await userEvent.click(screen.getByText("confirm"));
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
const { toast } = await import("react-hot-toast");
expect(toast.success).toHaveBeenCalled();
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
});
test("delete integration error", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
render(
<ManageIntegration
{...baseProps}
airtableIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
} as TIntegrationAirtable
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
await userEvent.click(screen.getByText("confirm"));
const { toast } = await import("react-hot-toast");
expect(toast.error).toHaveBeenCalled();
});
});

View File

@@ -1,223 +0,0 @@
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 { getSurveys } from "@/app/(app)/environments/[environmentId]/project/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 Page from "./page";
// Mock dependencies
vi.mock(
"@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper",
() => ({
AirtableWrapper: vi.fn(() => <div>AirtableWrapper Mock</div>),
})
);
vi.mock("@/app/(app)/environments/[environmentId]/project/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",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: true,
}));
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]/project/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]/project/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]/project/integrations/airtable/components/AirtableWrapper"
)
).AirtableWrapper
);
expect(AirtableWrapper).toHaveBeenCalledWith(
expect.objectContaining({
isEnabled: false, // Should be false
}),
undefined
);
});
});

View File

@@ -1,702 +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 {
TIntegrationGoogleSheets,
TIntegrationGoogleSheetsConfigData,
} from "@formbricks/types/integration/google-sheet";
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/AddIntegrationModal";
// Mock actions and utilities
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
createOrUpdateIntegrationAction: vi.fn(),
}));
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/actions", () => ({
getSpreadsheetNameByIdAction: vi.fn(),
}));
vi.mock("@/app/(app)/environments/[environmentId]/project/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/dialog", () => ({
Dialog: ({ children, open, onOpenChange }: any) =>
open ? (
<div data-testid="dialog" role="dialog">
{children}
<button onClick={() => onOpenChange(false)}>Close Dialog</button>
</div>
) : null,
DialogContent: ({ children, ...props }: any) => (
<div data-testid="dialog-content" {...props}>
{children}
</div>
),
DialogHeader: ({ children }: any) => <div data-testid="dialog-header">{children}</div>,
DialogTitle: ({ children }: any) => <h2 data-testid="dialog-title">{children}</h2>,
DialogDescription: ({ children }: any) => <p data-testid="dialog-description">{children}</p>,
DialogBody: ({ children }: any) => <div data-testid="dialog-body">{children}</div>,
DialogFooter: ({ children }: any) => <div data-testid="dialog-footer">{children}</div>,
}));
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]/project/integrations/actions"))
.createOrUpdateIntegrationAction
);
const getSpreadsheetNameByIdAction = vi.mocked(
(await import("@/app/(app)/environments/[environmentId]/project/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,
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,
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,
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,
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("dialog")).toBeInTheDocument();
expect(screen.getByTestId("dialog-title")).toHaveTextContent("Link Google Sheet");
expect(screen.getByTestId("dialog-description")).toHaveTextContent("Sync responses with Google Sheets.");
// 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("dialog")).toBeInTheDocument();
expect(screen.getByTestId("dialog-title")).toHaveTextContent("Link Google Sheet");
expect(screen.getByTestId("dialog-description")).toHaveTextContent("Sync responses with Google Sheets.");
// 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("");
});
});

View File

@@ -1,175 +0,0 @@
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 {
TIntegrationGoogleSheets,
TIntegrationGoogleSheetsCredential,
} from "@formbricks/types/integration/google-sheet";
import { TSurvey } from "@formbricks/types/surveys/types";
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/GoogleSheetWrapper";
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/google";
// Mock child components and functions
vi.mock(
"@/app/(app)/environments/[environmentId]/project/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]/project/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]/project/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 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();
});
});

View File

@@ -1,162 +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 { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
import { ManageIntegration } from "./ManageIntegration";
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
deleteIntegrationAction: vi.fn(),
}));
vi.mock("react-hot-toast", () => ({
default: { success: vi.fn(), error: vi.fn() },
}));
vi.mock("@/modules/ui/components/delete-dialog", () => ({
DeleteDialog: ({ open, setOpen, onDelete }: any) =>
open ? (
<div data-testid="delete-dialog">
<button onClick={onDelete}>confirm</button>
<button onClick={() => setOpen(false)}>cancel</button>
</div>
) : null,
}));
vi.mock("@/modules/ui/components/empty-space-filler", () => ({
EmptySpaceFiller: ({ emptyMessage }: any) => <div>{emptyMessage}</div>,
}));
const baseProps = {
environment: { id: "env1" } as TEnvironment,
setOpenAddIntegrationModal: vi.fn(),
setIsConnected: vi.fn(),
setSelectedIntegration: vi.fn(),
locale: "en-US" as const,
} as const;
describe("ManageIntegration (Google Sheets)", () => {
afterEach(() => {
cleanup();
});
test("empty state", () => {
render(
<ManageIntegration
{...baseProps}
googleSheetIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] },
} as unknown as TIntegrationGoogleSheets
}
/>
);
expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument();
expect(screen.getByText(/link_new_sheet/)).toBeInTheDocument();
});
test("click link new sheet", async () => {
render(
<ManageIntegration
{...baseProps}
googleSheetIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] },
} as unknown as TIntegrationGoogleSheets
}
/>
);
await userEvent.click(screen.getByText(/link_new_sheet/));
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null);
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
});
test("list integrations and open edit", async () => {
const item = {
spreadsheetId: "sid",
spreadsheetName: "SheetName",
surveyId: "s1",
surveyName: "Survey1",
questionIds: ["q1"],
questions: "Q",
createdAt: new Date(),
};
render(
<ManageIntegration
{...baseProps}
googleSheetIntegration={
{
id: "1",
config: { email: "a@b.com", data: [item] },
} as unknown as TIntegrationGoogleSheets
}
/>
);
expect(screen.getByText("Survey1")).toBeInTheDocument();
await userEvent.click(screen.getByText("Survey1"));
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({
...item,
index: 0,
});
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
});
test("delete integration success", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
render(
<ManageIntegration
{...baseProps}
googleSheetIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] },
} as unknown as TIntegrationGoogleSheets
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
await userEvent.click(screen.getByText("confirm"));
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
const { default: toast } = await import("react-hot-toast");
expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully");
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
});
test("delete integration error", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
render(
<ManageIntegration
{...baseProps}
googleSheetIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] },
} as unknown as TIntegrationGoogleSheets
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
await userEvent.click(screen.getByText("confirm"));
const { default: toast } = await import("react-hot-toast");
expect(toast.error).toHaveBeenCalledWith(expect.any(String));
});
});

View File

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

View File

@@ -1,225 +0,0 @@
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";
import Page from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/page";
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/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";
// Mock dependencies
vi.mock(
"@/app/(app)/environments/[environmentId]/project/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]/project/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,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,
languages: [],
pin: null,
segment: null,
singleUse: null,
styling: null,
surveyClosedMessage: null,
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
autoComplete: 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}/project/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]/project/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
});
});

View File

@@ -1,630 +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 {
TIntegrationNotion,
TIntegrationNotionConfigData,
TIntegrationNotionCredential,
TIntegrationNotionDatabase,
} from "@formbricks/types/integration/notion";
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/components/AddIntegrationModal";
// Mock actions and utilities
vi.mock("@/app/(app)/environments/[environmentId]/project/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/dialog", () => ({
Dialog: ({ open, children }: { open: boolean; children: React.ReactNode }) =>
open ? <div data-testid="dialog">{children}</div> : null,
DialogContent: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="dialog-content" className={className}>
{children}
</div>
),
DialogHeader: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="dialog-header" className={className}>
{children}
</div>
),
DialogDescription: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<p data-testid="dialog-description" className={className}>
{children}
</p>
),
DialogTitle: ({ children }: { children: React.ReactNode }) => (
<h2 data-testid="dialog-title">{children}</h2>
),
DialogBody: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="dialog-body" className={className}>
{children}
</div>
),
DialogFooter: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="dialog-footer" className={className}>
{children}
</div>
),
}));
vi.mock("lucide-react", () => ({
PlusIcon: () => <span data-testid="plus-icon">+</span>,
TrashIcon: () => <span data-testid="trash-icon">🗑</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]/project/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,
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,
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,
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,
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("dialog")).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("dialog")).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("trash-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 trashButton = screen.getAllByTestId("trash-icon")[0]; // Get the first trash button
await userEvent.click(trashButton);
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
});
});

View File

@@ -1,93 +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 type {
TIntegrationNotion,
TIntegrationNotionConfig,
TIntegrationNotionConfigData,
TIntegrationNotionCredential,
} from "@formbricks/types/integration/notion";
import { ManageIntegration } from "./ManageIntegration";
vi.mock("react-hot-toast", () => ({
default: { success: vi.fn(), error: vi.fn() },
}));
vi.mock("@/lib/time", () => ({ timeSince: () => "ago" }));
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
deleteIntegrationAction: vi.fn(),
}));
describe("ManageIntegration", () => {
afterEach(() => {
cleanup();
});
const defaultProps = {
environment: {} as any,
locale: "en-US" as const,
setOpenAddIntegrationModal: vi.fn(),
setIsConnected: vi.fn(),
setSelectedIntegration: vi.fn(),
handleNotionAuthorization: vi.fn(),
};
test("shows empty state when no databases", () => {
render(
<ManageIntegration
{...defaultProps}
notionIntegration={
{
id: "1",
config: {
data: [] as TIntegrationNotionConfigData[],
key: { workspace_name: "ws" } as TIntegrationNotionCredential,
} as TIntegrationNotionConfig,
} as TIntegrationNotion
}
/>
);
expect(screen.getByText("environments.integrations.notion.no_databases_found")).toBeInTheDocument();
});
test("renders list and handles clicks", async () => {
const data = [
{ surveyName: "S", databaseName: "D", createdAt: new Date().toISOString(), databaseId: "db" },
] as unknown as TIntegrationNotionConfigData[];
render(
<ManageIntegration
{...defaultProps}
notionIntegration={
{
id: "1",
config: { data, key: { workspace_name: "ws" } as TIntegrationNotionCredential },
} as TIntegrationNotion
}
/>
);
expect(screen.getByText("S")).toBeInTheDocument();
await userEvent.click(screen.getByText("S"));
expect(defaultProps.setSelectedIntegration).toHaveBeenCalledWith({ ...data[0], index: 0 });
expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled();
});
test("update and link new buttons invoke handlers", async () => {
render(
<ManageIntegration
{...defaultProps}
notionIntegration={
{
id: "1",
config: {
data: [],
key: { workspace_name: "ws" } as TIntegrationNotionCredential,
} as TIntegrationNotionConfig,
} as TIntegrationNotion
}
/>
);
await userEvent.click(screen.getByText("environments.integrations.notion.update_connection"));
expect(defaultProps.handleNotionAuthorization).toHaveBeenCalled();
await userEvent.click(screen.getByText("environments.integrations.notion.link_new_database"));
expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled();
});
});

View File

@@ -1,155 +0,0 @@
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 { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/lib/notion";
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",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: true,
}));
// 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]/project/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.mockImplementation(() => Promise.resolve(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);
});
});
});

View File

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

View File

@@ -1,248 +0,0 @@
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";
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
import Page from "@/app/(app)/environments/[environmentId]/project/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";
// Mock dependencies
vi.mock(
"@/app/(app)/environments/[environmentId]/project/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]/project/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,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,
languages: [],
pin: null,
segment: null,
singleUse: null,
styling: null,
surveyClosedMessage: null,
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
autoComplete: 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("./");
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();
});
});

View File

@@ -1,241 +0,0 @@
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";
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/webhook";
import Page from "@/app/(app)/environments/[environmentId]/project/integrations/page";
import { getIntegrations } from "@/lib/integration/service";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
// Mock dependencies
vi.mock("@/app/(app)/environments/[environmentId]/project/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.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 appSetupCompleted 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");
});
});

View File

@@ -1,761 +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 { TIntegrationItem } from "@formbricks/types/integration";
import {
TIntegrationSlack,
TIntegrationSlackConfigData,
TIntegrationSlackCredential,
} from "@formbricks/types/integration/slack";
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
import { AddChannelMappingModal } from "./AddChannelMappingModal";
// Mock dependencies
vi.mock("@/app/(app)/environments/[environmentId]/project/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/dialog", () => ({
Dialog: ({ children, open, onOpenChange }: any) =>
open ? (
<div data-testid="dialog" role="dialog">
{children}
<button onClick={() => onOpenChange(false)}>Close Dialog</button>
</div>
) : null,
DialogContent: ({ children, ...props }: any) => (
<div data-testid="dialog-content" {...props}>
{children}
</div>
),
DialogHeader: ({ children }: any) => <div data-testid="dialog-header">{children}</div>,
DialogTitle: ({ children }: any) => <h2 data-testid="dialog-title">{children}</h2>,
DialogDescription: ({ children }: any) => <p data-testid="dialog-description">{children}</p>,
DialogBody: ({ children }: any) => <div data-testid="dialog-body">{children}</div>,
DialogFooter: ({ children }: any) => <div data-testid="dialog-footer">{children}</div>,
}));
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 === "environments.integrations.slack.slack_integration_description")
return "Send responses directly to Slack.";
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,
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,
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,
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,
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("dialog")).toBeInTheDocument();
expect(screen.getByTestId("dialog-title")).toHaveTextContent("Link Slack Channel");
expect(screen.getByTestId("dialog-description")).toHaveTextContent("Send responses directly to Slack.");
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("dialog")).toBeInTheDocument();
expect(screen.getByTestId("dialog-title")).toHaveTextContent("Link Slack Channel");
expect(screen.getByTestId("dialog-description")).toHaveTextContent("Send responses directly to Slack.");
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();
});
});

View File

@@ -1,158 +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 { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
import { ManageIntegration } from "./ManageIntegration";
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
deleteIntegrationAction: vi.fn(),
}));
vi.mock("react-hot-toast", () => ({ default: { success: vi.fn(), error: vi.fn() } }));
vi.mock("@/modules/ui/components/delete-dialog", () => ({
DeleteDialog: ({ open, setOpen, onDelete }: any) =>
open ? (
<div data-testid="delete-dialog">
<button onClick={onDelete}>confirm</button>
<button onClick={() => setOpen(false)}>cancel</button>
</div>
) : null,
}));
vi.mock("@/modules/ui/components/empty-space-filler", () => ({
EmptySpaceFiller: ({ emptyMessage }: any) => <div>{emptyMessage}</div>,
}));
const baseProps = {
environment: { id: "env1" } as TEnvironment,
setOpenAddIntegrationModal: vi.fn(),
setIsConnected: vi.fn(),
setSelectedIntegration: vi.fn(),
refreshChannels: vi.fn(),
handleSlackAuthorization: vi.fn(),
showReconnectButton: false,
locale: "en-US" as const,
};
describe("ManageIntegration (Slack)", () => {
afterEach(() => cleanup());
test("empty state", () => {
render(
<ManageIntegration
{...baseProps}
slackIntegration={
{
id: "1",
config: { data: [], key: { team: { name: "team name" } } },
} as unknown as TIntegrationSlack
}
/>
);
expect(screen.getByText(/connect_your_first_slack_channel/)).toBeInTheDocument();
expect(screen.getByText(/link_channel/)).toBeInTheDocument();
});
test("link channel triggers handlers", async () => {
render(
<ManageIntegration
{...baseProps}
slackIntegration={
{
id: "1",
config: { data: [], key: { team: { name: "team name" } } },
} as unknown as TIntegrationSlack
}
/>
);
await userEvent.click(screen.getByText(/link_channel/));
expect(baseProps.refreshChannels).toHaveBeenCalled();
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null);
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
});
test("show reconnect button and triggers authorization", async () => {
render(
<ManageIntegration
{...baseProps}
showReconnectButton={true}
slackIntegration={
{
id: "1",
config: { data: [], key: { team: { name: "Team" } } },
} as unknown as TIntegrationSlack
}
/>
);
expect(screen.getByText("environments.integrations.slack.slack_reconnect_button")).toBeInTheDocument();
await userEvent.click(screen.getByText("environments.integrations.slack.slack_reconnect_button"));
expect(baseProps.handleSlackAuthorization).toHaveBeenCalled();
});
test("list integrations and open edit", async () => {
const item = {
surveyName: "S",
channelName: "C",
questions: "Q",
createdAt: new Date().toISOString(),
surveyId: "s",
channelId: "c",
} as unknown as TIntegrationSlackConfigData;
render(
<ManageIntegration
{...baseProps}
slackIntegration={
{
id: "1",
config: { data: [item], key: { team: { name: "team name" } } },
} as unknown as TIntegrationSlack
}
/>
);
expect(screen.getByText("S")).toBeInTheDocument();
await userEvent.click(screen.getByText("S"));
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({ ...item, index: 0 });
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
});
test("delete integration success", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
render(
<ManageIntegration
{...baseProps}
slackIntegration={
{
id: "1",
config: { data: [], key: { team: { name: "team name" } } },
} as unknown as TIntegrationSlack
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
await userEvent.click(screen.getByText("confirm"));
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
const { default: toast } = await import("react-hot-toast");
expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully");
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
});
test("delete integration error", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
render(
<ManageIntegration
{...baseProps}
slackIntegration={
{
id: "1",
config: { data: [], key: { team: { name: "team name" } } },
} as unknown as TIntegrationSlack
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
await userEvent.click(screen.getByText("confirm"));
const { default: toast } = await import("react-hot-toast");
expect(toast.error).toHaveBeenCalledWith(expect.any(String));
});
});

View File

@@ -1,174 +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 "@/app/(app)/environments/[environmentId]/project/integrations/slack/actions";
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper";
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/lib/slack";
// Mock child components and actions
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/slack/actions", () => ({
getSlackChannelsAction: vi.fn(),
}));
vi.mock(
"@/app/(app)/environments/[environmentId]/project/integrations/slack/components/AddChannelMappingModal",
() => ({
AddChannelMappingModal: vi.fn(({ open }) => (open ? <div data-testid="add-modal">Add Modal</div> : null)),
})
);
vi.mock(
"@/app/(app)/environments/[environmentId]/project/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]/project/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");
});
});
});

View File

@@ -1,222 +0,0 @@
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";
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper";
import Page from "@/app/(app)/environments/[environmentId]/project/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";
// Mock dependencies
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys", () => ({
getSurveys: vi.fn(),
}));
vi.mock(
"@/app/(app)/environments/[environmentId]/project/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,
displayPercentage: 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}/project/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();
});
});

View File

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

View File

@@ -1,15 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { LanguagesLoading as OriginalLanguagesLoading } from "@/modules/ee/languages/loading";
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);
});
});

View File

@@ -1,36 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { LanguagesPage } from "@/modules/ee/languages/page";
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",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: 1,
}));
describe("LanguagesPage re-export", () => {
test("should re-export LanguagesPage component", () => {
expect(Page).toBe(LanguagesPage);
});
});

View File

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

View File

@@ -1,17 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { ProjectLookSettingsLoading as OriginalProjectLookSettingsLoading } from "@/modules/projects/settings/look/loading";
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);
});
});

View File

@@ -1,42 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { ProjectLookSettingsPage } from "@/modules/projects/settings/look/page";
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",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: 1,
}));
vi.mock("@/lib/env", () => ({
env: {
PUBLIC_URL: "https://public-domain.com",
},
}));
describe("ProjectLookSettingsPage re-export", () => {
test("should re-export ProjectLookSettingsPage component", () => {
expect(Page).toBe(ProjectLookSettingsPage);
});
});

View File

@@ -1,33 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { ProjectSettingsPage } from "@/modules/projects/settings/page";
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);
});
});

View File

@@ -1,15 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { TagsLoading as OriginalTagsLoading } from "@/modules/projects/settings/tags/loading";
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);
});
});

View File

@@ -1,36 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { TagsPage } from "@/modules/projects/settings/tags/page";
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",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: 1,
}));
describe("TagsPage re-export", () => {
test("should re-export TagsPage component", () => {
expect(Page).toBe(TagsPage);
});
});

View File

@@ -1,36 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { ProjectTeams } from "@/modules/ee/teams/project-teams/page";
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",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: true,
}));
describe("ProjectTeams re-export", () => {
test("should re-export ProjectTeams component", () => {
expect(Page).toBe(ProjectTeams);
});
});

View File

@@ -1,148 +0,0 @@
import { cleanup, render } from "@testing-library/react";
import { usePathname } from "next/navigation";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
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
);
});
});

View File

@@ -1,98 +0,0 @@
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 { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
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",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: true,
}));
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");
});
});

View File

@@ -1,267 +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: {},
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",
})
);
});
});

View File

@@ -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}/project/integrations`);
});
});

View File

@@ -1,410 +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({ data: true })),
}));
const surveyId = "survey1";
const projectId = "project1";
const organizationId = "org1";
const baseNotificationSettings: TUserNotificationSettings = {
alert: {},
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 '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();
});
test("shows error toast when updateNotificationSettingsAction fails for 'alert' type", async () => {
const mockErrorResponse = { serverError: "Failed to update notification settings" };
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
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.error).toHaveBeenCalledWith("Failed to update notification settings", {
id: "notification-switch",
});
expect(toast.success).not.toHaveBeenCalled();
});
test("shows error toast when updateNotificationSettingsAction fails for 'unsubscribedOrganizationIds' type", async () => {
const mockErrorResponse = { serverError: "Permission denied" };
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
const initialSettings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [] };
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] },
});
expect(toast.error).toHaveBeenCalledWith("Permission denied", {
id: "notification-switch",
});
expect(toast.success).not.toHaveBeenCalled();
});
test("shows error toast when updateNotificationSettingsAction returns null", async () => {
const mockErrorResponse = { serverError: "An error occurred" };
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
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.error).toHaveBeenCalledWith("An error occurred", {
id: "notification-switch",
});
expect(toast.success).not.toHaveBeenCalled();
});
test("shows error toast when updateNotificationSettingsAction returns undefined", async () => {
const mockErrorResponse = { serverError: "An error occurred" };
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
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.error).toHaveBeenCalledWith("An error occurred", {
id: "notification-switch",
});
expect(toast.success).not.toHaveBeenCalled();
});
test("shows error toast when updateNotificationSettingsAction returns response without data property", async () => {
const mockErrorResponse = { validationErrors: { _errors: ["Invalid input"] } };
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
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.error).toHaveBeenCalledWith("Invalid input", {
id: "notification-switch",
});
expect(toast.success).not.toHaveBeenCalled();
});
test("shows error toast when updateNotificationSettingsAction throws an exception", async () => {
const mockErrorResponse = { serverError: "Network error" };
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
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.error).toHaveBeenCalledWith("Network error", {
id: "notification-switch",
});
expect(toast.success).not.toHaveBeenCalled();
});
test("switch remains enabled after error occurs", async () => {
const mockErrorResponse = { serverError: "Failed to update" };
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
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(toast.error).toHaveBeenCalledWith("Failed to update", {
id: "notification-switch",
});
expect(switchInput).toBeEnabled(); // Switch should be re-enabled after error
});
test("shows error toast with validation errors for specific fields", async () => {
const mockErrorResponse = {
validationErrors: {
notificationSettings: {
_errors: ["Invalid notification settings"],
},
},
};
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
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.error).toHaveBeenCalledWith("notificationSettingsInvalid notification settings", {
id: "notification-switch",
});
expect(toast.success).not.toHaveBeenCalled();
});
});

View File

@@ -1,38 +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();
});
});

View File

@@ -1,228 +0,0 @@
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 { getUser } from "@/lib/user/service";
import { EditAlerts } from "./components/EditAlerts";
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/IntegrationsTip", () => ({
IntegrationsTip: () => <div>IntegrationsTipComponent</div>,
}));
const mockUser: Partial<TUser> = {
id: "user-1",
name: "Test User",
email: "test@example.com",
notificationSettings: {
alert: { "survey-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();
// 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.
const finalExpectedSettings = {
alert: {
"survey-1": false,
"survey-2": 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);
});
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();
const expectedEmptySettings = {
alert: {},
unsubscribedOrganizationIds: [],
};
const editAlertsCall = vi.mocked(EditAlerts).mock.calls[0][0];
expect(editAlertsCall.user.notificationSettings).toEqual(expectedEmptySettings);
expect(editAlertsCall.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
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
},
unsubscribedOrganizationIds: [],
};
const editAlertsCall = vi.mocked(EditAlerts).mock.calls[0][0];
expect(editAlertsCall.user.notificationSettings).toEqual(expectedProcessedSettings);
});
});

View File

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

View File

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

View File

@@ -1,209 +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 { resetPasswordAction, 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: {},
unsubscribedOrganizationIds: [],
},
twoFactorEnabled: false,
identityProvider: "email",
createdAt: new Date(),
updatedAt: new Date(),
role: "project_manager",
objective: "other",
} as unknown as TUser;
vi.mock("next-auth/react", () => ({ signOut: vi.fn() }));
// 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(),
resetPasswordAction: vi.fn(),
}));
vi.mock("@/modules/auth/forgot-password/actions", () => ({
forgotPasswordAction: 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}
emailVerificationDisabled={true}
isPasswordResetEnabled={false}
/>
);
const nameInput = screen.getByPlaceholderText("common.full_name");
expect(nameInput).toHaveValue(mockUser.name);
// 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",
email: mockUser.email,
});
});
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}
emailVerificationDisabled={false}
isPasswordResetEnabled={false}
/>
);
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}
emailVerificationDisabled={false}
isPasswordResetEnabled={false}
/>
);
const updateButton = screen.getByText("common.update");
expect(updateButton).toBeDisabled();
const nameInput = screen.getByPlaceholderText("common.full_name");
await userEvent.type(nameInput, " updated");
expect(updateButton).toBeEnabled();
});
test("reset password button works", async () => {
vi.mocked(resetPasswordAction).mockResolvedValue({ data: { success: true } });
render(
<EditProfileDetailsForm
user={mockUser}
emailVerificationDisabled={false}
isPasswordResetEnabled={true}
/>
);
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
await userEvent.click(resetButton);
await waitFor(() => {
expect(resetPasswordAction).toHaveBeenCalled();
});
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith("auth.forgot-password.email-sent.heading");
});
});
test("reset password button handles error correctly", async () => {
const errorMessage = "Reset failed";
vi.mocked(resetPasswordAction).mockResolvedValue({ serverError: errorMessage });
render(
<EditProfileDetailsForm
user={mockUser}
emailVerificationDisabled={false}
isPasswordResetEnabled={true}
/>
);
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
await userEvent.click(resetButton);
await waitFor(() => {
expect(resetPasswordAction).toHaveBeenCalled();
});
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(errorMessage);
});
});
test("reset password button shows loading state", async () => {
vi.mocked(resetPasswordAction).mockImplementation(() => new Promise(() => {})); // Never resolves
render(
<EditProfileDetailsForm
user={mockUser}
emailVerificationDisabled={false}
isPasswordResetEnabled={true}
/>
);
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
await userEvent.click(resetButton);
expect(resetButton).toBeDisabled();
});
});

View File

@@ -1,141 +0,0 @@
import "@testing-library/jest-dom/vitest";
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 { PasswordConfirmationModal } from "./password-confirmation-modal";
// Mock the Dialog component
vi.mock("@/modules/ui/components/dialog", () => ({
Dialog: ({ children, open, onOpenChange }: any) =>
open ? (
<div data-testid="dialog" role="dialog">
{children}
<button data-testid="dialog-close" onClick={() => onOpenChange(false)}>
Close
</button>
</div>
) : null,
DialogContent: ({ children, ...props }: any) => (
<div data-testid="dialog-content" {...props}>
{children}
</div>
),
DialogHeader: ({ children }: any) => <div data-testid="dialog-header">{children}</div>,
DialogTitle: ({ children }: any) => <h2 data-testid="dialog-title">{children}</h2>,
DialogDescription: ({ children }: any) => <p data-testid="dialog-description">{children}</p>,
DialogBody: ({ children }: any) => <div data-testid="dialog-body">{children}</div>,
DialogFooter: ({ children }: any) => <div data-testid="dialog-footer">{children}</div>,
}));
// Mock the PasswordInput component
vi.mock("@/modules/ui/components/password-input", () => ({
PasswordInput: ({ onChange, value, placeholder }: any) => (
<input
type="password"
value={value || ""}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
data-testid="password-input"
/>
),
}));
// Mock the useTranslate hook
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key: string) => key,
}),
}));
describe("PasswordConfirmationModal", () => {
const defaultProps = {
open: true,
setOpen: vi.fn(),
oldEmail: "old@example.com",
newEmail: "new@example.com",
onConfirm: vi.fn(),
};
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders nothing when open is false", () => {
render(<PasswordConfirmationModal {...defaultProps} open={false} />);
expect(screen.queryByTestId("dialog")).not.toBeInTheDocument();
});
test("renders dialog content when open is true", () => {
render(<PasswordConfirmationModal {...defaultProps} />);
expect(screen.getByTestId("dialog")).toBeInTheDocument();
expect(screen.getByTestId("dialog-title")).toBeInTheDocument();
});
test("displays old and new email addresses", () => {
render(<PasswordConfirmationModal {...defaultProps} />);
expect(screen.getByText("old@example.com")).toBeInTheDocument();
expect(screen.getByText("new@example.com")).toBeInTheDocument();
});
test("shows password input field", () => {
render(<PasswordConfirmationModal {...defaultProps} />);
const passwordInput = screen.getByTestId("password-input");
expect(passwordInput).toBeInTheDocument();
expect(passwordInput).toHaveAttribute("placeholder", "*******");
});
test("disables confirm button when form is not dirty", () => {
render(<PasswordConfirmationModal {...defaultProps} />);
const confirmButton = screen.getByText("common.confirm");
expect(confirmButton).toBeDisabled();
});
test("disables confirm button when old and new emails are the same", () => {
render(
<PasswordConfirmationModal {...defaultProps} oldEmail="same@example.com" newEmail="same@example.com" />
);
const confirmButton = screen.getByText("common.confirm");
expect(confirmButton).toBeDisabled();
});
test("enables confirm button when password is entered and emails are different", async () => {
const user = userEvent.setup();
render(<PasswordConfirmationModal {...defaultProps} />);
const passwordInput = screen.getByTestId("password-input");
await user.type(passwordInput, "password123");
const confirmButton = screen.getByText("common.confirm");
expect(confirmButton).not.toBeDisabled();
});
test("shows error message when password is too short", async () => {
const user = userEvent.setup();
render(<PasswordConfirmationModal {...defaultProps} />);
const passwordInput = screen.getByTestId("password-input");
await user.type(passwordInput, "short");
const confirmButton = screen.getByText("common.confirm");
await user.click(confirmButton);
expect(screen.getByText("Password must be at least 8 characters long")).toBeInTheDocument();
});
test("handles cancel button click and resets form", async () => {
const user = userEvent.setup();
render(<PasswordConfirmationModal {...defaultProps} />);
const passwordInput = screen.getByTestId("password-input");
await user.type(passwordInput, "password123");
const cancelButton = screen.getByText("common.cancel");
await user.click(cancelButton);
expect(defaultProps.setOpen).toHaveBeenCalledWith(false);
await waitFor(() => {
expect(passwordInput).toHaveValue("");
});
});
});

View File

@@ -1,60 +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(2);
expect(loadingCards[0]).toHaveTextContent("environments.settings.profile.personal_information");
expect(loadingCards[0]).toHaveTextContent("environments.settings.profile.update_personal_info");
expect(loadingCards[1]).toHaveTextContent("environments.settings.profile.delete_account");
expect(loadingCards[1]).toHaveTextContent("environments.settings.profile.confirm_delete_account");
});
});

View File

@@ -1,184 +0,0 @@
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 { 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 Page from "./page";
// Mock services and utils
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: 1,
PASSWORD_RESET_DISABLED: 1,
EMAIL_VERIFICATION_DISABLED: 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/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",
twoFactorEnabled: false,
identityProvider: "email",
notificationSettings: { alert: {}, 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("account-security")).toBeInTheDocument(); // Shown because 2FA license is enabled
expect(screen.queryByTestId("upgrade-prompt")).not.toBeInTheDocument();
expect(screen.getByTestId("delete-account")).toBeInTheDocument();
// Check for IdBadge content
expect(screen.getByText("common.profile_id")).toBeInTheDocument();
expect(screen.getByText(mockUser.id)).toBeInTheDocument();
});
});
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");
}
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,134 +0,0 @@
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 { getAccessFlags } from "@/lib/membership/utils";
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);
});
});

View File

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

View File

@@ -1,199 +0,0 @@
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 { 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";
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(),
twoFactorEnabled: false,
identityProvider: "email",
notificationSettings: { alert: {} },
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 () => {
vi.resetModules();
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
getEnterpriseLicense: vi.fn().mockResolvedValue({
active: false,
isPendingDowngrade: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
fallbackLevel: "live",
}),
}));
const { default: EnterpriseSettingsPage } = await import("./page");
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();
});
});

View File

@@ -1,192 +0,0 @@
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 { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
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();
});
});
});

View File

@@ -1,149 +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 { TOrganization } from "@formbricks/types/organizations";
import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
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();
}
});
});

View File

@@ -1,67 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/tolgee/server";
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();
});
});

View File

@@ -1,405 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { TUser } from "@formbricks/types/user";
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getUser } from "@/lib/user/service";
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { getTranslate } from "@/tolgee/server";
import { DeleteOrganization } from "./components/DeleteOrganization";
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
import Page from "./page";
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
IS_PRODUCTION: false,
IS_STORAGE_CONFIGURED: true,
FB_LOGO_URL: "https://example.com/mock-logo.png",
ENCRYPTION_KEY: "mock-encryption-key",
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
GITHUB_ID: "mock-github-id",
GITHUB_SECRET: "mock-github-secret",
GOOGLE_CLIENT_ID: "mock-google-client-id",
GOOGLE_CLIENT_SECRET: "mock-google-client-secret",
AZUREAD_CLIENT_ID: "mock-azuread-client-id",
AZUREAD_CLIENT_SECRET: "mock-azure-client-secret",
AZUREAD_TENANT_ID: "mock-azuread-tenant-id",
OIDC_CLIENT_ID: "mock-oidc-client-id",
OIDC_CLIENT_SECRET: "mock-oidc-client-secret",
OIDC_ISSUER: "mock-oidc-issuer",
OIDC_DISPLAY_NAME: "mock-oidc-display-name",
OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm",
SAML_DATABASE_URL: "mock-saml-database-url",
WEBAPP_URL: "mock-webapp-url",
SMTP_HOST: "mock-smtp-host",
SMTP_PORT: "mock-smtp-port",
}));
vi.mock("next-auth", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@/tolgee/server", () => ({
getTranslate: vi.fn(),
}));
vi.mock("@/lib/user/service", () => ({
getUser: vi.fn(),
}));
vi.mock("@/modules/environments/lib/utils", () => ({
getEnvironmentAuth: vi.fn(),
}));
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
getIsMultiOrgEnabled: 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/id-badge", () => ({
IdBadge: vi.fn(() => <div>IdBadge</div>),
}));
vi.mock("@/modules/ui/components/alert", () => ({
Alert: ({ children, variant }: any) => (
<div data-testid="alert" data-variant={variant}>
{children}
</div>
),
AlertDescription: ({ children }: any) => <div data-testid="alert-description">{children}</div>,
}));
describe("Page", () => {
afterEach(() => {
cleanup();
});
let mockEnvironmentAuth = {
session: { user: { id: "test-user-id" } },
currentUserMembership: { role: "owner" },
organization: { id: "test-organization-id", billing: { plan: "free" } },
isOwner: true,
isManager: false,
} as unknown as TEnvironmentAuth;
const mockUser = { id: "test-user-id" } as TUser;
const mockTranslate = vi.fn((key) => key);
const mockParams = { environmentId: "env-123" };
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
vi.mocked(getUser).mockResolvedValue(mockUser);
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
});
test("renders the page with organization settings for owner", async () => {
const props = {
params: Promise.resolve(mockParams),
};
const PageComponent = await Page(props);
render(PageComponent);
expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument();
expect(OrganizationSettingsNavbar).toHaveBeenCalledWith(
{
environmentId: mockParams.environmentId,
isFormbricksCloud: IS_FORMBRICKS_CLOUD,
membershipRole: "owner",
activeId: "general",
},
undefined
);
expect(screen.getByText("environments.settings.general.organization_name")).toBeInTheDocument();
expect(EditOrganizationNameForm).toHaveBeenCalledWith(
{
organization: mockEnvironmentAuth.organization,
environmentId: mockParams.environmentId,
membershipRole: "owner",
},
undefined
);
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
{
organization: mockEnvironmentAuth.organization,
hasWhiteLabelPermission: true,
environmentId: mockParams.environmentId,
isReadOnly: false,
isFormbricksCloud: IS_FORMBRICKS_CLOUD,
fbLogoUrl: FB_LOGO_URL,
user: mockUser,
isStorageConfigured: true,
},
undefined
);
expect(screen.getByText("environments.settings.general.delete_organization")).toBeInTheDocument();
expect(DeleteOrganization).toHaveBeenCalledWith(
{
organization: mockEnvironmentAuth.organization,
isDeleteDisabled: false,
isUserOwner: true,
},
undefined
);
expect(IdBadge).toHaveBeenCalledWith(
{
id: mockEnvironmentAuth.organization.id,
label: "common.organization_id",
variant: "column",
},
undefined
);
});
test("renders correctly when user is manager", async () => {
const managerAuth = {
...mockEnvironmentAuth,
currentUserMembership: { role: "manager" },
isOwner: false,
isManager: true,
} as unknown as TEnvironmentAuth;
vi.mocked(getEnvironmentAuth).mockResolvedValue(managerAuth);
const props = {
params: Promise.resolve(mockParams),
};
const PageComponent = await Page(props);
render(PageComponent);
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
expect.objectContaining({
isReadOnly: false, // owner or manager can edit
}),
undefined
);
expect(DeleteOrganization).toHaveBeenCalledWith(
expect.objectContaining({
isDeleteDisabled: true, // only owner can delete
isUserOwner: false,
}),
undefined
);
});
test("renders correctly when multi-org is disabled", async () => {
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false);
const props = {
params: Promise.resolve(mockParams),
};
const PageComponent = await Page(props);
render(PageComponent);
expect(screen.queryByText("environments.settings.general.delete_organization")).not.toBeInTheDocument();
expect(DeleteOrganization).not.toHaveBeenCalled();
// isDeleteDisabled should be true because multiOrg is disabled, even for owner
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
expect.objectContaining({
isReadOnly: false,
}),
undefined
);
});
test("renders correctly when user is not owner or manager (e.g., admin)", async () => {
const adminAuth = {
...mockEnvironmentAuth,
currentUserMembership: { role: "admin" },
isOwner: false,
isManager: false,
} as unknown as TEnvironmentAuth;
vi.mocked(getEnvironmentAuth).mockResolvedValue(adminAuth);
const props = {
params: Promise.resolve(mockParams),
};
const PageComponent = await Page(props);
render(PageComponent);
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
expect.objectContaining({
isReadOnly: true,
}),
undefined
);
expect(DeleteOrganization).toHaveBeenCalledWith(
expect.objectContaining({
isDeleteDisabled: true,
isUserOwner: false,
}),
undefined
);
});
test("renders if session user id empty, user is null", async () => {
const noUserSessionAuth = {
...mockEnvironmentAuth,
session: { ...mockEnvironmentAuth.session, user: { ...mockEnvironmentAuth.session.user, id: "" } },
};
vi.mocked(getEnvironmentAuth).mockResolvedValue(noUserSessionAuth);
vi.mocked(getUser).mockResolvedValue(null);
const props = {
params: Promise.resolve(mockParams),
};
const PageComponent = await Page(props);
render(PageComponent);
expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument();
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
expect.objectContaining({
user: null,
}),
undefined
);
});
test("handles getEnvironmentAuth error", async () => {
vi.mocked(getEnvironmentAuth).mockRejectedValue(new Error("Authentication error"));
const props = {
params: Promise.resolve({ environmentId: "env-123" }),
};
await expect(Page(props)).rejects.toThrow("Authentication error");
});
test("does not show storage warning when IS_STORAGE_CONFIGURED is true", async () => {
const props = {
params: Promise.resolve(mockParams),
};
const PageComponent = await Page(props);
render(PageComponent);
expect(screen.queryByTestId("alert")).not.toBeInTheDocument();
});
test("shows storage warning when IS_STORAGE_CONFIGURED is false", async () => {
// Mock IS_STORAGE_CONFIGURED as false
vi.doMock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
IS_PRODUCTION: false,
IS_STORAGE_CONFIGURED: 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",
}));
// Re-import the module to get the updated mock
const { default: PageWithStorageDisabled } = await import("./page");
const props = {
params: Promise.resolve(mockParams),
};
const PageComponent = await PageWithStorageDisabled(props);
render(PageComponent);
expect(screen.getByTestId("alert")).toBeInTheDocument();
expect(screen.getByTestId("alert")).toHaveAttribute("data-variant", "warning");
expect(screen.getByTestId("alert-description")).toHaveTextContent("common.storage_not_configured");
});
test("passes isStorageConfigured=true to EmailCustomizationSettings when storage is configured", async () => {
const props = {
params: Promise.resolve(mockParams),
};
const PageComponent = await Page(props);
render(PageComponent);
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
expect.objectContaining({
isStorageConfigured: true,
}),
undefined
);
});
test("passes isStorageConfigured=false to EmailCustomizationSettings when storage is not configured", async () => {
// Mock IS_STORAGE_CONFIGURED as false
vi.doMock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
IS_PRODUCTION: false,
IS_STORAGE_CONFIGURED: 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",
}));
// Re-import the module to get the updated mock
const { default: PageWithStorageDisabled } = await import("./page");
const props = {
params: Promise.resolve(mockParams),
};
const PageComponent = await PageWithStorageDisabled(props);
render(PageComponent);
expect(EmailCustomizationSettings).toHaveBeenCalledWith(
expect.objectContaining({
isStorageConfigured: false,
}),
undefined
);
});
});

View File

@@ -1,98 +0,0 @@
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 { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
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");
});
});

View File

@@ -1,47 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { TeamsPage } from "@/modules/organization/settings/teams/page";
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",
SESSION_MAX_AGE: 1000,
REDIS_URL: undefined,
AUDIT_LOG_ENABLED: 1,
}));
vi.mock("@/lib/env", () => ({
env: {
PUBLIC_URL: "https://public-domain.com",
},
}));
describe("TeamsPage re-export", () => {
test("should re-export TeamsPage component", () => {
expect(Page).toBe(TeamsPage);
});
});

View File

@@ -1,72 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
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"
);
});
});

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