From 1bacf09dd559ca59db28e8e08a1fee0856f7f321 Mon Sep 17 00:00:00 2001 From: Tom <20648924+moT01@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:37:42 -0600 Subject: [PATCH] fix(client): use Link component for block links (#63559) --- .../components/block-header.test.tsx | 173 ++++++++++++++++++ .../Introduction/components/block-header.tsx | 72 ++++---- 2 files changed, 213 insertions(+), 32 deletions(-) create mode 100644 client/src/templates/Introduction/components/block-header.test.tsx diff --git a/client/src/templates/Introduction/components/block-header.test.tsx b/client/src/templates/Introduction/components/block-header.test.tsx new file mode 100644 index 00000000000..bafa7cf8f05 --- /dev/null +++ b/client/src/templates/Introduction/components/block-header.test.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; + +import BlockHeader from './block-header'; + +const defaultProps = { + blockDashed: 'test-block', + blockTitle: 'Test Block Title', + blockLabel: null, + courseCompletionStatus: '50% completed', + completedCount: 5, + handleClick: vi.fn(), + isCompleted: false, + isExpanded: false, + percentageCompleted: 50, + blockIntroArr: ['Introduction paragraph 1', 'Introduction paragraph 2'], + accordion: false +}; + +describe('', () => { + it('should render as a button with aria-expanded and aria-controls when not a link', () => { + render(); + + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('aria-expanded', 'false'); + expect(button).toHaveAttribute('aria-controls', 'test-block-panel'); + }); + + it('should render as a link without aria-expanded and aria-controls when blockUrl is provided in accordion mode', () => { + render( + + ); + + const link = screen.getByRole('link'); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '/learn/test-block'); + expect(link).not.toHaveAttribute('aria-expanded'); + expect(link).not.toHaveAttribute('aria-controls'); + }); + + it('should set aria-expanded to true when isExpanded is true', () => { + render(); + + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('aria-expanded', 'true'); + }); + + it('should display the block title', () => { + render(); + + expect(screen.getByText('Test Block Title')).toBeInTheDocument(); + }); + + it('should display the course completion status in sr-only text', () => { + render(); + + expect(screen.getByText(', 50% completed')).toBeInTheDocument(); + }); + + it('should show progress percentage when not expanded, not completed, and has completed challenges', () => { + render( + + ); + + expect(screen.getByText('50%')).toBeInTheDocument(); + }); + + it('should not show progress percentage when in accordion mode', () => { + render( + + ); + + expect(screen.queryByText('50%')).not.toBeInTheDocument(); + }); + + it('should not show progress percentage when expanded', () => { + render( + + ); + + expect(screen.queryByText('50%')).not.toBeInTheDocument(); + }); + + it('should not show progress percentage when completed', () => { + render( + + ); + + expect(screen.queryByText('50%')).not.toBeInTheDocument(); + }); + + it('should not show progress percentage when no challenges completed', () => { + render( + + ); + + expect(screen.queryByText('50%')).not.toBeInTheDocument(); + }); + + it('should render BlockIntros when expanded and blockIntroArr is provided', () => { + render(); + + expect(screen.getByText('Introduction paragraph 1')).toBeInTheDocument(); + expect(screen.getByText('Introduction paragraph 2')).toBeInTheDocument(); + }); + + it('should not render BlockIntros when not expanded', () => { + render(); + + expect( + screen.queryByText('Introduction paragraph 1') + ).not.toBeInTheDocument(); + expect( + screen.queryByText('Introduction paragraph 2') + ).not.toBeInTheDocument(); + }); + + it('should not render BlockIntros when blockIntroArr is empty', () => { + render( + + ); + + expect( + screen.queryByText('Introduction paragraph 1') + ).not.toBeInTheDocument(); + }); + + it('should not render BlockIntros when blockIntroArr is undefined', () => { + render( + + ); + + expect( + screen.queryByText('Introduction paragraph 1') + ).not.toBeInTheDocument(); + }); +}); diff --git a/client/src/templates/Introduction/components/block-header.tsx b/client/src/templates/Introduction/components/block-header.tsx index 3eeb3a37c4f..517fa33f217 100644 --- a/client/src/templates/Introduction/components/block-header.tsx +++ b/client/src/templates/Introduction/components/block-header.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { isEmpty } from 'lodash'; import { Button } from '@freecodecamp/ui'; +import { Link } from '../../../components/helpers'; import type { BlockLabel as BlockLabelType } from '../../../../../shared-dist/config/blocks'; import { ProgressBar } from '../../../components/Progress/progress-bar'; @@ -38,41 +39,48 @@ function BlockHeader({ accordion, blockUrl }: BlockHeaderProps): JSX.Element { + const InnerBlockHeader = () => ( + <> + + {accordion && + (blockUrl ? : )} + + {!accordion && blockLabel && } + + {blockTitle} + , {courseCompletionStatus} + + {accordion && blockLabel && } + {!accordion && } + + {!accordion && !isExpanded && !isCompleted && completedCount > 0 && ( + + )} + + ); + return ( <>

- + {accordion && blockUrl ? ( + + + + ) : ( + + )}

{isExpanded && !isEmpty(blockIntroArr) && (