Prepare for repo merge

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-10-15 11:35:21 +01:00
parent 0f670b8dc0
commit b084ff2313
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
807 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,82 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 Šimon Brandner <simon.bra.ag@gmail.com>
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { mockPlatformPeg, unmockPlatformPeg } from "../test-utils";
const PATH_TO_KEYBOARD_SHORTCUTS = "../../src/accessibility/KeyboardShortcuts";
const PATH_TO_KEYBOARD_SHORTCUT_UTILS = "../../src/accessibility/KeyboardShortcutUtils";
const mockKeyboardShortcuts = (override: Record<string, any>) => {
jest.doMock(PATH_TO_KEYBOARD_SHORTCUTS, () => {
const original = jest.requireActual(PATH_TO_KEYBOARD_SHORTCUTS);
return {
...original,
...override,
};
});
};
const getFile = async () => await import(PATH_TO_KEYBOARD_SHORTCUTS);
const getUtils = async () => await import(PATH_TO_KEYBOARD_SHORTCUT_UTILS);
describe("KeyboardShortcutUtils", () => {
beforeEach(() => {
unmockPlatformPeg();
jest.resetModules();
});
it("doesn't change KEYBOARD_SHORTCUTS when getting shortcuts", async () => {
mockKeyboardShortcuts({
KEYBOARD_SHORTCUTS: {
Keybind1: {},
Keybind2: {},
},
MAC_ONLY_SHORTCUTS: ["Keybind1"],
DESKTOP_SHORTCUTS: ["Keybind2"],
});
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
const utils = await getUtils();
const file = await getFile();
const copyKeyboardShortcuts = Object.assign({}, file.KEYBOARD_SHORTCUTS);
utils.getKeyboardShortcuts();
expect(file.KEYBOARD_SHORTCUTS).toEqual(copyKeyboardShortcuts);
utils.getKeyboardShortcutsForUI();
expect(file.KEYBOARD_SHORTCUTS).toEqual(copyKeyboardShortcuts);
});
describe("correctly filters shortcuts", () => {
it("when on web and not on macOS", async () => {
mockKeyboardShortcuts({
KEYBOARD_SHORTCUTS: {
Keybind1: {},
Keybind2: {},
Keybind3: { controller: { settingDisabled: true } },
Keybind4: {},
},
MAC_ONLY_SHORTCUTS: ["Keybind1"],
DESKTOP_SHORTCUTS: ["Keybind2"],
});
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
expect((await getUtils()).getKeyboardShortcuts()).toEqual({ Keybind4: {} });
});
it("when on desktop", async () => {
mockKeyboardShortcuts({
KEYBOARD_SHORTCUTS: {
Keybind1: {},
Keybind2: {},
},
MAC_ONLY_SHORTCUTS: [],
DESKTOP_SHORTCUTS: ["Keybind2"],
});
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(true) });
expect((await getUtils()).getKeyboardShortcuts()).toEqual({ Keybind1: {}, Keybind2: {} });
});
});
});

View file

