mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 10:22:16 +00:00
254 lines
7.3 KiB
TypeScript
254 lines
7.3 KiB
TypeScript
import React from 'react';
|
|
import { render, screen, within } from '@testing-library/react';
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import '@testing-library/jest-dom/vitest';
|
|
import { SuperBlocks } from '@freecodecamp/shared/config/curriculum';
|
|
import { SuperBlockAccordion } from './super-block-accordion';
|
|
import { BlockLabel, BlockLayouts } from '@freecodecamp/shared/config/blocks';
|
|
|
|
vi.mock('./block', () => ({
|
|
default: ({ block }: { block: string }) =>
|
|
React.createElement('div', { 'data-testid': `block-${block}` })
|
|
}));
|
|
|
|
const mockStructure = {
|
|
superBlock: SuperBlocks.RespWebDesign,
|
|
chapters: [
|
|
{
|
|
dashedName: 'test-chapter',
|
|
modules: [
|
|
{
|
|
dashedName: 'test-module',
|
|
blocks: ['test-block']
|
|
}
|
|
]
|
|
}
|
|
]
|
|
};
|
|
|
|
// Representative challenge used to populate module content.
|
|
const mockChallenge = {
|
|
id: 'test-challenge-id',
|
|
block: 'test-block',
|
|
blockLabel: BlockLabel.lecture,
|
|
title: 'Test Challenge',
|
|
fields: { slug: '/test-slug' },
|
|
dashedName: 'test-challenge',
|
|
challengeType: 0,
|
|
blockLayout: BlockLayouts.ChallengeList,
|
|
superBlock: SuperBlocks.RespWebDesign
|
|
};
|
|
|
|
describe('SuperBlockAccordion', () => {
|
|
it('does not show completed checkmark when there are zero challenges in a chapter', () => {
|
|
render(
|
|
<SuperBlockAccordion
|
|
challenges={[]}
|
|
superBlock={SuperBlocks.RespWebDesign}
|
|
structure={mockStructure}
|
|
chosenBlock={''}
|
|
completedChallengeIds={[]}
|
|
/>
|
|
);
|
|
|
|
expect(screen.queryByTestId('green-pass')).not.toBeInTheDocument();
|
|
|
|
const chapterButtons = screen.getAllByRole('button', {
|
|
name: /test-chapter/i
|
|
});
|
|
const chapterButton = chapterButtons[0];
|
|
const checkmark = within(chapterButton).getByTestId('green-not-completed');
|
|
expect(checkmark).toBeInTheDocument();
|
|
});
|
|
|
|
// With progress data present, the module button should render the steps label.
|
|
it('shows module progress when totalSteps > 0 and not comingSoon', () => {
|
|
const structureWithProgress = {
|
|
superBlock: SuperBlocks.RespWebDesign,
|
|
chapters: [
|
|
{
|
|
dashedName: 'test-chapter',
|
|
modules: [
|
|
{
|
|
dashedName: 'test-module',
|
|
blocks: ['test-block'],
|
|
comingSoon: false,
|
|
totalSteps: 10,
|
|
completedSteps: 5
|
|
}
|
|
]
|
|
}
|
|
]
|
|
};
|
|
|
|
render(
|
|
<SuperBlockAccordion
|
|
challenges={[mockChallenge]}
|
|
superBlock={SuperBlocks.RespWebDesign}
|
|
structure={structureWithProgress}
|
|
chosenBlock={''}
|
|
completedChallengeIds={[]}
|
|
/>
|
|
);
|
|
|
|
const moduleButtons = screen.getAllByRole('button', {
|
|
name: /test-module/i
|
|
});
|
|
const moduleButton = moduleButtons[0];
|
|
|
|
const moduleRight = within(moduleButton).getByTestId('module-button-right');
|
|
const moduleSteps = within(moduleRight).getByText(
|
|
/learn\.steps-completed/i
|
|
);
|
|
expect(moduleSteps).toBeInTheDocument();
|
|
expect(moduleSteps).toHaveClass('module-steps');
|
|
});
|
|
|
|
// A coming-soon module should hide the steps summary even if progress exists.
|
|
it('does not show module progress when comingSoon is true', () => {
|
|
const structureComingSoon = {
|
|
superBlock: SuperBlocks.RespWebDesign,
|
|
chapters: [
|
|
{
|
|
dashedName: 'test-chapter',
|
|
modules: [
|
|
{
|
|
dashedName: 'test-module',
|
|
blocks: ['test-block'],
|
|
comingSoon: true,
|
|
totalSteps: 10,
|
|
completedSteps: 5
|
|
}
|
|
]
|
|
}
|
|
]
|
|
};
|
|
|
|
render(
|
|
<SuperBlockAccordion
|
|
challenges={[mockChallenge]}
|
|
superBlock={SuperBlocks.RespWebDesign}
|
|
structure={structureComingSoon}
|
|
chosenBlock={''}
|
|
completedChallengeIds={[]}
|
|
/>
|
|
);
|
|
|
|
const moduleButtons = screen.getAllByRole('button', {
|
|
name: /test-module/i
|
|
});
|
|
const moduleButton = moduleButtons[0];
|
|
|
|
const moduleRight = within(moduleButton).getByTestId('module-button-right');
|
|
expect(within(moduleRight).queryByText(/steps/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
// Modules with zero total steps should not display any progress text.
|
|
it('does not show module progress when totalSteps is zero', () => {
|
|
const structureZeroSteps = {
|
|
superBlock: SuperBlocks.RespWebDesign,
|
|
chapters: [
|
|
{
|
|
dashedName: 'test-chapter',
|
|
modules: [
|
|
{
|
|
dashedName: 'test-module',
|
|
blocks: ['test-block'],
|
|
comingSoon: false,
|
|
totalSteps: 0,
|
|
completedSteps: 0
|
|
}
|
|
]
|
|
}
|
|
]
|
|
};
|
|
|
|
render(
|
|
<SuperBlockAccordion
|
|
challenges={[]}
|
|
superBlock={SuperBlocks.RespWebDesign}
|
|
structure={structureZeroSteps}
|
|
chosenBlock={''}
|
|
completedChallengeIds={[]}
|
|
/>
|
|
);
|
|
|
|
const moduleButtons = screen.getAllByRole('button', {
|
|
name: /test-module/i
|
|
});
|
|
const moduleButton = moduleButtons[0];
|
|
|
|
const moduleRight = within(moduleButton).getByTestId('module-button-right');
|
|
expect(within(moduleRight).queryByText(/steps/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should expand all chapters when expandAll is true', () => {
|
|
const multiChapterStructure = {
|
|
superBlock: SuperBlocks.RespWebDesign,
|
|
chapters: [
|
|
{
|
|
dashedName: 'chapter-one',
|
|
modules: [{ dashedName: 'mod-one', blocks: ['block-one'] }]
|
|
},
|
|
{
|
|
dashedName: 'chapter-two',
|
|
modules: [{ dashedName: 'mod-two', blocks: ['block-two'] }]
|
|
}
|
|
]
|
|
};
|
|
|
|
render(
|
|
<SuperBlockAccordion
|
|
challenges={[
|
|
{ ...mockChallenge, block: 'block-one', id: 'id-1' },
|
|
{ ...mockChallenge, block: 'block-two', id: 'id-2' }
|
|
]}
|
|
superBlock={SuperBlocks.RespWebDesign}
|
|
structure={multiChapterStructure}
|
|
chosenBlock={''}
|
|
completedChallengeIds={[]}
|
|
expandAll={true}
|
|
/>
|
|
);
|
|
|
|
// When expandAll=true, both chapters are open so their module buttons are visible
|
|
const moduleButtons = screen.getAllByRole('button', { name: /mod/i });
|
|
expect(moduleButtons).toHaveLength(2);
|
|
});
|
|
|
|
it('should not render a module when all its challenges are filtered out', () => {
|
|
render(
|
|
<SuperBlockAccordion
|
|
// Only challenges for block-one are passed; mod-two has no challenges
|
|
challenges={[{ ...mockChallenge, block: 'block-one', id: 'id-1' }]}
|
|
superBlock={SuperBlocks.RespWebDesign}
|
|
structure={{
|
|
superBlock: SuperBlocks.RespWebDesign,
|
|
chapters: [
|
|
{
|
|
dashedName: 'test-chapter',
|
|
modules: [
|
|
{ dashedName: 'mod-one', blocks: ['block-one'] },
|
|
{ dashedName: 'mod-two', blocks: ['block-two'] }
|
|
]
|
|
}
|
|
]
|
|
}}
|
|
chosenBlock={'block-one'}
|
|
completedChallengeIds={[]}
|
|
expandAll={true}
|
|
/>
|
|
);
|
|
|
|
// mod-one has a challenge — its button should render
|
|
expect(
|
|
screen.getByRole('button', { name: /mod-one/i })
|
|
).toBeInTheDocument();
|
|
|
|
// mod-two has no challenges — its button should not render
|
|
expect(
|
|
screen.queryByRole('button', { name: /mod-two/i })
|
|
).not.toBeInTheDocument();
|
|
});
|
|
});
|