mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-10-16 15:49:20 +08:00
test: move completion modal coverage to client (#68127)
Co-authored-by: sembauke <sembauke@users.noreply.github.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user