Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
# Conflicts:
#	frontend/src/quiz.tsx
  • Loading branch information
ahurniak committed Sep 5, 2024
2 parents ddb953d + 616ed37 commit f62b180
Show file tree
Hide file tree
Showing 14 changed files with 131 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public class QuizQuestion {
@JdbcTypeCode(SqlTypes.ARRAY)
private String[] answers;

@Column(name = "explanations", columnDefinition = "text[]")
@JdbcTypeCode(SqlTypes.ARRAY)
private String[] explanations;

private Integer correctAnswer;

@Transient
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package cz.scrumdojo.quizmaster.quiz;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api")
public class QuizQuestionController {
Expand Down Expand Up @@ -48,6 +48,13 @@ public ResponseEntity<Boolean> answerQuestionV2(@PathVariable Integer id, @Reque
return response(findQuestion(id).map(quizQuestion -> answers.contains(quizQuestion.getCorrectAnswer())));
}

@Transactional
@GetMapping("/quiz-question/all")
public ResponseEntity<List<QuizQuestion>> getAllQuestionList() {
List<QuizQuestion> quizQuestions = quizQuestionRepository.findAll();
return ResponseEntity.ok().body(quizQuestions);
}

private Optional<QuizQuestion> findQuestion(Integer id) {
return quizQuestionRepository.findById(id);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE quiz_question ADD COLUMN answers_validation boolean[] NULL;
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,12 @@ public void answerNonExistingQuestion() {

assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}

@Test
public void returnAllQuestions() {
ResponseEntity<?> response = quizQuestionController.getAllQuestionList();

assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
}
1 change: 1 addition & 0 deletions frontend/src/model/quiz-question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export interface QuizQuestion {
readonly id: number
readonly question: string
readonly answers: readonly string[]
readonly explanations: readonly string[]
readonly quizType: string
}
20 changes: 17 additions & 3 deletions frontend/src/quiz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ import { transformObjectToArray } from './utils/transformObjectToArray.ts'

const Feedback = (correct: boolean) => <p class="feedback">{correct ? 'Correct!' : 'Incorrect!'}</p>

const Question = ({ id, question, answers, quizType }: QuizQuestion) => {
const Explanation = (explanation: string) => <span class="explanation"> {explanation}</span>

const QuestionExplanation = <p class="questionExplanation">{'Question Explanation'}</p>

const Question = ({ id, question, answers, quizType, explanations }: QuizQuestion) => {
const [selectedAnswer, setSelectedAnswer] = createSignal<number | null>(null)
const [selectedAnswers, setSelectedAnswers] = createSignal<{ [key: string]: boolean } | Record<string, boolean>>({})
const [isAnswerCorrect, setIsAnswerCorrect] = createSignal(false)
const [explanation, setExplanation] = createSignal<string | ''>('')
const [explanationIdx, setExplanationIdx] = createSignal<number | null>(null)

const [submitted, setSubmitted] = createSignal(false)

Expand All @@ -25,6 +31,8 @@ const Question = ({ id, question, answers, quizType }: QuizQuestion) => {
api.isAnswerCorrect(id, selectedAnswerIdx).then(isCorrect => {
setSubmitted(true)
setIsAnswerCorrect(isCorrect)
setExplanation(explanations[selectedAnswerIdx])
setExplanationIdx(selectedAnswerIdx)
})
})

Expand All @@ -39,7 +47,9 @@ const Question = ({ id, question, answers, quizType }: QuizQuestion) => {
})
})

const selectAnswer = (answerIdx: number) => () => setSelectedAnswer(answerIdx)
const selectAnswer = (answerIdx: number) => () => {
setSelectedAnswer(answerIdx)
}

const handleCheckboxChange = (event: InputEvent) => {
const { name, checked } = event.target as HTMLInputElement
Expand Down Expand Up @@ -71,7 +81,10 @@ const Question = ({ id, question, answers, quizType }: QuizQuestion) => {
return (
<li>
<input type={'radio'} name={'answer'} id={answerId} value={answer} onClick={selectAnswer(idx())} />
<label for={answerId}>{answer}</label>
<label for={answerId}>
{answer}
<Show when={explanationIdx() === idx()} children={Explanation(explanation())} keyed />
</label>
</li>
)
}
Expand All @@ -84,6 +97,7 @@ const Question = ({ id, question, answers, quizType }: QuizQuestion) => {
</ul>
<input type="submit" value={'Submit'} />
<Show when={submitted()} children={Feedback(isAnswerCorrect())} keyed />
<Show when={submitted()} children={QuestionExplanation} />
</form>
)
}
Expand Down
6 changes: 6 additions & 0 deletions frontend/tests/pages/quiz-taking-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ export default class QuizTakingPage {

feedbackLocator = () => this.page.locator('p.feedback')

explanationLocator = () => this.page.locator('span.explanation')

getFeedback = async () => {
return this.page.locator('.feedback').innerText()
}

getQuestions = async () => {
return this.page.locator('.quiz-questions li').allTextContents()
}

questionExplanationLocator = () => this.page.locator('p.questionExplanation')

getUrl = () => this.page.url()
}
5 changes: 3 additions & 2 deletions frontend/tests/steps/create-quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ When('quiz taker clicks the link', async () => {
})

Then('quiz taker is on the quiz page', async () => {
const pageTitle = await world.quizTakingPage.getTitle()
expect(pageTitle).toBe('Take Quiz')
const currentUrl = await world.quizTakingPage.getUrl()
const urlPattern = /^http:\/\/localhost:(5173|8080)\/quiz\/\d+$/
expect(currentUrl).toMatch(urlPattern)
})

Then('quiz taker sees a correct list of the questions', async () => {
Expand Down
16 changes: 0 additions & 16 deletions frontend/tests/steps/multiple-choise-feedback-per-answer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,11 @@ interface MultipleChoiceWorld {

const world = worldAs<MultipleChoiceWorld>()

Then('quiz taker is on the quiz page', async () => {
const pageTitle = await world.quizTakingPage.getTitle()
expect(pageTitle).toBe('Take Quiz')
})

Then('quiz taker sees question with multiple choice', async () => {
const questionType = await world.quizTakingPage.getQuestionType()
expect(questionType).toBe('multiple choice')
})

When('quiz taker chooses {string}', async (answers: string) => {
const answerList = answers.split(',').map(answer => answer.trim())
for (const answer of answerList) {
await world.quizTakingPage.selectAnswer(answer)
}
})

When('quiz taker clicks on submit button', async () => {
await world.quizTakingPage.submit()
})

Then('quiz taker sees the {string}', async (result: string) => {
const feedback = await world.quizTakingPage.getFeedback()
expect(feedback).toContain(result)
Expand Down
26 changes: 21 additions & 5 deletions frontend/tests/steps/quiz-question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ interface QuizQuestionData {
readonly question: string
readonly answers: readonly string[]
readonly correctAnswer: number
readonly explanations: readonly string[]
}

interface QuizQuestion {
readonly quizQuestionId: number
readonly quizQuestion: QuizQuestionData
}

type AnswerRaw = [string, string]
type AnswerRaw = [string, string, string]
type Answers = string[]
type Explanations = string[]

const toAnswers = (raw: AnswerRaw[]): Answers => raw.map(([answer]) => answer)
const toCorrectAnswer = (raw: AnswerRaw[]): number => raw.findIndex(([, correct]) => correct === 'correct')
const toExplanations = (raw: AnswerRaw[]): Explanations => raw.map(([, , explanation]) => explanation)

interface QuizQuestionWorld {
questionCreationPage: QuestionCreationPage
Expand Down Expand Up @@ -56,18 +59,21 @@ Before(() => {

Given(
'a quiz question {string} bookmarked as {string} with answers',
async (question: string, bookmark: string, table: TableOf<AnswerRaw>) => {
async (question: string, bookmark: string, answerRawTable: TableOf<AnswerRaw>) => {
await bookmarkQuizQuestion(bookmark, {
question,
answers: toAnswers(table.raw()),
correctAnswer: toCorrectAnswer(table.raw()),
answers: toAnswers(answerRawTable.raw()),
correctAnswer: toCorrectAnswer(answerRawTable.raw()),
explanations: toExplanations(answerRawTable.raw()),
})
},
)

When('I visit the {string} quiz-taking page', async (bookmark: string) => {
world.activeBookmark = bookmark
await world.quizTakingPage.goto(world.bookmarks[bookmark].quizQuestionId)
const quizId = world.bookmarks[bookmark].quizQuestionId
console.log(`Navigating to the quiz-taking page: ${quizId}`)
await world.quizTakingPage.goto(quizId)
})

When('I select the answer {string}', async (answer: string) => {
Expand Down Expand Up @@ -100,6 +106,11 @@ Then('I should see {string}', async (feedback: string) => {
await expectTextToBe(feedbackLocator, feedback)
})

Then('I should see the explanation {string}', async (explanation: string) => {
const explanationLocator = world.quizTakingPage.explanationLocator()
await expectTextToBe(explanationLocator, ` ${explanation}`)
})

When('I visit the create question page', async () => {
await world.questionCreationPage.goto()
})
Expand All @@ -114,3 +125,8 @@ Then('I enter question {string}', async (question: string) => {
const questionLocator = world.questionCreationPage.questionLocator()
await expectInputToBe(questionLocator, question)
})

Then('I should see question explanation {string}', async (questionExplanation: string) => {
const questionExplanationLocator = world.quizTakingPage.questionExplanationLocator()
await expectTextToBe(questionExplanationLocator, questionExplanation)
})
19 changes: 19 additions & 0 deletions specs/DetailedFeedbackPerAnswer.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Feature: View explanations for answers after responding to a question

Background:
Given a quiz question "What is the capital of Italy?" bookmarked as "Italy" with answers
| Rome | correct | Rome is the capital of Italy |
| Naples | | Naples is not the capital of Italy |
| Florence | | Florence is not the capital of Italy |
| Palermo | | Palermo is not the capital of Italy |

Scenario Outline:
When I visit the "<question>" quiz-taking page
And I select the answer "<answer>"
And I submit the quiz
Then I should see "<feedback>"
And I should see the explanation "<explanation>"
Examples:
| question | answer | feedback | explanation |
| Italy | Rome | Correct! | Rome is the capital of Italy |
| Italy | Palermo | Incorrect! | Palermo is not the capital of Italy |
21 changes: 15 additions & 6 deletions specs/MultipleChoiseFeedbackPerAnswer.feature
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
Feature: Multiple choice - feedback per answer feature


Background:
Given a quiz question "what countries are in Europe?" bookmarked as "Europe" with answers
| Italy | correct |
| France | correct |
| Morocco | |
| Spain | |


Scenario: Multiple choice - feedback per answer - happy path
# Given I visit the "Europe" quiz-taking page
# Then quiz taker is on the quiz page
# And quiz taker sees question with multiple choise
#
# When quiz taker chooses <answers>
# And quiz taker clicks on submit button
Given I visit the "Europe" quiz-taking page
Then quiz taker is on the quiz page
And quiz taker sees question with multiple choise

When I select the answer "France"
And I select the answer "Italy"
And I submit the quiz
# Then quiz taker sees the <result>
#
# Examples:
Expand Down
26 changes: 26 additions & 0 deletions specs/QuestionExplanation.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Feature: Question explanation

Background:
Given a quiz question "What is the capital of Italy?" bookmarked as "Italy" with answers
| Rome | correct |
| Naples | |
| Florence | |
| Palermo | |
And a quiz question "What is the capital of France?" bookmarked as "France" with answers
| Marseille | |
| Lyon | |
| Paris | correct |
| Toulouse | |

Scenario Outline:
When I visit the "<question>" quiz-taking page
And I select the answer "<answer>"
And I submit the quiz
Then I should see "<feedback>"
And I should see question explanation "<question explanation>"
Examples:
| question | answer | feedback | question explanation |
| Italy | Rome | Correct! | Question Explanation |
| Italy | Palermo | Incorrect! | Question Explanation |
| France | Paris | Correct! | Question Explanation |
| France | Toulouse | Incorrect! | Question Explanation |

0 comments on commit f62b180

Please sign in to comment.