test: move completion modal coverage to client (#68127)

Co-authored-by: sembauke <sembauke@users.noreply.github.com>
This commit is contained in:
Sem Bauke
2026-06-23 11:38:00 -05:00
committed by GitHub
co-authored by sembauke
parent 44c6b2cd4a
commit 35ce17a2a4
3 changed files with 155 additions and 102 deletions
@@ -1,7 +1,8 @@
import React from 'react';
import type { TFunction } from 'i18next';
import { runSaga } from 'redux-saga';
import { describe, test, it, expect, beforeEach, vi, type Mock } from 'vitest';
import { render } from '../../../../utils/test-utils';
import { fireEvent, render, screen } from '../../../../utils/test-utils';
import { getCompletedPercentage } from '../../../utils/get-completion-percentage';
import { fireConfetti } from '../../../utils/fire-confetti';
@@ -18,16 +19,30 @@ import {
import { completedChallengesIdsSelector } from '../../../redux/selectors';
import { curriculumData } from '../../../services/curriculum-data';
import { getTestRunner } from '../utils/build';
import CompletionModal, { combineFileData } from './completion-modal';
import ConnectedCompletionModal, {
combineFileData,
CompletionModal
} from './completion-modal';
import { mockCurriculumData } from '../utils/__fixtures__/curriculum-data';
import { useStaticQuery } from 'gatsby';
import { ChallengeNode, SuperBlockStructure } from '../../../redux/prop-types';
vi.mock('../../../analytics');
vi.mock('../../../utils/fire-confetti');
vi.mock('../../../components/Progress');
vi.mock('../../../components/Progress', () => ({
default: () => <div data-testid='progress-bar-container' />
}));
vi.mock('../../../components/Header/components/login', () => ({
default: ({ children }: { children?: React.ReactNode }) => (
<a href='/learn'>{children}</a>
)
}));
vi.mock('../redux/selectors');
vi.mock('../../../redux/selectors');
vi.mock('../utils/build');
const mockSubmitChallenge = vi.hoisted(() => vi.fn());
vi.mock('../utils/fetch-all-curriculum-data', () => ({
useSubmit: () => mockSubmitChallenge
}));
vi.mock('../../../utils/get-words');
vi.mock('@freecodecamp/challenge-builder/build');
const mockFireConfetti = fireConfetti as Mock;
@@ -57,9 +72,45 @@ const completedChallengesIds = ['1', '3', '5'];
const currentBlockIds = ['1', '3', '5', '7'];
const id = '7';
const fakeCompletedChallengesIds = ['1', '3', '5', '7', '8'];
const t = ((key: string) => key) as TFunction;
function renderCompletionModal(
props: Partial<React.ComponentProps<typeof CompletionModal>> = {}
) {
return render(
<CompletionModal
challengeFiles={[]}
close={vi.fn()}
completedChallengesIds={[]}
dashedName='mock-challenge'
id='mock-id'
isOpen={true}
isSignedIn={true}
isSubmitting={false}
message='Great job'
t={t}
{...props}
/>,
createStore()
);
}
describe('<CompletionModal />', () => {
beforeEach(() => {
mockSubmitChallenge.mockClear();
vi.stubGlobal(
'ResizeObserver',
class ResizeObserver {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
}
);
vi.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Linux');
Object.defineProperty(window, 'innerWidth', {
configurable: true,
value: 1200
});
vi.mocked(useStaticQuery).mockReturnValue(mockCurriculumData);
// Initialize curriculum data singleton for tests
const structuresMap: Record<string, SuperBlockStructure> = {};
@@ -74,6 +125,104 @@ describe('<CompletionModal />', () => {
});
});
describe('rendering', () => {
it('renders the signed-out completion state', () => {
renderCompletionModal({ isSignedIn: false });
expect(
screen.getByRole('heading', { name: 'Great job' })
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
expect(
screen.getByTestId('fcc-completion-success-icon')
).toBeInTheDocument();
expect(screen.getByTestId('progress-bar-container')).toBeInTheDocument();
expect(
screen.getByRole('link', { name: 'learn.sign-in-save' })
).toHaveAttribute('href', '/learn');
expect(
screen.getByRole('button', { name: 'buttons.go-to-next-ctrl' })
).toBeInTheDocument();
});
it('renders the signed-in completion state', () => {
renderCompletionModal();
expect(
screen.queryByRole('link', { name: 'learn.sign-in-save' })
).not.toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'buttons.submit-and-go-ctrl' })
).toBeInTheDocument();
});
it('uses mobile button text when signed out on small screens', () => {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
value: 320
});
renderCompletionModal({ isSignedIn: false });
expect(
screen.getByRole('button', { name: 'buttons.go-to-next' })
).toBeInTheDocument();
});
it('uses Command button text on macOS desktops', () => {
vi.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mac OS');
renderCompletionModal();
expect(
screen.getByRole('button', { name: 'buttons.submit-and-go-cmd' })
).toBeInTheDocument();
});
it('uses mobile button text on small screens', () => {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
value: 320
});
renderCompletionModal();
expect(
screen.getByRole('button', { name: 'buttons.submit-and-go' })
).toBeInTheDocument();
});
it('closes when the close button is clicked', () => {
const close = vi.fn();
renderCompletionModal({ close });
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
expect(close).toHaveBeenCalled();
});
it('submits when the submit button is clicked', () => {
renderCompletionModal();
fireEvent.click(
screen.getByRole('button', { name: 'buttons.submit-and-go-ctrl' })
);
expect(mockSubmitChallenge).toHaveBeenCalled();
});
it('submits when the keyboard shortcut is pressed', () => {
renderCompletionModal();
fireEvent.keyDown(screen.getByRole('dialog'), {
ctrlKey: true,
key: 'Enter'
});
expect(mockSubmitChallenge).toHaveBeenCalled();
});
});
describe('fireConfetti', () => {
beforeEach(() => {
mockFireConfetti.mockClear();
@@ -161,7 +310,7 @@ describe('<CompletionModal />', () => {
}
}
});
render(<CompletionModal />, store);
render(<ConnectedCompletionModal />, store);
expect(mockFireConfetti).toHaveBeenCalledTimes(0);
});
@@ -176,7 +325,7 @@ describe('<CompletionModal />', () => {
}
});
render(<CompletionModal />, store);
render(<ConnectedCompletionModal />, store);
expect(mockFireConfetti).toHaveBeenCalledTimes(0);
});
@@ -77,7 +77,7 @@ interface DownloadableChallengeFile {
contents: string;
}
function CompletionModal({
export function CompletionModal({
challengeFiles,
close,
dashedName,
-96
View File
@@ -24,57 +24,6 @@ test.beforeEach(async ({ page }) => {
test.describe('Challenge Completion Modal Tests (Signed Out)', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('should render the modal correctly', async ({ page }) => {
await expect(page.getByRole('heading')).toBeVisible();
await expect(page.getByRole('button', { name: 'close' })).toBeVisible();
await expect(page.getByTestId('completion-success-icon')).toBeVisible();
await expect(page.getByTestId('progress-bar-container')).toBeVisible();
await expect(
page.getByRole('link', { name: translations.learn['sign-in-save'] })
).toBeVisible();
await expect(
page.getByRole('button', { name: translations.buttons['go-to-next'] })
).toBeVisible();
});
test('should close the modal after user click on close', async ({ page }) => {
await page.getByRole('button', { name: 'close' }).click();
await expect(page.getByTestId('completion-success-icon')).not.toBeVisible();
});
test('should close the modal after user presses escape', async ({ page }) => {
await expect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).not.toBeVisible();
});
test('should display the text of go to next challenge button accordingly based on device type', async ({
page,
isMobile,
browserName
}) => {
if (isMobile) {
await expect(
page.getByRole('button', {
name: 'Go to next challenge',
exact: true
})
).toBeVisible();
} else if (browserName === 'webkit') {
await expect(
page.getByRole('button', {
name: 'Go to next challenge (Command + Enter)'
})
).toBeVisible();
} else {
await expect(
page.getByRole('button', {
name: 'Go to next challenge (Ctrl + Enter)'
})
).toBeVisible();
}
});
test('should redirect to /learn after sign in', async ({ page }) => {
await page
.getByRole('link', { name: translations.learn['sign-in-save'] })
@@ -91,24 +40,6 @@ test.describe('Challenge Completion Modal Tests (Signed Out)', () => {
});
test.describe('Challenge Completion Modal Tests (Signed In)', () => {
test('should render the modal correctly', async ({ page }) => {
await expect(page.getByRole('heading')).toBeVisible();
await expect(page.getByRole('button', { name: 'close' })).toBeVisible();
await expect(page.getByTestId('completion-success-icon')).toBeVisible();
await expect(page.getByTestId('progress-bar-container')).toBeVisible();
await expect(
page.getByRole('link', { name: translations.learn['sign-in-save'] })
).not.toBeVisible();
await expect(
page.getByRole('button', { name: translations.buttons['submit-and-go'] })
).toBeVisible();
});
test('should close the modal after user click on close', async ({ page }) => {
await page.getByRole('button', { name: 'close' }).click();
await expect(page.getByTestId('completion-success-icon')).not.toBeVisible();
});
test('should close the modal after user presses escape while having keyboard shortcuts disabled', async ({
page,
request
@@ -143,33 +74,6 @@ test.describe('Challenge Completion Modal Tests (Signed In)', () => {
await expect(page.getByRole('dialog')).toBeVisible();
});
test('should display the text of go to next challenge button accordingly based on device type', async ({
page,
isMobile,
browserName
}) => {
if (isMobile) {
await expect(
page.getByRole('button', {
name: 'Submit and go to next challenge',
exact: true
})
).toBeVisible();
} else if (browserName === 'webkit') {
await expect(
page.getByRole('button', {
name: 'Submit and go to next challenge (Command + Enter)'
})
).toBeVisible();
} else {
await expect(
page.getByRole('button', {
name: 'Submit and go to next challenge (Ctrl + Enter)'
})
).toBeVisible();
}
});
test('should submit and go to the next challenge when the user clicks the submit button', async ({
page
}) => {