@momentum-ui/react-collaboration
Version:
Cisco Momentum UI Framework for React Collaboration Applications
431 lines (372 loc) • 13.9 kB
text/typescript
/* eslint-disable @typescript-eslint/no-empty-function */
import { renderHook, act } from '@testing-library/react-hooks';
import useOrientationBasedKeyboardNavigation, {
IUseOrientationBasedKeyboardNavigationProps,
} from './useOrientationBasedKeyboardNavigation';
import { KeyboardEvent } from 'react';
import SpatialNavigationProvider from '../components/SpatialNavigationProvider';
describe('useOrientationBasedKeyboardNavigation', () => {
const defaultProps = {
listSize: 2,
orientation: 'vertical',
noLoop: false,
contextProps: {},
} as IUseOrientationBasedKeyboardNavigationProps;
const stopPropagation = () => {};
const stopImmediatePropagation = () => {};
const preventDefault = () => {};
const baseEvent = {
stopPropagation,
nativeEvent: { stopImmediatePropagation },
preventDefault,
};
const downArrowEvent = { key: 'ArrowDown', ...baseEvent } as KeyboardEvent<HTMLElement>;
const upArrowEvent = { key: 'ArrowUp', ...baseEvent } as KeyboardEvent<HTMLElement>;
const leftArrowEvent = { key: 'ArrowLeft', ...baseEvent } as KeyboardEvent<HTMLElement>;
const rightArrowEvent = { key: 'ArrowRight', ...baseEvent } as KeyboardEvent<HTMLElement>;
it('should navigate correctly by default', () => {
const { result } = renderHook(() => useOrientationBasedKeyboardNavigation(defaultProps));
expect(result.current.getContext().noLoop).toStrictEqual(defaultProps.noLoop);
act(() => {
result.current.keyboardProps.onKeyDown(downArrowEvent);
});
const { getCurrentFocus } = result.current.getContext();
expect(getCurrentFocus()).toStrictEqual(1);
act(() => {
result.current.keyboardProps.onKeyDown(downArrowEvent);
});
// no loop true so should 0
expect(getCurrentFocus()).toStrictEqual(0);
act(() => {
result.current.keyboardProps.onKeyDown(leftArrowEvent);
});
expect(getCurrentFocus()).toStrictEqual(0);
act(() => {
result.current.keyboardProps.onKeyDown(upArrowEvent);
});
// no loop true should be 1
expect(getCurrentFocus()).toStrictEqual(1);
});
it('should navigate correctly when horizontal and noLoop', () => {
const props = {
noLoop: true,
orientation: 'horizontal',
} as Partial<IUseOrientationBasedKeyboardNavigationProps>;
const { result } = renderHook(() =>
useOrientationBasedKeyboardNavigation({ ...defaultProps, ...props })
);
act(() => {
result.current.keyboardProps.onKeyDown(rightArrowEvent);
});
const { getCurrentFocus } = result.current.getContext();
expect(getCurrentFocus()).toStrictEqual(1);
act(() => {
result.current.keyboardProps.onKeyDown(rightArrowEvent);
});
// no loop false so shouldn't wrap back to 0
expect(getCurrentFocus()).toStrictEqual(1);
act(() => {
result.current.keyboardProps.onKeyDown(leftArrowEvent);
});
expect(getCurrentFocus()).toStrictEqual(0);
act(() => {
result.current.keyboardProps.onKeyDown(leftArrowEvent);
});
// no loop false so shouldn't wrap back to 1
expect(getCurrentFocus()).toStrictEqual(0);
act(() => {
result.current.keyboardProps.onKeyDown(downArrowEvent);
});
expect(getCurrentFocus()).toStrictEqual(0);
});
describe('list size changes', () => {
it.each([
// removing items
{ focusedIndex: 0, expectedNextFocus: 0, newListSize: 4 },
{ focusedIndex: 1, expectedNextFocus: 1, newListSize: 4 },
{ focusedIndex: 2, expectedNextFocus: 2, newListSize: 4 },
{ focusedIndex: 3, expectedNextFocus: 3, newListSize: 4 },
{ focusedIndex: 4, expectedNextFocus: 3, newListSize: 4 },
{ focusedIndex: 4, expectedNextFocus: 2, newListSize: 3 },
{ focusedIndex: 4, expectedNextFocus: 1, newListSize: 2 },
{ focusedIndex: 4, expectedNextFocus: 0, newListSize: 1 },
// adding items
{ focusedIndex: 0, expectedNextFocus: 0, newListSize: 6 },
{ focusedIndex: 1, expectedNextFocus: 1, newListSize: 6 },
{ focusedIndex: 2, expectedNextFocus: 2, newListSize: 6 },
{ focusedIndex: 3, expectedNextFocus: 3, newListSize: 6 },
{ focusedIndex: 4, expectedNextFocus: 4, newListSize: 6 },
])(
'should handle list size changing with numeric indexes - non-focused item removed',
({ focusedIndex, expectedNextFocus, newListSize }) => {
const { result, rerender } = renderHook(
(props: any) => useOrientationBasedKeyboardNavigation(props),
{
initialProps: { listSize: 5, orientation: 'vertical' },
}
);
expect(result.current.getContext()).toEqual({
getCurrentFocus: expect.any(Function),
addFocusCallback: expect.any(Function),
isFocusedWithin: false,
noLoop: undefined,
setCurrentFocus: expect.any(Function),
setUpdateFocusBlocked: expect.any(Function),
});
expect(result.current.getContext().getCurrentFocus()).toEqual(0);
act(() => {
result.current.getContext().setCurrentFocus(focusedIndex);
});
expect(result.current.getContext()).toEqual({
getCurrentFocus: expect.any(Function),
addFocusCallback: expect.any(Function),
isFocusedWithin: false,
noLoop: undefined,
setCurrentFocus: expect.any(Function),
setUpdateFocusBlocked: expect.any(Function),
});
expect(result.current.getContext().getCurrentFocus()).toEqual(focusedIndex);
act(() => {
rerender({ listSize: newListSize, orientation: 'vertical' });
});
expect(result.current.getContext()).toEqual({
getCurrentFocus: expect.any(Function),
addFocusCallback: expect.any(Function),
isFocusedWithin: false,
noLoop: undefined,
setCurrentFocus: expect.any(Function),
setUpdateFocusBlocked: expect.any(Function),
});
expect(result.current.getContext().getCurrentFocus()).toEqual(expectedNextFocus);
}
);
it.each([
// removing items
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'a',
finalIndexes: ['a', 'b', 'c', 'd'],
expectedNextFocus: 'a',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'b',
finalIndexes: ['a', 'b', 'c', 'd'],
expectedNextFocus: 'b',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'c',
finalIndexes: ['a', 'b', 'c', 'd'],
expectedNextFocus: 'c',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'd',
finalIndexes: ['a', 'b', 'c', 'd'],
expectedNextFocus: 'd',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'e',
finalIndexes: ['a', 'b', 'c', 'd'],
expectedNextFocus: 'd',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'e',
finalIndexes: ['a', 'b', 'c'],
expectedNextFocus: 'c',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e', 'f'],
focusedIndex: 'c',
finalIndexes: ['a', 'b', 'e', 'f'],
expectedNextFocus: 'b',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'a',
finalIndexes: ['b', 'c', 'd', 'e'],
expectedNextFocus: 'b',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'b',
finalIndexes: ['b', 'c', 'd', 'e'],
expectedNextFocus: 'b',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'c',
finalIndexes: ['b', 'c', 'd', 'e'],
expectedNextFocus: 'c',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'd',
finalIndexes: ['b', 'c', 'd', 'e'],
expectedNextFocus: 'd',
},
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'e',
finalIndexes: ['b', 'c', 'd', 'e'],
expectedNextFocus: 'e',
},
// adding items
{
initialIndexes: ['a', 'b', 'c', 'd'],
focusedIndex: 'a',
finalIndexes: ['a', 'b', 'c', 'd', 'e'],
expectedNextFocus: 'a',
},
{
initialIndexes: ['a', 'b', 'c', 'd'],
focusedIndex: 'b',
finalIndexes: ['a', 'b', 'c', 'd', 'e'],
expectedNextFocus: 'b',
},
{
initialIndexes: ['a', 'b', 'c', 'd'],
focusedIndex: 'c',
finalIndexes: ['a', 'b', 'c', 'd', 'e'],
expectedNextFocus: 'c',
},
{
initialIndexes: ['a', 'b', 'c', 'd'],
focusedIndex: 'd',
finalIndexes: ['a', 'b', 'c', 'd', 'e'],
expectedNextFocus: 'd',
},
{
initialIndexes: ['a', 'b', 'd', 'e'],
focusedIndex: 'b',
finalIndexes: ['a', 'b', 'c', 'd', 'e'],
expectedNextFocus: 'b',
},
{
initialIndexes: ['a', 'b', 'd', 'e'],
focusedIndex: 'd',
finalIndexes: ['a', 'b', 'c', 'd', 'e'],
expectedNextFocus: 'd',
},
// swap all the items
{
initialIndexes: ['a', 'b', 'c', 'd', 'e'],
focusedIndex: 'c',
finalIndexes: ['g', 'h', 'i', 'j', 'k'],
expectedNextFocus: 'k',
},
])(
'should handle list size changing with non-numeric indexes',
({ initialIndexes, focusedIndex, finalIndexes, expectedNextFocus }) => {
const { result, rerender } = renderHook(
(props: any) => useOrientationBasedKeyboardNavigation(props),
{
initialProps: {
listSize: initialIndexes.length,
orientation: 'vertical',
allItemIndexes: initialIndexes,
},
}
);
expect(result.current.getContext()).toEqual({
// currentFocus: initialIndexes[0],
getCurrentFocus: expect.any(Function),
addFocusCallback: expect.any(Function),
isFocusedWithin: false,
noLoop: undefined,
setCurrentFocus: expect.any(Function),
setUpdateFocusBlocked: expect.any(Function),
});
expect(result.current.getContext().getCurrentFocus()).toEqual(initialIndexes[0]);
act(() => {
result.current.getContext().setCurrentFocus(focusedIndex);
});
expect(result.current.getContext()).toEqual({
getCurrentFocus: expect.any(Function),
addFocusCallback: expect.any(Function),
isFocusedWithin: false,
noLoop: undefined,
setCurrentFocus: expect.any(Function),
setUpdateFocusBlocked: expect.any(Function),
});
expect(result.current.getContext().getCurrentFocus()).toEqual(focusedIndex);
act(() => {
rerender({
listSize: finalIndexes.length,
orientation: 'vertical',
allItemIndexes: finalIndexes,
});
});
expect(result.current.getContext()).toEqual({
getCurrentFocus: expect.any(Function),
addFocusCallback: expect.any(Function),
isFocusedWithin: false,
noLoop: undefined,
setCurrentFocus: expect.any(Function),
setUpdateFocusBlocked: expect.any(Function),
});
expect(result.current.getContext().getCurrentFocus()).toEqual(expectedNextFocus);
}
);
it('should gracefully handle non-numeric item indexes without allItemIndexes', () => {
const { result, rerender } = renderHook(
(props: any) => useOrientationBasedKeyboardNavigation(props),
{
initialProps: { listSize: 3, orientation: 'vertical' },
}
);
jest.spyOn(console, 'warn');
expect(result.current.getContext()).toEqual({
getCurrentFocus: expect.any(Function),
addFocusCallback: expect.any(Function),
isFocusedWithin: false,
noLoop: undefined,
setCurrentFocus: expect.any(Function),
setUpdateFocusBlocked: expect.any(Function),
});
expect(result.current.getContext().getCurrentFocus()).toEqual(0);
act(() => {
result.current.getContext().setCurrentFocus('c');
});
expect(result.current.getContext()).toEqual({
getCurrentFocus: expect.any(Function),
addFocusCallback: expect.any(Function),
isFocusedWithin: false,
noLoop: undefined,
setCurrentFocus: expect.any(Function),
setUpdateFocusBlocked: expect.any(Function),
});
expect(result.current.getContext().getCurrentFocus()).toEqual('c');
act(() => {
rerender({ listSize: 2, orientation: 'vertical' });
});
// We can't handle non-numeric indexes without allItemIndexes
// So the current focus remains unchanged
expect(result.current.getContext()).toEqual({
getCurrentFocus: expect.any(Function),
addFocusCallback: expect.any(Function),
isFocusedWithin: false,
noLoop: undefined,
setCurrentFocus: expect.any(Function),
setUpdateFocusBlocked: expect.any(Function),
});
expect(result.current.getContext().getCurrentFocus()).toEqual('c');
expect(console.warn).toHaveBeenCalledWith(
'Unable to handle non-numeric index without allItemIndexes',
'c'
);
});
});
describe('with Spatial navigation context', () => {
it('should be always in no loop mode', () => {
const { result } = renderHook(
() => useOrientationBasedKeyboardNavigation({ ...defaultProps, noLoop: false }),
{ wrapper: SpatialNavigationProvider }
);
expect(result.current.getContext().noLoop).toStrictEqual(true);
});
});
});