mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-08 06:41:45 -05:00
chore: exclude TSX files from unit test coverage (#6723)
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
-292
@@ -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);
|
||||
});
|
||||
});
|
||||
-161
@@ -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");
|
||||
});
|
||||
});
|
||||
-118
@@ -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();
|
||||
});
|
||||
});
|
||||
-203
@@ -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();
|
||||
});
|
||||
});
|
||||
-326
@@ -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);
|
||||
});
|
||||
});
|
||||
-197
@@ -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();
|
||||
});
|
||||
});
|
||||
-117
@@ -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));
|
||||
});
|
||||
});
|
||||
-88
@@ -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();
|
||||
});
|
||||
});
|
||||
-74
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user