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 && (
+
+
+
{`${percentageCompleted}%`}
+
+ )}
+ >
+ );
+
return (
<>
-
+ {accordion && blockUrl ? (
+
+
+
+ ) : (
+
+ )}
{isExpanded && !isEmpty(blockIntroArr) && (