/*
* Portions of this file are based on code from react-spectrum.
* Apache License Version 2.0, Copyright 2020 Adobe.
*
* Credits to the React Spectrum team:
* https://github.com/adobe/react-spectrum/blob/703ab7b4559ecd4fc611e7f2c0e758867990fe01/packages/@react-spectrum/tabs/test/Tabs.test.js
*/
import { createPointerEvent } from "@kobalte/tests";
import { fireEvent, render, within } from "@solidjs/testing-library";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
import * as Tabs from ".";
describe("Tabs", () => {
// Make userEvent work with jest fakeTimers
// See https://github.com/testing-library/user-event/issues/833#issuecomment-1013797822
const user = userEvent.setup({ delay: null });
const onValueChangeSpy = vi.fn();
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.clearAllMocks();
vi.clearAllTimers();
});
afterAll(() => {
vi.restoreAllMocks();
});
it("renders properly", async () => {
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
const tablist = getByRole("tablist");
expect(tablist).toBeTruthy();
expect(tablist).toHaveAttribute("aria-orientation", "horizontal");
const tabs = within(tablist).getAllByRole("tab");
expect(tabs.length).toBe(3);
for (const 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("Body 1");
}
}
});
it("allows user to change tab item select via left/right arrow keys with horizontal tabs", async () => {
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
const selectedItem = tabs[0];
expect(tablist).toHaveAttribute("aria-orientation", "horizontal");
expect(selectedItem).toHaveAttribute("aria-selected", "true");
selectedItem.focus();
fireEvent.keyDown(selectedItem, {
key: "ArrowRight",
code: 39,
charCode: 39,
});
await Promise.resolve();
const nextSelectedItem = tabs[1];
expect(nextSelectedItem).toHaveAttribute("aria-selected", "true");
fireEvent.keyDown(nextSelectedItem, {
key: "ArrowLeft",
code: 37,
charCode: 37,
});
await Promise.resolve();
expect(selectedItem).toHaveAttribute("aria-selected", "true");
/** Doesn't change selection because its horizontal tabs. */
fireEvent.keyDown(selectedItem, { key: "ArrowUp", code: 38, charCode: 38 });
await Promise.resolve();
expect(selectedItem).toHaveAttribute("aria-selected", "true");
fireEvent.keyDown(selectedItem, {
key: "ArrowDown",
code: 40,
charCode: 40,
});
await Promise.resolve();
expect(selectedItem).toHaveAttribute("aria-selected", "true");
});
it("allows user to change tab item select via up/down arrow keys with vertical tabs", async () => {
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
const selectedItem = tabs[0];
selectedItem.focus();
expect(tablist).toHaveAttribute("aria-orientation", "vertical");
/** Doesn't change selection because its vertical tabs. */
expect(selectedItem).toHaveAttribute("aria-selected", "true");
fireEvent.keyDown(selectedItem, {
key: "ArrowRight",
code: 39,
charCode: 39,
});
await Promise.resolve();
expect(selectedItem).toHaveAttribute("aria-selected", "true");
fireEvent.keyDown(selectedItem, {
key: "ArrowLeft",
code: 37,
charCode: 37,
});
await Promise.resolve();
expect(selectedItem).toHaveAttribute("aria-selected", "true");
const nextSelectedItem = tabs[1];
fireEvent.keyDown(selectedItem, {
key: "ArrowDown",
code: 40,
charCode: 40,
});
await Promise.resolve();
expect(nextSelectedItem).toHaveAttribute("aria-selected", "true");
fireEvent.keyDown(nextSelectedItem, {
key: "ArrowUp",
code: 38,
charCode: 38,
});
await Promise.resolve();
expect(selectedItem).toHaveAttribute("aria-selected", "true");
});
it("wraps focus from first to last/last to first item", async () => {
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
const firstItem = tabs[0];
firstItem.focus();
expect(tablist).toHaveAttribute("aria-orientation", "horizontal");
expect(firstItem).toHaveAttribute("aria-selected", "true");
fireEvent.keyDown(firstItem, { key: "ArrowLeft", code: 37, charCode: 37 });
await Promise.resolve();
const lastItem = tabs[tabs.length - 1];
expect(lastItem).toHaveAttribute("aria-selected", "true");
fireEvent.keyDown(lastItem, { key: "ArrowRight", code: 39, charCode: 39 });
await Promise.resolve();
expect(firstItem).toHaveAttribute("aria-selected", "true");
});
it("select last item via end key / select first item via home key", async () => {
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
const firstItem = tabs[0];
firstItem.focus();
expect(tablist).toHaveAttribute("aria-orientation", "horizontal");
expect(firstItem).toHaveAttribute("aria-selected", "true");
fireEvent.keyDown(firstItem, { key: "End", code: 35, charCode: 35 });
await Promise.resolve();
const lastItem = tabs[tabs.length - 1];
expect(lastItem).toHaveAttribute("aria-selected", "true");
fireEvent.keyDown(lastItem, { key: "Home", code: 36, charCode: 36 });
await Promise.resolve();
expect(firstItem).toHaveAttribute("aria-selected", "true");
});
it("does not select via left / right keys if 'activationMode' is manual, select on enter / spacebar", async () => {
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
const [firstItem, secondItem, thirdItem] = tabs;
firstItem.focus();
expect(firstItem).toHaveAttribute("aria-selected", "true");
fireEvent.keyDown(firstItem, { key: "ArrowRight", code: 39, charCode: 39 });
await Promise.resolve();
expect(secondItem).toHaveAttribute("aria-selected", "false");
expect(document.activeElement).toBe(secondItem);
fireEvent.keyDown(secondItem, {
key: "ArrowRight",
code: 39,
charCode: 39,
});
await Promise.resolve();
expect(thirdItem).toHaveAttribute("aria-selected", "false");
expect(document.activeElement).toBe(thirdItem);
fireEvent.keyDown(thirdItem, { key: "Enter", code: 13, charCode: 13 });
await Promise.resolve();
expect(firstItem).toHaveAttribute("aria-selected", "false");
expect(secondItem).toHaveAttribute("aria-selected", "false");
expect(thirdItem).toHaveAttribute("aria-selected", "true");
expect(onValueChangeSpy).toBeCalledTimes(1);
});
it("supports using click to change tab", async () => {
const onValueChangeSpy = vi.fn();
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
const [firstItem, secondItem] = tabs;
expect(firstItem).toHaveAttribute("aria-selected", "true");
fireEvent(
secondItem,
createPointerEvent("pointerdown", { pointerId: 1, pointerType: "mouse" }),
);
await Promise.resolve();
fireEvent(
secondItem,
createPointerEvent("pointerup", { pointerId: 1, pointerType: "mouse" }),
);
await Promise.resolve();
expect(secondItem).toHaveAttribute("aria-selected", "true");
expect(secondItem).toHaveAttribute("aria-controls");
const tabpanel = document.getElementById(
secondItem.getAttribute("aria-controls")!,
);
expect(tabpanel).toBeTruthy();
expect(tabpanel).toHaveAttribute("aria-labelledby", secondItem.id);
expect(tabpanel).toHaveAttribute("role", "tabpanel");
expect(tabpanel).toHaveTextContent("Body 2");
expect(onValueChangeSpy).toBeCalledTimes(1);
});
it.skipIf(process.env.GITHUB_ACTIONS)(
"should focus the selected tab when tabbing in for the first time",
async () => {
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
await user.tab();
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
expect(document.activeElement).toBe(tabs[1]);
},
);
it.skipIf(process.env.GITHUB_ACTIONS)(
"should not focus any tabs when isDisabled tabbing in for the first time",
async () => {
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
await user.tab();
const tabpanel = getByRole("tabpanel");
expect(document.activeElement).toBe(tabpanel);
},
);
it.skipIf(process.env.GITHUB_ACTIONS)(
"disabled tabs cannot be keyboard navigated to",
async () => {
const onValueChangeSpy = vi.fn();
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
await user.tab();
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
expect(document.activeElement).toBe(tabs[0]);
fireEvent.keyDown(tabs[1], { key: "ArrowRight" });
await Promise.resolve();
fireEvent.keyUp(tabs[1], { key: "ArrowRight" });
await Promise.resolve();
expect(onValueChangeSpy).toBeCalledWith("three");
},
);
it.skipIf(process.env.GITHUB_ACTIONS)(
"disabled tabs cannot be pressed",
async () => {
const onValueChangeSpy = vi.fn();
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
await user.tab();
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
expect(document.activeElement).toBe(tabs[0]);
await user.click(tabs[1]);
expect(onValueChangeSpy).not.toBeCalled();
},
);
it.skipIf(process.env.GITHUB_ACTIONS)(
"selects first tab if all tabs are disabled",
async () => {
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
await user.tab();
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
const tabpanel = getByRole("tabpanel");
expect(tabs[0]).toHaveAttribute("aria-selected", "true");
expect(onValueChangeSpy).toBeCalledWith("one");
expect(document.activeElement).toBe(tabpanel);
},
);
it.skip("tabpanel should have tabIndex=0 only when there are no focusable elements", async () => {
// TODO test create-presence
const { getByRole, getAllByRole } = render(() => (
One
Two
));
const tabs = getAllByRole("tab");
const [firstItem, secondItem] = tabs;
let tabpanel = getByRole("tabpanel");
expect(tabpanel).not.toHaveAttribute("tabindex");
fireEvent(
secondItem,
createPointerEvent("pointerdown", { pointerId: 1, pointerType: "mouse" }),
);
await Promise.resolve();
fireEvent(
secondItem,
createPointerEvent("pointerup", { pointerId: 1, pointerType: "mouse" }),
);
await Promise.resolve();
vi.runAllTimers();
tabpanel = getByRole("tabpanel");
expect(tabpanel).toHaveAttribute("tabindex", "0");
fireEvent(
firstItem,
createPointerEvent("pointerdown", { pointerId: 1, pointerType: "mouse" }),
);
await Promise.resolve();
fireEvent(
firstItem,
createPointerEvent("pointerup", { pointerId: 1, pointerType: "mouse" }),
);
await Promise.resolve();
vi.runAllTimers();
tabpanel = getByRole("tabpanel");
expect(tabpanel).not.toHaveAttribute("tabindex");
});
it("fires onValueChange when clicking on the current tab", async () => {
const { getByRole } = render(() => (
One
Two
Three
Body 1
Body 2
Body 3
));
const tablist = getByRole("tablist");
const tabs = within(tablist).getAllByRole("tab");
const firstItem = tabs[0];
expect(firstItem).toHaveAttribute("aria-selected", "true");
fireEvent(
firstItem,
createPointerEvent("pointerdown", { pointerId: 1, pointerType: "mouse" }),
);
await Promise.resolve();
expect(onValueChangeSpy).toBeCalledTimes(1);
expect(onValueChangeSpy).toHaveBeenCalledWith("one");
});
});