Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show next question button after user answers exercise correctly #2349

Merged
merged 6 commits into from
Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}
/>
)
}