@ -0,0 +1,122 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import { render, screen, waitFor } from "jest-matrix-react";
import React from "react";
import { Landmark, LandmarkNavigation } from "../../src/accessibility/LandmarkNavigation";
import defaultDispatcher from "../../src/dispatcher/dispatcher";
describe("KeyboardLandmarkUtils", () => {
it("Landmarks are cycled through correctly without an opened room", () => {
render(
<div>
<div tabIndex={0} className="mx_SpaceButton_active" data-testid="mx_SpaceButton_active">
SPACE_BUTTON
</div>
<div tabIndex={0} className="mx_RoomSearch" data-testid="mx_RoomSearch">
ROOM_SEARCH
</div>
<div tabIndex={0} className="mx_RoomTile" data-testid="mx_RoomTile">
ROOM_TILE
</div>
<div tabIndex={0} className="mx_HomePage" data-testid="mx_HomePage">
HOME_PAGE
</div>
</div>,
);
// ACTIVE_SPACE_BUTTON <-> ROOM_SEARCH <-> ROOM_LIST <-> HOME <-> ACTIVE_SPACE_BUTTON
// ACTIVE_SPACE_BUTTON -> ROOM_SEARCH
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ACTIVE_SPACE_BUTTON);
expect(screen.getByTestId("mx_RoomSearch")).toHaveFocus();
// ROOM_SEARCH -> ROOM_LIST
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ROOM_SEARCH);
expect(screen.getByTestId("mx_RoomTile")).toHaveFocus();
// ROOM_LIST -> HOME_PAGE
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ROOM_LIST);
expect(screen.getByTestId("mx_HomePage")).toHaveFocus();
// HOME_PAGE -> ACTIVE_SPACE_BUTTON
LandmarkNavigation.findAndFocusNextLandmark(Landmark.MESSAGE_COMPOSER_OR_HOME);
expect(screen.getByTestId("mx_SpaceButton_active")).toHaveFocus();
// HOME_PAGE <- ACTIVE_SPACE_BUTTON
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ACTIVE_SPACE_BUTTON, true);
expect(screen.getByTestId("mx_HomePage")).toHaveFocus();
// ROOM_LIST <- HOME_PAGE
LandmarkNavigation.findAndFocusNextLandmark(Landmark.MESSAGE_COMPOSER_OR_HOME, true);
expect(screen.getByTestId("mx_RoomTile")).toHaveFocus();
// ROOM_SEARCH <- ROOM_LIST
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ROOM_LIST, true);
expect(screen.getByTestId("mx_RoomSearch")).toHaveFocus();
// ACTIVE_SPACE_BUTTON <- ROOM_SEARCH
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ROOM_SEARCH, true);
expect(screen.getByTestId("mx_SpaceButton_active")).toHaveFocus();
});
it("Landmarks are cycled through correctly with an opened room", async () => {
const callback = jest.fn();
defaultDispatcher.register(callback);
render(
<div>
<div tabIndex={0} className="mx_SpaceButton_active" data-testid="mx_SpaceButton_active">
SPACE_BUTTON
</div>
<div tabIndex={0} className="mx_RoomSearch" data-testid="mx_RoomSearch">
ROOM_SEARCH
</div>
<div tabIndex={0} className="mx_RoomTile_selected" data-testid="mx_RoomTile_selected">
ROOM_TILE
</div>
<div tabIndex={0} className="mx_Room" data-testid="mx_Room">
ROOM
<div tabIndex={0} className="mx_MessageComposer">
COMPOSER
</div>
</div>
</div>,
);
// ACTIVE_SPACE_BUTTON <-> ROOM_SEARCH <-> ROOM_LIST <-> MESSAGE_COMPOSER <-> ACTIVE_SPACE_BUTTON
// ACTIVE_SPACE_BUTTON -> ROOM_SEARCH
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ACTIVE_SPACE_BUTTON);
expect(screen.getByTestId("mx_RoomSearch")).toHaveFocus();
// ROOM_SEARCH -> ROOM_LIST
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ROOM_SEARCH);
expect(screen.getByTestId("mx_RoomTile_selected")).toHaveFocus();
// ROOM_LIST -> MESSAGE_COMPOSER
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ROOM_LIST);
await waitFor(() => expect(callback).toHaveBeenCalledTimes(1));
// MESSAGE_COMPOSER -> ACTIVE_SPACE_BUTTON
LandmarkNavigation.findAndFocusNextLandmark(Landmark.MESSAGE_COMPOSER_OR_HOME);
expect(screen.getByTestId("mx_SpaceButton_active")).toHaveFocus();
// MESSAGE_COMPOSER <- ACTIVE_SPACE_BUTTON
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ACTIVE_SPACE_BUTTON, true);
await waitFor(() => expect(callback).toHaveBeenCalledTimes(2));
// ROOM_LIST <- MESSAGE_COMPOSER
LandmarkNavigation.findAndFocusNextLandmark(Landmark.MESSAGE_COMPOSER_OR_HOME, true);
expect(screen.getByTestId("mx_RoomTile_selected")).toHaveFocus();
// ROOM_SEARCH <- ROOM_LIST
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ROOM_LIST, true);
expect(screen.getByTestId("mx_RoomSearch")).toHaveFocus();
// ACTIVE_SPACE_BUTTON <- ROOM_SEARCH
LandmarkNavigation.findAndFocusNextLandmark(Landmark.ROOM_SEARCH, true);
expect(screen.getByTestId("mx_SpaceButton_active")).toHaveFocus();
});
});

View file

