Skip to content

Commit d73c883

Browse files
author
Katie George
committed
chore: Adds more test coverage
1 parent d2762df commit d73c883

File tree

6 files changed

+223
-39
lines changed

6 files changed

+223
-39
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { describe, expect, it, vi } from "vitest";
4+
5+
import { CustomEventStub, fireCancelableEvent } from "..";
6+
7+
describe("CustomEventStub", () => {
8+
it("should initialize with default values", () => {
9+
const event = new CustomEventStub();
10+
11+
expect(event.defaultPrevented).toBe(false);
12+
expect(event.cancelBubble).toBe(false);
13+
expect(event.cancelable).toBe(false);
14+
expect(event.detail).toBeNull();
15+
});
16+
17+
it("should initialize with provided values", () => {
18+
const detail = { key: "value" };
19+
const event = new CustomEventStub(true, detail);
20+
21+
expect(event.cancelable).toBe(true);
22+
expect(event.detail).toBe(detail);
23+
});
24+
25+
it("should set defaultPrevented to true when preventDefault is called", () => {
26+
const event = new CustomEventStub();
27+
event.preventDefault();
28+
29+
expect(event.defaultPrevented).toBe(true);
30+
});
31+
32+
it("should set cancelBubble to true when stopPropagation is called", () => {
33+
const event = new CustomEventStub();
34+
event.stopPropagation();
35+
36+
expect(event.cancelBubble).toBe(true);
37+
});
38+
39+
it("should work with generic detail type", () => {
40+
const event = new CustomEventStub<string>(false, "Test detail");
41+
42+
expect(event.detail).toBe("Test detail");
43+
expect(typeof event.detail).toBe("string");
44+
});
45+
});
46+
47+
describe("fireCancelableEvent", () => {
48+
it("should call the handler with a cancelable event and return the result", () => {
49+
const handler = vi.fn();
50+
const detail = { key: "value" };
51+
52+
const result = fireCancelableEvent(handler, detail);
53+
54+
expect(handler).toHaveBeenCalledTimes(1);
55+
const event = handler.mock.calls[0][0];
56+
expect(event.cancelable).toBe(true);
57+
expect(event.detail).toEqual(detail);
58+
expect(result).toBe(false);
59+
});
60+
61+
it("should prevent the default action if preventDefault is called in the handler", () => {
62+
const handler = vi.fn((event) => {
63+
event.preventDefault();
64+
});
65+
const detail = { key: "value" };
66+
67+
const result = fireCancelableEvent(handler, detail);
68+
69+
expect(handler).toHaveBeenCalledTimes(1);
70+
const event = handler.mock.calls[0][0];
71+
expect(event.defaultPrevented).toBe(true);
72+
expect(result).toBe(true);
73+
});
74+
75+
it("should stop propagation if stopPropagation is called in the handler", () => {
76+
const handler = vi.fn((event) => {
77+
event.stopPropagation();
78+
});
79+
const detail = { key: "value" };
80+
const mockSourceEvent = { stopPropagation: vi.fn() };
81+
82+
const result = fireCancelableEvent(handler, detail, mockSourceEvent as unknown as Event);
83+
84+
expect(handler).toHaveBeenCalledTimes(1);
85+
const event = handler.mock.calls[0][0];
86+
expect(event.cancelBubble).toBe(true);
87+
expect(mockSourceEvent.stopPropagation).toHaveBeenCalledTimes(1);
88+
expect(result).toBe(false);
89+
});
90+
91+
it("should not call preventDefault or stopPropagation on sourceEvent if the event handler does not trigger them", () => {
92+
const handler = vi.fn();
93+
const detail = { key: "value" };
94+
const mockSourceEvent = {
95+
preventDefault: vi.fn(),
96+
stopPropagation: vi.fn(),
97+
};
98+
99+
fireCancelableEvent(handler, detail, mockSourceEvent as unknown as Event);
100+
101+
expect(mockSourceEvent.preventDefault).not.toHaveBeenCalled();
102+
expect(mockSourceEvent.stopPropagation).not.toHaveBeenCalled();
103+
});
104+
105+
it("should return false if no handler is provided", () => {
106+
const result = fireCancelableEvent(undefined, { key: "value" });
107+
108+
expect(result).toBe(false);
109+
});
110+
});

