mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-25 09:58:59 -06:00
test: ui module test part 2 (#5716)
This commit is contained in:
committed by
GitHub
parent
409f5b1791
commit
0eb64c0084
@@ -25,7 +25,7 @@ vi.mock("@/modules/ui/components/response-badges", () => ({
|
||||
ResponseBadges: ({ items }: any) => <div data-testid="ResponseBadges">{items.join(",")}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/ranking-response", () => ({
|
||||
RankingRespone: ({ value }: any) => <div data-testid="RankingRespone">{value.join(",")}</div>,
|
||||
RankingResponse: ({ value }: any) => <div data-testid="RankingResponse">{value.join(",")}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/analysis/utils", () => ({
|
||||
renderHyperlinkedContent: vi.fn((text: string) => "hyper:" + text),
|
||||
@@ -236,7 +236,7 @@ describe("RenderResponse", () => {
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("9");
|
||||
});
|
||||
|
||||
test("renders RankingRespone for 'Ranking' question", () => {
|
||||
test("renders RankingResponse for 'Ranking' question", () => {
|
||||
const question = { ...defaultQuestion, type: "ranking" };
|
||||
render(
|
||||
<RenderResponse
|
||||
@@ -246,7 +246,7 @@ describe("RenderResponse", () => {
|
||||
language={dummyLanguage}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("RankingRespone")).toHaveTextContent("first,second");
|
||||
expect(screen.getByTestId("RankingResponse")).toHaveTextContent("first,second");
|
||||
});
|
||||
|
||||
test("renders default branch for unknown question type with string", () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||
import { FileUploadResponse } from "@/modules/ui/components/file-upload-response";
|
||||
import { PictureSelectionResponse } from "@/modules/ui/components/picture-selection-response";
|
||||
import { RankingRespone } from "@/modules/ui/components/ranking-response";
|
||||
import { RankingResponse } from "@/modules/ui/components/ranking-response";
|
||||
import { RatingResponse } from "@/modules/ui/components/rating-response";
|
||||
import { ResponseBadges } from "@/modules/ui/components/response-badges";
|
||||
import { CheckCheckIcon, MousePointerClickIcon, PhoneIcon } from "lucide-react";
|
||||
@@ -101,7 +101,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
return (
|
||||
<p
|
||||
key={rowValueInSelectedLanguage}
|
||||
className="ph-no-capture my-1 font-normal text-slate-700 capitalize">
|
||||
className="ph-no-capture my-1 font-normal capitalize text-slate-700">
|
||||
{rowValueInSelectedLanguage}:{processResponseData(responseData[rowValueInSelectedLanguage])}
|
||||
</p>
|
||||
);
|
||||
@@ -161,7 +161,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
if (Array.isArray(responseData)) {
|
||||
return <RankingRespone value={responseData} isExpanded={isExpanded} />;
|
||||
return <RankingResponse value={responseData} isExpanded={isExpanded} />;
|
||||
}
|
||||
default:
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { HighlightedText } from "./index";
|
||||
|
||||
describe("HighlightedText", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders text without highlighting when search value is empty", () => {
|
||||
render(<HighlightedText value="Hello world" searchValue="" />);
|
||||
expect(screen.getByText("Hello world")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("mark")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders text without highlighting when search value is just whitespace", () => {
|
||||
render(<HighlightedText value="Hello world" searchValue=" " />);
|
||||
expect(screen.getByText("Hello world")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("mark")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("highlights matching text when search value is provided", () => {
|
||||
const { container } = render(<HighlightedText value="Hello world" searchValue="world" />);
|
||||
const markElement = container.querySelector("mark");
|
||||
expect(markElement).toBeInTheDocument();
|
||||
expect(markElement?.textContent).toBe("world");
|
||||
expect(container.textContent).toBe("Hello world");
|
||||
});
|
||||
|
||||
test("highlights all instances of matching text", () => {
|
||||
const { container } = render(<HighlightedText value="Hello world, hello everyone" searchValue="hello" />);
|
||||
const markElements = container.querySelectorAll("mark");
|
||||
expect(markElements).toHaveLength(2);
|
||||
expect(markElements[0].textContent?.toLowerCase()).toBe("hello");
|
||||
expect(markElements[1].textContent?.toLowerCase()).toBe("hello");
|
||||
});
|
||||
|
||||
test("handles case insensitive matches", () => {
|
||||
const { container } = render(<HighlightedText value="Hello World" searchValue="world" />);
|
||||
const markElement = container.querySelector("mark");
|
||||
expect(markElement).toBeInTheDocument();
|
||||
expect(markElement?.textContent).toBe("World");
|
||||
});
|
||||
|
||||
test("escapes special regex characters in search value", () => {
|
||||
const { container } = render(<HighlightedText value="Hello (world)" searchValue="(world)" />);
|
||||
const markElement = container.querySelector("mark");
|
||||
expect(markElement).toBeInTheDocument();
|
||||
expect(markElement?.textContent).toBe("(world)");
|
||||
});
|
||||
|
||||
test("maintains the correct order of text fragments", () => {
|
||||
const { container } = render(<HighlightedText value="apple banana apple" searchValue="apple" />);
|
||||
expect(container.textContent).toBe("apple banana apple");
|
||||
|
||||
const markElements = container.querySelectorAll("mark");
|
||||
expect(markElements).toHaveLength(2);
|
||||
expect(markElements[0].textContent).toBe("apple");
|
||||
expect(markElements[1].textContent).toBe("apple");
|
||||
});
|
||||
});
|
||||
147
apps/web/modules/ui/components/iconbar/index.test.tsx
Normal file
147
apps/web/modules/ui/components/iconbar/index.test.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { IconBar } from "./index";
|
||||
|
||||
vi.mock("@/modules/ui/components/tooltip", () => ({
|
||||
TooltipRenderer: ({ children, tooltipContent }: { children: React.ReactNode; tooltipContent: string }) => (
|
||||
<div data-testid="tooltip" data-tooltip={tooltipContent}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../button", () => ({
|
||||
Button: ({ children, onClick, className, size, "aria-label": ariaLabel }: any) => (
|
||||
<button
|
||||
data-testid="button"
|
||||
data-class-name={className}
|
||||
data-size={size}
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("IconBar", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders nothing when actions array is empty", () => {
|
||||
const { container } = render(<IconBar actions={[]} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
test("renders only visible actions", () => {
|
||||
const MockIcon1 = () => <div data-testid="mock-icon-1">Icon 1</div>;
|
||||
const MockIcon2 = () => <div data-testid="mock-icon-2">Icon 2</div>;
|
||||
|
||||
const actions = [
|
||||
{
|
||||
icon: MockIcon1,
|
||||
tooltip: "Action 1",
|
||||
onClick: vi.fn(),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
icon: MockIcon2,
|
||||
tooltip: "Action 2",
|
||||
onClick: vi.fn(),
|
||||
isVisible: false,
|
||||
},
|
||||
];
|
||||
|
||||
render(<IconBar actions={actions as any} />);
|
||||
|
||||
expect(screen.getByRole("toolbar")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-icon-1")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("mock-icon-2")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders multiple actions correctly", () => {
|
||||
const MockIcon1 = () => <div data-testid="mock-icon-1">Icon 1</div>;
|
||||
const MockIcon2 = () => <div data-testid="mock-icon-2">Icon 2</div>;
|
||||
|
||||
const actions = [
|
||||
{
|
||||
icon: MockIcon1,
|
||||
tooltip: "Action 1",
|
||||
onClick: vi.fn(),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
icon: MockIcon2,
|
||||
tooltip: "Action 2",
|
||||
onClick: vi.fn(),
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
render(<IconBar actions={actions as any} />);
|
||||
|
||||
expect(screen.getAllByTestId("tooltip")).toHaveLength(2);
|
||||
expect(screen.getByTestId("mock-icon-1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-icon-2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("triggers onClick handler when button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const MockIcon = () => <div data-testid="mock-icon">Icon</div>;
|
||||
const handleClick = vi.fn();
|
||||
|
||||
const actions = [
|
||||
{
|
||||
icon: MockIcon,
|
||||
tooltip: "Action",
|
||||
onClick: handleClick,
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
render(<IconBar actions={actions as any} />);
|
||||
|
||||
const button = screen.getByTestId("button");
|
||||
await user.click(button);
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("renders tooltip with correct content", () => {
|
||||
const MockIcon = () => <div data-testid="mock-icon">Icon</div>;
|
||||
|
||||
const actions = [
|
||||
{
|
||||
icon: MockIcon,
|
||||
tooltip: "Test Tooltip",
|
||||
onClick: vi.fn(),
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
render(<IconBar actions={actions as any} />);
|
||||
|
||||
const tooltip = screen.getByTestId("tooltip");
|
||||
expect(tooltip).toHaveAttribute("data-tooltip", "Test Tooltip");
|
||||
});
|
||||
|
||||
test("sets aria-label on button correctly", () => {
|
||||
const MockIcon = () => <div data-testid="mock-icon">Icon</div>;
|
||||
|
||||
const actions = [
|
||||
{
|
||||
icon: MockIcon,
|
||||
tooltip: "Test Tooltip",
|
||||
onClick: vi.fn(),
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
render(<IconBar actions={actions as any} />);
|
||||
|
||||
const button = screen.getByTestId("button");
|
||||
expect(button).toHaveAttribute("aria-label", "Test Tooltip");
|
||||
});
|
||||
});
|
||||
264
apps/web/modules/ui/components/input-combo-box/index.test.tsx
Normal file
264
apps/web/modules/ui/components/input-combo-box/index.test.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { LucideSettings, User } from "lucide-react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { InputCombobox, TComboboxOption } from "./index";
|
||||
|
||||
// Mock components used by InputCombobox
|
||||
vi.mock("@/modules/ui/components/command", () => ({
|
||||
Command: ({ children, className }: any) => (
|
||||
<div data-testid="command" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CommandInput: ({ placeholder, className }: any) => (
|
||||
<input data-testid="command-input" placeholder={placeholder} className={className} />
|
||||
),
|
||||
CommandList: ({ children, className }: any) => (
|
||||
<div data-testid="command-list" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CommandEmpty: ({ children, className }: any) => (
|
||||
<div data-testid="command-empty" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CommandGroup: ({ children, heading }: any) => (
|
||||
<div data-testid="command-group" data-heading={heading}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CommandItem: ({ children, onSelect, className }: any) => (
|
||||
<div data-testid="command-item" className={className} onClick={onSelect}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CommandSeparator: ({ className }: any) => <hr data-testid="command-separator" className={className} />,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/popover", () => ({
|
||||
Popover: ({ children, open, onOpenChange }: any) => (
|
||||
<div data-testid="popover" data-open={open}>
|
||||
{children}
|
||||
<button data-testid="toggle-popover" onClick={() => onOpenChange(!open)}>
|
||||
Toggle Popover
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
PopoverTrigger: ({ children, asChild }: any) => (
|
||||
<div data-testid="popover-trigger" data-as-child={asChild}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
PopoverContent: ({ children, className }: any) => (
|
||||
<div data-testid="popover-content" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/input", () => ({
|
||||
Input: ({ id, className, value, onChange, ...props }: any) => (
|
||||
<input
|
||||
data-testid="input"
|
||||
id={id}
|
||||
className={className}
|
||||
value={value || ""}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("next/image", () => ({
|
||||
default: ({ src, alt, width, height, className }: any) => (
|
||||
<img data-testid="next-image" src={src} alt={alt} width={width} height={height} className={className} />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("InputCombobox", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockOptions: TComboboxOption[] = [
|
||||
{ label: "Option 1", value: "opt1" },
|
||||
{ label: "Option 2", value: "opt2" },
|
||||
{ icon: User, label: "User Option", value: "user" },
|
||||
{ imgSrc: "/test-image.jpg", label: "Image Option", value: "img" },
|
||||
];
|
||||
|
||||
const mockGroupedOptions = [
|
||||
{
|
||||
label: "Group 1",
|
||||
value: "group1",
|
||||
options: [
|
||||
{ label: "Group 1 Option 1", value: "g1opt1" },
|
||||
{ label: "Group 1 Option 2", value: "g1opt2" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Group 2",
|
||||
value: "group2",
|
||||
options: [
|
||||
{ label: "Group 2 Option 1", value: "g2opt1" },
|
||||
{ icon: LucideSettings, label: "Settings", value: "settings" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
test("renders with default props", () => {
|
||||
render(<InputCombobox id="test-combo" options={mockOptions} onChangeValue={() => {}} />);
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("popover")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("command-input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders without search when showSearch is false", () => {
|
||||
render(
|
||||
<InputCombobox id="test-combo" options={mockOptions} onChangeValue={() => {}} showSearch={false} />
|
||||
);
|
||||
expect(screen.queryByTestId("command-input")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with options", () => {
|
||||
render(<InputCombobox id="test-combo" options={mockOptions} onChangeValue={() => {}} />);
|
||||
expect(screen.getAllByTestId("command-item")).toHaveLength(mockOptions.length);
|
||||
});
|
||||
|
||||
test("renders with grouped options", () => {
|
||||
render(<InputCombobox id="test-combo" groupedOptions={mockGroupedOptions} onChangeValue={() => {}} />);
|
||||
expect(screen.getAllByTestId("command-group")).toHaveLength(mockGroupedOptions.length);
|
||||
expect(screen.getByTestId("command-separator")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with input when withInput is true", () => {
|
||||
render(<InputCombobox id="test-combo" options={mockOptions} onChangeValue={() => {}} withInput={true} />);
|
||||
expect(screen.getByTestId("input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles option selection", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChangeValue = vi.fn();
|
||||
|
||||
render(<InputCombobox id="test-combo" options={mockOptions} onChangeValue={onChangeValue} />);
|
||||
|
||||
// Toggle popover to open dropdown
|
||||
await user.click(screen.getByTestId("toggle-popover"));
|
||||
|
||||
// Click on an option
|
||||
const items = screen.getAllByTestId("command-item");
|
||||
await user.click(items[0]);
|
||||
|
||||
expect(onChangeValue).toHaveBeenCalledWith("opt1", expect.objectContaining({ value: "opt1" }));
|
||||
});
|
||||
|
||||
test("handles multi-select", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChangeValue = vi.fn();
|
||||
|
||||
render(
|
||||
<InputCombobox
|
||||
id="test-combo"
|
||||
options={mockOptions}
|
||||
onChangeValue={onChangeValue}
|
||||
allowMultiSelect={true}
|
||||
showCheckIcon={true}
|
||||
/>
|
||||
);
|
||||
|
||||
// Toggle popover to open dropdown
|
||||
await user.click(screen.getByTestId("toggle-popover"));
|
||||
|
||||
// Click on an option
|
||||
const items = screen.getAllByTestId("command-item");
|
||||
await user.click(items[0]);
|
||||
|
||||
expect(onChangeValue).toHaveBeenCalledWith(["opt1"], expect.objectContaining({ value: "opt1" }));
|
||||
|
||||
// Click on another option
|
||||
await user.click(items[1]);
|
||||
|
||||
expect(onChangeValue).toHaveBeenCalledWith(["opt1", "opt2"], expect.objectContaining({ value: "opt2" }));
|
||||
});
|
||||
|
||||
test("handles input change when withInput is true", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChangeValue = vi.fn();
|
||||
|
||||
render(
|
||||
<InputCombobox id="test-combo" options={mockOptions} onChangeValue={onChangeValue} withInput={true} />
|
||||
);
|
||||
|
||||
const input = screen.getByTestId("input");
|
||||
await user.type(input, "test");
|
||||
|
||||
expect(onChangeValue).toHaveBeenCalledWith("test", undefined, true);
|
||||
});
|
||||
|
||||
test("renders with clearable option and handles clear", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChangeValue = vi.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<InputCombobox id="test-combo" options={mockOptions} onChangeValue={onChangeValue} clearable={true} />
|
||||
);
|
||||
|
||||
// Select an option first to show the clear button
|
||||
await user.click(screen.getByTestId("toggle-popover"));
|
||||
const items = screen.getAllByTestId("command-item");
|
||||
await user.click(items[0]);
|
||||
|
||||
// Rerender with the selected value
|
||||
rerender(
|
||||
<InputCombobox
|
||||
id="test-combo"
|
||||
options={mockOptions}
|
||||
value="opt1"
|
||||
onChangeValue={onChangeValue}
|
||||
clearable={true}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find and click the X icon (simulated)
|
||||
const clearButton = screen.getByText("Toggle Popover");
|
||||
await user.click(clearButton);
|
||||
|
||||
// Verify onChangeValue was called
|
||||
expect(onChangeValue).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders custom empty dropdown text", () => {
|
||||
render(
|
||||
<InputCombobox
|
||||
id="test-combo"
|
||||
options={[]}
|
||||
onChangeValue={() => {}}
|
||||
emptyDropdownText="custom.empty.text"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("command-empty").textContent).toBe("custom.empty.text");
|
||||
});
|
||||
|
||||
test("renders with value pre-selected", () => {
|
||||
render(<InputCombobox id="test-combo" options={mockOptions} value="opt1" onChangeValue={() => {}} />);
|
||||
|
||||
expect(screen.getByRole("combobox")).toHaveTextContent("Option 1");
|
||||
});
|
||||
|
||||
test("handles icons and images in options", () => {
|
||||
render(<InputCombobox id="test-combo" options={mockOptions} value="user" onChangeValue={() => {}} />);
|
||||
|
||||
// Should render the User icon for the selected option
|
||||
expect(screen.getByRole("combobox")).toHaveTextContent("User Option");
|
||||
});
|
||||
});
|
||||
93
apps/web/modules/ui/components/input/index.test.tsx
Normal file
93
apps/web/modules/ui/components/input/index.test.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import * as React from "react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { Input } from "./index";
|
||||
|
||||
describe("Input", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with default props", () => {
|
||||
render(<Input data-testid="test-input" />);
|
||||
const input = screen.getByTestId("test-input");
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveClass("flex h-10 w-full rounded-md border border-slate-300");
|
||||
});
|
||||
|
||||
test("applies additional className when provided", () => {
|
||||
render(<Input data-testid="test-input" className="test-class" />);
|
||||
const input = screen.getByTestId("test-input");
|
||||
expect(input).toHaveClass("test-class");
|
||||
});
|
||||
|
||||
test("renders with invalid styling when isInvalid is true", () => {
|
||||
render(<Input data-testid="test-input" isInvalid={true} />);
|
||||
const input = screen.getByTestId("test-input");
|
||||
expect(input).toHaveClass("border-red-500");
|
||||
});
|
||||
|
||||
test("forwards ref to input element", () => {
|
||||
const inputRef = React.createRef<HTMLInputElement>();
|
||||
render(<Input ref={inputRef} data-testid="test-input" />);
|
||||
expect(inputRef.current).not.toBeNull();
|
||||
expect(inputRef.current).toBe(screen.getByTestId("test-input"));
|
||||
});
|
||||
|
||||
test("applies disabled styles when disabled prop is provided", () => {
|
||||
render(<Input data-testid="test-input" disabled />);
|
||||
const input = screen.getByTestId("test-input");
|
||||
expect(input).toBeDisabled();
|
||||
expect(input).toHaveClass("disabled:cursor-not-allowed disabled:opacity-50");
|
||||
});
|
||||
|
||||
test("handles user input correctly", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Input data-testid="test-input" />);
|
||||
const input = screen.getByTestId("test-input");
|
||||
|
||||
await user.type(input, "hello");
|
||||
expect(input).toHaveValue("hello");
|
||||
});
|
||||
|
||||
test("handles value prop correctly", () => {
|
||||
render(<Input data-testid="test-input" value="test-value" readOnly />);
|
||||
const input = screen.getByTestId("test-input");
|
||||
expect(input).toHaveValue("test-value");
|
||||
});
|
||||
|
||||
test("handles placeholder prop correctly", () => {
|
||||
render(<Input data-testid="test-input" placeholder="test-placeholder" />);
|
||||
const input = screen.getByTestId("test-input");
|
||||
expect(input).toHaveAttribute("placeholder", "test-placeholder");
|
||||
});
|
||||
|
||||
test("passes HTML attributes to the input element", () => {
|
||||
render(
|
||||
<Input
|
||||
data-testid="test-input"
|
||||
type="password"
|
||||
name="password"
|
||||
maxLength={10}
|
||||
aria-label="Password input"
|
||||
/>
|
||||
);
|
||||
const input = screen.getByTestId("test-input");
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
expect(input).toHaveAttribute("name", "password");
|
||||
expect(input).toHaveAttribute("maxLength", "10");
|
||||
expect(input).toHaveAttribute("aria-label", "Password input");
|
||||
});
|
||||
|
||||
test("applies focus styles on focus", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Input data-testid="test-input" />);
|
||||
const input = screen.getByTestId("test-input");
|
||||
|
||||
expect(input).not.toHaveFocus();
|
||||
await user.click(input);
|
||||
expect(input).toHaveFocus();
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, isInv
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
"focus:border-brand-dark flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300",
|
||||
"focus:border-brand-dark flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300",
|
||||
className,
|
||||
isInvalid && "border border-red-500 focus:border-red-500"
|
||||
)}
|
||||
|
||||
161
apps/web/modules/ui/components/integration-card/index.test.tsx
Normal file
161
apps/web/modules/ui/components/integration-card/index.test.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { Card } from "./index";
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ children, href, target }: { children: React.ReactNode; href: string; target?: string }) => (
|
||||
<a href={href} target={target} data-testid="mock-link">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, disabled, size, variant }: any) => (
|
||||
<button disabled={disabled} data-size={size} data-variant={variant} data-testid="mock-button">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("Integration Card", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders basic card with label and description", () => {
|
||||
render(<Card label="Test Label" description="Test Description" />);
|
||||
|
||||
expect(screen.getByText("Test Label")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Description")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders icon when provided", () => {
|
||||
const testIcon = <div data-testid="test-icon">Icon</div>;
|
||||
render(<Card label="Test Label" description="Test Description" icon={testIcon} />);
|
||||
|
||||
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders connect button with link when connectHref is provided", () => {
|
||||
render(
|
||||
<Card label="Test Label" description="Test Description" connectText="Connect" connectHref="/connect" />
|
||||
);
|
||||
|
||||
const button = screen.getByTestId("mock-button");
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
const link = screen.getByTestId("mock-link");
|
||||
expect(link).toHaveAttribute("href", "/connect");
|
||||
expect(link).toHaveTextContent("Connect");
|
||||
});
|
||||
|
||||
test("renders docs button with link when docsHref is provided", () => {
|
||||
render(
|
||||
<Card label="Test Label" description="Test Description" docsText="Documentation" docsHref="/docs" />
|
||||
);
|
||||
|
||||
const button = screen.getByTestId("mock-button");
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveAttribute("data-variant", "secondary");
|
||||
|
||||
const link = screen.getByTestId("mock-link");
|
||||
expect(link).toHaveAttribute("href", "/docs");
|
||||
expect(link).toHaveTextContent("Documentation");
|
||||
});
|
||||
|
||||
test("renders both connect and docs buttons when both hrefs are provided", () => {
|
||||
render(
|
||||
<Card
|
||||
label="Test Label"
|
||||
description="Test Description"
|
||||
connectText="Connect"
|
||||
connectHref="/connect"
|
||||
docsText="Documentation"
|
||||
docsHref="/docs"
|
||||
/>
|
||||
);
|
||||
|
||||
const buttons = screen.getAllByTestId("mock-button");
|
||||
expect(buttons).toHaveLength(2);
|
||||
|
||||
const links = screen.getAllByTestId("mock-link");
|
||||
expect(links).toHaveLength(2);
|
||||
expect(links[0]).toHaveAttribute("href", "/connect");
|
||||
expect(links[1]).toHaveAttribute("href", "/docs");
|
||||
});
|
||||
|
||||
test("sets target to _blank when connectNewTab is true", () => {
|
||||
render(
|
||||
<Card
|
||||
label="Test Label"
|
||||
description="Test Description"
|
||||
connectText="Connect"
|
||||
connectHref="/connect"
|
||||
connectNewTab={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const link = screen.getByTestId("mock-link");
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
});
|
||||
|
||||
test("sets target to _blank when docsNewTab is true", () => {
|
||||
render(
|
||||
<Card
|
||||
label="Test Label"
|
||||
description="Test Description"
|
||||
docsText="Documentation"
|
||||
docsHref="/docs"
|
||||
docsNewTab={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const link = screen.getByTestId("mock-link");
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
});
|
||||
|
||||
test("renders status text with green indicator when connected is true", () => {
|
||||
render(
|
||||
<Card label="Test Label" description="Test Description" connected={true} statusText="Connected" />
|
||||
);
|
||||
|
||||
expect(screen.getByText("Connected")).toBeInTheDocument();
|
||||
// Check for green indicator by inspecting the span with the animation class
|
||||
const container = screen.getByText("Connected").parentElement;
|
||||
const animatedSpan = container?.querySelector(".animate-ping-slow");
|
||||
expect(animatedSpan).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders status text with gray indicator when connected is false", () => {
|
||||
render(
|
||||
<Card label="Test Label" description="Test Description" connected={false} statusText="Disconnected" />
|
||||
);
|
||||
|
||||
expect(screen.getByText("Disconnected")).toBeInTheDocument();
|
||||
// Check for gray indicator by inspecting the span without the animation class
|
||||
const container = screen.getByText("Disconnected").parentElement;
|
||||
const graySpan = container?.querySelector(".bg-slate-400");
|
||||
expect(graySpan).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("disables buttons when disabled prop is true", () => {
|
||||
render(
|
||||
<Card
|
||||
label="Test Label"
|
||||
description="Test Description"
|
||||
connectText="Connect"
|
||||
connectHref="/connect"
|
||||
docsText="Documentation"
|
||||
docsHref="/docs"
|
||||
disabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const buttons = screen.getAllByTestId("mock-button");
|
||||
buttons.forEach((button) => {
|
||||
expect(button).toHaveAttribute("disabled");
|
||||
});
|
||||
});
|
||||
});
|
||||
57
apps/web/modules/ui/components/label/index.test.tsx
Normal file
57
apps/web/modules/ui/components/label/index.test.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { Label } from "./index";
|
||||
|
||||
// Mock Radix UI Label primitive
|
||||
vi.mock("@radix-ui/react-label", () => ({
|
||||
Root: ({ children, className, htmlFor, ...props }: any) => (
|
||||
<label className={className} htmlFor={htmlFor} data-testid="radix-label" {...props}>
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("Label", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with default styling", () => {
|
||||
render(<Label>Test Label</Label>);
|
||||
|
||||
const label = screen.getByTestId("radix-label");
|
||||
expect(label).toBeInTheDocument();
|
||||
expect(label).toHaveTextContent("Test Label");
|
||||
expect(label).toHaveClass("text-sm", "leading-none", "font-medium", "text-slate-800");
|
||||
});
|
||||
|
||||
test("applies additional className when provided", () => {
|
||||
render(<Label className="custom-class">Test Label</Label>);
|
||||
|
||||
const label = screen.getByTestId("radix-label");
|
||||
expect(label).toHaveClass("custom-class");
|
||||
expect(label).toHaveClass("text-sm", "leading-none", "font-medium", "text-slate-800");
|
||||
});
|
||||
|
||||
test("forwards ref to underlying label element", () => {
|
||||
const ref = React.createRef<HTMLLabelElement>();
|
||||
render(<Label ref={ref}>Test Label</Label>);
|
||||
|
||||
expect(ref.current).not.toBeNull();
|
||||
expect(ref.current).toBe(screen.getByTestId("radix-label"));
|
||||
});
|
||||
|
||||
test("passes additional props to underlying label element", () => {
|
||||
render(
|
||||
<Label data-custom="test-data" id="test-id">
|
||||
Test Label
|
||||
</Label>
|
||||
);
|
||||
|
||||
const label = screen.getByTestId("radix-label");
|
||||
expect(label).toHaveAttribute("data-custom", "test-data");
|
||||
expect(label).toHaveAttribute("id", "test-id");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { LimitsReachedBanner } from "./index";
|
||||
|
||||
// Mock the next/link component
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href} data-testid="mock-link">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("LimitsReachedBanner", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockOrganization: TOrganization = {
|
||||
id: "org-123",
|
||||
name: "Test Organization",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
billing: {
|
||||
plan: "free",
|
||||
period: "monthly",
|
||||
periodStart: new Date(),
|
||||
stripeCustomerId: null,
|
||||
limits: {
|
||||
monthly: {
|
||||
responses: 100,
|
||||
miu: 100,
|
||||
},
|
||||
projects: 1,
|
||||
},
|
||||
},
|
||||
isAIEnabled: false,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
organization: mockOrganization,
|
||||
environmentId: "env-123",
|
||||
peopleCount: 0,
|
||||
responseCount: 0,
|
||||
};
|
||||
|
||||
test("does not render when no limits are reached", () => {
|
||||
const { container } = render(<LimitsReachedBanner {...defaultProps} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
test("renders when people limit is reached", () => {
|
||||
const peopleCount = 100;
|
||||
render(<LimitsReachedBanner {...defaultProps} peopleCount={peopleCount} />);
|
||||
|
||||
expect(screen.getByText("common.limits_reached")).toBeInTheDocument();
|
||||
|
||||
const learnMoreLink = screen.getByTestId("mock-link");
|
||||
expect(learnMoreLink).toHaveAttribute("href", "/environments/env-123/settings/billing");
|
||||
expect(learnMoreLink.textContent).toBe("common.learn_more");
|
||||
});
|
||||
|
||||
test("renders when response limit is reached", () => {
|
||||
const responseCount = 100;
|
||||
render(<LimitsReachedBanner {...defaultProps} responseCount={responseCount} />);
|
||||
|
||||
expect(screen.getByText("common.limits_reached")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders when both limits are reached", () => {
|
||||
const peopleCount = 100;
|
||||
const responseCount = 100;
|
||||
render(<LimitsReachedBanner {...defaultProps} peopleCount={peopleCount} responseCount={responseCount} />);
|
||||
|
||||
expect(screen.getByText("common.limits_reached")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("closes the banner when the close button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LimitsReachedBanner {...defaultProps} peopleCount={100} />);
|
||||
|
||||
const closeButton = screen.getByRole("button", { name: /close/i });
|
||||
expect(closeButton).toBeInTheDocument();
|
||||
|
||||
await user.click(closeButton);
|
||||
|
||||
expect(screen.queryByText("common.limits_reached")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not render when limits are undefined", () => {
|
||||
const orgWithoutLimits: TOrganization = {
|
||||
...mockOrganization,
|
||||
billing: {
|
||||
...mockOrganization.billing,
|
||||
limits: {
|
||||
monthly: {
|
||||
responses: null,
|
||||
miu: null,
|
||||
},
|
||||
projects: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
<LimitsReachedBanner
|
||||
{...defaultProps}
|
||||
organization={orgWithoutLimits}
|
||||
peopleCount={100}
|
||||
responseCount={100}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
243
apps/web/modules/ui/components/load-segment-modal/index.test.tsx
Normal file
243
apps/web/modules/ui/components/load-segment-modal/index.test.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
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 { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { LoadSegmentModal } from ".";
|
||||
|
||||
// Mock the nested SegmentDetail component
|
||||
vi.mock("lucide-react", async () => {
|
||||
const actual = await vi.importActual<typeof import("lucide-react")>("lucide-react");
|
||||
return {
|
||||
...actual,
|
||||
Loader2: vi.fn(() => <div data-testid="loader-icon">Loader</div>),
|
||||
UsersIcon: vi.fn(() => <div data-testid="users-icon">Users</div>),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock react-hot-toast
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("LoadSegmentModal", () => {
|
||||
const mockDate = new Date("2023-01-01T12:00:00Z");
|
||||
|
||||
const mockCurrentSegment: TSegment = {
|
||||
id: "current-segment-id",
|
||||
title: "Current Segment",
|
||||
description: "Current segment description",
|
||||
isPrivate: false,
|
||||
filters: [],
|
||||
environmentId: "env-1",
|
||||
surveys: ["survey-1"],
|
||||
createdAt: mockDate,
|
||||
updatedAt: mockDate,
|
||||
};
|
||||
|
||||
const mockSegments: TSegment[] = [
|
||||
{
|
||||
id: "segment-1",
|
||||
title: "Segment 1",
|
||||
description: "Test segment 1",
|
||||
isPrivate: false,
|
||||
filters: [],
|
||||
environmentId: "env-1",
|
||||
surveys: ["survey-1"],
|
||||
createdAt: new Date("2023-01-02T12:00:00Z"),
|
||||
updatedAt: new Date("2023-01-05T12:00:00Z"),
|
||||
},
|
||||
{
|
||||
id: "segment-2",
|
||||
title: "Segment 2",
|
||||
description: "Test segment 2",
|
||||
isPrivate: false,
|
||||
filters: [],
|
||||
environmentId: "env-1",
|
||||
surveys: ["survey-1"],
|
||||
createdAt: new Date("2023-02-02T12:00:00Z"),
|
||||
updatedAt: new Date("2023-02-05T12:00:00Z"),
|
||||
},
|
||||
{
|
||||
id: "segment-3",
|
||||
title: "Segment 3 (Private)",
|
||||
description: "This is private",
|
||||
isPrivate: true,
|
||||
filters: [],
|
||||
environmentId: "env-1",
|
||||
surveys: ["survey-1"],
|
||||
createdAt: mockDate,
|
||||
updatedAt: mockDate,
|
||||
},
|
||||
];
|
||||
|
||||
const mockSurveyId = "survey-1";
|
||||
const mockSetOpen = vi.fn();
|
||||
const mockSetSegment = vi.fn();
|
||||
const mockSetIsSegmentEditorOpen = vi.fn();
|
||||
const mockOnSegmentLoad = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
open: true,
|
||||
setOpen: mockSetOpen,
|
||||
surveyId: mockSurveyId,
|
||||
currentSegment: mockCurrentSegment,
|
||||
segments: mockSegments,
|
||||
setSegment: mockSetSegment,
|
||||
setIsSegmentEditorOpen: mockSetIsSegmentEditorOpen,
|
||||
onSegmentLoad: mockOnSegmentLoad,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders empty state when no segments are available", () => {
|
||||
render(<LoadSegmentModal {...defaultProps} segments={[]} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("environments.surveys.edit.you_have_not_created_a_segment_yet")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText("common.segment")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders segments list correctly when segments are available", () => {
|
||||
render(<LoadSegmentModal {...defaultProps} />);
|
||||
|
||||
// Headers
|
||||
expect(screen.getByText("common.segment")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.updated_at")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.created_at")).toBeInTheDocument();
|
||||
|
||||
// Only non-private segments should be visible (2 out of 3)
|
||||
expect(screen.getByText("Segment 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Segment 2")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Segment 3 (Private)")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("clicking on a segment loads it and closes the modal", async () => {
|
||||
mockOnSegmentLoad.mockResolvedValueOnce({
|
||||
id: "survey-1",
|
||||
segment: {
|
||||
id: "segment-1",
|
||||
title: "Segment 1",
|
||||
description: "Test segment 1",
|
||||
isPrivate: false,
|
||||
filters: [],
|
||||
environmentId: "env-1",
|
||||
surveys: ["survey-1"],
|
||||
},
|
||||
} as unknown as TSurvey);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<LoadSegmentModal {...defaultProps} />);
|
||||
|
||||
// Find and click the first segment
|
||||
const segmentElements = screen.getAllByText(/Segment \d/);
|
||||
await user.click(segmentElements[0]);
|
||||
|
||||
// Wait for the segment to be loaded
|
||||
await waitFor(() => {
|
||||
expect(mockOnSegmentLoad).toHaveBeenCalledWith(mockSurveyId, "segment-1");
|
||||
expect(mockSetSegment).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test("displays loading indicator while loading a segment", async () => {
|
||||
// Mock a delayed resolution to see the loading state
|
||||
mockOnSegmentLoad.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
id: "survey-1",
|
||||
segment: {
|
||||
id: "segment-1",
|
||||
title: "Segment 1",
|
||||
description: "Test segment 1",
|
||||
isPrivate: false,
|
||||
filters: [],
|
||||
environmentId: "env-1",
|
||||
surveys: ["survey-1"],
|
||||
},
|
||||
} as unknown as TSurvey);
|
||||
}, 100);
|
||||
})
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<LoadSegmentModal {...defaultProps} />);
|
||||
|
||||
// Find and click the first segment
|
||||
const segmentElements = screen.getAllByText(/Segment \d/);
|
||||
await user.click(segmentElements[0]);
|
||||
|
||||
// Check for loader
|
||||
expect(screen.getByTestId("loader-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows error toast when segment loading fails", async () => {
|
||||
mockOnSegmentLoad.mockRejectedValueOnce(new Error("Failed to load segment"));
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<LoadSegmentModal {...defaultProps} />);
|
||||
|
||||
// Find and click the first segment
|
||||
const segmentElements = screen.getAllByText(/Segment \d/);
|
||||
await user.click(segmentElements[0]);
|
||||
|
||||
// Wait for the error toast
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
// The toast error is mocked, so we're just verifying the modal closes
|
||||
});
|
||||
});
|
||||
|
||||
test("doesn't attempt to load a segment if it's the current one", async () => {
|
||||
const currentSegmentProps = {
|
||||
...defaultProps,
|
||||
segments: [mockCurrentSegment], // Only the current segment is available
|
||||
};
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<LoadSegmentModal {...currentSegmentProps} />);
|
||||
|
||||
// Click the current segment
|
||||
await user.click(screen.getByText("Current Segment"));
|
||||
|
||||
// onSegmentLoad shouldn't be called since we're already using this segment
|
||||
expect(mockOnSegmentLoad).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles invalid segment data gracefully", async () => {
|
||||
// Mock an incomplete response from onSegmentLoad
|
||||
mockOnSegmentLoad.mockResolvedValueOnce({
|
||||
// Missing id or segment properties
|
||||
} as unknown as TSurvey);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<LoadSegmentModal {...defaultProps} />);
|
||||
|
||||
// Find and click the first segment
|
||||
const segmentElements = screen.getAllByText(/Segment \d/);
|
||||
await user.click(segmentElements[0]);
|
||||
|
||||
// Wait for error handling
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { LoadingSpinner } from ".";
|
||||
|
||||
describe("LoadingSpinner", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with default className", () => {
|
||||
render(<LoadingSpinner />);
|
||||
|
||||
const svg = document.querySelector("svg");
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(svg?.classList.contains("h-6")).toBe(true);
|
||||
expect(svg?.classList.contains("w-6")).toBe(true);
|
||||
expect(svg?.classList.contains("m-2")).toBe(true);
|
||||
expect(svg?.classList.contains("animate-spin")).toBe(true);
|
||||
expect(svg?.classList.contains("text-slate-700")).toBe(true);
|
||||
});
|
||||
|
||||
test("renders with custom className", () => {
|
||||
render(<LoadingSpinner className="h-10 w-10" />);
|
||||
|
||||
const svg = document.querySelector("svg");
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(svg?.classList.contains("h-10")).toBe(true);
|
||||
expect(svg?.classList.contains("w-10")).toBe(true);
|
||||
expect(svg?.classList.contains("m-2")).toBe(true);
|
||||
expect(svg?.classList.contains("animate-spin")).toBe(true);
|
||||
expect(svg?.classList.contains("text-slate-700")).toBe(true);
|
||||
});
|
||||
|
||||
test("renders with correct SVG structure", () => {
|
||||
render(<LoadingSpinner />);
|
||||
|
||||
const svg = document.querySelector("svg");
|
||||
expect(svg).toBeInTheDocument();
|
||||
|
||||
// Check that SVG has correct attributes
|
||||
expect(svg?.getAttribute("xmlns")).toBe("http://www.w3.org/2000/svg");
|
||||
expect(svg?.getAttribute("fill")).toBe("none");
|
||||
expect(svg?.getAttribute("viewBox")).toBe("0 0 24 24");
|
||||
|
||||
// Check that SVG contains circle and path elements
|
||||
const circle = svg?.querySelector("circle");
|
||||
const path = svg?.querySelector("path");
|
||||
|
||||
expect(circle).toBeInTheDocument();
|
||||
expect(path).toBeInTheDocument();
|
||||
|
||||
// Check circle attributes
|
||||
expect(circle?.getAttribute("cx")).toBe("12");
|
||||
expect(circle?.getAttribute("cy")).toBe("12");
|
||||
expect(circle?.getAttribute("r")).toBe("10");
|
||||
expect(circle?.getAttribute("stroke")).toBe("currentColor");
|
||||
expect(circle?.classList.contains("opacity-25")).toBe(true);
|
||||
|
||||
// Check path attributes
|
||||
expect(path?.getAttribute("fill")).toBe("currentColor");
|
||||
expect(path?.classList.contains("opacity-75")).toBe(true);
|
||||
});
|
||||
});
|
||||
40
apps/web/modules/ui/components/logo/index.test.tsx
Normal file
40
apps/web/modules/ui/components/logo/index.test.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { Logo } from ".";
|
||||
|
||||
describe("Logo", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders correctly", () => {
|
||||
const { container } = render(<Logo />);
|
||||
const svg = container.querySelector("svg");
|
||||
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(svg).toHaveAttribute("viewBox", "0 0 697 150");
|
||||
expect(svg).toHaveAttribute("fill", "none");
|
||||
expect(svg).toHaveAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||
});
|
||||
|
||||
test("accepts and passes through props", () => {
|
||||
const testClassName = "test-class";
|
||||
const { container } = render(<Logo className={testClassName} />);
|
||||
const svg = container.querySelector("svg");
|
||||
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(svg).toHaveAttribute("class", testClassName);
|
||||
});
|
||||
|
||||
test("contains expected svg elements", () => {
|
||||
const { container } = render(<Logo />);
|
||||
const svg = container.querySelector("svg");
|
||||
|
||||
expect(svg?.querySelectorAll("path").length).toBeGreaterThan(0);
|
||||
expect(svg?.querySelector("line")).toBeInTheDocument();
|
||||
expect(svg?.querySelectorAll("mask").length).toBe(2);
|
||||
expect(svg?.querySelectorAll("filter").length).toBe(3);
|
||||
expect(svg?.querySelectorAll("linearGradient").length).toBe(6);
|
||||
});
|
||||
});
|
||||
211
apps/web/modules/ui/components/media-background/index.test.tsx
Normal file
211
apps/web/modules/ui/components/media-background/index.test.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { SurveyType } from "@prisma/client";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { MediaBackground } from ".";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("next/image", () => ({
|
||||
default: ({ src, alt, onLoadingComplete }: any) => {
|
||||
// Call onLoadingComplete to simulate image load
|
||||
if (onLoadingComplete) setTimeout(() => onLoadingComplete(), 0);
|
||||
return <img src={src} alt={alt} data-testid="next-image" />;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children }: any) => <a href={href}>{children}</a>,
|
||||
}));
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("MediaBackground", () => {
|
||||
const defaultProps = {
|
||||
styling: {
|
||||
background: {
|
||||
bgType: "color",
|
||||
bg: "#ffffff",
|
||||
brightness: 100,
|
||||
},
|
||||
} as TProjectStyling,
|
||||
surveyType: "app" as SurveyType,
|
||||
children: <div data-testid="child-content">Test Content</div>,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("renders with color background", () => {
|
||||
render(<MediaBackground {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("child-content")).toBeInTheDocument();
|
||||
const backgroundDiv = document.querySelector(".absolute.inset-0");
|
||||
expect(backgroundDiv).toHaveStyle("background-color: #ffffff");
|
||||
});
|
||||
|
||||
test("renders with image background", () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
styling: {
|
||||
background: {
|
||||
bgType: "image",
|
||||
bg: "/test-image.jpg",
|
||||
brightness: 90,
|
||||
},
|
||||
} as TProjectStyling,
|
||||
};
|
||||
|
||||
render(<MediaBackground {...props} />);
|
||||
|
||||
expect(screen.getByTestId("child-content")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("next-image")).toHaveAttribute("src", "/test-image.jpg");
|
||||
});
|
||||
|
||||
test("renders with Unsplash image background with author attribution", () => {
|
||||
const unsplashImageUrl =
|
||||
"https://unsplash.com/photos/test?authorName=John%20Doe&authorLink=https://unsplash.com/@johndoe";
|
||||
const props = {
|
||||
...defaultProps,
|
||||
styling: {
|
||||
background: {
|
||||
bgType: "image",
|
||||
bg: unsplashImageUrl,
|
||||
brightness: 100,
|
||||
},
|
||||
} as TProjectStyling,
|
||||
};
|
||||
|
||||
render(<MediaBackground {...props} />);
|
||||
|
||||
expect(screen.getByTestId("child-content")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("next-image")).toHaveAttribute("src", unsplashImageUrl);
|
||||
expect(screen.getByText("common.photo_by")).toBeInTheDocument();
|
||||
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with upload background", () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
styling: {
|
||||
background: {
|
||||
bgType: "upload",
|
||||
bg: "/uploads/test-image.jpg",
|
||||
brightness: 100,
|
||||
},
|
||||
} as TProjectStyling,
|
||||
};
|
||||
|
||||
render(<MediaBackground {...props} />);
|
||||
|
||||
expect(screen.getByTestId("child-content")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("next-image")).toHaveAttribute("src", "/uploads/test-image.jpg");
|
||||
});
|
||||
|
||||
test("renders error message when image not found", () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
styling: {
|
||||
background: {
|
||||
bgType: "image",
|
||||
bg: "",
|
||||
brightness: 100,
|
||||
},
|
||||
} as TProjectStyling,
|
||||
};
|
||||
|
||||
render(<MediaBackground {...props} />);
|
||||
|
||||
expect(screen.getByText("common.no_background_image_found")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders mobile preview", () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
isMobilePreview: true,
|
||||
};
|
||||
|
||||
render(<MediaBackground {...props} />);
|
||||
|
||||
const mobileContainer = document.querySelector(".w-\\[22rem\\]");
|
||||
expect(mobileContainer).toBeInTheDocument();
|
||||
expect(screen.getByTestId("child-content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders editor view", () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
isEditorView: true,
|
||||
};
|
||||
|
||||
render(<MediaBackground {...props} />);
|
||||
|
||||
const editorContainer = document.querySelector(".rounded-b-lg");
|
||||
expect(editorContainer).toBeInTheDocument();
|
||||
expect(screen.getByTestId("child-content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onBackgroundLoaded when background is loaded", () => {
|
||||
const onBackgroundLoaded = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onBackgroundLoaded,
|
||||
};
|
||||
|
||||
render(<MediaBackground {...props} />);
|
||||
|
||||
// For color backgrounds, it should be called immediately
|
||||
expect(onBackgroundLoaded).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("renders animation background", () => {
|
||||
// Mock HTMLMediaElement.prototype methods
|
||||
Object.defineProperty(window.HTMLMediaElement.prototype, "muted", {
|
||||
set: vi.fn(),
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const props = {
|
||||
...defaultProps,
|
||||
styling: {
|
||||
background: {
|
||||
bgType: "animation",
|
||||
bg: "/test-animation.mp4",
|
||||
brightness: 100,
|
||||
},
|
||||
} as TProjectStyling,
|
||||
};
|
||||
|
||||
render(<MediaBackground {...props} />);
|
||||
|
||||
expect(screen.getByTestId("child-content")).toBeInTheDocument();
|
||||
const videoElement = document.querySelector("video");
|
||||
expect(videoElement).toBeInTheDocument();
|
||||
expect(videoElement?.querySelector("source")).toHaveAttribute("src", "/test-animation.mp4");
|
||||
});
|
||||
|
||||
test("applies correct brightness filter", () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
styling: {
|
||||
background: {
|
||||
bgType: "color",
|
||||
bg: "#ffffff",
|
||||
brightness: 80,
|
||||
},
|
||||
} as TSurveyStyling,
|
||||
};
|
||||
|
||||
render(<MediaBackground {...props} />);
|
||||
|
||||
const backgroundDiv = document.querySelector(".absolute.inset-0");
|
||||
expect(backgroundDiv).toHaveStyle("filter: brightness(80%)");
|
||||
});
|
||||
});
|
||||
159
apps/web/modules/ui/components/modal-with-tabs/index.test.tsx
Normal file
159
apps/web/modules/ui/components/modal-with-tabs/index.test.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { Modal } from "../modal";
|
||||
import { ModalWithTabs } from "./index";
|
||||
|
||||
// Mock Modal component
|
||||
vi.mock("../modal", () => ({
|
||||
Modal: vi.fn(({ children, open, setOpen, closeOnOutsideClick, size, restrictOverflow, noPadding }) =>
|
||||
open ? (
|
||||
<div
|
||||
data-testid="modal-component"
|
||||
data-no-padding={noPadding ? "true" : "false"}
|
||||
data-size={size}
|
||||
data-restrict-overflow={restrictOverflow ? "true" : "false"}
|
||||
data-close-outside={closeOnOutsideClick ? "true" : "false"}>
|
||||
{children}
|
||||
<button data-testid="close-modal" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
) : null
|
||||
),
|
||||
}));
|
||||
|
||||
describe("ModalWithTabs", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockTabs = [
|
||||
{
|
||||
title: "Tab 1",
|
||||
children: <div data-testid="tab-1-content">Content for Tab 1</div>,
|
||||
},
|
||||
{
|
||||
title: "Tab 2",
|
||||
children: <div data-testid="tab-2-content">Content for Tab 2</div>,
|
||||
},
|
||||
{
|
||||
title: "Tab 3",
|
||||
children: <div data-testid="tab-3-content">Content for Tab 3</div>,
|
||||
},
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
open: true,
|
||||
setOpen: vi.fn(),
|
||||
tabs: mockTabs,
|
||||
label: "Test Label",
|
||||
description: "Test Description",
|
||||
};
|
||||
|
||||
test("renders modal with tabs when open", () => {
|
||||
render(<ModalWithTabs {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("modal-component")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Label")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Description")).toBeInTheDocument();
|
||||
|
||||
// Check all tab titles are displayed
|
||||
expect(screen.getByText("Tab 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Tab 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Tab 3")).toBeInTheDocument();
|
||||
|
||||
// First tab should be displayed by default
|
||||
expect(screen.getByTestId("tab-1-content")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("tab-2-content")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("tab-3-content")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("doesn't render when not open", () => {
|
||||
render(<ModalWithTabs {...defaultProps} open={false} />);
|
||||
|
||||
expect(screen.queryByTestId("modal-component")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("switches tabs when clicking on tab buttons", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ModalWithTabs {...defaultProps} />);
|
||||
|
||||
// First tab should be active by default
|
||||
expect(screen.getByTestId("tab-1-content")).toBeInTheDocument();
|
||||
|
||||
// Click on second tab
|
||||
await user.click(screen.getByText("Tab 2"));
|
||||
|
||||
// Second tab content should be displayed
|
||||
expect(screen.queryByTestId("tab-1-content")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-2-content")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("tab-3-content")).not.toBeInTheDocument();
|
||||
|
||||
// Click on third tab
|
||||
await user.click(screen.getByText("Tab 3"));
|
||||
|
||||
// Third tab content should be displayed
|
||||
expect(screen.queryByTestId("tab-1-content")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("tab-2-content")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-3-content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("resets to first tab when reopened", async () => {
|
||||
const setOpen = vi.fn();
|
||||
const { rerender } = render(<ModalWithTabs {...defaultProps} setOpen={setOpen} />);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Switch to second tab
|
||||
await user.click(screen.getByText("Tab 2"));
|
||||
expect(screen.getByTestId("tab-2-content")).toBeInTheDocument();
|
||||
|
||||
// Close the modal
|
||||
await user.click(screen.getByTestId("close-modal"));
|
||||
expect(setOpen).toHaveBeenCalledWith(false);
|
||||
|
||||
// Reopen the modal
|
||||
rerender(<ModalWithTabs {...defaultProps} setOpen={setOpen} open={false} />);
|
||||
rerender(<ModalWithTabs {...defaultProps} setOpen={setOpen} open={true} />);
|
||||
|
||||
// First tab should be active again
|
||||
expect(screen.getByTestId("tab-1-content")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("tab-2-content")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with icon when provided", () => {
|
||||
const mockIcon = <div data-testid="test-icon">Icon</div>;
|
||||
render(<ModalWithTabs {...defaultProps} icon={mockIcon} />);
|
||||
|
||||
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("passes proper props to Modal component", () => {
|
||||
render(
|
||||
<ModalWithTabs
|
||||
open={true}
|
||||
setOpen={vi.fn()}
|
||||
tabs={mockTabs}
|
||||
closeOnOutsideClick={true}
|
||||
size="md"
|
||||
restrictOverflow={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const modalElement = screen.getByTestId("modal-component");
|
||||
expect(modalElement).toHaveAttribute("data-no-padding", "true");
|
||||
expect(modalElement).toHaveAttribute("data-size", "md");
|
||||
expect(modalElement).toHaveAttribute("data-restrict-overflow", "true");
|
||||
expect(modalElement).toHaveAttribute("data-close-outside", "true");
|
||||
});
|
||||
|
||||
test("uses default values for optional props", () => {
|
||||
render(<ModalWithTabs open={true} setOpen={vi.fn()} tabs={mockTabs} />);
|
||||
|
||||
const modalElement = screen.getByTestId("modal-component");
|
||||
expect(modalElement).toHaveAttribute("data-size", "lg");
|
||||
expect(modalElement).toHaveAttribute("data-restrict-overflow", "false");
|
||||
});
|
||||
});
|
||||
192
apps/web/modules/ui/components/modal/index.test.tsx
Normal file
192
apps/web/modules/ui/components/modal/index.test.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { Modal } from ".";
|
||||
|
||||
// Mock the Dialog components from radix-ui
|
||||
vi.mock("@radix-ui/react-dialog", async () => {
|
||||
const actual = await vi.importActual<typeof import("@radix-ui/react-dialog")>("@radix-ui/react-dialog");
|
||||
return {
|
||||
...actual,
|
||||
Root: ({ children, open, onOpenChange }: any) => (
|
||||
<div data-testid="dialog-root" data-state={open ? "open" : "closed"}>
|
||||
{open && children}
|
||||
<button data-testid="mock-close-trigger" onClick={() => onOpenChange(false)}>
|
||||
Close Dialog
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
Portal: ({ children }: any) => <div data-testid="dialog-portal">{children}</div>,
|
||||
Overlay: ({ className, ...props }: any) => (
|
||||
<div data-testid="dialog-overlay" className={className} {...props} />
|
||||
),
|
||||
Content: ({ className, children, ...props }: any) => (
|
||||
<div data-testid="dialog-content" className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Close: ({ className, children }: any) => (
|
||||
<button data-testid="dialog-close" className={className}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
DialogTitle: ({ children }: any) => <div data-testid="dialog-title">{children}</div>,
|
||||
DialogDescription: () => <div data-testid="dialog-description" />,
|
||||
};
|
||||
});
|
||||
|
||||
describe("Modal", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders nothing when open is false", () => {
|
||||
render(
|
||||
<Modal open={false} setOpen={() => {}}>
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("dialog-root")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders modal content when open is true", () => {
|
||||
render(
|
||||
<Modal open={true} setOpen={() => {}}>
|
||||
<div data-testid="modal-content">Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("dialog-root")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dialog-portal")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dialog-content")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("modal-content")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with title when provided", () => {
|
||||
render(
|
||||
<Modal open={true} setOpen={() => {}} title="Test Title">
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("dialog-title")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies size classes correctly", () => {
|
||||
const { rerender } = render(
|
||||
<Modal open={true} setOpen={() => {}} size="lg">
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
let content = screen.getByTestId("dialog-content");
|
||||
expect(content.className).toContain("sm:max-w-[820px]");
|
||||
|
||||
rerender(
|
||||
<Modal open={true} setOpen={() => {}} size="xl">
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
content = screen.getByTestId("dialog-content");
|
||||
expect(content.className).toContain("sm:max-w-[960px]");
|
||||
expect(content.className).toContain("sm:max-h-[640px]");
|
||||
|
||||
rerender(
|
||||
<Modal open={true} setOpen={() => {}} size="xxl">
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
content = screen.getByTestId("dialog-content");
|
||||
expect(content.className).toContain("sm:max-w-[1240px]");
|
||||
expect(content.className).toContain("sm:max-h-[760px]");
|
||||
});
|
||||
|
||||
test("applies noPadding class when noPadding is true", () => {
|
||||
render(
|
||||
<Modal open={true} setOpen={() => {}} noPadding>
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const content = screen.getByTestId("dialog-content");
|
||||
expect(content.className).not.toContain("px-4 pt-5 pb-4 sm:p-6");
|
||||
});
|
||||
|
||||
test("applies the blur class to overlay when blur is true", () => {
|
||||
render(
|
||||
<Modal open={true} setOpen={() => {}} blur={true}>
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const overlay = screen.getByTestId("dialog-overlay");
|
||||
expect(overlay.className).toContain("backdrop-blur-md");
|
||||
});
|
||||
|
||||
test("does not apply the blur class to overlay when blur is false", () => {
|
||||
render(
|
||||
<Modal open={true} setOpen={() => {}} blur={false}>
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const overlay = screen.getByTestId("dialog-overlay");
|
||||
expect(overlay.className).not.toContain("backdrop-blur-md");
|
||||
});
|
||||
|
||||
test("hides close button when hideCloseButton is true", () => {
|
||||
render(
|
||||
<Modal open={true} setOpen={() => {}} hideCloseButton={true}>
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const closeButton = screen.getByTestId("dialog-close");
|
||||
expect(closeButton.className).toContain("!hidden");
|
||||
});
|
||||
|
||||
test("calls setOpen when dialog is closed", async () => {
|
||||
const setOpen = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<Modal open={true} setOpen={setOpen}>
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId("mock-close-trigger"));
|
||||
expect(setOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("applies restrictOverflow class when restrictOverflow is true", () => {
|
||||
render(
|
||||
<Modal open={true} setOpen={() => {}} restrictOverflow={true}>
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const content = screen.getByTestId("dialog-content");
|
||||
expect(content.className).not.toContain("overflow-hidden");
|
||||
});
|
||||
|
||||
test("applies custom className when provided", () => {
|
||||
const customClass = "test-custom-class";
|
||||
|
||||
render(
|
||||
<Modal open={true} setOpen={() => {}} className={customClass}>
|
||||
<div>Test Content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const content = screen.getByTestId("dialog-content");
|
||||
expect(content.className).toContain(customClass);
|
||||
});
|
||||
});
|
||||
67
apps/web/modules/ui/components/multi-select/badge.test.tsx
Normal file
67
apps/web/modules/ui/components/multi-select/badge.test.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { Badge, badgeVariants } from "./badge";
|
||||
|
||||
describe("Badge", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with default variant", () => {
|
||||
const { container } = render(<Badge>Test Badge</Badge>);
|
||||
const badgeElement = container.firstChild as HTMLElement;
|
||||
|
||||
expect(badgeElement).toBeInTheDocument();
|
||||
expect(badgeElement.textContent).toBe("Test Badge");
|
||||
expect(badgeElement.className).toContain("bg-primary");
|
||||
expect(badgeElement.className).toContain("border-transparent");
|
||||
expect(badgeElement.className).toContain("text-primary-foreground");
|
||||
});
|
||||
|
||||
test("renders with secondary variant", () => {
|
||||
const { container } = render(<Badge variant="secondary">Secondary Badge</Badge>);
|
||||
const badgeElement = container.firstChild as HTMLElement;
|
||||
|
||||
expect(badgeElement).toBeInTheDocument();
|
||||
expect(badgeElement.textContent).toBe("Secondary Badge");
|
||||
expect(badgeElement.className).toContain("bg-secondary");
|
||||
expect(badgeElement.className).toContain("text-secondary-foreground");
|
||||
});
|
||||
|
||||
test("renders with destructive variant", () => {
|
||||
const { container } = render(<Badge variant="destructive">Destructive Badge</Badge>);
|
||||
const badgeElement = container.firstChild as HTMLElement;
|
||||
|
||||
expect(badgeElement).toBeInTheDocument();
|
||||
expect(badgeElement.textContent).toBe("Destructive Badge");
|
||||
expect(badgeElement.className).toContain("bg-destructive");
|
||||
expect(badgeElement.className).toContain("text-destructive-foreground");
|
||||
});
|
||||
|
||||
test("renders with outline variant", () => {
|
||||
const { container } = render(<Badge variant="outline">Outline Badge</Badge>);
|
||||
const badgeElement = container.firstChild as HTMLElement;
|
||||
|
||||
expect(badgeElement).toBeInTheDocument();
|
||||
expect(badgeElement.textContent).toBe("Outline Badge");
|
||||
expect(badgeElement.className).toContain("text-foreground");
|
||||
});
|
||||
|
||||
test("accepts additional className", () => {
|
||||
const { container } = render(<Badge className="custom-class">Custom Badge</Badge>);
|
||||
const badgeElement = container.firstChild as HTMLElement;
|
||||
|
||||
expect(badgeElement).toBeInTheDocument();
|
||||
expect(badgeElement.className).toContain("custom-class");
|
||||
expect(badgeElement.className).toContain("bg-primary"); // Default variant still applies
|
||||
});
|
||||
|
||||
test("passes additional props", () => {
|
||||
const { container } = render(<Badge data-testid="test-badge">Props Test</Badge>);
|
||||
const badgeElement = container.firstChild as HTMLElement;
|
||||
|
||||
expect(badgeElement).toBeInTheDocument();
|
||||
expect(badgeElement).toHaveAttribute("data-testid", "test-badge");
|
||||
});
|
||||
});
|
||||
218
apps/web/modules/ui/components/multi-select/index.test.tsx
Normal file
218
apps/web/modules/ui/components/multi-select/index.test.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { MultiSelect } from "./index";
|
||||
|
||||
// Mock cmdk library
|
||||
vi.mock("cmdk", () => {
|
||||
const CommandInput = vi.fn(({ onValueChange, placeholder, disabled, onBlur, onFocus, value }: any) => (
|
||||
<input
|
||||
data-testid="cmdk-input"
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
onChange={(e) => onValueChange?.(e.target.value)}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
));
|
||||
|
||||
const Command = Object.assign(
|
||||
vi.fn(({ children, onKeyDown }: any) => (
|
||||
<div data-testid="cmdk-command" onKeyDown={onKeyDown}>
|
||||
{children}
|
||||
</div>
|
||||
)),
|
||||
{ Input: CommandInput }
|
||||
);
|
||||
|
||||
return { Command };
|
||||
});
|
||||
|
||||
// Mock the Badge component
|
||||
vi.mock("@/modules/ui/components/multi-select/badge", () => ({
|
||||
Badge: ({ children, className }: any) => (
|
||||
<div data-testid="badge" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Command components
|
||||
vi.mock("@/modules/ui/components/command", () => ({
|
||||
Command: ({ children, className, onKeyDown }: any) => (
|
||||
<div data-testid="command" className={className} onKeyDown={onKeyDown}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CommandGroup: ({ children, className }: any) => (
|
||||
<div data-testid="command-group" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CommandItem: ({ children, className, onSelect, onMouseDown }: any) => (
|
||||
<div
|
||||
data-testid="command-item"
|
||||
className={className}
|
||||
onClick={() => onSelect?.()}
|
||||
onMouseDown={onMouseDown}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CommandList: ({ children }: any) => <div data-testid="command-list">{children}</div>,
|
||||
}));
|
||||
|
||||
describe("MultiSelect", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const options = [
|
||||
{ value: "apple", label: "Apple" },
|
||||
{ value: "banana", label: "Banana" },
|
||||
{ value: "orange", label: "Orange" },
|
||||
];
|
||||
|
||||
test("renders with default props", () => {
|
||||
render(<MultiSelect options={options} />);
|
||||
|
||||
const input = screen.getByTestId("cmdk-input");
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveAttribute("placeholder", "Select options...");
|
||||
});
|
||||
|
||||
test("renders with custom placeholder", () => {
|
||||
render(<MultiSelect options={options} placeholder="Custom placeholder" />);
|
||||
|
||||
const input = screen.getByTestId("cmdk-input");
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveAttribute("placeholder", "Custom placeholder");
|
||||
});
|
||||
|
||||
test("renders with preselected values", () => {
|
||||
render(<MultiSelect options={options} value={["apple", "banana"]} />);
|
||||
|
||||
const badges = screen.getAllByTestId("badge");
|
||||
expect(badges).toHaveLength(2);
|
||||
expect(badges[0].textContent).toContain("Apple");
|
||||
expect(badges[1].textContent).toContain("Banana");
|
||||
});
|
||||
|
||||
test("renders in disabled state", () => {
|
||||
render(<MultiSelect options={options} disabled={true} />);
|
||||
|
||||
const command = screen.getByTestId("command");
|
||||
expect(command.className).toContain("opacity-50");
|
||||
expect(command.className).toContain("cursor-not-allowed");
|
||||
|
||||
const input = screen.getByTestId("cmdk-input");
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
|
||||
test("shows options list on input focus", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MultiSelect options={options} />);
|
||||
|
||||
const input = screen.getByTestId("cmdk-input");
|
||||
await user.click(input);
|
||||
|
||||
// Simulate focus event
|
||||
input.dispatchEvent(new FocusEvent("focus"));
|
||||
|
||||
// After focus, the command list should be present which contains command items
|
||||
const commandList = screen.getByTestId("command-list");
|
||||
expect(commandList).toBeInTheDocument();
|
||||
|
||||
// Test that the commandList contains at least one command item
|
||||
const commandGroup = within(commandList).getByTestId("command-group");
|
||||
expect(commandGroup).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("filters options based on input text", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { rerender } = render(<MultiSelect options={options} />);
|
||||
|
||||
const input = screen.getByTestId("cmdk-input");
|
||||
await user.click(input);
|
||||
input.dispatchEvent(new FocusEvent("focus"));
|
||||
|
||||
// Mock the filtered state by rerendering with a specific input value
|
||||
// This simulates what happens when a user types "app"
|
||||
rerender(<MultiSelect options={options} />);
|
||||
|
||||
// Manually trigger the display of filtered options
|
||||
const commandList = screen.getByTestId("command-list");
|
||||
const commandGroup = within(commandList).getByTestId("command-group");
|
||||
const appleOption = within(commandGroup).getByText("Apple");
|
||||
|
||||
expect(appleOption).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("selects an option on click", async () => {
|
||||
const onChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<MultiSelect options={options} onChange={onChange} />);
|
||||
|
||||
const input = screen.getByTestId("cmdk-input");
|
||||
await user.click(input);
|
||||
input.dispatchEvent(new FocusEvent("focus"));
|
||||
|
||||
const appleOption = screen.getAllByTestId("command-item")[0];
|
||||
await user.click(appleOption);
|
||||
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("unselects an option when X button is clicked", async () => {
|
||||
const onChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<MultiSelect options={options} value={["apple", "banana"]} onChange={onChange} />);
|
||||
|
||||
// Find all badges
|
||||
const badges = screen.getAllByTestId("badge");
|
||||
expect(badges).toHaveLength(2);
|
||||
|
||||
// Find the X buttons (they are children of the badges)
|
||||
const xButtons = screen.getAllByRole("button");
|
||||
expect(xButtons).toHaveLength(2);
|
||||
|
||||
// Click the first X button
|
||||
await user.click(xButtons[0]);
|
||||
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("doesn't show already selected options in dropdown", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<MultiSelect options={options} value={["apple"]} />);
|
||||
|
||||
const input = screen.getByTestId("cmdk-input");
|
||||
await user.click(input);
|
||||
input.dispatchEvent(new FocusEvent("focus"));
|
||||
|
||||
// Should only show non-selected options
|
||||
const optionItems = screen.getAllByTestId("command-item");
|
||||
expect(optionItems).toHaveLength(2);
|
||||
expect(optionItems[0].textContent).toBe("Banana");
|
||||
expect(optionItems[1].textContent).toBe("Orange");
|
||||
});
|
||||
|
||||
test("updates when value prop changes", () => {
|
||||
const { rerender } = render(<MultiSelect options={options} value={["apple"]} />);
|
||||
|
||||
let badges = screen.getAllByTestId("badge");
|
||||
expect(badges).toHaveLength(1);
|
||||
expect(badges[0].textContent).toContain("Apple");
|
||||
|
||||
rerender(<MultiSelect options={options} value={["apple", "banana"]} />);
|
||||
|
||||
badges = screen.getAllByTestId("badge");
|
||||
expect(badges).toHaveLength(2);
|
||||
expect(badges[0].textContent).toContain("Apple");
|
||||
expect(badges[1].textContent).toContain("Banana");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { CssSelector } from "./css-selector";
|
||||
|
||||
// Mock the AdvancedOptionToggle component
|
||||
vi.mock("@/modules/ui/components/advanced-option-toggle", () => ({
|
||||
AdvancedOptionToggle: ({ children, isChecked, onToggle, title, disabled, htmlId }: any) => {
|
||||
// Store a reference to onToggle so we can actually toggle state when the button is clicked
|
||||
const handleToggle = () => onToggle(!isChecked);
|
||||
|
||||
return (
|
||||
<div data-testid="advanced-option-toggle" data-checked={isChecked} data-disabled={disabled}>
|
||||
<div data-testid="toggle-title">{title}</div>
|
||||
<button data-testid={`toggle-button-${htmlId}`} onClick={handleToggle}>
|
||||
Toggle
|
||||
</button>
|
||||
{isChecked && <div data-testid="toggle-content">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the Input component
|
||||
vi.mock("@/modules/ui/components/input", () => ({
|
||||
Input: ({ disabled, placeholder, onChange, value, isInvalid }: any) => (
|
||||
<input
|
||||
data-testid="css-input"
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange && onChange(e)}
|
||||
data-invalid={isInvalid}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the form components
|
||||
vi.mock("@/modules/ui/components/form", () => ({
|
||||
FormControl: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
FormField: ({ render, name }: any) =>
|
||||
render({
|
||||
field: {
|
||||
value: undefined,
|
||||
onChange: vi.fn(),
|
||||
name,
|
||||
},
|
||||
fieldState: { error: null },
|
||||
}),
|
||||
FormItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
// Mock the tolgee translation
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Helper component for the form
|
||||
const TestWrapper = ({ cssSelector, disabled = false }: { cssSelector?: string; disabled?: boolean }) => {
|
||||
const form = useForm<TActionClassInput>({
|
||||
defaultValues: {
|
||||
name: "Test Action",
|
||||
description: "Test Description",
|
||||
noCodeConfig: {
|
||||
type: "click",
|
||||
elementSelector: {
|
||||
cssSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Override the watch function to simulate the state change
|
||||
form.watch = vi.fn().mockImplementation((name) => {
|
||||
if (name === "noCodeConfig.elementSelector.cssSelector") {
|
||||
return cssSelector;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
return <CssSelector form={form} disabled={disabled} />;
|
||||
};
|
||||
|
||||
describe("CssSelector", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with cssSelector undefined", () => {
|
||||
render(<TestWrapper cssSelector={undefined} />);
|
||||
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("toggle-title")).toHaveTextContent("environments.actions.css_selector");
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveAttribute("data-checked", "false");
|
||||
expect(screen.queryByTestId("toggle-content")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with cssSelector defined", () => {
|
||||
render(<TestWrapper cssSelector=".button" />);
|
||||
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveAttribute("data-checked", "true");
|
||||
expect(screen.getByTestId("toggle-content")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("css-input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("disables the component when disabled prop is true", () => {
|
||||
render(<TestWrapper disabled={true} />);
|
||||
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveAttribute("data-disabled", "true");
|
||||
});
|
||||
|
||||
test("toggle opens and closes the input field", async () => {
|
||||
const user = userEvent.setup();
|
||||
// Start with cssSelector undefined to have the toggle closed initially
|
||||
const { rerender } = render(<TestWrapper cssSelector={undefined} />);
|
||||
|
||||
const toggleButton = screen.getByTestId("toggle-button-CssSelector");
|
||||
|
||||
// Initially closed
|
||||
expect(screen.queryByTestId("toggle-content")).not.toBeInTheDocument();
|
||||
|
||||
// Open it - simulate change through rerender
|
||||
await user.click(toggleButton);
|
||||
rerender(<TestWrapper cssSelector="" />);
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveAttribute("data-checked", "true");
|
||||
|
||||
// Close it again
|
||||
await user.click(toggleButton);
|
||||
rerender(<TestWrapper cssSelector={undefined} />);
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveAttribute("data-checked", "false");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { InnerHtmlSelector } from "./inner-html-selector";
|
||||
|
||||
// Mock the AdvancedOptionToggle component
|
||||
vi.mock("@/modules/ui/components/advanced-option-toggle", () => ({
|
||||
AdvancedOptionToggle: ({ children, isChecked, onToggle, title, disabled, htmlId }: any) => {
|
||||
// Store a reference to onToggle so we can actually toggle state when the button is clicked
|
||||
const handleToggle = () => onToggle(!isChecked);
|
||||
|
||||
return (
|
||||
<div data-testid="advanced-option-toggle" data-checked={isChecked} data-disabled={disabled}>
|
||||
<div data-testid="toggle-title">{title}</div>
|
||||
<button data-testid={`toggle-button-${htmlId}`} onClick={handleToggle}>
|
||||
Toggle
|
||||
</button>
|
||||
{isChecked && <div data-testid="toggle-content">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the Input component
|
||||
vi.mock("@/modules/ui/components/input", () => ({
|
||||
Input: ({ disabled, placeholder, onChange, value, isInvalid }: any) => (
|
||||
<input
|
||||
data-testid="innerhtml-input"
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange && onChange(e)}
|
||||
data-invalid={isInvalid}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the form components
|
||||
vi.mock("@/modules/ui/components/form", () => ({
|
||||
FormControl: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
FormField: ({ render, name }: any) =>
|
||||
render({
|
||||
field: {
|
||||
value: undefined,
|
||||
onChange: vi.fn(),
|
||||
name,
|
||||
},
|
||||
fieldState: { error: null },
|
||||
}),
|
||||
FormItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
// Mock the tolgee translation
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Helper component for the form
|
||||
const TestWrapper = ({ innerHtml, disabled = false }: { innerHtml?: string; disabled?: boolean }) => {
|
||||
const form = useForm<TActionClassInput>({
|
||||
defaultValues: {
|
||||
name: "Test Action",
|
||||
description: "Test Description",
|
||||
noCodeConfig: {
|
||||
type: "click",
|
||||
elementSelector: {
|
||||
innerHtml,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Override the watch function to simulate the state change
|
||||
form.watch = vi.fn().mockImplementation((name) => {
|
||||
if (name === "noCodeConfig.elementSelector.innerHtml") {
|
||||
return innerHtml;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
return <InnerHtmlSelector form={form} disabled={disabled} />;
|
||||
};
|
||||
|
||||
describe("InnerHtmlSelector", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with innerHtml undefined", () => {
|
||||
render(<TestWrapper innerHtml={undefined} />);
|
||||
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("toggle-title")).toHaveTextContent("environments.actions.inner_text");
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveAttribute("data-checked", "false");
|
||||
expect(screen.queryByTestId("toggle-content")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with innerHtml defined", () => {
|
||||
render(<TestWrapper innerHtml="Get Started" />);
|
||||
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveAttribute("data-checked", "true");
|
||||
expect(screen.getByTestId("toggle-content")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("innerhtml-input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("disables the component when disabled prop is true", () => {
|
||||
render(<TestWrapper disabled={true} />);
|
||||
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveAttribute("data-disabled", "true");
|
||||
});
|
||||
|
||||
test("toggle opens and closes the input field", async () => {
|
||||
const user = userEvent.setup();
|
||||
// Start with innerHtml undefined to have the toggle closed initially
|
||||
const { rerender } = render(<TestWrapper innerHtml={undefined} />);
|
||||
|
||||
const toggleButton = screen.getByTestId("toggle-button-InnerText");
|
||||
|
||||
// Initially closed
|
||||
expect(screen.queryByTestId("toggle-content")).not.toBeInTheDocument();
|
||||
|
||||
// Open it - simulate change through rerender
|
||||
await user.click(toggleButton);
|
||||
rerender(<TestWrapper innerHtml="" />);
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveAttribute("data-checked", "true");
|
||||
|
||||
// Close it again
|
||||
await user.click(toggleButton);
|
||||
rerender(<TestWrapper innerHtml={undefined} />);
|
||||
expect(screen.getByTestId("advanced-option-toggle")).toHaveAttribute("data-checked", "false");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { PageUrlSelector } from "./page-url-selector";
|
||||
|
||||
// Mock testURLmatch function
|
||||
vi.mock("@/lib/utils/url", () => ({
|
||||
testURLmatch: vi.fn((testUrl, value, rule) => {
|
||||
// Simple mock implementation
|
||||
if (rule === "exactMatch" && testUrl === value) return "yes";
|
||||
if (rule === "contains" && testUrl.includes(value)) return "yes";
|
||||
if (rule === "startsWith" && testUrl.startsWith(value)) return "yes";
|
||||
if (rule === "endsWith" && testUrl.endsWith(value)) return "yes";
|
||||
if (rule === "notMatch" && testUrl !== value) return "yes";
|
||||
if (rule === "notContains" && !testUrl.includes(value)) return "yes";
|
||||
return "no";
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock toast
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the TabToggle component
|
||||
vi.mock("@/modules/ui/components/tab-toggle", () => ({
|
||||
TabToggle: ({ options, onChange, defaultSelected, id, disabled }: any) => (
|
||||
<div data-testid={`tab-toggle-${id}`} data-disabled={disabled}>
|
||||
{options.map((option: any) => (
|
||||
<button
|
||||
key={option.value}
|
||||
data-testid={`tab-option-${option.value}`}
|
||||
onClick={() => onChange(option.value)}
|
||||
data-selected={option.value === defaultSelected}>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Input component
|
||||
vi.mock("@/modules/ui/components/input", () => ({
|
||||
Input: ({
|
||||
className,
|
||||
type,
|
||||
disabled,
|
||||
placeholder,
|
||||
onChange,
|
||||
value,
|
||||
isInvalid,
|
||||
name,
|
||||
autoComplete,
|
||||
...rest
|
||||
}: any) => (
|
||||
<input
|
||||
data-testid={name ? `input-${name}` : "input"}
|
||||
type={type}
|
||||
className={className}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange && onChange(e)}
|
||||
data-invalid={isInvalid}
|
||||
autoComplete={autoComplete}
|
||||
{...rest}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Button component - Fixed to use correct data-testid values
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, variant, size, onClick, disabled, className, type }: any) => (
|
||||
<button
|
||||
data-testid={
|
||||
typeof children === "string"
|
||||
? `button-${children.toLowerCase().replace(/\s+/g, "-")}`
|
||||
: "button-add-url"
|
||||
}
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
type={type}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Select component
|
||||
vi.mock("@/modules/ui/components/select", () => ({
|
||||
Select: ({ children, onValueChange, value, name, disabled }: any) => (
|
||||
<div data-testid={`select-${name}`} data-value={value} data-disabled={disabled}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectContent: ({ children }: any) => <div data-testid="select-content">{children}</div>,
|
||||
SelectItem: ({ children, value }: any) => (
|
||||
<div data-testid={`select-item-${value}`} data-value={value}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectTrigger: ({ children, className }: any) => (
|
||||
<div data-testid="select-trigger" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectValue: ({ placeholder }: any) => <div data-testid="select-value">{placeholder}</div>,
|
||||
}));
|
||||
|
||||
// Mock the Label component
|
||||
vi.mock("@/modules/ui/components/label", () => ({
|
||||
Label: ({ children, className }: any) => (
|
||||
<label data-testid="form-label" className={className}>
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock icons
|
||||
vi.mock("lucide-react", () => ({
|
||||
PlusIcon: () => <div data-testid="plus-icon" />,
|
||||
TrashIcon: () => <div data-testid="trash-icon" />,
|
||||
}));
|
||||
|
||||
// Mock the form components
|
||||
vi.mock("@/modules/ui/components/form", () => ({
|
||||
FormControl: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
FormField: ({ render, control, name }: any) =>
|
||||
render({
|
||||
field: {
|
||||
onChange: vi.fn(),
|
||||
value: (() => {
|
||||
if (name === "noCodeConfig.urlFilters") {
|
||||
return control?._formValues?.noCodeConfig?.urlFilters || [];
|
||||
}
|
||||
if (name?.startsWith("noCodeConfig.urlFilters.")) {
|
||||
const parts = name.split(".");
|
||||
const index = parseInt(parts[2]);
|
||||
const property = parts[3];
|
||||
return control?._formValues?.noCodeConfig?.urlFilters?.[index]?.[property] || "";
|
||||
}
|
||||
return "";
|
||||
})(),
|
||||
name,
|
||||
},
|
||||
fieldState: { error: null },
|
||||
}),
|
||||
FormItem: ({ children, className }: any) => <div className={className}>{children}</div>,
|
||||
}));
|
||||
|
||||
// Mock the tolgee translation
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Helper component for the form
|
||||
const TestWrapper = ({
|
||||
urlFilters = [] as {
|
||||
rule: "startsWith" | "exactMatch" | "contains" | "endsWith" | "notMatch" | "notContains";
|
||||
value: string;
|
||||
}[],
|
||||
isReadOnly = false,
|
||||
}) => {
|
||||
const form = useForm<TActionClassInput>({
|
||||
defaultValues: {
|
||||
name: "Test Action",
|
||||
description: "Test Description",
|
||||
noCodeConfig: {
|
||||
type: "click",
|
||||
urlFilters,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return <PageUrlSelector form={form} isReadOnly={isReadOnly} />;
|
||||
};
|
||||
|
||||
describe("PageUrlSelector", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with default values and 'all' filter type", () => {
|
||||
render(<TestWrapper />);
|
||||
|
||||
expect(screen.getByTestId("form-label")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.actions.page_filter")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-toggle-filter")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-option-all")).toHaveAttribute("data-selected", "true");
|
||||
expect(screen.queryByTestId("button-add-url")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with 'specific' filter type", () => {
|
||||
render(<TestWrapper urlFilters={[{ rule: "exactMatch", value: "https://example.com" }]} />);
|
||||
|
||||
expect(screen.getByTestId("tab-option-specific")).toHaveAttribute("data-selected", "true");
|
||||
expect(screen.getByTestId("select-noCodeConfig.urlFilters.0.rule")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("button-add-url")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("input-noCodeConfig.urlFilters.testUrl")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("disables components when isReadOnly is true", () => {
|
||||
render(
|
||||
<TestWrapper urlFilters={[{ rule: "exactMatch", value: "https://example.com" }]} isReadOnly={true} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("tab-toggle-filter")).toHaveAttribute("data-disabled", "true");
|
||||
expect(screen.getByTestId("button-add-url")).toHaveAttribute("disabled", "");
|
||||
});
|
||||
|
||||
test("shows multiple URL filters", () => {
|
||||
const urlFilters = [
|
||||
{ rule: "exactMatch" as const, value: "https://example.com" },
|
||||
{ rule: "contains" as const, value: "pricing" },
|
||||
];
|
||||
|
||||
render(<TestWrapper urlFilters={urlFilters} />);
|
||||
|
||||
expect(screen.getByTestId("select-noCodeConfig.urlFilters.0.rule")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("select-noCodeConfig.urlFilters.1.rule")).toBeInTheDocument();
|
||||
// Check that we have a "trash" button for each rule (since there are multiple)
|
||||
const trashIcons = screen.getAllByTestId("trash-icon");
|
||||
expect(trashIcons.length).toBe(2);
|
||||
});
|
||||
|
||||
test("test URL match functionality", async () => {
|
||||
const testUrl = "https://example.com/pricing";
|
||||
const urlFilters = [{ rule: "contains" as const, value: "pricing" }];
|
||||
|
||||
render(<TestWrapper urlFilters={urlFilters} />);
|
||||
|
||||
const testInput = screen.getByTestId("input-noCodeConfig.urlFilters.testUrl");
|
||||
// Updated testId to match the actual button's testId from our mock
|
||||
const testButton = screen.getByTestId("button-environments.actions.test_match");
|
||||
|
||||
await userEvent.type(testInput, testUrl);
|
||||
await userEvent.click(testButton);
|
||||
|
||||
// Toast should be called to show match result
|
||||
const toast = await import("react-hot-toast");
|
||||
expect(toast.default.success).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -99,7 +99,7 @@ export const PageUrlSelector = ({ form, isReadOnly }: PageUrlSelectorProps) => {
|
||||
/>
|
||||
</div>
|
||||
{filterType === "specific" && (
|
||||
<div className="mt-4 mb-2 w-full space-y-3 pe-2">
|
||||
<div className="mb-2 mt-4 w-full space-y-3 pe-2">
|
||||
<Label>{t("environments.actions.url")}</Label>
|
||||
<UrlInput
|
||||
control={form.control}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { NoCodeActionForm } from ".";
|
||||
|
||||
// Mock the Alert component
|
||||
vi.mock("@/modules/ui/components/alert", () => ({
|
||||
Alert: ({ children }: { children: React.ReactNode }) => <div data-testid="alert">{children}</div>,
|
||||
AlertTitle: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="alert-title">{children}</div>
|
||||
),
|
||||
AlertDescription: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="alert-description">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the form components
|
||||
vi.mock("@/modules/ui/components/form", () => ({
|
||||
FormControl: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
FormField: ({ render, control }: any) =>
|
||||
render({
|
||||
field: {
|
||||
value: control?._formValues?.noCodeConfig?.type || "",
|
||||
onChange: vi.fn(),
|
||||
},
|
||||
}),
|
||||
FormItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
FormError: () => null,
|
||||
}));
|
||||
|
||||
// Mock the TabToggle component
|
||||
vi.mock("@/modules/ui/components/tab-toggle", () => ({
|
||||
TabToggle: ({ options, onChange, defaultSelected, id, disabled }: any) => (
|
||||
<div data-testid={`tab-toggle-${id}`}>
|
||||
{options.map((option: any) => (
|
||||
<button
|
||||
key={option.value}
|
||||
data-testid={`tab-option-${option.value}`}
|
||||
onClick={() => onChange(option.value)}
|
||||
data-selected={option.value === defaultSelected ? "true" : "false"}>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Label component
|
||||
vi.mock("@/modules/ui/components/label", () => ({
|
||||
Label: ({ children, className }: any) => (
|
||||
<label data-testid="form-label" className={className}>
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
vi.mock("./components/css-selector", () => ({
|
||||
CssSelector: ({ form, disabled }: any) => (
|
||||
<div data-testid="css-selector" data-disabled={disabled}>
|
||||
CSS Selector
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./components/inner-html-selector", () => ({
|
||||
InnerHtmlSelector: ({ form, disabled }: any) => (
|
||||
<div data-testid="inner-html-selector" data-disabled={disabled}>
|
||||
Inner HTML Selector
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./components/page-url-selector", () => ({
|
||||
PageUrlSelector: ({ form, isReadOnly }: any) => (
|
||||
<div data-testid="page-url-selector" data-readonly={isReadOnly}>
|
||||
Page URL Selector
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the tolgee translation
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Helper component for the form
|
||||
const TestWrapper = ({
|
||||
noCodeConfig = { type: "click" },
|
||||
isReadOnly = false,
|
||||
}: {
|
||||
noCodeConfig?: { type: "click" | "pageView" | "exitIntent" | "fiftyPercentScroll" };
|
||||
isReadOnly?: boolean;
|
||||
}) => {
|
||||
const form = useForm<TActionClassInput>({
|
||||
defaultValues: {
|
||||
name: "Test Action",
|
||||
description: "Test Description",
|
||||
noCodeConfig,
|
||||
},
|
||||
});
|
||||
|
||||
return <NoCodeActionForm form={form} isReadOnly={isReadOnly} />;
|
||||
};
|
||||
|
||||
describe("NoCodeActionForm", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the form with click type", () => {
|
||||
render(<TestWrapper noCodeConfig={{ type: "click" }} />);
|
||||
|
||||
expect(screen.getByTestId("tab-toggle-userAction")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-option-click")).toHaveAttribute("data-selected", "true");
|
||||
expect(screen.getByTestId("css-selector")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("inner-html-selector")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("page-url-selector")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders the form with pageView type", () => {
|
||||
render(<TestWrapper noCodeConfig={{ type: "pageView" }} />);
|
||||
|
||||
expect(screen.getByTestId("tab-option-pageView")).toHaveAttribute("data-selected", "true");
|
||||
expect(screen.getByTestId("alert")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("alert-title")).toHaveTextContent("environments.actions.page_view");
|
||||
});
|
||||
|
||||
test("renders the form with exitIntent type", () => {
|
||||
render(<TestWrapper noCodeConfig={{ type: "exitIntent" }} />);
|
||||
|
||||
expect(screen.getByTestId("tab-option-exitIntent")).toHaveAttribute("data-selected", "true");
|
||||
expect(screen.getByTestId("alert")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("alert-title")).toHaveTextContent("environments.actions.exit_intent");
|
||||
});
|
||||
|
||||
test("renders the form with fiftyPercentScroll type", () => {
|
||||
render(<TestWrapper noCodeConfig={{ type: "fiftyPercentScroll" }} />);
|
||||
|
||||
expect(screen.getByTestId("tab-option-fiftyPercentScroll")).toHaveAttribute("data-selected", "true");
|
||||
expect(screen.getByTestId("alert")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("alert-title")).toHaveTextContent("environments.actions.fifty_percent_scroll");
|
||||
});
|
||||
|
||||
test("passes isReadOnly to child components", () => {
|
||||
render(<TestWrapper isReadOnly={true} />);
|
||||
|
||||
expect(screen.getByTestId("tab-toggle-userAction")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("css-selector")).toHaveAttribute("data-disabled", "true");
|
||||
expect(screen.getByTestId("inner-html-selector")).toHaveAttribute("data-disabled", "true");
|
||||
expect(screen.getByTestId("page-url-selector")).toHaveAttribute("data-readonly", "true");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { NoMobileOverlay } from "./index";
|
||||
|
||||
// Mock the tolgee translation
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) =>
|
||||
key === "common.mobile_overlay_text" ? "Please use desktop to access this section" : key,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("NoMobileOverlay", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders overlay with correct text", () => {
|
||||
render(<NoMobileOverlay />);
|
||||
|
||||
expect(screen.getByText("Please use desktop to access this section")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("has proper z-index for overlay", () => {
|
||||
render(<NoMobileOverlay />);
|
||||
|
||||
const overlay = screen.getByText("Please use desktop to access this section").closest("div.fixed");
|
||||
expect(overlay).toHaveClass("z-[9999]");
|
||||
});
|
||||
|
||||
test("has responsive layout with sm:hidden class", () => {
|
||||
render(<NoMobileOverlay />);
|
||||
|
||||
const overlay = screen.getByText("Please use desktop to access this section").closest("div.fixed");
|
||||
expect(overlay).toHaveClass("sm:hidden");
|
||||
});
|
||||
});
|
||||
81
apps/web/modules/ui/components/option-card/index.test.tsx
Normal file
81
apps/web/modules/ui/components/option-card/index.test.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { OptionCard } from "./index";
|
||||
|
||||
vi.mock("@/modules/ui/components/loading-spinner", () => ({
|
||||
LoadingSpinner: () => <div data-testid="loading-spinner">Loading Spinner</div>,
|
||||
}));
|
||||
|
||||
describe("OptionCard", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with small size correctly", () => {
|
||||
render(<OptionCard size="sm" title="Test Title" description="Test Description" />);
|
||||
|
||||
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Description")).toBeInTheDocument();
|
||||
|
||||
const card = screen.getByRole("button");
|
||||
expect(card).toHaveClass("p-4", "rounded-lg", "w-60", "shadow-md");
|
||||
});
|
||||
|
||||
test("renders with medium size correctly", () => {
|
||||
render(<OptionCard size="md" title="Test Title" description="Test Description" />);
|
||||
|
||||
const card = screen.getByRole("button");
|
||||
expect(card).toHaveClass("p-6", "rounded-xl", "w-80", "shadow-lg");
|
||||
});
|
||||
|
||||
test("renders with large size correctly", () => {
|
||||
render(<OptionCard size="lg" title="Test Title" description="Test Description" />);
|
||||
|
||||
const card = screen.getByRole("button");
|
||||
expect(card).toHaveClass("p-8", "rounded-2xl", "w-100", "shadow-xl");
|
||||
});
|
||||
|
||||
test("displays loading spinner when loading is true", () => {
|
||||
render(<OptionCard size="md" title="Test Title" description="Test Description" loading={true} />);
|
||||
|
||||
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not display loading spinner when loading is false", () => {
|
||||
render(<OptionCard size="md" title="Test Title" description="Test Description" loading={false} />);
|
||||
|
||||
expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onSelect when clicked", async () => {
|
||||
const handleSelect = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OptionCard size="md" title="Test Title" description="Test Description" onSelect={handleSelect} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button"));
|
||||
expect(handleSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("renders with custom cssId", () => {
|
||||
render(<OptionCard size="md" title="Test Title" description="Test Description" cssId="custom-id" />);
|
||||
|
||||
const card = screen.getByRole("button");
|
||||
expect(card).toHaveAttribute("id", "custom-id");
|
||||
});
|
||||
|
||||
test("renders children correctly", () => {
|
||||
render(
|
||||
<OptionCard size="md" title="Test Title" description="Test Description">
|
||||
<div data-testid="child-element">Child content</div>
|
||||
</OptionCard>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("child-element")).toBeInTheDocument();
|
||||
expect(screen.getByText("Child content")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
92
apps/web/modules/ui/components/options-switch/index.test.tsx
Normal file
92
apps/web/modules/ui/components/options-switch/index.test.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { OptionsSwitch } from "./index";
|
||||
|
||||
describe("OptionsSwitch", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockOptions = [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3", disabled: true },
|
||||
];
|
||||
|
||||
test("renders all options correctly", () => {
|
||||
render(<OptionsSwitch options={mockOptions} currentOption="option1" handleOptionChange={() => {}} />);
|
||||
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("highlights the current option", () => {
|
||||
render(<OptionsSwitch options={mockOptions} currentOption="option1" handleOptionChange={() => {}} />);
|
||||
|
||||
// Check that the highlight div exists
|
||||
const highlight = document.querySelector(".absolute.bottom-1.top-1.rounded-md.bg-slate-100");
|
||||
expect(highlight).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls handleOptionChange with option value when clicked", async () => {
|
||||
const handleOptionChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OptionsSwitch options={mockOptions} currentOption="option1" handleOptionChange={handleOptionChange} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Option 2"));
|
||||
|
||||
expect(handleOptionChange).toHaveBeenCalledWith("option2");
|
||||
});
|
||||
|
||||
test("does not call handleOptionChange when disabled option is clicked", async () => {
|
||||
const handleOptionChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OptionsSwitch options={mockOptions} currentOption="option1" handleOptionChange={handleOptionChange} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Option 3"));
|
||||
|
||||
expect(handleOptionChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders icons when provided", () => {
|
||||
const optionsWithIcons = [
|
||||
{
|
||||
value: "option1",
|
||||
label: "Option 1",
|
||||
icon: <svg data-testid="icon-option1" />,
|
||||
},
|
||||
{
|
||||
value: "option2",
|
||||
label: "Option 2",
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<OptionsSwitch options={optionsWithIcons} currentOption="option1" handleOptionChange={() => {}} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("icon-option1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("updates highlight position when current option changes", () => {
|
||||
const { rerender } = render(
|
||||
<OptionsSwitch options={mockOptions} currentOption="option1" handleOptionChange={() => {}} />
|
||||
);
|
||||
|
||||
// Re-render with different current option
|
||||
rerender(<OptionsSwitch options={mockOptions} currentOption="option2" handleOptionChange={() => {}} />);
|
||||
|
||||
// The highlight style should be updated through useEffect
|
||||
// We can verify the component doesn't crash on re-render
|
||||
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
119
apps/web/modules/ui/components/otp-input/index.test.tsx
Normal file
119
apps/web/modules/ui/components/otp-input/index.test.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { OTPInput } from "./index";
|
||||
|
||||
describe("OTPInput", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders correct number of input fields", () => {
|
||||
const onChange = vi.fn();
|
||||
render(<OTPInput value="" valueLength={6} onChange={onChange} />);
|
||||
|
||||
const inputs = screen.getAllByRole("textbox");
|
||||
expect(inputs).toHaveLength(6);
|
||||
});
|
||||
|
||||
test("displays provided value correctly", () => {
|
||||
const onChange = vi.fn();
|
||||
render(<OTPInput value="123456" valueLength={6} onChange={onChange} />);
|
||||
|
||||
const inputs = screen.getAllByRole("textbox");
|
||||
expect(inputs[0]).toHaveValue("1");
|
||||
expect(inputs[1]).toHaveValue("2");
|
||||
expect(inputs[2]).toHaveValue("3");
|
||||
expect(inputs[3]).toHaveValue("4");
|
||||
expect(inputs[4]).toHaveValue("5");
|
||||
expect(inputs[5]).toHaveValue("6");
|
||||
});
|
||||
|
||||
test("applies custom container class", () => {
|
||||
const onChange = vi.fn();
|
||||
render(
|
||||
<OTPInput value="" valueLength={4} onChange={onChange} containerClassName="test-container-class" />
|
||||
);
|
||||
|
||||
const container = screen.getAllByRole("textbox")[0].parentElement;
|
||||
expect(container).toHaveClass("test-container-class");
|
||||
});
|
||||
|
||||
test("applies custom input box class", () => {
|
||||
const onChange = vi.fn();
|
||||
render(<OTPInput value="" valueLength={4} onChange={onChange} inputBoxClassName="test-input-class" />);
|
||||
|
||||
const inputs = screen.getAllByRole("textbox");
|
||||
inputs.forEach((input) => {
|
||||
expect(input).toHaveClass("test-input-class");
|
||||
});
|
||||
});
|
||||
|
||||
test("disables inputs when disabled prop is true", () => {
|
||||
const onChange = vi.fn();
|
||||
render(<OTPInput value="" valueLength={4} onChange={onChange} disabled={true} />);
|
||||
|
||||
const inputs = screen.getAllByRole("textbox");
|
||||
inputs.forEach((input) => {
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test("calls onChange with updated value when input changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
render(<OTPInput value="123" valueLength={4} onChange={onChange} />);
|
||||
|
||||
const inputs = screen.getAllByRole("textbox");
|
||||
await user.click(inputs[3]);
|
||||
await user.keyboard("4");
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith("1234");
|
||||
});
|
||||
|
||||
test("only accepts digit inputs", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
render(<OTPInput value="" valueLength={4} onChange={onChange} />);
|
||||
|
||||
const inputs = screen.getAllByRole("textbox");
|
||||
await user.click(inputs[0]);
|
||||
await user.keyboard("a");
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("moves focus to next input after entering a digit", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
render(<OTPInput value="" valueLength={4} onChange={onChange} />);
|
||||
|
||||
const inputs = screen.getAllByRole("textbox");
|
||||
await user.click(inputs[0]);
|
||||
await user.keyboard("1");
|
||||
|
||||
expect(document.activeElement).toBe(inputs[1]);
|
||||
});
|
||||
|
||||
test("navigates inputs with arrow keys", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
render(<OTPInput value="1234" valueLength={4} onChange={onChange} />);
|
||||
|
||||
const inputs = screen.getAllByRole("textbox");
|
||||
await user.click(inputs[1]); // Focus on the 2nd input
|
||||
|
||||
await user.keyboard("{ArrowRight}");
|
||||
expect(document.activeElement).toBe(inputs[2]);
|
||||
|
||||
await user.keyboard("{ArrowLeft}");
|
||||
expect(document.activeElement).toBe(inputs[1]);
|
||||
|
||||
await user.keyboard("{ArrowDown}");
|
||||
expect(document.activeElement).toBe(inputs[2]);
|
||||
|
||||
await user.keyboard("{ArrowUp}");
|
||||
expect(document.activeElement).toBe(inputs[1]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { PageContentWrapper } from "./index";
|
||||
|
||||
describe("PageContentWrapper", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders children correctly", () => {
|
||||
const { getByText } = render(
|
||||
<PageContentWrapper>
|
||||
<div>Test Content</div>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
|
||||
expect(getByText("Test Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies default classes", () => {
|
||||
const { container } = render(
|
||||
<PageContentWrapper>
|
||||
<div>Test Content</div>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveClass("h-full");
|
||||
expect(wrapper).toHaveClass("space-y-6");
|
||||
expect(wrapper).toHaveClass("p-6");
|
||||
});
|
||||
|
||||
test("applies additional className when provided", () => {
|
||||
const { container } = render(
|
||||
<PageContentWrapper className="rounded-lg bg-gray-100">
|
||||
<div>Test Content</div>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveClass("h-full");
|
||||
expect(wrapper).toHaveClass("space-y-6");
|
||||
expect(wrapper).toHaveClass("p-6");
|
||||
expect(wrapper).toHaveClass("bg-gray-100");
|
||||
expect(wrapper).toHaveClass("rounded-lg");
|
||||
});
|
||||
});
|
||||
58
apps/web/modules/ui/components/page-header/index.test.tsx
Normal file
58
apps/web/modules/ui/components/page-header/index.test.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { PageHeader } from "./index";
|
||||
|
||||
describe("PageHeader", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders page title correctly", () => {
|
||||
render(<PageHeader pageTitle="Dashboard" />);
|
||||
expect(screen.getByText("Dashboard")).toBeInTheDocument();
|
||||
expect(screen.getByText("Dashboard")).toHaveClass("text-3xl font-bold text-slate-800 capitalize");
|
||||
});
|
||||
|
||||
test("renders with CTA", () => {
|
||||
render(<PageHeader pageTitle="Users" cta={<button data-testid="cta-button">Add User</button>} />);
|
||||
|
||||
expect(screen.getByText("Users")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("cta-button")).toBeInTheDocument();
|
||||
expect(screen.getByText("Add User")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders children correctly", () => {
|
||||
render(
|
||||
<PageHeader pageTitle="Settings">
|
||||
<div data-testid="child-element">Additional content</div>
|
||||
</PageHeader>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Settings")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("child-element")).toBeInTheDocument();
|
||||
expect(screen.getByText("Additional content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with both CTA and children", () => {
|
||||
render(
|
||||
<PageHeader pageTitle="Products" cta={<button data-testid="cta-button">New Product</button>}>
|
||||
<div data-testid="child-element">Product filters</div>
|
||||
</PageHeader>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Products")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("cta-button")).toBeInTheDocument();
|
||||
expect(screen.getByText("New Product")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("child-element")).toBeInTheDocument();
|
||||
expect(screen.getByText("Product filters")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("has border-b class", () => {
|
||||
const { container } = render(<PageHeader pageTitle="Dashboard" />);
|
||||
const headerElement = container.firstChild as HTMLElement;
|
||||
|
||||
expect(headerElement).toHaveClass("border-b");
|
||||
expect(headerElement).toHaveClass("border-slate-200");
|
||||
});
|
||||
});
|
||||
83
apps/web/modules/ui/components/password-input/index.test.tsx
Normal file
83
apps/web/modules/ui/components/password-input/index.test.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { PasswordInput } from "./index";
|
||||
|
||||
describe("PasswordInput", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders password input with type password by default", () => {
|
||||
render(<PasswordInput placeholder="Enter password" />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Enter password");
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
test("toggles password visibility when eye icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<PasswordInput placeholder="Enter password" />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Enter password");
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
|
||||
// Find and click the toggle button (eye icon)
|
||||
const toggleButton = screen.getByRole("button");
|
||||
await user.click(toggleButton);
|
||||
|
||||
// Check if input type changed to text
|
||||
expect(input).toHaveAttribute("type", "text");
|
||||
|
||||
// Click the toggle button again
|
||||
await user.click(toggleButton);
|
||||
|
||||
// Check if input type changed back to password
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
test("applies custom className to input", () => {
|
||||
render(<PasswordInput className="custom-input-class" placeholder="Enter password" />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Enter password");
|
||||
expect(input).toHaveClass("custom-input-class");
|
||||
});
|
||||
|
||||
test("applies custom containerClassName", () => {
|
||||
render(<PasswordInput containerClassName="custom-container-class" placeholder="Enter password" />);
|
||||
|
||||
const container = screen.getByPlaceholderText("Enter password").parentElement;
|
||||
expect(container).toHaveClass("custom-container-class");
|
||||
});
|
||||
|
||||
test("passes through other HTML input attributes", () => {
|
||||
render(
|
||||
<PasswordInput placeholder="Enter password" id="password-field" name="password" required disabled />
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText("Enter password");
|
||||
expect(input).toHaveAttribute("id", "password-field");
|
||||
expect(input).toHaveAttribute("name", "password");
|
||||
expect(input).toHaveAttribute("required");
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
|
||||
test("displays EyeIcon when password is hidden", () => {
|
||||
render(<PasswordInput placeholder="Enter password" />);
|
||||
|
||||
const eyeIcon = document.querySelector("svg");
|
||||
expect(eyeIcon).toBeInTheDocument();
|
||||
|
||||
// This is a simple check for the presence of the icon
|
||||
// We can't easily test the exact Lucide icon type in this setup
|
||||
});
|
||||
|
||||
test("toggle button is of type button to prevent form submission", () => {
|
||||
render(<PasswordInput placeholder="Enter password" />);
|
||||
|
||||
const toggleButton = screen.getByRole("button");
|
||||
expect(toggleButton).toHaveAttribute("type", "button");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { PendingDowngradeBanner } from "./index";
|
||||
|
||||
// Mock the useTranslate hook
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
if (key === "common.pending_downgrade") return "Pending Downgrade";
|
||||
if (key === "common.we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable")
|
||||
return "We were unable to verify your license because the license server is unreachable";
|
||||
if (key === "common.you_will_be_downgraded_to_the_community_edition_on_date")
|
||||
return `You will be downgraded to the community edition on ${params?.date}`;
|
||||
if (key === "common.you_are_downgraded_to_the_community_edition")
|
||||
return "You are downgraded to the community edition";
|
||||
if (key === "common.learn_more") return "Learn more";
|
||||
if (key === "common.close") return "Close";
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock next/link
|
||||
vi.mock("next/link", () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href} data-testid="mock-link">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("PendingDowngradeBanner", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders banner when active and isPendingDowngrade are true", () => {
|
||||
const currentDate = new Date();
|
||||
const lastChecked = new Date(currentDate.getTime() - 24 * 60 * 60 * 1000); // One day ago
|
||||
|
||||
render(
|
||||
<PendingDowngradeBanner
|
||||
lastChecked={lastChecked}
|
||||
active={true}
|
||||
isPendingDowngrade={true}
|
||||
environmentId="env-123"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Pending Downgrade")).toBeInTheDocument();
|
||||
// Check if learn more link is present
|
||||
const learnMoreLink = screen.getByText("Learn more");
|
||||
expect(learnMoreLink).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-link")).toHaveAttribute(
|
||||
"href",
|
||||
"/environments/env-123/settings/enterprise"
|
||||
);
|
||||
});
|
||||
|
||||
test("doesn't render when active is false", () => {
|
||||
const currentDate = new Date();
|
||||
const lastChecked = new Date(currentDate.getTime() - 24 * 60 * 60 * 1000); // One day ago
|
||||
|
||||
render(
|
||||
<PendingDowngradeBanner
|
||||
lastChecked={lastChecked}
|
||||
active={false}
|
||||
isPendingDowngrade={true}
|
||||
environmentId="env-123"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Pending Downgrade")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("doesn't render when isPendingDowngrade is false", () => {
|
||||
const currentDate = new Date();
|
||||
const lastChecked = new Date(currentDate.getTime() - 24 * 60 * 60 * 1000); // One day ago
|
||||
|
||||
render(
|
||||
<PendingDowngradeBanner
|
||||
lastChecked={lastChecked}
|
||||
active={true}
|
||||
isPendingDowngrade={false}
|
||||
environmentId="env-123"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Pending Downgrade")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("closes banner when close button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const currentDate = new Date();
|
||||
const lastChecked = new Date(currentDate.getTime() - 24 * 60 * 60 * 1000); // One day ago
|
||||
|
||||
render(
|
||||
<PendingDowngradeBanner
|
||||
lastChecked={lastChecked}
|
||||
active={true}
|
||||
isPendingDowngrade={true}
|
||||
environmentId="env-123"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Pending Downgrade")).toBeInTheDocument();
|
||||
|
||||
// Find and click the close button
|
||||
const closeButton = screen.getByRole("button", { name: "Close" });
|
||||
await user.click(closeButton);
|
||||
|
||||
// Banner should no longer be visible
|
||||
expect(screen.queryByText("Pending Downgrade")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { PictureSelectionResponse } from "./index";
|
||||
|
||||
// Mock next/image because it's not available in the test environment
|
||||
vi.mock("next/image", () => ({
|
||||
__esModule: true,
|
||||
default: ({ src, alt, className }: { src: string; alt: string; className: string }) => (
|
||||
<img src={src} alt={alt} className={className} />
|
||||
),
|
||||
}));
|
||||
|
||||
describe("PictureSelectionResponse", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockChoices = [
|
||||
{
|
||||
id: "choice1",
|
||||
imageUrl: "https://example.com/image1.jpg",
|
||||
},
|
||||
{
|
||||
id: "choice2",
|
||||
imageUrl: "https://example.com/image2.jpg",
|
||||
},
|
||||
{
|
||||
id: "choice3",
|
||||
imageUrl: "https://example.com/image3.jpg",
|
||||
},
|
||||
];
|
||||
|
||||
test("renders images for selected choices", () => {
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "choice3"]} />
|
||||
);
|
||||
|
||||
const images = container.querySelectorAll("img");
|
||||
expect(images).toHaveLength(2);
|
||||
expect(images[0]).toHaveAttribute("src", "https://example.com/image1.jpg");
|
||||
expect(images[1]).toHaveAttribute("src", "https://example.com/image3.jpg");
|
||||
});
|
||||
|
||||
test("renders nothing when selected is not an array", () => {
|
||||
// @ts-ignore - Testing invalid prop type
|
||||
const { container } = render(<PictureSelectionResponse choices={mockChoices} selected="choice1" />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
test("handles expanded layout", () => {
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "choice2"]} isExpanded={true} />
|
||||
);
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveClass("flex-wrap");
|
||||
});
|
||||
|
||||
test("handles non-expanded layout", () => {
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "choice2"]} isExpanded={false} />
|
||||
);
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).not.toHaveClass("flex-wrap");
|
||||
});
|
||||
|
||||
test("handles choices not in the mapping", () => {
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "nonExistentChoice"]} />
|
||||
);
|
||||
|
||||
const images = container.querySelectorAll("img");
|
||||
expect(images).toHaveLength(1); // Only one valid image should be rendered
|
||||
});
|
||||
});
|
||||
112
apps/web/modules/ui/components/popover/index.test.tsx
Normal file
112
apps/web/modules/ui/components/popover/index.test.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./index";
|
||||
|
||||
// Mock RadixUI's Portal to make testing easier
|
||||
vi.mock("@radix-ui/react-popover", async () => {
|
||||
const actual = await vi.importActual("@radix-ui/react-popover");
|
||||
return {
|
||||
...actual,
|
||||
Portal: ({ children }: { children: React.ReactNode }) => <div data-testid="portal">{children}</div>,
|
||||
};
|
||||
});
|
||||
|
||||
describe("Popover", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the popover with trigger and content", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<Popover>
|
||||
<PopoverTrigger>Open Popover</PopoverTrigger>
|
||||
<PopoverContent>Popover Content</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
// Trigger should be visible
|
||||
const trigger = screen.getByText("Open Popover");
|
||||
expect(trigger).toBeInTheDocument();
|
||||
|
||||
// Content should not be visible initially
|
||||
expect(screen.queryByText("Popover Content")).not.toBeInTheDocument();
|
||||
|
||||
// Click the trigger to open the popover
|
||||
await user.click(trigger);
|
||||
|
||||
// Content should now be visible inside the Portal
|
||||
const portal = screen.getByTestId("portal");
|
||||
expect(portal).toBeInTheDocument();
|
||||
expect(portal).toHaveTextContent("Popover Content");
|
||||
});
|
||||
|
||||
test("passes align and sideOffset props to popover content", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<Popover>
|
||||
<PopoverTrigger>Open Popover</PopoverTrigger>
|
||||
<PopoverContent align="start" sideOffset={10} data-testid="popover-content">
|
||||
Popover Content
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
// Click the trigger to open the popover
|
||||
await user.click(screen.getByText("Open Popover"));
|
||||
|
||||
// Content should have the align and sideOffset props
|
||||
const content = screen.getByTestId("portal").firstChild as HTMLElement;
|
||||
|
||||
// These attributes are handled by RadixUI internally, so we can't directly test the DOM
|
||||
// but we can verify the component doesn't crash when these props are provided
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(content).toHaveTextContent("Popover Content");
|
||||
});
|
||||
|
||||
test("forwards ref to popover content", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ref = vi.fn();
|
||||
|
||||
render(
|
||||
<Popover>
|
||||
<PopoverTrigger>Open Popover</PopoverTrigger>
|
||||
<PopoverContent ref={ref}>Popover Content</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
// Click the trigger to open the popover
|
||||
await user.click(screen.getByText("Open Popover"));
|
||||
|
||||
// Ref should have been called - this test is mostly to ensure the component supports refs
|
||||
expect(screen.getByTestId("portal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("closes when clicking outside", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<>
|
||||
<div data-testid="outside-element">Outside</div>
|
||||
<Popover>
|
||||
<PopoverTrigger>Open Popover</PopoverTrigger>
|
||||
<PopoverContent>Popover Content</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
|
||||
// Open the popover
|
||||
await user.click(screen.getByText("Open Popover"));
|
||||
expect(screen.getByTestId("portal")).toBeInTheDocument();
|
||||
|
||||
// Click outside
|
||||
await user.click(screen.getByTestId("outside-element"));
|
||||
|
||||
// This test is more about ensuring the component has the default behavior of closing on outside click
|
||||
// The actual closing is handled by RadixUI, so we can't directly test it without more complex mocking
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TabOption } from "./tab-option";
|
||||
|
||||
describe("TabOption", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with active state", () => {
|
||||
render(<TabOption active={true} icon={<span data-testid="test-icon">Icon</span>} onClick={() => {}} />);
|
||||
|
||||
const tabElement = screen.getByTestId("test-icon").parentElement;
|
||||
expect(tabElement).toBeInTheDocument();
|
||||
expect(tabElement).toHaveClass("rounded-full");
|
||||
expect(tabElement).toHaveClass("bg-slate-200");
|
||||
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with inactive state", () => {
|
||||
render(<TabOption active={false} icon={<span data-testid="test-icon">Icon</span>} onClick={() => {}} />);
|
||||
|
||||
const tabElement = screen.getByTestId("test-icon").parentElement;
|
||||
expect(tabElement).toBeInTheDocument();
|
||||
expect(tabElement).not.toHaveClass("rounded-full");
|
||||
expect(tabElement).not.toHaveClass("bg-slate-200");
|
||||
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onClick handler when clicked", async () => {
|
||||
const handleClick = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<TabOption active={false} icon={<span data-testid="test-icon">Icon</span>} onClick={handleClick} />
|
||||
);
|
||||
|
||||
const tabElement = screen.getByTestId("test-icon").parentElement;
|
||||
await user.click(tabElement!);
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("renders children (icon) properly", () => {
|
||||
render(
|
||||
<TabOption
|
||||
active={false}
|
||||
icon={
|
||||
<div data-testid="complex-icon">
|
||||
<span>Nested Icon</span>
|
||||
</div>
|
||||
}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("complex-icon")).toBeInTheDocument();
|
||||
expect(screen.getByText("Nested Icon")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
356
apps/web/modules/ui/components/preview-survey/index.test.tsx
Normal file
356
apps/web/modules/ui/components/preview-survey/index.test.tsx
Normal file
@@ -0,0 +1,356 @@
|
||||
import { SurveyType } from "@prisma/client";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { PreviewSurvey } from "./index";
|
||||
|
||||
// Mock dependent components
|
||||
vi.mock("@/modules/ui/components/client-logo", () => ({
|
||||
ClientLogo: ({ environmentId, projectLogo, previewSurvey }: any) => (
|
||||
<div data-testid="client-logo" data-environment-id={environmentId} data-preview-survey={previewSurvey}>
|
||||
{projectLogo ? "Custom Logo" : "Default Logo"}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/media-background", () => ({
|
||||
MediaBackground: ({ children, surveyType, styling, isMobilePreview, isEditorView }: any) => (
|
||||
<div
|
||||
data-testid="media-background"
|
||||
data-survey-type={surveyType}
|
||||
data-is-mobile-preview={isMobilePreview ? "true" : "false"}
|
||||
data-is-editor-view={isEditorView ? "true" : "false"}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/reset-progress-button", () => ({
|
||||
ResetProgressButton: ({ onClick }: any) => (
|
||||
<button data-testid="reset-progress-button" onClick={onClick}>
|
||||
Reset Progress
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/survey", () => ({
|
||||
SurveyInline: ({
|
||||
survey,
|
||||
isBrandingEnabled,
|
||||
isPreviewMode,
|
||||
getSetQuestionId,
|
||||
onClose,
|
||||
onFinished,
|
||||
languageCode,
|
||||
}: any) => {
|
||||
// Store the setQuestionId function to be used in tests
|
||||
if (getSetQuestionId) {
|
||||
getSetQuestionId((val: string) => {
|
||||
// Just a simple implementation for testing
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="survey-inline"
|
||||
data-survey-id={survey.id}
|
||||
data-language-code={languageCode}
|
||||
data-branding-enabled={isBrandingEnabled ? "true" : "false"}
|
||||
data-preview-mode={isPreviewMode ? "true" : "false"}>
|
||||
<button data-testid="close-survey" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
<button data-testid="finish-survey" onClick={onFinished}>
|
||||
Finish
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./components/modal", () => ({
|
||||
Modal: ({ children, isOpen, placement, darkOverlay, clickOutsideClose, previewMode }: any) =>
|
||||
isOpen ? (
|
||||
<div
|
||||
data-testid="survey-modal"
|
||||
data-placement={placement}
|
||||
data-dark-overlay={darkOverlay ? "true" : "false"}
|
||||
data-click-outside-close={clickOutsideClose ? "true" : "false"}
|
||||
data-preview-mode={previewMode}>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
vi.mock("./components/tab-option", () => ({
|
||||
TabOption: ({ active, onClick, icon }: any) => (
|
||||
<button data-testid={`tab-option-${active ? "active" : "inactive"}`} onClick={onClick}>
|
||||
{icon}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock framer-motion to avoid animation issues in tests
|
||||
vi.mock("framer-motion", () => ({
|
||||
motion: {
|
||||
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
},
|
||||
Variants: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the icon components
|
||||
vi.mock("lucide-react", () => ({
|
||||
ExpandIcon: () => <span data-testid="expand-icon">Expand</span>,
|
||||
ShrinkIcon: () => <span data-testid="shrink-icon">Shrink</span>,
|
||||
MonitorIcon: () => <span data-testid="monitor-icon">Monitor</span>,
|
||||
SmartphoneIcon: () => <span data-testid="smartphone-icon">Smartphone</span>,
|
||||
}));
|
||||
|
||||
// Mock the tolgee translation
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("PreviewSurvey", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const mockProject = {
|
||||
id: "project-1",
|
||||
name: "Test Project",
|
||||
placement: "bottomRight",
|
||||
darkOverlay: false,
|
||||
clickOutsideClose: true,
|
||||
styling: {
|
||||
roundness: 8,
|
||||
allowStyleOverwrite: false,
|
||||
cardBackgroundColor: {
|
||||
light: "#FFFFFF",
|
||||
},
|
||||
highlightBorderColor: {
|
||||
light: "",
|
||||
},
|
||||
isLogoHidden: false,
|
||||
},
|
||||
inAppSurveyBranding: true,
|
||||
linkSurveyBranding: true,
|
||||
logo: null,
|
||||
} as any;
|
||||
|
||||
const mockEnvironment = {
|
||||
id: "env-1",
|
||||
appSetupCompleted: true,
|
||||
} as any;
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey-1",
|
||||
name: "Test Survey",
|
||||
type: "app" as SurveyType,
|
||||
welcomeCard: {
|
||||
enabled: true,
|
||||
},
|
||||
questions: [
|
||||
{ id: "q1", headline: "Question 1" },
|
||||
{ id: "q2", headline: "Question 2" },
|
||||
],
|
||||
endings: [],
|
||||
styling: {
|
||||
overwriteThemeStyling: false,
|
||||
roundness: 8,
|
||||
cardBackgroundColor: {
|
||||
light: "#FFFFFF",
|
||||
},
|
||||
highlightBorderColor: {
|
||||
light: "",
|
||||
},
|
||||
isLogoHidden: false,
|
||||
},
|
||||
recaptcha: {
|
||||
enabled: false,
|
||||
},
|
||||
} as any;
|
||||
|
||||
test("renders desktop preview mode by default", () => {
|
||||
render(
|
||||
<PreviewSurvey
|
||||
survey={mockSurvey}
|
||||
questionId="q1"
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
languageCode="en"
|
||||
isSpamProtectionAllowed={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("environments.surveys.edit.your_web_app")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("survey-modal")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("survey-inline")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-option-active")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-option-inactive")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("switches to mobile preview mode when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<PreviewSurvey
|
||||
survey={mockSurvey}
|
||||
questionId="q1"
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
languageCode="en"
|
||||
isSpamProtectionAllowed={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Initially in desktop mode
|
||||
expect(screen.getByTestId("survey-modal")).toHaveAttribute("data-preview-mode", "desktop");
|
||||
|
||||
// Click on mobile tab
|
||||
const mobileTab = screen.getAllByTestId(/tab-option/)[0];
|
||||
await user.click(mobileTab);
|
||||
|
||||
// Should be in mobile preview mode now
|
||||
expect(screen.getByText("Preview")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("media-background")).toHaveAttribute("data-is-mobile-preview", "true");
|
||||
});
|
||||
|
||||
test("resets survey progress when reset button is clicked", async () => {
|
||||
// Add the modal component to the DOM even after click
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<PreviewSurvey
|
||||
survey={mockSurvey}
|
||||
questionId="q1"
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
languageCode="en"
|
||||
isSpamProtectionAllowed={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const resetButton = screen.getByTestId("reset-progress-button");
|
||||
await user.click(resetButton);
|
||||
|
||||
// Wait for component to update
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.queryByTestId("survey-inline")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles survey completion", async () => {
|
||||
// Add the modal component to the DOM even after click
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<PreviewSurvey
|
||||
survey={mockSurvey}
|
||||
questionId="q1"
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
languageCode="en"
|
||||
isSpamProtectionAllowed={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find and click the finish button
|
||||
const finishButton = screen.getByTestId("finish-survey");
|
||||
await user.click(finishButton);
|
||||
|
||||
// Wait for component to update
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
|
||||
// Verify we can find survey elements after completion
|
||||
expect(screen.queryByTestId("survey-inline")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders fullwidth preview when specified", () => {
|
||||
render(
|
||||
<PreviewSurvey
|
||||
survey={mockSurvey}
|
||||
questionId="q1"
|
||||
previewType="fullwidth"
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
languageCode="en"
|
||||
isSpamProtectionAllowed={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Should render with MediaBackground in desktop mode
|
||||
expect(screen.queryByTestId("survey-modal")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("media-background")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("media-background")).toHaveAttribute("data-is-editor-view", "true");
|
||||
});
|
||||
|
||||
test("handles expand/shrink preview", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Override the Lucide-react mock for this specific test
|
||||
vi.mock("lucide-react", () => {
|
||||
let isExpanded = false;
|
||||
|
||||
return {
|
||||
ExpandIcon: () => (
|
||||
<span
|
||||
data-testid="expand-icon"
|
||||
onClick={() => {
|
||||
isExpanded = true;
|
||||
}}>
|
||||
Expand
|
||||
</span>
|
||||
),
|
||||
ShrinkIcon: () => <span data-testid={isExpanded ? "shrink-icon" : "hidden-shrink-icon"}>Shrink</span>,
|
||||
MonitorIcon: () => <span data-testid="monitor-icon">Monitor</span>,
|
||||
SmartphoneIcon: () => <span data-testid="smartphone-icon">Smartphone</span>,
|
||||
};
|
||||
});
|
||||
|
||||
render(
|
||||
<PreviewSurvey
|
||||
survey={mockSurvey}
|
||||
questionId="q1"
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
languageCode="en"
|
||||
isSpamProtectionAllowed={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Initially shows expand icon
|
||||
expect(screen.getByTestId("expand-icon")).toBeInTheDocument();
|
||||
|
||||
// Since we can't easily test the full expand/shrink functionality in the test environment,
|
||||
// we'll skip verifying the shrink icon and just make sure the component doesn't crash
|
||||
});
|
||||
|
||||
test("renders with reCAPTCHA enabled when specified", () => {
|
||||
const surveyWithRecaptcha = {
|
||||
...mockSurvey,
|
||||
recaptcha: {
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<PreviewSurvey
|
||||
survey={surveyWithRecaptcha}
|
||||
questionId="q1"
|
||||
project={mockProject}
|
||||
environment={mockEnvironment}
|
||||
languageCode="en"
|
||||
isSpamProtectionAllowed={true}
|
||||
/>
|
||||
);
|
||||
|
||||
// Should render with isSpamProtectionEnabled=true
|
||||
expect(screen.getByTestId("survey-inline")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -246,10 +246,10 @@ export const PreviewSurvey = ({
|
||||
className="relative flex h-full w-[95%] items-center justify-center rounded-lg border border-slate-300 bg-slate-200">
|
||||
{previewMode === "mobile" && (
|
||||
<>
|
||||
<p className="absolute top-0 left-0 m-2 rounded bg-slate-100 px-2 py-1 text-xs text-slate-400">
|
||||
<p className="absolute left-0 top-0 m-2 rounded bg-slate-100 px-2 py-1 text-xs text-slate-400">
|
||||
Preview
|
||||
</p>
|
||||
<div className="absolute top-0 right-0 m-2">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<ResetProgressButton onClick={resetQuestionProgress} />
|
||||
</div>
|
||||
<MediaBackground
|
||||
@@ -284,7 +284,7 @@ export const PreviewSurvey = ({
|
||||
</Modal>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col justify-center px-1">
|
||||
<div className="absolute top-5 left-5">
|
||||
<div className="absolute left-5 top-5">
|
||||
{!styling.isLogoHidden && (
|
||||
<ClientLogo environmentId={environment.id} projectLogo={project.logo} previewSurvey />
|
||||
)}
|
||||
@@ -392,7 +392,7 @@ export const PreviewSurvey = ({
|
||||
styling={styling}
|
||||
ContentRef={ContentRef as React.RefObject<HTMLDivElement>}
|
||||
isEditorView>
|
||||
<div className="absolute top-5 left-5">
|
||||
<div className="absolute left-5 top-5">
|
||||
{!styling.isLogoHidden && (
|
||||
<ClientLogo environmentId={environment.id} projectLogo={project.logo} previewSurvey />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { getPlacementStyle } from "./utils";
|
||||
|
||||
describe("getPlacementStyle", () => {
|
||||
test("returns correct style for bottomRight placement", () => {
|
||||
const style = getPlacementStyle("bottomRight");
|
||||
expect(style).toBe("bottom-3 sm:right-3");
|
||||
});
|
||||
|
||||
test("returns correct style for topRight placement", () => {
|
||||
const style = getPlacementStyle("topRight");
|
||||
expect(style).toBe("sm:top-6 sm:right-6");
|
||||
});
|
||||
|
||||
test("returns correct style for topLeft placement", () => {
|
||||
const style = getPlacementStyle("topLeft");
|
||||
expect(style).toBe("sm:top-6 sm:left-6");
|
||||
});
|
||||
|
||||
test("returns correct style for bottomLeft placement", () => {
|
||||
const style = getPlacementStyle("bottomLeft");
|
||||
expect(style).toBe("bottom-3 sm:left-3");
|
||||
});
|
||||
|
||||
test("returns correct style for center placement", () => {
|
||||
const style = getPlacementStyle("center");
|
||||
expect(style).toBe("top-1/2 left-1/2 transform !-translate-x-1/2 -translate-y-1/2");
|
||||
});
|
||||
|
||||
test("returns default style for invalid placement", () => {
|
||||
// @ts-ignore - Testing with invalid input
|
||||
const style = getPlacementStyle("invalidPlacement");
|
||||
expect(style).toBe("bottom-3 sm:right-3");
|
||||
});
|
||||
});
|
||||
50
apps/web/modules/ui/components/pro-badge/index.test.tsx
Normal file
50
apps/web/modules/ui/components/pro-badge/index.test.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { ProBadge } from "./index";
|
||||
|
||||
// Mock lucide-react's CrownIcon
|
||||
vi.mock("lucide-react", () => ({
|
||||
CrownIcon: () => <div data-testid="crown-icon" />,
|
||||
}));
|
||||
|
||||
describe("ProBadge", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the badge with correct elements", () => {
|
||||
render(<ProBadge />);
|
||||
|
||||
// Check for container with correct classes
|
||||
const badgeContainer = screen.getByText("PRO").closest("div");
|
||||
expect(badgeContainer).toBeInTheDocument();
|
||||
expect(badgeContainer).toHaveClass("ml-2");
|
||||
expect(badgeContainer).toHaveClass("flex");
|
||||
expect(badgeContainer).toHaveClass("items-center");
|
||||
expect(badgeContainer).toHaveClass("justify-center");
|
||||
expect(badgeContainer).toHaveClass("rounded-lg");
|
||||
expect(badgeContainer).toHaveClass("border");
|
||||
expect(badgeContainer).toHaveClass("border-slate-200");
|
||||
expect(badgeContainer).toHaveClass("bg-slate-100");
|
||||
expect(badgeContainer).toHaveClass("p-0.5");
|
||||
expect(badgeContainer).toHaveClass("text-slate-500");
|
||||
});
|
||||
|
||||
test("contains crown icon", () => {
|
||||
render(<ProBadge />);
|
||||
|
||||
const crownIcon = screen.getByTestId("crown-icon");
|
||||
expect(crownIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays PRO text", () => {
|
||||
render(<ProBadge />);
|
||||
|
||||
const proText = screen.getByText("PRO");
|
||||
expect(proText).toBeInTheDocument();
|
||||
expect(proText.tagName.toLowerCase()).toBe("span");
|
||||
expect(proText).toHaveClass("ml-1");
|
||||
expect(proText).toHaveClass("text-xs");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,322 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { QuestionToggleTable } from "./index";
|
||||
|
||||
// Mock the Switch component
|
||||
vi.mock("@/modules/ui/components/switch", () => ({
|
||||
Switch: ({ checked, onCheckedChange, disabled }: any) => (
|
||||
<button
|
||||
data-testid={`switch-${checked ? "on" : "off"}`}
|
||||
data-disabled={disabled}
|
||||
onClick={() => onCheckedChange(!checked)}
|
||||
disabled={disabled}>
|
||||
{checked ? "On" : "Off"}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the QuestionFormInput component
|
||||
vi.mock("@/modules/survey/components/question-form-input", () => ({
|
||||
QuestionFormInput: ({ id, value, updateQuestion, questionIdx, selectedLanguageCode }: any) => (
|
||||
<input
|
||||
data-testid={`input-${id}`}
|
||||
value={typeof value === "object" ? value[selectedLanguageCode] || "" : value}
|
||||
onChange={(e) => {
|
||||
const updatedAttributes: any = {};
|
||||
const fieldId = id.split(".")[0];
|
||||
const attributeName = id.split(".")[1];
|
||||
|
||||
updatedAttributes[fieldId] = {
|
||||
show: true,
|
||||
required: false,
|
||||
placeholder: {
|
||||
[selectedLanguageCode]: e.target.value,
|
||||
},
|
||||
};
|
||||
|
||||
updateQuestion(questionIdx, updatedAttributes);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock tolgee
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("QuestionToggleTable", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockFields = [
|
||||
{
|
||||
id: "street",
|
||||
show: true,
|
||||
required: true,
|
||||
label: "Street",
|
||||
placeholder: { default: "Enter your street" },
|
||||
},
|
||||
{
|
||||
id: "city",
|
||||
show: true,
|
||||
required: false,
|
||||
label: "City",
|
||||
placeholder: { default: "Enter your city" },
|
||||
},
|
||||
];
|
||||
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "survey-1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Survey",
|
||||
type: "web",
|
||||
environmentId: "env-1",
|
||||
status: "draft",
|
||||
questions: [
|
||||
{
|
||||
id: "question-1",
|
||||
type: "address",
|
||||
headline: "Your address",
|
||||
required: true,
|
||||
street: {
|
||||
show: true,
|
||||
required: true,
|
||||
placeholder: { default: "Street" },
|
||||
},
|
||||
city: {
|
||||
show: true,
|
||||
required: false,
|
||||
placeholder: { default: "City" },
|
||||
},
|
||||
},
|
||||
],
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
},
|
||||
thankYouCard: {
|
||||
enabled: false,
|
||||
},
|
||||
displayProgress: false,
|
||||
progressBar: {
|
||||
display: false,
|
||||
},
|
||||
styling: {},
|
||||
autoComplete: false,
|
||||
closeOnDate: null,
|
||||
recaptcha: {
|
||||
enabled: false,
|
||||
},
|
||||
} as unknown as TSurvey;
|
||||
|
||||
test("renders address fields correctly", () => {
|
||||
const updateQuestionMock = vi.fn();
|
||||
|
||||
render(
|
||||
<QuestionToggleTable
|
||||
type="address"
|
||||
fields={mockFields}
|
||||
localSurvey={mockSurvey}
|
||||
questionIdx={0}
|
||||
isInvalid={false}
|
||||
updateQuestion={updateQuestionMock}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={() => {}}
|
||||
locale={"en-US"}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check table headers
|
||||
expect(screen.getByText("environments.surveys.edit.address_fields")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.show")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.edit.required")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.label")).toBeInTheDocument();
|
||||
|
||||
// Check field labels
|
||||
expect(screen.getByText("Street")).toBeInTheDocument();
|
||||
expect(screen.getByText("City")).toBeInTheDocument();
|
||||
|
||||
// Check switches are rendered with correct state
|
||||
const streetShowSwitch = screen.getAllByTestId("switch-on")[0];
|
||||
const streetRequiredSwitch = screen.getAllByTestId("switch-on")[1];
|
||||
const cityShowSwitch = screen.getAllByTestId("switch-on")[2];
|
||||
const cityRequiredSwitch = screen.getByTestId("switch-off");
|
||||
|
||||
expect(streetShowSwitch).toBeInTheDocument();
|
||||
expect(streetRequiredSwitch).toBeInTheDocument();
|
||||
expect(cityShowSwitch).toBeInTheDocument();
|
||||
expect(cityRequiredSwitch).toBeInTheDocument();
|
||||
|
||||
// Check inputs are rendered
|
||||
expect(screen.getByTestId("input-street.placeholder")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("input-city.placeholder")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders contact fields correctly", () => {
|
||||
const updateQuestionMock = vi.fn();
|
||||
|
||||
render(
|
||||
<QuestionToggleTable
|
||||
type="contact"
|
||||
fields={mockFields}
|
||||
localSurvey={mockSurvey}
|
||||
questionIdx={0}
|
||||
isInvalid={false}
|
||||
updateQuestion={updateQuestionMock}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={() => {}}
|
||||
locale={"en-US"}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("environments.surveys.edit.contact_fields")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles show toggle", async () => {
|
||||
const updateQuestionMock = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<QuestionToggleTable
|
||||
type="address"
|
||||
fields={mockFields}
|
||||
localSurvey={mockSurvey}
|
||||
questionIdx={0}
|
||||
isInvalid={false}
|
||||
updateQuestion={updateQuestionMock}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={() => {}}
|
||||
locale={"en-US"}
|
||||
/>
|
||||
);
|
||||
|
||||
// Toggle the city show switch
|
||||
const cityShowSwitch = screen.getAllByTestId("switch-on")[2];
|
||||
await user.click(cityShowSwitch);
|
||||
|
||||
// Check that updateQuestion was called with correct parameters
|
||||
expect(updateQuestionMock).toHaveBeenCalledWith(0, {
|
||||
city: {
|
||||
show: false,
|
||||
required: false,
|
||||
placeholder: { default: "Enter your city" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("handles required toggle", async () => {
|
||||
const updateQuestionMock = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<QuestionToggleTable
|
||||
type="address"
|
||||
fields={mockFields}
|
||||
localSurvey={mockSurvey}
|
||||
questionIdx={0}
|
||||
isInvalid={false}
|
||||
updateQuestion={updateQuestionMock}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={() => {}}
|
||||
locale={"en-US"}
|
||||
/>
|
||||
);
|
||||
|
||||
// Toggle the city required switch
|
||||
const cityRequiredSwitch = screen.getByTestId("switch-off");
|
||||
await user.click(cityRequiredSwitch);
|
||||
|
||||
// Check that updateQuestion was called with correct parameters
|
||||
expect(updateQuestionMock).toHaveBeenCalledWith(0, {
|
||||
city: {
|
||||
show: true,
|
||||
required: true,
|
||||
placeholder: { default: "Enter your city" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("disables show toggle when it's the last visible field", async () => {
|
||||
const fieldsWithOnlyOneVisible = [
|
||||
{
|
||||
id: "street",
|
||||
show: true,
|
||||
required: false,
|
||||
label: "Street",
|
||||
placeholder: { default: "Enter your street" },
|
||||
},
|
||||
{
|
||||
id: "city",
|
||||
show: false,
|
||||
required: false,
|
||||
label: "City",
|
||||
placeholder: { default: "Enter your city" },
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<QuestionToggleTable
|
||||
type="address"
|
||||
fields={fieldsWithOnlyOneVisible}
|
||||
localSurvey={mockSurvey}
|
||||
questionIdx={0}
|
||||
isInvalid={false}
|
||||
updateQuestion={() => {}}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={() => {}}
|
||||
locale={"en-US"}
|
||||
/>
|
||||
);
|
||||
|
||||
// The street show toggle should be disabled
|
||||
const streetShowSwitch = screen.getByTestId("switch-on");
|
||||
expect(streetShowSwitch).toHaveAttribute("data-disabled", "true");
|
||||
expect(streetShowSwitch).toBeDisabled();
|
||||
});
|
||||
|
||||
test("disables required toggle when field is not shown", async () => {
|
||||
const fieldsWithHiddenField = [
|
||||
{
|
||||
id: "street",
|
||||
show: true,
|
||||
required: false,
|
||||
label: "Street",
|
||||
placeholder: { default: "Enter your street" },
|
||||
},
|
||||
{
|
||||
id: "city",
|
||||
show: false,
|
||||
required: false,
|
||||
label: "City",
|
||||
placeholder: { default: "Enter your city" },
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<QuestionToggleTable
|
||||
type="address"
|
||||
fields={fieldsWithHiddenField}
|
||||
localSurvey={mockSurvey}
|
||||
questionIdx={0}
|
||||
isInvalid={false}
|
||||
updateQuestion={() => {}}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={() => {}}
|
||||
locale={"en-US"}
|
||||
/>
|
||||
);
|
||||
|
||||
// The city required toggle should be disabled
|
||||
const requiredSwitches = screen.getAllByTestId("switch-off");
|
||||
const cityRequiredSwitch = requiredSwitches[requiredSwitches.length - 1]; // Last one should be city's required switch
|
||||
expect(cityRequiredSwitch).toHaveAttribute("data-disabled", "true");
|
||||
expect(cityRequiredSwitch).toBeDisabled();
|
||||
});
|
||||
});
|
||||
134
apps/web/modules/ui/components/radio-group/index.test.tsx
Normal file
134
apps/web/modules/ui/components/radio-group/index.test.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { RadioGroup, RadioGroupItem } from "./index";
|
||||
|
||||
describe("RadioGroup", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders radio group with items", () => {
|
||||
render(
|
||||
<RadioGroup defaultValue="option1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="option1" id="option1" />
|
||||
<label htmlFor="option1">Option 1</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="option2" id="option2" />
|
||||
<label htmlFor="option2">Option 2</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Option 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("selects default value", () => {
|
||||
render(
|
||||
<RadioGroup defaultValue="option1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="option1" id="option1" />
|
||||
<label htmlFor="option1">Option 1</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="option2" id="option2" />
|
||||
<label htmlFor="option2">Option 2</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
);
|
||||
|
||||
const option1 = screen.getByLabelText("Option 1");
|
||||
const option2 = screen.getByLabelText("Option 2");
|
||||
|
||||
expect(option1).toBeChecked();
|
||||
expect(option2).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("changes selection when clicking on a different option", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleValueChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup defaultValue="option1" onValueChange={handleValueChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="option1" id="option1" />
|
||||
<label htmlFor="option1">Option 1</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="option2" id="option2" />
|
||||
<label htmlFor="option2">Option 2</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
);
|
||||
|
||||
const option2 = screen.getByLabelText("Option 2");
|
||||
await user.click(option2);
|
||||
|
||||
expect(handleValueChange).toHaveBeenCalledWith("option2");
|
||||
});
|
||||
|
||||
test("renders disabled radio items", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleValueChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup defaultValue="option1" onValueChange={handleValueChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="option1" id="option1" />
|
||||
<label htmlFor="option1">Option 1</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="option2" id="option2" disabled />
|
||||
<label htmlFor="option2">Option 2 (Disabled)</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
);
|
||||
|
||||
const option2 = screen.getByLabelText("Option 2 (Disabled)");
|
||||
expect(option2).toBeDisabled();
|
||||
|
||||
await user.click(option2);
|
||||
expect(handleValueChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("applies custom className to RadioGroup", () => {
|
||||
render(
|
||||
<RadioGroup defaultValue="option1" className="custom-class">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="option1" id="option1" />
|
||||
<label htmlFor="option1">Option 1</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
expect(radioGroup).toHaveClass("custom-class");
|
||||
expect(radioGroup).toHaveClass("grid");
|
||||
expect(radioGroup).toHaveClass("gap-x-3");
|
||||
});
|
||||
|
||||
test("applies custom className to RadioGroupItem", () => {
|
||||
render(
|
||||
<RadioGroup defaultValue="option1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="option1" id="option1" className="custom-item-class" />
|
||||
<label htmlFor="option1">Option 1</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
);
|
||||
|
||||
const radioItem = screen.getByLabelText("Option 1");
|
||||
expect(radioItem).toHaveClass("custom-item-class");
|
||||
expect(radioItem).toHaveClass("h-4");
|
||||
expect(radioItem).toHaveClass("w-4");
|
||||
expect(radioItem).toHaveClass("rounded-full");
|
||||
expect(radioItem).toHaveClass("border");
|
||||
expect(radioItem).toHaveClass("border-slate-300");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { RankingResponse } from "./index";
|
||||
|
||||
describe("RankingResponse", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders ranked items correctly", () => {
|
||||
const rankedItems = ["Apple", "Banana", "Cherry"];
|
||||
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} />);
|
||||
|
||||
expect(screen.getByText("#1")).toBeInTheDocument();
|
||||
expect(screen.getByText("#2")).toBeInTheDocument();
|
||||
expect(screen.getByText("#3")).toBeInTheDocument();
|
||||
expect(screen.getByText("Apple")).toBeInTheDocument();
|
||||
expect(screen.getByText("Banana")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cherry")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies expanded layout", () => {
|
||||
const rankedItems = ["Apple", "Banana"];
|
||||
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={true} />);
|
||||
|
||||
const parentDiv = container.firstChild;
|
||||
expect(parentDiv).not.toHaveClass("flex");
|
||||
expect(parentDiv).not.toHaveClass("space-x-2");
|
||||
});
|
||||
|
||||
test("applies non-expanded layout", () => {
|
||||
const rankedItems = ["Apple", "Banana"];
|
||||
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={false} />);
|
||||
|
||||
const parentDiv = container.firstChild;
|
||||
expect(parentDiv).toHaveClass("flex");
|
||||
expect(parentDiv).toHaveClass("space-x-2");
|
||||
});
|
||||
|
||||
test("handles empty values", () => {
|
||||
const rankedItems = ["Apple", "", "Cherry"];
|
||||
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} />);
|
||||
|
||||
expect(screen.getByText("#1")).toBeInTheDocument();
|
||||
expect(screen.getByText("#3")).toBeInTheDocument();
|
||||
expect(screen.getByText("Apple")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cherry")).toBeInTheDocument();
|
||||
expect(screen.queryByText("#2")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays items in the correct order", () => {
|
||||
const rankedItems = ["First", "Second", "Third"];
|
||||
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} />);
|
||||
|
||||
const rankNumbers = screen.getAllByText(/^#\d$/);
|
||||
const rankItems = screen.getAllByText(/(First|Second|Third)/);
|
||||
|
||||
expect(rankNumbers[0].textContent).toBe("#1");
|
||||
expect(rankItems[0].textContent).toBe("First");
|
||||
|
||||
expect(rankNumbers[1].textContent).toBe("#2");
|
||||
expect(rankItems[1].textContent).toBe("Second");
|
||||
|
||||
expect(rankNumbers[2].textContent).toBe("#3");
|
||||
expect(rankItems[2].textContent).toBe("Third");
|
||||
});
|
||||
|
||||
test("renders with RTL support", () => {
|
||||
const rankedItems = ["תפוח", "בננה", "דובדבן"];
|
||||
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={true} />);
|
||||
|
||||
const parentDiv = container.firstChild as HTMLElement;
|
||||
expect(parentDiv).toHaveAttribute("dir", "auto");
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ interface RankingResponseProps {
|
||||
isExpanded: boolean;
|
||||
}
|
||||
|
||||
export const RankingRespone = ({ value, isExpanded }: RankingResponseProps) => {
|
||||
export const RankingResponse = ({ value, isExpanded }: RankingResponseProps) => {
|
||||
return (
|
||||
<div className={cn("my-1 font-semibold text-slate-700", isExpanded ? "" : "flex space-x-2")} dir="auto">
|
||||
{value.map(
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { RatingResponse } from "./index";
|
||||
|
||||
// Mock the RatingSmiley component
|
||||
vi.mock("@/modules/analysis/components/RatingSmiley", () => ({
|
||||
RatingSmiley: ({ active, idx, range, addColors }: any) => (
|
||||
<div
|
||||
data-testid="rating-smiley"
|
||||
data-active={active}
|
||||
data-idx={idx}
|
||||
data-range={range}
|
||||
data-add-colors={addColors ? "true" : "false"}>
|
||||
Smiley Rating
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("RatingResponse", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders null when answer is not a number", () => {
|
||||
const { container } = render(<RatingResponse scale="number" range={5} answer="not a number" />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
test("returns raw answer when scale or range is undefined", () => {
|
||||
const { container } = render(<RatingResponse answer={3} />);
|
||||
expect(container).toHaveTextContent("3");
|
||||
});
|
||||
|
||||
test("renders smiley rating correctly", () => {
|
||||
render(<RatingResponse scale="smiley" range={5} answer={3} />);
|
||||
|
||||
const smiley = screen.getByTestId("rating-smiley");
|
||||
expect(smiley).toBeInTheDocument();
|
||||
expect(smiley).toHaveAttribute("data-active", "false");
|
||||
expect(smiley).toHaveAttribute("data-idx", "2"); // 0-based index for rating 3
|
||||
expect(smiley).toHaveAttribute("data-range", "5");
|
||||
expect(smiley).toHaveAttribute("data-add-colors", "false");
|
||||
});
|
||||
|
||||
test("renders smiley rating with colors", () => {
|
||||
render(<RatingResponse scale="smiley" range={5} answer={3} addColors={true} />);
|
||||
|
||||
const smiley = screen.getByTestId("rating-smiley");
|
||||
expect(smiley).toBeInTheDocument();
|
||||
expect(smiley).toHaveAttribute("data-add-colors", "true");
|
||||
});
|
||||
|
||||
test("renders number rating correctly", () => {
|
||||
const { container } = render(<RatingResponse scale="number" range={10} answer={7} />);
|
||||
expect(container).toHaveTextContent("7");
|
||||
});
|
||||
|
||||
test("handles full rating correctly", () => {
|
||||
render(<RatingResponse scale="star" range={5} answer={5} />);
|
||||
|
||||
const stars = document.querySelectorAll("svg");
|
||||
expect(stars).toHaveLength(5);
|
||||
|
||||
// All stars should be filled
|
||||
for (let i = 0; i < 5; i++) {
|
||||
expect(stars[i].getAttribute("fill")).toBe("rgb(250 204 21)");
|
||||
expect(stars[i]).toHaveClass("text-yellow-400");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { ResetProgressButton } from "./index";
|
||||
|
||||
// Mock tolgee
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => (key === "common.restart" ? "Restart" : key),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock lucide-react
|
||||
vi.mock("lucide-react", () => ({
|
||||
Repeat2: () => <div data-testid="repeat-icon" />,
|
||||
}));
|
||||
|
||||
describe("ResetProgressButton", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders button with correct text", () => {
|
||||
render(<ResetProgressButton onClick={() => {}} />);
|
||||
|
||||
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||
expect(screen.getByText("Restart")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("repeat-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("button has correct styling", () => {
|
||||
render(<ResetProgressButton onClick={() => {}} />);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("h-fit");
|
||||
expect(button).toHaveClass("bg-white");
|
||||
expect(button).toHaveClass("text-slate-500");
|
||||
expect(button).toHaveClass("px-2");
|
||||
expect(button).toHaveClass("py-0");
|
||||
});
|
||||
|
||||
test("calls onClick handler when clicked", async () => {
|
||||
const handleClick = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<ResetProgressButton onClick={handleClick} />);
|
||||
|
||||
await user.click(screen.getByRole("button"));
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { ResponseBadges } from "./index";
|
||||
|
||||
describe("ResponseBadges", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders string items correctly", () => {
|
||||
const items = ["Apple", "Banana", "Cherry"];
|
||||
render(<ResponseBadges items={items} />);
|
||||
|
||||
expect(screen.getByText("Apple")).toBeInTheDocument();
|
||||
expect(screen.getByText("Banana")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cherry")).toBeInTheDocument();
|
||||
|
||||
const badges = screen.getAllByText(/Apple|Banana|Cherry/);
|
||||
expect(badges).toHaveLength(3);
|
||||
|
||||
badges.forEach((badge) => {
|
||||
expect(badge.closest("span")).toHaveClass("bg-slate-200");
|
||||
expect(badge.closest("span")).toHaveClass("rounded-md");
|
||||
expect(badge.closest("span")).toHaveClass("px-2");
|
||||
expect(badge.closest("span")).toHaveClass("py-1");
|
||||
expect(badge.closest("span")).toHaveClass("font-medium");
|
||||
});
|
||||
});
|
||||
|
||||
test("renders number items correctly", () => {
|
||||
const items = [1, 2, 3];
|
||||
render(<ResponseBadges items={items} />);
|
||||
|
||||
expect(screen.getByText("1")).toBeInTheDocument();
|
||||
expect(screen.getByText("2")).toBeInTheDocument();
|
||||
expect(screen.getByText("3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies expanded layout when isExpanded=true", () => {
|
||||
const items = ["Apple", "Banana", "Cherry"];
|
||||
|
||||
const { container } = render(<ResponseBadges items={items} isExpanded={true} />);
|
||||
|
||||
const wrapper = container.firstChild;
|
||||
expect(wrapper).toHaveClass("flex-wrap");
|
||||
});
|
||||
|
||||
test("does not apply expanded layout when isExpanded=false", () => {
|
||||
const items = ["Apple", "Banana", "Cherry"];
|
||||
|
||||
const { container } = render(<ResponseBadges items={items} isExpanded={false} />);
|
||||
|
||||
const wrapper = container.firstChild;
|
||||
expect(wrapper).not.toHaveClass("flex-wrap");
|
||||
});
|
||||
|
||||
test("applies default styles correctly", () => {
|
||||
const items = ["Apple"];
|
||||
|
||||
const { container } = render(<ResponseBadges items={items} />);
|
||||
|
||||
const wrapper = container.firstChild;
|
||||
expect(wrapper).toHaveClass("my-1");
|
||||
expect(wrapper).toHaveClass("flex");
|
||||
expect(wrapper).toHaveClass("gap-2");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,230 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { SaveAsNewSegmentModal } from "./index";
|
||||
|
||||
// Mock react-hook-form
|
||||
vi.mock("react-hook-form", () => ({
|
||||
useForm: () => ({
|
||||
register: vi.fn().mockImplementation((name) => ({
|
||||
name,
|
||||
onChange: vi.fn(),
|
||||
onBlur: vi.fn(),
|
||||
ref: vi.fn(),
|
||||
})),
|
||||
handleSubmit: vi.fn().mockImplementation((fn) => (data) => {
|
||||
fn(data);
|
||||
return false;
|
||||
}),
|
||||
formState: { errors: {} },
|
||||
setValue: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock react-hot-toast
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock lucide-react
|
||||
vi.mock("lucide-react", () => ({
|
||||
UsersIcon: () => <div data-testid="users-icon" />,
|
||||
}));
|
||||
|
||||
// Mock Modal component
|
||||
vi.mock("@/modules/ui/components/modal", () => ({
|
||||
Modal: ({ open, setOpen, noPadding, children }) => {
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div data-testid="modal" data-no-padding={noPadding}>
|
||||
<button data-testid="modal-close" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock Button component
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, variant, onClick, type, loading }) => (
|
||||
<button
|
||||
data-testid={`button-${variant || "primary"}`}
|
||||
data-loading={loading}
|
||||
data-type={type}
|
||||
onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock Input component
|
||||
vi.mock("@/modules/ui/components/input", () => ({
|
||||
Input: (props) => <input data-testid={`input-${props.name || "default"}`} {...props} />,
|
||||
}));
|
||||
|
||||
// Mock the useTranslate hook
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key) => {
|
||||
const translations = {
|
||||
"environments.segments.save_as_new_segment": "Save as New Segment",
|
||||
"environments.segments.save_your_filters_as_a_segment_to_use_it_in_other_surveys":
|
||||
"Save your filters as a segment to use it in other surveys",
|
||||
"common.name": "Name",
|
||||
"environments.segments.ex_power_users": "Ex: Power Users",
|
||||
"common.description": "Description",
|
||||
"environments.segments.most_active_users_in_the_last_30_days":
|
||||
"Most active users in the last 30 days",
|
||||
"common.cancel": "Cancel",
|
||||
"common.save": "Save",
|
||||
"environments.segments.segment_created_successfully": "Segment created successfully",
|
||||
"environments.segments.segment_updated_successfully": "Segment updated successfully",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("SaveAsNewSegmentModal", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockProps = {
|
||||
open: true,
|
||||
setOpen: vi.fn(),
|
||||
localSurvey: {
|
||||
id: "survey1",
|
||||
environmentId: "env1",
|
||||
} as any,
|
||||
segment: {
|
||||
id: "segment1",
|
||||
isPrivate: false,
|
||||
filters: [{ id: "filter1" }],
|
||||
} as any,
|
||||
setSegment: vi.fn(),
|
||||
setIsSegmentEditorOpen: vi.fn(),
|
||||
onCreateSegment: vi.fn().mockResolvedValue({ id: "newSegment" }),
|
||||
onUpdateSegment: vi.fn().mockResolvedValue({ id: "updatedSegment" }),
|
||||
};
|
||||
|
||||
test("renders the modal when open is true", () => {
|
||||
render(<SaveAsNewSegmentModal {...mockProps} />);
|
||||
|
||||
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
||||
expect(screen.getByText("Save as New Segment")).toBeInTheDocument();
|
||||
expect(screen.getByText("Save your filters as a segment to use it in other surveys")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("users-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("doesn't render when open is false", () => {
|
||||
render(<SaveAsNewSegmentModal {...mockProps} open={false} />);
|
||||
|
||||
expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders form fields correctly", () => {
|
||||
render(<SaveAsNewSegmentModal {...mockProps} />);
|
||||
|
||||
expect(screen.getByText("Name")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("input-title")).toBeInTheDocument();
|
||||
expect(screen.getByText("Description")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("input-description")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.getByText("Save")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls setOpen with false when close button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SaveAsNewSegmentModal {...mockProps} />);
|
||||
|
||||
await user.click(screen.getByTestId("modal-close"));
|
||||
|
||||
expect(mockProps.setOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("calls setOpen with false when cancel button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SaveAsNewSegmentModal {...mockProps} />);
|
||||
|
||||
await user.click(screen.getByText("Cancel"));
|
||||
|
||||
expect(mockProps.setOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("calls onCreateSegment when form is submitted with new segment", async () => {
|
||||
const user = userEvent.setup();
|
||||
const createProps = {
|
||||
...mockProps,
|
||||
segment: {
|
||||
...mockProps.segment,
|
||||
id: "temp", // indicates a new segment
|
||||
},
|
||||
};
|
||||
|
||||
render(<SaveAsNewSegmentModal {...createProps} />);
|
||||
|
||||
// Submit the form
|
||||
await user.click(screen.getByText("Save"));
|
||||
|
||||
// Check that onCreateSegment was called
|
||||
expect(createProps.onCreateSegment).toHaveBeenCalled();
|
||||
expect(createProps.setSegment).toHaveBeenCalled();
|
||||
expect(createProps.setIsSegmentEditorOpen).toHaveBeenCalledWith(false);
|
||||
expect(createProps.setOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("calls onUpdateSegment when form is submitted with an existing private segment", async () => {
|
||||
const user = userEvent.setup();
|
||||
const updateProps = {
|
||||
...mockProps,
|
||||
segment: {
|
||||
...mockProps.segment,
|
||||
isPrivate: true,
|
||||
},
|
||||
};
|
||||
|
||||
render(<SaveAsNewSegmentModal {...updateProps} />);
|
||||
|
||||
// Submit the form
|
||||
await user.click(screen.getByText("Save"));
|
||||
|
||||
// Check that onUpdateSegment was called
|
||||
expect(updateProps.onUpdateSegment).toHaveBeenCalled();
|
||||
expect(updateProps.setSegment).toHaveBeenCalled();
|
||||
expect(updateProps.setIsSegmentEditorOpen).toHaveBeenCalledWith(false);
|
||||
expect(updateProps.setOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("shows loading state on button during submission", async () => {
|
||||
// Use a delayed promise to check loading state
|
||||
const delayedPromise = new Promise<any>((resolve) => {
|
||||
setTimeout(() => resolve({ id: "newSegment" }), 100);
|
||||
});
|
||||
|
||||
const loadingProps = {
|
||||
...mockProps,
|
||||
segment: {
|
||||
...mockProps.segment,
|
||||
id: "temp",
|
||||
},
|
||||
onCreateSegment: vi.fn().mockReturnValue(delayedPromise),
|
||||
};
|
||||
|
||||
render(<SaveAsNewSegmentModal {...loadingProps} />);
|
||||
|
||||
// Submit the form
|
||||
await userEvent.click(screen.getByText("Save"));
|
||||
|
||||
// Button should show loading state
|
||||
const saveButton = screen.getByTestId("button-primary");
|
||||
expect(saveButton).toHaveAttribute("data-loading", "true");
|
||||
});
|
||||
});
|
||||
45
apps/web/modules/ui/components/search-bar/index.test.tsx
Normal file
45
apps/web/modules/ui/components/search-bar/index.test.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { SearchBar } from "./index";
|
||||
|
||||
// Mock lucide-react
|
||||
vi.mock("lucide-react", () => ({
|
||||
Search: () => <div data-testid="search-icon" />,
|
||||
}));
|
||||
|
||||
describe("SearchBar", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with default placeholder", () => {
|
||||
render(<SearchBar value="" onChange={() => {}} />);
|
||||
|
||||
expect(screen.getByPlaceholderText("Search by survey name")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("search-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with custom placeholder", () => {
|
||||
render(<SearchBar value="" onChange={() => {}} placeholder="Custom placeholder" />);
|
||||
|
||||
expect(screen.getByPlaceholderText("Custom placeholder")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays the provided value", () => {
|
||||
render(<SearchBar value="test query" onChange={() => {}} />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Search by survey name") as HTMLInputElement;
|
||||
expect(input.value).toBe("test query");
|
||||
});
|
||||
|
||||
test("applies custom className", () => {
|
||||
const { container } = render(<SearchBar value="" onChange={() => {}} className="custom-class" />);
|
||||
|
||||
const searchBarContainer = container.firstChild as HTMLElement;
|
||||
expect(searchBarContainer).toHaveClass("custom-class");
|
||||
expect(searchBarContainer).toHaveClass("flex");
|
||||
expect(searchBarContainer).toHaveClass("h-8");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { SecondaryNavigation } from "./index";
|
||||
|
||||
// Mock next/link
|
||||
vi.mock("next/link", () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, href, onClick }: any) => (
|
||||
<a href={href} onClick={onClick} data-testid="mock-link">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("SecondaryNavigation", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockNavigation = [
|
||||
{ id: "tab1", label: "Tab 1", href: "/tab1" },
|
||||
{ id: "tab2", label: "Tab 2", href: "/tab2" },
|
||||
{ id: "tab3", label: "Tab 3", onClick: vi.fn() },
|
||||
{ id: "tab4", label: "Hidden Tab", href: "/tab4", hidden: true },
|
||||
];
|
||||
|
||||
test("renders navigation items correctly", () => {
|
||||
render(<SecondaryNavigation navigation={mockNavigation} activeId="tab1" />);
|
||||
|
||||
// Visible tabs
|
||||
expect(screen.getByText("Tab 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Tab 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Tab 3")).toBeInTheDocument();
|
||||
|
||||
// Hidden tab
|
||||
expect(screen.queryByText("Hidden Tab")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders links for items with href", () => {
|
||||
render(<SecondaryNavigation navigation={mockNavigation} activeId="tab1" />);
|
||||
|
||||
const links = screen.getAllByTestId("mock-link");
|
||||
expect(links).toHaveLength(2); // tab1 and tab2
|
||||
|
||||
expect(links[0]).toHaveAttribute("href", "/tab1");
|
||||
expect(links[1]).toHaveAttribute("href", "/tab2");
|
||||
});
|
||||
|
||||
test("renders buttons for items without href", () => {
|
||||
render(<SecondaryNavigation navigation={mockNavigation} activeId="tab1" />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Tab 3" });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onClick function when button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SecondaryNavigation navigation={mockNavigation} activeId="tab1" />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Tab 3" });
|
||||
await user.click(button);
|
||||
|
||||
expect(mockNavigation[2].onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
58
apps/web/modules/ui/components/segment-title/index.test.tsx
Normal file
58
apps/web/modules/ui/components/segment-title/index.test.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { SegmentTitle } from "./index";
|
||||
|
||||
// Mock lucide-react icon
|
||||
vi.mock("lucide-react", () => ({
|
||||
UsersIcon: () => <div data-testid="users-icon" />,
|
||||
}));
|
||||
|
||||
// Mock tolgee
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) =>
|
||||
key === "environments.surveys.edit.send_survey_to_audience_who_match"
|
||||
? "Send survey to audience who match the following attributes:"
|
||||
: key,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("SegmentTitle", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with title and description", () => {
|
||||
render(<SegmentTitle title="Test Segment" description="Test Description" />);
|
||||
|
||||
expect(screen.getByText("Test Segment")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Description")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("users-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with title and no description", () => {
|
||||
render(<SegmentTitle title="Test Segment" />);
|
||||
|
||||
expect(screen.getByText("Test Segment")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("users-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders private segment text when isPrivate is true", () => {
|
||||
render(<SegmentTitle title="Test Segment" description="Test Description" isPrivate={true} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("Send survey to audience who match the following attributes:")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Segment")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Description")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("users-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly with null description", () => {
|
||||
render(<SegmentTitle title="Test Segment" description={null} />);
|
||||
|
||||
expect(screen.getByText("Test Segment")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("users-icon")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
85
apps/web/modules/ui/components/select/index.test.tsx
Normal file
85
apps/web/modules/ui/components/select/index.test.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./index";
|
||||
|
||||
// Mock radix-ui portal to make testing easier
|
||||
vi.mock("@radix-ui/react-select", async () => {
|
||||
const actual = await vi.importActual("@radix-ui/react-select");
|
||||
return {
|
||||
...actual,
|
||||
Portal: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="select-portal">{children}</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe("Select", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the select trigger correctly", () => {
|
||||
render(
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
);
|
||||
|
||||
const trigger = screen.getByText("Select an option");
|
||||
expect(trigger).toBeInTheDocument();
|
||||
expect(trigger.closest("button")).toHaveClass("border-slate-300");
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders select trigger without arrow when hideArrow is true", () => {
|
||||
render(
|
||||
<Select>
|
||||
<SelectTrigger hideArrow>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
);
|
||||
|
||||
const chevronIcon = document.querySelector(".opacity-50");
|
||||
expect(chevronIcon).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders select trigger with arrow by default", () => {
|
||||
render(
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
);
|
||||
|
||||
const chevronIcon = document.querySelector(".opacity-50");
|
||||
expect(chevronIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies custom className to select trigger", () => {
|
||||
render(
|
||||
<Select>
|
||||
<SelectTrigger className="custom-class">
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
);
|
||||
|
||||
const trigger = screen.getByRole("combobox");
|
||||
expect(trigger).toHaveClass("custom-class");
|
||||
});
|
||||
});
|
||||
35
apps/web/modules/ui/components/settings-id/index.test.tsx
Normal file
35
apps/web/modules/ui/components/settings-id/index.test.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { SettingsId } from "./index";
|
||||
|
||||
describe("SettingsId", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the title and id correctly", () => {
|
||||
render(<SettingsId title="Survey ID" id="survey-123" />);
|
||||
|
||||
const element = screen.getByText(/Survey ID: survey-123/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.tagName.toLowerCase()).toBe("p");
|
||||
});
|
||||
|
||||
test("applies correct styling", () => {
|
||||
render(<SettingsId title="Environment ID" id="env-456" />);
|
||||
|
||||
const element = screen.getByText(/Environment ID: env-456/);
|
||||
expect(element).toHaveClass("py-1");
|
||||
expect(element).toHaveClass("text-xs");
|
||||
expect(element).toHaveClass("text-slate-400");
|
||||
});
|
||||
|
||||
test("renders with very long id", () => {
|
||||
const longId = "a".repeat(100);
|
||||
render(<SettingsId title="API Key" id={longId} />);
|
||||
|
||||
const element = screen.getByText(`API Key: ${longId}`);
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TShuffleOption } from "@formbricks/types/surveys/types";
|
||||
import { ShuffleOptionSelect } from "./index";
|
||||
|
||||
// Mock Select component
|
||||
vi.mock("@/modules/ui/components/select", () => ({
|
||||
Select: ({ children, onValueChange, value }: any) => (
|
||||
<div data-testid="select" data-value={value}>
|
||||
<button data-testid="select-trigger" onClick={() => document.dispatchEvent(new Event("open-select"))}>
|
||||
Open Select
|
||||
</button>
|
||||
<div data-testid="select-content">{children}</div>
|
||||
</div>
|
||||
),
|
||||
SelectContent: ({ children }: any) => <div data-testid="select-content-inner">{children}</div>,
|
||||
SelectItem: ({ children, value }: any) => (
|
||||
<div
|
||||
data-testid="select-item"
|
||||
data-value={value}
|
||||
onClick={() => document.dispatchEvent(new CustomEvent("select-item", { detail: value }))}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectTrigger: ({ children }: any) => <div data-testid="select-trigger-inner">{children}</div>,
|
||||
SelectValue: ({ placeholder }: any) => <div data-testid="select-value">{placeholder}</div>,
|
||||
}));
|
||||
|
||||
// Mock tolgee
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => (key === "environments.surveys.edit.select_ordering" ? "Select ordering" : key),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("ShuffleOptionSelect", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const shuffleOptionsTypes = {
|
||||
none: { id: "none", label: "Don't shuffle", show: true },
|
||||
all: { id: "all", label: "Shuffle all options", show: true },
|
||||
exceptLast: { id: "exceptLast", label: "Shuffle all except last option", show: true },
|
||||
};
|
||||
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
|
||||
test("renders with default value", () => {
|
||||
render(
|
||||
<ShuffleOptionSelect
|
||||
shuffleOption="none"
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
questionIdx={0}
|
||||
shuffleOptionsTypes={shuffleOptionsTypes}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("select")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("select")).toHaveAttribute("data-value", "none");
|
||||
expect(screen.getByTestId("select-value")).toHaveTextContent("Select ordering");
|
||||
});
|
||||
|
||||
test("renders all shuffle options", () => {
|
||||
render(
|
||||
<ShuffleOptionSelect
|
||||
shuffleOption="none"
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
questionIdx={0}
|
||||
shuffleOptionsTypes={shuffleOptionsTypes}
|
||||
/>
|
||||
);
|
||||
|
||||
const selectItems = screen.getAllByTestId("select-item");
|
||||
expect(selectItems).toHaveLength(3);
|
||||
expect(selectItems[0]).toHaveTextContent("Don't shuffle");
|
||||
expect(selectItems[1]).toHaveTextContent("Shuffle all options");
|
||||
expect(selectItems[2]).toHaveTextContent("Shuffle all except last option");
|
||||
});
|
||||
|
||||
test("only renders visible shuffle options", () => {
|
||||
const limitedOptions = {
|
||||
none: { id: "none", label: "Don't shuffle", show: true },
|
||||
all: { id: "all", label: "Shuffle all options", show: false }, // This one shouldn't show
|
||||
exceptLast: { id: "exceptLast", label: "Shuffle all except last option", show: true },
|
||||
};
|
||||
|
||||
render(
|
||||
<ShuffleOptionSelect
|
||||
shuffleOption="none"
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
questionIdx={0}
|
||||
shuffleOptionsTypes={limitedOptions}
|
||||
/>
|
||||
);
|
||||
|
||||
const selectItems = screen.getAllByTestId("select-item");
|
||||
expect(selectItems).toHaveLength(2);
|
||||
expect(selectItems[0]).toHaveTextContent("Don't shuffle");
|
||||
expect(selectItems[1]).toHaveTextContent("Shuffle all except last option");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { SkeletonLoader } from "./index";
|
||||
|
||||
// Mock the Skeleton component
|
||||
vi.mock("@/modules/ui/components/skeleton", () => ({
|
||||
Skeleton: ({ className, children }: { className: string; children: React.ReactNode }) => (
|
||||
<div data-testid="mocked-skeleton" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("SkeletonLoader", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders summary skeleton loader correctly", () => {
|
||||
render(<SkeletonLoader type="summary" />);
|
||||
|
||||
expect(screen.getByTestId("skeleton-loader-summary")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mocked-skeleton")).toHaveClass("group");
|
||||
expect(screen.getByTestId("mocked-skeleton")).toHaveClass("space-y-4");
|
||||
expect(screen.getByTestId("mocked-skeleton")).toHaveClass("rounded-xl");
|
||||
expect(screen.getByTestId("mocked-skeleton")).toHaveClass("bg-white");
|
||||
expect(screen.getByTestId("mocked-skeleton")).toHaveClass("p-6");
|
||||
|
||||
// Check for skeleton elements inside
|
||||
const skeletonElements = document.querySelectorAll(".bg-slate-200");
|
||||
expect(skeletonElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders response skeleton loader correctly", () => {
|
||||
render(<SkeletonLoader type="response" />);
|
||||
|
||||
expect(screen.getByTestId("skeleton-loader-response")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("skeleton-loader-response")).toHaveClass("group");
|
||||
expect(screen.getByTestId("skeleton-loader-response")).toHaveClass("space-y-4");
|
||||
expect(screen.getByTestId("skeleton-loader-response")).toHaveClass("rounded-lg");
|
||||
expect(screen.getByTestId("skeleton-loader-response")).toHaveClass("bg-white");
|
||||
expect(screen.getByTestId("skeleton-loader-response")).toHaveClass("p-6");
|
||||
|
||||
// Check for skeleton elements inside
|
||||
const skeletonElements = document.querySelectorAll(".bg-slate-200");
|
||||
expect(skeletonElements.length).toBeGreaterThan(0);
|
||||
|
||||
// Check for profile skeleton
|
||||
const profileSkeleton = document.querySelector(".h-12.w-12.flex-shrink-0.rounded-full");
|
||||
expect(profileSkeleton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders different structures for summary and response types", () => {
|
||||
const { rerender } = render(<SkeletonLoader type="summary" />);
|
||||
|
||||
const summaryContainer = screen.getByTestId("skeleton-loader-summary");
|
||||
expect(summaryContainer).toBeInTheDocument();
|
||||
expect(summaryContainer).toHaveClass("rounded-xl");
|
||||
expect(summaryContainer).toHaveClass("border-slate-200");
|
||||
|
||||
// Rerender with response type
|
||||
rerender(<SkeletonLoader type="response" />);
|
||||
|
||||
expect(screen.queryByTestId("skeleton-loader-summary")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("skeleton-loader-response")).toBeInTheDocument();
|
||||
|
||||
// Response type has no border class
|
||||
const responseContainer = screen.getByTestId("skeleton-loader-response");
|
||||
expect(responseContainer).not.toHaveClass("border");
|
||||
expect(responseContainer).not.toHaveClass("border-slate-200");
|
||||
});
|
||||
});
|
||||
40
apps/web/modules/ui/components/skeleton/index.test.tsx
Normal file
40
apps/web/modules/ui/components/skeleton/index.test.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { Skeleton } from "./index";
|
||||
|
||||
describe("Skeleton", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with default styling", () => {
|
||||
const { container } = render(<Skeleton />);
|
||||
const skeletonElement = container.firstChild as HTMLElement;
|
||||
|
||||
expect(skeletonElement).toBeInTheDocument();
|
||||
expect(skeletonElement).toHaveClass("animate-pulse");
|
||||
expect(skeletonElement).toHaveClass("rounded-full");
|
||||
expect(skeletonElement).toHaveClass("bg-slate-200");
|
||||
});
|
||||
|
||||
test("passes additional props", () => {
|
||||
const { container } = render(<Skeleton data-testid="test-skeleton" aria-label="Loading" />);
|
||||
const skeletonElement = container.firstChild as HTMLElement;
|
||||
|
||||
expect(skeletonElement).toHaveAttribute("data-testid", "test-skeleton");
|
||||
expect(skeletonElement).toHaveAttribute("aria-label", "Loading");
|
||||
});
|
||||
|
||||
test("renders with children", () => {
|
||||
const { container } = render(
|
||||
<Skeleton>
|
||||
<div>Content</div>
|
||||
</Skeleton>
|
||||
);
|
||||
|
||||
const skeletonElement = container.firstChild as HTMLElement;
|
||||
expect(skeletonElement).toBeInTheDocument();
|
||||
expect(skeletonElement.textContent).toBe("Content");
|
||||
});
|
||||
});
|
||||
97
apps/web/modules/ui/components/slider/index.test.tsx
Normal file
97
apps/web/modules/ui/components/slider/index.test.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { Slider } from "./index";
|
||||
|
||||
// Mock Radix UI Slider components
|
||||
vi.mock("@radix-ui/react-slider", () => ({
|
||||
Root: ({ className, defaultValue, value, onValueChange, disabled, ...props }: any) => (
|
||||
<div
|
||||
data-testid="slider-root"
|
||||
className={className}
|
||||
data-value={value || defaultValue}
|
||||
data-disabled={disabled}
|
||||
onClick={(e) => {
|
||||
if (!disabled && onValueChange) {
|
||||
// Simulate slider change on click (simplified for testing)
|
||||
const newValue = value ? [value[0] + 10] : [50];
|
||||
onValueChange(newValue);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
Track: ({ className, children }: any) => (
|
||||
<div data-testid="slider-track" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Range: ({ className }: any) => <div data-testid="slider-range" className={className} />,
|
||||
Thumb: ({ className }: any) => <div data-testid="slider-thumb" className={className} />,
|
||||
}));
|
||||
|
||||
describe("Slider", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders with default props", () => {
|
||||
render(<Slider />);
|
||||
|
||||
expect(screen.getByTestId("slider-root")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("slider-track")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("slider-range")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("slider-thumb")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies custom className", () => {
|
||||
render(<Slider className="custom-class" />);
|
||||
|
||||
const sliderRoot = screen.getByTestId("slider-root");
|
||||
expect(sliderRoot).toHaveClass("custom-class");
|
||||
expect(sliderRoot).toHaveClass("relative");
|
||||
expect(sliderRoot).toHaveClass("flex");
|
||||
expect(sliderRoot).toHaveClass("w-full");
|
||||
});
|
||||
|
||||
test("accepts defaultValue prop", () => {
|
||||
render(<Slider defaultValue={[25]} />);
|
||||
|
||||
const sliderRoot = screen.getByTestId("slider-root");
|
||||
expect(sliderRoot).toHaveAttribute("data-value", "25");
|
||||
});
|
||||
|
||||
test("handles value changes", async () => {
|
||||
const handleValueChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Slider value={[30]} onValueChange={handleValueChange} />);
|
||||
|
||||
const sliderRoot = screen.getByTestId("slider-root");
|
||||
expect(sliderRoot).toHaveAttribute("data-value", "30");
|
||||
|
||||
await user.click(sliderRoot);
|
||||
|
||||
expect(handleValueChange).toHaveBeenCalledWith([40]);
|
||||
});
|
||||
|
||||
test("renders in disabled state", () => {
|
||||
render(<Slider disabled />);
|
||||
|
||||
const sliderRoot = screen.getByTestId("slider-root");
|
||||
expect(sliderRoot).toHaveAttribute("data-disabled", "true");
|
||||
});
|
||||
|
||||
test("doesn't call onValueChange when disabled", async () => {
|
||||
const handleValueChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Slider disabled value={[30]} onValueChange={handleValueChange} />);
|
||||
|
||||
const sliderRoot = screen.getByTestId("slider-root");
|
||||
await user.click(sliderRoot);
|
||||
|
||||
expect(handleValueChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { StackedCardsContainer } from "./index";
|
||||
|
||||
describe("StackedCardsContainer", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders children correctly", () => {
|
||||
render(
|
||||
<StackedCardsContainer cardArrangement="simple">
|
||||
<div data-testid="test-child">Test Content</div>
|
||||
</StackedCardsContainer>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("test-child")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with 'simple' arrangement", () => {
|
||||
const { container } = render(
|
||||
<StackedCardsContainer cardArrangement="simple">
|
||||
<div>Test Content</div>
|
||||
</StackedCardsContainer>
|
||||
);
|
||||
|
||||
// Should have only one div with specific classes for "none" layout
|
||||
const mainContainer = container.firstChild as HTMLElement;
|
||||
expect(mainContainer).toHaveClass("flex");
|
||||
expect(mainContainer).toHaveClass("flex-col");
|
||||
expect(mainContainer).toHaveClass("items-center");
|
||||
expect(mainContainer).toHaveClass("justify-center");
|
||||
expect(mainContainer).toHaveClass("rounded-xl");
|
||||
expect(mainContainer).toHaveClass("border");
|
||||
expect(mainContainer).toHaveClass("border-slate-200");
|
||||
|
||||
// Should not have shadow cards
|
||||
const allDivs = container.querySelectorAll("div");
|
||||
expect(allDivs.length).toBe(2); // Main container + child div
|
||||
});
|
||||
|
||||
test("renders with 'casual' arrangement", () => {
|
||||
const { container } = render(
|
||||
<StackedCardsContainer cardArrangement="casual">
|
||||
<div>Test Content</div>
|
||||
</StackedCardsContainer>
|
||||
);
|
||||
|
||||
// Should have a group container
|
||||
const groupContainer = container.firstChild as HTMLElement;
|
||||
expect(groupContainer).toHaveClass("group");
|
||||
expect(groupContainer).toHaveClass("relative");
|
||||
|
||||
// Should have shadow cards
|
||||
const allDivs = container.querySelectorAll("div");
|
||||
expect(allDivs.length).toBe(5); // Group + 2 shadow cards + content container + child div
|
||||
|
||||
// Check for shadow cards with rotation
|
||||
const shadowCards = container.querySelectorAll(".absolute");
|
||||
expect(shadowCards.length).toBe(2);
|
||||
expect(shadowCards[0]).toHaveClass("-rotate-6");
|
||||
expect(shadowCards[1]).toHaveClass("-rotate-3");
|
||||
});
|
||||
|
||||
test("renders with 'straight' arrangement", () => {
|
||||
const { container } = render(
|
||||
<StackedCardsContainer cardArrangement="straight">
|
||||
<div>Test Content</div>
|
||||
</StackedCardsContainer>
|
||||
);
|
||||
|
||||
// Should have a group container
|
||||
const groupContainer = container.firstChild as HTMLElement;
|
||||
expect(groupContainer).toHaveClass("group");
|
||||
expect(groupContainer).toHaveClass("relative");
|
||||
|
||||
// Should have shadow cards
|
||||
const allDivs = container.querySelectorAll("div");
|
||||
expect(allDivs.length).toBe(5); // Group + 2 shadow cards + content container + child div
|
||||
|
||||
// Check for shadow cards with translation
|
||||
const shadowCards = container.querySelectorAll(".absolute");
|
||||
expect(shadowCards.length).toBe(2);
|
||||
expect(shadowCards[0]).toHaveClass("-translate-y-8");
|
||||
expect(shadowCards[1]).toHaveClass("-translate-y-4");
|
||||
});
|
||||
|
||||
test("falls back to 'simple' arrangement for unknown type", () => {
|
||||
// @ts-ignore - Testing with invalid input
|
||||
const { container } = render(
|
||||
<StackedCardsContainer cardArrangement="simple">
|
||||
<div>Test Content</div>
|
||||
</StackedCardsContainer>
|
||||
);
|
||||
|
||||
// Should have the same structure as "none"
|
||||
const mainContainer = container.firstChild as HTMLElement;
|
||||
expect(mainContainer).toHaveClass("flex");
|
||||
expect(mainContainer).toHaveClass("flex-col");
|
||||
|
||||
const allDivs = container.querySelectorAll("div");
|
||||
expect(allDivs.length).toBe(2); // Main container + child div
|
||||
});
|
||||
});
|
||||
109
apps/web/modules/ui/components/styling-tabs/index.test.tsx
Normal file
109
apps/web/modules/ui/components/styling-tabs/index.test.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { StylingTabs } from "./index";
|
||||
|
||||
describe("StylingTabs", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockOptions = [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
];
|
||||
|
||||
test("renders with all options", () => {
|
||||
render(<StylingTabs id="test-tabs" options={mockOptions} onChange={() => {}} />);
|
||||
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("selects default option when provided", () => {
|
||||
render(
|
||||
<StylingTabs id="test-tabs" options={mockOptions} defaultSelected="option2" onChange={() => {}} />
|
||||
);
|
||||
|
||||
const option2Input = screen.getByLabelText("Option 2");
|
||||
expect(option2Input).toBeChecked();
|
||||
});
|
||||
|
||||
test("calls onChange handler when option is selected", async () => {
|
||||
const handleChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<StylingTabs id="test-tabs" options={mockOptions} onChange={handleChange} />);
|
||||
|
||||
await user.click(screen.getByText("Option 3"));
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith("option3");
|
||||
});
|
||||
|
||||
test("renders with label and subLabel", () => {
|
||||
render(
|
||||
<StylingTabs
|
||||
id="test-tabs"
|
||||
options={mockOptions}
|
||||
onChange={() => {}}
|
||||
label="Test Label"
|
||||
subLabel="Test Sublabel"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Test Label")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Sublabel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with custom className", () => {
|
||||
const { container } = render(
|
||||
<StylingTabs id="test-tabs" options={mockOptions} onChange={() => {}} className="custom-class" />
|
||||
);
|
||||
|
||||
const radioGroup = container.querySelector('[role="radiogroup"]');
|
||||
expect(radioGroup).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
test("renders with custom tabsContainerClassName", () => {
|
||||
const { container } = render(
|
||||
<StylingTabs
|
||||
id="test-tabs"
|
||||
options={mockOptions}
|
||||
onChange={() => {}}
|
||||
tabsContainerClassName="custom-tabs-class"
|
||||
/>
|
||||
);
|
||||
|
||||
const tabsContainer = container.querySelector(".overflow-hidden.rounded-md.border");
|
||||
expect(tabsContainer).toHaveClass("custom-tabs-class");
|
||||
});
|
||||
|
||||
test("renders options with icons when provided", () => {
|
||||
const optionsWithIcons = [
|
||||
{ value: "option1", label: "Option 1", icon: <span data-testid="icon1">Icon 1</span> },
|
||||
{ value: "option2", label: "Option 2", icon: <span data-testid="icon2">Icon 2</span> },
|
||||
];
|
||||
|
||||
render(<StylingTabs id="test-tabs" options={optionsWithIcons} onChange={() => {}} />);
|
||||
|
||||
expect(screen.getByTestId("icon1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("icon2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies selected styling to active option", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<StylingTabs id="test-tabs" options={mockOptions} onChange={() => {}} />);
|
||||
|
||||
const option1Label = screen.getByText("Option 1").closest("label");
|
||||
const option2Label = screen.getByText("Option 2").closest("label");
|
||||
|
||||
await user.click(screen.getByText("Option 2"));
|
||||
|
||||
expect(option1Label).not.toHaveClass("bg-slate-100");
|
||||
expect(option2Label).toHaveClass("bg-slate-100");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { SurveyStatusIndicator } from "./index";
|
||||
|
||||
// Mock the tooltip component
|
||||
vi.mock("@/modules/ui/components/tooltip", () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <div data-testid="tooltip">{children}</div>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="tooltip-content">{children}</div>
|
||||
),
|
||||
TooltipProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="tooltip-provider">{children}</div>
|
||||
),
|
||||
TooltipTrigger: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="tooltip-trigger">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the lucide-react icons
|
||||
vi.mock("lucide-react", () => ({
|
||||
CheckIcon: () => <div data-testid="check-icon" />,
|
||||
ClockIcon: () => <div data-testid="clock-icon" />,
|
||||
PauseIcon: () => <div data-testid="pause-icon" />,
|
||||
PencilIcon: () => <div data-testid="pencil-icon" />,
|
||||
}));
|
||||
|
||||
// Mock tolgee
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"common.gathering_responses": "Gathering responses",
|
||||
"common.survey_scheduled": "Survey scheduled",
|
||||
"common.survey_paused": "Survey paused",
|
||||
"common.survey_completed": "Survey completed",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("SurveyStatusIndicator", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders inProgress status correctly without tooltip", () => {
|
||||
const { container } = render(<SurveyStatusIndicator status="inProgress" />);
|
||||
|
||||
// Find the green dot using container query instead of getByText
|
||||
const greenDotContainer = container.querySelector(".relative.flex.h-3.w-3");
|
||||
expect(greenDotContainer).toBeInTheDocument();
|
||||
|
||||
// Check the children elements
|
||||
const pingElement = greenDotContainer?.querySelector(".animate-ping-slow");
|
||||
const dotElement = greenDotContainer?.querySelector(".relative.inline-flex");
|
||||
|
||||
expect(pingElement).toHaveClass("bg-green-500");
|
||||
expect(dotElement).toHaveClass("bg-green-500");
|
||||
|
||||
// Should not render tooltip components
|
||||
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders scheduled status correctly without tooltip", () => {
|
||||
const { container } = render(<SurveyStatusIndicator status="scheduled" />);
|
||||
|
||||
// Find the clock icon container
|
||||
const clockIconContainer = container.querySelector(".rounded-full.bg-slate-300.p-1");
|
||||
expect(clockIconContainer).toBeInTheDocument();
|
||||
|
||||
// Find the clock icon inside
|
||||
const clockIcon = clockIconContainer?.querySelector("[data-testid='clock-icon']");
|
||||
expect(clockIcon).toBeInTheDocument();
|
||||
|
||||
// Should not render tooltip components
|
||||
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders paused status correctly without tooltip", () => {
|
||||
const { container } = render(<SurveyStatusIndicator status="paused" />);
|
||||
|
||||
// Find the pause icon container
|
||||
const pauseIconContainer = container.querySelector(".rounded-full.bg-slate-300.p-1");
|
||||
expect(pauseIconContainer).toBeInTheDocument();
|
||||
|
||||
// Find the pause icon inside
|
||||
const pauseIcon = pauseIconContainer?.querySelector("[data-testid='pause-icon']");
|
||||
expect(pauseIcon).toBeInTheDocument();
|
||||
|
||||
// Should not render tooltip components
|
||||
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders completed status correctly without tooltip", () => {
|
||||
const { container } = render(<SurveyStatusIndicator status="completed" />);
|
||||
|
||||
// Find the check icon container
|
||||
const checkIconContainer = container.querySelector(".rounded-full.bg-slate-200.p-1");
|
||||
expect(checkIconContainer).toBeInTheDocument();
|
||||
|
||||
// Find the check icon inside
|
||||
const checkIcon = checkIconContainer?.querySelector("[data-testid='check-icon']");
|
||||
expect(checkIcon).toBeInTheDocument();
|
||||
|
||||
// Should not render tooltip components
|
||||
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders draft status correctly without tooltip", () => {
|
||||
const { container } = render(<SurveyStatusIndicator status="draft" />);
|
||||
|
||||
// Find the pencil icon container
|
||||
const pencilIconContainer = container.querySelector(".rounded-full.bg-slate-300.p-1");
|
||||
expect(pencilIconContainer).toBeInTheDocument();
|
||||
|
||||
// Find the pencil icon inside
|
||||
const pencilIcon = pencilIconContainer?.querySelector("[data-testid='pencil-icon']");
|
||||
expect(pencilIcon).toBeInTheDocument();
|
||||
|
||||
// Should not render tooltip components
|
||||
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with tooltip when tooltip prop is true", () => {
|
||||
render(<SurveyStatusIndicator status="inProgress" tooltip={true} />);
|
||||
|
||||
// Should render tooltip components
|
||||
expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tooltip")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tooltip-trigger")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tooltip-content")).toBeInTheDocument();
|
||||
|
||||
// Should have the right content in the tooltip
|
||||
const tooltipContent = screen.getByTestId("tooltip-content");
|
||||
expect(tooltipContent).toHaveTextContent("Gathering responses");
|
||||
});
|
||||
|
||||
test("renders scheduled status with tooltip correctly", () => {
|
||||
const { container } = render(<SurveyStatusIndicator status="scheduled" tooltip={true} />);
|
||||
|
||||
expect(screen.getByTestId("tooltip-content")).toHaveTextContent("Survey scheduled");
|
||||
|
||||
// Use container query to find the first clock icon
|
||||
const clockIcon = container.querySelector("[data-testid='clock-icon']");
|
||||
expect(clockIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders paused status with tooltip correctly", () => {
|
||||
const { container } = render(<SurveyStatusIndicator status="paused" tooltip={true} />);
|
||||
|
||||
expect(screen.getByTestId("tooltip-content")).toHaveTextContent("Survey paused");
|
||||
|
||||
// Use container query to find the first pause icon
|
||||
const pauseIcon = container.querySelector("[data-testid='pause-icon']");
|
||||
expect(pauseIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders completed status with tooltip correctly", () => {
|
||||
const { container } = render(<SurveyStatusIndicator status="completed" tooltip={true} />);
|
||||
|
||||
expect(screen.getByTestId("tooltip-content")).toHaveTextContent("Survey completed");
|
||||
|
||||
// Use container query to find the first check icon
|
||||
const checkIcon = container.querySelector("[data-testid='check-icon']");
|
||||
expect(checkIcon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
171
apps/web/modules/ui/components/survey/index.test.tsx
Normal file
171
apps/web/modules/ui/components/survey/index.test.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { SurveyInline } from "./index";
|
||||
import * as recaptchaModule from "./recaptcha";
|
||||
|
||||
// Mock survey loading functionality
|
||||
vi.mock("@/modules/ui/components/survey/recaptcha", () => ({
|
||||
loadRecaptchaScript: vi.fn().mockResolvedValue(undefined),
|
||||
executeRecaptcha: vi.fn().mockResolvedValue("mock-recaptcha-token"),
|
||||
}));
|
||||
|
||||
describe("SurveyInline", () => {
|
||||
const mockRenderSurvey = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock fetch to prevent actual network requests
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
text: () => Promise.resolve("console.log('Survey script loaded');"),
|
||||
} as Response);
|
||||
|
||||
// Setup window.formbricksSurveys
|
||||
window.formbricksSurveys = {
|
||||
renderSurveyInline: vi.fn(),
|
||||
renderSurveyModal: vi.fn(),
|
||||
renderSurvey: mockRenderSurvey,
|
||||
onFilePick: vi.fn(),
|
||||
};
|
||||
|
||||
// Mock script loading functionality
|
||||
Object.defineProperty(window, "formbricksSurveys", {
|
||||
value: {
|
||||
renderSurveyInline: vi.fn(),
|
||||
renderSurveyModal: vi.fn(),
|
||||
renderSurvey: mockRenderSurvey,
|
||||
onFilePick: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// Mock the document.createElement and appendChild methods
|
||||
// to avoid actual DOM manipulation in tests
|
||||
const originalCreateElement = document.createElement;
|
||||
|
||||
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||
if (tagName === "script") {
|
||||
const mockScript = originalCreateElement.call(document, "script");
|
||||
Object.defineProperty(mockScript, "textContent", {
|
||||
set: () => {
|
||||
/* mock setter */
|
||||
},
|
||||
get: () => "",
|
||||
});
|
||||
return mockScript;
|
||||
}
|
||||
return originalCreateElement.call(document, tagName);
|
||||
});
|
||||
|
||||
vi.spyOn(document.head, "appendChild").mockImplementation(() => document.head);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
// @ts-ignore
|
||||
delete window.formbricksSurveys;
|
||||
});
|
||||
|
||||
test("renders a container with the correct ID", () => {
|
||||
const { container } = render(
|
||||
<SurveyInline
|
||||
survey={{ id: "survey1" } as any}
|
||||
styling={{}}
|
||||
isBrandingEnabled={true}
|
||||
languageCode="en-US"
|
||||
/>
|
||||
);
|
||||
|
||||
const surveyContainer = container.querySelector('[id^="formbricks-survey-container"]');
|
||||
expect(surveyContainer).toBeInTheDocument();
|
||||
expect(surveyContainer).toHaveClass("h-full");
|
||||
expect(surveyContainer).toHaveClass("w-full");
|
||||
});
|
||||
|
||||
test("calls renderSurvey with correct props when formbricksSurveys is available", async () => {
|
||||
const mockSurvey = { id: "survey1" };
|
||||
|
||||
render(
|
||||
<SurveyInline survey={mockSurvey as any} styling={{}} isBrandingEnabled={true} languageCode="en-US" />
|
||||
);
|
||||
|
||||
// Verify the mock was called with correct props
|
||||
expect(mockRenderSurvey).toHaveBeenCalled();
|
||||
|
||||
const callArgs = mockRenderSurvey.mock.calls[0][0];
|
||||
expect(callArgs.survey).toBe(mockSurvey);
|
||||
expect(callArgs.mode).toBe("inline");
|
||||
expect(callArgs.containerId).toMatch(/formbricks-survey-container/);
|
||||
});
|
||||
|
||||
test("doesn't load recaptcha script when isSpamProtectionEnabled is false", async () => {
|
||||
const loadRecaptchaScriptMock = vi.mocked(recaptchaModule.loadRecaptchaScript);
|
||||
loadRecaptchaScriptMock.mockClear(); // Reset mock call counts
|
||||
|
||||
render(
|
||||
<SurveyInline
|
||||
survey={{ id: "survey1" } as any}
|
||||
isSpamProtectionEnabled={false}
|
||||
styling={{}}
|
||||
isBrandingEnabled={true}
|
||||
languageCode="en-US"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(loadRecaptchaScriptMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles script loading error gracefully", async () => {
|
||||
// Remove formbricksSurveys to test script loading
|
||||
// @ts-ignore
|
||||
delete window.formbricksSurveys;
|
||||
|
||||
// Mock fetch to reject
|
||||
vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Failed to load script"));
|
||||
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
render(
|
||||
<SurveyInline
|
||||
survey={{ id: "survey1" } as any}
|
||||
styling={{}}
|
||||
isBrandingEnabled={true}
|
||||
languageCode="en-US"
|
||||
/>
|
||||
);
|
||||
|
||||
// Wait for the error to be logged
|
||||
await vi.waitFor(() => {
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Failed to load the surveys package: ", expect.any(Error));
|
||||
});
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("provides a getRecaptchaToken function to the survey renderer", async () => {
|
||||
const executeRecaptchaMock = vi.mocked(recaptchaModule.executeRecaptcha);
|
||||
executeRecaptchaMock.mockClear(); // Reset mock call counts
|
||||
|
||||
render(
|
||||
<SurveyInline
|
||||
survey={{ id: "survey1" } as any}
|
||||
recaptchaSiteKey="test-site-key"
|
||||
styling={{}}
|
||||
isBrandingEnabled={true}
|
||||
languageCode="en-US"
|
||||
/>
|
||||
);
|
||||
|
||||
// Verify the mock was called with the right function
|
||||
expect(mockRenderSurvey).toHaveBeenCalled();
|
||||
|
||||
// Get the getRecaptchaToken function from the props
|
||||
const callArgs = mockRenderSurvey.mock.calls[0][0];
|
||||
expect(callArgs.getRecaptchaToken).toBeDefined();
|
||||
|
||||
// Call the function to verify it works
|
||||
await callArgs.getRecaptchaToken();
|
||||
expect(executeRecaptchaMock).toHaveBeenCalledWith("test-site-key");
|
||||
});
|
||||
});
|
||||
112
apps/web/modules/ui/components/switch/index.test.tsx
Normal file
112
apps/web/modules/ui/components/switch/index.test.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { Switch } from "./index";
|
||||
|
||||
// Mock radix-ui components
|
||||
vi.mock("@radix-ui/react-switch", () => ({
|
||||
Root: ({ className, checked, onCheckedChange, disabled, id, "aria-label": ariaLabel }: any) => (
|
||||
<button
|
||||
data-testid="switch-root"
|
||||
className={className}
|
||||
data-state={checked ? "checked" : "unchecked"}
|
||||
onClick={() => !disabled && onCheckedChange && onCheckedChange(!checked)}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
aria-label={ariaLabel}>
|
||||
<span
|
||||
data-testid="switch-thumb"
|
||||
className="pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
data-state={checked ? "checked" : "unchecked"}
|
||||
/>
|
||||
</button>
|
||||
),
|
||||
Thumb: ({ className, checked }: any) => (
|
||||
<span data-testid="switch-thumb" className={className} data-state={checked ? "checked" : "unchecked"} />
|
||||
),
|
||||
}));
|
||||
|
||||
describe("Switch", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders default switch correctly", () => {
|
||||
render(<Switch />);
|
||||
|
||||
const switchRoot = screen.getByTestId("switch-root");
|
||||
expect(switchRoot).toBeInTheDocument();
|
||||
|
||||
// Check default state classes
|
||||
expect(switchRoot).toHaveClass("peer");
|
||||
expect(switchRoot).toHaveClass("inline-flex");
|
||||
expect(switchRoot).toHaveClass("rounded-full");
|
||||
expect(switchRoot).toHaveClass("border-2");
|
||||
|
||||
// Check default state (unchecked)
|
||||
expect(switchRoot).toHaveAttribute("data-state", "unchecked");
|
||||
|
||||
// Check thumb element
|
||||
const switchThumb = screen.getByTestId("switch-thumb");
|
||||
expect(switchThumb).toBeInTheDocument();
|
||||
expect(switchThumb).toHaveAttribute("data-state", "unchecked");
|
||||
});
|
||||
|
||||
test("applies custom className", () => {
|
||||
render(<Switch className="custom-class" />);
|
||||
|
||||
const switchRoot = screen.getByTestId("switch-root");
|
||||
expect(switchRoot).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
test("renders in checked state", () => {
|
||||
render(<Switch checked />);
|
||||
|
||||
const switchRoot = screen.getByTestId("switch-root");
|
||||
expect(switchRoot).toHaveAttribute("data-state", "checked");
|
||||
|
||||
const switchThumb = screen.getByTestId("switch-thumb");
|
||||
expect(switchThumb).toHaveAttribute("data-state", "checked");
|
||||
});
|
||||
|
||||
test("renders in disabled state", () => {
|
||||
render(<Switch disabled />);
|
||||
|
||||
const switchRoot = screen.getByTestId("switch-root");
|
||||
expect(switchRoot).toBeDisabled();
|
||||
});
|
||||
|
||||
test("handles onChange callback", async () => {
|
||||
const handleChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Switch onCheckedChange={handleChange} />);
|
||||
|
||||
const switchRoot = screen.getByTestId("switch-root");
|
||||
await user.click(switchRoot);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
expect(handleChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("doesn't trigger onChange when disabled", async () => {
|
||||
const handleChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Switch disabled onCheckedChange={handleChange} />);
|
||||
|
||||
const switchRoot = screen.getByTestId("switch-root");
|
||||
await user.click(switchRoot);
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("passes props correctly", () => {
|
||||
render(<Switch id="test-switch" aria-label="Toggle" name="toggle" />);
|
||||
|
||||
const switchRoot = screen.getByTestId("switch-root");
|
||||
expect(switchRoot).toHaveAttribute("id", "test-switch");
|
||||
expect(switchRoot).toHaveAttribute("aria-label", "Toggle");
|
||||
});
|
||||
});
|
||||
97
apps/web/modules/ui/components/tab-bar/index.test.tsx
Normal file
97
apps/web/modules/ui/components/tab-bar/index.test.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TabBar } from "./index";
|
||||
|
||||
describe("TabBar", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockTabs = [
|
||||
{ id: "tab1", label: "Tab One" },
|
||||
{ id: "tab2", label: "Tab Two" },
|
||||
{ id: "tab3", label: "Tab Three" },
|
||||
];
|
||||
|
||||
test("calls setActiveId when tab is clicked", async () => {
|
||||
const handleSetActiveId = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TabBar tabs={mockTabs} activeId="tab1" setActiveId={handleSetActiveId} />);
|
||||
|
||||
await user.click(screen.getByText("Tab Two"));
|
||||
|
||||
expect(handleSetActiveId).toHaveBeenCalledTimes(1);
|
||||
expect(handleSetActiveId).toHaveBeenCalledWith("tab2");
|
||||
});
|
||||
|
||||
test("renders tabs with icons", () => {
|
||||
const tabsWithIcons = [
|
||||
{ id: "tab1", label: "Tab One", icon: <span data-testid="icon1">🔍</span> },
|
||||
{ id: "tab2", label: "Tab Two", icon: <span data-testid="icon2">📁</span> },
|
||||
];
|
||||
|
||||
render(<TabBar tabs={tabsWithIcons} activeId="tab1" setActiveId={() => {}} />);
|
||||
|
||||
expect(screen.getByTestId("icon1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("icon2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies custom className", () => {
|
||||
const { container } = render(
|
||||
<TabBar tabs={mockTabs} activeId="tab1" setActiveId={() => {}} className="custom-class" />
|
||||
);
|
||||
|
||||
const tabContainer = container.firstChild as HTMLElement;
|
||||
expect(tabContainer).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
test("applies activeTabClassName to active tab", () => {
|
||||
render(
|
||||
<TabBar
|
||||
tabs={mockTabs}
|
||||
activeId="tab1"
|
||||
setActiveId={() => {}}
|
||||
activeTabClassName="custom-active-class"
|
||||
/>
|
||||
);
|
||||
|
||||
const activeTab = screen.getByText("Tab One").closest("button");
|
||||
expect(activeTab).toHaveClass("custom-active-class");
|
||||
});
|
||||
|
||||
test("renders in disabled state", async () => {
|
||||
const handleSetActiveId = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={mockTabs}
|
||||
activeId="tab1"
|
||||
setActiveId={handleSetActiveId}
|
||||
tabStyle="button"
|
||||
disabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const navContainer = screen.getByRole("navigation");
|
||||
expect(navContainer).toHaveClass("cursor-not-allowed");
|
||||
expect(navContainer).toHaveClass("opacity-50");
|
||||
|
||||
await user.click(screen.getByText("Tab Two"));
|
||||
|
||||
expect(handleSetActiveId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("doesn't apply disabled styles when not disabled", () => {
|
||||
render(
|
||||
<TabBar tabs={mockTabs} activeId="tab1" setActiveId={() => {}} tabStyle="button" disabled={false} />
|
||||
);
|
||||
|
||||
const navContainer = screen.getByRole("navigation");
|
||||
expect(navContainer).not.toHaveClass("cursor-not-allowed");
|
||||
expect(navContainer).not.toHaveClass("opacity-50");
|
||||
});
|
||||
});
|
||||
121
apps/web/modules/ui/components/tab-toggle/index.test.tsx
Normal file
121
apps/web/modules/ui/components/tab-toggle/index.test.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TabToggle } from "./index";
|
||||
|
||||
describe("TabToggle", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockOptions = [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
];
|
||||
|
||||
test("renders all options correctly", () => {
|
||||
render(<TabToggle id="test" options={mockOptions} onChange={() => {}} />);
|
||||
|
||||
expect(screen.getByLabelText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Option 2")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Option 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("selects default option when provided", () => {
|
||||
render(<TabToggle id="test" options={mockOptions} defaultSelected="option2" onChange={() => {}} />);
|
||||
|
||||
const option1Radio = screen.getByLabelText("Option 1") as HTMLInputElement;
|
||||
const option2Radio = screen.getByLabelText("Option 2") as HTMLInputElement;
|
||||
const option3Radio = screen.getByLabelText("Option 3") as HTMLInputElement;
|
||||
|
||||
expect(option1Radio.checked).toBe(false);
|
||||
expect(option2Radio.checked).toBe(true);
|
||||
expect(option3Radio.checked).toBe(false);
|
||||
});
|
||||
|
||||
test("calls onChange handler when option is selected", async () => {
|
||||
const handleChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TabToggle id="test" options={mockOptions} onChange={handleChange} />);
|
||||
|
||||
await user.click(screen.getByLabelText("Option 2"));
|
||||
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
expect(handleChange).toHaveBeenCalledWith("option2");
|
||||
});
|
||||
|
||||
test("displays option labels correctly", () => {
|
||||
render(<TabToggle id="test" options={mockOptions} onChange={() => {}} />);
|
||||
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies correct styling to selected option", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TabToggle id="test" options={mockOptions} onChange={() => {}} />);
|
||||
|
||||
const option2Label = screen.getByText("Option 2").closest("label");
|
||||
expect(option2Label).not.toHaveClass("bg-white");
|
||||
|
||||
await user.click(screen.getByLabelText("Option 2"));
|
||||
|
||||
expect(option2Label).toHaveClass("bg-white");
|
||||
});
|
||||
|
||||
test("renders in disabled state", () => {
|
||||
render(<TabToggle id="test" options={mockOptions} onChange={() => {}} disabled={true} />);
|
||||
|
||||
const option1Radio = screen.getByLabelText("Option 1") as HTMLInputElement;
|
||||
const option2Radio = screen.getByLabelText("Option 2") as HTMLInputElement;
|
||||
const option3Radio = screen.getByLabelText("Option 3") as HTMLInputElement;
|
||||
|
||||
expect(option1Radio).toBeDisabled();
|
||||
expect(option2Radio).toBeDisabled();
|
||||
expect(option3Radio).toBeDisabled();
|
||||
|
||||
const labels = screen.getAllByRole("radio").map((radio) => radio.closest("label"));
|
||||
labels.forEach((label) => {
|
||||
expect(label).toHaveClass("cursor-not-allowed");
|
||||
expect(label).toHaveClass("opacity-50");
|
||||
});
|
||||
});
|
||||
|
||||
test("doesn't call onChange when disabled", async () => {
|
||||
const handleChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TabToggle id="test" options={mockOptions} onChange={handleChange} disabled={true} />);
|
||||
|
||||
await user.click(screen.getByLabelText("Option 2"));
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders with number values", () => {
|
||||
const numberOptions = [
|
||||
{ value: 1, label: "One" },
|
||||
{ value: 2, label: "Two" },
|
||||
];
|
||||
|
||||
render(<TabToggle id="test" options={numberOptions} defaultSelected={1} onChange={() => {}} />);
|
||||
|
||||
const option1Radio = screen.getByLabelText("One") as HTMLInputElement;
|
||||
const option2Radio = screen.getByLabelText("Two") as HTMLInputElement;
|
||||
|
||||
expect(option1Radio.checked).toBe(true);
|
||||
expect(option2Radio.checked).toBe(false);
|
||||
});
|
||||
|
||||
test("sets correct aria attributes", () => {
|
||||
render(<TabToggle id="test-id" options={mockOptions} onChange={() => {}} />);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
expect(radioGroup).toHaveAttribute("aria-labelledby", "test-id-toggle-label");
|
||||
});
|
||||
});
|
||||
202
apps/web/modules/ui/components/table/index.test.tsx
Normal file
202
apps/web/modules/ui/components/table/index.test.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "./index";
|
||||
|
||||
describe("Table", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders table correctly", () => {
|
||||
render(<Table data-testid="test-table" />);
|
||||
|
||||
const table = screen.getByTestId("test-table");
|
||||
expect(table).toBeInTheDocument();
|
||||
expect(table.tagName).toBe("TABLE");
|
||||
expect(table).toHaveClass("w-full");
|
||||
expect(table).toHaveClass("caption-bottom");
|
||||
expect(table).toHaveClass("text-sm");
|
||||
});
|
||||
|
||||
test("applies custom className to Table", () => {
|
||||
render(<Table className="custom-class" data-testid="test-table" />);
|
||||
|
||||
const table = screen.getByTestId("test-table");
|
||||
expect(table).toHaveClass("custom-class");
|
||||
expect(table).toHaveClass("w-full");
|
||||
});
|
||||
|
||||
test("renders TableHeader correctly", () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableHeader data-testid="test-header">
|
||||
<TableRow>
|
||||
<TableHead>Header</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const header = screen.getByTestId("test-header");
|
||||
expect(header).toBeInTheDocument();
|
||||
expect(header.tagName).toBe("THEAD");
|
||||
expect(header).toHaveClass("pointer-events-none");
|
||||
expect(header).toHaveClass("text-slate-800");
|
||||
});
|
||||
|
||||
test("renders TableBody correctly", () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableBody data-testid="test-body">
|
||||
<TableRow>
|
||||
<TableCell>Cell</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const body = screen.getByTestId("test-body");
|
||||
expect(body).toBeInTheDocument();
|
||||
expect(body.tagName).toBe("TBODY");
|
||||
});
|
||||
|
||||
test("renders TableFooter correctly", () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableFooter data-testid="test-footer">
|
||||
<TableRow>
|
||||
<TableCell>Footer</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const footer = screen.getByTestId("test-footer");
|
||||
expect(footer).toBeInTheDocument();
|
||||
expect(footer.tagName).toBe("TFOOT");
|
||||
expect(footer).toHaveClass("border-t");
|
||||
});
|
||||
|
||||
test("renders TableRow correctly", () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow data-testid="test-row">
|
||||
<TableCell>Cell</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const row = screen.getByTestId("test-row");
|
||||
expect(row).toBeInTheDocument();
|
||||
expect(row.tagName).toBe("TR");
|
||||
expect(row).toHaveClass("border-b");
|
||||
expect(row).toHaveClass("bg-white");
|
||||
expect(row).toHaveClass("hover:bg-slate-100");
|
||||
});
|
||||
|
||||
test("renders TableHead correctly", () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead data-testid="test-head">Header</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const head = screen.getByTestId("test-head");
|
||||
expect(head).toBeInTheDocument();
|
||||
expect(head.tagName).toBe("TH");
|
||||
expect(head).toHaveClass("h-12");
|
||||
expect(head).toHaveClass("px-4");
|
||||
expect(head).toHaveClass("text-left");
|
||||
expect(head).toHaveClass("align-middle");
|
||||
});
|
||||
|
||||
test("renders TableCell correctly", () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell data-testid="test-cell">Cell</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const cell = screen.getByTestId("test-cell");
|
||||
expect(cell).toBeInTheDocument();
|
||||
expect(cell.tagName).toBe("TD");
|
||||
expect(cell).toHaveClass("p-4");
|
||||
expect(cell).toHaveClass("align-middle");
|
||||
});
|
||||
|
||||
test("renders TableCaption correctly", () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableCaption data-testid="test-caption">Caption</TableCaption>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const caption = screen.getByTestId("test-caption");
|
||||
expect(caption).toBeInTheDocument();
|
||||
expect(caption.tagName).toBe("CAPTION");
|
||||
expect(caption).toHaveClass("mt-4");
|
||||
expect(caption).toHaveClass("text-sm");
|
||||
expect(caption.textContent).toBe("Caption");
|
||||
});
|
||||
|
||||
test("renders full table structure correctly", () => {
|
||||
render(
|
||||
<Table data-testid="full-table">
|
||||
<TableCaption>A list of users</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>John Doe</TableCell>
|
||||
<TableCell>john@example.com</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Jane Smith</TableCell>
|
||||
<TableCell>jane@example.com</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2}>Total: 2 users</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const table = screen.getByTestId("full-table");
|
||||
expect(table).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("A list of users")).toBeInTheDocument();
|
||||
expect(screen.getByText("Name")).toBeInTheDocument();
|
||||
expect(screen.getByText("Email")).toBeInTheDocument();
|
||||
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||
expect(screen.getByText("john@example.com")).toBeInTheDocument();
|
||||
expect(screen.getByText("Jane Smith")).toBeInTheDocument();
|
||||
expect(screen.getByText("jane@example.com")).toBeInTheDocument();
|
||||
expect(screen.getByText("Total: 2 users")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
40
apps/web/modules/ui/components/tag/index.test.tsx
Normal file
40
apps/web/modules/ui/components/tag/index.test.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { Tag } from "./index";
|
||||
|
||||
describe("Tag", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders tag with correct name", () => {
|
||||
render(<Tag tagId="tag1" tagName="Test Tag" onDelete={() => {}} />);
|
||||
|
||||
expect(screen.getByText("Test Tag")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies highlight class when highlight prop is true", () => {
|
||||
const { container } = render(
|
||||
<Tag tagId="tag1" tagName="Test Tag" onDelete={() => {}} highlight={true} />
|
||||
);
|
||||
|
||||
const tagElement = container.firstChild as HTMLElement;
|
||||
expect(tagElement).toHaveClass("animate-shake");
|
||||
});
|
||||
|
||||
test("does not apply highlight class when highlight prop is false", () => {
|
||||
const { container } = render(
|
||||
<Tag tagId="tag1" tagName="Test Tag" onDelete={() => {}} highlight={false} />
|
||||
);
|
||||
|
||||
const tagElement = container.firstChild as HTMLElement;
|
||||
expect(tagElement).not.toHaveClass("animate-shake");
|
||||
});
|
||||
|
||||
test("does not render delete icon when allowDelete is false", () => {
|
||||
render(<Tag tagId="tag1" tagName="Test Tag" onDelete={() => {}} allowDelete={false} />);
|
||||
|
||||
expect(screen.queryByRole("img", { hidden: true })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
184
apps/web/modules/ui/components/tags-combobox/index.test.tsx
Normal file
184
apps/web/modules/ui/components/tags-combobox/index.test.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TagsCombobox } from "./index";
|
||||
|
||||
// Mock components
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, onClick, size }: any) => (
|
||||
<button data-testid="button" onClick={onClick} data-size={size}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/command", () => ({
|
||||
Command: ({ children, filter }: any) => (
|
||||
<div data-testid="command" data-filter-fn={filter ? "true" : "false"}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CommandGroup: ({ children }: any) => <div data-testid="command-group">{children}</div>,
|
||||
CommandInput: ({ placeholder, value, onValueChange, onKeyDown }: any) => (
|
||||
<input
|
||||
data-testid="command-input"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
),
|
||||
CommandItem: ({ children, value, onSelect, className }: any) => (
|
||||
<div data-testid="command-item" data-value={value} onClick={() => onSelect(value)} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CommandList: ({ children }: any) => <div data-testid="command-list">{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/popover", () => ({
|
||||
Popover: ({ children, open }: any) => (
|
||||
<div data-testid="popover" data-open={open}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
PopoverContent: ({ children, className }: any) => (
|
||||
<div data-testid="popover-content" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
PopoverTrigger: ({ children, asChild }: any) => (
|
||||
<div data-testid="popover-trigger" data-as-child={asChild}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock tolgee
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"environments.project.tags.add_tag": "Add tag",
|
||||
"environments.project.tags.search_tags": "Search tags",
|
||||
"environments.project.tags.add": "Add",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("TagsCombobox", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockTags = [
|
||||
{ label: "Tag1", value: "tag1" },
|
||||
{ label: "Tag2", value: "tag2" },
|
||||
{ label: "Tag3", value: "tag3" },
|
||||
];
|
||||
|
||||
const mockCurrentTags = [{ label: "Tag1", value: "tag1" }];
|
||||
|
||||
const mockProps = {
|
||||
tags: mockTags,
|
||||
currentTags: mockCurrentTags,
|
||||
addTag: vi.fn(),
|
||||
createTag: vi.fn(),
|
||||
searchValue: "",
|
||||
setSearchValue: vi.fn(),
|
||||
open: false,
|
||||
setOpen: vi.fn(),
|
||||
};
|
||||
|
||||
test("renders with default props", () => {
|
||||
render(<TagsCombobox {...mockProps} />);
|
||||
|
||||
expect(screen.getByTestId("popover")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("popover-trigger")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("button")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("button")).toHaveTextContent("Add tag");
|
||||
});
|
||||
|
||||
test("renders popover content when open is true", () => {
|
||||
render(<TagsCombobox {...mockProps} open={true} />);
|
||||
|
||||
expect(screen.getByTestId("popover")).toHaveAttribute("data-open", "true");
|
||||
expect(screen.getByTestId("popover-content")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("command")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("command-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("command-list")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows available tags excluding current tags", () => {
|
||||
render(<TagsCombobox {...mockProps} open={true} />);
|
||||
|
||||
const commandItems = screen.getAllByTestId("command-item");
|
||||
expect(commandItems).toHaveLength(2); // Should show Tag2 and Tag3 but not Tag1 (which is in currentTags)
|
||||
expect(commandItems[0]).toHaveAttribute("data-value", "tag2");
|
||||
expect(commandItems[1]).toHaveAttribute("data-value", "tag3");
|
||||
});
|
||||
|
||||
test("calls addTag when a tag is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
const addTagMock = vi.fn();
|
||||
const setOpenMock = vi.fn();
|
||||
|
||||
render(<TagsCombobox {...mockProps} open={true} addTag={addTagMock} setOpen={setOpenMock} />);
|
||||
|
||||
const tag2Item = screen.getAllByTestId("command-item")[0];
|
||||
await user.click(tag2Item);
|
||||
|
||||
expect(addTagMock).toHaveBeenCalledWith("tag2");
|
||||
expect(setOpenMock).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("calls createTag when Enter is pressed with a new tag", async () => {
|
||||
const user = userEvent.setup();
|
||||
const createTagMock = vi.fn();
|
||||
|
||||
render(<TagsCombobox {...mockProps} open={true} searchValue="NewTag" createTag={createTagMock} />);
|
||||
|
||||
const input = screen.getByTestId("command-input");
|
||||
await user.type(input, "{enter}");
|
||||
|
||||
expect(createTagMock).toHaveBeenCalledWith("NewTag");
|
||||
});
|
||||
|
||||
test("doesn't show create option when searchValue matches existing tag", () => {
|
||||
render(<TagsCombobox {...mockProps} open={true} searchValue="Tag2" />);
|
||||
|
||||
const commandItems = screen.getAllByTestId("command-item");
|
||||
expect(commandItems).toHaveLength(2); // Tag2 and Tag3
|
||||
expect(commandItems[0]).toHaveAttribute("data-value", "tag2");
|
||||
expect(screen.queryByRole("button", { name: /\+ Add Tag2/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("resets search value when closed", () => {
|
||||
const setSearchValueMock = vi.fn();
|
||||
const { rerender } = render(
|
||||
<TagsCombobox {...mockProps} open={true} searchValue="test" setSearchValue={setSearchValueMock} />
|
||||
);
|
||||
|
||||
// Change to closed state
|
||||
rerender(
|
||||
<TagsCombobox {...mockProps} open={false} searchValue="test" setSearchValue={setSearchValueMock} />
|
||||
);
|
||||
|
||||
expect(setSearchValueMock).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
test("updates placeholder based on available tags", () => {
|
||||
// With available tags
|
||||
const { rerender } = render(<TagsCombobox {...mockProps} open={true} />);
|
||||
|
||||
expect(screen.getByTestId("command-input")).toHaveAttribute("placeholder", "Search tags");
|
||||
|
||||
// Without available tags
|
||||
rerender(<TagsCombobox {...mockProps} open={true} tags={[]} />);
|
||||
|
||||
expect(screen.getByTestId("command-input")).toHaveAttribute("placeholder", "Add tag");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TBaseFilters, TSegment } from "@formbricks/types/segment";
|
||||
import { TargetingIndicator } from "./index";
|
||||
|
||||
// Mock tolgee
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"environments.surveys.edit.audience": "Audience",
|
||||
"environments.surveys.edit.targeted": "Targeted",
|
||||
"environments.surveys.edit.everyone": "Everyone",
|
||||
"environments.surveys.edit.only_people_who_match_your_targeting_can_be_surveyed":
|
||||
"Only people who match your targeting can be surveyed",
|
||||
"environments.surveys.edit.without_a_filter_all_of_your_users_can_be_surveyed":
|
||||
"Without a filter all of your users can be surveyed",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("TargetingIndicator", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders correctly with null segment", () => {
|
||||
render(<TargetingIndicator segment={null} />);
|
||||
|
||||
expect(screen.getByText("Audience:")).toBeInTheDocument();
|
||||
expect(screen.getByText("Everyone")).toBeInTheDocument();
|
||||
expect(screen.getByText("Without a filter all of your users can be surveyed")).toBeInTheDocument();
|
||||
|
||||
// Should show the filter icon when no targeting
|
||||
const filterIcon = document.querySelector("svg");
|
||||
expect(filterIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly with empty filters", () => {
|
||||
const emptySegment: TSegment = {
|
||||
id: "seg_123",
|
||||
environmentId: "env_123",
|
||||
title: "Test Segment",
|
||||
description: "A test segment",
|
||||
isPrivate: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
filters: [],
|
||||
surveys: [],
|
||||
};
|
||||
|
||||
render(<TargetingIndicator segment={emptySegment} />);
|
||||
|
||||
expect(screen.getByText("Audience:")).toBeInTheDocument();
|
||||
expect(screen.getByText("Everyone")).toBeInTheDocument();
|
||||
expect(screen.getByText("Without a filter all of your users can be surveyed")).toBeInTheDocument();
|
||||
|
||||
// Should show the filter icon when no targeting
|
||||
const filterIcon = document.querySelector("svg");
|
||||
expect(filterIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly with filters", () => {
|
||||
const segmentWithFilters: TSegment = {
|
||||
id: "seg_123",
|
||||
environmentId: "env_123",
|
||||
title: "Test Segment",
|
||||
description: "A test segment",
|
||||
isPrivate: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
filters: [
|
||||
{
|
||||
id: "filter_123",
|
||||
},
|
||||
] as unknown as TBaseFilters,
|
||||
surveys: [],
|
||||
};
|
||||
|
||||
render(<TargetingIndicator segment={segmentWithFilters} />);
|
||||
|
||||
expect(screen.getByText("Audience:")).toBeInTheDocument();
|
||||
expect(screen.getByText("Targeted")).toBeInTheDocument();
|
||||
expect(screen.getByText("Only people who match your targeting can be surveyed")).toBeInTheDocument();
|
||||
|
||||
// Should show the users icon when targeting is active
|
||||
const usersIcon = document.querySelector("svg");
|
||||
expect(usersIcon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,302 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { ThemeStylingPreviewSurvey } from "./index";
|
||||
|
||||
// Mock required components
|
||||
vi.mock("@/modules/ui/components/client-logo", () => ({
|
||||
ClientLogo: ({ projectLogo, previewSurvey }: any) => (
|
||||
<div data-testid="client-logo" data-preview={previewSurvey ? "true" : "false"}>
|
||||
{projectLogo?.url ? "Logo" : "No Logo"}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/media-background", () => ({
|
||||
MediaBackground: ({ children, isEditorView }: any) => (
|
||||
<div data-testid="media-background" data-editor={isEditorView ? "true" : "false"}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/preview-survey/components/modal", () => ({
|
||||
Modal: ({ children, isOpen, placement, darkOverlay, clickOutsideClose, previewMode }: any) => (
|
||||
<div
|
||||
data-testid="preview-modal"
|
||||
data-open={isOpen ? "true" : "false"}
|
||||
data-placement={placement}
|
||||
data-dark-overlay={darkOverlay ? "true" : "false"}
|
||||
data-click-outside-close={clickOutsideClose ? "true" : "false"}
|
||||
data-preview-mode={previewMode}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/reset-progress-button", () => ({
|
||||
ResetProgressButton: ({ onClick }: any) => (
|
||||
<button data-testid="reset-progress-button" onClick={onClick}>
|
||||
Reset Progress
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/survey", () => ({
|
||||
SurveyInline: ({ survey, isPreviewMode, isBrandingEnabled, languageCode }: any) => (
|
||||
<div
|
||||
data-testid="survey-inline"
|
||||
data-preview-mode={isPreviewMode ? "true" : "false"}
|
||||
data-survey-type={survey.type}
|
||||
data-branding-enabled={isBrandingEnabled ? "true" : "false"}
|
||||
data-language={languageCode}>
|
||||
Survey Content
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock framer-motion
|
||||
vi.mock("framer-motion", async () => {
|
||||
const actual = await vi.importActual("framer-motion");
|
||||
return {
|
||||
...actual,
|
||||
motion: {
|
||||
div: ({ children, className, animate }: any) => (
|
||||
<div data-testid="motion-div" data-animate={animate} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock tolgee
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"common.link_survey": "Link Survey",
|
||||
"common.app_survey": "App Survey",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("ThemeStylingPreviewSurvey", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
environmentId: "env1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
status: "draft",
|
||||
languages: {},
|
||||
projectOverwrites: {
|
||||
placement: "bottomRight",
|
||||
darkOverlay: true,
|
||||
clickOutsideClose: true,
|
||||
},
|
||||
} as TSurvey;
|
||||
|
||||
const mockProject = {
|
||||
id: "project1",
|
||||
name: "Test Project",
|
||||
placement: "center",
|
||||
darkOverlay: false,
|
||||
clickOutsideClose: false,
|
||||
inAppSurveyBranding: true,
|
||||
linkSurveyBranding: true,
|
||||
logo: { url: "http://example.com/logo.png" },
|
||||
styling: {
|
||||
roundness: 8,
|
||||
cardBackgroundColor: { light: "#ffffff" },
|
||||
isLogoHidden: false,
|
||||
},
|
||||
} as any;
|
||||
|
||||
test("renders correctly with link survey type", () => {
|
||||
const setPreviewType = vi.fn();
|
||||
|
||||
render(
|
||||
<ThemeStylingPreviewSurvey
|
||||
survey={mockSurvey}
|
||||
project={mockProject}
|
||||
previewType="link"
|
||||
setPreviewType={setPreviewType}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check if browser header elements are rendered
|
||||
expect(screen.getByText("Preview")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("reset-progress-button")).toBeInTheDocument();
|
||||
|
||||
// Check if MediaBackground is rendered for link survey
|
||||
const mediaBackground = screen.getByTestId("media-background");
|
||||
expect(mediaBackground).toBeInTheDocument();
|
||||
expect(mediaBackground).toHaveAttribute("data-editor", "true");
|
||||
|
||||
// Check if ClientLogo is rendered
|
||||
const clientLogo = screen.getByTestId("client-logo");
|
||||
expect(clientLogo).toBeInTheDocument();
|
||||
expect(clientLogo).toHaveAttribute("data-preview", "true");
|
||||
|
||||
// Check if SurveyInline is rendered with correct props
|
||||
const surveyInline = screen.getByTestId("survey-inline");
|
||||
expect(surveyInline).toBeInTheDocument();
|
||||
expect(surveyInline).toHaveAttribute("data-survey-type", "link");
|
||||
expect(surveyInline).toHaveAttribute("data-preview-mode", "true");
|
||||
expect(surveyInline).toHaveAttribute("data-branding-enabled", "true");
|
||||
|
||||
// Check if toggle buttons are rendered
|
||||
expect(screen.getByText("Link Survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("App Survey")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly with app survey type", () => {
|
||||
const setPreviewType = vi.fn();
|
||||
|
||||
render(
|
||||
<ThemeStylingPreviewSurvey
|
||||
survey={{ ...mockSurvey, type: "app" }}
|
||||
project={mockProject}
|
||||
previewType="app"
|
||||
setPreviewType={setPreviewType}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check if browser header elements are rendered
|
||||
expect(screen.getByText("Your web app")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("reset-progress-button")).toBeInTheDocument();
|
||||
|
||||
// Check if Modal is rendered for app survey
|
||||
const previewModal = screen.getByTestId("preview-modal");
|
||||
expect(previewModal).toBeInTheDocument();
|
||||
expect(previewModal).toHaveAttribute("data-open", "true");
|
||||
expect(previewModal).toHaveAttribute("data-placement", "bottomRight");
|
||||
expect(previewModal).toHaveAttribute("data-dark-overlay", "true");
|
||||
expect(previewModal).toHaveAttribute("data-click-outside-close", "true");
|
||||
expect(previewModal).toHaveAttribute("data-preview-mode", "desktop");
|
||||
|
||||
// Check if SurveyInline is rendered with correct props
|
||||
const surveyInline = screen.getByTestId("survey-inline");
|
||||
expect(surveyInline).toBeInTheDocument();
|
||||
expect(surveyInline).toHaveAttribute("data-survey-type", "app");
|
||||
expect(surveyInline).toHaveAttribute("data-preview-mode", "true");
|
||||
expect(surveyInline).toHaveAttribute("data-branding-enabled", "true");
|
||||
});
|
||||
|
||||
test("handles toggle between link and app survey types", async () => {
|
||||
const setPreviewType = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<ThemeStylingPreviewSurvey
|
||||
survey={mockSurvey}
|
||||
project={mockProject}
|
||||
previewType="link"
|
||||
setPreviewType={setPreviewType}
|
||||
/>
|
||||
);
|
||||
|
||||
// Click on App Survey button
|
||||
await user.click(screen.getByText("App Survey"));
|
||||
|
||||
// Check if setPreviewType was called with "app"
|
||||
expect(setPreviewType).toHaveBeenCalledWith("app");
|
||||
|
||||
// Clean up and reset
|
||||
cleanup();
|
||||
setPreviewType.mockClear();
|
||||
|
||||
// Render with app type
|
||||
render(
|
||||
<ThemeStylingPreviewSurvey
|
||||
survey={mockSurvey}
|
||||
project={mockProject}
|
||||
previewType="app"
|
||||
setPreviewType={setPreviewType}
|
||||
/>
|
||||
);
|
||||
|
||||
// Click on Link Survey button
|
||||
await user.click(screen.getByText("Link Survey"));
|
||||
|
||||
// Check if setPreviewType was called with "link"
|
||||
expect(setPreviewType).toHaveBeenCalledWith("link");
|
||||
});
|
||||
|
||||
test("handles reset progress button click", async () => {
|
||||
const setPreviewType = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<ThemeStylingPreviewSurvey
|
||||
survey={mockSurvey}
|
||||
project={mockProject}
|
||||
previewType="link"
|
||||
setPreviewType={setPreviewType}
|
||||
/>
|
||||
);
|
||||
|
||||
// Click the reset progress button
|
||||
await user.click(screen.getByTestId("reset-progress-button"));
|
||||
|
||||
// Check if a new survey component renders with a new key
|
||||
// Since we can't easily check the key directly, we can verify the content is still there
|
||||
expect(screen.getByTestId("survey-inline")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders without logo when isLogoHidden is true", () => {
|
||||
const setPreviewType = vi.fn();
|
||||
const projectWithHiddenLogo = {
|
||||
...mockProject,
|
||||
styling: {
|
||||
...mockProject.styling,
|
||||
isLogoHidden: true,
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<ThemeStylingPreviewSurvey
|
||||
survey={mockSurvey}
|
||||
project={projectWithHiddenLogo}
|
||||
previewType="link"
|
||||
setPreviewType={setPreviewType}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check that the logo is not rendered
|
||||
expect(screen.queryByTestId("client-logo")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("uses project settings when projectOverwrites are not provided", () => {
|
||||
const setPreviewType = vi.fn();
|
||||
const surveyWithoutOverwrites = {
|
||||
...mockSurvey,
|
||||
projectOverwrites: undefined,
|
||||
};
|
||||
|
||||
render(
|
||||
<ThemeStylingPreviewSurvey
|
||||
survey={surveyWithoutOverwrites as unknown as TSurvey}
|
||||
project={mockProject}
|
||||
previewType="app"
|
||||
setPreviewType={setPreviewType}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check if Modal uses project settings
|
||||
const previewModal = screen.getByTestId("preview-modal");
|
||||
expect(previewModal).toHaveAttribute("data-placement", "center");
|
||||
expect(previewModal).toHaveAttribute("data-dark-overlay", "false");
|
||||
expect(previewModal).toHaveAttribute("data-click-outside-close", "false");
|
||||
});
|
||||
});
|
||||
39
apps/web/modules/ui/components/toaster-client/index.test.tsx
Normal file
39
apps/web/modules/ui/components/toaster-client/index.test.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { ToasterClient } from "./index";
|
||||
|
||||
// Mock react-hot-toast
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
Toaster: ({ toastOptions }: any) => (
|
||||
<div data-testid="mock-toaster" data-toast-options={JSON.stringify(toastOptions)}>
|
||||
Mock Toaster
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("ToasterClient", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders the Toaster component", () => {
|
||||
const { getByTestId } = render(<ToasterClient />);
|
||||
|
||||
const toaster = getByTestId("mock-toaster");
|
||||
expect(toaster).toBeInTheDocument();
|
||||
expect(toaster).toHaveTextContent("Mock Toaster");
|
||||
});
|
||||
|
||||
test("passes the correct toast options to the Toaster", () => {
|
||||
const { getByTestId } = render(<ToasterClient />);
|
||||
|
||||
const toaster = getByTestId("mock-toaster");
|
||||
const toastOptions = JSON.parse(toaster.getAttribute("data-toast-options") || "{}");
|
||||
|
||||
expect(toastOptions).toHaveProperty("success");
|
||||
expect(toastOptions).toHaveProperty("error");
|
||||
expect(toastOptions.success).toHaveProperty("className", "formbricks__toast__success");
|
||||
expect(toastOptions.error).toHaveProperty("className", "formbricks__toast__error");
|
||||
});
|
||||
});
|
||||
196
apps/web/modules/ui/components/tooltip/index.test.tsx
Normal file
196
apps/web/modules/ui/components/tooltip/index.test.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipRenderer, TooltipTrigger } from "./index";
|
||||
|
||||
// Mock radix-ui tooltip
|
||||
vi.mock("@radix-ui/react-tooltip", () => ({
|
||||
Root: ({ children, ...props }: any) => (
|
||||
<div data-testid="tooltip-root" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Provider: ({ children, ...props }: any) => (
|
||||
<div data-testid="tooltip-provider" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Trigger: ({ children, asChild, ...props }: any) => (
|
||||
<div data-testid="tooltip-trigger" data-as-child={asChild} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Content: ({ children, sideOffset, className, ...props }: any) => (
|
||||
<div data-testid="tooltip-content" data-side-offset={sideOffset} className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Tooltip: ({ children, ...props }: any) => (
|
||||
<div data-testid="tooltip" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("Tooltip", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders basic tooltip components", () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||
<TooltipContent>Tooltip content</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tooltip-root")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tooltip-trigger")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tooltip-content")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Hover me")).toBeInTheDocument();
|
||||
expect(screen.getByText("Tooltip content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies correct default classes to TooltipContent", () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||
<TooltipContent>Tooltip content</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
const contentElement = screen.getByTestId("tooltip-content");
|
||||
expect(contentElement).toHaveClass("animate-in");
|
||||
expect(contentElement).toHaveClass("fade-in-50");
|
||||
expect(contentElement).toHaveClass("z-50");
|
||||
expect(contentElement).toHaveClass("rounded-md");
|
||||
expect(contentElement).toHaveClass("border");
|
||||
expect(contentElement).toHaveClass("border-slate-100");
|
||||
expect(contentElement).toHaveClass("bg-white");
|
||||
});
|
||||
|
||||
test("applies custom classes to TooltipContent", () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||
<TooltipContent className="custom-class">Tooltip content</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
const contentElement = screen.getByTestId("tooltip-content");
|
||||
expect(contentElement).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
test("accepts custom sideOffset prop", () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||
<TooltipContent sideOffset={10}>Tooltip content</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
const contentElement = screen.getByTestId("tooltip-content");
|
||||
expect(contentElement).toHaveAttribute("data-side-offset", "10");
|
||||
});
|
||||
|
||||
test("uses default sideOffset when not provided", () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||
<TooltipContent>Tooltip content</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
const contentElement = screen.getByTestId("tooltip-content");
|
||||
expect(contentElement).toHaveAttribute("data-side-offset", "4");
|
||||
});
|
||||
|
||||
test("sets asChild prop on trigger", () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Click me</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Tooltip content</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
const triggerElement = screen.getByTestId("tooltip-trigger");
|
||||
expect(triggerElement).toHaveAttribute("data-as-child", "true");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TooltipRenderer", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders tooltip with content", () => {
|
||||
render(
|
||||
<TooltipRenderer tooltipContent="Tooltip text" className="test-class">
|
||||
<button>Trigger</button>
|
||||
</TooltipRenderer>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Trigger")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tooltip-content")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tooltip-content")).toHaveTextContent("Tooltip text");
|
||||
expect(screen.getByTestId("tooltip-content")).toHaveClass("test-class");
|
||||
});
|
||||
|
||||
test("applies triggerClass to the trigger wrapper", () => {
|
||||
render(
|
||||
<TooltipRenderer tooltipContent="Tooltip text" triggerClass="trigger-class">
|
||||
<button>Trigger</button>
|
||||
</TooltipRenderer>
|
||||
);
|
||||
|
||||
const trigger = screen.getByTestId("tooltip-trigger").firstChild;
|
||||
expect(trigger).toHaveClass("trigger-class");
|
||||
});
|
||||
|
||||
test("doesn't render tooltip when shouldRender is false", () => {
|
||||
render(
|
||||
<TooltipRenderer tooltipContent="Tooltip text" shouldRender={false}>
|
||||
<button>Trigger</button>
|
||||
</TooltipRenderer>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Trigger")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("tooltip-content")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders tooltip with React node as content", () => {
|
||||
render(
|
||||
<TooltipRenderer
|
||||
tooltipContent={
|
||||
<div>
|
||||
Complex tooltip <strong>content</strong>
|
||||
</div>
|
||||
}>
|
||||
<button>Trigger</button>
|
||||
</TooltipRenderer>
|
||||
);
|
||||
|
||||
const tooltipContent = screen.getByTestId("tooltip-content");
|
||||
expect(tooltipContent).toBeInTheDocument();
|
||||
expect(tooltipContent.innerHTML).toContain("Complex tooltip ");
|
||||
expect(tooltipContent.innerHTML).toContain("<strong>content</strong>");
|
||||
});
|
||||
});
|
||||
158
apps/web/modules/ui/components/typography/index.test.tsx
Normal file
158
apps/web/modules/ui/components/typography/index.test.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { H1, H2, H3, H4, InlineCode, Large, Lead, List, Muted, P, Quote, Small } from "./index";
|
||||
|
||||
describe("Typography Components", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders H1 correctly", () => {
|
||||
const { container } = render(<H1>Heading 1</H1>);
|
||||
const h1Element = container.querySelector("h1");
|
||||
|
||||
expect(h1Element).toBeInTheDocument();
|
||||
expect(h1Element).toHaveTextContent("Heading 1");
|
||||
expect(h1Element?.className).toContain("text-4xl");
|
||||
expect(h1Element?.className).toContain("font-bold");
|
||||
expect(h1Element?.className).toContain("tracking-tight");
|
||||
expect(h1Element?.className).toContain("text-slate-800");
|
||||
});
|
||||
|
||||
test("renders H2 correctly", () => {
|
||||
const { container } = render(<H2>Heading 2</H2>);
|
||||
const h2Element = container.querySelector("h2");
|
||||
|
||||
expect(h2Element).toBeInTheDocument();
|
||||
expect(h2Element).toHaveTextContent("Heading 2");
|
||||
expect(h2Element?.className).toContain("text-3xl");
|
||||
expect(h2Element?.className).toContain("font-semibold");
|
||||
expect(h2Element?.className).toContain("border-b");
|
||||
expect(h2Element?.className).toContain("text-slate-800");
|
||||
});
|
||||
|
||||
test("renders H3 correctly", () => {
|
||||
const { container } = render(<H3>Heading 3</H3>);
|
||||
const h3Element = container.querySelector("h3");
|
||||
|
||||
expect(h3Element).toBeInTheDocument();
|
||||
expect(h3Element).toHaveTextContent("Heading 3");
|
||||
expect(h3Element?.className).toContain("text-2xl");
|
||||
expect(h3Element?.className).toContain("font-semibold");
|
||||
expect(h3Element?.className).toContain("text-slate-800");
|
||||
});
|
||||
|
||||
test("renders H4 correctly", () => {
|
||||
const { container } = render(<H4>Heading 4</H4>);
|
||||
const h4Element = container.querySelector("h4");
|
||||
|
||||
expect(h4Element).toBeInTheDocument();
|
||||
expect(h4Element).toHaveTextContent("Heading 4");
|
||||
expect(h4Element?.className).toContain("text-xl");
|
||||
expect(h4Element?.className).toContain("font-semibold");
|
||||
expect(h4Element?.className).toContain("text-slate-800");
|
||||
});
|
||||
|
||||
test("renders Lead correctly", () => {
|
||||
const { container } = render(<Lead>Lead paragraph</Lead>);
|
||||
const pElement = container.querySelector("p");
|
||||
|
||||
expect(pElement).toBeInTheDocument();
|
||||
expect(pElement).toHaveTextContent("Lead paragraph");
|
||||
expect(pElement?.className).toContain("text-xl");
|
||||
expect(pElement?.className).toContain("text-slate-800");
|
||||
});
|
||||
|
||||
test("renders P correctly", () => {
|
||||
const { container } = render(<P>Standard paragraph</P>);
|
||||
const pElement = container.querySelector("p");
|
||||
|
||||
expect(pElement).toBeInTheDocument();
|
||||
expect(pElement).toHaveTextContent("Standard paragraph");
|
||||
expect(pElement?.className).toContain("leading-7");
|
||||
});
|
||||
|
||||
test("renders Large correctly", () => {
|
||||
const { container } = render(<Large>Large text</Large>);
|
||||
const divElement = container.querySelector("div");
|
||||
|
||||
expect(divElement).toBeInTheDocument();
|
||||
expect(divElement).toHaveTextContent("Large text");
|
||||
expect(divElement?.className).toContain("text-lg");
|
||||
expect(divElement?.className).toContain("font-semibold");
|
||||
});
|
||||
|
||||
test("renders Small correctly", () => {
|
||||
const { container } = render(<Small>Small text</Small>);
|
||||
const pElement = container.querySelector("p");
|
||||
|
||||
expect(pElement).toBeInTheDocument();
|
||||
expect(pElement).toHaveTextContent("Small text");
|
||||
expect(pElement?.className).toContain("text-sm");
|
||||
expect(pElement?.className).toContain("font-medium");
|
||||
});
|
||||
|
||||
test("renders Muted correctly", () => {
|
||||
const { container } = render(<Muted>Muted text</Muted>);
|
||||
const spanElement = container.querySelector("span");
|
||||
|
||||
expect(spanElement).toBeInTheDocument();
|
||||
expect(spanElement).toHaveTextContent("Muted text");
|
||||
expect(spanElement?.className).toContain("text-sm");
|
||||
expect(spanElement?.className).toContain("text-muted-foreground");
|
||||
});
|
||||
|
||||
test("renders InlineCode correctly", () => {
|
||||
const { container } = render(<InlineCode>code</InlineCode>);
|
||||
const codeElement = container.querySelector("code");
|
||||
|
||||
expect(codeElement).toBeInTheDocument();
|
||||
expect(codeElement).toHaveTextContent("code");
|
||||
expect(codeElement?.className).toContain("font-mono");
|
||||
expect(codeElement?.className).toContain("text-sm");
|
||||
expect(codeElement?.className).toContain("font-semibold");
|
||||
});
|
||||
|
||||
test("renders List correctly", () => {
|
||||
const { container } = render(
|
||||
<List>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
</List>
|
||||
);
|
||||
const ulElement = container.querySelector("ul");
|
||||
const liElements = container.querySelectorAll("li");
|
||||
|
||||
expect(ulElement).toBeInTheDocument();
|
||||
expect(liElements.length).toBe(2);
|
||||
expect(ulElement?.className).toContain("list-disc");
|
||||
expect(liElements[0]).toHaveTextContent("Item 1");
|
||||
expect(liElements[1]).toHaveTextContent("Item 2");
|
||||
});
|
||||
|
||||
test("renders Quote correctly", () => {
|
||||
const { container } = render(<Quote>Quoted text</Quote>);
|
||||
const blockquoteElement = container.querySelector("blockquote");
|
||||
|
||||
expect(blockquoteElement).toBeInTheDocument();
|
||||
expect(blockquoteElement).toHaveTextContent("Quoted text");
|
||||
expect(blockquoteElement?.className).toContain("border-l-2");
|
||||
expect(blockquoteElement?.className).toContain("italic");
|
||||
});
|
||||
|
||||
test("applies custom className to components", () => {
|
||||
const { container } = render(<H1 className="custom-class">Custom Heading</H1>);
|
||||
const h1Element = container.querySelector("h1");
|
||||
|
||||
expect(h1Element).toHaveClass("custom-class");
|
||||
expect(h1Element).toHaveClass("text-4xl"); // Should still have default classes
|
||||
});
|
||||
|
||||
test("passes additional props to components", () => {
|
||||
const { container } = render(<H1 data-testid="test-heading">Test Heading</H1>);
|
||||
const h1Element = container.querySelector("h1");
|
||||
|
||||
expect(h1Element).toHaveAttribute("data-testid", "test-heading");
|
||||
});
|
||||
});
|
||||
132
apps/web/modules/ui/components/upgrade-prompt/index.test.tsx
Normal file
132
apps/web/modules/ui/components/upgrade-prompt/index.test.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { UpgradePrompt } from "./index";
|
||||
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, onClick, asChild, variant }: any) => (
|
||||
<button onClick={onClick} data-as-child={asChild ? "true" : "false"} data-variant={variant || "default"}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
KeyIcon: () => <div data-testid="key-icon" />,
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
__esModule: true,
|
||||
default: ({ href, children, target, rel }: any) => (
|
||||
<a href={href} target={target} rel={rel}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("UpgradePrompt", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockProps = {
|
||||
title: "Upgrade Your Account",
|
||||
description: "Get access to premium features by upgrading your account.",
|
||||
buttons: [
|
||||
{ text: "Upgrade Now", href: "/pricing" },
|
||||
{ text: "Learn More", href: "/features" },
|
||||
] as [any, any],
|
||||
};
|
||||
|
||||
test("renders component with correct content", () => {
|
||||
render(<UpgradePrompt {...mockProps} />);
|
||||
|
||||
// Check if title and description are rendered
|
||||
expect(screen.getByText("Upgrade Your Account")).toBeInTheDocument();
|
||||
expect(screen.getByText("Get access to premium features by upgrading your account.")).toBeInTheDocument();
|
||||
|
||||
// Check if the KeyIcon is rendered
|
||||
expect(screen.getByTestId("key-icon")).toBeInTheDocument();
|
||||
|
||||
// Check if buttons are rendered with correct text
|
||||
expect(screen.getByText("Upgrade Now")).toBeInTheDocument();
|
||||
expect(screen.getByText("Learn More")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders buttons with correct links", () => {
|
||||
render(<UpgradePrompt {...mockProps} />);
|
||||
|
||||
// Check if buttons have correct href attributes
|
||||
const primaryLink = screen.getByText("Upgrade Now").closest("a");
|
||||
const secondaryLink = screen.getByText("Learn More").closest("a");
|
||||
|
||||
expect(primaryLink).toHaveAttribute("href", "/pricing");
|
||||
expect(secondaryLink).toHaveAttribute("href", "/features");
|
||||
|
||||
// Check if links have correct attributes
|
||||
expect(primaryLink).toHaveAttribute("target", "_blank");
|
||||
expect(primaryLink).toHaveAttribute("rel", "noopener noreferrer");
|
||||
expect(secondaryLink).toHaveAttribute("target", "_blank");
|
||||
expect(secondaryLink).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
test("handles onClick for buttons without href", async () => {
|
||||
const primaryOnClick = vi.fn();
|
||||
const secondaryOnClick = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const propsWithClickHandlers = {
|
||||
...mockProps,
|
||||
buttons: [
|
||||
{ text: "Primary Action", onClick: primaryOnClick },
|
||||
{ text: "Secondary Action", onClick: secondaryOnClick },
|
||||
] as [any, any],
|
||||
};
|
||||
|
||||
render(<UpgradePrompt {...propsWithClickHandlers} />);
|
||||
|
||||
// Click the buttons and check if handlers are called
|
||||
await user.click(screen.getByText("Primary Action"));
|
||||
await user.click(screen.getByText("Secondary Action"));
|
||||
|
||||
expect(primaryOnClick).toHaveBeenCalledTimes(1);
|
||||
expect(secondaryOnClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("renders with mixed button types (href and onClick)", () => {
|
||||
const secondaryOnClick = vi.fn();
|
||||
|
||||
const mixedProps = {
|
||||
...mockProps,
|
||||
buttons: [
|
||||
{ text: "Primary Link", href: "/primary" },
|
||||
{ text: "Secondary Button", onClick: secondaryOnClick },
|
||||
] as [any, any],
|
||||
};
|
||||
|
||||
render(<UpgradePrompt {...mixedProps} />);
|
||||
|
||||
// Check primary button is a link
|
||||
const primaryButton = screen.getByText("Primary Link");
|
||||
expect(primaryButton.closest("a")).toHaveAttribute("href", "/primary");
|
||||
|
||||
// Check secondary button is not a link
|
||||
const secondaryButton = screen.getByText("Secondary Button");
|
||||
expect(secondaryButton.closest("a")).toBeNull();
|
||||
});
|
||||
|
||||
test("applies asChild and variant correctly to buttons", () => {
|
||||
render(<UpgradePrompt {...mockProps} />);
|
||||
|
||||
// In our mock, we're checking data attributes that represent the props
|
||||
const primaryButton = screen.getByText("Upgrade Now").closest("button");
|
||||
const secondaryButton = screen.getByText("Learn More").closest("button");
|
||||
|
||||
expect(primaryButton).toHaveAttribute("data-as-child", "true");
|
||||
expect(primaryButton).toHaveAttribute("data-variant", "default");
|
||||
|
||||
expect(secondaryButton).toHaveAttribute("data-as-child", "true");
|
||||
expect(secondaryButton).toHaveAttribute("data-variant", "secondary");
|
||||
});
|
||||
});
|
||||
@@ -61,6 +61,7 @@ export default defineConfig({
|
||||
"modules/setup/**/signup/**",
|
||||
"modules/setup/**/layout.tsx",
|
||||
"modules/survey/follow-ups/**",
|
||||
"modules/ui/components/icons/**",
|
||||
"app/share/**",
|
||||
"lib/shortUrl/**",
|
||||
"modules/ee/contacts/[contactId]/**",
|
||||
|
||||
@@ -274,6 +274,8 @@ export const ZSurveyPictureChoice = z.object({
|
||||
imageUrl: z.string(),
|
||||
});
|
||||
|
||||
export type TSurveyPictureChoice = z.infer<typeof ZSurveyPictureChoice>;
|
||||
|
||||
export type TSurveyQuestionChoice = z.infer<typeof ZSurveyQuestionChoice>;
|
||||
|
||||
// Logic types
|
||||
|
||||
@@ -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.*,**/*.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,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**
|
||||
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,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**
|
||||
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,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,apps/web/modules/ui/components/icons/**
|
||||
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,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,apps/web/modules/ui/components/icons/**
|
||||
Reference in New Issue
Block a user