test: move exam show coverage to client (#68172)

Co-authored-by: sembauke <sembauke@users.noreply.github.com>
This commit is contained in:
Sem Bauke
2026-06-26 09:05:03 +02:00
committed by GitHub
parent 4a3bbb0904
commit 7a8e8465cb
4 changed files with 363 additions and 191 deletions
@@ -0,0 +1,357 @@
import React from 'react';
import { Provider } from 'react-redux';
import { SuperBlocks } from '@freecodecamp/shared/config/curriculum';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useStaticQuery } from 'gatsby';
import { I18nextProvider } from 'react-i18next';
import { describe, expect, it, vi } from 'vitest';
import introTranslations from '../../../../i18n/locales/english/intro.json';
import i18nTestConfig from '../../../../i18n/config-for-tests';
import translations from '../../../../i18n/locales/english/translations.json';
import { initialState as appInitialState } from '../../../redux';
import { createStore } from '../../../redux/create-store';
import type {
ChallengeMeta,
ChallengeNode,
GeneratedExamQuestion
} from '../../../redux/prop-types';
import { getGenerateExam } from '../../../utils/ajax';
import ShowExam from './show';
vi.unmock('react-i18next');
i18nTestConfig.addResourceBundle('en', 'intro', introTranslations, true, true);
i18nTestConfig.addResourceBundle(
'en',
'translations',
translations,
true,
true
);
const t = i18nTestConfig.t.bind(i18nTestConfig);
vi.mock('../../../components/layouts/learn', () => ({
default: ({ children }: { children: React.ReactNode }) => (
<main>{children}</main>
)
}));
vi.mock('../../../utils/ajax', async () => {
const actual = await vi.importActual<typeof import('../../../utils/ajax')>(
'../../../utils/ajax'
);
return {
...actual,
getGenerateExam: vi.fn()
};
});
vi.mock('../../../utils/get-words');
const examBlock = 'foundational-c-sharp-with-microsoft-certification-exam';
const examTitle = t(
`intro:${SuperBlocks.FoundationalCSharp}.blocks.${examBlock}.title`
);
const windowTitle = `${examTitle}: ${examTitle} | freeCodeCamp.org`;
const examDescription =
'Pass this exam to earn your Foundational C# with Microsoft Certification. Before starting the exam, please review the following guidelines:';
const prerequisite = {
id: '647f85d407d29547b3bee1bb',
title: 'Write Your First C# Code',
slug: '/learn/foundational-c-sharp-with-microsoft/write-your-first-code-using-c-sharp/'
};
const completedExamRequirements = {
completedChallenges: [{ id: prerequisite.id }],
completedSurveys: [{ title: t('survey.foundational-c-sharp.title') }]
};
const generatedExam: GeneratedExamQuestion[] = Array.from(
{ length: 5 },
(_, index) => ({
id: `exam-question-${index + 1}`,
question: `Question ${index + 1}?`,
answers: [
{
id: `exam-question-${index + 1}-answer-1`,
answer: `Answer 1 for question ${index + 1}`
},
{
id: `exam-question-${index + 1}-answer-2`,
answer: `Answer 2 for question ${index + 1}`
}
]
})
);
const data: {
challengeNode: { challenge: Partial<ChallengeNode['challenge']> };
} = {
challengeNode: {
challenge: {
block: examBlock,
challengeType: 5,
dashedName: examBlock,
description: examDescription,
fields: {
blockHashSlug:
'/learn/foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/',
slug: '/learn/foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam'
},
helpCategory: 'Exam',
id: '647e22d18acb466c97ccbef8',
instructions: '',
prerequisites: [prerequisite],
superBlock: SuperBlocks.FoundationalCSharp,
tests: [],
title: examTitle,
translationPending: false
}
}
};
const pageContext: { challengeMeta: ChallengeMeta } = {
challengeMeta: {
block: examBlock,
challengeType: 5,
id: '647e22d18acb466c97ccbef8',
disableLoopProtectPreview: false,
disableLoopProtectTests: false,
helpCategory: 'Exam',
isFirstStep: false,
nextChallengePath: '/',
prevChallengePath: '/',
saveSubmissionToDB: false,
superBlock: SuperBlocks.FoundationalCSharp,
title: examTitle
}
};
function renderShowExam({
completedChallenges = [],
completedSurveys = []
}: {
completedChallenges?: { id: string }[];
completedSurveys?: { title: string }[];
} = {}) {
vi.mocked(useStaticQuery).mockReturnValue({
allChallengeNode: {
nodes: [
{
challenge: {
block: examBlock,
id: pageContext.challengeMeta.id
}
}
],
edges: [
{
node: {
challenge: {
id: prerequisite.id,
title: prerequisite.title,
fields: {
slug: prerequisite.slug
}
}
}
}
]
},
allCertificateNode: {
nodes: []
},
allSuperBlockStructure: {
nodes: []
}
});
vi.mocked(getGenerateExam).mockResolvedValue({
response: new Response(null, { status: 200 }),
data: { generatedExam }
});
const store = createStore({
app: {
...appInitialState,
examInProgress: false,
user: {
...appInitialState.user,
sessionUser: {
completedChallenges,
completedSurveys,
examResults: null
}
}
}
});
render(
<Provider store={store}>
<I18nextProvider i18n={i18nTestConfig}>
<ShowExam
data={data as { challengeNode: ChallengeNode }}
pageContext={pageContext}
/>
</I18nextProvider>
</Provider>
);
}
async function startExam() {
const user = userEvent.setup();
renderShowExam(completedExamRequirements);
await user.click(getButton('buttons.click-start-exam'));
await screen.findByText(getQuestionNumberText(1));
return user;
}
async function expectExamLandingPage(qualification: 'qualified' | 'blocked') {
await waitFor(() => expect(document.title).toBe(windowTitle));
expect(
screen.getByRole('heading', { level: 1, name: examTitle })
).toBeInTheDocument();
expect(screen.getByText(examDescription)).toBeInTheDocument();
if (qualification === 'qualified') {
expect(screen.getByText(t('learn.exam.qualified'))).toBeInTheDocument();
expect(
screen.queryByText(t('learn.exam.not-qualified'))
).not.toBeInTheDocument();
expect(getButton('buttons.click-start-exam')).not.toHaveAttribute(
'aria-disabled',
'true'
);
return;
}
expect(screen.queryByText(t('learn.exam.qualified'))).not.toBeInTheDocument();
expect(screen.getByText(t('learn.exam.not-qualified'))).toBeInTheDocument();
expect(
screen.getByRole('link', { name: prerequisite.title })
).toHaveAttribute('href', prerequisite.slug);
expect(getButton('buttons.click-start-exam')).toHaveAttribute(
'aria-disabled',
'true'
);
}
function expectStartedExamHeader() {
expect(document.title).toBe(windowTitle);
expect(screen.getByText(examTitle)).toBeInTheDocument();
expect(
screen.getByText(t('learn.exam.time', { t: '0:00' }))
).toBeInTheDocument();
}
function getQuestionNumberText(questionNumber: number) {
return t('learn.exam.questions', {
n: questionNumber,
t: generatedExam.length
});
}
function expectQuestionNumber(questionNumber: number) {
expect(
screen.getByText(getQuestionNumberText(questionNumber))
).toBeInTheDocument();
}
function getButton(translationKey: string) {
return screen.getByRole('button', { name: t(translationKey) });
}
function queryButton(translationKey: string) {
return screen.queryByRole('button', { name: t(translationKey) });
}
async function answerCurrentQuestion(user: ReturnType<typeof userEvent.setup>) {
await user.click(screen.getAllByRole('radio')[0]);
}
async function answerCurrentQuestionAndGoNext(
user: ReturnType<typeof userEvent.setup>
) {
await answerCurrentQuestion(user);
await user.click(getButton('buttons.next-question'));
}
describe('<ShowExam /> non-qualified state', () => {
it('renders the non-qualified exam page with a disabled start button', async () => {
expect.hasAssertions();
renderShowExam();
await expectExamLandingPage('blocked');
});
});
describe('<ShowExam /> qualified state', () => {
it('renders the qualified exam page with an enabled start button', async () => {
expect.hasAssertions();
renderShowExam(completedExamRequirements);
await expectExamLandingPage('qualified');
});
});
describe('<ShowExam /> started exam state', () => {
it('renders the started exam page and moves through the questions', async () => {
const user = await startExam();
expectStartedExamHeader();
expectQuestionNumber(1);
expect(getButton('buttons.previous-question')).toHaveAttribute(
'aria-disabled',
'true'
);
await answerCurrentQuestionAndGoNext(user);
for (
let questionNumber = 2;
questionNumber < generatedExam.length;
questionNumber++
) {
expectQuestionNumber(questionNumber);
expect(getButton('buttons.previous-question')).not.toHaveAttribute(
'aria-disabled',
'true'
);
expect(getButton('buttons.exit-exam')).not.toHaveAttribute(
'aria-disabled',
'true'
);
expect(queryButton('buttons.finish-exam')).not.toBeInTheDocument();
await answerCurrentQuestionAndGoNext(user);
}
expectQuestionNumber(generatedExam.length);
expect(getButton('buttons.previous-question')).not.toHaveAttribute(
'aria-disabled',
'true'
);
await answerCurrentQuestion(user);
expect(queryButton('buttons.next-question')).not.toBeInTheDocument();
expect(getButton('buttons.finish-exam')).not.toHaveAttribute(
'aria-disabled',
'true'
);
});
it('moves back to the previous question', async () => {
expect.hasAssertions();
const user = await startExam();
await answerCurrentQuestionAndGoNext(user);
expectQuestionNumber(2);
await user.click(getButton('buttons.previous-question'));
expectQuestionNumber(1);
});
});
-54
View File
@@ -1,54 +0,0 @@
import { test, expect } from '@playwright/test';
import intro from '../client/i18n/locales/english/intro.json';
import translations from '../client/i18n/locales/english/translations.json';
const examUrl =
'/learn/foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam';
test.use({ storageState: { cookies: [], origins: [] } });
test.beforeEach(async ({ page }) => {
await page.goto(examUrl);
});
test.describe('Exam Show E2E Test Suite for non-qualified user', () => {
test('The page renders with correct title', async ({ page }) => {
await expect(page).toHaveTitle(
'Foundational C# with Microsoft Certification Exam: Foundational C# with Microsoft Certification Exam | freeCodeCamp.org'
);
});
test('The page has correct header', async ({ page }) => {
const header = page.getByTestId('challenge-title');
await expect(header).toBeVisible();
await expect(header).toContainText(
intro['foundational-c-sharp-with-microsoft'].blocks[
'foundational-c-sharp-with-microsoft-certification-exam'
].title
);
});
test('The page has an alert informing the user of their eligibility', async ({
page
}) => {
await expect(
page.getByText(translations.learn.exam.qualified)
).not.toBeVisible();
await expect(
page.getByText(translations.learn.exam['not-qualified'])
).toBeVisible();
});
test('Verifies the Correct Rendering of the Exam show non-qualified', async ({
page
}) => {
const startExam = page.getByRole('button', {
name: translations.buttons['click-start-exam']
});
await startExam.isVisible();
await startExam.isDisabled();
await expect(
page.getByText(
'Pass this exam to earn your Foundational C# with Microsoft Certification. Before starting the exam, please review the following guidelines:'
)
).toBeVisible();
});
});
+6 -41
View File
@@ -10,52 +10,17 @@ test.beforeEach(async ({ page }) => {
});
test.describe('Exam Show E2E Test Suite for qualified user', () => {
test('The page renders with correct title', async ({ page }) => {
await expect(page).toHaveTitle(
'Foundational C# with Microsoft Certification Exam: Foundational C# with Microsoft Certification Exam | freeCodeCamp.org'
);
});
test('The page has correct header', async ({ page }) => {
const header = page.getByTestId('challenge-title');
await expect(header).toBeVisible();
await expect(header).toContainText(
intro['foundational-c-sharp-with-microsoft'].blocks[
'foundational-c-sharp-with-microsoft-certification-exam'
].title
);
});
test('The page has qualified for exam alert ', async ({ page }) => {
await expect(
page.getByText(translations.learn.exam['not-qualified'])
).not.toBeVisible();
await expect(
page.getByText(translations.learn.exam.qualified)
).toBeVisible();
});
test('Verifies the Correct Rendering of the Exam show', async ({ page }) => {
const startExam = page.getByRole('button', {
name: translations.buttons['click-start-exam']
});
await startExam.isVisible();
await startExam.isEnabled();
await expect(
page.getByText(
'Pass this exam to earn your Foundational C# with Microsoft Certification. Before starting the exam, please review the following guidelines:'
)
).toBeVisible();
});
test('Exam Show When the User clicks on Start exam button', async ({
page
}) => {
test('starts the exam without navigating away', async ({ page }) => {
await page
.getByRole('button', {
name: translations.buttons['click-start-exam']
})
.click();
await expect(page).toHaveURL(examUrl);
await expect(page.getByTestId('exam-show-title')).toHaveText(
intro['foundational-c-sharp-with-microsoft'].blocks[
'foundational-c-sharp-with-microsoft-certification-exam'
].title
);
});
});
-96
View File
@@ -1,96 +0,0 @@
import { test, expect } from '@playwright/test';
import translations from '../client/i18n/locales/english/translations.json';
import intro from '../client/i18n/locales/english/intro.json';
const examUrl =
'/learn/foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam';
test.beforeEach(async ({ page }) => {
await page.goto(examUrl);
await page
.getByRole('button', {
name: translations.buttons['click-start-exam']
})
.click();
});
test.describe('Exam Show E2E Test Suite for started exam', () => {
test('The page renders with correct title', async ({ page }) => {
await expect(page).toHaveTitle(
'Foundational C# with Microsoft Certification Exam: Foundational C# with Microsoft Certification Exam | freeCodeCamp.org'
);
});
test('The page has correct navigation direct flow', async ({ page }) => {
const QUESTION_COUNT = 5;
const headerTitle = page.getByTestId('exam-show-title');
const prevQuestionBtn = page.getByRole('button', {
name: translations.buttons['previous-question']
});
const exitButton = page.getByRole('button', {
name: translations.buttons['exit-exam']
});
const nextBtn = page.getByRole('button', {
name: translations.buttons['next-question']
});
const finishExamBtn = page.getByRole('button', {
name: translations.buttons['finish-exam']
});
for (let i = 0; i < QUESTION_COUNT; i++) {
await expect(headerTitle).toBeVisible();
await expect(headerTitle).toContainText(
intro['foundational-c-sharp-with-microsoft'].blocks[
'foundational-c-sharp-with-microsoft-certification-exam'
].title
);
await expect(page.getByTestId('exam-show-question-time')).toContainText(
translations.learn.exam.time.split(':')[0]
);
await expect(
page.getByText(`Question ${i + 1} of ${QUESTION_COUNT}`)
).toBeVisible();
await expect(prevQuestionBtn).toBeVisible();
if (i != 0) {
await expect(prevQuestionBtn).toBeEnabled();
} else {
await expect(prevQuestionBtn).not.toBeEnabled();
}
await page.getByRole('radio').first().check({ force: true });
await expect(exitButton).toBeVisible();
await expect(exitButton).toBeEnabled();
if (i < QUESTION_COUNT - 1) {
await expect(finishExamBtn).not.toBeVisible();
await expect(nextBtn).toBeVisible();
await nextBtn.click();
} else {
await expect(finishExamBtn).toBeEnabled();
await expect(finishExamBtn).toBeVisible();
await expect(nextBtn).not.toBeVisible();
}
}
});
test('The page has correct navigation back flow', async ({ page }) => {
const headerTitle = page.getByTestId('exam-show-title');
await expect(headerTitle).toBeVisible();
await expect(headerTitle).toContainText(
intro['foundational-c-sharp-with-microsoft'].blocks[
'foundational-c-sharp-with-microsoft-certification-exam'
].title
);
await expect(page.getByTestId('exam-show-question-time')).toContainText(
translations.learn.exam.time.split(':')[0]
);
await page.getByRole('radio').first().check({ force: true });
await page
.getByRole('button', { name: translations.buttons['next-question'] })
.click();
await expect(page.getByText('Question 2 of 5')).toBeVisible();
await page
.getByRole('button', { name: translations.buttons['previous-question'] })
.click();
await expect(page.getByText('Question 1 of 5')).toBeVisible();
});
});