Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions src/courseware/course/sidebar/common/Sidebar.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this React import?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

import { Factory } from 'rosie';
import {
Comment on lines 1 to 5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[code style]: Let’s separate external library imports from local imports with a blank line for better readability.

Suggested change
import { Factory } from 'rosie';
import {
import { Factory } from 'rosie';
import {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the line import { createRef } from 'react'; come before import { Factory } from 'rosie';? The idea is to separate library imports from local imports.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

initializeTestStore,
render,
screen,
fireEvent,
waitFor,
} from '@src/setupTest';
import userEvent from '@testing-library/user-event';
import SidebarContext from '../SidebarContext';
import SidebarBase from './SidebarBase';
import messages from '../../messages';
import { useSidebarFocusAndKeyboard } from './hooks/useSidebarFocusAndKeyboard';

jest.mock('./hooks/useSidebarFocusAndKeyboard');

const SIDEBAR_ID = 'test-sidebar';

const mockUseSidebarFocusAndKeyboard = useSidebarFocusAndKeyboard;

describe('SidebarBase (Refactored)', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
describe('SidebarBase (Refactored)', () => {
describe('SidebarBase', () => {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

let mockContextValue;
const courseMetadata = Factory.build('courseMetadata');
const user = userEvent.setup();

let mockCloseBtnRef;
let mockBackBtnRef;
let mockHandleClose;
let mockHandleKeyDown;
let mockHandleBackBtnKeyDown;

const renderSidebar = (contextProps = {}, componentProps = {}) => {
const fullContextValue = { ...mockContextValue, ...contextProps };
const defaultProps = {
title: 'Test Sidebar Title',
ariaLabel: 'Test Sidebar Aria Label',
sidebarId: SIDEBAR_ID,
className: 'test-class',
children: <div>Sidebar Content</div>,
...componentProps,
};
return render(
<SidebarContext.Provider value={fullContextValue}>
<SidebarBase {...defaultProps} />
</SidebarContext.Provider>,
);
};

beforeEach(async () => {
await initializeTestStore({
courseMetadata,
excludeFetchCourse: true,
excludeFetchSequence: true,
});

mockContextValue = {
courseId: courseMetadata.id,
toggleSidebar: jest.fn(),
shouldDisplayFullScreen: false,
currentSidebar: null,
};

mockCloseBtnRef = React.createRef();
mockBackBtnRef = React.createRef();
mockHandleClose = jest.fn();
mockHandleKeyDown = jest.fn();
mockHandleBackBtnKeyDown = jest.fn();

mockUseSidebarFocusAndKeyboard.mockReturnValue({
closeBtnRef: mockCloseBtnRef,
backBtnRef: mockBackBtnRef,
handleClose: mockHandleClose,
handleKeyDown: mockHandleKeyDown,
handleBackBtnKeyDown: mockHandleBackBtnKeyDown,
});
});

afterEach(() => {
jest.clearAllMocks();
});

it('should render children, title, and close button when visible', () => {
renderSidebar({ currentSidebar: SIDEBAR_ID });
expect(screen.getByText('Sidebar Content')).toBeInTheDocument();
expect(screen.getByText('Test Sidebar Title')).toBeInTheDocument();
expect(screen.getByRole('button', { name: messages.closeNotificationTrigger.defaultMessage })).toBeInTheDocument();
expect(screen.getByTestId(`sidebar-${SIDEBAR_ID}`)).toBeInTheDocument();
expect(screen.getByTestId(`sidebar-${SIDEBAR_ID}`)).not.toHaveClass('d-none');
});

it('should be hidden via CSS class when not the current sidebar', () => {
renderSidebar({ currentSidebar: 'another-sidebar-id' });
const sidebarElement = screen.queryByTestId(`sidebar-${SIDEBAR_ID}`);
expect(sidebarElement).toBeInTheDocument();
expect(sidebarElement).toHaveClass('d-none');
});

it('should hide title bar when showTitleBar prop is false', () => {
renderSidebar({ currentSidebar: SIDEBAR_ID }, { showTitleBar: false });
expect(screen.queryByText('Test Sidebar Title')).not.toBeInTheDocument();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace the static text in these tests with values ​​from defaultProps?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

expect(screen.queryByRole('button', { name: messages.closeNotificationTrigger.defaultMessage })).not.toBeInTheDocument();
expect(screen.getByText('Sidebar Content')).toBeInTheDocument();
});

it('should render back button instead of close button in fullscreen', () => {
renderSidebar({ currentSidebar: SIDEBAR_ID, shouldDisplayFullScreen: true });
expect(screen.getByRole('button', { name: messages.responsiveCloseNotificationTray.defaultMessage })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: messages.closeNotificationTrigger.defaultMessage })).not.toBeInTheDocument();
});

it('should call handleClose from hook on close button click', async () => {
renderSidebar({ currentSidebar: SIDEBAR_ID });
const closeButton = screen.getByRole('button', { name: messages.closeNotificationTrigger.defaultMessage });
await user.click(closeButton);
expect(mockHandleClose).toHaveBeenCalledTimes(1);
});

it('should call handleClose from hook on fullscreen back button click', async () => {
renderSidebar({ currentSidebar: SIDEBAR_ID, shouldDisplayFullScreen: true });
const backButton = screen.getByRole('button', { name: messages.responsiveCloseNotificationTray.defaultMessage });
await user.click(backButton);
expect(mockHandleClose).toHaveBeenCalledTimes(1);
});

it('should call handleKeyDown from hook on standard close button keydown', () => {

Check failure on line 126 in src/courseware/course/sidebar/common/Sidebar.test.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 2 spaces but found 3
renderSidebar({ currentSidebar: SIDEBAR_ID });

Check failure on line 127 in src/courseware/course/sidebar/common/Sidebar.test.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 4 spaces but found 5
const closeButton = screen.getByRole('button', { name: messages.closeNotificationTrigger.defaultMessage });

Check failure on line 128 in src/courseware/course/sidebar/common/Sidebar.test.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 4 spaces but found 5
fireEvent.keyDown(closeButton, { key: 'Tab' });

Check failure on line 129 in src/courseware/course/sidebar/common/Sidebar.test.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 4 spaces but found 5
expect(mockHandleKeyDown).toHaveBeenCalledTimes(1);

Check failure on line 130 in src/courseware/course/sidebar/common/Sidebar.test.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 4 spaces but found 5
});

Check failure on line 131 in src/courseware/course/sidebar/common/Sidebar.test.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 2 spaces but found 3

it('should call handleBackBtnKeyDown from hook on fullscreen back button keydown', () => {

Check failure on line 133 in src/courseware/course/sidebar/common/Sidebar.test.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 2 spaces but found 3
renderSidebar({ currentSidebar: SIDEBAR_ID, shouldDisplayFullScreen: true });

Check failure on line 134 in src/courseware/course/sidebar/common/Sidebar.test.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 4 spaces but found 5
const backButton = screen.getByRole('button', { name: messages.responsiveCloseNotificationTray.defaultMessage });

Check failure on line 135 in src/courseware/course/sidebar/common/Sidebar.test.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 4 spaces but found 5
fireEvent.keyDown(backButton, { key: 'Enter' });

Check failure on line 136 in src/courseware/course/sidebar/common/Sidebar.test.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 4 spaces but found 5
expect(mockHandleBackBtnKeyDown).toHaveBeenCalledTimes(1);
});

it('should call toggleSidebar(null) upon receiving a "close" postMessage event', async () => {
renderSidebar({ currentSidebar: SIDEBAR_ID });

fireEvent(window, new MessageEvent('message', {
data: { type: 'learning.events.sidebar.close' },
}));

await waitFor(() => {
expect(mockContextValue.toggleSidebar).toHaveBeenCalledTimes(1);
expect(mockContextValue.toggleSidebar).toHaveBeenCalledWith(null);
});
});
});
129 changes: 16 additions & 113 deletions src/courseware/course/sidebar/common/SidebarBase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { ArrowBackIos, Close } from '@openedx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import {
useCallback, useContext, useEffect, useRef,
useCallback, useContext,
} from 'react';

import { useEventListener } from '@src/generic/hooks';
import { setSessionStorage, getSessionStorage } from '@src/data/sessionStorage';
import messages from '../../messages';
import SidebarContext from '../SidebarContext';
import { useSidebarFocusAndKeyboard } from './hooks/useSidebarFocusAndKeyboard';

const SidebarBase = ({
title,
Expand All @@ -23,26 +23,20 @@ const SidebarBase = ({
}) => {
const intl = useIntl();
const {
courseId,
toggleSidebar,
shouldDisplayFullScreen,
currentSidebar,
} = useContext(SidebarContext);

const closeBtnRef = useRef(null);
const responsiveCloseNotificationTrayRef = useRef(null);
const isOpenNotificationTray = getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open';
const isFocusedNotificationTray = getSessionStorage(`notificationTrayFocus.${courseId}`) === 'true';

useEffect(() => {
if (isOpenNotificationTray && isFocusedNotificationTray && closeBtnRef.current) {
closeBtnRef.current.focus();
}
const {
closeBtnRef,
backBtnRef,
handleClose,
handleKeyDown,
handleBackBtnKeyDown,
} = useSidebarFocusAndKeyboard(sidebarId);

if (shouldDisplayFullScreen) {
responsiveCloseNotificationTrayRef.current?.focus();
}
});
const isOpen = currentSidebar === sidebarId;

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

useEventListener('message', receiveMessage);

const focusSidebarTriggerBtn = () => {
const performFocus = () => {
const sidebarTriggerBtn = document.querySelector('.sidebar-trigger-btn');
if (sidebarTriggerBtn) {
sidebarTriggerBtn.focus();
}
};

requestAnimationFrame(() => {
requestAnimationFrame(performFocus);
});
};

const handleCloseNotificationTray = () => {
toggleSidebar(null);
setSessionStorage(`notificationTrayFocus.${courseId}`, 'true');
setSessionStorage(`notificationTrayStatus.${courseId}`, 'closed');
focusSidebarTriggerBtn();
};

const handleKeyDown = useCallback((event) => {
const { key, shiftKey, target } = event;

if (key !== 'Tab' || target !== closeBtnRef.current) {
return;
}

// Shift + Tab
if (shiftKey) {
event.preventDefault();
focusSidebarTriggerBtn();
return;
}

// Tab
const courseOutlineTrigger = document.querySelector('#courseOutlineTrigger');
if (courseOutlineTrigger) {
event.preventDefault();
courseOutlineTrigger.focus();
return;
}

const leftArrow = document.querySelector('.previous-button');
if (leftArrow && !leftArrow.disabled) {
event.preventDefault();
leftArrow.focus();
return;
}

const rightArrow = document.querySelector('.next-button');
if (rightArrow && !rightArrow.disabled) {
event.preventDefault();
rightArrow.focus();
}
}, [focusSidebarTriggerBtn, closeBtnRef]);

useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);

const handleKeyDownNotificationTray = (event) => {
const { key, shiftKey } = event;
const currentElement = event.target === responsiveCloseNotificationTrayRef.current;
const sidebarTriggerBtn = document.querySelector('.call-to-action-btn');

switch (key) {
case 'Enter':
if (currentElement) {
handleCloseNotificationTray();
}
break;

case 'Tab':
if (!shiftKey && sidebarTriggerBtn) {
event.preventDefault();
sidebarTriggerBtn.focus();
} else if (shiftKey) {
event.preventDefault();
responsiveCloseNotificationTrayRef.current?.focus();
}
break;

default:
break;
}
};

return (
<section
className={classNames('ml-0 border border-light-400 rounded-sm h-auto align-top zindex-0', {
'bg-white m-0 border-0 fixed-top vh-100 rounded-0': shouldDisplayFullScreen,
'align-self-start': !shouldDisplayFullScreen,
'd-none': currentSidebar !== sidebarId,
'd-none': !isOpen,
}, className)}
data-testid={`sidebar-${sidebarId}`}
style={{ width: shouldDisplayFullScreen ? '100%' : width }}
Expand All @@ -159,10 +63,10 @@ const SidebarBase = ({
{shouldDisplayFullScreen ? (
<div
className="pt-2 pb-2.5 border-bottom border-light-400 d-flex align-items-center ml-2"
onClick={handleCloseNotificationTray}
onKeyDown={handleKeyDownNotificationTray}
onClick={handleClose}
onKeyDown={handleBackBtnKeyDown}
role="button"
ref={responsiveCloseNotificationTrayRef}
ref={backBtnRef}
tabIndex="0"
>
<Icon src={ArrowBackIos} />
Expand All @@ -174,8 +78,6 @@ const SidebarBase = ({
{showTitleBar && (
<>
<div className="d-flex align-items-center mb-2">
{/* TODO: view this title in UI and decide */}
{/* <strong className="p-2.5 d-inline-block course-sidebar-title">{title}</strong> */}
<h2 className="p-2.5 d-inline-block m-0 text-gray-700 h4">{title}</h2>
{shouldDisplayFullScreen
? null
Expand All @@ -187,7 +89,8 @@ const SidebarBase = ({
size="sm"
ref={closeBtnRef}
iconAs={Icon}
onClick={handleCloseNotificationTray}
onClick={handleClose}
onKeyDown={handleKeyDown}
variant="primary"
alt={intl.formatMessage(messages.closeNotificationTrigger)}
/>
Expand Down
Loading