diff --git a/__tests__/pages/exercises/[lessonSlug].test.js b/__tests__/pages/exercises/[lessonSlug].test.js index 738271e14..e7a17d734 100644 --- a/__tests__/pages/exercises/[lessonSlug].test.js +++ b/__tests__/pages/exercises/[lessonSlug].test.js @@ -1,5 +1,5 @@ import React from 'react' -import { render, waitFor, screen, fireEvent } from '@testing-library/react' +import { render, screen, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom' import Exercises from '../../../pages/exercises/[lessonSlug]' import { useRouter } from 'next/router' @@ -28,9 +28,7 @@ describe('Exercises page', () => { ) - await waitFor(() => - screen.getByRole('heading', { name: /Foundations of JavaScript/i }) - ) + await screen.findByRole('heading', { name: /Foundations of JavaScript/i }) screen.getByRole('link', { name: 'CHALLENGES' }) screen.getByRole('link', { name: 'EXERCISES' }) @@ -66,15 +64,13 @@ describe('Exercises page', () => { } ] - const { getByRole, queryByRole, getByLabelText } = render( + const { getByRole, findByRole, queryByRole, getByLabelText } = render( ) - await waitFor(() => - getByRole('heading', { name: /Foundations of JavaScript/i }) - ) + await findByRole('heading', { name: /Foundations of JavaScript/i }) const solveExercisesButton = getByRole('button', { name: 'SOLVE EXERCISES' @@ -111,6 +107,115 @@ describe('Exercises page', () => { expect(queryByRole('button', { name: 'SKIP' })).not.toBeInTheDocument() }) + test('Renders different exercises depending on whether the show-only-incompleted checkbox is checked', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: getExercisesData + } + }, + { + request: { + query: ADD_EXERCISE_SUBMISSION, + variables: { + exerciseId: 2, + userAnswer: 'blah blah' + } + }, + result: { + data: { + addExerciseSubmissions: { + id: 1, + exerciseId: 2, + userId: 4, + userAnswer: 'blah blah' + } + } + } + }, + { + request: { + query: ADD_EXERCISE_SUBMISSION, + variables: { + exerciseId: 3, + userAnswer: '-1' + } + }, + result: { + data: { + addExerciseSubmissions: { + id: 1, + exerciseId: 3, + userId: 4, + userAnswer: '-1' + } + } + } + } + ] + + const { findByLabelText, findAllByText, findByRole } = render( + + + + ) + + let checkbox = await findByLabelText('Show incomplete exercises only') + + let exercisePreviews = await findAllByText('Problem') + expect(exercisePreviews).toHaveLength(3) + let notAnsweredExercisePreviews = await findAllByText('NOT ANSWERED') + expect(notAnsweredExercisePreviews).toHaveLength(2) + + fireEvent.click(checkbox) + + exercisePreviews = await findAllByText('Problem') + expect(exercisePreviews).toHaveLength(2) + notAnsweredExercisePreviews = await findAllByText('NOT ANSWERED') + expect(notAnsweredExercisePreviews).toHaveLength(2) + + const solveExercisesButton = await findByRole('button', { + name: 'SOLVE EXERCISES' + }) + fireEvent.click(solveExercisesButton) + + let inputBox = await findByLabelText('User answer') + fireEvent.change(inputBox, { + target: { value: 'blah blah' } + }) + let submitButton = await findByRole('button', { name: 'SUBMIT' }) + fireEvent.click(submitButton) + + const skipButton = await findByRole('button', { name: 'SKIP' }) + fireEvent.click(skipButton) + + inputBox = await findByLabelText('User answer') + fireEvent.change(inputBox, { + target: { value: '-1' } + }) + submitButton = await findByRole('button', { name: 'SUBMIT' }) + fireEvent.click(submitButton) + + const nextQuestionButton = await findByRole('button', { + name: 'NEXT QUESTION' + }) + fireEvent.click(nextQuestionButton) + + exercisePreviews = await findAllByText('Problem') + expect(exercisePreviews).toHaveLength(1) + let incorrectExercisePreviews = await findAllByText('INCORRECT') + expect(incorrectExercisePreviews).toHaveLength(1) + + checkbox = await findByLabelText('Show incomplete exercises only') + fireEvent.click(checkbox) + + exercisePreviews = await findAllByText('Problem') + expect(exercisePreviews).toHaveLength(3) + incorrectExercisePreviews = await findAllByText('INCORRECT') + expect(incorrectExercisePreviews).toHaveLength(1) + }) + test('Should not render lessons nav card tab if lesson docUrl is null', async () => { const mocks = [ { @@ -133,9 +238,7 @@ describe('Exercises page', () => { ) - await waitFor(() => - screen.getByRole('heading', { name: /Foundations of JavaScript/i }) - ) + await screen.findByRole('heading', { name: /Foundations of JavaScript/i }) screen.getByRole('link', { name: 'CHALLENGES' }) screen.getByRole('link', { name: 'EXERCISES' }) @@ -161,7 +264,7 @@ describe('Exercises page', () => { ) - await waitFor(() => screen.getByRole('heading', { name: /500 Error/i })) + await screen.findByRole('heading', { name: /500 Error/i }) }) test('Should render a 404 error page if the lesson is not found', async () => { @@ -183,7 +286,7 @@ describe('Exercises page', () => { ) - await waitFor(() => screen.getByRole('heading', { name: /404 Error/i })) + await screen.findByRole('heading', { name: /404 Error/i }) }) test('Should render a loading spinner if useRouter is not ready', async () => { @@ -205,6 +308,6 @@ describe('Exercises page', () => { ) - await waitFor(() => screen.getByText('Loading...')) + await screen.findByText('Loading...') }) }) diff --git a/components/ExercisePreviewCard/ExercisePreviewCard.tsx b/components/ExercisePreviewCard/ExercisePreviewCard.tsx index 186acda87..31ec20e9b 100644 --- a/components/ExercisePreviewCard/ExercisePreviewCard.tsx +++ b/components/ExercisePreviewCard/ExercisePreviewCard.tsx @@ -3,7 +3,7 @@ import styles from './exercisePreviewCard.module.scss' export type ExercisePreviewCardProps = { moduleName: string - state: 'NOT ANSWERED' | 'ANSWERED' + state: 'NOT ANSWERED' | 'INCORRECT' | 'ANSWERED' problem: string className?: string } diff --git a/pages/exercises/[lessonSlug].tsx b/pages/exercises/[lessonSlug].tsx index 50cfaab8e..5d2585741 100644 --- a/pages/exercises/[lessonSlug].tsx +++ b/pages/exercises/[lessonSlug].tsx @@ -12,7 +12,9 @@ import Error, { StatusCode } from '../../components/Error' import LoadingSpinner from '../../components/LoadingSpinner' import AlertsDisplay from '../../components/AlertsDisplay' import NavCard from '../../components/NavCard' -import ExercisePreviewCard from '../../components/ExercisePreviewCard' +import ExercisePreviewCard, { + ExercisePreviewCardProps +} from '../../components/ExercisePreviewCard' import { NewButton } from '../../components/theme/Button' import ExerciseCard, { Message } from '../../components/ExerciseCard' import { ArrowLeftIcon } from '@primer/octicons-react' @@ -24,7 +26,8 @@ const Exercises: React.FC> = ({ }) => { const { lessons, alerts, exercises, exerciseSubmissions } = queryData const router = useRouter() - const [exerciseIndex, setExerciseIndex] = useState(-1) + const [solvingExercise, setSolvingExercise] = useState(false) + const [hideAnswered, setHideAnswered] = useState(false) const [addExerciseSubmission] = useAddExerciseSubmissionMutation() const [userAnswers, setUserAnswers] = useState>({}) useEffect(() => { @@ -62,39 +65,50 @@ const Exercises: React.FC> = ({ const currentExercises = exercises .filter(exercise => exercise?.module.lesson.slug === slug) - .map(exercise => ({ - id: exercise.id, - moduleName: exercise.module.name, - problem: exercise.description, - answer: exercise.answer, - explanation: exercise.explanation || '', - userAnswer: userAnswers[exercise.id] ?? null - })) - - const exercise = currentExercises[exerciseIndex] + .map(exercise => { + const userAnswer = userAnswers[exercise.id] ?? null + return { + id: exercise.id, + moduleName: exercise.module.name, + problem: exercise.description, + answer: exercise.answer, + explanation: exercise.explanation || '', + userAnswer, + state: ((): ExercisePreviewCardProps['state'] => { + if (userAnswer === exercise.answer) return 'ANSWERED' + if (userAnswer) return 'INCORRECT' + return 'NOT ANSWERED' + })() + } + }) + .filter( + exercise => !hideAnswered || exercise.userAnswer !== exercise.answer + ) return ( - {exercise ? ( + {solvingExercise ? ( 0} - hasNext={exerciseIndex < currentExercises.length - 1} - submitUserAnswer={(userAnswer: string) => { - setUserAnswers({ ...userAnswers, [exercise.id]: userAnswer }) + exercises={currentExercises} + userAnswers={userAnswers} + onExit={localUserAnswers => { + setUserAnswers({ ...userAnswers, ...localUserAnswers }) + setSolvingExercise(false) + }} + submitUserAnswer={(exerciseId, userAnswer) => { addExerciseSubmission({ - variables: { exerciseId: exercise.id, userAnswer } + variables: { exerciseId, userAnswer } }) }} /> ) : ( setSolvingExercise(true)} lessonTitle={currentLesson.title} + hideAnswered={hideAnswered} + setHideAnswered={setHideAnswered} exercises={currentExercises} /> )} @@ -104,42 +118,48 @@ const Exercises: React.FC> = ({ } type ExerciseData = { + id: number problem: string answer: string explanation: string } type ExerciseProps = { - exercise: ExerciseData - setExerciseIndex: React.Dispatch> lessonTitle: string - hasPrevious: boolean - hasNext: boolean - submitUserAnswer: (userAnswer: string) => void + exercises: ExerciseData[] + userAnswers: Record + submitUserAnswer: (exerciseId: number, userAnswer: string) => void + onExit: (userAnswers: Record) => void } const Exercise = ({ - exercise, - setExerciseIndex, lessonTitle, - hasPrevious, - hasNext, - submitUserAnswer + exercises, + userAnswers, + submitUserAnswer, + onExit }: ExerciseProps) => { const [answerShown, setAnswerShown] = useState(false) const [message, setMessage] = useState(Message.EMPTY) + const [exerciseIndex, setExerciseIndex] = useState(0) + const [localUserAnswers, setLocalUserAnswers] = useState(userAnswers) + const exercise = exercises[exerciseIndex] + + const hasPrevious = exerciseIndex > 0 + const hasNext = exerciseIndex < exercises.length - 1 return (

{lessonTitle}

{ + setLocalUserAnswers({ + ...localUserAnswers, + [exercise.id]: userAnswer + }) + submitUserAnswer(exercise.id, userAnswer) + }} />
{hasPrevious ? (
-

{lessonTitle}

-
- setExerciseIndex(0)} - > - SOLVE EXERCISES - +
+

{lessonTitle}

+
+ {exercises.length > 0 && ( +
+ + SOLVE EXERCISES + +
+ )}
{exercises.map((exercise, i) => ( ))}