Skip to content

Commit c6fc88b

Browse files
fix: upgrate test coverage & fix disccusion & some refactor
1 parent 220d14e commit c6fc88b

File tree

8 files changed

+682
-154
lines changed

8 files changed

+682
-154
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import React from 'react';
2+
import { Factory } from 'rosie';
3+
import {
4+
initializeTestStore,
5+
render,
6+
screen,
7+
fireEvent,
8+
waitFor,
9+
} from '@src/setupTest';
10+
import userEvent from '@testing-library/user-event';
11+
import SidebarContext from '../SidebarContext';
12+
import SidebarBase from './SidebarBase';
13+
import messages from '../../messages';
14+
import { useSidebarFocusAndKeyboard } from './hooks/useSidebarFocusAndKeyboard';
15+
16+
jest.mock('./hooks/useSidebarFocusAndKeyboard');
17+
18+
const SIDEBAR_ID = 'test-sidebar';
19+
20+
const mockUseSidebarFocusAndKeyboard = useSidebarFocusAndKeyboard;
21+
22+
describe('SidebarBase (Refactored)', () => {
23+
let mockContextValue;
24+
const courseMetadata = Factory.build('courseMetadata');
25+
const user = userEvent.setup();
26+
27+
let mockCloseBtnRef;
28+
let mockBackBtnRef;
29+
let mockHandleClose;
30+
let mockHandleKeyDown;
31+
let mockHandleBackBtnKeyDown;
32+
33+
const renderSidebar = (contextProps = {}, componentProps = {}) => {
34+
const fullContextValue = { ...mockContextValue, ...contextProps };
35+
const defaultProps = {
36+
title: 'Test Sidebar Title',
37+
ariaLabel: 'Test Sidebar Aria Label',
38+
sidebarId: SIDEBAR_ID,
39+
className: 'test-class',
40+
children: <div>Sidebar Content</div>,
41+
...componentProps,
42+
};
43+
return render(
44+
<SidebarContext.Provider value={fullContextValue}>
45+
<SidebarBase {...defaultProps} />
46+
</SidebarContext.Provider>,
47+
);
48+
};
49+
50+
beforeEach(async () => {
51+
await initializeTestStore({
52+
courseMetadata,
53+
excludeFetchCourse: true,
54+
excludeFetchSequence: true,
55+
});
56+
57+
mockContextValue = {
58+
courseId: courseMetadata.id,
59+
toggleSidebar: jest.fn(),
60+
shouldDisplayFullScreen: false,
61+
currentSidebar: null,
62+
};
63+
64+
mockCloseBtnRef = React.createRef();
65+
mockBackBtnRef = React.createRef();
66+
mockHandleClose = jest.fn();
67+
mockHandleKeyDown = jest.fn();
68+
mockHandleBackBtnKeyDown = jest.fn();
69+
70+
mockUseSidebarFocusAndKeyboard.mockReturnValue({
71+
closeBtnRef: mockCloseBtnRef,
72+
backBtnRef: mockBackBtnRef,
73+
handleClose: mockHandleClose,
74+
handleKeyDown: mockHandleKeyDown,
75+
handleBackBtnKeyDown: mockHandleBackBtnKeyDown,
76+
});
77+
});
78+
79+
afterEach(() => {
80+
jest.clearAllMocks();
81+
});
82+
83+
it('should render children, title, and close button when visible', () => {
84+
renderSidebar({ currentSidebar: SIDEBAR_ID });
85+
expect(screen.getByText('Sidebar Content')).toBeInTheDocument();
86+
expect(screen.getByText('Test Sidebar Title')).toBeInTheDocument();
87+
expect(screen.getByRole('button', { name: messages.closeNotificationTrigger.defaultMessage })).toBeInTheDocument();
88+
expect(screen.getByTestId(`sidebar-${SIDEBAR_ID}`)).toBeInTheDocument();
89+
expect(screen.getByTestId(`sidebar-${SIDEBAR_ID}`)).not.toHaveClass('d-none');
90+
});
91+
92+
it('should be hidden via CSS class when not the current sidebar', () => {
93+
renderSidebar({ currentSidebar: 'another-sidebar-id' });
94+
const sidebarElement = screen.queryByTestId(`sidebar-${SIDEBAR_ID}`);
95+
expect(sidebarElement).toBeInTheDocument();
96+
expect(sidebarElement).toHaveClass('d-none');
97+
});
98+
99+
it('should hide title bar when showTitleBar prop is false', () => {
100+
renderSidebar({ currentSidebar: SIDEBAR_ID }, { showTitleBar: false });
101+
expect(screen.queryByText('Test Sidebar Title')).not.toBeInTheDocument();
102+
expect(screen.queryByRole('button', { name: messages.closeNotificationTrigger.defaultMessage })).not.toBeInTheDocument();
103+
expect(screen.getByText('Sidebar Content')).toBeInTheDocument();
104+
});
105+
106+
it('should render back button instead of close button in fullscreen', () => {
107+
renderSidebar({ currentSidebar: SIDEBAR_ID, shouldDisplayFullScreen: true });
108+
expect(screen.getByRole('button', { name: messages.responsiveCloseNotificationTray.defaultMessage })).toBeInTheDocument();
109+
expect(screen.queryByRole('button', { name: messages.closeNotificationTrigger.defaultMessage })).not.toBeInTheDocument();
110+
});
111+
112+
it('should call handleClose from hook on close button click', async () => {
113+
renderSidebar({ currentSidebar: SIDEBAR_ID });
114+
const closeButton = screen.getByRole('button', { name: messages.closeNotificationTrigger.defaultMessage });
115+
await user.click(closeButton);
116+
expect(mockHandleClose).toHaveBeenCalledTimes(1);
117+
});
118+
119+
it('should call handleClose from hook on fullscreen back button click', async () => {
120+
renderSidebar({ currentSidebar: SIDEBAR_ID, shouldDisplayFullScreen: true });
121+
const backButton = screen.getByRole('button', { name: messages.responsiveCloseNotificationTray.defaultMessage });
122+
await user.click(backButton);
123+
expect(mockHandleClose).toHaveBeenCalledTimes(1);
124+
});
125+
126+
it('should call handleKeyDown from hook on standard close button keydown', () => {
127+
renderSidebar({ currentSidebar: SIDEBAR_ID });
128+
const closeButton = screen.getByRole('button', { name: messages.closeNotificationTrigger.defaultMessage });
129+
fireEvent.keyDown(closeButton, { key: 'Tab' });
130+
expect(mockHandleKeyDown).toHaveBeenCalledTimes(1);
131+
});
132+
133+
it('should call handleBackBtnKeyDown from hook on fullscreen back button keydown', () => {
134+
renderSidebar({ currentSidebar: SIDEBAR_ID, shouldDisplayFullScreen: true });
135+
const backButton = screen.getByRole('button', { name: messages.responsiveCloseNotificationTray.defaultMessage });
136+
fireEvent.keyDown(backButton, { key: 'Enter' });
137+
expect(mockHandleBackBtnKeyDown).toHaveBeenCalledTimes(1);
138+
});
139+
140+
it('should call toggleSidebar(null) upon receiving a "close" postMessage event', async () => {
141+
renderSidebar({ currentSidebar: SIDEBAR_ID });
142+
143+
fireEvent(window, new MessageEvent('message', {
144+
data: { type: 'learning.events.sidebar.close' },
145+
}));
146+
147+
await waitFor(() => {
148+
expect(mockContextValue.toggleSidebar).toHaveBeenCalledTimes(1);
149+
expect(mockContextValue.toggleSidebar).toHaveBeenCalledWith(null);
150+
});
151+
});
152+
});

