diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx index df1ff65836..47b9ff8c33 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -6,12 +7,13 @@ import { OverlayTrigger, Stack, Tooltip, + IconButton, } from '@openedx/paragon'; import { InfoOutline, Locked } from '@openedx/paragon/icons'; -import { useContextId } from '../../../../data/hooks'; +import { useContextId } from '@src/data/hooks'; +import { useModel } from '@src/generic/model-store'; import messages from '../messages'; -import { useModel } from '../../../../generic/model-store'; const GradeSummaryHeader = ({ allOfSomeAssignmentTypeIsLocked }) => { const intl = useIntl(); @@ -20,24 +22,36 @@ const GradeSummaryHeader = ({ allOfSomeAssignmentTypeIsLocked }) => { verifiedMode, gradesFeatureIsFullyLocked, } = useModel('progress', courseId); + const [showTooltip, setShowTooltip] = useState(false); + + const handleKeyDown = (event) => { + if (event.key === 'Escape') { + setShowTooltip(false); + } + }; return (

{intl.formatMessage(messages.gradeSummary)}

{intl.formatMessage(messages.gradeSummaryTooltipBody)} )} > - setShowTooltip(!showTooltip)} + onBlur={() => setShowTooltip(false)} + onKeyDown={handleKeyDown} alt={intl.formatMessage(messages.gradeSummaryTooltipAlt)} - src={InfoOutline} + iconAs={InfoOutline} size="sm" + disabled={gradesFeatureIsFullyLocked} />
diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.test.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.test.jsx new file mode 100644 index 0000000000..cdd3ca1af3 --- /dev/null +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.test.jsx @@ -0,0 +1,125 @@ +import userEvent from '@testing-library/user-event'; +import { useSelector } from 'react-redux'; + +import { useModel } from '@src/generic/model-store'; +import { + initializeMockApp, render, screen, waitFor, +} from '@src/setupTest'; +import GradeSummaryHeader from './GradeSummaryHeader'; +import messages from '../messages'; + +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), +})); + +jest.mock('@src/generic/model-store', () => ({ + useModel: jest.fn(), +})); + +jest.mock('@src/data/hooks', () => ({ + useContextId: () => 'test-course-id', +})); + +describe('GradeSummaryHeader', () => { + beforeAll(() => { + initializeMockApp(); + }); + + beforeEach(() => { + useSelector.mockImplementation((selector) => selector({ + courseHome: { courseId: 'test-course-id' }, + })); + useModel.mockReturnValue({ gradesFeatureIsFullyLocked: false }); + }); + + const renderComponent = (props = {}) => { + render( + , + ); + }; + + it('shows tooltip on icon button click', async () => { + renderComponent(); + + const iconButton = screen.getByRole('button', { + name: messages.gradeSummaryTooltipAlt.defaultMessage, + }); + + await userEvent.click(iconButton); + + await waitFor(() => { + expect(screen.getByText(messages.gradeSummaryTooltipBody.defaultMessage)).toBeInTheDocument(); + }); + }); + + it('hides tooltip on click', async () => { + renderComponent(); + + const iconButton = screen.getByRole('button', { + name: messages.gradeSummaryTooltipAlt.defaultMessage, + }); + + await userEvent.click(iconButton); + + await waitFor(() => { + expect(screen.getByText(messages.gradeSummaryTooltipBody.defaultMessage)).toBeVisible(); + }); + + await userEvent.click(iconButton); + + await waitFor(() => { + expect(screen.queryByText(messages.gradeSummaryTooltipBody.defaultMessage)).toBeNull(); + }); + }); + + it('hides tooltip on blur', async () => { + renderComponent(); + + const iconButton = screen.getByRole('button', { + name: messages.gradeSummaryTooltipAlt.defaultMessage, + }); + + await userEvent.hover(iconButton); + await userEvent.click(iconButton); + + await waitFor(() => { + expect(screen.getByText(messages.gradeSummaryTooltipBody.defaultMessage)).toBeInTheDocument(); + }); + + const blurTarget = document.createElement('button'); + blurTarget.textContent = 'Outside'; + document.body.appendChild(blurTarget); + + await userEvent.click(blurTarget); + + await waitFor(() => { + expect(screen.queryByText(messages.gradeSummaryTooltipBody.defaultMessage)).not.toBeInTheDocument(); + }); + + document.body.removeChild(blurTarget); + }); + + it('hides tooltip when Escape is pressed (covers handleKeyDown)', async () => { + renderComponent(); + + const iconButton = screen.getByRole('button', { + name: messages.gradeSummaryTooltipAlt.defaultMessage, + }); + + await userEvent.hover(iconButton); + await userEvent.click(iconButton); + + await waitFor(() => { + expect(screen.getByText(messages.gradeSummaryTooltipBody.defaultMessage)).toBeInTheDocument(); + }); + + await userEvent.keyboard('{Escape}'); + + await waitFor(() => { + expect(screen.queryByText(messages.gradeSummaryTooltipBody.defaultMessage)).not.toBeInTheDocument(); + }); + }); +});