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();
+ });
+ });
+});