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
@@ -1,139 +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 toast from "react-hot-toast";
import { afterEach, describe, expect, test, vi } from "vitest";
import { createOrganizationAction } from "@/modules/organization/actions";
import { CreateOrganizationModal } from "./index";
vi.mock("@/modules/ui/components/dialog", () => ({
Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) =>
open ? <div data-testid="dialog">{children}</div> : null,
DialogContent: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-content">{children}</div>
),
DialogHeader: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-header">{children}</div>
),
DialogTitle: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-title">{children}</div>
),
DialogDescription: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-description">{children}</div>
),
DialogBody: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-body">{children}</div>
),
DialogFooter: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-footer">{children}</div>
),
}));
vi.mock("lucide-react", () => ({
PlusCircleIcon: () => <svg data-testid="plus-icon" />,
}));
const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: vi.fn(() => ({
push: mockPush,
})),
}));
vi.mock("@/modules/organization/actions", () => ({
createOrganizationAction: vi.fn(),
}));
vi.mock("@/lib/utils/helper", () => ({
getFormattedErrorMessage: vi.fn(() => "Formatted error"),
}));
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({ t: (k) => k }),
}));
describe("CreateOrganizationModal", () => {
afterEach(() => {
cleanup();
});
test("renders dialog and form fields", () => {
render(<CreateOrganizationModal open={true} setOpen={vi.fn()} />);
expect(screen.getByTestId("dialog")).toBeInTheDocument();
expect(screen.getByTestId("dialog-header")).toBeInTheDocument();
expect(screen.getByTestId("dialog-title")).toBeInTheDocument();
expect(screen.getByTestId("dialog-description")).toBeInTheDocument();
expect(screen.getByTestId("dialog-body")).toBeInTheDocument();
expect(screen.getByTestId("dialog-footer")).toBeInTheDocument();
expect(
screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder")
).toBeInTheDocument();
expect(screen.getByText("common.cancel")).toBeInTheDocument();
});
test("disables submit button if organization name is empty", () => {
render(<CreateOrganizationModal open={true} setOpen={vi.fn()} />);
const submitBtn = screen.getByText("environments.settings.general.create_new_organization", {
selector: "button[type='submit']",
});
expect(submitBtn).toBeDisabled();
});
test("enables submit button when organization name is entered", async () => {
render(<CreateOrganizationModal open={true} setOpen={vi.fn()} />);
const input = screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder");
const submitBtn = screen.getByText("environments.settings.general.create_new_organization", {
selector: "button[type='submit']",
});
await userEvent.type(input, "Formbricks Org");
expect(submitBtn).not.toBeDisabled();
});
test("calls createOrganizationAction and closes dialog on success", async () => {
const setOpen = vi.fn();
vi.mocked(createOrganizationAction).mockResolvedValue({ data: { id: "org-1" } } as any);
render(<CreateOrganizationModal open={true} setOpen={setOpen} />);
const input = screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder");
await userEvent.type(input, "Formbricks Org");
const submitBtn = screen.getByText("environments.settings.general.create_new_organization", {
selector: "button[type='submit']",
});
await userEvent.click(submitBtn);
await waitFor(() => {
expect(createOrganizationAction).toHaveBeenCalledWith({ organizationName: "Formbricks Org" });
expect(setOpen).toHaveBeenCalledWith(false);
expect(mockPush).toHaveBeenCalledWith("/organizations/org-1");
});
});
test("shows error toast on failure", async () => {
const setOpen = vi.fn();
vi.mocked(createOrganizationAction).mockResolvedValue({});
render(<CreateOrganizationModal open={true} setOpen={setOpen} />);
const input = screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder");
await userEvent.type(input, "Fail Org");
const submitBtn = screen.getByText("environments.settings.general.create_new_organization", {
selector: "button[type='submit']",
});
await userEvent.click(submitBtn);
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith("Formatted error");
});
});
test("does not submit if name is only whitespace", async () => {
const setOpen = vi.fn();
render(<CreateOrganizationModal open={true} setOpen={setOpen} />);
const input = screen.getByPlaceholderText("environments.settings.general.organization_name_placeholder");
await userEvent.type(input, " ");
const submitBtn = screen.getByText("environments.settings.general.create_new_organization", {
selector: "button[type='submit']",
});
await userEvent.click(submitBtn);
expect(createOrganizationAction).not.toHaveBeenCalled();
});
test("calls setOpen(false) when cancel is clicked", async () => {
const setOpen = vi.fn();
render(<CreateOrganizationModal open={true} setOpen={setOpen} />);
const cancelBtn = screen.getByText("common.cancel");
await userEvent.click(cancelBtn);
expect(setOpen).toHaveBeenCalledWith(false);
});
});
@@ -1,292 +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 { TProject } from "@formbricks/types/project";
import { AddApiKeyModal } from "./add-api-key-modal";
// Mock the Dialog components
vi.mock("@/modules/ui/components/dialog", () => ({
Dialog: ({
open,
onOpenChange,
children,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
children: React.ReactNode;
}) =>
open ? (
<div data-testid="dialog">
{children}
<button data-testid="dialog-close" onClick={() => onOpenChange(false)}>
Close
</button>
</div>
) : null,
DialogContent: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="dialog-content" className={className}>
{children}
</div>
),
DialogHeader: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-header">{children}</div>
),
DialogTitle: ({ children }: { children: React.ReactNode }) => (
<h2 data-testid="dialog-title">{children}</h2>
),
DialogBody: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-body">{children}</div>
),
DialogFooter: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-footer">{children}</div>
),
}));
// Mock the translate hook
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key: string) => key, // Return the key as is for testing
}),
}));
// Base project definition (customize as needed)
const baseProject = {
id: "project1",
name: "Project 1",
createdAt: new Date(),
updatedAt: new Date(),
organizationId: "org1",
styling: {
allowStyleOverwrite: true,
brandColor: { light: "#000000" },
},
recontactDays: 0,
inAppSurveyBranding: false,
linkSurveyBranding: false,
config: {
channel: "link" as const,
industry: "saas" as const,
},
placement: "bottomLeft" as const,
clickOutsideClose: true,
darkOverlay: false,
languages: [],
};
const mockProjects: TProject[] = [
{
...baseProject,
environments: [
{
id: "env1",
type: "production",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project1",
appSetupCompleted: true,
},
{
id: "env2",
type: "development",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project1",
appSetupCompleted: true,
},
],
} as TProject,
{
...baseProject,
id: "project2",
name: "Project 2",
environments: [
{
id: "env3",
type: "production",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project2",
appSetupCompleted: true,
},
{
id: "env4",
type: "development",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project2",
appSetupCompleted: true,
},
],
} as TProject,
];
describe("AddApiKeyModal", () => {
const mockSetOpen = vi.fn();
const mockOnSubmit = vi.fn();
const defaultProps = {
open: true,
setOpen: mockSetOpen,
onSubmit: mockOnSubmit,
projects: mockProjects,
isCreatingAPIKey: false,
};
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders the modal with initial state", () => {
render(<AddApiKeyModal {...defaultProps} />);
expect(screen.getByTestId("dialog")).toBeInTheDocument();
expect(screen.getByTestId("dialog-title")).toHaveTextContent("environments.project.api_keys.add_api_key");
expect(screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack")).toBeInTheDocument();
expect(screen.getByText("environments.project.api_keys.project_access")).toBeInTheDocument();
});
test("handles label input", async () => {
render(<AddApiKeyModal {...defaultProps} />);
const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack") as HTMLInputElement;
await userEvent.type(labelInput, "Test API Key");
expect(labelInput.value).toBe("Test API Key");
});
test("handles permission changes", async () => {
render(<AddApiKeyModal {...defaultProps} />);
const addButton = screen.getByRole("button", { name: /add_permission/i });
await userEvent.click(addButton);
// Open project dropdown for the first permission row
const projectDropdowns = screen.getAllByRole("button", { name: /Project 1/i });
await userEvent.click(projectDropdowns[0]);
// Wait for dropdown content and select 'Project 2'
const project2Option = await screen.findByRole("menuitem", { name: "Project 2" });
await userEvent.click(project2Option);
// Verify project selection by checking the updated button text
const updatedButton = await screen.findByRole("button", { name: "Project 2" });
expect(updatedButton).toBeInTheDocument();
});
test("adds and removes permissions", async () => {
render(<AddApiKeyModal {...defaultProps} />);
// Add new permission
const addButton = screen.getByRole("button", { name: /add_permission/i });
await userEvent.click(addButton);
await userEvent.click(addButton);
// Verify new permission row is added
const deleteButtons = screen.getAllByRole("button", { name: "" }); // Trash icons
expect(deleteButtons).toHaveLength(2);
// Remove the new permission
await userEvent.click(deleteButtons[1]);
// Check that only the original permission row remains
expect(screen.getAllByRole("button", { name: "" })).toHaveLength(1);
});
test("submits form with correct data", async () => {
render(<AddApiKeyModal {...defaultProps} />);
// Fill in label
const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack") as HTMLInputElement;
await userEvent.type(labelInput, "Test API Key");
const addButton = screen.getByRole("button", { name: /add_permission/i });
await userEvent.click(addButton);
// Click submit
const submitButton = screen.getByRole("button", {
name: "environments.project.api_keys.add_api_key",
});
await userEvent.click(submitButton);
expect(mockOnSubmit).toHaveBeenCalledWith({
label: "Test API Key",
environmentPermissions: [
{
environmentId: "env1",
permission: "read",
},
],
organizationAccess: {
accessControl: {
read: false,
write: false,
},
},
});
});
test("submits form with correct data including organization access toggles", async () => {
render(<AddApiKeyModal {...defaultProps} />);
// Fill in label
const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack");
await userEvent.type(labelInput, "Test API Key");
// Toggle the first switch (read) under organizationAccess
const readSwitch = screen.getByTestId("organization-access-accessControl-read"); // first is read, second is write
await userEvent.click(readSwitch); // toggle 'read' to true
// Submit form
const submitButton = screen.getByRole("button", {
name: "environments.project.api_keys.add_api_key",
});
await userEvent.click(submitButton);
expect(mockOnSubmit).toHaveBeenCalledWith({
label: "Test API Key",
environmentPermissions: [],
organizationAccess: {
accessControl: {
read: true,
write: false,
},
},
});
});
test("disables submit button when label is empty and there are not environment permissions", async () => {
render(<AddApiKeyModal {...defaultProps} />);
const submitButton = screen.getByRole("button", {
name: "environments.project.api_keys.add_api_key",
});
const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack");
// Initially disabled
expect(submitButton).toBeDisabled();
const addButton = screen.getByRole("button", { name: /add_permission/i });
await userEvent.click(addButton);
// After typing, it should be enabled
await userEvent.type(labelInput, "Test");
expect(submitButton).not.toBeDisabled();
});
test("closes modal and resets form on cancel", async () => {
render(<AddApiKeyModal {...defaultProps} />);
// Type something into the label
const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack") as HTMLInputElement;
await userEvent.type(labelInput, "Test API Key");
// Click the cancel button
const cancelButton = screen.getByRole("button", { name: "common.cancel" });
await userEvent.click(cancelButton);
// Verify modal is closed and form is reset
expect(mockSetOpen).toHaveBeenCalledWith(false);
expect(labelInput.value).toBe("");
});
});
@@ -1,169 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { render } from "@testing-library/react";
import { describe, expect, test, vi } from "vitest";
import { TProject } from "@formbricks/types/project";
import { getApiKeysWithEnvironmentPermissions } from "../lib/api-key";
import { ApiKeyList } from "./api-key-list";
// Mock the getApiKeysWithEnvironmentPermissions function
vi.mock("../lib/api-key", () => ({
getApiKeysWithEnvironmentPermissions: vi.fn(),
}));
// Mock @/lib/constants
vi.mock("@/lib/constants", () => ({
INTERCOM_SECRET_KEY: "test-secret-key",
IS_INTERCOM_CONFIGURED: true,
INTERCOM_APP_ID: "test-app-id",
ENCRYPTION_KEY: "test-encryption-key",
ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key",
GITHUB_ID: "test-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",
SESSION_MAX_AGE: 1000,
AUDIT_LOG_ENABLED: 1,
REDIS_URL: undefined,
}));
// Mock @/lib/env
vi.mock("@/lib/env", () => ({
env: {
IS_FORMBRICKS_CLOUD: "0",
},
}));
const baseProject = {
id: "project1",
name: "Project 1",
createdAt: new Date(),
updatedAt: new Date(),
organizationId: "org1",
styling: {
allowStyleOverwrite: true,
brandColor: { light: "#000000" },
},
recontactDays: 0,
inAppSurveyBranding: false,
linkSurveyBranding: false,
config: {
channel: "link" as const,
industry: "saas" as const,
},
placement: "bottomLeft" as const,
clickOutsideClose: true,
darkOverlay: false,
languages: [],
};
const mockProjects: TProject[] = [
{
...baseProject,
environments: [
{
id: "env1",
type: "production",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project1",
appSetupCompleted: true,
},
{
id: "env2",
type: "development",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project1",
appSetupCompleted: true,
},
],
},
];
const mockApiKeys = [
{
id: "key1",
hashedKey: "hashed1",
label: "Test Key 1",
createdAt: new Date(),
lastUsedAt: null,
organizationId: "org1",
createdBy: "user1",
},
{
id: "key2",
hashedKey: "hashed2",
label: "Test Key 2",
createdAt: new Date(),
lastUsedAt: null,
organizationId: "org1",
createdBy: "user1",
},
];
describe("ApiKeyList", () => {
test("renders EditAPIKeys with correct props", async () => {
// Mock the getApiKeysWithEnvironmentPermissions function to return our mock data
(getApiKeysWithEnvironmentPermissions as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
mockApiKeys
);
const props = {
organizationId: "org1",
locale: "en-US" as const,
isReadOnly: false,
projects: mockProjects,
};
const { container } = render(await ApiKeyList(props));
// Verify that EditAPIKeys is rendered with the correct props
expect(getApiKeysWithEnvironmentPermissions).toHaveBeenCalledWith("org1");
expect(container).toBeInTheDocument();
});
test("handles empty api keys", async () => {
// Mock the getApiKeysWithEnvironmentPermissions function to return empty array
(getApiKeysWithEnvironmentPermissions as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([]);
const props = {
organizationId: "org1",
locale: "en-US" as const,
isReadOnly: false,
projects: mockProjects,
};
const { container } = render(await ApiKeyList(props));
// Verify that EditAPIKeys is rendered even with empty api keys
expect(getApiKeysWithEnvironmentPermissions).toHaveBeenCalledWith("org1");
expect(container).toBeInTheDocument();
});
test("passes isReadOnly prop correctly", async () => {
(getApiKeysWithEnvironmentPermissions as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
mockApiKeys
);
const props = {
organizationId: "org1",
locale: "en-US" as const,
isReadOnly: true,
projects: mockProjects,
};
const { container } = render(await ApiKeyList(props));
expect(getApiKeysWithEnvironmentPermissions).toHaveBeenCalledWith("org1");
expect(container).toBeInTheDocument();
});
});
@@ -1,367 +0,0 @@
import { ApiKeyPermission } from "@prisma/client";
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 { TProject } from "@formbricks/types/project";
import { createApiKeyAction, deleteApiKeyAction, updateApiKeyAction } from "../actions";
import { TApiKeyWithEnvironmentPermission } from "../types/api-keys";
import { EditAPIKeys } from "./edit-api-keys";
// Mock the actions
vi.mock("../actions", () => ({
createApiKeyAction: vi.fn(),
updateApiKeyAction: vi.fn(),
deleteApiKeyAction: vi.fn(),
}));
// Mock react-hot-toast
vi.mock("react-hot-toast", () => ({
default: {
success: vi.fn(),
error: vi.fn(),
},
}));
// Mock the translate hook from @tolgee/react
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key: string) => key, // simply return the key
}),
}));
// Mock the timeSince function
vi.mock("@/lib/time", () => ({
timeSince: vi.fn(() => "2 days ago"),
}));
// Mock the Dialog components
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, className }: any) => (
<h2 data-testid="dialog-title" className={className}>
{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>,
}));
// Base project setup
const baseProject = {};
// Example project data
const mockProjects: TProject[] = [
{
...baseProject,
id: "project1",
name: "Project 1",
createdAt: new Date(),
updatedAt: new Date(),
organizationId: "org1",
styling: {
allowStyleOverwrite: true,
brandColor: { light: "#000000" },
},
config: {
channel: "link" as const,
industry: "saas" as const,
},
environments: [
{
id: "env1",
type: "production",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project1",
appSetupCompleted: true,
},
{
id: "env2",
type: "development",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project1",
appSetupCompleted: true,
},
],
} as TProject,
];
// Example API keys
const mockApiKeys: TApiKeyWithEnvironmentPermission[] = [
{
id: "key1",
label: "Test Key 1",
createdAt: new Date(),
organizationAccess: {
accessControl: {
read: true,
write: false,
},
},
apiKeyEnvironments: [
{
environmentId: "env1",
permission: ApiKeyPermission.read,
},
],
},
{
id: "key2",
label: "Test Key 2",
createdAt: new Date(),
organizationAccess: {
accessControl: {
read: true,
write: false,
},
},
apiKeyEnvironments: [
{
environmentId: "env2",
permission: ApiKeyPermission.read,
},
],
},
];
describe("EditAPIKeys", () => {
// Reset environment after each test
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
const defaultProps = {
organizationId: "org1",
apiKeys: mockApiKeys,
locale: "en-US" as const,
isReadOnly: false,
projects: mockProjects,
};
test("renders the API keys list", () => {
render(<EditAPIKeys {...defaultProps} />);
expect(screen.getByText("common.label")).toBeInTheDocument();
expect(screen.getByText("Test Key 1")).toBeInTheDocument();
expect(screen.getByText("Test Key 2")).toBeInTheDocument();
});
test("renders empty state when no API keys", () => {
render(<EditAPIKeys {...defaultProps} apiKeys={[]} />);
expect(screen.getByText("environments.project.api_keys.no_api_keys_yet")).toBeInTheDocument();
});
test("shows add API key button when not readonly", () => {
render(<EditAPIKeys {...defaultProps} />);
expect(
screen.getByRole("button", { name: "environments.settings.api_keys.add_api_key" })
).toBeInTheDocument();
});
test("hides add API key button when readonly", () => {
render(<EditAPIKeys {...defaultProps} isReadOnly={true} />);
expect(
screen.queryByRole("button", { name: "environments.settings.api_keys.add_api_key" })
).not.toBeInTheDocument();
});
test("opens add API key modal when clicking add button", async () => {
render(<EditAPIKeys {...defaultProps} />);
const addButton = screen.getByRole("button", { name: "environments.settings.api_keys.add_api_key" });
await userEvent.click(addButton);
// Look for the modal title using the correct test id
const modalTitle = screen.getByTestId("dialog-title");
expect(modalTitle).toBeInTheDocument();
expect(modalTitle).toHaveTextContent("environments.project.api_keys.add_api_key");
});
test("handles API key deletion", async () => {
(deleteApiKeyAction as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({ data: true });
render(<EditAPIKeys {...defaultProps} />);
const deleteButtons = screen.getAllByRole("button", { name: "" }); // Trash icons
// Click delete button for first API key
await userEvent.click(deleteButtons[0]);
const confirmDeleteButton = screen.getByRole("button", { name: "common.delete" });
await userEvent.click(confirmDeleteButton);
expect(deleteApiKeyAction).toHaveBeenCalledWith({ id: "key1" });
expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_deleted");
});
test("handles API key updation", async () => {
const updatedApiKey: TApiKeyWithEnvironmentPermission = {
id: "key1",
label: "Updated Key",
createdAt: new Date(),
organizationAccess: {
accessControl: {
read: true,
write: false,
},
},
apiKeyEnvironments: [
{
environmentId: "env1",
permission: ApiKeyPermission.read,
},
],
};
(updateApiKeyAction as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({ data: updatedApiKey });
render(<EditAPIKeys {...defaultProps} />);
// Open view permission modal
const apiKeyRows = screen.getAllByTestId("api-key-row");
// click on the first row
await userEvent.click(apiKeyRows[0]);
const labelInput = screen.getByTestId("api-key-label");
await userEvent.clear(labelInput);
await userEvent.type(labelInput, "Updated Key");
const submitButton = screen.getByRole("button", { name: "common.update" });
await userEvent.click(submitButton);
expect(updateApiKeyAction).toHaveBeenCalledWith({
apiKeyId: "key1",
apiKeyData: {
label: "Updated Key",
},
});
expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_updated");
});
test("handles API key creation", async () => {
const newApiKey: TApiKeyWithEnvironmentPermission = {
id: "key3",
label: "New Key",
createdAt: new Date(),
organizationAccess: {
accessControl: {
read: true,
write: false,
},
},
apiKeyEnvironments: [
{
environmentId: "env2",
permission: ApiKeyPermission.read,
},
],
};
(createApiKeyAction as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({ data: newApiKey });
render(<EditAPIKeys {...defaultProps} />);
// Open add modal
const addButton = screen.getByRole("button", { name: "environments.settings.api_keys.add_api_key" });
await userEvent.click(addButton);
// Fill in form
const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack");
await userEvent.type(labelInput, "New Key");
// Optionally toggle the read switch
const readSwitch = screen.getByTestId("organization-access-accessControl-read"); // first is read, second is write
await userEvent.click(readSwitch); // toggle 'read' to true
// Submit form
const submitButton = screen.getByRole("button", { name: "environments.project.api_keys.add_api_key" });
await userEvent.click(submitButton);
expect(createApiKeyAction).toHaveBeenCalledWith({
organizationId: "org1",
apiKeyData: {
label: "New Key",
environmentPermissions: [],
organizationAccess: {
accessControl: { read: true, write: false },
},
},
});
expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_created");
});
test("handles copy to clipboard", async () => {
// Mock the clipboard writeText method
const writeText = vi.fn();
Object.assign(navigator, {
clipboard: {
writeText,
},
});
// Provide an API key that has an actualKey
const apiKeyWithActual = {
...mockApiKeys[0],
actualKey: "test-api-key-123",
} as TApiKeyWithEnvironmentPermission & { actualKey: string };
render(<EditAPIKeys {...defaultProps} apiKeys={[apiKeyWithActual]} />);
// Find the copy icon button by testid
const copyButton = screen.getByTestId("copy-button");
await userEvent.click(copyButton);
expect(writeText).toHaveBeenCalledWith("test-api-key-123");
expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_copied_to_clipboard");
});
test("displays 'secret' when no actualKey is provided", () => {
render(<EditAPIKeys {...defaultProps} />);
// The API keys in mockApiKeys don't have actualKey, so they should display "secret"
expect(screen.getAllByText("environments.project.api_keys.secret")).toHaveLength(2);
});
test("stops propagation when clicking copy button", async () => {
const writeText = vi.fn();
Object.assign(navigator, {
clipboard: {
writeText,
},
});
const apiKeyWithActual = {
...mockApiKeys[0],
actualKey: "test-api-key-123",
} as TApiKeyWithEnvironmentPermission & { actualKey: string };
render(<EditAPIKeys {...defaultProps} apiKeys={[apiKeyWithActual]} />);
const copyButton = screen.getByTestId("copy-button");
await userEvent.click(copyButton);
// View permission modal should not open when clicking copy button
expect(screen.queryByTestId("dialog")).not.toBeInTheDocument();
});
test("displays created at time for each API key", () => {
render(<EditAPIKeys {...defaultProps} />);
// Should show "2 days ago" for both API keys (mocked)
expect(screen.getAllByText("2 days ago")).toHaveLength(2);
});
});
@@ -1,161 +0,0 @@
import { ApiKeyPermission } from "@prisma/client";
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TProject } from "@formbricks/types/project";
import { TApiKeyWithEnvironmentPermission } from "../types/api-keys";
import { ViewPermissionModal } from "./view-permission-modal";
// Mock the translate hook
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key: string) => key,
}),
}));
// Base project setup
const baseProject = {};
// Example project data
const mockProjects: TProject[] = [
{
...baseProject,
id: "project1",
name: "Project 1",
createdAt: new Date(),
updatedAt: new Date(),
organizationId: "org1",
styling: {
allowStyleOverwrite: true,
brandColor: { light: "#000000" },
},
config: {
channel: "link" as const,
industry: "saas" as const,
},
environments: [
{
id: "env1",
type: "production",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project1",
appSetupCompleted: true,
},
{
id: "env2",
type: "development",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project1",
appSetupCompleted: true,
},
],
} as TProject,
];
// Example API key with permissions
const mockApiKey: TApiKeyWithEnvironmentPermission = {
id: "key1",
label: "Test Key 1",
createdAt: new Date(),
organizationAccess: {
accessControl: {
read: true,
write: false,
},
},
apiKeyEnvironments: [
{
environmentId: "env1",
permission: ApiKeyPermission.read,
},
{
environmentId: "env2",
permission: ApiKeyPermission.write,
},
],
};
// API key with additional organization access
const mockApiKeyWithOrgAccess = {
...mockApiKey,
organizationAccess: {
accessControl: { read: true, write: false },
otherAccess: { read: false, write: true },
},
};
// API key with no environment permissions
const apiKeyWithoutPermissions = {
...mockApiKey,
apiKeyEnvironments: [],
};
describe("ViewPermissionModal", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
const defaultProps = {
open: true,
setOpen: vi.fn(),
projects: mockProjects,
apiKey: mockApiKey,
onSubmit: vi.fn(),
isUpdating: false,
};
test("renders the modal with correct title", () => {
render(<ViewPermissionModal {...defaultProps} />);
// Check the localized text for the modal's title
expect(screen.getByText(mockApiKey.label)).toBeInTheDocument();
});
test("renders all permissions for the API key", () => {
render(<ViewPermissionModal {...defaultProps} />);
// The same key has two environment permissions
const projectNames = screen.getAllByText("Project 1");
expect(projectNames).toHaveLength(2); // once for each permission
expect(screen.getByText("production")).toBeInTheDocument();
expect(screen.getByText("development")).toBeInTheDocument();
expect(screen.getByText("read")).toBeInTheDocument();
expect(screen.getByText("write")).toBeInTheDocument();
});
test("displays correct project and environment names", () => {
render(<ViewPermissionModal {...defaultProps} />);
// Check for 'Project 1', 'production', 'development'
const projectNames = screen.getAllByText("Project 1");
expect(projectNames).toHaveLength(2);
expect(screen.getByText("production")).toBeInTheDocument();
expect(screen.getByText("development")).toBeInTheDocument();
});
test("displays correct permission levels", () => {
render(<ViewPermissionModal {...defaultProps} />);
// Check if permission levels 'read' and 'write' appear
expect(screen.getByText("read")).toBeInTheDocument();
expect(screen.getByText("write")).toBeInTheDocument();
});
test("handles API key with no permissions", () => {
render(<ViewPermissionModal {...defaultProps} apiKey={apiKeyWithoutPermissions} />);
// Ensure environment/permission section is empty
expect(screen.queryByText("Project 1")).not.toBeInTheDocument();
expect(screen.queryByText("production")).not.toBeInTheDocument();
expect(screen.queryByText("development")).not.toBeInTheDocument();
});
test("displays organizationAccess toggles", () => {
render(<ViewPermissionModal {...defaultProps} apiKey={mockApiKeyWithOrgAccess} />);
expect(screen.getByTestId("organization-access-accessControl-read")).toBeChecked();
expect(screen.getByTestId("organization-access-accessControl-read")).toBeDisabled();
expect(screen.getByTestId("organization-access-accessControl-write")).not.toBeChecked();
expect(screen.getByTestId("organization-access-accessControl-write")).toBeDisabled();
expect(screen.getByTestId("organization-access-otherAccess-read")).toBeChecked();
expect(screen.getByTestId("organization-access-otherAccess-write")).toBeChecked();
});
});
@@ -1,43 +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 Loading from "./loading";
vi.mock(
"@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar",
() => ({
OrganizationSettingsNavbar: () => <div data-testid="org-navbar">OrgNavbar</div>,
})
);
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
PageContentWrapper: ({ children }) => <div data-testid="content-wrapper">{children}</div>,
}));
vi.mock("@/modules/ui/components/page-header", () => ({
PageHeader: ({ children, pageTitle }) => (
<div data-testid="page-header">
<span>{pageTitle}</span>
{children}
</div>
),
}));
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({ t: (k) => k }),
}));
describe("Loading (API Keys)", () => {
afterEach(() => {
cleanup();
});
test("renders loading skeletons and tolgee strings", () => {
render(<Loading isFormbricksCloud={true} />);
expect(screen.getByTestId("content-wrapper")).toBeInTheDocument();
expect(screen.getByTestId("page-header")).toBeInTheDocument();
expect(screen.getByTestId("org-navbar")).toBeInTheDocument();
expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument();
expect(screen.getAllByText("common.loading").length).toBeGreaterThan(0);
expect(screen.getByText("environments.project.api_keys.api_key")).toBeInTheDocument();
expect(screen.getByText("common.label")).toBeInTheDocument();
expect(screen.getByText("common.created_at")).toBeInTheDocument();
});
});
@@ -1,104 +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 { findMatchingLocale } from "@/lib/utils/locale";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { getProjectsByOrganizationId } from "@/modules/organization/settings/api-keys/lib/projects";
import { TOrganizationProject } from "@/modules/organization/settings/api-keys/types/api-keys";
import { APIKeysPage } from "./page";
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
PageContentWrapper: ({ children }) => <div data-testid="content-wrapper">{children}</div>,
}));
vi.mock("@/modules/ui/components/page-header", () => ({
PageHeader: ({ children, pageTitle }) => (
<div data-testid="page-header">
<span>{pageTitle}</span>
{children}
</div>
),
}));
vi.mock(
"@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar",
() => ({
OrganizationSettingsNavbar: () => <div data-testid="org-navbar">OrgNavbar</div>,
})
);
vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({
SettingsCard: ({ title, description, children }) => (
<div data-testid="settings-card">
<span>{title}</span>
<span>{description}</span>
{children}
</div>
),
}));
vi.mock("@/modules/organization/settings/api-keys/lib/projects", () => ({
getProjectsByOrganizationId: vi.fn(),
}));
vi.mock("@/modules/environments/lib/utils", () => ({
getEnvironmentAuth: vi.fn(),
}));
vi.mock("@/lib/utils/locale", () => ({
findMatchingLocale: vi.fn(),
}));
vi.mock("./components/api-key-list", () => ({
ApiKeyList: ({ organizationId, locale, isReadOnly, projects }) => (
<div data-testid="api-key-list">
{organizationId}-{locale}-{isReadOnly ? "readonly" : "editable"}-{projects.length}
</div>
),
}));
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: true,
}));
// Mock the server-side translation function
vi.mock("@/tolgee/server", () => ({
getTranslate: async () => (key: string) => key,
}));
const mockParams = { environmentId: "env-1" };
const mockLocale = "en-US";
const mockOrg = { id: "org-1" };
const mockMembership = { role: "owner" };
const mockProjects: TOrganizationProject[] = [
{ id: "p1", environments: [], name: "project1" },
{ id: "p2", environments: [], name: "project2" },
];
describe("APIKeysPage", () => {
afterEach(() => {
cleanup();
});
test("renders all main components and passes props", async () => {
vi.mocked(getEnvironmentAuth).mockResolvedValue({
currentUserMembership: mockMembership,
organization: mockOrg,
isOwner: true,
} as any);
vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale);
vi.mocked(getProjectsByOrganizationId).mockResolvedValue(mockProjects);
const props = { params: Promise.resolve(mockParams) };
render(await APIKeysPage(props));
expect(screen.getByTestId("content-wrapper")).toBeInTheDocument();
expect(screen.getByTestId("page-header")).toBeInTheDocument();
expect(screen.getByTestId("org-navbar")).toBeInTheDocument();
expect(screen.getByTestId("settings-card")).toBeInTheDocument();
expect(screen.getByTestId("api-key-list")).toHaveTextContent("org-1-en-US-editable-2");
expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument();
expect(screen.getByText("common.api_keys")).toBeInTheDocument();
expect(screen.getByText("environments.settings.api_keys.api_keys_description")).toBeInTheDocument();
});
test("throws error if not owner", async () => {
vi.mocked(getEnvironmentAuth).mockResolvedValue({
currentUserMembership: { role: "member" },
organization: mockOrg,
} as any);
const props = { params: Promise.resolve(mockParams) };
await expect(APIKeysPage(props)).rejects.toThrow("common.not_authorized");
});
});
@@ -1,118 +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 { TOrganization } from "@formbricks/types/organizations";
import { EditMemberships } from "./edit-memberships";
vi.mock("@/modules/organization/settings/teams/components/edit-memberships/members-info", () => ({
MembersInfo: (props: any) => <div data-testid="members-info" data-props={JSON.stringify(props)} />,
}));
vi.mock("@/modules/organization/settings/teams/lib/invite", () => ({
getInvitesByOrganizationId: vi.fn(async () => [
{
id: "invite-1",
email: "invite@example.com",
name: "Invitee",
role: "member",
expiresAt: new Date(),
createdAt: new Date(),
},
]),
}));
vi.mock("@/modules/organization/settings/teams/lib/membership", () => ({
getMembershipByOrganizationId: vi.fn(async () => [
{
userId: "user-1",
name: "User One",
email: "user1@example.com",
role: "owner",
accepted: true,
isActive: true,
},
]),
}));
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: 0,
}));
vi.mock("@/tolgee/server", () => ({
getTranslate: async () => (key: string) => key,
}));
const mockOrg: TOrganization = {
id: "org-1",
name: "Test Org",
createdAt: new Date(),
updatedAt: new Date(),
billing: {
plan: "free",
period: "monthly",
periodStart: new Date(),
stripeCustomerId: null,
limits: { monthly: { responses: 100, miu: 100 }, projects: 1 },
},
isAIEnabled: false,
};
describe("EditMemberships", () => {
afterEach(() => {
cleanup();
});
test("renders all table headers and MembersInfo when role is present", async () => {
const ui = await EditMemberships({
organization: mockOrg,
currentUserId: "user-1",
role: "owner",
isAccessControlAllowed: true,
isUserManagementDisabledFromUi: false,
});
render(ui);
expect(screen.getByText("common.full_name")).toBeInTheDocument();
expect(screen.getByText("common.email")).toBeInTheDocument();
expect(screen.getByText("common.role")).toBeInTheDocument();
expect(screen.getByText("common.status")).toBeInTheDocument();
expect(screen.getByText("common.actions")).toBeInTheDocument();
expect(screen.getByTestId("members-info")).toBeInTheDocument();
const props = JSON.parse(screen.getByTestId("members-info").getAttribute("data-props")!);
expect(props.organization.id).toBe("org-1");
expect(props.currentUserId).toBe("user-1");
expect(props.currentUserRole).toBe("owner");
expect(props.isAccessControlAllowed).toBe(true);
expect(props.isUserManagementDisabledFromUi).toBe(false);
expect(Array.isArray(props.invites)).toBe(true);
expect(Array.isArray(props.members)).toBe(true);
});
test("does not render role/actions columns if isAccessControlAllowed or isUserManagementDisabledFromUi is false", async () => {
const ui = await EditMemberships({
organization: mockOrg,
currentUserId: "user-1",
role: "member",
isAccessControlAllowed: false,
isUserManagementDisabledFromUi: true,
});
render(ui);
expect(screen.getByText("common.full_name")).toBeInTheDocument();
expect(screen.getByText("common.email")).toBeInTheDocument();
expect(screen.queryByText("common.role")).not.toBeInTheDocument();
expect(screen.getByText("common.status")).toBeInTheDocument();
expect(screen.queryByText("common.actions")).not.toBeInTheDocument();
expect(screen.getByTestId("members-info")).toBeInTheDocument();
});
test("does not render MembersInfo if role is falsy", async () => {
const ui = await EditMemberships({
organization: mockOrg,
currentUserId: "user-1",
role: undefined as any,
isAccessControlAllowed: true,
isUserManagementDisabledFromUi: false,
});
render(ui);
expect(screen.queryByTestId("members-info")).not.toBeInTheDocument();
});
});
@@ -1,203 +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 { TMember } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { getAccessFlags } from "@/lib/membership/utils";
import { isInviteExpired } from "@/modules/organization/settings/teams/lib/utils";
import { TInvite } from "@/modules/organization/settings/teams/types/invites";
import { MembersInfo } from "./members-info";
vi.mock("@/modules/ee/role-management/components/edit-membership-role", () => ({
EditMembershipRole: (props: any) => (
<div data-testid="edit-membership-role" data-props={JSON.stringify(props)} />
),
}));
vi.mock("@/modules/organization/settings/teams/components/edit-memberships/member-actions", () => ({
MemberActions: (props: any) => <div data-testid="member-actions" data-props={JSON.stringify(props)} />,
}));
vi.mock("@/modules/ui/components/badge", () => ({
Badge: (props: any) => <div data-testid={props["data-testid"] ?? "badge"}>{props.text}</div>,
}));
vi.mock("@/modules/ui/components/tooltip", () => ({
TooltipRenderer: (props: any) => <div data-testid="tooltip">{props.children}</div>,
}));
vi.mock("@/modules/organization/settings/teams/lib/utils", () => ({
isInviteExpired: vi.fn(() => false),
}));
vi.mock("@/lib/membership/utils", () => ({
getAccessFlags: vi.fn(() => ({ isOwner: false, isManager: false })),
}));
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({ t: (key: string) => key }),
}));
const org: TOrganization = {
id: "org-1",
name: "Test Org",
createdAt: new Date(),
updatedAt: new Date(),
billing: {
plan: "free",
period: "monthly",
periodStart: new Date(),
stripeCustomerId: null,
limits: { monthly: { responses: 100, miu: 100 }, projects: 1 },
},
isAIEnabled: false,
};
const member: TMember = {
userId: "user-1",
name: "User One",
email: "user1@example.com",
role: "owner",
accepted: true,
isActive: true,
};
const inactiveMember: TMember = {
...member,
isActive: false,
role: "member",
userId: "user-2",
email: "user2@example.com",
};
const invite: TInvite = {
id: "invite-1",
email: "invite@example.com",
name: "Invitee",
role: "member",
expiresAt: new Date(),
createdAt: new Date(),
};
describe("MembersInfo", () => {
afterEach(() => {
cleanup();
});
test("renders member info and EditMembershipRole when isAccessControlAllowed", () => {
render(
<MembersInfo
organization={org}
members={[member]}
invites={[]}
currentUserRole="owner"
currentUserId="user-1"
isAccessControlAllowed={true}
isFormbricksCloud={true}
isUserManagementDisabledFromUi={false}
/>
);
expect(screen.getByText("User One")).toBeInTheDocument();
expect(screen.getByText("user1@example.com")).toBeInTheDocument();
expect(screen.getByTestId("edit-membership-role")).toBeInTheDocument();
expect(screen.getByTestId("badge")).toHaveTextContent("Active");
expect(screen.getByTestId("member-actions")).toBeInTheDocument();
});
test("renders badge as Inactive for inactive member", () => {
render(
<MembersInfo
organization={org}
members={[inactiveMember]}
invites={[]}
currentUserRole="owner"
currentUserId="user-1"
isAccessControlAllowed={true}
isFormbricksCloud={true}
isUserManagementDisabledFromUi={false}
/>
);
expect(screen.getByTestId("badge")).toHaveTextContent("Inactive");
});
test("renders invite as Pending with tooltip if not expired", () => {
render(
<MembersInfo
organization={org}
members={[]}
invites={[invite]}
currentUserRole="owner"
currentUserId="user-1"
isAccessControlAllowed={true}
isFormbricksCloud={true}
isUserManagementDisabledFromUi={false}
/>
);
expect(screen.getByTestId("tooltip")).toBeInTheDocument();
expect(screen.getByTestId("badge")).toHaveTextContent("Pending");
});
test("renders invite as Expired if isInviteExpired returns true", () => {
vi.mocked(isInviteExpired).mockReturnValueOnce(true);
render(
<MembersInfo
organization={org}
members={[]}
invites={[invite]}
currentUserRole="owner"
currentUserId="user-1"
isAccessControlAllowed={true}
isFormbricksCloud={true}
isUserManagementDisabledFromUi={false}
/>
);
expect(screen.getByTestId("expired-badge")).toHaveTextContent("Expired");
});
test("does not render EditMembershipRole if isAccessControlAllowed is false", () => {
render(
<MembersInfo
organization={org}
members={[member]}
invites={[]}
currentUserRole="owner"
currentUserId="user-1"
isAccessControlAllowed={false}
isFormbricksCloud={true}
isUserManagementDisabledFromUi={false}
/>
);
expect(screen.queryByTestId("edit-membership-role")).not.toBeInTheDocument();
});
test("does not render MemberActions if isUserManagementDisabledFromUi is true", () => {
render(
<MembersInfo
organization={org}
members={[member]}
invites={[]}
currentUserRole="owner"
currentUserId="user-1"
isAccessControlAllowed={true}
isFormbricksCloud={true}
isUserManagementDisabledFromUi={true}
/>
);
expect(screen.queryByTestId("member-actions")).not.toBeInTheDocument();
});
test("showDeleteButton returns correct values for different roles and invite/member types", () => {
vi.mocked(getAccessFlags).mockReturnValueOnce({
isOwner: true,
isManager: false,
isBilling: false,
isMember: false,
});
render(
<MembersInfo
organization={org}
members={[]}
invites={[invite]}
currentUserRole="owner"
currentUserId="user-1"
isAccessControlAllowed={true}
isFormbricksCloud={true}
isUserManagementDisabledFromUi={false}
/>
);
expect(screen.getByTestId("member-actions")).toBeInTheDocument();
});
});
@@ -1,326 +0,0 @@
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { useRouter } from "next/navigation";
import toast from "react-hot-toast";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { TOrganization } from "@formbricks/types/organizations";
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
import { inviteUserAction, leaveOrganizationAction } from "@/modules/organization/settings/teams/actions";
import { InviteMemberModal } from "@/modules/organization/settings/teams/components/invite-member/invite-member-modal";
import { OrganizationActions } from "./organization-actions";
// Mock the next/navigation module
vi.mock("next/navigation", () => ({
useRouter: vi.fn(),
}));
// Mock the actions
vi.mock("@/modules/organization/settings/teams/actions", () => ({
inviteUserAction: vi.fn(),
leaveOrganizationAction: vi.fn(),
}));
// Mock the InviteMemberModal
vi.mock("@/modules/organization/settings/teams/components/invite-member/invite-member-modal", () => ({
InviteMemberModal: vi.fn(({ open, setOpen, onSubmit }) => {
if (!open) return null;
return (
<div data-testid="invite-member-modal">
<button
data-testid="invite-submit-btn"
onClick={() =>
onSubmit([{ email: "test@example.com", name: "Test User", role: "admin", teamIds: [] }])
}>
Submit
</button>
<button data-testid="invite-close-btn" onClick={() => setOpen(false)}>
Close
</button>
</div>
);
}),
}));
// Mock the Dialog components
vi.mock("@/modules/ui/components/dialog", () => ({
Dialog: vi.fn(({ children, open, onOpenChange }) => {
if (!open) return null;
return (
<div data-testid="leave-org-modal" onClick={() => onOpenChange && onOpenChange(false)}>
{children}
</div>
);
}),
DialogContent: vi.fn(({ children }) => <div data-testid="dialog-content">{children}</div>),
DialogHeader: vi.fn(({ children }) => <div data-testid="dialog-header">{children}</div>),
DialogTitle: vi.fn(({ children }) => <div data-testid="dialog-title">{children}</div>),
DialogDescription: vi.fn(({ children }) => <div data-testid="dialog-description">{children}</div>),
DialogFooter: vi.fn(({ children }) => <div data-testid="dialog-footer">{children}</div>),
}));
// Mock react-hot-toast
vi.mock("react-hot-toast", () => ({
default: {
success: vi.fn(),
error: vi.fn(),
},
}));
// Mock localStorage
const localStorageMock = (() => {
let store = {};
return {
getItem: vi.fn((key) => store[key] || null),
setItem: vi.fn((key, value) => {
store[key] = value.toString();
}),
removeItem: vi.fn((key) => {
delete store[key];
}),
clear: vi.fn(() => {
store = {};
}),
};
})();
Object.defineProperty(window, "localStorage", { value: localStorageMock });
// Mock tolgee
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key) => key,
}),
}));
// Mock Button component
vi.mock("@/modules/ui/components/button", () => ({
Button: vi.fn(({ children, onClick, loading, disabled, ...props }) => (
<button
onClick={onClick}
disabled={disabled || loading}
data-testid={
props.variant === "destructive"
? "leave-org-confirm-btn"
: props.variant === "secondary" && children === "common.cancel"
? "leave-org-cancel-btn"
: undefined
}
{...props}>
{children}
</button>
)),
}));
describe("OrganizationActions Component", () => {
const mockRouter = {
push: vi.fn(),
refresh: vi.fn(),
};
const defaultProps = {
role: "member" as const,
membershipRole: "member" as const,
isLeaveOrganizationDisabled: false,
organization: { id: "org-123", name: "Test Org" } as TOrganization,
teams: [{ id: "team-1", name: "Team 1" }],
isInviteDisabled: false,
isAccessControlAllowed: true,
isFormbricksCloud: false,
environmentId: "env-123",
isMultiOrgEnabled: true,
isUserManagementDisabledFromUi: false,
isStorageConfigured: true,
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useRouter).mockReturnValue(mockRouter as unknown as AppRouterInstance);
});
afterEach(() => {
cleanup();
});
test("renders without crashing", () => {
render(<OrganizationActions {...defaultProps} />);
expect(screen.getByText("environments.settings.general.leave_organization")).toBeInTheDocument();
});
test("does not show leave organization button when role is owner", () => {
render(<OrganizationActions {...defaultProps} role="owner" />);
expect(screen.queryByText("environments.settings.general.leave_organization")).not.toBeInTheDocument();
});
test("does not show leave organization button when multi-org is disabled", () => {
render(<OrganizationActions {...defaultProps} isMultiOrgEnabled={false} />);
expect(screen.queryByText("environments.settings.general.leave_organization")).not.toBeInTheDocument();
});
test("does not show invite button when isInviteDisabled is true", () => {
render(<OrganizationActions {...defaultProps} isInviteDisabled={true} />);
expect(screen.queryByText("environments.settings.teams.invite_member")).not.toBeInTheDocument();
});
test("does not show invite button when user is not owner or manager", () => {
render(<OrganizationActions {...defaultProps} membershipRole="member" />);
expect(screen.queryByText("environments.settings.teams.invite_member")).not.toBeInTheDocument();
});
test("shows invite button when user is owner", () => {
render(<OrganizationActions {...defaultProps} membershipRole="owner" />);
expect(screen.getByText("environments.settings.teams.invite_member")).toBeInTheDocument();
});
test("shows invite button when user is manager", () => {
render(<OrganizationActions {...defaultProps} membershipRole="manager" />);
expect(screen.getByText("environments.settings.teams.invite_member")).toBeInTheDocument();
});
test("opens invite member modal when clicking the invite button", () => {
render(<OrganizationActions {...defaultProps} membershipRole="owner" />);
fireEvent.click(screen.getByText("environments.settings.teams.invite_member"));
expect(screen.getByTestId("invite-member-modal")).toBeInTheDocument();
});
test("opens leave organization modal when clicking the leave button", () => {
render(<OrganizationActions {...defaultProps} />);
fireEvent.click(screen.getByText("environments.settings.general.leave_organization"));
expect(screen.getByTestId("leave-org-modal")).toBeInTheDocument();
});
test("handles successful member invite", async () => {
vi.mocked(inviteUserAction).mockResolvedValue({ data: "invite-123" });
render(<OrganizationActions {...defaultProps} membershipRole="owner" />);
fireEvent.click(screen.getByText("environments.settings.teams.invite_member"));
fireEvent.click(screen.getByTestId("invite-submit-btn"));
await waitFor(() => {
expect(inviteUserAction).toHaveBeenCalledWith({
organizationId: "org-123",
email: "test@example.com",
name: "Test User",
role: "admin",
teamIds: [],
});
expect(toast.success).toHaveBeenCalledWith("environments.settings.general.member_invited_successfully");
});
});
test("handles failed member invite", async () => {
vi.mocked(inviteUserAction).mockResolvedValue({ serverError: "Failed to invite user" });
render(<OrganizationActions {...defaultProps} membershipRole="owner" />);
fireEvent.click(screen.getByText("environments.settings.teams.invite_member"));
fireEvent.click(screen.getByTestId("invite-submit-btn"));
await waitFor(() => {
expect(inviteUserAction).toHaveBeenCalled();
expect(toast.error).toHaveBeenCalled();
});
});
test("handles leave organization successfully", async () => {
vi.mocked(leaveOrganizationAction).mockResolvedValue({
data: [
{
userId: "123",
role: "admin",
teamId: "team-1",
},
],
});
render(<OrganizationActions {...defaultProps} />);
fireEvent.click(screen.getByText("environments.settings.general.leave_organization"));
fireEvent.click(screen.getByTestId("leave-org-confirm-btn"));
await waitFor(() => {
expect(leaveOrganizationAction).toHaveBeenCalledWith({ organizationId: "org-123" });
expect(toast.success).toHaveBeenCalledWith("environments.settings.general.member_deleted_successfully");
expect(localStorage.removeItem).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS);
expect(mockRouter.push).toHaveBeenCalledWith("/");
expect(mockRouter.refresh).toHaveBeenCalled();
});
});
test("handles leave organization error", async () => {
const mockError = new Error("Failed to leave organization");
vi.mocked(leaveOrganizationAction).mockRejectedValue(mockError);
render(<OrganizationActions {...defaultProps} />);
fireEvent.click(screen.getByText("environments.settings.general.leave_organization"));
fireEvent.click(screen.getByTestId("leave-org-confirm-btn"));
await waitFor(() => {
expect(leaveOrganizationAction).toHaveBeenCalledWith({ organizationId: "org-123" });
expect(toast.error).toHaveBeenCalledWith("Error: Failed to leave organization");
});
});
test("cannot leave organization when only one organization is present", () => {
render(<OrganizationActions {...defaultProps} isMultiOrgEnabled={false} />);
expect(screen.queryByText("environments.settings.general.leave_organization")).not.toBeInTheDocument();
});
test("invite member modal closes on close button click", () => {
render(<OrganizationActions {...defaultProps} membershipRole="owner" />);
fireEvent.click(screen.getByText("environments.settings.teams.invite_member"));
expect(screen.getByTestId("invite-member-modal")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("invite-close-btn"));
expect(screen.queryByTestId("invite-member-modal")).not.toBeInTheDocument();
});
test("leave organization modal closes on cancel", () => {
render(<OrganizationActions {...defaultProps} />);
fireEvent.click(screen.getByText("environments.settings.general.leave_organization"));
expect(screen.getByTestId("leave-org-modal")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("leave-org-cancel-btn"));
expect(screen.queryByTestId("leave-org-modal")).not.toBeInTheDocument();
});
test("leave organization button is disabled and warning shown when isLeaveOrganizationDisabled is true", () => {
render(<OrganizationActions {...defaultProps} isLeaveOrganizationDisabled={true} />);
fireEvent.click(screen.getByText("environments.settings.general.leave_organization"));
expect(screen.getByTestId("leave-org-modal")).toBeInTheDocument();
expect(
screen.getByText("environments.settings.general.cannot_leave_only_organization")
).toBeInTheDocument();
});
test("invite button is hidden when isUserManagementDisabledFromUi is true", () => {
render(
<OrganizationActions {...defaultProps} membershipRole="owner" isUserManagementDisabledFromUi={true} />
);
expect(screen.queryByText("environments.settings.teams.invite_member")).not.toBeInTheDocument();
});
test("invite button is hidden when membershipRole is undefined", () => {
render(<OrganizationActions {...defaultProps} membershipRole={undefined} />);
expect(screen.queryByText("environments.settings.teams.invite_member")).not.toBeInTheDocument();
});
test("invite member modal receives correct props", () => {
render(<OrganizationActions {...defaultProps} membershipRole="owner" />);
fireEvent.click(screen.getByText("environments.settings.teams.invite_member"));
const modal = screen.getByTestId("invite-member-modal");
expect(modal).toBeInTheDocument();
const calls = vi.mocked(InviteMemberModal).mock.calls;
expect(
calls.some((call) =>
expect
.objectContaining({
environmentId: "env-123",
isAccessControlAllowed: true,
isFormbricksCloud: false,
teams: expect.arrayContaining(defaultProps.teams),
membershipRole: "owner",
open: true,
setOpen: expect.any(Function),
onSubmit: expect.any(Function),
})
.asymmetricMatch(call[0])
)
).toBe(true);
});
});
@@ -1,197 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { BulkInviteTab } from "./bulk-invite-tab";
// Hoisted fns for mocks to avoid hoisting pitfalls
const h = vi.hoisted(() => ({
mockParse: vi.fn(),
mockToastError: vi.fn(),
}));
// Mocks
vi.mock("papaparse", () => ({
default: { parse: h.mockParse },
}));
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({ t: (k: string) => k }),
}));
vi.mock("react-hot-toast", () => ({
default: { error: h.mockToastError },
}));
vi.mock("@/modules/organization/settings/teams/types/invites", () => ({
ZInvitees: { parse: vi.fn() },
}));
let lastUploaderProps: any;
vi.mock("@/modules/ui/components/file-input/components/uploader", () => ({
Uploader: vi.fn((props: any) => {
lastUploaderProps = props;
return (
<div data-testid="uploader-mock">
<input data-testid="upload-file-input" />
</div>
);
}),
}));
describe("BulkInviteTab", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
lastUploaderProps = undefined;
});
const baseProps = {
setOpen: vi.fn(),
onSubmit: vi.fn(),
isAccessControlAllowed: true,
isFormbricksCloud: true,
isStorageConfigured: true,
};
test("renders Uploader with correct props", () => {
render(<BulkInviteTab {...baseProps} />);
expect(screen.getByTestId("uploader-mock")).toBeInTheDocument();
expect(lastUploaderProps).toEqual(
expect.objectContaining({
allowedFileExtensions: ["csv"],
id: "bulk-invite",
name: "bulk-invite",
multiple: false,
disabled: false,
isStorageConfigured: true,
})
);
});
test("selecting a CSV shows filename, disables uploader, enables import; removing clears it", async () => {
render(<BulkInviteTab {...baseProps} />);
const user = userEvent.setup();
const file = new File(["name,email,role\nA,a@example.com,manager"], "people.csv", {
type: "text/csv",
});
// Simulate upload via mocked Uploader
lastUploaderProps.handleUpload([file]);
// Filename visible
expect(await screen.findByText("people.csv")).toBeInTheDocument();
// Uploader should be disabled after selection (component re-renders)
await waitFor(() => expect(lastUploaderProps.disabled).toBe(true));
// Import button enabled
const importButton = screen.getByRole("button", { name: /common.import/i });
expect(importButton).toBeEnabled();
// Remove file (icon-only button near filename)
const nameEl = screen.getByText("people.csv");
const container = nameEl.closest("div") as HTMLElement;
const removeBtn = within(container).getByRole("button");
await user.click(removeBtn);
expect(screen.queryByText("people.csv")).not.toBeInTheDocument();
});
test("onImport parses CSV and calls onSubmit (access control allowed)", async () => {
render(<BulkInviteTab {...baseProps} />);
const file = new File(["dummy"], "people.csv", { type: "text/csv" });
lastUploaderProps.handleUpload([file]);
// Mock Papa.parse to synchronously call complete with parsed rows
h.mockParse.mockImplementation((_file: File, opts: any) => {
opts.complete({
data: [
{ name: " Alice ", email: " alice@example.com ", role: " Manager " },
{ name: "Bob", email: "bob@example.com", role: "member" },
],
});
});
const importButton = screen.getByRole("button", { name: /common.import/i });
await userEvent.click(importButton);
expect(baseProps.onSubmit).toHaveBeenCalledWith([
{ name: "Alice", email: "alice@example.com", role: "manager", teamIds: [] },
{ name: "Bob", email: "bob@example.com", role: "member", teamIds: [] },
]);
expect(baseProps.setOpen).toHaveBeenCalledWith(false);
});
test("onImport forces owner when access control not allowed", async () => {
const props = { ...baseProps, isAccessControlAllowed: false };
render(<BulkInviteTab {...props} />);
const file = new File(["dummy"], "people.csv", { type: "text/csv" });
lastUploaderProps.handleUpload([file]);
h.mockParse.mockImplementation((_file: File, opts: any) => {
opts.complete({
data: [{ name: "Carol", email: "carol@example.com", role: "admin" }],
});
});
const importButton = screen.getByRole("button", { name: /common.import/i });
await userEvent.click(importButton);
expect(props.onSubmit).toHaveBeenCalledWith([
{ name: "Carol", email: "carol@example.com", role: "owner", teamIds: [] },
]);
});
test("onImport maps billing to owner when not Formbricks Cloud", async () => {
const props = { ...baseProps, isFormbricksCloud: false };
render(<BulkInviteTab {...props} />);
const file = new File(["dummy"], "people.csv", { type: "text/csv" });
lastUploaderProps.handleUpload([file]);
h.mockParse.mockImplementation((_file: File, opts: any) => {
opts.complete({
data: [{ name: "Dave", email: "dave@example.com", role: "billing" }],
});
});
const importButton = screen.getByRole("button", { name: /common.import/i });
await userEvent.click(importButton);
expect(props.onSubmit).toHaveBeenCalledWith([
{ name: "Dave", email: "dave@example.com", role: "owner", teamIds: [] },
]);
});
test("invalid drop file type shows toast error", async () => {
render(<BulkInviteTab {...baseProps} />);
// Call handleDrop with a non-csv file
const evt = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
dataTransfer: { files: [new File(["x"], "image.png", { type: "image/png" })] },
} as any;
await lastUploaderProps.handleDrop(evt);
expect(h.mockToastError).toHaveBeenCalled();
});
test("remove file button clears selection", async () => {
render(<BulkInviteTab {...baseProps} />);
const user = userEvent.setup();
const file = new File(["x"], "people.csv", { type: "text/csv" });
lastUploaderProps.handleUpload([file]);
// Locate the container that shows filename and its button
const nameEl = await screen.findByText("people.csv");
const container = nameEl.closest("div") as HTMLElement;
const removeButton = within(container).getByRole("button");
await user.click(removeButton);
expect(screen.queryByText("people.csv")).not.toBeInTheDocument();
});
});
@@ -1,117 +0,0 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { IndividualInviteTab } from "./individual-invite-tab";
const t = (k: string) => k;
vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t }) }));
vi.mock("@/modules/ee/role-management/components/add-member-role", () => ({
AddMemberRole: () => <div data-testid="add-member-role">AddMemberRole</div>,
}));
vi.mock("@/modules/ui/components/multi-select", () => ({
MultiSelect: ({ value, options, onChange, disabled }: any) => (
<select
data-testid="multi-select"
value={value}
disabled={disabled}
onChange={(e) => onChange([e.target.value])}>
{options.map((opt: any) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
),
}));
const defaultProps = {
setOpen: vi.fn(),
onSubmit: vi.fn(),
teams: [
{ id: "team-1", name: "Team 1" },
{ id: "team-2", name: "Team 2" },
],
isAccessControlAllowed: true,
isFormbricksCloud: true,
environmentId: "env-1",
membershipRole: "owner" as TOrganizationRole,
};
describe("IndividualInviteTab", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders form fields and buttons", () => {
render(<IndividualInviteTab {...defaultProps} />);
expect(screen.getByLabelText("common.full_name")).toBeInTheDocument();
expect(screen.getByLabelText("common.email")).toBeInTheDocument();
expect(screen.getByTestId("add-member-role")).toBeInTheDocument();
expect(screen.getByText("common.cancel")).toBeInTheDocument();
expect(screen.getByText("common.invite")).toBeInTheDocument();
});
test("submits valid form and calls onSubmit", async () => {
render(<IndividualInviteTab {...defaultProps} />);
await userEvent.type(screen.getByLabelText("common.full_name"), "Test User");
await userEvent.type(screen.getByLabelText("common.email"), "test@example.com");
fireEvent.submit(screen.getByRole("button", { name: "common.invite" }).closest("form")!);
await waitFor(() =>
expect(defaultProps.onSubmit).toHaveBeenCalledWith([
expect.objectContaining({ name: "Test User", email: "test@example.com", role: "member" }),
])
);
expect(defaultProps.setOpen).toHaveBeenCalledWith(false);
});
test("shows error for empty name", async () => {
render(<IndividualInviteTab {...defaultProps} />);
await userEvent.type(screen.getByLabelText("common.email"), "test@example.com");
fireEvent.submit(screen.getByRole("button", { name: "common.invite" }).closest("form")!);
expect(await screen.findByText("Name should be at least 1 character long")).toBeInTheDocument();
});
test("shows error for invalid email", async () => {
render(<IndividualInviteTab {...defaultProps} />);
await userEvent.type(screen.getByLabelText("common.full_name"), "Test User");
await userEvent.type(screen.getByLabelText("common.email"), "not-an-email");
fireEvent.submit(screen.getByRole("button", { name: "common.invite" }).closest("form")!);
expect(await screen.findByText(/Invalid email/)).toBeInTheDocument();
});
test("shows member role info alert when role is member", async () => {
render(<IndividualInviteTab {...defaultProps} isAccessControlAllowed={true} />);
await userEvent.type(screen.getByLabelText("common.full_name"), "Test User");
await userEvent.type(screen.getByLabelText("common.email"), "test@example.com");
// Simulate selecting member role
// Not needed as default is member if isAccessControlAllowed is true
expect(screen.getByText("environments.settings.teams.member_role_info_message")).toBeInTheDocument();
});
test("shows team select when isAccessControlAllowed is true", () => {
render(<IndividualInviteTab {...defaultProps} isAccessControlAllowed={true} />);
expect(screen.getByTestId("multi-select")).toBeInTheDocument();
});
test("shows upgrade alert when isAccessControlAllowed is false", () => {
render(<IndividualInviteTab {...defaultProps} isAccessControlAllowed={false} />);
expect(screen.getByText("environments.settings.teams.upgrade_plan_notice_message")).toBeInTheDocument();
expect(screen.getByText("common.start_free_trial")).toBeInTheDocument();
});
test("shows team select placeholder and message when no teams", () => {
render(<IndividualInviteTab {...defaultProps} teams={[]} />);
expect(screen.getByText("environments.settings.teams.create_first_team_message")).toBeInTheDocument();
});
test("cancel button closes modal", async () => {
render(<IndividualInviteTab {...defaultProps} />);
userEvent.click(screen.getByText("common.cancel"));
await waitFor(() => expect(defaultProps.setOpen).toHaveBeenCalledWith(false));
});
});
@@ -1,88 +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 { TOrganizationRole } from "@formbricks/types/memberships";
import { InviteMemberModal } from "./invite-member-modal";
const t = (k: string) => k;
vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t }) }));
vi.mock("./bulk-invite-tab", () => ({
BulkInviteTab: () => <div data-testid="bulk-invite-tab">BulkInviteTab</div>,
}));
vi.mock("./individual-invite-tab", () => ({
IndividualInviteTab: () => <div data-testid="individual-invite-tab">IndividualInviteTab</div>,
}));
vi.mock("@/modules/ui/components/dialog", () => ({
Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) =>
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 }: { children: React.ReactNode }) => (
<div data-testid="dialog-header">{children}</div>
),
DialogTitle: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-title">{children}</div>
),
DialogDescription: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-description">{children}</div>
),
DialogBody: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="dialog-body" className={className}>
{children}
</div>
),
}));
vi.mock("@/modules/ui/components/tab-toggle", () => ({
TabToggle: ({ options, onChange, defaultSelected }: any) => (
<select data-testid="tab-toggle" value={defaultSelected} onChange={(e) => onChange(e.target.value)}>
{options.map((opt: any) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
),
}));
const defaultProps = {
open: true,
setOpen: vi.fn(),
onSubmit: vi.fn(),
teams: [],
isAccessControlAllowed: true,
isFormbricksCloud: true,
environmentId: "env-1",
membershipRole: "owner" as TOrganizationRole,
isStorageConfigured: true,
};
describe("InviteMemberModal", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders dialog and individual tab by default", () => {
render(<InviteMemberModal {...defaultProps} />);
expect(screen.getByTestId("dialog")).toBeInTheDocument();
expect(screen.getByTestId("dialog-content")).toBeInTheDocument();
expect(screen.getByTestId("dialog-header")).toBeInTheDocument();
expect(screen.getByTestId("dialog-title")).toBeInTheDocument();
expect(screen.getByTestId("dialog-description")).toBeInTheDocument();
expect(screen.getByTestId("dialog-body")).toBeInTheDocument();
expect(screen.getByTestId("individual-invite-tab")).toBeInTheDocument();
expect(screen.getByTestId("tab-toggle")).toBeInTheDocument();
});
test("renders correct texts", () => {
render(<InviteMemberModal {...defaultProps} />);
expect(screen.getByText("environments.settings.teams.invite_member")).toBeInTheDocument();
expect(screen.getByText("environments.settings.teams.invite_member_description")).toBeInTheDocument();
});
});
@@ -1,74 +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 { ShareInviteModal } from "./share-invite-modal";
const t = (k: string) => k;
vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t }) }));
vi.mock("@/modules/ui/components/dialog", () => ({
Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) =>
open ? <div data-testid="dialog">{children}</div> : null,
DialogContent: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-content">{children}</div>
),
DialogHeader: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="dialog-header" className={className}>
{children}
</div>
),
DialogTitle: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="dialog-title" className={className}>
{children}
</div>
),
DialogDescription: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-description">{children}</div>
),
DialogBody: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-body">{children}</div>
),
DialogFooter: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-footer">{children}</div>
),
}));
const defaultProps = {
inviteToken: "test-token",
open: true,
setOpen: vi.fn(),
};
describe("ShareInviteModal", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
test("renders dialog and invite link", () => {
render(<ShareInviteModal {...defaultProps} />);
expect(screen.getByTestId("dialog")).toBeInTheDocument();
expect(screen.getByTestId("dialog-content")).toBeInTheDocument();
expect(screen.getByTestId("dialog-header")).toBeInTheDocument();
expect(screen.getByTestId("dialog-title")).toBeInTheDocument();
expect(screen.getByTestId("dialog-description")).toBeInTheDocument();
expect(screen.getByTestId("dialog-body")).toBeInTheDocument();
expect(
screen.getByText("environments.settings.general.organization_invite_link_ready")
).toBeInTheDocument();
expect(
screen.getByText(
"environments.settings.general.share_this_link_to_let_your_organization_member_join_your_organization"
)
).toBeInTheDocument();
expect(screen.getByText("common.copy_link")).toBeInTheDocument();
});
test("calls setOpen when dialog is closed", () => {
render(<ShareInviteModal {...defaultProps} open={false} />);
expect(screen.queryByTestId("dialog")).not.toBeInTheDocument();
});
});
@@ -1,119 +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 { TOrganizationRole } from "@formbricks/types/memberships";
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
import { getTeamsByOrganizationId } from "@/modules/ee/teams/team-list/lib/team";
import { getMembershipsByUserId } from "@/modules/organization/settings/teams/lib/membership";
import { MembersLoading, MembersView } from "./members-view";
vi.mock("@/app/(app)/environments/[environmentId]/settings/components/SettingsCard", () => ({
SettingsCard: ({ title, description, children }: any) => (
<div data-testid="SettingsCard">
<div>{title}</div>
<div>{description}</div>
{children}
</div>
),
}));
vi.mock("@/lib/constants", () => ({
INVITE_DISABLED: false,
IS_FORMBRICKS_CLOUD: true,
IS_STORAGE_CONFIGURED: true,
}));
vi.mock("@/modules/organization/settings/teams/components/edit-memberships/organization-actions", () => ({
OrganizationActions: (props: any) => <div data-testid="OrganizationActions">{JSON.stringify(props)}</div>,
}));
vi.mock("@/modules/organization/settings/teams/components/edit-memberships", () => ({
EditMemberships: (props: any) => <div data-testid="EditMemberships">{JSON.stringify(props)}</div>,
}));
vi.mock("@/tolgee/server", () => ({
getTranslate: async () => (key: string) => key,
}));
vi.mock("@/modules/organization/settings/teams/lib/membership", () => ({
getMembershipsByUserId: vi.fn(),
}));
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
getIsMultiOrgEnabled: vi.fn(),
}));
vi.mock("@/modules/ee/teams/team-list/lib/team", () => ({
getTeamsByOrganizationId: vi.fn(),
}));
describe("MembersView", () => {
afterEach(() => {
cleanup();
});
const baseProps = {
membershipRole: "owner",
organization: { id: "org-1", name: "Test Org" },
currentUserId: "user-1",
environmentId: "env-1",
isAccessControlAllowed: true,
isUserManagementDisabledFromUi: false,
} as any;
const mockMembership = {
organizationId: "org-1",
userId: "user-1",
accepted: true,
role: "owner" as TOrganizationRole,
};
test("renders SettingsCard and children with correct props", async () => {
vi.mocked(getMembershipsByUserId).mockResolvedValue([mockMembership]);
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
vi.mocked(getTeamsByOrganizationId).mockResolvedValue([{ id: "t1", name: "Team 1" }]);
const ui = await MembersView(baseProps);
render(ui);
expect(screen.getByTestId("SettingsCard")).toBeInTheDocument();
expect(screen.getByText("environments.settings.general.manage_members")).toBeInTheDocument();
expect(screen.getByText("environments.settings.general.manage_members_description")).toBeInTheDocument();
expect(screen.getByTestId("OrganizationActions")).toBeInTheDocument();
expect(screen.getByTestId("EditMemberships")).toBeInTheDocument();
});
test("disables leave organization if only one membership", async () => {
vi.mocked(getMembershipsByUserId).mockResolvedValue([]);
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
vi.mocked(getTeamsByOrganizationId).mockResolvedValue([{ id: "t1", name: "Team 1" }]);
const ui = await MembersView(baseProps);
render(ui);
expect(screen.getByTestId("OrganizationActions").textContent).toContain(
'"isLeaveOrganizationDisabled":true'
);
});
test("does not render OrganizationActions or EditMemberships if no membershipRole", async () => {
vi.mocked(getMembershipsByUserId).mockResolvedValue([mockMembership]);
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
vi.mocked(getTeamsByOrganizationId).mockResolvedValue([{ id: "t1", name: "Team 1" }]);
const ui = await MembersView({ ...baseProps, membershipRole: undefined });
render(ui);
expect(screen.queryByTestId("OrganizationActions")).toBeNull();
expect(screen.queryByTestId("EditMemberships")).toBeNull();
});
});
describe("MembersLoading", () => {
afterEach(() => {
cleanup();
});
test("renders two skeleton loaders", () => {
const { container } = render(<MembersLoading />);
const skeletons = container.querySelectorAll(".animate-pulse");
expect(skeletons.length).toBe(2);
expect(skeletons[0]).toHaveClass("h-8", "w-80", "rounded-full", "bg-slate-200");
});
});
@@ -1,93 +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 { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { TeamsPage } from "./page";
vi.mock(
"@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar",
() => ({
OrganizationSettingsNavbar: (props) => <div data-testid="org-navbar">OrgNavbar-{props.activeId}</div>,
})
);
vi.mock("@/lib/constants", () => ({
USER_MANAGEMENT_MINIMUM_ROLE: "owner",
IS_FORMBRICKS_CLOUD: 1,
ENCRYPTION_KEY: "test-key",
ENTERPRISE_LICENSE_KEY: "test-enterprise-key",
}));
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
getAccessControlPermission: vi.fn(),
}));
vi.mock("@/modules/ee/teams/team-list/components/teams-view", () => ({
TeamsView: (props) => <div data-testid="teams-view">TeamsView-{props.organizationId}</div>,
}));
vi.mock("@/modules/environments/lib/utils", () => ({
getEnvironmentAuth: vi.fn(),
}));
vi.mock("@/modules/organization/settings/teams/components/members-view", () => ({
MembersView: (props) => <div data-testid="members-view">MembersView-{props.membershipRole}</div>,
}));
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
PageContentWrapper: ({ children }) => <div data-testid="content-wrapper">{children}</div>,
}));
vi.mock("@/modules/ui/components/page-header", () => ({
PageHeader: ({ children, pageTitle }) => (
<div data-testid="page-header">
<span>{pageTitle}</span>
{children}
</div>
),
}));
vi.mock("@/tolgee/server", () => ({
getTranslate: async () => (key: string) => key,
}));
const mockParams = { environmentId: "env-1" };
const mockOrg = { id: "org-1", billing: { plan: "free" } };
const mockMembership = { role: "owner" };
const mockSession = { user: { id: "user-1" } };
describe("TeamsPage", () => {
afterEach(() => {
cleanup();
});
test("renders all main components and passes props", async () => {
vi.mocked(getEnvironmentAuth).mockResolvedValue({
session: mockSession,
currentUserMembership: mockMembership,
organization: mockOrg,
} as any);
vi.mocked(getAccessControlPermission).mockResolvedValue(true);
const props = { params: Promise.resolve(mockParams) };
render(await TeamsPage(props));
expect(screen.getByTestId("content-wrapper")).toBeInTheDocument();
expect(screen.getByTestId("page-header")).toBeInTheDocument();
expect(screen.getByTestId("org-navbar")).toHaveTextContent("OrgNavbar-teams");
expect(screen.getByTestId("members-view")).toHaveTextContent("MembersView-owner");
expect(screen.getByTestId("teams-view")).toHaveTextContent("TeamsView-org-1");
expect(screen.getByText("environments.settings.general.organization_settings")).toBeInTheDocument();
});
test("passes correct props to role management util", async () => {
vi.mocked(getEnvironmentAuth).mockResolvedValue({
session: mockSession,
currentUserMembership: mockMembership,
organization: mockOrg,
} as any);
vi.mocked(getAccessControlPermission).mockResolvedValue(false);
const props = { params: Promise.resolve(mockParams) };
render(await TeamsPage(props));
expect(getAccessControlPermission).toHaveBeenCalledWith("free");
});
});