diff --git a/src/course-home/live-tab/LiveTab.test.jsx b/src/course-home/live-tab/LiveTab.test.jsx new file mode 100644 index 0000000000..71ffb4f4ba --- /dev/null +++ b/src/course-home/live-tab/LiveTab.test.jsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { useSelector } from 'react-redux'; +import LiveTab from './LiveTab'; + +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), +})); + +describe('LiveTab', () => { + afterEach(() => { + jest.clearAllMocks(); + document.body.innerHTML = ''; + }); + + it('renders iframe from liveModel using dangerouslySetInnerHTML', () => { + useSelector.mockImplementation((selector) => selector({ + courseHome: { courseId: 'course-v1:test+id+2024' }, + models: { + live: { + 'course-v1:test+id+2024': { + iframe: '', + }, + }, + }, + })); + + render(); + + const iframe = document.getElementById('lti-tab-embed'); + expect(iframe).toBeInTheDocument(); + expect(iframe.src).toBe('about:blank'); + }); + + it('adds classes to iframe after mount', () => { + document.body.innerHTML = ` +
+ +
+ `; + + useSelector.mockImplementation((selector) => selector({ + courseHome: { courseId: 'course-v1:test+id+2024' }, + models: { + live: { + 'course-v1:test+id+2024': { + iframe: '', + }, + }, + }, + })); + + render(); + + const iframe = document.getElementById('lti-tab-embed'); + expect(iframe.className).toContain('vh-100'); + expect(iframe.className).toContain('w-100'); + expect(iframe.className).toContain('border-0'); + }); + + it('does not throw if iframe is not found in DOM', () => { + useSelector.mockImplementation((selector) => selector({ + courseHome: { courseId: 'course-v1:test+id+2024' }, + models: { + live: { + 'course-v1:test+id+2024': { + iframe: '
No iframe here
', + }, + }, + }, + })); + + expect(() => render()).not.toThrow(); + const iframe = document.getElementById('lti-tab-embed'); + expect(iframe).toBeNull(); + }); +}); 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..85603314a0 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 React, { useState } from 'react'; import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -6,6 +7,7 @@ import { OverlayTrigger, Stack, Tooltip, + IconButton, } from '@openedx/paragon'; import { InfoOutline, Locked } from '@openedx/paragon/icons'; import { useContextId } from '../../../../data/hooks'; @@ -20,6 +22,13 @@ const GradeSummaryHeader = ({ allOfSomeAssignmentTypeIsLocked }) => { verifiedMode, gradesFeatureIsFullyLocked, } = useModel('progress', courseId); + const [showTooltip, setShowTooltip] = useState(false); + + const handleKeyDown = (event) => { + if (event.key === 'Escape') { + setShowTooltip(false); + } + }; return ( @@ -34,9 +43,13 @@ const GradeSummaryHeader = ({ allOfSomeAssignmentTypeIsLocked }) => { )} > - { setShowTooltip(!showTooltip); }} + onBlur={() => { setShowTooltip(false); }} + onKeyDown={handleKeyDown} alt={intl.formatMessage(messages.gradeSummaryTooltipAlt)} src={InfoOutline} + className="mb-3" size="sm" /> 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..39eb1aa6da --- /dev/null +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.test.jsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useSelector } from 'react-redux'; +import { IntlProvider } from 'react-intl'; + +import { fireEvent } from '@testing-library/dom'; +import GradeSummaryHeader from './GradeSummaryHeader'; +import { useModel } from '../../../../generic/model-store'; +import messages from '../messages'; + +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), +})); + +jest.mock('../../../../generic/model-store', () => ({ + useModel: jest.fn(), +})); + +jest.mock('../../../../data/hooks', () => ({ + useContextId: () => 'test-course-id', +})); + +describe('GradeSummaryHeader', () => { + 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 mouse out', async () => { + renderComponent(); + + const iconButton = screen.getByRole('button', { + name: messages.gradeSummaryTooltipAlt.defaultMessage, + }); + + fireEvent.mouseOver(iconButton); + + await waitFor(() => { + expect(screen.getByText(messages.gradeSummaryTooltipBody.defaultMessage)).toBeVisible(); + }); + + fireEvent.mouseOut(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); + blurTarget.focus(); + + await userEvent.unhover(iconButton); + + 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(); + }); + + fireEvent.keyDown(iconButton, { key: 'Escape', code: 'Escape' }); + + await userEvent.unhover(iconButton); + + await waitFor(() => { + expect(screen.queryByText(messages.gradeSummaryTooltipBody.defaultMessage)).not.toBeInTheDocument(); + }); + }); +});