Skip to content

Commit

Permalink
Merge pull request #2349 from bryanjenningz/show-next-question-button…
Browse files Browse the repository at this point in the history
…-after-user-answers-exercise-correctly

Show next question button after user answers exercise correctly
  • Loading branch information
bryanjenningz authored Sep 26, 2022
2 parents b02826a + 3ca645f commit 5a6e048
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 43 deletions.
16 changes: 12 additions & 4 deletions __tests__/pages/exercises/[lessonSlug].test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('Exercises page', () => {
}
]

const { getByRole, queryByRole } = render(
const { getByRole, queryByRole, getByLabelText } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<Exercises />
</MockedProvider>
Expand All @@ -64,12 +64,20 @@ describe('Exercises page', () => {
// Previous button is not in the document on the first exercise.
expect(queryByRole('button', { name: 'PREVIOUS' })).not.toBeInTheDocument()

let skipButton = getByRole('button', { name: 'SKIP' })
const skipButton = getByRole('button', { name: 'SKIP' })
fireEvent.click(skipButton)
expect(queryByRole('button', { name: 'PREVIOUS' })).toBeInTheDocument()

skipButton = getByRole('button', { name: 'SKIP' })
fireEvent.click(skipButton)
// Expect "NEXT QUESTION" button to appear once you answered a question correctly.
const inputBox = getByLabelText('User answer')
fireEvent.change(inputBox, {
target: { value: '3' }
})
const submitButton = getByRole('button', { name: 'SUBMIT' })
fireEvent.click(submitButton)
const nextQuestionButton = getByRole('button', { name: 'NEXT QUESTION' })
fireEvent.click(nextQuestionButton)

// Skip button should not be in the document because we're on the last exercise now.
expect(queryByRole('button', { name: 'SKIP' })).not.toBeInTheDocument()

Expand Down
108 changes: 93 additions & 15 deletions components/ExerciseCard/ExerciseCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,133 @@
import '@testing-library/jest-dom'
import React from 'react'
import { fireEvent, render } from '@testing-library/react'
import ExerciseCard from './ExerciseCard'
import ExerciseCard, { Message } from './ExerciseCard'

describe('ExerciseCard component', () => {
it('Should render an exercise card', () => {
const exampleProblem = `let a = 5
const exampleProblem = `let a = 5
a = a + 10
// what is a?`
const exampleAnswer = '15'
const exampleExplanation = `You can reassign variables that are initialized with "let".`
const exampleAnswer = '15'
const exampleExplanation = `You can reassign variables that are initialized with "let".`

const { getByRole, queryByText, getByLabelText } = render(
describe('ExerciseCard component', () => {
it('Should render an exercise card', async () => {
const setAnswerShown = jest.fn()
const setMessage = jest.fn()

const { getByRole, queryByText } = render(
<ExerciseCard
problem={exampleProblem}
answer={exampleAnswer}
explanation={exampleExplanation}
answerShown={false}
setAnswerShown={setAnswerShown}
message={Message.EMPTY}
setMessage={setMessage}
/>
)

// Test that an error message shows if the user is wrong

const errorMessage = 'Your answer is incorrect - please try again.'

expect(queryByText(errorMessage)).not.toBeInTheDocument()
expect(queryByText(Message.ERROR)).not.toBeInTheDocument()

const submitButton = getByRole('button', { name: 'SUBMIT' })
fireEvent.click(submitButton)

expect(queryByText(errorMessage)).toBeInTheDocument()
expect(setAnswerShown).toBeCalledTimes(0)
expect(setMessage).toBeCalledWith(Message.ERROR)
expect(setMessage).toBeCalledTimes(1)
})

// Test that a success message shows and the answer is shown if the user is right
it('Should render an error message', () => {
const setAnswerShown = jest.fn()
const setMessage = jest.fn()

const successMessage = '🎉 Your answer is correct!'
const { getByRole, queryByText, getByLabelText } = render(
<ExerciseCard
problem={exampleProblem}
answer={exampleAnswer}
explanation={exampleExplanation}
answerShown={false}
setAnswerShown={setAnswerShown}
message={Message.ERROR}
setMessage={setMessage}
/>
)

expect(queryByText(successMessage)).not.toBeInTheDocument()
expect(queryByText(Message.ERROR)).toBeInTheDocument()
expect(queryByText(Message.SUCCESS)).not.toBeInTheDocument()

const inputBox = getByLabelText('User answer')
fireEvent.change(inputBox, {
target: { value: '15' }
})

// Test that the submit button shows the success message and the answer explanation

const submitButton = getByRole('button', { name: 'SUBMIT' })
fireEvent.click(submitButton)

expect(queryByText(successMessage)).toBeInTheDocument()
expect(setAnswerShown).toBeCalledWith(true)
expect(setAnswerShown).toBeCalledTimes(1)
expect(setMessage).toBeCalledWith(Message.SUCCESS)
expect(setMessage).toBeCalledTimes(1)
})

it('Should render a success message', () => {
const setAnswerShown = jest.fn()
const setMessage = jest.fn()

const { getByRole, queryByText } = render(
<ExerciseCard
problem={exampleProblem}
answer={exampleAnswer}
explanation={exampleExplanation}
answerShown={true}
setAnswerShown={setAnswerShown}
message={Message.SUCCESS}
setMessage={setMessage}
/>
)

expect(queryByText(Message.SUCCESS)).toBeInTheDocument()
expect(queryByText(exampleExplanation)).toBeInTheDocument()

// Test that the hide button hides the answer explanation

const hideButton = getByRole('button', { name: 'Hide Answer' })
fireEvent.click(hideButton)

expect(setAnswerShown).toBeCalledWith(false)
expect(setAnswerShown).toBeCalledTimes(1)
expect(setMessage).toBeCalledTimes(0)
})

it('Should hide the answer', () => {
const setAnswerShown = jest.fn()
const setMessage = jest.fn()

const { queryByText, getByRole } = render(
<ExerciseCard
problem={exampleProblem}
answer={exampleAnswer}
explanation={exampleExplanation}
answerShown={false}
setAnswerShown={setAnswerShown}
message={Message.SUCCESS}
setMessage={setMessage}
/>
)

expect(queryByText(Message.SUCCESS)).toBeInTheDocument()
expect(queryByText(exampleExplanation)).not.toBeInTheDocument()

// Test that the show button shows the answer explanation

const hideButton = getByRole('button', { name: 'Show Answer' })
fireEvent.click(hideButton)

expect(setAnswerShown).toBeCalledWith(true)
expect(setAnswerShown).toBeCalledTimes(1)
expect(setMessage).toBeCalledTimes(0)
})
})
31 changes: 20 additions & 11 deletions components/ExerciseCard/ExerciseCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,28 @@ export type ExerciseCardProps = {
problem: string
answer: string
explanation: string
answerShown: boolean
setAnswerShown: (answerShown: boolean) => void
message: Message
setMessage: (message: Message) => void
}

enum Message {
export enum Message {
EMPTY = '',
ERROR = 'Your answer is incorrect - please try again.',
SUCCESS = '🎉 Your answer is correct!'
}

type MessageKey = keyof typeof Message

const ExerciseCard = ({ problem, answer, explanation }: ExerciseCardProps) => {
const ExerciseCard = ({
problem,
answer,
explanation,
answerShown,
setAnswerShown,
message,
setMessage
}: ExerciseCardProps) => {
const [studentAnswer, setStudentAnswer] = useState('')
const [answerShown, setAnswerShown] = useState(false)
const [messageKey, setMessageKey] = useState<MessageKey>('EMPTY')
const message = Message[messageKey]

return (
<section className="card p-5 border-0 shadow">
Expand All @@ -35,14 +42,16 @@ const ExerciseCard = ({ problem, answer, explanation }: ExerciseCardProps) => {
<input
aria-label="User answer"
className={`form-control mb-2 ${
messageKey === 'ERROR' ? styles.exerciseCard__input__error : ''
message === Message.ERROR ? styles.exerciseCard__input__error : ''
}`}
value={studentAnswer}
onChange={e => setStudentAnswer(e.target.value)}
/>
<div
className={`${styles.exerciseCard__message} ${
messageKey === 'ERROR' ? styles.exerciseCard__message__error : ''
message === Message.ERROR
? styles.exerciseCard__message__error
: ''
} my-3`}
>
{message}
Expand All @@ -51,10 +60,10 @@ const ExerciseCard = ({ problem, answer, explanation }: ExerciseCardProps) => {
<NewButton
onClick={() => {
if (studentAnswer.trim() === answer.trim()) {
setMessageKey('SUCCESS')
setMessage(Message.SUCCESS)
setAnswerShown(true)
} else {
setMessageKey('ERROR')
setMessage(Message.ERROR)
}
}}
>
Expand Down
2 changes: 1 addition & 1 deletion components/ExerciseCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default, type ExerciseCardProps } from './ExerciseCard'
export { default, type ExerciseCardProps, Message } from './ExerciseCard'
37 changes: 27 additions & 10 deletions pages/exercises/[lessonSlug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ExercisePreviewCard, {
ExercisePreviewCardProps
} from '../../components/ExercisePreviewCard'
import { NewButton } from '../../components/theme/Button'
import ExerciseCard, { ExerciseCardProps } from '../../components/ExerciseCard'
import ExerciseCard, { Message } from '../../components/ExerciseCard'
import { ArrowLeftIcon } from '@primer/octicons-react'
import GET_EXERCISES from '../../graphql/queries/getExercises'
import styles from '../../scss/exercises.module.scss'
Expand Down Expand Up @@ -76,8 +76,8 @@ const Exercises: React.FC<QueryDataProps<GetExercisesQuery>> = ({
exercise={exercise}
setExerciseIndex={setExerciseIndex}
lessonTitle={currentLesson.title}
showPreviousButton={exerciseIndex > 0}
showSkipButton={exerciseIndex < currentExercises.length - 1}
hasPrevious={exerciseIndex > 0}
hasNext={exerciseIndex < currentExercises.length - 1}
/>
) : (
<ExerciseList
Expand All @@ -91,21 +91,30 @@ const Exercises: React.FC<QueryDataProps<GetExercisesQuery>> = ({
)
}

type ExerciseData = {
problem: string
answer: string
explanation: string
}

type ExerciseProps = {
exercise: ExerciseCardProps
exercise: ExerciseData
setExerciseIndex: React.Dispatch<React.SetStateAction<number>>
lessonTitle: string
showPreviousButton: boolean
showSkipButton: boolean
hasPrevious: boolean
hasNext: boolean
}

const Exercise = ({
exercise,
setExerciseIndex,
lessonTitle,
showPreviousButton,
showSkipButton
hasPrevious,
hasNext
}: ExerciseProps) => {
const [answerShown, setAnswerShown] = useState(false)
const [message, setMessage] = useState(Message.EMPTY)

return (
<div className={`mx-auto ${styles.exercise__container}`}>
<button
Expand All @@ -120,9 +129,13 @@ const Exercise = ({
problem={exercise.problem}
answer={exercise.answer}
explanation={exercise.explanation}
answerShown={answerShown}
setAnswerShown={setAnswerShown}
message={message}
setMessage={setMessage}
/>
<div className="d-flex justify-content-between mt-4">
{showPreviousButton ? (
{hasPrevious ? (
<button
onClick={() => setExerciseIndex(i => i - 1)}
className="btn btn-outline-primary fw-bold px-4 py-2"
Expand All @@ -133,7 +146,11 @@ const Exercise = ({
) : (
<div />
)}
{showSkipButton ? (
{message === Message.SUCCESS ? (
<NewButton onClick={() => setExerciseIndex(i => i + 1)}>
NEXT QUESTION
</NewButton>
) : hasNext ? (
<button
onClick={() => setExerciseIndex(i => i + 1)}
className="btn btn-outline-primary fw-bold px-4 py-2"
Expand Down
11 changes: 9 additions & 2 deletions stories/components/ExerciseCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import ExerciseCard from '../../components/ExerciseCard'
import React, { useState } from 'react'
import ExerciseCard, { Message } from '../../components/ExerciseCard'

export default {
component: ExerciseCard,
Expand All @@ -15,11 +15,18 @@ const exampleAnswer = '15'
const exampleExplanation = `You can reassign variables that are initialized with "let".`

export const Basic = () => {
const [answerShown, setAnswerShown] = useState(false)
const [message, setMessage] = useState(Message.EMPTY)

return (
<ExerciseCard
problem={exampleProblem}
answer={exampleAnswer}
explanation={exampleExplanation}
answerShown={answerShown}
setAnswerShown={setAnswerShown}
message={message}
setMessage={setMessage}
/>
)
}

1 comment on commit 5a6e048

@vercel
Copy link

@vercel vercel bot commented on 5a6e048 Sep 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.