mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-02 11:30:31 -05:00
chore: add tests to environments path - part 2 (#5667)
This commit is contained in:
+456
@@ -0,0 +1,456 @@
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationAirtable,
|
||||
TIntegrationAirtableConfigData,
|
||||
TIntegrationAirtableCredential,
|
||||
TIntegrationAirtableTables,
|
||||
} from "@formbricks/types/integration/airtable";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { AddIntegrationModal } from "./AddIntegrationModal";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown",
|
||||
() => ({
|
||||
BaseSelectDropdown: ({ control, airtableArray, fetchTable, defaultValue, setValue }) => (
|
||||
<div>
|
||||
<label htmlFor="base">Base</label>
|
||||
<select
|
||||
id="base"
|
||||
defaultValue={defaultValue}
|
||||
onChange={(e) => {
|
||||
control._mockOnChange({ target: { name: "base", value: e.target.value } });
|
||||
setValue("table", ""); // Reset table when base changes
|
||||
fetchTable(e.target.value);
|
||||
}}>
|
||||
<option value="">Select Base</option>
|
||||
{airtableArray.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable", () => ({
|
||||
fetchTables: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: (value, _locale) => value?.default || value || "",
|
||||
}));
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
replaceHeadlineRecall: (survey, _locale) => survey,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/additional-integration-settings", () => ({
|
||||
AdditionalIntegrationSettings: ({
|
||||
includeVariables,
|
||||
setIncludeVariables,
|
||||
includeHiddenFields,
|
||||
setIncludeHiddenFields,
|
||||
includeMetadata,
|
||||
setIncludeMetadata,
|
||||
includeCreatedAt,
|
||||
setIncludeCreatedAt,
|
||||
}) => (
|
||||
<div data-testid="additional-settings">
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid="include-variables"
|
||||
checked={includeVariables}
|
||||
onChange={(e) => setIncludeVariables(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid="include-hidden"
|
||||
checked={includeHiddenFields}
|
||||
onChange={(e) => setIncludeHiddenFields(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid="include-metadata"
|
||||
checked={includeMetadata}
|
||||
onChange={(e) => setIncludeMetadata(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid="include-createdat"
|
||||
checked={includeCreatedAt}
|
||||
onChange={(e) => setIncludeCreatedAt(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: ({ children, open, setOpen }) =>
|
||||
open ? (
|
||||
<div data-testid="modal">
|
||||
{children}
|
||||
<button onClick={() => setOpen(false)}>Close Modal</button>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/alert", () => ({
|
||||
Alert: ({ children }) => <div data-testid="alert">{children}</div>,
|
||||
AlertTitle: ({ children }) => <div data-testid="alert-title">{children}</div>,
|
||||
AlertDescription: ({ children }) => <div data-testid="alert-description">{children}</div>,
|
||||
}));
|
||||
vi.mock("next/image", () => ({
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
default: (props) => <img alt="test" {...props} />,
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: vi.fn(() => ({ refresh: vi.fn() })),
|
||||
}));
|
||||
|
||||
// Mock the Select component used for Table and Survey selections
|
||||
vi.mock("@/modules/ui/components/select", () => ({
|
||||
Select: ({ children }) => (
|
||||
// Render children, assuming Controller passes props to the Trigger/Value
|
||||
// The actual select logic will be handled by the mocked Controller/field
|
||||
// We need to simulate the structure expected by the Controller render prop
|
||||
<div>{children}</div>
|
||||
),
|
||||
SelectTrigger: ({ children, ...props }) => <div {...props}>{children}</div>, // Mock Trigger
|
||||
SelectValue: ({ placeholder }) => <span>{placeholder || "Select..."}</span>, // Mock Value display
|
||||
SelectContent: ({ children }) => <div>{children}</div>, // Mock Content wrapper
|
||||
SelectItem: ({ children, value, ...props }) => (
|
||||
// Mock Item - crucial for userEvent.selectOptions if we were using a real select
|
||||
// For Controller, the value change is handled by field.onChange directly
|
||||
<div data-value={value} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock react-hook-form Controller to render a simple select
|
||||
vi.mock("react-hook-form", async () => {
|
||||
const actual = await vi.importActual("react-hook-form");
|
||||
let fields = {};
|
||||
const mockReset = vi.fn((values) => {
|
||||
fields = values || {}; // Reset fields, optionally with new values
|
||||
});
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useForm: vi.fn((options) => {
|
||||
fields = options?.defaultValues || {};
|
||||
const mockControlOnChange = (event) => {
|
||||
if (event && event.target) {
|
||||
fields[event.target.name] = event.target.value;
|
||||
}
|
||||
};
|
||||
return {
|
||||
handleSubmit: (fn) => (e) => {
|
||||
e?.preventDefault();
|
||||
fn(fields);
|
||||
},
|
||||
control: {
|
||||
_mockOnChange: mockControlOnChange,
|
||||
// Add other necessary control properties if needed
|
||||
register: vi.fn(),
|
||||
unregister: vi.fn(),
|
||||
getFieldState: vi.fn(() => ({ invalid: false, isDirty: false, isTouched: false, error: null })),
|
||||
_names: { mount: new Set(), unMount: new Set(), array: new Set(), watch: new Set() },
|
||||
_options: {},
|
||||
_proxyFormState: {
|
||||
isDirty: false,
|
||||
isValidating: false,
|
||||
dirtyFields: {},
|
||||
touchedFields: {},
|
||||
errors: {},
|
||||
},
|
||||
_formState: { isDirty: false, isValidating: false, dirtyFields: {}, touchedFields: {}, errors: {} },
|
||||
_updateFormState: vi.fn(),
|
||||
_updateFieldArray: vi.fn(),
|
||||
_executeSchema: vi.fn().mockResolvedValue({ errors: {}, values: {} }),
|
||||
_getWatch: vi.fn(),
|
||||
_subjects: {
|
||||
watch: { subscribe: vi.fn() },
|
||||
array: { subscribe: vi.fn() },
|
||||
state: { subscribe: vi.fn() },
|
||||
},
|
||||
_getDirty: vi.fn(),
|
||||
_reset: vi.fn(),
|
||||
_removeUnmounted: vi.fn(),
|
||||
},
|
||||
watch: (name) => fields[name],
|
||||
setValue: (name, value) => {
|
||||
fields[name] = value;
|
||||
},
|
||||
reset: mockReset,
|
||||
formState: { errors: {}, isDirty: false, isValid: true, isSubmitting: false },
|
||||
getValues: (name) => (name ? fields[name] : fields),
|
||||
};
|
||||
}),
|
||||
Controller: ({ name, defaultValue }) => {
|
||||
// Initialize field value if not already set by reset/defaultValues
|
||||
if (fields[name] === undefined && defaultValue !== undefined) {
|
||||
fields[name] = defaultValue;
|
||||
}
|
||||
|
||||
const field = {
|
||||
onChange: (valueOrEvent) => {
|
||||
const value = valueOrEvent?.target ? valueOrEvent.target.value : valueOrEvent;
|
||||
fields[name] = value;
|
||||
// Re-render might be needed here in a real scenario, but testing library handles it
|
||||
},
|
||||
onBlur: vi.fn(),
|
||||
value: fields[name],
|
||||
name: name,
|
||||
ref: vi.fn(),
|
||||
};
|
||||
|
||||
// Find the corresponding label to associate with the select
|
||||
const labelId = name; // Assuming label 'for' matches field name
|
||||
const labelText =
|
||||
name === "table" ? "environments.integrations.airtable.table_name" : "common.select_survey";
|
||||
|
||||
// Render a simple select element instead of the complex component
|
||||
// This makes interaction straightforward with userEvent.selectOptions
|
||||
return (
|
||||
<>
|
||||
{/* The actual label is rendered outside the Controller in the component */}
|
||||
<select
|
||||
id={labelId}
|
||||
aria-label={labelText} // Use aria-label for accessibility in tests
|
||||
{...field} // Spread field props
|
||||
defaultValue={defaultValue} // Pass defaultValue
|
||||
>
|
||||
{/* Need to dynamically get options based on context, simplified here */}
|
||||
{name === "table" &&
|
||||
mockTables.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name}
|
||||
</option>
|
||||
))}
|
||||
{name === "survey" &&
|
||||
mockSurveys.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
},
|
||||
reset: mockReset,
|
||||
};
|
||||
});
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const mockSurveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
name: "Survey 1",
|
||||
questions: [
|
||||
{ id: "q1", headline: { default: "Question 1" } },
|
||||
{ id: "q2", headline: { default: "Question 2" } },
|
||||
],
|
||||
hiddenFields: { enabled: true, fieldIds: ["hf1"] },
|
||||
variables: { enabled: true, fieldIds: ["var1"] },
|
||||
} as any,
|
||||
{
|
||||
id: "survey2",
|
||||
name: "Survey 2",
|
||||
questions: [{ id: "q3", headline: { default: "Question 3" } }],
|
||||
hiddenFields: { enabled: false },
|
||||
variables: { enabled: false },
|
||||
} as any,
|
||||
];
|
||||
const mockAirtableArray: TIntegrationItem[] = [
|
||||
{ id: "base1", name: "Base 1" },
|
||||
{ id: "base2", name: "Base 2" },
|
||||
];
|
||||
const mockAirtableIntegration: TIntegrationAirtable = {
|
||||
id: "integration1",
|
||||
type: "airtable",
|
||||
environmentId,
|
||||
config: {
|
||||
key: { access_token: "abc" } as TIntegrationAirtableCredential,
|
||||
email: "test@test.com",
|
||||
data: [],
|
||||
},
|
||||
};
|
||||
const mockTables: TIntegrationAirtableTables["tables"] = [
|
||||
{ id: "table1", name: "Table 1" },
|
||||
{ id: "table2", name: "Table 2" },
|
||||
];
|
||||
const mockSetOpenWithStates = vi.fn();
|
||||
const mockRouterRefresh = vi.fn();
|
||||
|
||||
describe("AddIntegrationModal", () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(useRouter).mockReturnValue({ refresh: mockRouterRefresh } as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders in add mode correctly", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={mockAirtableArray}
|
||||
surveys={mockSurveys}
|
||||
airtableIntegration={mockAirtableIntegration}
|
||||
isEditMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("environments.integrations.airtable.link_airtable_table")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Base")).toBeInTheDocument();
|
||||
// Use getByLabelText for the mocked selects
|
||||
expect(screen.getByLabelText("environments.integrations.airtable.table_name")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("common.select_survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.save")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.cancel")).toBeInTheDocument();
|
||||
expect(screen.queryByText("common.delete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows 'No Base Found' error when airtableArray is empty", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={[]}
|
||||
surveys={mockSurveys}
|
||||
airtableIntegration={mockAirtableIntegration}
|
||||
isEditMode={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("alert-title")).toHaveTextContent(
|
||||
"environments.integrations.airtable.no_bases_found"
|
||||
);
|
||||
});
|
||||
|
||||
test("shows 'No Surveys Found' warning when surveys array is empty", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={mockAirtableArray}
|
||||
surveys={[]}
|
||||
airtableIntegration={mockAirtableIntegration}
|
||||
isEditMode={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("environments.integrations.create_survey_warning")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("fetches and displays tables when a base is selected", async () => {
|
||||
vi.mocked(fetchTables).mockResolvedValue({ tables: mockTables });
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={mockAirtableArray}
|
||||
surveys={mockSurveys}
|
||||
airtableIntegration={mockAirtableIntegration}
|
||||
isEditMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const baseSelect = screen.getByLabelText("Base");
|
||||
await userEvent.selectOptions(baseSelect, "base1");
|
||||
|
||||
expect(fetchTables).toHaveBeenCalledWith(environmentId, "base1");
|
||||
await waitFor(() => {
|
||||
// Use getByLabelText (mocked select)
|
||||
const tableSelect = screen.getByLabelText("environments.integrations.airtable.table_name");
|
||||
expect(tableSelect).toBeEnabled();
|
||||
// Check options within the mocked select
|
||||
expect(tableSelect.querySelector("option[value='table1']")).toBeInTheDocument();
|
||||
expect(tableSelect.querySelector("option[value='table2']")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles deletion in edit mode", async () => {
|
||||
const initialData: TIntegrationAirtableConfigData = {
|
||||
baseId: "base1",
|
||||
tableId: "table1",
|
||||
surveyId: "survey1",
|
||||
questionIds: ["q1"],
|
||||
questions: "common.selected_questions",
|
||||
tableName: "Table 1",
|
||||
surveyName: "Survey 1",
|
||||
createdAt: new Date(),
|
||||
includeVariables: false,
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: false,
|
||||
includeCreatedAt: true,
|
||||
};
|
||||
const integrationWithData = {
|
||||
...mockAirtableIntegration,
|
||||
config: { ...mockAirtableIntegration.config, data: [initialData] },
|
||||
};
|
||||
const defaultData = { ...initialData, index: 0 } as any;
|
||||
|
||||
vi.mocked(fetchTables).mockResolvedValue({ tables: mockTables });
|
||||
vi.mocked(createOrUpdateIntegrationAction).mockResolvedValue({ ok: true, data: {} } as any);
|
||||
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={mockAirtableArray}
|
||||
surveys={mockSurveys}
|
||||
airtableIntegration={integrationWithData}
|
||||
isEditMode={true}
|
||||
defaultData={defaultData}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => expect(fetchTables).toHaveBeenCalled()); // Wait for initial load
|
||||
|
||||
// Click delete
|
||||
await userEvent.click(screen.getByText("common.delete"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationAction).toHaveBeenCalledTimes(1);
|
||||
const submittedData = vi.mocked(createOrUpdateIntegrationAction).mock.calls[0][0].integrationData;
|
||||
// Expect data array to be empty after deletion
|
||||
expect(submittedData.config.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully");
|
||||
expect(mockSetOpenWithStates).toHaveBeenCalledWith(false);
|
||||
expect(mockRouterRefresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles cancel button click", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
open={true}
|
||||
setOpenWithStates={mockSetOpenWithStates}
|
||||
environmentId={environmentId}
|
||||
airtableArray={mockAirtableArray}
|
||||
surveys={mockSurveys}
|
||||
airtableIntegration={mockAirtableIntegration}
|
||||
isEditMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText("common.cancel"));
|
||||
expect(mockSetOpenWithStates).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { AirtableWrapper } from "./AirtableWrapper";
|
||||
|
||||
// Mock child components
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration",
|
||||
() => ({
|
||||
ManageIntegration: ({ setIsConnected }) => (
|
||||
<div data-testid="manage-integration">
|
||||
<button onClick={() => setIsConnected(false)}>Disconnect</button>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
ConnectIntegration: ({ handleAuthorization, isEnabled }) => (
|
||||
<div data-testid="connect-integration">
|
||||
<button onClick={handleAuthorization} disabled={!isEnabled}>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock library function
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable", () => ({
|
||||
authorize: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock image import
|
||||
vi.mock("@/images/airtableLogo.svg", () => ({
|
||||
default: "airtable-logo-path",
|
||||
}));
|
||||
|
||||
// Mock window.location.replace
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
replace: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const webAppUrl = "https://app.formbricks.com";
|
||||
const environment = { id: environmentId } as TEnvironment;
|
||||
const surveys = [];
|
||||
const airtableArray = [];
|
||||
const locale = "en-US" as const;
|
||||
|
||||
const baseProps = {
|
||||
environmentId,
|
||||
airtableArray,
|
||||
surveys,
|
||||
environment,
|
||||
webAppUrl,
|
||||
locale,
|
||||
};
|
||||
|
||||
describe("AirtableWrapper", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when not connected (no integration)", () => {
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={true} airtableIntegration={undefined} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when not connected (integration without key)", () => {
|
||||
const integrationWithoutKey = { config: {} } as TIntegrationAirtable;
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={true} airtableIntegration={integrationWithoutKey} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration disabled when isEnabled is false", () => {
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={false} airtableIntegration={undefined} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeDisabled();
|
||||
});
|
||||
|
||||
test("calls authorize and redirects when Connect button is clicked", async () => {
|
||||
const mockAuthorize = vi.mocked(authorize);
|
||||
const redirectUrl = "https://airtable.com/auth";
|
||||
mockAuthorize.mockResolvedValue(redirectUrl);
|
||||
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={true} airtableIntegration={undefined} />);
|
||||
|
||||
const connectButton = screen.getByRole("button", { name: "Connect" });
|
||||
await userEvent.click(connectButton);
|
||||
|
||||
expect(mockAuthorize).toHaveBeenCalledWith(environmentId, webAppUrl);
|
||||
await vi.waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith(redirectUrl);
|
||||
});
|
||||
});
|
||||
|
||||
test("renders ManageIntegration when connected", () => {
|
||||
const connectedIntegration = {
|
||||
id: "int-1",
|
||||
config: { key: { access_token: "abc" }, email: "test@test.com", data: [] },
|
||||
} as unknown as TIntegrationAirtable;
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={true} airtableIntegration={connectedIntegration} />);
|
||||
expect(screen.getByTestId("manage-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("connect-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("switches from ManageIntegration to ConnectIntegration when disconnected", async () => {
|
||||
const connectedIntegration = {
|
||||
id: "int-1",
|
||||
config: { key: { access_token: "abc" }, email: "test@test.com", data: [] },
|
||||
} as unknown as TIntegrationAirtable;
|
||||
render(<AirtableWrapper {...baseProps} isEnabled={true} airtableIntegration={connectedIntegration} />);
|
||||
|
||||
// Initially, ManageIntegration is shown
|
||||
expect(screen.getByTestId("manage-integration")).toBeInTheDocument();
|
||||
|
||||
// Simulate disconnection via ManageIntegration's button
|
||||
const disconnectButton = screen.getByRole("button", { name: "Disconnect" });
|
||||
await userEvent.click(disconnectButton);
|
||||
|
||||
// Now, ConnectIntegration should be shown
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { IntegrationModalInputs } from "./AddIntegrationModal";
|
||||
import { BaseSelectDropdown } from "./BaseSelectDropdown";
|
||||
|
||||
// Mock UI components
|
||||
vi.mock("@/modules/ui/components/label", () => ({
|
||||
Label: ({ children, htmlFor }: { children: React.ReactNode; htmlFor: string }) => (
|
||||
<label htmlFor={htmlFor}>{children}</label>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/select", () => ({
|
||||
Select: ({ children, onValueChange, disabled, defaultValue }) => (
|
||||
<select
|
||||
data-testid="base-select"
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
defaultValue={defaultValue}>
|
||||
{children}
|
||||
</select>
|
||||
),
|
||||
SelectTrigger: ({ children }) => <div>{children}</div>,
|
||||
SelectValue: () => <span>SelectValueMock</span>,
|
||||
SelectContent: ({ children }) => <div>{children}</div>,
|
||||
SelectItem: ({ children, value }) => <option value={value}>{children}</option>,
|
||||
}));
|
||||
|
||||
// Mock react-hook-form's Controller specifically
|
||||
vi.mock("react-hook-form", async () => {
|
||||
const actual = await vi.importActual("react-hook-form");
|
||||
// Keep the actual useForm
|
||||
const originalUseForm = actual.useForm;
|
||||
|
||||
// Mock Controller
|
||||
const MockController = ({ name, _, render, defaultValue }) => {
|
||||
// Minimal mock: call render with a basic field object
|
||||
const field = {
|
||||
onChange: vi.fn(), // Simple spy for field.onChange
|
||||
onBlur: vi.fn(),
|
||||
value: defaultValue, // Use defaultValue passed to Controller
|
||||
name: name,
|
||||
ref: vi.fn(),
|
||||
};
|
||||
// The component passes the render prop result to the actual Select component
|
||||
return render({ field });
|
||||
};
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useForm: originalUseForm, // Use the actual useForm
|
||||
Controller: MockController, // Use the mocked Controller
|
||||
};
|
||||
});
|
||||
|
||||
const mockAirtableArray: TIntegrationItem[] = [
|
||||
{ id: "base1", name: "Base One" },
|
||||
{ id: "base2", name: "Base Two" },
|
||||
];
|
||||
|
||||
const mockFetchTable = vi.fn();
|
||||
|
||||
// Use a wrapper component that utilizes the actual useForm
|
||||
const renderComponent = (
|
||||
isLoading = false,
|
||||
defaultValue: string | undefined = undefined,
|
||||
airtableArray = mockAirtableArray
|
||||
) => {
|
||||
const Component = () => {
|
||||
// Now uses the actual useForm because Controller is mocked separately
|
||||
const { control, setValue } = useForm<IntegrationModalInputs>({
|
||||
defaultValues: { base: defaultValue },
|
||||
});
|
||||
return (
|
||||
<BaseSelectDropdown
|
||||
control={control}
|
||||
isLoading={isLoading}
|
||||
fetchTable={mockFetchTable} // The spy
|
||||
airtableArray={airtableArray}
|
||||
setValue={setValue} // Actual RHF setValue
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return render(<Component />);
|
||||
};
|
||||
|
||||
describe("BaseSelectDropdown", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders the label and select trigger", () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText("environments.integrations.airtable.airtable_base")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("base-select")).toBeInTheDocument();
|
||||
expect(screen.getByText("SelectValueMock")).toBeInTheDocument(); // From mocked SelectValue
|
||||
});
|
||||
|
||||
test("renders options from airtableArray", () => {
|
||||
renderComponent();
|
||||
const select = screen.getByTestId("base-select");
|
||||
expect(select.querySelectorAll("option")).toHaveLength(mockAirtableArray.length);
|
||||
expect(screen.getByText("Base One")).toBeInTheDocument();
|
||||
expect(screen.getByText("Base Two")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("disables the select when isLoading is true", () => {
|
||||
renderComponent(true);
|
||||
expect(screen.getByTestId("base-select")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("enables the select when isLoading is false", () => {
|
||||
renderComponent(false);
|
||||
expect(screen.getByTestId("base-select")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("renders correctly with empty airtableArray", () => {
|
||||
renderComponent(false, undefined, []);
|
||||
const select = screen.getByTestId("base-select");
|
||||
expect(select.querySelectorAll("option")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TIntegrationAirtableTables } from "@formbricks/types/integration/airtable";
|
||||
import { authorize, fetchTables } from "./airtable";
|
||||
|
||||
// Mock the logger
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const baseId = "test-base-id";
|
||||
const apiHost = "http://localhost:3000";
|
||||
|
||||
describe("Airtable Library", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchTables", () => {
|
||||
test("should fetch tables successfully", async () => {
|
||||
const mockTables: TIntegrationAirtableTables = {
|
||||
tables: [
|
||||
{ id: "tbl1", name: "Table 1" },
|
||||
{ id: "tbl2", name: "Table 2" },
|
||||
],
|
||||
};
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: async () => ({ data: mockTables }),
|
||||
};
|
||||
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
||||
|
||||
const tables = await fetchTables(environmentId, baseId);
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(`/api/v1/integrations/airtable/tables?baseId=${baseId}`, {
|
||||
method: "GET",
|
||||
headers: { environmentId: environmentId },
|
||||
cache: "no-store",
|
||||
});
|
||||
expect(tables).toEqual(mockTables);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authorize", () => {
|
||||
test("should return authUrl successfully", async () => {
|
||||
const mockAuthUrl = "https://airtable.com/oauth2/v1/authorize?...";
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: async () => ({ data: { authUrl: mockAuthUrl } }),
|
||||
};
|
||||
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
||||
|
||||
const authUrl = await authorize(environmentId, apiHost);
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(`${apiHost}/api/v1/integrations/airtable`, {
|
||||
method: "GET",
|
||||
headers: { environmentId: environmentId },
|
||||
});
|
||||
expect(authUrl).toBe(mockAuthUrl);
|
||||
});
|
||||
|
||||
test("should throw error and log when fetch fails", async () => {
|
||||
const errorText = "Failed to fetch";
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
text: async () => errorText,
|
||||
};
|
||||
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
||||
|
||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(`${apiHost}/api/v1/integrations/airtable`, {
|
||||
method: "GET",
|
||||
headers: { environmentId: environmentId },
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith({ errorText }, "authorize: Could not fetch airtable config");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { getAirtableTables } from "@/lib/airtable/service";
|
||||
import { WEBAPP_URL } from "@/lib/constants";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable, TIntegrationAirtableCredential } from "@formbricks/types/integration/airtable";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import Page from "./page";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper", () => ({
|
||||
AirtableWrapper: vi.fn(() => <div>AirtableWrapper Mock</div>),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys");
|
||||
vi.mock("@/lib/airtable/service");
|
||||
|
||||
let mockAirtableClientId: string | undefined = "test-client-id";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
get AIRTABLE_CLIENT_ID() {
|
||||
return mockAirtableClientId;
|
||||
},
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
IS_PRODUCTION: true,
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/integration/service");
|
||||
vi.mock("@/lib/utils/locale");
|
||||
vi.mock("@/modules/environments/lib/utils");
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: vi.fn(() => <div>GoBackButton Mock</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: vi.fn(({ children }) => <div>{children}</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: vi.fn(({ pageTitle }) => <h1>{pageTitle}</h1>),
|
||||
}));
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
vi.mock("next/navigation");
|
||||
|
||||
const mockEnvironmentId = "test-env-id";
|
||||
const mockEnvironment = {
|
||||
id: mockEnvironmentId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
} as unknown as TEnvironment;
|
||||
const mockSurveys: TSurvey[] = [{ id: "survey1", name: "Survey 1" } as TSurvey];
|
||||
const mockAirtableIntegration: TIntegrationAirtable = {
|
||||
type: "airtable",
|
||||
config: {
|
||||
key: { access_token: "test-token" } as unknown as TIntegrationAirtableCredential,
|
||||
data: [],
|
||||
email: "test@example.com",
|
||||
},
|
||||
environmentId: mockEnvironmentId,
|
||||
id: "int_airtable_123",
|
||||
};
|
||||
const mockAirtableTables: TIntegrationItem[] = [{ id: "table1", name: "Table 1" } as TIntegrationItem];
|
||||
const mockLocale = "en-US";
|
||||
|
||||
const props = {
|
||||
params: {
|
||||
environmentId: mockEnvironmentId,
|
||||
},
|
||||
};
|
||||
|
||||
describe("Airtable Integration Page", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
vi.mocked(getSurveys).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getIntegrations).mockResolvedValue([mockAirtableIntegration]);
|
||||
vi.mocked(getAirtableTables).mockResolvedValue(mockAirtableTables);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("redirects if user is readOnly", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: true,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
await render(await Page(props));
|
||||
expect(redirect).toHaveBeenCalledWith("./");
|
||||
});
|
||||
|
||||
test("renders correctly when integration is configured", async () => {
|
||||
await render(await Page(props));
|
||||
|
||||
expect(screen.getByText("environments.integrations.airtable.airtable_integration")).toBeInTheDocument();
|
||||
expect(screen.getByText("GoBackButton Mock")).toBeInTheDocument();
|
||||
expect(screen.getByText("AirtableWrapper Mock")).toBeInTheDocument();
|
||||
|
||||
expect(vi.mocked(getEnvironmentAuth)).toHaveBeenCalledWith(mockEnvironmentId);
|
||||
expect(vi.mocked(getSurveys)).toHaveBeenCalledWith(mockEnvironmentId);
|
||||
expect(vi.mocked(getIntegrations)).toHaveBeenCalledWith(mockEnvironmentId);
|
||||
expect(vi.mocked(getAirtableTables)).toHaveBeenCalledWith(mockEnvironmentId);
|
||||
expect(vi.mocked(findMatchingLocale)).toHaveBeenCalled();
|
||||
|
||||
const AirtableWrapper = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"
|
||||
)
|
||||
).AirtableWrapper
|
||||
);
|
||||
expect(AirtableWrapper).toHaveBeenCalledWith(
|
||||
{
|
||||
isEnabled: true,
|
||||
airtableIntegration: mockAirtableIntegration,
|
||||
airtableArray: mockAirtableTables,
|
||||
environmentId: mockEnvironmentId,
|
||||
surveys: mockSurveys,
|
||||
environment: mockEnvironment,
|
||||
webAppUrl: WEBAPP_URL,
|
||||
locale: mockLocale,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("renders correctly when integration exists but is not configured (no key)", async () => {
|
||||
const integrationWithoutKey = {
|
||||
...mockAirtableIntegration,
|
||||
config: { ...mockAirtableIntegration.config, key: undefined },
|
||||
} as unknown as TIntegrationAirtable;
|
||||
vi.mocked(getIntegrations).mockResolvedValue([integrationWithoutKey]);
|
||||
|
||||
await render(await Page(props));
|
||||
|
||||
expect(screen.getByText("environments.integrations.airtable.airtable_integration")).toBeInTheDocument();
|
||||
expect(screen.getByText("AirtableWrapper Mock")).toBeInTheDocument();
|
||||
|
||||
expect(vi.mocked(getAirtableTables)).not.toHaveBeenCalled(); // Should not fetch tables if no key
|
||||
|
||||
const AirtableWrapper = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"
|
||||
)
|
||||
).AirtableWrapper
|
||||
);
|
||||
// Update assertion to match the actual call
|
||||
expect(AirtableWrapper).toHaveBeenCalledWith(
|
||||
{
|
||||
isEnabled: true, // isEnabled is true because AIRTABLE_CLIENT_ID is set in beforeEach
|
||||
airtableIntegration: integrationWithoutKey,
|
||||
airtableArray: [], // Should be empty as getAirtableTables is not called
|
||||
environmentId: mockEnvironmentId,
|
||||
surveys: mockSurveys,
|
||||
environment: mockEnvironment,
|
||||
webAppUrl: WEBAPP_URL,
|
||||
locale: mockLocale,
|
||||
},
|
||||
undefined // Change second argument to undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("renders correctly when integration is disabled (no client ID)", async () => {
|
||||
mockAirtableClientId = undefined; // Simulate disabled integration
|
||||
|
||||
await render(await Page(props));
|
||||
|
||||
expect(screen.getByText("environments.integrations.airtable.airtable_integration")).toBeInTheDocument();
|
||||
expect(screen.getByText("AirtableWrapper Mock")).toBeInTheDocument();
|
||||
|
||||
const AirtableWrapper = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"
|
||||
)
|
||||
).AirtableWrapper
|
||||
);
|
||||
expect(AirtableWrapper).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isEnabled: false, // Should be false
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
+694
@@ -0,0 +1,694 @@
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsConfigData,
|
||||
} from "@formbricks/types/integration/google-sheet";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock actions and utilities
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions", () => ({
|
||||
getSpreadsheetNameByIdAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util", () => ({
|
||||
constructGoogleSheetsUrl: (id: string) => `https://docs.google.com/spreadsheets/d/${id}`,
|
||||
extractSpreadsheetIdFromUrl: (url: string) => url.split("/")[5],
|
||||
isValidGoogleSheetsUrl: (url: string) => url.startsWith("https://docs.google.com/spreadsheets/d/"),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: (value: any, _locale: string) => value?.default || "",
|
||||
}));
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
replaceHeadlineRecall: (survey: any) => survey,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/additional-integration-settings", () => ({
|
||||
AdditionalIntegrationSettings: ({
|
||||
includeVariables,
|
||||
setIncludeVariables,
|
||||
includeHiddenFields,
|
||||
setIncludeHiddenFields,
|
||||
includeMetadata,
|
||||
setIncludeMetadata,
|
||||
includeCreatedAt,
|
||||
setIncludeCreatedAt,
|
||||
}: any) => (
|
||||
<div>
|
||||
<span>Additional Settings</span>
|
||||
<input
|
||||
data-testid="include-variables"
|
||||
type="checkbox"
|
||||
checked={includeVariables}
|
||||
onChange={(e) => setIncludeVariables(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-hidden-fields"
|
||||
type="checkbox"
|
||||
checked={includeHiddenFields}
|
||||
onChange={(e) => setIncludeHiddenFields(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-metadata"
|
||||
type="checkbox"
|
||||
checked={includeMetadata}
|
||||
onChange={(e) => setIncludeMetadata(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-created-at"
|
||||
type="checkbox"
|
||||
checked={includeCreatedAt}
|
||||
onChange={(e) => setIncludeCreatedAt(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/dropdown-selector", () => ({
|
||||
DropdownSelector: ({ label, items, selectedItem, setSelectedItem }: any) => (
|
||||
<div>
|
||||
<label>{label}</label>
|
||||
<select
|
||||
data-testid="survey-dropdown"
|
||||
value={selectedItem?.id || ""}
|
||||
onChange={(e) => {
|
||||
const selected = items.find((item: any) => item.id === e.target.value);
|
||||
setSelectedItem(selected);
|
||||
}}>
|
||||
<option value="">Select a survey</option>
|
||||
{items.map((item: any) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: ({ open, children }: { open: boolean; children: React.ReactNode }) =>
|
||||
open ? <div data-testid="modal">{children}</div> : null,
|
||||
}));
|
||||
vi.mock("next/image", () => ({
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
|
||||
}));
|
||||
vi.mock("react-hook-form", () => ({
|
||||
useForm: () => ({
|
||||
handleSubmit: (callback: any) => (event: any) => {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@tolgee/react", async () => {
|
||||
const MockTolgeeProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
const useTranslate = () => ({
|
||||
t: (key: string, _?: any) => {
|
||||
// NOSONAR
|
||||
// Simple mock translation function
|
||||
if (key === "common.all_questions") return "All questions";
|
||||
if (key === "common.selected_questions") return "Selected questions";
|
||||
if (key === "environments.integrations.google_sheets.link_google_sheet") return "Link Google Sheet";
|
||||
if (key === "common.update") return "Update";
|
||||
if (key === "common.delete") return "Delete";
|
||||
if (key === "common.cancel") return "Cancel";
|
||||
if (key === "environments.integrations.google_sheets.spreadsheet_url") return "Spreadsheet URL";
|
||||
if (key === "common.select_survey") return "Select survey";
|
||||
if (key === "common.questions") return "Questions";
|
||||
if (key === "environments.integrations.google_sheets.enter_a_valid_spreadsheet_url_error")
|
||||
return "Please enter a valid Google Sheet URL.";
|
||||
if (key === "environments.integrations.please_select_a_survey_error") return "Please select a survey.";
|
||||
if (key === "environments.integrations.select_at_least_one_question_error")
|
||||
return "Please select at least one question.";
|
||||
if (key === "environments.integrations.integration_updated_successfully")
|
||||
return "Integration updated successfully.";
|
||||
if (key === "environments.integrations.integration_added_successfully")
|
||||
return "Integration added successfully.";
|
||||
if (key === "environments.integrations.integration_removed_successfully")
|
||||
return "Integration removed successfully.";
|
||||
if (key === "environments.integrations.google_sheets.google_sheet_logo") return "Google Sheet logo";
|
||||
if (key === "environments.integrations.google_sheets.google_sheets_integration_description")
|
||||
return "Sync responses with Google Sheets.";
|
||||
if (key === "environments.integrations.create_survey_warning")
|
||||
return "You need to create a survey first.";
|
||||
return key; // Return key if no translation is found
|
||||
},
|
||||
});
|
||||
return { TolgeeProvider: MockTolgeeProvider, useTranslate };
|
||||
});
|
||||
|
||||
// Mock dependencies
|
||||
const createOrUpdateIntegrationAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/integrations/actions"))
|
||||
.createOrUpdateIntegrationAction
|
||||
);
|
||||
const getSpreadsheetNameByIdAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions"))
|
||||
.getSpreadsheetNameByIdAction
|
||||
);
|
||||
const toast = vi.mocked((await import("react-hot-toast")).default);
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const mockSetOpen = vi.fn();
|
||||
|
||||
const surveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 1",
|
||||
type: "app",
|
||||
environmentId: environmentId,
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1?" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 2?" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Choice 1" } },
|
||||
{ id: "c2", label: { default: "Choice 2" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
variables: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
{
|
||||
id: "survey2",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 2",
|
||||
type: "link",
|
||||
environmentId: environmentId,
|
||||
status: "draft",
|
||||
questions: [
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate this?" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
variables: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const mockGoogleSheetIntegration = {
|
||||
id: "integration1",
|
||||
type: "googleSheets",
|
||||
config: {
|
||||
key: {
|
||||
access_token: "mock_access_token",
|
||||
expiry_date: Date.now() + 3600000,
|
||||
refresh_token: "mock_refresh_token",
|
||||
scope: "mock_scope",
|
||||
token_type: "Bearer",
|
||||
},
|
||||
email: "test@example.com",
|
||||
data: [], // Initially empty, will be populated in beforeEach
|
||||
},
|
||||
} as unknown as TIntegrationGoogleSheets;
|
||||
|
||||
const mockSelectedIntegration: TIntegrationGoogleSheetsConfigData & { index: number } = {
|
||||
spreadsheetId: "existing-sheet-id",
|
||||
spreadsheetName: "Existing Sheet",
|
||||
surveyId: surveys[0].id,
|
||||
surveyName: surveys[0].name,
|
||||
questionIds: [surveys[0].questions[0].id],
|
||||
questions: "Selected questions",
|
||||
createdAt: new Date(),
|
||||
includeVariables: true,
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: true,
|
||||
includeCreatedAt: false,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
describe("AddIntegrationModal", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset integration data before each test if needed
|
||||
mockGoogleSheetIntegration.config.data = [
|
||||
{ ...mockSelectedIntegration }, // Simulate existing data for update/delete tests
|
||||
];
|
||||
});
|
||||
|
||||
test("renders correctly when open (create mode)", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Link Google Sheet", { selector: "div.text-xl.font-medium" })
|
||||
).toBeInTheDocument();
|
||||
// Use getByPlaceholderText for the input
|
||||
expect(
|
||||
screen.getByPlaceholderText("https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>")
|
||||
).toBeInTheDocument();
|
||||
// Use getByTestId for the dropdown
|
||||
expect(screen.getByTestId("survey-dropdown")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Link Google Sheet" })).toBeInTheDocument();
|
||||
expect(screen.queryByText("Delete")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Questions")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly when open (update mode)", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Link Google Sheet", { selector: "div.text-xl.font-medium" })
|
||||
).toBeInTheDocument();
|
||||
// Use getByPlaceholderText for the input
|
||||
expect(
|
||||
screen.getByPlaceholderText("https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>")
|
||||
).toHaveValue("https://docs.google.com/spreadsheets/d/existing-sheet-id");
|
||||
expect(screen.getByTestId("survey-dropdown")).toHaveValue(surveys[0].id);
|
||||
expect(screen.getByText("Questions")).toBeInTheDocument();
|
||||
expect(screen.getByText("Delete")).toBeInTheDocument();
|
||||
expect(screen.getByText("Update")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("include-variables")).toBeChecked();
|
||||
expect(screen.getByTestId("include-hidden-fields")).not.toBeChecked();
|
||||
expect(screen.getByTestId("include-metadata")).toBeChecked();
|
||||
expect(screen.getByTestId("include-created-at")).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("selects survey and shows questions", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[1].id);
|
||||
|
||||
expect(screen.getByText("Questions")).toBeInTheDocument();
|
||||
surveys[1].questions.forEach((q) => {
|
||||
expect(screen.getByLabelText(q.headline.default)).toBeInTheDocument();
|
||||
// Initially all questions should be checked when a survey is selected in create mode
|
||||
expect(screen.getByLabelText(q.headline.default)).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles question selection", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
const firstQuestionCheckbox = screen.getByLabelText(surveys[0].questions[0].headline.default);
|
||||
expect(firstQuestionCheckbox).toBeChecked(); // Initially checked
|
||||
|
||||
await userEvent.click(firstQuestionCheckbox);
|
||||
expect(firstQuestionCheckbox).not.toBeChecked(); // Unchecked after click
|
||||
|
||||
await userEvent.click(firstQuestionCheckbox);
|
||||
expect(firstQuestionCheckbox).toBeChecked(); // Checked again
|
||||
});
|
||||
|
||||
test("creates integration successfully", async () => {
|
||||
getSpreadsheetNameByIdAction.mockResolvedValue({ data: "Test Sheet Name" });
|
||||
createOrUpdateIntegrationAction.mockResolvedValue({ data: null as any }); // Mock successful action
|
||||
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={{
|
||||
...mockGoogleSheetIntegration,
|
||||
config: { ...mockGoogleSheetIntegration.config, data: [] },
|
||||
}} // Start with empty data
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Google Sheet" });
|
||||
|
||||
await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/new-sheet-id");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
// Wait for questions to appear and potentially uncheck one
|
||||
const firstQuestionCheckbox = await screen.findByLabelText(surveys[0].questions[0].headline.default);
|
||||
await userEvent.click(firstQuestionCheckbox); // Uncheck first question
|
||||
|
||||
// Check additional settings
|
||||
await userEvent.click(screen.getByTestId("include-variables"));
|
||||
await userEvent.click(screen.getByTestId("include-metadata"));
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getSpreadsheetNameByIdAction).toHaveBeenCalledWith({
|
||||
googleSheetIntegration: expect.any(Object),
|
||||
environmentId,
|
||||
spreadsheetId: "new-sheet-id",
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationAction).toHaveBeenCalledWith({
|
||||
environmentId,
|
||||
integrationData: expect.objectContaining({
|
||||
type: "googleSheets",
|
||||
config: expect.objectContaining({
|
||||
key: mockGoogleSheetIntegration.config.key,
|
||||
email: mockGoogleSheetIntegration.config.email,
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
spreadsheetId: "new-sheet-id",
|
||||
spreadsheetName: "Test Sheet Name",
|
||||
surveyId: surveys[0].id,
|
||||
surveyName: surveys[0].name,
|
||||
questionIds: surveys[0].questions.slice(1).map((q) => q.id), // Excludes the first question
|
||||
questions: "Selected questions",
|
||||
includeVariables: true,
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: true,
|
||||
includeCreatedAt: true, // Default
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Integration added successfully.");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes integration successfully", async () => {
|
||||
createOrUpdateIntegrationAction.mockResolvedValue({ data: null as any });
|
||||
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration} // Contains initial data at index 0
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteButton = screen.getByText("Delete");
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationAction).toHaveBeenCalledWith({
|
||||
environmentId,
|
||||
integrationData: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
data: [], // Data array should be empty after deletion
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Integration removed successfully.");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("shows validation error for invalid URL", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Google Sheet" });
|
||||
|
||||
await userEvent.type(urlInput, "invalid-url");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please enter a valid Google Sheet URL.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationAction).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows validation error if no survey selected", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const submitButton = screen.getByRole("button", { name: "Link Google Sheet" });
|
||||
|
||||
await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/some-id");
|
||||
// No survey selected
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select a survey.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationAction).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows validation error if no questions selected", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Google Sheet" });
|
||||
|
||||
await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/some-id");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
// Uncheck all questions
|
||||
for (const question of surveys[0].questions) {
|
||||
const checkbox = await screen.findByLabelText(question.headline.default);
|
||||
await userEvent.click(checkbox);
|
||||
}
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select at least one question.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationAction).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows error toast if createOrUpdateIntegrationAction fails", async () => {
|
||||
const errorMessage = "Failed to update integration";
|
||||
getSpreadsheetNameByIdAction.mockResolvedValue({ data: "Some Sheet Name" });
|
||||
createOrUpdateIntegrationAction.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Google Sheet" });
|
||||
|
||||
await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/another-id");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getSpreadsheetNameByIdAction).toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationAction).toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls setOpen(false) and resets form on cancel", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
// Use getByPlaceholderText for the input
|
||||
const urlInput = screen.getByPlaceholderText(
|
||||
"https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
|
||||
);
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
|
||||
// Simulate some interaction
|
||||
await userEvent.type(urlInput, "https://docs.google.com/spreadsheets/d/temp-id");
|
||||
await userEvent.click(cancelButton);
|
||||
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
// Re-render with open=true to check if state was reset (URL should be empty)
|
||||
cleanup();
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
// Use getByPlaceholderText for the input check after re-render
|
||||
expect(
|
||||
screen.getByPlaceholderText("https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>")
|
||||
).toHaveValue("");
|
||||
});
|
||||
});
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsCredential,
|
||||
} from "@formbricks/types/integration/google-sheet";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock child components and functions
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration",
|
||||
() => ({
|
||||
ManageIntegration: vi.fn(({ setOpenAddIntegrationModal }) => (
|
||||
<div data-testid="manage-integration">
|
||||
<button onClick={() => setOpenAddIntegrationModal(true)}>Open Modal</button>
|
||||
</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
ConnectIntegration: vi.fn(({ handleAuthorization }) => (
|
||||
<div data-testid="connect-integration">
|
||||
<button onClick={handleAuthorization}>Connect</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal",
|
||||
() => ({
|
||||
AddIntegrationModal: vi.fn(({ open }) =>
|
||||
open ? <div data-testid="add-integration-modal">Modal</div> : null
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google", () => ({
|
||||
authorize: vi.fn(() => Promise.resolve("http://google.com/auth")),
|
||||
}));
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
appSetupCompleted: false,
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockSurveys: TSurvey[] = [];
|
||||
const mockWebAppUrl = "http://localhost:3000";
|
||||
const mockLocale = "en-US";
|
||||
|
||||
const mockGoogleSheetIntegration = {
|
||||
id: "test-integration-id",
|
||||
type: "googleSheets",
|
||||
config: {
|
||||
key: { access_token: "test-token" } as unknown as TIntegrationGoogleSheetsCredential,
|
||||
data: [],
|
||||
email: "test@example.com",
|
||||
},
|
||||
} as unknown as TIntegrationGoogleSheets;
|
||||
|
||||
describe("GoogleSheetWrapper", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when not connected", () => {
|
||||
render(
|
||||
<GoogleSheetWrapper
|
||||
isEnabled={true}
|
||||
environment={mockEnvironment}
|
||||
surveys={mockSurveys}
|
||||
webAppUrl={mockWebAppUrl}
|
||||
locale={mockLocale}
|
||||
// No googleSheetIntegration provided initially
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("add-integration-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when integration exists but has no key", () => {
|
||||
const integrationWithoutKey = {
|
||||
...mockGoogleSheetIntegration,
|
||||
config: { data: [], email: "test" },
|
||||
} as unknown as TIntegrationGoogleSheets;
|
||||
render(
|
||||
<GoogleSheetWrapper
|
||||
isEnabled={true}
|
||||
environment={mockEnvironment}
|
||||
surveys={mockSurveys}
|
||||
googleSheetIntegration={integrationWithoutKey}
|
||||
webAppUrl={mockWebAppUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls authorize when connect button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
// Mock window.location.replace
|
||||
const originalLocation = window.location;
|
||||
// @ts-expect-error
|
||||
delete window.location;
|
||||
window.location = { ...originalLocation, replace: vi.fn() } as any;
|
||||
|
||||
render(
|
||||
<GoogleSheetWrapper
|
||||
isEnabled={true}
|
||||
environment={mockEnvironment}
|
||||
surveys={mockSurveys}
|
||||
webAppUrl={mockWebAppUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
const connectButton = screen.getByRole("button", { name: "Connect" });
|
||||
await user.click(connectButton);
|
||||
|
||||
expect(vi.mocked(authorize)).toHaveBeenCalledWith(mockEnvironment.id, mockWebAppUrl);
|
||||
// Need to wait for the promise returned by authorize to resolve
|
||||
await vi.waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith("http://google.com/auth");
|
||||
});
|
||||
|
||||
// Restore window.location
|
||||
window.location = originalLocation as any;
|
||||
});
|
||||
|
||||
test("renders ManageIntegration and AddIntegrationModal when connected", () => {
|
||||
render(
|
||||
<GoogleSheetWrapper
|
||||
isEnabled={true}
|
||||
environment={mockEnvironment}
|
||||
surveys={mockSurveys}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
webAppUrl={mockWebAppUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("manage-integration")).toBeInTheDocument();
|
||||
// Modal is rendered but initially hidden
|
||||
expect(screen.queryByTestId("add-integration-modal")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("connect-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("opens AddIntegrationModal when triggered from ManageIntegration", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<GoogleSheetWrapper
|
||||
isEnabled={true}
|
||||
environment={mockEnvironment}
|
||||
surveys={mockSurveys}
|
||||
googleSheetIntegration={mockGoogleSheetIntegration}
|
||||
webAppUrl={mockWebAppUrl}
|
||||
locale={mockLocale}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("add-integration-modal")).not.toBeInTheDocument();
|
||||
const openModalButton = screen.getByRole("button", { name: "Open Modal" }); // Button inside mocked ManageIntegration
|
||||
await user.click(openModalButton);
|
||||
expect(screen.getByTestId("add-integration-modal")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { authorize } from "./google";
|
||||
|
||||
// Mock the logger
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
const mockFetch = vi.fn();
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
describe("authorize", () => {
|
||||
const environmentId = "test-env-id";
|
||||
const apiHost = "http://test.com";
|
||||
const expectedUrl = `${apiHost}/api/google-sheet`;
|
||||
const expectedHeaders = { environmentId: environmentId };
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should return authUrl on successful fetch", async () => {
|
||||
const mockAuthUrl = "https://accounts.google.com/o/oauth2/v2/auth?...";
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { authUrl: mockAuthUrl } }),
|
||||
});
|
||||
|
||||
const authUrl = await authorize(environmentId, apiHost);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: expectedHeaders,
|
||||
});
|
||||
expect(authUrl).toBe(mockAuthUrl);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should throw error and log on failed fetch", async () => {
|
||||
const errorText = "Failed to fetch";
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
text: async () => errorText,
|
||||
});
|
||||
|
||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: expectedHeaders,
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ errorText },
|
||||
"authorize: Could not fetch google sheet config"
|
||||
);
|
||||
});
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { constructGoogleSheetsUrl, extractSpreadsheetIdFromUrl, isValidGoogleSheetsUrl } from "./util";
|
||||
|
||||
describe("Google Sheets Util", () => {
|
||||
describe("extractSpreadsheetIdFromUrl", () => {
|
||||
test("should extract spreadsheet ID from a valid URL", () => {
|
||||
const url =
|
||||
"https://docs.google.com/spreadsheets/d/1aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPq/edit#gid=0";
|
||||
const expectedId = "1aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPq";
|
||||
expect(extractSpreadsheetIdFromUrl(url)).toBe(expectedId);
|
||||
});
|
||||
|
||||
test("should throw an error for an invalid URL", () => {
|
||||
const invalidUrl = "https://not-a-google-sheet-url.com";
|
||||
expect(() => extractSpreadsheetIdFromUrl(invalidUrl)).toThrow("Invalid Google Sheets URL");
|
||||
});
|
||||
|
||||
test("should throw an error for a URL without an ID", () => {
|
||||
const urlWithoutId = "https://docs.google.com/spreadsheets/d/";
|
||||
expect(() => extractSpreadsheetIdFromUrl(urlWithoutId)).toThrow("Invalid Google Sheets URL");
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructGoogleSheetsUrl", () => {
|
||||
test("should construct a valid Google Sheets URL from a spreadsheet ID", () => {
|
||||
const spreadsheetId = "1aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPq";
|
||||
const expectedUrl =
|
||||
"https://docs.google.com/spreadsheets/d/1aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPq";
|
||||
expect(constructGoogleSheetsUrl(spreadsheetId)).toBe(expectedUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidGoogleSheetsUrl", () => {
|
||||
test("should return true for a valid Google Sheets URL", () => {
|
||||
const validUrl =
|
||||
"https://docs.google.com/spreadsheets/d/1aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPq/edit#gid=0";
|
||||
expect(isValidGoogleSheetsUrl(validUrl)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for an invalid URL", () => {
|
||||
const invalidUrl = "https://not-a-google-sheet-url.com";
|
||||
expect(isValidGoogleSheetsUrl(invalidUrl)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true for a base Google Sheets URL", () => {
|
||||
const baseUrl = "https://docs.google.com/spreadsheets/d/";
|
||||
expect(isValidGoogleSheetsUrl(baseUrl)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Loading from "./loading";
|
||||
|
||||
// Mock the GoBackButton component
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: () => <div>GoBackButton</div>,
|
||||
}));
|
||||
|
||||
describe("Loading", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the loading state correctly", () => {
|
||||
render(<Loading />);
|
||||
|
||||
// Check for GoBackButton mock
|
||||
expect(screen.getByText("GoBackButton")).toBeInTheDocument();
|
||||
|
||||
// Check for the disabled button text
|
||||
expect(screen.getByText("environments.integrations.google_sheets.link_new_sheet")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.google_sheets.link_new_sheet").closest("button")
|
||||
).toHaveClass("pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none");
|
||||
|
||||
// Check for table headers
|
||||
expect(screen.getByText("common.survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.google_sheets.google_sheet_name")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.questions")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.updated_at")).toBeInTheDocument();
|
||||
|
||||
// Check for placeholder elements (count based on the loop)
|
||||
const placeholders = screen.getAllByRole("generic", { hidden: true }); // Using generic role as divs don't have implicit roles
|
||||
// Calculate expected placeholders: 3 rows * 5 placeholders per row = 15
|
||||
// Plus the button, header divs (4), and the main containers
|
||||
// It's simpler to check if there are *any* pulse animations
|
||||
expect(placeholders.some((el) => el.classList.contains("animate-pulse"))).toBe(true);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -10,7 +10,7 @@ const Loading = () => {
|
||||
<div className="mt-6 p-6">
|
||||
<GoBackButton />
|
||||
<div className="mb-6 text-right">
|
||||
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
|
||||
<Button className="pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none">
|
||||
{t("environments.integrations.google_sheets.link_new_sheet")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@ const Loading = () => {
|
||||
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
|
||||
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="text-center"></div>
|
||||
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/page";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsCredential,
|
||||
} from "@formbricks/types/integration/google-sheet";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper",
|
||||
() => ({
|
||||
GoogleSheetWrapper: vi.fn(
|
||||
({ isEnabled, environment, surveys, googleSheetIntegration, webAppUrl, locale }) => (
|
||||
<div>
|
||||
<span>Mocked GoogleSheetWrapper</span>
|
||||
<span data-testid="isEnabled">{isEnabled.toString()}</span>
|
||||
<span data-testid="environmentId">{environment.id}</span>
|
||||
<span data-testid="surveyCount">{surveys?.length ?? 0}</span>
|
||||
<span data-testid="integrationId">{googleSheetIntegration?.id}</span>
|
||||
<span data-testid="webAppUrl">{webAppUrl}</span>
|
||||
<span data-testid="locale">{locale}</span>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
})
|
||||
);
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys", () => ({
|
||||
getSurveys: vi.fn(),
|
||||
}));
|
||||
|
||||
let mockGoogleSheetClientId: string | undefined = "test-client-id";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
get GOOGLE_SHEETS_CLIENT_ID() {
|
||||
return mockGoogleSheetClientId;
|
||||
},
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: "test-client-secret",
|
||||
GOOGLE_SHEETS_REDIRECT_URL: "test-redirect-url",
|
||||
}));
|
||||
vi.mock("@/lib/integration/service", () => ({
|
||||
getIntegrations: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/utils/locale", () => ({
|
||||
findMatchingLocale: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: vi.fn(({ url }) => <div data-testid="go-back">{url}</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: vi.fn(({ children }) => <div>{children}</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: vi.fn(({ pageTitle }) => <h1>{pageTitle}</h1>),
|
||||
}));
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
appSetupCompleted: false,
|
||||
type: "development",
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockSurveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
name: "Survey 1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "test-env-id",
|
||||
status: "inProgress",
|
||||
type: "app",
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
languages: [],
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
segment: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const mockGoogleSheetIntegration = {
|
||||
id: "integration1",
|
||||
type: "googleSheets",
|
||||
config: {
|
||||
data: [],
|
||||
key: {
|
||||
refresh_token: "refresh",
|
||||
access_token: "access",
|
||||
expiry_date: Date.now() + 3600000,
|
||||
} as unknown as TIntegrationGoogleSheetsCredential,
|
||||
email: "test@example.com",
|
||||
},
|
||||
} as unknown as TIntegrationGoogleSheets;
|
||||
|
||||
const mockProps = {
|
||||
params: { environmentId: "test-env-id" },
|
||||
};
|
||||
|
||||
describe("GoogleSheetsIntegrationPage", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
} as TEnvironmentAuth);
|
||||
vi.mocked(getSurveys).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getIntegrations).mockResolvedValue([mockGoogleSheetIntegration]);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
|
||||
});
|
||||
|
||||
test("renders the page with GoogleSheetWrapper when enabled and not read-only", async () => {
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(
|
||||
screen.getByText("environments.integrations.google_sheets.google_sheets_integration")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Mocked GoogleSheetWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("isEnabled")).toHaveTextContent("true");
|
||||
expect(screen.getByTestId("environmentId")).toHaveTextContent(mockEnvironment.id);
|
||||
expect(screen.getByTestId("surveyCount")).toHaveTextContent(mockSurveys.length.toString());
|
||||
expect(screen.getByTestId("integrationId")).toHaveTextContent(mockGoogleSheetIntegration.id);
|
||||
expect(screen.getByTestId("webAppUrl")).toHaveTextContent("test-webapp-url");
|
||||
expect(screen.getByTestId("locale")).toHaveTextContent("en-US");
|
||||
expect(screen.getByTestId("go-back")).toHaveTextContent(
|
||||
`test-webapp-url/environments/${mockProps.params.environmentId}/integrations`
|
||||
);
|
||||
expect(vi.mocked(redirect)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls redirect when user is read-only", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: true,
|
||||
} as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(vi.mocked(redirect)).toHaveBeenCalledWith("./");
|
||||
});
|
||||
|
||||
test("passes isEnabled=false to GoogleSheetWrapper when constants are missing", async () => {
|
||||
mockGoogleSheetClientId = undefined;
|
||||
|
||||
const { default: PageWithMissingConstants } = (await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/page"
|
||||
)) as { default: typeof Page };
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
} as TEnvironmentAuth);
|
||||
vi.mocked(getSurveys).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getIntegrations).mockResolvedValue([mockGoogleSheetIntegration]);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
|
||||
|
||||
const PageComponent = await PageWithMissingConstants(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByTestId("isEnabled")).toHaveTextContent("false");
|
||||
});
|
||||
|
||||
test("handles case where no Google Sheet integration exists", async () => {
|
||||
vi.mocked(getIntegrations).mockResolvedValue([]); // No integrations
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("Mocked GoogleSheetWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("integrationId")).toBeEmptyDOMElement(); // No integration ID passed
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
import { cache } from "@/lib/cache";
|
||||
import { surveyCache } from "@/lib/survey/cache";
|
||||
import { selectSurvey } from "@/lib/survey/service";
|
||||
import { transformPrismaSurvey } from "@/lib/survey/utils";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getSurveys } from "./surveys";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/cache");
|
||||
vi.mock("@/lib/survey/cache", () => ({
|
||||
surveyCache: {
|
||||
tag: {
|
||||
byEnvironmentId: vi.fn((environmentId) => `survey_environment_${environmentId}`),
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/survey/service", () => ({
|
||||
selectSurvey: { id: true, name: true, status: true, updatedAt: true }, // Expanded mock based on usage
|
||||
}));
|
||||
vi.mock("@/lib/survey/utils");
|
||||
vi.mock("@/lib/utils/validate");
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
survey: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("react", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("react")>();
|
||||
return {
|
||||
...actual,
|
||||
cache: vi.fn((fn) => fn), // Mock reactCache to just return the function
|
||||
};
|
||||
});
|
||||
|
||||
const environmentId = "test-environment-id";
|
||||
// Ensure mockPrismaSurveys includes all fields used in selectSurvey mock
|
||||
const mockPrismaSurveys = [
|
||||
{ id: "survey1", name: "Survey 1", status: "inProgress", updatedAt: new Date() },
|
||||
{ id: "survey2", name: "Survey 2", status: "draft", updatedAt: new Date() },
|
||||
];
|
||||
const mockTransformedSurveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
name: "Survey 1",
|
||||
status: "inProgress",
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: false },
|
||||
type: "app", // Changed type to web to match original file
|
||||
environmentId: environmentId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
languages: [],
|
||||
styling: null,
|
||||
} as unknown as TSurvey,
|
||||
{
|
||||
id: "survey2",
|
||||
name: "Survey 2",
|
||||
status: "draft",
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: false },
|
||||
type: "app",
|
||||
environmentId: environmentId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
languages: [],
|
||||
styling: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
describe("getSurveys", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(cache).mockImplementation((fn) => async () => {
|
||||
return fn();
|
||||
});
|
||||
});
|
||||
|
||||
test("should fetch and transform surveys successfully", async () => {
|
||||
vi.mocked(prisma.survey.findMany).mockResolvedValue(mockPrismaSurveys);
|
||||
vi.mocked(transformPrismaSurvey).mockImplementation((survey) => {
|
||||
const found = mockTransformedSurveys.find((ts) => ts.id === survey.id);
|
||||
if (!found) throw new Error("Survey not found in mock transformed data");
|
||||
// Ensure the returned object matches the TSurvey structure precisely
|
||||
return { ...found } as TSurvey;
|
||||
});
|
||||
|
||||
const surveys = await getSurveys(environmentId);
|
||||
|
||||
expect(surveys).toEqual(mockTransformedSurveys);
|
||||
// Use expect.any(ZId) for the Zod schema validation check
|
||||
expect(validateInputs).toHaveBeenCalledWith([environmentId, expect.any(Object)]); // Adjusted expectation
|
||||
expect(prisma.survey.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
environmentId,
|
||||
status: {
|
||||
not: "completed",
|
||||
},
|
||||
},
|
||||
select: selectSurvey,
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
});
|
||||
expect(transformPrismaSurvey).toHaveBeenCalledTimes(mockPrismaSurveys.length);
|
||||
expect(transformPrismaSurvey).toHaveBeenCalledWith(mockPrismaSurveys[0]);
|
||||
expect(transformPrismaSurvey).toHaveBeenCalledWith(mockPrismaSurveys[1]);
|
||||
// Check if the inner cache function was called with the correct arguments
|
||||
expect(cache).toHaveBeenCalledWith(
|
||||
expect.any(Function), // The async function passed to cache
|
||||
[`getSurveys-${environmentId}`], // The cache key
|
||||
{
|
||||
tags: [surveyCache.tag.byEnvironmentId(environmentId)], // Cache tags
|
||||
}
|
||||
);
|
||||
// Remove the assertion for reactCache being called within the test execution
|
||||
// expect(reactCache).toHaveBeenCalled(); // Removed this line
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on Prisma known request error", async () => {
|
||||
// No need to mock cache here again as beforeEach handles it
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Test error", {
|
||||
code: "P2025",
|
||||
clientVersion: "5.0.0",
|
||||
meta: {}, // Added meta property
|
||||
});
|
||||
vi.mocked(prisma.survey.findMany).mockRejectedValue(prismaError);
|
||||
|
||||
await expect(getSurveys(environmentId)).rejects.toThrow(DatabaseError);
|
||||
expect(logger.error).toHaveBeenCalledWith({ error: prismaError }, "getSurveys: Could not fetch surveys");
|
||||
expect(cache).toHaveBeenCalled(); // Ensure cache wrapper was still called
|
||||
});
|
||||
|
||||
test("should throw original error on other errors", async () => {
|
||||
// No need to mock cache here again as beforeEach handles it
|
||||
const genericError = new Error("Something went wrong");
|
||||
vi.mocked(prisma.survey.findMany).mockRejectedValue(genericError);
|
||||
|
||||
await expect(getSurveys(environmentId)).rejects.toThrow(genericError);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
expect(cache).toHaveBeenCalled(); // Ensure cache wrapper was still called
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { cache } from "@/lib/cache";
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { getWebhookCountBySource } from "./webhook";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/cache");
|
||||
vi.mock("@/lib/cache/webhook", () => ({
|
||||
webhookCache: {
|
||||
tag: {
|
||||
byEnvironmentIdAndSource: vi.fn((envId, source) => `webhook_${envId}_${source ?? "all"}`),
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/utils/validate");
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
webhook: {
|
||||
count: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const environmentId = "test-environment-id";
|
||||
const sourceZapier = "zapier";
|
||||
|
||||
describe("getWebhookCountBySource", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(cache).mockImplementation((fn) => async () => {
|
||||
return fn();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("should return webhook count for a specific source", async () => {
|
||||
const mockCount = 5;
|
||||
vi.mocked(prisma.webhook.count).mockResolvedValue(mockCount);
|
||||
|
||||
const count = await getWebhookCountBySource(environmentId, sourceZapier);
|
||||
|
||||
expect(count).toBe(mockCount);
|
||||
expect(validateInputs).toHaveBeenCalledWith(
|
||||
[environmentId, expect.any(Object)],
|
||||
[sourceZapier, expect.any(Object)]
|
||||
);
|
||||
expect(prisma.webhook.count).toHaveBeenCalledWith({
|
||||
where: {
|
||||
environmentId,
|
||||
source: sourceZapier,
|
||||
},
|
||||
});
|
||||
expect(cache).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
[`getWebhookCountBySource-${environmentId}-${sourceZapier}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, sourceZapier)],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("should return total webhook count when source is undefined", async () => {
|
||||
const mockCount = 10;
|
||||
vi.mocked(prisma.webhook.count).mockResolvedValue(mockCount);
|
||||
|
||||
const count = await getWebhookCountBySource(environmentId);
|
||||
|
||||
expect(count).toBe(mockCount);
|
||||
expect(validateInputs).toHaveBeenCalledWith(
|
||||
[environmentId, expect.any(Object)],
|
||||
[undefined, expect.any(Object)]
|
||||
);
|
||||
expect(prisma.webhook.count).toHaveBeenCalledWith({
|
||||
where: {
|
||||
environmentId,
|
||||
source: undefined,
|
||||
},
|
||||
});
|
||||
expect(cache).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
[`getWebhookCountBySource-${environmentId}-undefined`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, undefined)],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on Prisma known request error", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
|
||||
code: "P2025",
|
||||
clientVersion: "5.0.0",
|
||||
});
|
||||
vi.mocked(prisma.webhook.count).mockRejectedValue(prismaError);
|
||||
|
||||
await expect(getWebhookCountBySource(environmentId, sourceZapier)).rejects.toThrow(DatabaseError);
|
||||
expect(prisma.webhook.count).toHaveBeenCalledTimes(1);
|
||||
expect(cache).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should throw original error on other errors", async () => {
|
||||
const genericError = new Error("Something went wrong");
|
||||
vi.mocked(prisma.webhook.count).mockRejectedValue(genericError);
|
||||
|
||||
await expect(getWebhookCountBySource(environmentId)).rejects.toThrow(genericError);
|
||||
expect(prisma.webhook.count).toHaveBeenCalledTimes(1);
|
||||
expect(cache).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
+606
@@ -0,0 +1,606 @@
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/AddIntegrationModal";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
TIntegrationNotion,
|
||||
TIntegrationNotionConfigData,
|
||||
TIntegrationNotionCredential,
|
||||
TIntegrationNotionDatabase,
|
||||
} from "@formbricks/types/integration/notion";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock actions and utilities
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: (value: any, _locale: string) => value?.default || "",
|
||||
}));
|
||||
vi.mock("@/lib/pollyfills/structuredClone", () => ({
|
||||
structuredClone: (obj: any) => JSON.parse(JSON.stringify(obj)),
|
||||
}));
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
replaceHeadlineRecall: (survey: any) => survey,
|
||||
}));
|
||||
vi.mock("@/modules/survey/lib/questions", () => ({
|
||||
getQuestionTypes: () => [
|
||||
{ id: TSurveyQuestionTypeEnum.OpenText, label: "Open Text" },
|
||||
{ id: TSurveyQuestionTypeEnum.MultipleChoiceSingle, label: "Multiple Choice Single" },
|
||||
{ id: TSurveyQuestionTypeEnum.Date, label: "Date" },
|
||||
],
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, onClick, loading, variant, type = "button" }: any) => (
|
||||
<button onClick={onClick} disabled={loading} data-variant={variant} type={type}>
|
||||
{loading ? "Loading..." : children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/dropdown-selector", () => ({
|
||||
DropdownSelector: ({ label, items, selectedItem, setSelectedItem, placeholder, disabled }: any) => {
|
||||
// Ensure the selected item is always available as an option
|
||||
const allOptions = [...items];
|
||||
if (selectedItem && !items.some((item: any) => item.id === selectedItem.id)) {
|
||||
// Use a simple object structure consistent with how options are likely used
|
||||
allOptions.push({ id: selectedItem.id, name: selectedItem.name });
|
||||
}
|
||||
// Remove duplicates just in case
|
||||
const uniqueOptions = Array.from(new Map(allOptions.map((item) => [item.id, item])).values());
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && <label>{label}</label>}
|
||||
<select
|
||||
data-testid={`dropdown-${label?.toLowerCase().replace(/\s+/g, "-") || placeholder?.toLowerCase().replace(/\s+/g, "-")}`}
|
||||
value={selectedItem?.id || ""} // Still set value based on selectedItem prop
|
||||
onChange={(e) => {
|
||||
const selected = uniqueOptions.find((item: any) => item.id === e.target.value);
|
||||
setSelectedItem(selected);
|
||||
}}
|
||||
disabled={disabled}>
|
||||
<option value="">{placeholder || "Select..."}</option>
|
||||
{/* Render options from the potentially augmented list */}
|
||||
{uniqueOptions.map((item: any) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/label", () => ({
|
||||
Label: ({ children }: { children: React.ReactNode }) => <label>{children}</label>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: ({ open, children }: { open: boolean; children: React.ReactNode }) =>
|
||||
open ? <div data-testid="modal">{children}</div> : null,
|
||||
}));
|
||||
vi.mock("lucide-react", () => ({
|
||||
PlusIcon: () => <span data-testid="plus-icon">+</span>,
|
||||
XIcon: () => <span data-testid="x-icon">x</span>,
|
||||
}));
|
||||
vi.mock("next/image", () => ({
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
|
||||
}));
|
||||
vi.mock("react-hook-form", () => ({
|
||||
useForm: () => ({
|
||||
handleSubmit: (callback: any) => (event: any) => {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@tolgee/react", async () => {
|
||||
const MockTolgeeProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
const useTranslate = () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
// NOSONAR
|
||||
// Simple mock translation function
|
||||
if (key === "common.warning") return "Warning";
|
||||
if (key === "common.metadata") return "Metadata";
|
||||
if (key === "common.created_at") return "Created at";
|
||||
if (key === "common.hidden_field") return "Hidden Field";
|
||||
if (key === "environments.integrations.notion.link_notion_database") return "Link Notion Database";
|
||||
if (key === "environments.integrations.notion.sync_responses_with_a_notion_database")
|
||||
return "Sync responses with a Notion database.";
|
||||
if (key === "environments.integrations.notion.select_a_database") return "Select a database";
|
||||
if (key === "common.select_survey") return "Select survey";
|
||||
if (key === "environments.integrations.notion.map_formbricks_fields_to_notion_property")
|
||||
return "Map Formbricks fields to Notion property";
|
||||
if (key === "environments.integrations.notion.select_a_survey_question")
|
||||
return "Select a survey question";
|
||||
if (key === "environments.integrations.notion.select_a_field_to_map") return "Select a field to map";
|
||||
if (key === "common.delete") return "Delete";
|
||||
if (key === "common.cancel") return "Cancel";
|
||||
if (key === "common.update") return "Update";
|
||||
if (key === "environments.integrations.notion.please_select_a_database")
|
||||
return "Please select a database.";
|
||||
if (key === "environments.integrations.please_select_a_survey_error") return "Please select a survey.";
|
||||
if (key === "environments.integrations.notion.please_select_at_least_one_mapping")
|
||||
return "Please select at least one mapping.";
|
||||
if (key === "environments.integrations.notion.please_resolve_mapping_errors")
|
||||
return "Please resolve mapping errors.";
|
||||
if (key === "environments.integrations.notion.please_complete_mapping_fields_with_notion_property")
|
||||
return "Please complete mapping fields.";
|
||||
if (key === "environments.integrations.integration_updated_successfully")
|
||||
return "Integration updated successfully.";
|
||||
if (key === "environments.integrations.integration_added_successfully")
|
||||
return "Integration added successfully.";
|
||||
if (key === "environments.integrations.integration_removed_successfully")
|
||||
return "Integration removed successfully.";
|
||||
if (key === "environments.integrations.notion.notion_logo") return "Notion logo";
|
||||
if (key === "environments.integrations.create_survey_warning")
|
||||
return "You need to create a survey first.";
|
||||
if (key === "environments.integrations.notion.create_at_least_one_database_to_setup_this_integration")
|
||||
return "Create at least one database.";
|
||||
if (key === "environments.integrations.notion.duplicate_connection_warning")
|
||||
return "Duplicate connection warning.";
|
||||
if (key === "environments.integrations.notion.que_name_of_type_cant_be_mapped_to")
|
||||
return `Question ${params.que_name} (${params.question_label}) can't be mapped to ${params.col_name} (${params.col_type}). Allowed types: ${params.mapped_type}`;
|
||||
|
||||
return key; // Return key if no translation is found
|
||||
},
|
||||
});
|
||||
return { TolgeeProvider: MockTolgeeProvider, useTranslate };
|
||||
});
|
||||
|
||||
// Mock dependencies
|
||||
const createOrUpdateIntegrationAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/integrations/actions"))
|
||||
.createOrUpdateIntegrationAction
|
||||
);
|
||||
const toast = vi.mocked((await import("react-hot-toast")).default);
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const mockSetOpen = vi.fn();
|
||||
|
||||
const surveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 1",
|
||||
type: "app",
|
||||
environmentId: environmentId,
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1?" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 2?" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Choice 1" } },
|
||||
{ id: "c2", label: { default: "Choice 2" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
variables: [{ id: "var1", name: "Variable 1" }],
|
||||
hiddenFields: { enabled: true, fieldIds: ["hf1"] },
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
{
|
||||
id: "survey2",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 2",
|
||||
type: "link",
|
||||
environmentId: environmentId,
|
||||
status: "draft",
|
||||
questions: [
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyQuestionTypeEnum.Date,
|
||||
headline: { default: "Date Question?" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
variables: [],
|
||||
hiddenFields: { enabled: false },
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const databases: TIntegrationNotionDatabase[] = [
|
||||
{
|
||||
id: "db1",
|
||||
name: "Database 1 Title",
|
||||
properties: {
|
||||
prop1: { id: "p1", name: "Title Prop", type: "title" },
|
||||
prop2: { id: "p2", name: "Text Prop", type: "rich_text" },
|
||||
prop3: { id: "p3", name: "Number Prop", type: "number" },
|
||||
prop4: { id: "p4", name: "Date Prop", type: "date" },
|
||||
prop5: { id: "p5", name: "Unsupported Prop", type: "formula" }, // Unsupported
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "db2",
|
||||
name: "Database 2 Title",
|
||||
properties: {
|
||||
propA: { id: "pa", name: "Name", type: "title" },
|
||||
propB: { id: "pb", name: "Email", type: "email" },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockNotionIntegration: TIntegrationNotion = {
|
||||
id: "integration1",
|
||||
type: "notion",
|
||||
environmentId: environmentId,
|
||||
config: {
|
||||
key: {
|
||||
access_token: "token",
|
||||
bot_id: "bot",
|
||||
workspace_name: "ws",
|
||||
workspace_icon: "",
|
||||
} as unknown as TIntegrationNotionCredential,
|
||||
data: [], // Initially empty
|
||||
},
|
||||
};
|
||||
|
||||
const mockSelectedIntegration: TIntegrationNotionConfigData & { index: number } = {
|
||||
databaseId: databases[0].id,
|
||||
databaseName: databases[0].name,
|
||||
surveyId: surveys[0].id,
|
||||
surveyName: surveys[0].name,
|
||||
mapping: [
|
||||
{
|
||||
column: { id: "p1", name: "Title Prop", type: "title" },
|
||||
question: { id: "q1", name: "Question 1?", type: TSurveyQuestionTypeEnum.OpenText },
|
||||
},
|
||||
{
|
||||
column: { id: "p2", name: "Text Prop", type: "rich_text" },
|
||||
question: { id: "var1", name: "Variable 1", type: TSurveyQuestionTypeEnum.OpenText },
|
||||
},
|
||||
],
|
||||
createdAt: new Date(),
|
||||
index: 0,
|
||||
};
|
||||
|
||||
describe("AddIntegrationModal (Notion)", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset integration data before each test if needed
|
||||
mockNotionIntegration.config.data = [
|
||||
{ ...mockSelectedIntegration }, // Simulate existing data for update/delete tests
|
||||
];
|
||||
});
|
||||
|
||||
test("renders correctly when open (create mode)", () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.notion.link_database")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-select-a-database")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-select-survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "environments.integrations.notion.link_database" })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText("Delete")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Map Formbricks fields to Notion property")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly when open (update mode)", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={mockNotionIntegration}
|
||||
databases={databases}
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-select-a-database")).toHaveValue(databases[0].id);
|
||||
expect(screen.getByTestId("dropdown-select-survey")).toHaveValue(surveys[0].id);
|
||||
expect(screen.getByText("Map Formbricks fields to Notion property")).toBeInTheDocument();
|
||||
|
||||
// Check if mapping rows are rendered
|
||||
await waitFor(() => {
|
||||
const questionDropdowns = screen.getAllByTestId("dropdown-select-a-survey-question");
|
||||
const columnDropdowns = screen.getAllByTestId("dropdown-select-a-field-to-map");
|
||||
|
||||
expect(questionDropdowns).toHaveLength(2); // Expecting two rows based on mockSelectedIntegration
|
||||
expect(columnDropdowns).toHaveLength(2);
|
||||
|
||||
// Assert values for the first row
|
||||
expect(questionDropdowns[0]).toHaveValue("q1");
|
||||
expect(columnDropdowns[0]).toHaveValue("p1");
|
||||
|
||||
// Assert values for the second row
|
||||
expect(questionDropdowns[1]).toHaveValue("var1");
|
||||
expect(columnDropdowns[1]).toHaveValue("p2");
|
||||
|
||||
expect(screen.getAllByTestId("plus-icon").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByTestId("x-icon").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(screen.getByText("Delete")).toBeInTheDocument();
|
||||
expect(screen.getByText("Update")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("selects database and survey, shows mapping", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const dbDropdown = screen.getByTestId("dropdown-select-a-database");
|
||||
const surveyDropdown = screen.getByTestId("dropdown-select-survey");
|
||||
|
||||
await userEvent.selectOptions(dbDropdown, databases[0].id);
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
expect(screen.getByText("Map Formbricks fields to Notion property")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-select-a-survey-question")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-select-a-field-to-map")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("adds and removes mapping rows", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const dbDropdown = screen.getByTestId("dropdown-select-a-database");
|
||||
const surveyDropdown = screen.getByTestId("dropdown-select-survey");
|
||||
|
||||
await userEvent.selectOptions(dbDropdown, databases[0].id);
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
expect(screen.getAllByTestId("dropdown-select-a-survey-question")).toHaveLength(1);
|
||||
|
||||
const plusButton = screen.getByTestId("plus-icon");
|
||||
await userEvent.click(plusButton);
|
||||
|
||||
expect(screen.getAllByTestId("dropdown-select-a-survey-question")).toHaveLength(2);
|
||||
|
||||
const xButton = screen.getAllByTestId("x-icon")[0]; // Get the first X button
|
||||
await userEvent.click(xButton);
|
||||
|
||||
expect(screen.getAllByTestId("dropdown-select-a-survey-question")).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("deletes integration successfully", async () => {
|
||||
createOrUpdateIntegrationAction.mockResolvedValue({ data: null as any });
|
||||
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={mockNotionIntegration} // Contains initial data at index 0
|
||||
databases={databases}
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteButton = screen.getByText("Delete");
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationAction).toHaveBeenCalledWith({
|
||||
environmentId,
|
||||
integrationData: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
data: [], // Data array should be empty after deletion
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Integration removed successfully.");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("shows validation error if no database selected", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
await userEvent.selectOptions(screen.getByTestId("dropdown-select-survey"), surveys[0].id);
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "environments.integrations.notion.link_database" })
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select a database.");
|
||||
});
|
||||
});
|
||||
|
||||
test("shows validation error if no survey selected", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
await userEvent.selectOptions(screen.getByTestId("dropdown-select-a-database"), databases[0].id);
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "environments.integrations.notion.link_database" })
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select a survey.");
|
||||
});
|
||||
});
|
||||
|
||||
test("shows validation error if no mapping defined", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
await userEvent.selectOptions(screen.getByTestId("dropdown-select-a-database"), databases[0].id);
|
||||
await userEvent.selectOptions(screen.getByTestId("dropdown-select-survey"), surveys[0].id);
|
||||
// Default mapping row is empty
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "environments.integrations.notion.link_database" })
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select at least one mapping.");
|
||||
});
|
||||
});
|
||||
|
||||
test("calls setOpen(false) and resets form on cancel", async () => {
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const dbDropdown = screen.getByTestId("dropdown-select-a-database");
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
|
||||
await userEvent.selectOptions(dbDropdown, databases[0].id); // Simulate interaction
|
||||
await userEvent.click(cancelButton);
|
||||
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
// Re-render with open=true to check if state was reset
|
||||
cleanup();
|
||||
render(
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
notionIntegration={{
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, data: [] },
|
||||
}}
|
||||
databases={databases}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("dropdown-select-a-database")).toHaveValue(""); // Should be reset
|
||||
});
|
||||
});
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/notion/lib/notion";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationNotion, TIntegrationNotionCredential } from "@formbricks/types/integration/notion";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { NotionWrapper } from "./NotionWrapper";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: "test-client-secret",
|
||||
GOOGLE_SHEETS_REDIRECT_URL: "test-redirect-url",
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration", () => ({
|
||||
ManageIntegration: vi.fn(({ setIsConnected }) => (
|
||||
<div data-testid="manage-integration">
|
||||
<button onClick={() => setIsConnected(false)}>Disconnect</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
ConnectIntegration: vi.fn(
|
||||
(
|
||||
{ handleAuthorization, isEnabled } // Reverted back to isEnabled
|
||||
) => (
|
||||
<div data-testid="connect-integration">
|
||||
<button onClick={handleAuthorization} disabled={!isEnabled}>
|
||||
{" "}
|
||||
{/* Reverted back to isEnabled */}
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock library function
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/notion/lib/notion", () => ({
|
||||
authorize: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock image import
|
||||
vi.mock("@/images/notion-logo.svg", () => ({
|
||||
default: "notion-logo-path",
|
||||
}));
|
||||
|
||||
// Mock window.location.replace
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
replace: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const webAppUrl = "https://app.formbricks.com";
|
||||
const environment = { id: environmentId } as TEnvironment;
|
||||
const surveys: TSurvey[] = [];
|
||||
const databases = [];
|
||||
const locale = "en-US" as const;
|
||||
|
||||
const mockNotionIntegration: TIntegrationNotion = {
|
||||
id: "int-notion-123",
|
||||
type: "notion",
|
||||
environmentId: environmentId,
|
||||
config: {
|
||||
key: { access_token: "test-token" } as TIntegrationNotionCredential,
|
||||
data: [],
|
||||
},
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
environment,
|
||||
surveys,
|
||||
databasesArray: databases, // Renamed databases to databasesArray to match component prop
|
||||
webAppUrl,
|
||||
locale,
|
||||
};
|
||||
|
||||
describe("NotionWrapper", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration disabled when enabled is false", () => {
|
||||
// Changed description slightly
|
||||
render(<NotionWrapper {...baseProps} enabled={false} notionIntegration={undefined} />); // Changed isEnabled to enabled
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeDisabled();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration enabled when enabled is true and not connected (no integration)", () => {
|
||||
// Changed description slightly
|
||||
render(<NotionWrapper {...baseProps} enabled={true} notionIntegration={undefined} />); // Changed isEnabled to enabled
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration enabled when enabled is true and not connected (integration without key)", () => {
|
||||
// Changed description slightly
|
||||
const integrationWithoutKey = {
|
||||
...mockNotionIntegration,
|
||||
config: { data: [] },
|
||||
} as unknown as TIntegrationNotion;
|
||||
render(<NotionWrapper {...baseProps} enabled={true} notionIntegration={integrationWithoutKey} />); // Changed isEnabled to enabled
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls authorize and redirects when Connect button is clicked", async () => {
|
||||
const mockAuthorize = vi.mocked(authorize);
|
||||
const redirectUrl = "https://notion.com/auth";
|
||||
mockAuthorize.mockResolvedValue(redirectUrl);
|
||||
|
||||
render(<NotionWrapper {...baseProps} enabled={true} notionIntegration={undefined} />); // Changed isEnabled to enabled
|
||||
|
||||
const connectButton = screen.getByRole("button", { name: "Connect" });
|
||||
await userEvent.click(connectButton);
|
||||
|
||||
expect(mockAuthorize).toHaveBeenCalledWith(environmentId, webAppUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith(redirectUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { authorize } from "./notion";
|
||||
|
||||
// Mock the logger
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
const mockFetch = vi.fn();
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
describe("authorize", () => {
|
||||
const environmentId = "test-env-id";
|
||||
const apiHost = "http://test.com";
|
||||
const expectedUrl = `${apiHost}/api/v1/integrations/notion`;
|
||||
const expectedHeaders = { environmentId: environmentId };
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should return authUrl on successful fetch", async () => {
|
||||
const mockAuthUrl = "https://api.notion.com/v1/oauth/authorize?...";
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { authUrl: mockAuthUrl } }),
|
||||
});
|
||||
|
||||
const authUrl = await authorize(environmentId, apiHost);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: expectedHeaders,
|
||||
});
|
||||
expect(authUrl).toBe(mockAuthUrl);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should throw error and log on failed fetch", async () => {
|
||||
const errorText = "Failed to fetch";
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
text: async () => errorText,
|
||||
});
|
||||
|
||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: expectedHeaders,
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith({ errorText }, "authorize: Could not fetch notion config");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Loading from "./loading";
|
||||
|
||||
// Mock child components
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, className }: { children: React.ReactNode; className: string }) => (
|
||||
<button className={className}>{children}</button>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: () => <div data-testid="go-back-button">Go Back</div>,
|
||||
}));
|
||||
|
||||
// Mock @tolgee/react
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key, // Simple mock translation
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("Notion Integration Loading", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders loading state correctly", () => {
|
||||
render(<Loading />);
|
||||
|
||||
// Check for GoBackButton mock
|
||||
expect(screen.getByTestId("go-back-button")).toBeInTheDocument();
|
||||
|
||||
// Check for the disabled button
|
||||
const linkButton = screen.getByText("environments.integrations.notion.link_database");
|
||||
expect(linkButton).toBeInTheDocument();
|
||||
expect(linkButton.closest("button")).toHaveClass(
|
||||
"pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200"
|
||||
);
|
||||
|
||||
// Check for table headers
|
||||
expect(screen.getByText("common.survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.notion.database_name")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.updated_at")).toBeInTheDocument();
|
||||
|
||||
// Check for placeholder elements (skeleton loaders)
|
||||
// There should be 3 rows * 5 pulse divs per row = 15 pulse divs
|
||||
const pulseDivs = screen.getAllByText("", { selector: "div.animate-pulse" });
|
||||
expect(pulseDivs.length).toBeGreaterThanOrEqual(15); // Check if at least 15 pulse divs are rendered
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,250 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/notion/page";
|
||||
import { getIntegrationByType } from "@/lib/integration/service";
|
||||
import { getNotionDatabases } from "@/lib/notion/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper", () => ({
|
||||
NotionWrapper: vi.fn(
|
||||
({ enabled, environment, surveys, notionIntegration, webAppUrl, databasesArray, locale }) => (
|
||||
<div>
|
||||
<span>Mocked NotionWrapper</span>
|
||||
<span data-testid="enabled">{enabled.toString()}</span>
|
||||
<span data-testid="environmentId">{environment.id}</span>
|
||||
<span data-testid="surveyCount">{surveys?.length ?? 0}</span>
|
||||
<span data-testid="integrationId">{notionIntegration?.id}</span>
|
||||
<span data-testid="webAppUrl">{webAppUrl}</span>
|
||||
<span data-testid="databaseCount">{databasesArray?.length ?? 0}</span>
|
||||
<span data-testid="locale">{locale}</span>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys", () => ({
|
||||
getSurveys: vi.fn(),
|
||||
}));
|
||||
|
||||
let mockNotionClientId: string | undefined = "test-client-id";
|
||||
let mockNotionClientSecret: string | undefined = "test-client-secret";
|
||||
let mockNotionAuthUrl: string | undefined = "https://notion.com/auth";
|
||||
let mockNotionRedirectUri: string | undefined = "https://app.formbricks.com/redirect";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
get NOTION_OAUTH_CLIENT_ID() {
|
||||
return mockNotionClientId;
|
||||
},
|
||||
get NOTION_OAUTH_CLIENT_SECRET() {
|
||||
return mockNotionClientSecret;
|
||||
},
|
||||
get NOTION_AUTH_URL() {
|
||||
return mockNotionAuthUrl;
|
||||
},
|
||||
get NOTION_REDIRECT_URI() {
|
||||
return mockNotionRedirectUri;
|
||||
},
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
vi.mock("@/lib/integration/service", () => ({
|
||||
getIntegrationByType: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/notion/service", () => ({
|
||||
getNotionDatabases: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/utils/locale", () => ({
|
||||
findMatchingLocale: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: vi.fn(({ url }) => <div data-testid="go-back">{url}</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: vi.fn(({ children }) => <div>{children}</div>),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: vi.fn(({ pageTitle }) => <h1>{pageTitle}</h1>),
|
||||
}));
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
appSetupCompleted: false,
|
||||
type: "development",
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockSurveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
name: "Survey 1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "test-env-id",
|
||||
status: "inProgress",
|
||||
type: "app",
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
languages: [],
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
segment: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const mockNotionIntegration = {
|
||||
id: "integration1",
|
||||
type: "notion",
|
||||
config: {
|
||||
data: [],
|
||||
key: { bot_id: "bot-id-123" },
|
||||
email: "test@example.com",
|
||||
},
|
||||
} as unknown as TIntegrationNotion;
|
||||
|
||||
const mockDatabases: TIntegrationNotionDatabase[] = [
|
||||
{ id: "db1", name: "Database 1", properties: {} },
|
||||
{ id: "db2", name: "Database 2", properties: {} },
|
||||
];
|
||||
|
||||
const mockProps = {
|
||||
params: { environmentId: "test-env-id" },
|
||||
};
|
||||
|
||||
describe("NotionIntegrationPage", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
} as TEnvironmentAuth);
|
||||
vi.mocked(getSurveys).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getIntegrationByType).mockResolvedValue(mockNotionIntegration);
|
||||
vi.mocked(getNotionDatabases).mockResolvedValue(mockDatabases);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
|
||||
mockNotionClientId = "test-client-id";
|
||||
mockNotionClientSecret = "test-client-secret";
|
||||
mockNotionAuthUrl = "https://notion.com/auth";
|
||||
mockNotionRedirectUri = "https://app.formbricks.com/redirect";
|
||||
});
|
||||
|
||||
test("renders the page with NotionWrapper when enabled and not read-only", async () => {
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("environments.integrations.notion.notion_integration")).toBeInTheDocument();
|
||||
expect(screen.getByText("Mocked NotionWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("enabled")).toHaveTextContent("true");
|
||||
expect(screen.getByTestId("environmentId")).toHaveTextContent(mockEnvironment.id);
|
||||
expect(screen.getByTestId("surveyCount")).toHaveTextContent(mockSurveys.length.toString());
|
||||
expect(screen.getByTestId("integrationId")).toHaveTextContent(mockNotionIntegration.id);
|
||||
expect(screen.getByTestId("webAppUrl")).toHaveTextContent("test-webapp-url");
|
||||
expect(screen.getByTestId("databaseCount")).toHaveTextContent(mockDatabases.length.toString());
|
||||
expect(screen.getByTestId("locale")).toHaveTextContent("en-US");
|
||||
expect(screen.getByTestId("go-back")).toHaveTextContent(
|
||||
`test-webapp-url/environments/${mockProps.params.environmentId}/integrations`
|
||||
);
|
||||
expect(vi.mocked(redirect)).not.toHaveBeenCalled();
|
||||
expect(vi.mocked(getNotionDatabases)).toHaveBeenCalledWith(mockEnvironment.id);
|
||||
});
|
||||
|
||||
test("calls redirect when user is read-only", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: true,
|
||||
} as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(vi.mocked(redirect)).toHaveBeenCalledWith("./");
|
||||
});
|
||||
|
||||
test("passes enabled=false to NotionWrapper when constants are missing", async () => {
|
||||
mockNotionClientId = undefined; // Simulate missing constant
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByTestId("enabled")).toHaveTextContent("false");
|
||||
});
|
||||
|
||||
test("handles case where no Notion integration exists", async () => {
|
||||
vi.mocked(getIntegrationByType).mockResolvedValue(null); // No integration
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("Mocked NotionWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("integrationId")).toBeEmptyDOMElement(); // No integration ID passed
|
||||
expect(screen.getByTestId("databaseCount")).toHaveTextContent("0"); // No databases fetched
|
||||
expect(vi.mocked(getNotionDatabases)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles case where integration exists but has no key (bot_id)", async () => {
|
||||
const integrationWithoutKey = {
|
||||
...mockNotionIntegration,
|
||||
config: { ...mockNotionIntegration.config, key: undefined },
|
||||
} as unknown as TIntegrationNotion;
|
||||
vi.mocked(getIntegrationByType).mockResolvedValue(integrationWithoutKey);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("Mocked NotionWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("integrationId")).toHaveTextContent(integrationWithoutKey.id);
|
||||
expect(screen.getByTestId("databaseCount")).toHaveTextContent("0"); // No databases fetched
|
||||
expect(vi.mocked(getNotionDatabases)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,243 @@
|
||||
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/integrations/lib/webhook";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/page";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegration } from "@formbricks/types/integration";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/webhook", () => ({
|
||||
getWebhookCountBySource: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/integration/service", () => ({
|
||||
getIntegrations: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/integration-card", () => ({
|
||||
Card: ({ label, description, statusText, disabled }) => (
|
||||
<div data-testid={`card-${label}`}>
|
||||
<h1>{label}</h1>
|
||||
<p>{description}</p>
|
||||
<span>{statusText}</span>
|
||||
{disabled && <span>Disabled</span>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: ({ children }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: ({ pageTitle }) => <h1>{pageTitle}</h1>,
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("next/image", () => ({
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
default: ({ alt }) => <img alt={alt} />,
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
appSetupCompleted: true,
|
||||
} as unknown as TEnvironment;
|
||||
|
||||
const mockIntegrations: TIntegration[] = [
|
||||
{
|
||||
id: "google-sheets-id",
|
||||
type: "googleSheets",
|
||||
environmentId: "test-env-id",
|
||||
config: { data: [], email: "test@example.com" } as unknown as TIntegration["config"],
|
||||
},
|
||||
{
|
||||
id: "slack-id",
|
||||
type: "slack",
|
||||
environmentId: "test-env-id",
|
||||
config: { data: [] } as unknown as TIntegration["config"],
|
||||
},
|
||||
];
|
||||
|
||||
const mockParams = { environmentId: "test-env-id" };
|
||||
const mockProps = { params: mockParams };
|
||||
|
||||
describe("Integrations Page", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getWebhookCountBySource).mockResolvedValue(0);
|
||||
vi.mocked(getIntegrations).mockResolvedValue([]);
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
isBilling: false,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
});
|
||||
|
||||
test("renders the page header and integration cards", async () => {
|
||||
vi.mocked(getWebhookCountBySource).mockImplementation(async (envId, source) => {
|
||||
if (source === "zapier") return 1;
|
||||
if (source === "user") return 2;
|
||||
return 0;
|
||||
});
|
||||
vi.mocked(getIntegrations).mockResolvedValue(mockIntegrations);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("common.integrations")).toBeInTheDocument(); // Page Header
|
||||
expect(screen.getByTestId("card-Javascript SDK")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.website_or_app_integration_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.connected")[0]).toBeInTheDocument(); // JS SDK status
|
||||
|
||||
expect(screen.getByTestId("card-Zapier")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.zapier_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getByText("1 zap")).toBeInTheDocument(); // Zapier status
|
||||
|
||||
expect(screen.getByTestId("card-Webhooks")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.webhook_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getByText("2 webhooks")).toBeInTheDocument(); // Webhook status
|
||||
|
||||
expect(screen.getByTestId("card-Google Sheets")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.google_sheet_integration_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.connected")[1]).toBeInTheDocument(); // Google Sheets status
|
||||
|
||||
expect(screen.getByTestId("card-Airtable")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.airtable_integration_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.not_connected")[0]).toBeInTheDocument(); // Airtable status
|
||||
|
||||
expect(screen.getByTestId("card-Slack")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.slack_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.connected")[2]).toBeInTheDocument(); // Slack status
|
||||
|
||||
expect(screen.getByTestId("card-n8n")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.n8n_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.not_connected")[1]).toBeInTheDocument(); // n8n status
|
||||
|
||||
expect(screen.getByTestId("card-Make.com")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.make_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.not_connected")[2]).toBeInTheDocument(); // Make status
|
||||
|
||||
expect(screen.getByTestId("card-Notion")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.integrations.notion_integration_description")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.not_connected")[3]).toBeInTheDocument(); // Notion status
|
||||
|
||||
expect(screen.getByTestId("card-Activepieces")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.activepieces_integration_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText("common.not_connected")[4]).toBeInTheDocument(); // Activepieces status
|
||||
});
|
||||
|
||||
test("renders disabled cards when isReadOnly is true", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: true,
|
||||
isBilling: false,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
// JS SDK and Webhooks should not be disabled
|
||||
expect(screen.getByTestId("card-Javascript SDK")).not.toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Webhooks")).not.toHaveTextContent("Disabled");
|
||||
|
||||
// Other cards should be disabled
|
||||
expect(screen.getByTestId("card-Zapier")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Google Sheets")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Airtable")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Slack")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-n8n")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Make.com")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Notion")).toHaveTextContent("Disabled");
|
||||
expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("Disabled");
|
||||
});
|
||||
|
||||
test("redirects when isBilling is true", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
isBilling: true,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
await Page(mockProps);
|
||||
|
||||
expect(vi.mocked(redirect)).toHaveBeenCalledWith(
|
||||
`/environments/${mockParams.environmentId}/settings/billing`
|
||||
);
|
||||
});
|
||||
|
||||
test("renders correct status text for single integration", async () => {
|
||||
vi.mocked(getWebhookCountBySource).mockImplementation(async (envId, source) => {
|
||||
if (source === "n8n") return 1;
|
||||
if (source === "make") return 1;
|
||||
if (source === "activepieces") return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByTestId("card-n8n")).toHaveTextContent("1 common.integration");
|
||||
expect(screen.getByTestId("card-Make.com")).toHaveTextContent("1 common.integration");
|
||||
expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("1 common.integration");
|
||||
});
|
||||
|
||||
test("renders correct status text for multiple integrations", async () => {
|
||||
vi.mocked(getWebhookCountBySource).mockImplementation(async (envId, source) => {
|
||||
if (source === "n8n") return 3;
|
||||
if (source === "make") return 4;
|
||||
if (source === "activepieces") return 5;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByTestId("card-n8n")).toHaveTextContent("3 common.integrations");
|
||||
expect(screen.getByTestId("card-Make.com")).toHaveTextContent("4 common.integrations");
|
||||
expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("5 common.integrations");
|
||||
});
|
||||
|
||||
test("renders not connected status when widgetSetupCompleted is false", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: { ...mockEnvironment, appSetupCompleted: false },
|
||||
isReadOnly: false,
|
||||
isBilling: false,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByTestId("card-Javascript SDK")).toHaveTextContent("common.not_connected");
|
||||
});
|
||||
});
|
||||
+750
@@ -0,0 +1,750 @@
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationSlack,
|
||||
TIntegrationSlackConfigData,
|
||||
TIntegrationSlackCredential,
|
||||
} from "@formbricks/types/integration/slack";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { AddChannelMappingModal } from "./AddChannelMappingModal";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: (value: any, _locale: string) => value?.default || "",
|
||||
}));
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
replaceHeadlineRecall: (survey: any) => survey,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/additional-integration-settings", () => ({
|
||||
AdditionalIntegrationSettings: ({
|
||||
includeVariables,
|
||||
setIncludeVariables,
|
||||
includeHiddenFields,
|
||||
setIncludeHiddenFields,
|
||||
includeMetadata,
|
||||
setIncludeMetadata,
|
||||
includeCreatedAt,
|
||||
setIncludeCreatedAt,
|
||||
}: any) => (
|
||||
<div>
|
||||
<span>Additional Settings</span>
|
||||
<input
|
||||
data-testid="include-variables"
|
||||
type="checkbox"
|
||||
checked={includeVariables}
|
||||
onChange={(e) => setIncludeVariables(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-hidden-fields"
|
||||
type="checkbox"
|
||||
checked={includeHiddenFields}
|
||||
onChange={(e) => setIncludeHiddenFields(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-metadata"
|
||||
type="checkbox"
|
||||
checked={includeMetadata}
|
||||
onChange={(e) => setIncludeMetadata(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
data-testid="include-created-at"
|
||||
type="checkbox"
|
||||
checked={includeCreatedAt}
|
||||
onChange={(e) => setIncludeCreatedAt(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/dropdown-selector", () => ({
|
||||
DropdownSelector: ({ label, items, selectedItem, setSelectedItem, disabled }: any) => (
|
||||
<div>
|
||||
<label>{label}</label>
|
||||
<select
|
||||
data-testid={label.includes("channel") ? "channel-dropdown" : "survey-dropdown"}
|
||||
value={selectedItem?.id || ""}
|
||||
onChange={(e) => {
|
||||
const selected = items.find((item: any) => item.id === e.target.value);
|
||||
setSelectedItem(selected);
|
||||
}}
|
||||
disabled={disabled}>
|
||||
<option value="">Select...</option>
|
||||
{items.map((item: any) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: ({ open, children }: { open: boolean; children: React.ReactNode }) =>
|
||||
open ? <div data-testid="modal">{children}</div> : null,
|
||||
}));
|
||||
vi.mock("next/image", () => ({
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
|
||||
}));
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
vi.mock("react-hook-form", () => ({
|
||||
useForm: () => ({
|
||||
handleSubmit: (callback: any) => (event: any) => {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@tolgee/react", async () => {
|
||||
const MockTolgeeProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
const useTranslate = () => ({
|
||||
t: (key: string, _?: any) => {
|
||||
// NOSONAR
|
||||
// Simple mock translation function
|
||||
if (key === "common.all_questions") return "All questions";
|
||||
if (key === "common.selected_questions") return "Selected questions";
|
||||
if (key === "environments.integrations.slack.link_slack_channel") return "Link Slack Channel";
|
||||
if (key === "common.update") return "Update";
|
||||
if (key === "common.delete") return "Delete";
|
||||
if (key === "common.cancel") return "Cancel";
|
||||
if (key === "environments.integrations.slack.select_channel") return "Select channel";
|
||||
if (key === "common.select_survey") return "Select survey";
|
||||
if (key === "common.questions") return "Questions";
|
||||
if (key === "environments.integrations.slack.please_select_a_channel")
|
||||
return "Please select a channel.";
|
||||
if (key === "environments.integrations.please_select_a_survey_error") return "Please select a survey.";
|
||||
if (key === "environments.integrations.select_at_least_one_question_error")
|
||||
return "Please select at least one question.";
|
||||
if (key === "environments.integrations.integration_updated_successfully")
|
||||
return "Integration updated successfully.";
|
||||
if (key === "environments.integrations.integration_added_successfully")
|
||||
return "Integration added successfully.";
|
||||
if (key === "environments.integrations.integration_removed_successfully")
|
||||
return "Integration removed successfully.";
|
||||
if (key === "environments.integrations.slack.dont_see_your_channel") return "Don't see your channel?";
|
||||
if (key === "common.note") return "Note";
|
||||
if (key === "environments.integrations.slack.already_connected_another_survey")
|
||||
return "This channel is already connected to another survey.";
|
||||
if (key === "environments.integrations.slack.create_at_least_one_channel_error")
|
||||
return "Please create at least one channel in Slack first.";
|
||||
if (key === "environments.integrations.create_survey_warning")
|
||||
return "You need to create a survey first.";
|
||||
if (key === "environments.integrations.slack.link_channel") return "Link Channel";
|
||||
return key; // Return key if no translation is found
|
||||
},
|
||||
});
|
||||
return { TolgeeProvider: MockTolgeeProvider, useTranslate };
|
||||
});
|
||||
vi.mock("lucide-react", () => ({
|
||||
CircleHelpIcon: () => <div data-testid="circle-help-icon" />,
|
||||
Check: () => <div data-testid="check-icon" />, // Add the Check icon mock
|
||||
Loader2: () => <div data-testid="loader-icon" />, // Add the Loader2 icon mock
|
||||
}));
|
||||
|
||||
// Mock dependencies
|
||||
const createOrUpdateIntegrationActionMock = vi.mocked(createOrUpdateIntegrationAction);
|
||||
const toast = vi.mocked((await import("react-hot-toast")).default);
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const mockSetOpen = vi.fn();
|
||||
|
||||
const surveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 1",
|
||||
type: "app",
|
||||
environmentId: environmentId,
|
||||
status: "inProgress",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1?" },
|
||||
required: true,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 2?" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Choice 1" } },
|
||||
{ id: "c2", label: { default: "Choice 2" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
variables: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
{
|
||||
id: "survey2",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Survey 2",
|
||||
type: "link",
|
||||
environmentId: environmentId,
|
||||
status: "draft",
|
||||
questions: [
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate this?" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
variables: [],
|
||||
welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayLimit: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const channels: TIntegrationItem[] = [
|
||||
{ id: "channel1", name: "#general" },
|
||||
{ id: "channel2", name: "#random" },
|
||||
];
|
||||
|
||||
const mockSlackIntegration: TIntegrationSlack = {
|
||||
id: "integration1",
|
||||
type: "slack",
|
||||
environmentId: environmentId,
|
||||
config: {
|
||||
key: {
|
||||
access_token: "xoxb-test-token",
|
||||
team_name: "Test Team",
|
||||
team_id: "T123",
|
||||
} as unknown as TIntegrationSlackCredential,
|
||||
data: [], // Initially empty
|
||||
},
|
||||
};
|
||||
|
||||
const mockSelectedIntegration: TIntegrationSlackConfigData & { index: number } = {
|
||||
channelId: channels[0].id,
|
||||
channelName: channels[0].name,
|
||||
surveyId: surveys[0].id,
|
||||
surveyName: surveys[0].name,
|
||||
questionIds: [surveys[0].questions[0].id],
|
||||
questions: "Selected questions",
|
||||
createdAt: new Date(),
|
||||
includeVariables: true,
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: true,
|
||||
includeCreatedAt: false,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
describe("AddChannelMappingModal", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset integration data before each test if needed
|
||||
mockSlackIntegration.config.data = [
|
||||
{ ...mockSelectedIntegration }, // Simulate existing data for update/delete tests
|
||||
];
|
||||
});
|
||||
|
||||
test("renders correctly when open (create mode)", () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Link Slack Channel", { selector: "div.text-xl.font-medium" })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("channel-dropdown")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("survey-dropdown")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Link Channel" })).toBeInTheDocument();
|
||||
expect(screen.queryByText("Delete")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Questions")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("circle-help-icon")).toBeInTheDocument();
|
||||
expect(screen.getByText("Don't see your channel?")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly when open (update mode)", () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Link Slack Channel", { selector: "div.text-xl.font-medium" })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("channel-dropdown")).toHaveValue(channels[0].id);
|
||||
expect(screen.getByTestId("survey-dropdown")).toHaveValue(surveys[0].id);
|
||||
expect(screen.getByText("Questions")).toBeInTheDocument();
|
||||
expect(screen.getByText("Delete")).toBeInTheDocument();
|
||||
expect(screen.getByText("Update")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("include-variables")).toBeChecked();
|
||||
expect(screen.getByTestId("include-hidden-fields")).not.toBeChecked();
|
||||
expect(screen.getByTestId("include-metadata")).toBeChecked();
|
||||
expect(screen.getByTestId("include-created-at")).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("selects survey and shows questions", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[1].id);
|
||||
|
||||
expect(screen.getByText("Questions")).toBeInTheDocument();
|
||||
surveys[1].questions.forEach((q) => {
|
||||
expect(screen.getByLabelText(q.headline.default)).toBeInTheDocument();
|
||||
// Initially all questions should be checked when a survey is selected in create mode
|
||||
expect(screen.getByLabelText(q.headline.default)).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles question selection", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
const firstQuestionCheckbox = screen.getByLabelText(surveys[0].questions[0].headline.default);
|
||||
expect(firstQuestionCheckbox).toBeChecked(); // Initially checked
|
||||
|
||||
await userEvent.click(firstQuestionCheckbox);
|
||||
expect(firstQuestionCheckbox).not.toBeChecked(); // Unchecked after click
|
||||
|
||||
await userEvent.click(firstQuestionCheckbox);
|
||||
expect(firstQuestionCheckbox).toBeChecked(); // Checked again
|
||||
});
|
||||
|
||||
test("creates integration successfully", async () => {
|
||||
createOrUpdateIntegrationActionMock.mockResolvedValue({ data: null as any }); // Mock successful action
|
||||
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={{ ...mockSlackIntegration, config: { ...mockSlackIntegration.config, data: [] } }} // Start with empty data
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Channel" });
|
||||
|
||||
await userEvent.selectOptions(channelDropdown, channels[1].id);
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
// Wait for questions to appear and potentially uncheck one
|
||||
const firstQuestionCheckbox = await screen.findByLabelText(surveys[0].questions[0].headline.default);
|
||||
await userEvent.click(firstQuestionCheckbox); // Uncheck first question
|
||||
|
||||
// Check additional settings
|
||||
await userEvent.click(screen.getByTestId("include-variables"));
|
||||
await userEvent.click(screen.getByTestId("include-metadata"));
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationActionMock).toHaveBeenCalledWith({
|
||||
environmentId,
|
||||
integrationData: expect.objectContaining({
|
||||
type: "slack",
|
||||
config: expect.objectContaining({
|
||||
key: mockSlackIntegration.config.key,
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
channelId: channels[1].id,
|
||||
channelName: channels[1].name,
|
||||
surveyId: surveys[0].id,
|
||||
surveyName: surveys[0].name,
|
||||
questionIds: surveys[0].questions.slice(1).map((q) => q.id), // Excludes the first question
|
||||
questions: "Selected questions",
|
||||
includeVariables: true,
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: true,
|
||||
includeCreatedAt: true, // Default
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Integration added successfully.");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes integration successfully", async () => {
|
||||
createOrUpdateIntegrationActionMock.mockResolvedValue({ data: null as any });
|
||||
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration} // Contains initial data at index 0
|
||||
channels={channels}
|
||||
selectedIntegration={mockSelectedIntegration}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteButton = screen.getByText("Delete");
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationActionMock).toHaveBeenCalledWith({
|
||||
environmentId,
|
||||
integrationData: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
data: [], // Data array should be empty after deletion
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Integration removed successfully.");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("shows validation error if no channel selected", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Channel" });
|
||||
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
// No channel selected
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select a channel.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationActionMock).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows validation error if no survey selected", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Channel" });
|
||||
|
||||
await userEvent.selectOptions(channelDropdown, channels[0].id);
|
||||
// No survey selected
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select a survey.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationActionMock).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows validation error if no questions selected", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Channel" });
|
||||
|
||||
await userEvent.selectOptions(channelDropdown, channels[0].id);
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
|
||||
// Uncheck all questions
|
||||
for (const question of surveys[0].questions) {
|
||||
const checkbox = await screen.findByLabelText(question.headline.default);
|
||||
await userEvent.click(checkbox);
|
||||
}
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Please select at least one question.");
|
||||
});
|
||||
expect(createOrUpdateIntegrationActionMock).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shows error toast if createOrUpdateIntegrationAction fails", async () => {
|
||||
const errorMessage = "Failed to update integration";
|
||||
createOrUpdateIntegrationActionMock.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
const surveyDropdown = screen.getByTestId("survey-dropdown");
|
||||
const submitButton = screen.getByRole("button", { name: "Link Channel" });
|
||||
|
||||
await userEvent.selectOptions(channelDropdown, channels[0].id);
|
||||
await userEvent.selectOptions(surveyDropdown, surveys[0].id);
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createOrUpdateIntegrationActionMock).toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls setOpen(false) and resets form on cancel", async () => {
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
|
||||
// Simulate some interaction
|
||||
await userEvent.selectOptions(channelDropdown, channels[0].id);
|
||||
await userEvent.click(cancelButton);
|
||||
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
// Re-render with open=true to check if state was reset (channel should be unselected)
|
||||
cleanup();
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={mockSlackIntegration}
|
||||
channels={channels}
|
||||
selectedIntegration={null}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("channel-dropdown")).toHaveValue("");
|
||||
});
|
||||
|
||||
test("shows warning when selected channel is already connected (add mode)", async () => {
|
||||
// Add an existing connection for channel1
|
||||
const integrationWithExisting = {
|
||||
...mockSlackIntegration,
|
||||
config: {
|
||||
...mockSlackIntegration.config,
|
||||
data: [
|
||||
{
|
||||
channelId: "channel1",
|
||||
channelName: "#general",
|
||||
surveyId: "survey-other",
|
||||
surveyName: "Other Survey",
|
||||
questionIds: ["q-other"],
|
||||
questions: "All questions",
|
||||
createdAt: new Date(),
|
||||
} as TIntegrationSlackConfigData,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={integrationWithExisting}
|
||||
channels={channels}
|
||||
selectedIntegration={null} // Add mode
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
await userEvent.selectOptions(channelDropdown, "channel1");
|
||||
|
||||
expect(screen.getByText("This channel is already connected to another survey.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not show warning when selected channel is the one being edited", async () => {
|
||||
// Edit the existing connection for channel1
|
||||
const integrationToEdit = {
|
||||
...mockSlackIntegration,
|
||||
config: {
|
||||
...mockSlackIntegration.config,
|
||||
data: [
|
||||
{
|
||||
channelId: "channel1",
|
||||
channelName: "#general",
|
||||
surveyId: "survey1",
|
||||
surveyName: "Survey 1",
|
||||
questionIds: ["q1"],
|
||||
questions: "Selected questions",
|
||||
createdAt: new Date(),
|
||||
index: 0,
|
||||
} as TIntegrationSlackConfigData & { index: number },
|
||||
],
|
||||
},
|
||||
};
|
||||
const selectedIntegrationForEdit = integrationToEdit.config.data[0];
|
||||
|
||||
render(
|
||||
<AddChannelMappingModal
|
||||
environmentId={environmentId}
|
||||
open={true}
|
||||
surveys={surveys}
|
||||
setOpen={mockSetOpen}
|
||||
slackIntegration={integrationToEdit}
|
||||
channels={channels}
|
||||
selectedIntegration={selectedIntegrationForEdit} // Edit mode
|
||||
/>
|
||||
);
|
||||
|
||||
const channelDropdown = screen.getByTestId("channel-dropdown");
|
||||
// Channel is already selected via selectedIntegration prop
|
||||
expect(channelDropdown).toHaveValue("channel1");
|
||||
|
||||
expect(
|
||||
screen.queryByText("This channel is already connected to another survey.")
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationSlack, TIntegrationSlackCredential } from "@formbricks/types/integration/slack";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { getSlackChannelsAction } from "../actions";
|
||||
import { authorize } from "../lib/slack";
|
||||
import { SlackWrapper } from "./SlackWrapper";
|
||||
|
||||
// Mock child components and actions
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/actions", () => ({
|
||||
getSlackChannelsAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal",
|
||||
() => ({
|
||||
AddChannelMappingModal: vi.fn(({ open }) => (open ? <div data-testid="add-modal">Add Modal</div> : null)),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration", () => ({
|
||||
ManageIntegration: vi.fn(({ setOpenAddIntegrationModal, setIsConnected, handleSlackAuthorization }) => (
|
||||
<div data-testid="manage-integration">
|
||||
<button onClick={() => setOpenAddIntegrationModal(true)}>Open Modal</button>
|
||||
<button onClick={() => setIsConnected(false)}>Disconnect</button>
|
||||
<button onClick={handleSlackAuthorization}>Reconnect</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/lib/slack", () => ({
|
||||
authorize: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/images/slacklogo.png", () => ({
|
||||
default: "slack-logo-path",
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
ConnectIntegration: vi.fn(({ handleAuthorization, isEnabled }) => (
|
||||
<div data-testid="connect-integration">
|
||||
<button onClick={handleAuthorization} disabled={!isEnabled}>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock window.location.replace
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
replace: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const mockEnvironment = { id: "test-env-id" } as TEnvironment;
|
||||
const mockSurveys: TSurvey[] = [];
|
||||
const mockWebAppUrl = "http://localhost:3000";
|
||||
const mockLocale: TUserLocale = "en-US";
|
||||
const mockSlackChannels: TIntegrationItem[] = [{ id: "C123", name: "general" }];
|
||||
|
||||
const mockSlackIntegration: TIntegrationSlack = {
|
||||
id: "slack-int-1",
|
||||
type: "slack",
|
||||
environmentId: "test-env-id",
|
||||
config: {
|
||||
key: { access_token: "xoxb-valid-token" } as unknown as TIntegrationSlackCredential,
|
||||
data: [],
|
||||
},
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
environment: mockEnvironment,
|
||||
surveys: mockSurveys,
|
||||
webAppUrl: mockWebAppUrl,
|
||||
locale: mockLocale,
|
||||
};
|
||||
|
||||
describe("SlackWrapper", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getSlackChannelsAction).mockResolvedValue({ data: mockSlackChannels });
|
||||
vi.mocked(authorize).mockResolvedValue("https://slack.com/auth");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when not connected (no integration)", () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={undefined} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeEnabled();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration when not connected (integration without key)", () => {
|
||||
const integrationWithoutKey = { ...mockSlackIntegration, config: { data: [], email: "test" } } as any;
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={integrationWithoutKey} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ConnectIntegration disabled when isEnabled is false", () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={false} slackIntegration={undefined} />);
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeDisabled();
|
||||
});
|
||||
|
||||
test("calls authorize and redirects when Connect button is clicked", async () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={undefined} />);
|
||||
const connectButton = screen.getByRole("button", { name: "Connect" });
|
||||
await userEvent.click(connectButton);
|
||||
|
||||
expect(authorize).toHaveBeenCalledWith(mockEnvironment.id, mockWebAppUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith("https://slack.com/auth");
|
||||
});
|
||||
});
|
||||
|
||||
test("renders ManageIntegration and AddChannelMappingModal (hidden) when connected", () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={mockSlackIntegration} />);
|
||||
expect(screen.getByTestId("manage-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("connect-integration")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("add-modal")).not.toBeInTheDocument(); // Modal is initially hidden
|
||||
});
|
||||
|
||||
test("calls getSlackChannelsAction on mount", async () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={mockSlackIntegration} />);
|
||||
await waitFor(() => {
|
||||
expect(getSlackChannelsAction).toHaveBeenCalledWith({ environmentId: mockEnvironment.id });
|
||||
});
|
||||
});
|
||||
|
||||
test("switches from ManageIntegration to ConnectIntegration when disconnected", async () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={mockSlackIntegration} />);
|
||||
expect(screen.getByTestId("manage-integration")).toBeInTheDocument();
|
||||
|
||||
const disconnectButton = screen.getByRole("button", { name: "Disconnect" });
|
||||
await userEvent.click(disconnectButton);
|
||||
|
||||
expect(screen.getByTestId("connect-integration")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("manage-integration")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("opens AddChannelMappingModal when triggered from ManageIntegration", async () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={mockSlackIntegration} />);
|
||||
expect(screen.queryByTestId("add-modal")).not.toBeInTheDocument();
|
||||
|
||||
const openModalButton = screen.getByRole("button", { name: "Open Modal" });
|
||||
await userEvent.click(openModalButton);
|
||||
|
||||
expect(screen.getByTestId("add-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls handleSlackAuthorization when reconnect button is clicked in ManageIntegration", async () => {
|
||||
render(<SlackWrapper {...baseProps} isEnabled={true} slackIntegration={mockSlackIntegration} />);
|
||||
const reconnectButton = screen.getByRole("button", { name: "Reconnect" });
|
||||
await userEvent.click(reconnectButton);
|
||||
|
||||
expect(authorize).toHaveBeenCalledWith(mockEnvironment.id, mockWebAppUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith("https://slack.com/auth");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { authorize } from "./slack";
|
||||
|
||||
// Mock the logger
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe("authorize", () => {
|
||||
const environmentId = "test-env-id";
|
||||
const apiHost = "http://test.com";
|
||||
const expectedUrl = `${apiHost}/api/v1/integrations/slack`;
|
||||
const expectedAuthUrl = "http://slack.com/auth";
|
||||
|
||||
test("should return authUrl on successful fetch", async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { authUrl: expectedAuthUrl } }),
|
||||
} as Response);
|
||||
|
||||
const authUrl = await authorize(environmentId, apiHost);
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: { environmentId },
|
||||
});
|
||||
expect(authUrl).toBe(expectedAuthUrl);
|
||||
});
|
||||
|
||||
test("should throw error and log error on failed fetch", async () => {
|
||||
const errorText = "Failed to fetch";
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
text: async () => errorText,
|
||||
} as Response);
|
||||
|
||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(expectedUrl, {
|
||||
method: "GET",
|
||||
headers: { environmentId },
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith({ errorText }, "authorize: Could not fetch slack config");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/slack/page";
|
||||
import { getIntegrationByType } from "@/lib/integration/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationSlack, TIntegrationSlackCredential } from "@formbricks/types/integration/slack";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys", () => ({
|
||||
getSurveys: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper", () => ({
|
||||
SlackWrapper: vi.fn(({ isEnabled, environment, surveys, slackIntegration, webAppUrl, locale }) => (
|
||||
<div data-testid="slack-wrapper">
|
||||
Mock SlackWrapper: isEnabled={isEnabled.toString()}, envId={environment.id}, surveys=
|
||||
{surveys.length}, integrationId={slackIntegration?.id}, webAppUrl={webAppUrl}, locale={locale}
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_PRODUCTION: true,
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
SLACK_CLIENT_ID: "test-slack-client-id",
|
||||
SLACK_CLIENT_SECRET: "test-slack-client-secret",
|
||||
WEBAPP_URL: "http://test.formbricks.com",
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/integration/service", () => ({
|
||||
getIntegrationByType: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/locale", () => ({
|
||||
findMatchingLocale: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/go-back-button", () => ({
|
||||
GoBackButton: vi.fn(({ url }) => <div data-testid="go-back-button">Go Back: {url}</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: vi.fn(({ children }) => <div>{children}</div>),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: vi.fn(({ pageTitle }) => <h1 data-testid="page-header">{pageTitle}</h1>),
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock data
|
||||
const environmentId = "test-env-id";
|
||||
const mockEnvironment = {
|
||||
id: environmentId,
|
||||
createdAt: new Date(),
|
||||
type: "development",
|
||||
} as unknown as TEnvironment;
|
||||
const mockSurveys: TSurvey[] = [
|
||||
{
|
||||
id: "survey1",
|
||||
name: "Survey 1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: environmentId,
|
||||
status: "inProgress",
|
||||
type: "link",
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||
hiddenFields: { enabled: false },
|
||||
languages: [],
|
||||
styling: null,
|
||||
segment: null,
|
||||
resultShareKey: null,
|
||||
displayPercentage: null,
|
||||
closeOnDate: null,
|
||||
runOnDate: null,
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
const mockSlackIntegration = {
|
||||
id: "slack-int-id",
|
||||
type: "slack",
|
||||
config: {
|
||||
data: [],
|
||||
key: "test-key" as unknown as TIntegrationSlackCredential,
|
||||
},
|
||||
} as unknown as TIntegrationSlack;
|
||||
const mockLocale = "en-US";
|
||||
const mockParams = { params: { environmentId } };
|
||||
|
||||
describe("SlackIntegrationPage", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getSurveys).mockResolvedValue(mockSurveys);
|
||||
vi.mocked(getIntegrationByType).mockResolvedValue(mockSlackIntegration);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale);
|
||||
});
|
||||
|
||||
test("renders correctly when user is not read-only", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
isReadOnly: false,
|
||||
environment: mockEnvironment,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
const tree = await Page(mockParams);
|
||||
render(tree);
|
||||
|
||||
expect(screen.getByTestId("page-header")).toHaveTextContent(
|
||||
"environments.integrations.slack.slack_integration"
|
||||
);
|
||||
expect(screen.getByTestId("go-back-button")).toHaveTextContent(
|
||||
`Go Back: http://test.formbricks.com/environments/${environmentId}/integrations`
|
||||
);
|
||||
expect(screen.getByTestId("slack-wrapper")).toBeInTheDocument();
|
||||
|
||||
// Check props passed to SlackWrapper
|
||||
expect(vi.mocked(SlackWrapper)).toHaveBeenCalledWith(
|
||||
{
|
||||
isEnabled: true, // Since SLACK_CLIENT_ID and SLACK_CLIENT_SECRET are mocked
|
||||
environment: mockEnvironment,
|
||||
surveys: mockSurveys,
|
||||
slackIntegration: mockSlackIntegration,
|
||||
webAppUrl: "http://test.formbricks.com",
|
||||
locale: mockLocale,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(vi.mocked(redirect)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("redirects when user is read-only", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
isReadOnly: true,
|
||||
environment: mockEnvironment,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
|
||||
// Need to actually call the component function to trigger the redirect logic
|
||||
await Page(mockParams);
|
||||
|
||||
expect(vi.mocked(redirect)).toHaveBeenCalledWith("./");
|
||||
expect(vi.mocked(SlackWrapper)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders correctly when Slack integration is not configured", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
isReadOnly: false,
|
||||
environment: mockEnvironment,
|
||||
} as unknown as TEnvironmentAuth);
|
||||
vi.mocked(getIntegrationByType).mockResolvedValue(null); // Simulate no integration found
|
||||
|
||||
const tree = await Page(mockParams);
|
||||
render(tree);
|
||||
|
||||
expect(screen.getByTestId("page-header")).toHaveTextContent(
|
||||
"environments.integrations.slack.slack_integration"
|
||||
);
|
||||
expect(screen.getByTestId("slack-wrapper")).toBeInTheDocument();
|
||||
|
||||
// Check props passed to SlackWrapper when integration is null
|
||||
expect(vi.mocked(SlackWrapper)).toHaveBeenCalledWith(
|
||||
{
|
||||
isEnabled: true,
|
||||
environment: mockEnvironment,
|
||||
surveys: mockSurveys,
|
||||
slackIntegration: null, // Expecting null here
|
||||
webAppUrl: "http://test.formbricks.com",
|
||||
locale: mockLocale,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(vi.mocked(redirect)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import WebhooksPage from "./page";
|
||||
|
||||
vi.mock("@/modules/integrations/webhooks/page", () => ({
|
||||
WebhooksPage: vi.fn(() => <div>WebhooksPageMock</div>),
|
||||
}));
|
||||
|
||||
describe("WebhooksIntegrationPage", () => {
|
||||
test("renders WebhooksPage component", () => {
|
||||
render(<WebhooksPage params={{ environmentId: "test-env-id" }} />);
|
||||
expect(WebhooksPage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -146,7 +146,7 @@ export const PricingTable = ({
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex w-full">
|
||||
<h2 className="mb-3 mr-2 inline-flex w-full text-2xl font-bold text-slate-700">
|
||||
<h2 className="mr-2 mb-3 inline-flex w-full text-2xl font-bold text-slate-700">
|
||||
{t("environments.settings.billing.current_plan")}:{" "}
|
||||
{capitalizeFirstLetter(organization.billing.plan)}
|
||||
{cancellingOn && (
|
||||
@@ -201,7 +201,7 @@ export const PricingTable = ({
|
||||
<div
|
||||
className={cn(
|
||||
"relative mx-8 mb-8 flex flex-col gap-4",
|
||||
peopleUnlimitedCheck && "mb-0 mt-4 flex-row pb-0"
|
||||
peopleUnlimitedCheck && "mt-4 mb-0 flex-row pb-0"
|
||||
)}>
|
||||
<p className="text-md font-semibold text-slate-700">
|
||||
{t("environments.settings.billing.monthly_identified_users")}
|
||||
@@ -224,7 +224,7 @@ export const PricingTable = ({
|
||||
<div
|
||||
className={cn(
|
||||
"relative mx-8 flex flex-col gap-4 pb-12",
|
||||
projectsUnlimitedCheck && "mb-0 mt-4 flex-row pb-0"
|
||||
projectsUnlimitedCheck && "mt-4 mb-0 flex-row pb-0"
|
||||
)}>
|
||||
<p className="text-md font-semibold text-slate-700">{t("common.projects")}</p>
|
||||
{organization.billing.limits.projects && (
|
||||
@@ -260,7 +260,7 @@ export const PricingTable = ({
|
||||
{t("environments.settings.billing.monthly")}
|
||||
</div>
|
||||
<div
|
||||
className={`flex-1 items-center whitespace-nowrap rounded-md py-0.5 pl-4 pr-2 text-center ${
|
||||
className={`flex-1 items-center rounded-md py-0.5 pr-2 pl-4 text-center whitespace-nowrap ${
|
||||
planPeriod === "yearly" ? "bg-slate-200 font-semibold" : "bg-transparent"
|
||||
}`}
|
||||
onClick={() => handleMonthlyToggle("yearly")}>
|
||||
@@ -272,7 +272,7 @@ export const PricingTable = ({
|
||||
</div>
|
||||
<div className="relative mx-auto grid max-w-md grid-cols-1 gap-y-8 lg:mx-0 lg:-mb-14 lg:max-w-none lg:grid-cols-4">
|
||||
<div
|
||||
className="hidden lg:absolute lg:inset-x-px lg:bottom-0 lg:top-4 lg:block lg:rounded-xl lg:rounded-t-2xl lg:border lg:border-slate-200 lg:bg-slate-100 lg:pb-8 lg:ring-1 lg:ring-white/10"
|
||||
className="hidden lg:absolute lg:inset-x-px lg:top-4 lg:bottom-0 lg:block lg:rounded-xl lg:rounded-t-2xl lg:border lg:border-slate-200 lg:bg-slate-100 lg:pb-8 lg:ring-1 lg:ring-white/10"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{getCloudPricingData(t).plans.map((plan) => (
|
||||
|
||||
@@ -297,7 +297,7 @@ export const UploadContactsCSVButton = ({
|
||||
<div className="sticky top-0 flex h-full flex-col rounded-lg">
|
||||
<button
|
||||
className={cn(
|
||||
"absolute right-0 top-0 hidden pr-4 pt-4 text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-0 sm:block"
|
||||
"absolute top-0 right-0 hidden pt-4 pr-4 text-slate-400 hover:text-slate-500 focus:ring-0 focus:outline-none sm:block"
|
||||
)}
|
||||
onClick={() => {
|
||||
resetState(true);
|
||||
@@ -343,7 +343,7 @@ export const UploadContactsCSVButton = ({
|
||||
)}
|
||||
onDragOver={(e) => handleDragOver(e)}
|
||||
onDrop={(e) => handleDrop(e)}>
|
||||
<div className="flex flex-col items-center justify-center pb-6 pt-5">
|
||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<ArrowUpFromLineIcon className="h-6 text-slate-500" />
|
||||
<p className={cn("mt-2 text-center text-sm text-slate-500")}>
|
||||
<span className="font-semibold">{t("common.upload_input_description")}</span>
|
||||
|
||||
@@ -45,6 +45,12 @@ export default defineConfig({
|
||||
"**/*.css", // CSS files
|
||||
"**/templates.ts", // Project-specific template files
|
||||
"scripts/**", // Utility scripts
|
||||
"apps/web/modules/ui/components/icons/*",
|
||||
"**/cache.ts", // Exclude cache files
|
||||
"packages/surveys/src/components/general/smileys.tsx",
|
||||
"apps/web/modules/auth/lib/mock-data.ts", // Exclude mock data files
|
||||
"apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx",
|
||||
"**/*.mjs"
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -21,5 +21,5 @@ sonar.scm.exclusions.disabled=false
|
||||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
# Coverage
|
||||
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts
|
||||
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts
|
||||
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts
|
||||
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts
|
||||
Reference in New Issue
Block a user