Files
element-web/packages/shared-components/src/hooks/useListKeyboardNavigation.test.ts
T
Michael Telatynski 35fca4d339 Switch shared-components from jest & test-runner to vitest (#31800)
* Remove babel

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove duplicated patch-package dep

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to @fetch-mock/vitest

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests to import & call vitest functions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update test-utils imports

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update unit test snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch from jest->vitest for unit tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update visual test screenshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch from test-runner->vitest for visual tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update README

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update CI for shared-components unit & visual tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update yarn.lock

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update README

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix storybook trying to import vitest

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix css modules leaking between storybook tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tweak screenshot update script to accept args

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-22 14:17:36 +00:00

157 lines
4.9 KiB
TypeScript

/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type KeyboardEvent } from "react";
import { renderHook } from "@test-utils";
import { vi, describe, expect, it, beforeEach, afterEach } from "vitest";
import { useListKeyboardNavigation } from "./useListKeyboardNavigation";
describe("useListKeyDown", () => {
let mockList: HTMLUListElement;
let mockItems: HTMLElement[];
let mockEvent: Partial<KeyboardEvent<HTMLUListElement>>;
beforeEach(() => {
// Create mock DOM elements
mockList = document.createElement("ul");
mockItems = [document.createElement("li"), document.createElement("li"), document.createElement("li")];
// Set up the DOM structure
mockItems.forEach((item, index) => {
item.setAttribute("tabindex", "0");
item.setAttribute("data-testid", `item-${index}`);
mockList.appendChild(item);
});
document.body.appendChild(mockList);
// Mock event object
mockEvent = {
preventDefault: vi.fn(),
key: "",
};
// Mock focus methods
mockItems.forEach((item) => {
item.focus = vi.fn();
item.click = vi.fn();
});
});
afterEach(() => {
document.body.removeChild(mockList);
vi.clearAllMocks();
});
function render(): {
current: {
listRef: React.RefObject<HTMLUListElement | null>;
onKeyDown: React.KeyboardEventHandler<HTMLUListElement>;
onFocus: React.FocusEventHandler<HTMLUListElement>;
};
} {
const { result } = renderHook(() => useListKeyboardNavigation());
result.current.listRef.current = mockList;
return result;
}
it.each([
["Enter", "Enter"],
["Space", " "],
])("should handle %s key to click active element", (name, key) => {
const result = render();
// Mock document.activeElement
Object.defineProperty(document, "activeElement", {
value: mockItems[1],
configurable: true,
});
// Simulate key press
result.current.onKeyDown({
...mockEvent,
key,
} as KeyboardEvent<HTMLUListElement>);
expect(mockItems[1].click).toHaveBeenCalledTimes(1);
expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1);
});
it.each(
// key, finalPosition, startPosition
[
["ArrowDown", 1, 0],
["ArrowUp", 1, 2],
["Home", 0, 1],
["End", 2, 1],
],
)("should handle %s to focus the %inth element", (key, finalPosition, startPosition) => {
const result = render();
mockList.contains = vi.fn().mockReturnValue(true);
Object.defineProperty(document, "activeElement", {
value: mockItems[startPosition],
configurable: true,
});
result.current.onKeyDown({
...mockEvent,
key,
} as KeyboardEvent<HTMLUListElement>);
expect(mockItems[finalPosition].focus).toHaveBeenCalledTimes(1);
expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1);
});
it.each([["ArrowDown"], ["ArrowUp"]])("should not handle %s when active element is not in list", (key) => {
const result = render();
mockList.contains = vi.fn().mockReturnValue(false);
const outsideElement = document.createElement("button");
Object.defineProperty(document, "activeElement", {
value: outsideElement,
configurable: true,
});
result.current.onKeyDown({
...mockEvent,
key,
} as KeyboardEvent<HTMLUListElement>);
// No item should be focused
mockItems.forEach((item) => expect(item.focus).not.toHaveBeenCalled());
expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1);
});
it("should not prevent default for unhandled keys", () => {
const result = render();
result.current.onKeyDown({
...mockEvent,
key: "Tab",
} as KeyboardEvent<HTMLUListElement>);
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
});
it("should focus the first item if list itself is focused", () => {
const result = render();
result.current.onFocus({ target: mockList } as React.FocusEvent<HTMLUListElement>);
expect(mockItems[0].focus).toHaveBeenCalledTimes(1);
});
it("should focus the selected item if list itself is focused", () => {
mockItems[1].setAttribute("aria-selected", "true");
const result = render();
result.current.onFocus({ target: mockList } as React.FocusEvent<HTMLUListElement>);
expect(mockItems[1].focus).toHaveBeenCalledTimes(1);
});
});