mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
602 lines
18 KiB
TypeScript
602 lines
18 KiB
TypeScript
import type {UserEvent} from "@testing-library/user-event";
|
|
import type {TabsProps} from "../src";
|
|
|
|
import * as React from "react";
|
|
import {act, render, fireEvent, within, waitFor} from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import {focus} from "@heroui/test-utils";
|
|
import {spy, shouldIgnoreReactWarning} from "@heroui/test-utils";
|
|
import {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter} from "@heroui/modal";
|
|
import {Button} from "@heroui/button";
|
|
|
|
import {Tabs, Tab} from "../src";
|
|
|
|
type Item = {
|
|
id: string;
|
|
label: string;
|
|
content?: React.ReactNode;
|
|
};
|
|
|
|
let defaultItems: Item[] = [
|
|
{
|
|
id: "item1",
|
|
label: "Item1 ",
|
|
content: "Content 1",
|
|
},
|
|
{
|
|
id: "item2",
|
|
label: "Item 2",
|
|
content: "Content 2",
|
|
},
|
|
{
|
|
id: "item3",
|
|
label: "Item 3",
|
|
content: "Content 3",
|
|
},
|
|
];
|
|
|
|
function getPlacementTemplate(position: TabsProps["placement"]) {
|
|
return (
|
|
<Tabs aria-label="Tabs static test" data-testid="tabWrapper" placement={position}>
|
|
<Tab key="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>
|
|
);
|
|
}
|
|
|
|
describe("Tabs", () => {
|
|
let user: UserEvent;
|
|
|
|
beforeEach(() => {
|
|
user = userEvent.setup();
|
|
});
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it("should render correctly (static)", () => {
|
|
const wrapper = render(
|
|
<Tabs aria-label="Tabs static test">
|
|
<Tab key="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
expect(() => wrapper.unmount()).not.toThrow();
|
|
|
|
if (!shouldIgnoreReactWarning(spy)) {
|
|
expect(spy).toHaveBeenCalledTimes(0);
|
|
}
|
|
});
|
|
|
|
it("should render correctly (dynamic)", () => {
|
|
const wrapper = render(
|
|
<Tabs aria-label="Tabs static test" items={defaultItems}>
|
|
{(item) => (
|
|
<Tab key={item.id} title={item.label}>
|
|
<div>{item.content}</div>
|
|
</Tab>
|
|
)}
|
|
</Tabs>,
|
|
);
|
|
|
|
expect(() => wrapper.unmount()).not.toThrow();
|
|
});
|
|
|
|
it("renders property", () => {
|
|
const wrapper = render(
|
|
<Tabs aria-label="Tabs property test">
|
|
{defaultItems.map((item) => (
|
|
<Tab key={item.id} title={item.label}>
|
|
<div>{item.content}</div>
|
|
</Tab>
|
|
))}
|
|
</Tabs>,
|
|
);
|
|
const tablist = wrapper.getByRole("tablist");
|
|
|
|
expect(tablist).toBeTruthy();
|
|
const tabs = within(tablist).getAllByRole("tab");
|
|
|
|
expect(tabs.length).toBe(3);
|
|
|
|
for (let tab of tabs) {
|
|
expect(tab).toHaveAttribute("tabindex");
|
|
expect(tab).toHaveAttribute("aria-selected");
|
|
const isSelected = tab.getAttribute("aria-selected") === "true";
|
|
|
|
if (isSelected) {
|
|
expect(tab).toHaveAttribute("aria-controls");
|
|
const tabpanel = document.getElementById(tab.getAttribute("aria-controls")!);
|
|
|
|
expect(tabpanel).toBeTruthy();
|
|
expect(tabpanel).toHaveAttribute("aria-labelledby", tab.id);
|
|
expect(tabpanel).toHaveAttribute("role", "tabpanel");
|
|
expect(tabpanel).toHaveTextContent(defaultItems[0]?.content as string);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("ref should be forwarded", () => {
|
|
const ref = React.createRef<HTMLDivElement>();
|
|
|
|
render(
|
|
<Tabs ref={ref} aria-label="Tabs static test">
|
|
<Tab key="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
expect(ref.current).not.toBeNull();
|
|
});
|
|
|
|
test("should select the correct tab with keyboard navigation", async () => {
|
|
const wrapper = render(
|
|
<Tabs aria-label="Tabs static test">
|
|
<Tab key="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
const tab1 = wrapper.getByRole("tab", {name: "Item 1"});
|
|
const tab2 = wrapper.getByRole("tab", {name: "Item 2"});
|
|
const tab3 = wrapper.getByRole("tab", {name: "Item 3"});
|
|
|
|
expect(tab1).toHaveAttribute("aria-selected", "true");
|
|
expect(tab2).toHaveAttribute("aria-selected", "false");
|
|
expect(tab3).toHaveAttribute("aria-selected", "false");
|
|
|
|
act(() => {
|
|
focus(tab1);
|
|
});
|
|
|
|
await user.keyboard("[ArrowRight]");
|
|
expect(tab1).toHaveAttribute("aria-selected", "false");
|
|
expect(tab2).toHaveAttribute("aria-selected", "true");
|
|
expect(tab3).toHaveAttribute("aria-selected", "false");
|
|
|
|
await user.keyboard("[ArrowRight]");
|
|
expect(tab1).toHaveAttribute("aria-selected", "false");
|
|
expect(tab2).toHaveAttribute("aria-selected", "false");
|
|
expect(tab3).toHaveAttribute("aria-selected", "true");
|
|
|
|
await user.keyboard("[ArrowRight]");
|
|
expect(tab1).toHaveAttribute("aria-selected", "true");
|
|
expect(tab2).toHaveAttribute("aria-selected", "false");
|
|
expect(tab3).toHaveAttribute("aria-selected", "false");
|
|
});
|
|
|
|
test("should focus the correct tab with manual keyboard navigation", async () => {
|
|
const wrapper = render(
|
|
<Tabs aria-label="Tabs static test" keyboardActivation="manual">
|
|
<Tab key="item1" data-testid="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" data-testid="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" data-testid="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
const tab1 = wrapper.getByTestId("item1");
|
|
const tab2 = wrapper.getByTestId("item2");
|
|
const tab3 = wrapper.getByTestId("item3");
|
|
|
|
expect(tab1).toHaveAttribute("aria-selected", "true");
|
|
expect(tab2).toHaveAttribute("aria-selected", "false");
|
|
expect(tab3).toHaveAttribute("aria-selected", "false");
|
|
|
|
act(() => {
|
|
focus(tab1);
|
|
});
|
|
|
|
await user.keyboard("[ArrowRight]");
|
|
expect(tab2).toHaveFocus();
|
|
|
|
await user.keyboard("[ArrowRight]");
|
|
expect(tab3).toHaveFocus();
|
|
|
|
await user.keyboard("[ArrowLeft]");
|
|
expect(tab2).toHaveFocus();
|
|
|
|
expect(tab1).toHaveAttribute("aria-selected", "true");
|
|
expect(tab2).toHaveAttribute("aria-selected", "false");
|
|
expect(tab3).toHaveAttribute("aria-selected", "false");
|
|
});
|
|
|
|
it("it should work with defaultSelectedKey", () => {
|
|
const wrapper = render(
|
|
<Tabs aria-label="Tabs static test" defaultSelectedKey="item2">
|
|
<Tab key="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" data-testid="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
const tab2 = wrapper.getByTestId("item2");
|
|
|
|
expect(tab2).toHaveAttribute("aria-selected", "true");
|
|
});
|
|
|
|
it("should not select a tab when disabled", async () => {
|
|
const wrapper = render(
|
|
<Tabs aria-label="Tabs static test" disabledKeys={["item2"]}>
|
|
<Tab key="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" data-testid="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
const tab2 = wrapper.getByTestId("item2");
|
|
|
|
await user.click(tab2);
|
|
expect(tab2).toHaveAttribute("aria-selected", "false");
|
|
});
|
|
|
|
it("should change the position of the tabs", () => {
|
|
const wrapper = render(getPlacementTemplate("top"));
|
|
|
|
const tabWrapper = wrapper.getByTestId("tabWrapper").parentNode;
|
|
|
|
expect(tabWrapper).toHaveAttribute("data-placement", "top");
|
|
expect(tabWrapper).toHaveAttribute("data-vertical", "horizontal");
|
|
|
|
// Test bottom position
|
|
wrapper.rerender(getPlacementTemplate("bottom"));
|
|
|
|
expect(tabWrapper).toHaveAttribute("data-placement", "bottom");
|
|
expect(tabWrapper).toHaveAttribute("data-vertical", "horizontal");
|
|
|
|
// Test start position
|
|
wrapper.rerender(getPlacementTemplate("start"));
|
|
|
|
expect(tabWrapper).toHaveAttribute("data-placement", "start");
|
|
expect(tabWrapper).toHaveAttribute("data-vertical", "vertical");
|
|
|
|
// Test end position
|
|
wrapper.rerender(getPlacementTemplate("end"));
|
|
|
|
expect(tabWrapper).toHaveAttribute("data-placement", "end");
|
|
expect(tabWrapper).toHaveAttribute("data-vertical", "vertical");
|
|
});
|
|
|
|
it("should change the orientation of the tabs", () => {
|
|
const wrapper = render(
|
|
<Tabs isVertical aria-label="Tabs static test" data-testid="tabWrapper">
|
|
<Tab key="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
const tabWrapper = wrapper.getByTestId("tabWrapper").parentNode;
|
|
|
|
expect(tabWrapper).toHaveAttribute("data-placement", "start");
|
|
expect(tabWrapper).toHaveAttribute("data-vertical", "vertical");
|
|
|
|
// Test horizontal orientation
|
|
wrapper.rerender(
|
|
<Tabs aria-label="Tabs static test" data-testid="tabWrapper" isVertical={false}>
|
|
<Tab key="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
expect(tabWrapper).toHaveAttribute("data-placement", "top");
|
|
expect(tabWrapper).toHaveAttribute("data-vertical", "horizontal");
|
|
});
|
|
|
|
test("should destroy inactive tab panels", () => {
|
|
const {container} = render(
|
|
<Tabs aria-label="Tabs test (destroyInactiveTabPanel=true)">
|
|
<Tab key="tab1" data-testid="item1" title="Tab 1">
|
|
<input className="border-2" data-testid="input" id="firstTab" />
|
|
</Tab>
|
|
<Tab key="tab2" data-testid="item2" title="Tab 2">
|
|
<p id="secondTab">second tab content</p>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
expect(container.querySelectorAll("[data-slot='panel']")).toHaveLength(1);
|
|
});
|
|
|
|
test("should not destroy inactive tab panels", async () => {
|
|
const wrapper = render(
|
|
<Tabs aria-label="Tabs test (destroyInactiveTabPanel=false)" destroyInactiveTabPanel={false}>
|
|
<Tab key="tab1" data-testid="item1" title="Tab 1">
|
|
<input className="border-2" data-testid="input" id="firstTab" />
|
|
</Tab>
|
|
<Tab key="tab2" data-testid="item2" title="Tab 2">
|
|
<p id="secondTab">second tab content</p>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
const {container} = wrapper;
|
|
|
|
expect(container.querySelectorAll("[data-slot='panel']")).toHaveLength(2);
|
|
|
|
const tab1 = wrapper.getByTestId("item1");
|
|
const tab2 = wrapper.getByTestId("item2");
|
|
const input = wrapper.getByTestId("input");
|
|
|
|
fireEvent.change(input, {target: {value: "23"}});
|
|
|
|
expect(input).toHaveValue("23");
|
|
|
|
act(() => {
|
|
focus(tab1);
|
|
});
|
|
|
|
await user.keyboard("[ArrowRight]");
|
|
expect(tab2).toHaveFocus();
|
|
|
|
await user.keyboard("[ArrowLeft]");
|
|
expect(tab1).toHaveFocus();
|
|
|
|
expect(input).toHaveValue("23");
|
|
});
|
|
|
|
test("should forward ref to the tab item", () => {
|
|
const ref = React.createRef<HTMLButtonElement>();
|
|
|
|
render(
|
|
<Tabs aria-label="Tabs static test">
|
|
<Tab key="item1" tabRef={ref} title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
expect(ref.current).not.toBeNull();
|
|
});
|
|
|
|
it("Tab click should be handled", async () => {
|
|
const item1Click = jest.fn();
|
|
const item2Click = jest.fn();
|
|
const wrapper = render(
|
|
<Tabs>
|
|
<Tab key="item1" data-testid="item1" title="Item 1" onClick={item1Click}>
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" data-testid="item2" title="Item 2" onClick={item2Click}>
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
const tab1 = wrapper.getByTestId("item1");
|
|
const tab2 = wrapper.getByTestId("item2");
|
|
|
|
// Test initial state
|
|
expect(tab1).toHaveAttribute("aria-selected", "true");
|
|
expect(tab2).toHaveAttribute("aria-selected", "false");
|
|
|
|
// Test clicking tab2
|
|
await user.click(tab2);
|
|
expect(item2Click).toHaveBeenCalledTimes(1);
|
|
expect(tab1).toHaveAttribute("aria-selected", "false");
|
|
expect(tab2).toHaveAttribute("aria-selected", "true");
|
|
|
|
// Test clicking tab2 again
|
|
await user.click(tab2);
|
|
expect(item2Click).toHaveBeenCalledTimes(2);
|
|
expect(tab2).toHaveAttribute("aria-selected", "true");
|
|
});
|
|
|
|
it("should allow reopening modal with tabs without blocking", async () => {
|
|
const TestComponent = () => {
|
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
|
|
return (
|
|
<>
|
|
<Button data-testid="open-modal-btn" onPress={() => setIsOpen(true)}>
|
|
Open Modal
|
|
</Button>
|
|
<Modal data-testid="test-modal" isOpen={isOpen} onOpenChange={setIsOpen}>
|
|
<ModalContent>
|
|
{(onClose) => (
|
|
<>
|
|
<ModalHeader>Test Modal with Tabs</ModalHeader>
|
|
<ModalBody>
|
|
<Tabs aria-label="Test tabs" data-testid="modal-tabs">
|
|
<Tab key="tab1" data-testid="tab-1" title="Tab 1">
|
|
<div data-testid="tab1-content">Content for Tab 1</div>
|
|
</Tab>
|
|
<Tab key="tab2" data-testid="tab-2" title="Tab 2">
|
|
<div data-testid="tab2-content">Content for Tab 2</div>
|
|
</Tab>
|
|
<Tab key="tab3" data-testid="tab-3" title="Tab 3">
|
|
<div data-testid="tab3-content">Content for Tab 3</div>
|
|
</Tab>
|
|
</Tabs>
|
|
</ModalBody>
|
|
<ModalFooter>
|
|
<Button data-testid="close-modal-btn" onPress={onClose}>
|
|
Close
|
|
</Button>
|
|
</ModalFooter>
|
|
</>
|
|
)}
|
|
</ModalContent>
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const {getByTestId, getByRole, queryByRole} = render(<TestComponent />);
|
|
|
|
const openButton = getByTestId("open-modal-btn");
|
|
|
|
await act(async () => {
|
|
fireEvent.click(openButton);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
const modal = getByRole("dialog");
|
|
|
|
expect(modal).toBeInTheDocument();
|
|
});
|
|
|
|
const tabButtons = getByRole("dialog").querySelectorAll('[role="tab"]');
|
|
|
|
expect(tabButtons).toHaveLength(3);
|
|
|
|
await act(async () => {
|
|
fireEvent.click(tabButtons[1]);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(tabButtons[1]).toHaveAttribute("aria-selected", "true");
|
|
});
|
|
|
|
const closeButton = getByTestId("close-modal-btn");
|
|
|
|
await act(async () => {
|
|
fireEvent.click(closeButton);
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
expect(queryByRole("dialog")).not.toBeInTheDocument();
|
|
},
|
|
{timeout: 1000},
|
|
);
|
|
|
|
await act(async () => {
|
|
fireEvent.click(openButton);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
const modal = getByRole("dialog");
|
|
|
|
expect(modal).toBeInTheDocument();
|
|
});
|
|
|
|
const newTabButtons = getByRole("dialog").querySelectorAll('[role="tab"]');
|
|
|
|
expect(newTabButtons).toHaveLength(3);
|
|
|
|
await act(async () => {
|
|
fireEvent.click(newTabButtons[2]);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(newTabButtons[2]).toHaveAttribute("aria-selected", "true");
|
|
});
|
|
});
|
|
|
|
test("should have correct aria-orientation for vertical tabs", () => {
|
|
const wrapper = render(
|
|
<Tabs isVertical aria-label="Vertical tabs test">
|
|
<Tab key="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
const tablist = wrapper.getByRole("tablist");
|
|
|
|
expect(tablist).toHaveAttribute("aria-orientation", "vertical");
|
|
});
|
|
|
|
test("should navigate vertical tabs with ArrowUp and ArrowDown keys", async () => {
|
|
const wrapper = render(
|
|
<Tabs isVertical aria-label="Vertical tabs keyboard test">
|
|
<Tab key="item1" title="Item 1">
|
|
<div>Content 1</div>
|
|
</Tab>
|
|
<Tab key="item2" title="Item 2">
|
|
<div>Content 2</div>
|
|
</Tab>
|
|
<Tab key="item3" title="Item 3">
|
|
<div>Content 3</div>
|
|
</Tab>
|
|
</Tabs>,
|
|
);
|
|
|
|
const tab1 = wrapper.getByRole("tab", {name: "Item 1"});
|
|
const tab2 = wrapper.getByRole("tab", {name: "Item 2"});
|
|
const tab3 = wrapper.getByRole("tab", {name: "Item 3"});
|
|
|
|
act(() => {
|
|
focus(tab1);
|
|
});
|
|
|
|
await user.keyboard("[ArrowDown]");
|
|
expect(tab1).toHaveAttribute("aria-selected", "false");
|
|
expect(tab2).toHaveAttribute("aria-selected", "true");
|
|
expect(tab3).toHaveAttribute("aria-selected", "false");
|
|
|
|
await user.keyboard("[ArrowDown]");
|
|
expect(tab1).toHaveAttribute("aria-selected", "false");
|
|
expect(tab2).toHaveAttribute("aria-selected", "false");
|
|
expect(tab3).toHaveAttribute("aria-selected", "true");
|
|
|
|
await user.keyboard("[ArrowUp]");
|
|
expect(tab1).toHaveAttribute("aria-selected", "false");
|
|
expect(tab2).toHaveAttribute("aria-selected", "true");
|
|
expect(tab3).toHaveAttribute("aria-selected", "false");
|
|
});
|
|
});
|