src/internal/events/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export interface ClickDetail {
1010
metaKey: boolean;
1111
}
1212

13-
class CustomEventStub<T> {
13+
export class CustomEventStub<T> {
1414
defaultPrevented = false;
1515
cancelBubble = false;
1616
constructor(
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { describe, expect, test, vi } from "vitest";
4+
5+
import { getNextFocusTarget, onUnregisterActive } from "../../../lib/components/support-prompt-group/internal.js";
6+
7+
describe("getNextFocusTarget", () => {
8+
test("should return null if containerObjectRef.current is null", () => {
9+
const containerObjectRef = { current: null }; // Simulate null container
10+
const focusedIdRef = { current: null }; // Simulate no focused item
11+
12+
const result = getNextFocusTarget(containerObjectRef, focusedIdRef);
13+
14+
expect(result).toBeNull();
15+
});
16+
});
17+
18+
describe("onUnregisterActive", () => {
19+
test("should call target.focus() when target exists and has a different dataset.itemid", () => {
20+
const mockFocus = vi.fn();
21+
const mockTarget = document.createElement("button");
22+
mockTarget.dataset.itemid = "different-id";
23+
mockTarget.focus = mockFocus;
24+
25+
const mockNavigationAPI = {
26+
current: {
27+
getFocusTarget: vi.fn(() => mockTarget),
28+
},
29+
};
30+
31+
const focusableElement = document.createElement("div");
32+
focusableElement.dataset.itemid = "current-id";
33+
34+
onUnregisterActive(focusableElement, mockNavigationAPI);
35+
36+
expect(mockNavigationAPI.current?.getFocusTarget).toHaveBeenCalled();
37+
expect(mockFocus).toHaveBeenCalledTimes(1);
38+
});
39+
40+
test("should not call target.focus() when target.dataset.itemid matches focusableElement.dataset.itemid", () => {
41+
const mockFocus = vi.fn();
42+
const mockTarget = document.createElement("button");
43+
mockTarget.dataset.itemid = "same-id";
44+
mockTarget.focus = mockFocus;
45+
46+
const mockNavigationAPI = {
47+
current: {
48+
getFocusTarget: vi.fn(() => mockTarget),
49+
},
50+
};
51+
52+
const focusableElement = document.createElement("div");
53+
focusableElement.dataset.itemid = "same-id";
54+
55+
onUnregisterActive(focusableElement, mockNavigationAPI);
56+
57+
expect(mockNavigationAPI.current?.getFocusTarget).toHaveBeenCalled();
58+
expect(mockFocus).not.toHaveBeenCalled();
59+
});
60+
61+
test("should not call target.focus() when getFocusTarget returns null", () => {
62+
const mockNavigationAPI = {
63+
current: {
64+
getFocusTarget: vi.fn(() => null),
65+
},
66+
};
67+
68+
const focusableElement = document.createElement("div");
69+
focusableElement.dataset.itemid = "current-id";
70+
71+
onUnregisterActive(focusableElement, mockNavigationAPI);
72+
73+
expect(mockNavigationAPI.current?.getFocusTarget).toHaveBeenCalled();
74+
});
75+
});

src/support-prompt-group/__tests__/support-prompt-group.test.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,13 @@ describe("Support prompt group", () => {
151151
expect(document.body).toHaveFocus();
152152
});
153153

154-
test("Keyboard doesn't move focus", () => {
155-
const wrapper = renderSupportPromptGroup({}, ref);
154+
test("Null container ref doesn't move focus", () => {
155+
const wrapper = renderSupportPromptGroup({});
156+
const ref: { current: SupportPromptGroupProps.Ref | null } = { current: null };
156157

157-
document.body.focus();
158-
fireEvent.keyDown(document.body, { keyCode: KeyCode.down });
159-
expect(wrapper.findItemById("item-1")!.getElement()).not.toHaveFocus();
158+
ref.current?.focus("item-1");
159+
160+
fireEvent.keyDown(wrapper.getElement(), { keyCode: KeyCode.down });
160161
expect(document.body).toHaveFocus();
161162
});
162163
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import styles from "./styles.css.js";
4+
5+
export function getNextFocusTarget(
6+
containerObjectRef: React.RefObject<HTMLDivElement>,
7+
focusedIdRef: React.MutableRefObject<string | null>,
8+
): null | HTMLElement {
9+
if (containerObjectRef.current) {
10+
const buttons: HTMLButtonElement[] = Array.from(
11+
containerObjectRef?.current.querySelectorAll(`.${styles["support-prompt"]}`),
12+
);
13+
return buttons.find((button) => button.dataset.itemid === focusedIdRef?.current) ?? buttons[0] ?? null;
14+
}
15+
return null;
16+
}
17+
18+
export function onUnregisterActive(
19+
focusableElement: HTMLElement,
20+
navigationAPI: React.RefObject<{ getFocusTarget: () => HTMLElement | null }>,
21+
) {
22+
const target = navigationAPI.current?.getFocusTarget();
23+
24+
if (target && target.dataset.itemid !== focusableElement.dataset.itemid) {
25+
target.focus();
26+
}
27+
}

src/support-prompt-group/internal.tsx

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { InternalBaseComponentProps } from "../internal/base-component/use-base-
1717
import { fireCancelableEvent } from "../internal/events";
1818
import { SingleTabStopNavigationProvider } from "../internal/single-tab-stop";
1919
import { useMergeRefs } from "../internal/utils/use-merge-refs";
20+
import { getNextFocusTarget, onUnregisterActive } from "./focus-helpers";
2021
import { SupportPromptGroupProps } from "./interfaces";
2122
import { Prompt } from "./prompt";
2223

@@ -53,28 +54,6 @@ export const InternalSupportPromptGroup = forwardRef(
5354
fireCancelableEvent(onItemClick, { id, altKey, button, ctrlKey, metaKey, shiftKey }, event);
5455
};
5556

56-
function getNextFocusTarget(): null | HTMLElement {
57-
if (containerObjectRef.current) {
58-
const buttons: HTMLButtonElement[] = Array.from(
59-
containerObjectRef.current.querySelectorAll(`.${styles["support-prompt"]}`),
60-
);
61-
const activeButtons = buttons.filter((button) => !button.disabled);
62-
return (
63-
activeButtons.find((button) => button.dataset.itemid === focusedIdRef.current) ?? activeButtons[0] ?? null
64-
);
65-
}
66-
return null;
67-
}
68-
69-
function onUnregisterActive(focusableElement: HTMLElement) {
70-
// Only refocus when the node is actually removed (no such ID anymore).
71-
const target = navigationAPI.current?.getFocusTarget();
72-
73-
if (target && target.dataset.itemid !== focusableElement.dataset.itemid) {
74-
target.focus();
75-
}
76-
}
77-
7857
useEffect(() => {
7958
navigationAPI.current?.updateFocusTarget();
8059
});
@@ -142,15 +121,7 @@ export const InternalSupportPromptGroup = forwardRef(
142121
return navigationAPI.current?.isRegistered(element) ?? false;
143122
}
144123

145-
function isElementDisabled(element: HTMLElement) {
146-
if (element instanceof HTMLButtonElement) {
147-
return element.disabled;
148-
}
149-
150-
return false;
151-
}
152-
153-
return getAllFocusables(target).filter((el) => isElementRegistered(el) && !isElementDisabled(el));
124+
return getAllFocusables(target).filter((el) => isElementRegistered(el));
154125
}
155126

156127
if (!items || items.length === 0) {
@@ -173,8 +144,8 @@ export const InternalSupportPromptGroup = forwardRef(
173144
<SingleTabStopNavigationProvider
174145
ref={navigationAPI}
175146
navigationActive={true}
176-
getNextFocusTarget={getNextFocusTarget}
177-
onUnregisterActive={onUnregisterActive}
147+
getNextFocusTarget={() => getNextFocusTarget(containerObjectRef, focusedIdRef)}
148+
onUnregisterActive={(element: HTMLElement) => onUnregisterActive(element, navigationAPI)}
178149
>
179150
{items.map((item, index) => {
180151
return (

0 commit comments

Comments
 (0)