src/courseware/course/sidebar/common/SidebarBase.jsx

Lines changed: 16 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { ArrowBackIos, Close } from '@openedx/paragon/icons';
44
import classNames from 'classnames';
55
import PropTypes from 'prop-types';
66
import {
7-
useCallback, useContext, useEffect, useRef,
7+
useCallback, useContext,
88
} from 'react';
99

1010
import { useEventListener } from '@src/generic/hooks';
11-
import { setSessionStorage, getSessionStorage } from '@src/data/sessionStorage';
1211
import messages from '../../messages';
1312
import SidebarContext from '../SidebarContext';
13+
import { useSidebarFocusAndKeyboard } from './hooks/useSidebarFocusAndKeyboard';
1414

1515
const SidebarBase = ({
1616
title,
@@ -23,26 +23,20 @@ const SidebarBase = ({
2323
}) => {
2424
const intl = useIntl();
2525
const {
26-
courseId,
2726
toggleSidebar,
2827
shouldDisplayFullScreen,
2928
currentSidebar,
3029
} = useContext(SidebarContext);
3130

32-
const closeBtnRef = useRef(null);
33-
const responsiveCloseNotificationTrayRef = useRef(null);
34-
const isOpenNotificationTray = getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open';
35-
const isFocusedNotificationTray = getSessionStorage(`notificationTrayFocus.${courseId}`) === 'true';
36-
37-
useEffect(() => {
38-
if (isOpenNotificationTray && isFocusedNotificationTray && closeBtnRef.current) {
39-
closeBtnRef.current.focus();
40-
}
31+
const {
32+
closeBtnRef,
33+
backBtnRef,
34+
handleClose,
35+
handleKeyDown,
36+
handleBackBtnKeyDown,
37+
} = useSidebarFocusAndKeyboard(sidebarId);
4138

42-
if (shouldDisplayFullScreen) {
43-
responsiveCloseNotificationTrayRef.current?.focus();
44-
}
45-
});
39+
const isOpen = currentSidebar === sidebarId;
4640

4741
const receiveMessage = useCallback(({ data }) => {
4842
const { type } = data;
@@ -54,102 +48,12 @@ const SidebarBase = ({
5448

5549
useEventListener('message', receiveMessage);
5650

57-
const focusSidebarTriggerBtn = () => {
58-
const performFocus = () => {
59-
const sidebarTriggerBtn = document.querySelector('.sidebar-trigger-btn');
60-
if (sidebarTriggerBtn) {
61-
sidebarTriggerBtn.focus();
62-
}
63-
};
64-
65-
requestAnimationFrame(() => {
66-
requestAnimationFrame(performFocus);
67-
});
68-
};
69-
70-
const handleCloseNotificationTray = () => {
71-
toggleSidebar(null);
72-
setSessionStorage(`notificationTrayFocus.${courseId}`, 'true');
73-
setSessionStorage(`notificationTrayStatus.${courseId}`, 'closed');
74-
focusSidebarTriggerBtn();
75-
};
76-
77-
const handleKeyDown = useCallback((event) => {
78-
const { key, shiftKey, target } = event;
79-
80-
if (key !== 'Tab' || target !== closeBtnRef.current) {
81-
return;
82-
}
83-
84-
// Shift + Tab
85-
if (shiftKey) {
86-
event.preventDefault();
87-
focusSidebarTriggerBtn();
88-
return;
89-
}
90-
91-
// Tab
92-
const courseOutlineTrigger = document.querySelector('#courseOutlineTrigger');
93-
if (courseOutlineTrigger) {
94-
event.preventDefault();
95-
courseOutlineTrigger.focus();
96-
return;
97-
}
98-
99-
const leftArrow = document.querySelector('.previous-button');
100-
if (leftArrow && !leftArrow.disabled) {
101-
event.preventDefault();
102-
leftArrow.focus();
103-
return;
104-
}
105-
106-
const rightArrow = document.querySelector('.next-button');
107-
if (rightArrow && !rightArrow.disabled) {
108-
event.preventDefault();
109-
rightArrow.focus();
110-
}
111-
}, [focusSidebarTriggerBtn, closeBtnRef]);
112-
113-
useEffect(() => {
114-
document.addEventListener('keydown', handleKeyDown);
115-
return () => {
116-
document.removeEventListener('keydown', handleKeyDown);
117-
};
118-
}, [handleKeyDown]);
119-
120-
const handleKeyDownNotificationTray = (event) => {
121-
const { key, shiftKey } = event;
122-
const currentElement = event.target === responsiveCloseNotificationTrayRef.current;
123-
const sidebarTriggerBtn = document.querySelector('.call-to-action-btn');
124-
125-
switch (key) {
126-
case 'Enter':
127-
if (currentElement) {
128-
handleCloseNotificationTray();
129-
}
130-
break;
131-
132-
case 'Tab':
133-
if (!shiftKey && sidebarTriggerBtn) {
134-
event.preventDefault();
135-
sidebarTriggerBtn.focus();
136-
} else if (shiftKey) {
137-
event.preventDefault();
138-
responsiveCloseNotificationTrayRef.current?.focus();
139-
}
140-
break;
141-
142-
default:
143-
break;
144-
}
145-
};
146-
14751
return (
14852
<section
14953
className={classNames('ml-0 border border-light-400 rounded-sm h-auto align-top zindex-0', {
15054
'bg-white m-0 border-0 fixed-top vh-100 rounded-0': shouldDisplayFullScreen,
15155
'align-self-start': !shouldDisplayFullScreen,
152-
'd-none': currentSidebar !== sidebarId,
56+
'd-none': !isOpen,
15357
}, className)}
15458
data-testid={`sidebar-${sidebarId}`}
15559
style={{ width: shouldDisplayFullScreen ? '100%' : width }}
@@ -159,10 +63,10 @@ const SidebarBase = ({
15963
{shouldDisplayFullScreen ? (
16064
<div
16165
className="pt-2 pb-2.5 border-bottom border-light-400 d-flex align-items-center ml-2"
162-
onClick={handleCloseNotificationTray}
163-
onKeyDown={handleKeyDownNotificationTray}
66+
onClick={handleClose}
67+
onKeyDown={handleBackBtnKeyDown}
16468
role="button"
165-
ref={responsiveCloseNotificationTrayRef}
69+
ref={backBtnRef}
16670
tabIndex="0"
16771
>
16872
<Icon src={ArrowBackIos} />
@@ -174,8 +78,6 @@ const SidebarBase = ({
17478
{showTitleBar && (
17579
<>
17680
<div className="d-flex align-items-center mb-2">
177-
{/* TODO: view this title in UI and decide */}
178-
{/* <strong className="p-2.5 d-inline-block course-sidebar-title">{title}</strong> */}
17981
<h2 className="p-2.5 d-inline-block m-0 text-gray-700 h4">{title}</h2>
18082
{shouldDisplayFullScreen
18183
? null
@@ -187,7 +89,8 @@ const SidebarBase = ({
18789
size="sm"
18890
ref={closeBtnRef}
18991
iconAs={Icon}
190-
onClick={handleCloseNotificationTray}
92+
onClick={handleClose}
93+
onKeyDown={handleKeyDown}
19194
variant="primary"
19295
alt={intl.formatMessage(messages.closeNotificationTrigger)}
19396
/>

0 commit comments

Comments
 (0)