From 76ca3cf0be7a3788393efdad750feefe08c8c5fb Mon Sep 17 00:00:00 2001 From: vladislavkeblysh Date: Wed, 5 Mar 2025 11:26:54 +0200 Subject: [PATCH 1/2] feat: added the ability to close the hint using the keyboard --- .../grade-summary/GradeSummaryHeader.jsx | 15 +++++- .../grade-summary/GradeSummaryHeader.test.jsx | 54 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.test.jsx 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..4804eea053 --- /dev/null +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.test.jsx @@ -0,0 +1,54 @@ +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 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(), +})); + +describe('GradeSummaryHeader', () => { + beforeEach(() => { + useSelector.mockImplementation((selector) => selector({ + courseHome: { courseId: 'test-course-id' }, + })); + useModel.mockReturnValue({ gradesFeatureIsFullyLocked: false }); + }); + + const renderComponent = (props = {}) => { + render( + + msg.defaultMessage) }} + allOfSomeAssignmentTypeIsLocked={false} + {...props} + /> + , + ); + }; + + it('visible the tooltip when Escape is pressed', async () => { + renderComponent(); + + const iconButton = screen.getByRole('button', { + name: messages.gradeSummaryTooltipAlt.defaultMessage, + }); + + userEvent.click(iconButton); + + await waitFor(() => { + expect(screen.getByText(messages.gradeSummaryTooltipBody.defaultMessage)).toBeVisible(); + }); + }); +}); From 2f5866608b07b5b0115c62e31553648404a40a50 Mon Sep 17 00:00:00 2001 From: vladislavkeblysh Date: Tue, 15 Apr 2025 16:21:59 +0300 Subject: [PATCH 2/2] feat: refactor tests --- src/course-home/live-tab/LiveTab.test.jsx | 77 +++++++++++++++++ .../grade-summary/GradeSummaryHeader.test.jsx | 85 +++++++++++++++++-- 2 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 src/course-home/live-tab/LiveTab.test.jsx 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.test.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.test.jsx index 4804eea053..39eb1aa6da 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.test.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.test.jsx @@ -1,11 +1,10 @@ import React from 'react'; -import { - render, screen, waitFor, -} from '@testing-library/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'; @@ -18,6 +17,10 @@ jest.mock('../../../../generic/model-store', () => ({ useModel: jest.fn(), })); +jest.mock('../../../../data/hooks', () => ({ + useContextId: () => 'test-course-id', +})); + describe('GradeSummaryHeader', () => { beforeEach(() => { useSelector.mockImplementation((selector) => selector({ @@ -30,7 +33,6 @@ describe('GradeSummaryHeader', () => { render( msg.defaultMessage) }} allOfSomeAssignmentTypeIsLocked={false} {...props} /> @@ -38,17 +40,88 @@ describe('GradeSummaryHeader', () => { ); }; - it('visible the tooltip when Escape is pressed', async () => { + 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, }); - userEvent.click(iconButton); + 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(); + }); }); });