@ -0,0 +1,414 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { HTMLAttributes } from "react";
import { render } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import {
IState,
reducer,
RovingTabIndexProvider,
RovingTabIndexWrapper,
Type,
useRovingTabIndex,
} from "../../src/accessibility/RovingTabIndex";
const Button = (props: HTMLAttributes<HTMLButtonElement>) => {
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLButtonElement>();
return <button {...props} onFocus={onFocus} tabIndex={isActive ? 0 : -1} ref={ref} />;
};
const checkTabIndexes = (buttons: NodeListOf<HTMLElement>, expectations: number[]) => {
expect([...buttons].map((b) => b.tabIndex)).toStrictEqual(expectations);
};
// give the buttons keys for the fibre reconciler to not treat them all as the same
const button1 = <Button key={1}>a</Button>;
const button2 = <Button key={2}>b</Button>;
const button3 = <Button key={3}>c</Button>;
const button4 = <Button key={4}>d</Button>;
// mock offsetParent
Object.defineProperty(HTMLElement.prototype, "offsetParent", {
get() {
return this.parentNode;
},
});
describe("RovingTabIndex", () => {
it("RovingTabIndexProvider renders children as expected", () => {
const { container } = render(
<RovingTabIndexProvider>
{() => (
<div>
<span>Test</span>
</div>
)}
</RovingTabIndexProvider>,
);
expect(container.textContent).toBe("Test");
expect(container.innerHTML).toBe("<div><span>Test</span></div>");
});
it("RovingTabIndexProvider works as expected with useRovingTabIndex", () => {
const { container, rerender } = render(
<RovingTabIndexProvider>
{() => (
<React.Fragment>
{button1}
{button2}
{button3}
</React.Fragment>
)}
</RovingTabIndexProvider>,
);
// should begin with 0th being active
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
// focus on 2nd button and test it is the only active one
container.querySelectorAll("button")[2].focus();
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
// focus on 1st button and test it is the only active one
container.querySelectorAll("button")[1].focus();
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
// check that the active button does not change even on an explicit blur event
container.querySelectorAll("button")[1].blur();
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
// update the children, it should remain on the same button
rerender(
<RovingTabIndexProvider>
{() => (
<React.Fragment>
{button1}
{button4}
{button2}
{button3}
</React.Fragment>
)}
</RovingTabIndexProvider>,
);
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0, -1]);
// update the children, remove the active button, it should move to the next one
rerender(
<RovingTabIndexProvider>
{() => (
<React.Fragment>
{button1}
{button4}
{button3}
</React.Fragment>
)}
</RovingTabIndexProvider>,
);
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
});
it("RovingTabIndexProvider works as expected with RovingTabIndexWrapper", () => {
const { container } = render(
<RovingTabIndexProvider>
{() => (
<React.Fragment>
{button1}
{button2}
<RovingTabIndexWrapper>
{({ onFocus, isActive, ref }) => (
<button
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
ref={ref as React.RefObject<HTMLButtonElement>}
>
.
</button>
)}
</RovingTabIndexWrapper>
</React.Fragment>
)}
</RovingTabIndexProvider>,
);
// should begin with 0th being active
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
// focus on 2nd button and test it is the only active one
container.querySelectorAll("button")[2].focus();
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
});
describe("reducer functions as expected", () => {
it("SetFocus works as expected", () => {
const ref1 = React.createRef<HTMLElement>();
const ref2 = React.createRef<HTMLElement>();
expect(
reducer(
{
activeRef: ref1,
refs: [ref1, ref2],
},
{
type: Type.SetFocus,
payload: {
ref: ref2,
},
},
),
).toStrictEqual({
activeRef: ref2,
refs: [ref1, ref2],
});
});
it("Unregister works as expected", () => {
const ref1 = React.createRef<HTMLElement>();
const ref2 = React.createRef<HTMLElement>();
const ref3 = React.createRef<HTMLElement>();
const ref4 = React.createRef<HTMLElement>();
let state: IState = {
refs: [ref1, ref2, ref3, ref4],
};
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref2,
},
});
expect(state).toStrictEqual({
refs: [ref1, ref3, ref4],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref3,
},
});
expect(state).toStrictEqual({
refs: [ref1, ref4],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref4,
},
});
expect(state).toStrictEqual({
refs: [ref1],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref1,
},
});
expect(state).toStrictEqual({
refs: [],
});
});
it("Register works as expected", () => {
const ref1 = React.createRef<HTMLElement>();
const ref2 = React.createRef<HTMLElement>();
const ref3 = React.createRef<HTMLElement>();
const ref4 = React.createRef<HTMLElement>();
render(
<React.Fragment>
<span ref={ref1} />
<span ref={ref2} />
<span ref={ref3} />
<span ref={ref4} />
</React.Fragment>,
);
let state: IState = {
refs: [],
};
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref1,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref2,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1, ref2],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref3,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1, ref2, ref3],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref4,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1, ref2, ref3, ref4],
});
// test that the automatic focus switch works for unmounting
state = reducer(state, {
type: Type.SetFocus,
payload: {
ref: ref2,
},
});
expect(state).toStrictEqual({
activeRef: ref2,
refs: [ref1, ref2, ref3, ref4],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref2,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref1, ref3, ref4],
});
// test that the insert into the middle works as expected
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref2,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref1, ref2, ref3, ref4],
});
// test that insertion at the edges works
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref1,
},
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref4,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref2, ref3],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref1,
},
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref4,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref1, ref2, ref3, ref4],
});
});
});
describe("handles arrow keys", () => {
it("should handle up/down arrow keys work when handleUpDown=true", async () => {
const { container } = render(
<RovingTabIndexProvider handleUpDown>
{({ onKeyDownHandler }) => (
<div onKeyDown={onKeyDownHandler}>
{button1}
{button2}
{button3}
</div>
)}
</RovingTabIndexProvider>,
);
container.querySelectorAll("button")[0].focus();
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
await userEvent.keyboard("[ArrowDown]");
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
await userEvent.keyboard("[ArrowDown]");
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
await userEvent.keyboard("[ArrowUp]");
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
await userEvent.keyboard("[ArrowUp]");
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
// Does not loop without
await userEvent.keyboard("[ArrowUp]");
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
});
it("should call scrollIntoView if specified", async () => {
const { container } = render(
<RovingTabIndexProvider handleUpDown scrollIntoView>
{({ onKeyDownHandler }) => (
<div onKeyDown={onKeyDownHandler}>
{button1}
{button2}
{button3}
</div>
)}
</RovingTabIndexProvider>,
);
container.querySelectorAll("button")[0].focus();
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
const button = container.querySelectorAll("button")[1];
const mock = jest.spyOn(button, "scrollIntoView");
await userEvent.keyboard("[ArrowDown]");
expect(mock).toHaveBeenCalled();
});
});
});