diff --git a/backend/src/main/java/cz/scrumdojo/quizmaster/quiz/QuizQuestion.java b/backend/src/main/java/cz/scrumdojo/quizmaster/quiz/QuizQuestion.java index 5ffb45e..33959f5 100644 --- a/backend/src/main/java/cz/scrumdojo/quizmaster/quiz/QuizQuestion.java +++ b/backend/src/main/java/cz/scrumdojo/quizmaster/quiz/QuizQuestion.java @@ -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 diff --git a/backend/src/main/java/cz/scrumdojo/quizmaster/quiz/QuizQuestionController.java b/backend/src/main/java/cz/scrumdojo/quizmaster/quiz/QuizQuestionController.java index 5ffa44c..8621210 100644 --- a/backend/src/main/java/cz/scrumdojo/quizmaster/quiz/QuizQuestionController.java +++ b/backend/src/main/java/cz/scrumdojo/quizmaster/quiz/QuizQuestionController.java @@ -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 { @@ -48,6 +48,13 @@ public ResponseEntity answerQuestionV2(@PathVariable Integer id, @Reque return response(findQuestion(id).map(quizQuestion -> answers.contains(quizQuestion.getCorrectAnswer()))); } + @Transactional + @GetMapping("/quiz-question/all") + public ResponseEntity> getAllQuestionList() { + List quizQuestions = quizQuestionRepository.findAll(); + return ResponseEntity.ok().body(quizQuestions); + } + private Optional findQuestion(Integer id) { return quizQuestionRepository.findById(id); } diff --git a/backend/src/main/resources/db/migration/V00005_quiz.sql b/backend/src/main/resources/db/migration/V00005__quiz.sql similarity index 100% rename from backend/src/main/resources/db/migration/V00005_quiz.sql rename to backend/src/main/resources/db/migration/V00005__quiz.sql diff --git a/backend/src/main/resources/db/migration/V00006__quiz_question_answers_validation.sql b/backend/src/main/resources/db/migration/V00006__quiz_question_answers_validation.sql new file mode 100644 index 0000000..c2cbf3f --- /dev/null +++ b/backend/src/main/resources/db/migration/V00006__quiz_question_answers_validation.sql @@ -0,0 +1 @@ +ALTER TABLE quiz_question ADD COLUMN answers_validation boolean[] NULL; \ No newline at end of file diff --git a/backend/src/test/java/cz/scrumdojo/quizmaster/quiz/QuizQuestionControllerTest.java b/backend/src/test/java/cz/scrumdojo/quizmaster/quiz/QuizQuestionControllerTest.java index 0ceb95c..f4f4d2e 100644 --- a/backend/src/test/java/cz/scrumdojo/quizmaster/quiz/QuizQuestionControllerTest.java +++ b/backend/src/test/java/cz/scrumdojo/quizmaster/quiz/QuizQuestionControllerTest.java @@ -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()); + } } diff --git a/frontend/src/model/quiz-question.ts b/frontend/src/model/quiz-question.ts index 88a41a0..3affcdd 100644 --- a/frontend/src/model/quiz-question.ts +++ b/frontend/src/model/quiz-question.ts @@ -2,5 +2,6 @@ export interface QuizQuestion { readonly id: number readonly question: string readonly answers: readonly string[] + readonly explanations: readonly string[] readonly quizType: string } diff --git a/frontend/src/quiz.tsx b/frontend/src/quiz.tsx index 7a0b210..ab8f48c 100644 --- a/frontend/src/quiz.tsx +++ b/frontend/src/quiz.tsx @@ -10,10 +10,16 @@ import { transformObjectToArray } from './utils/transformObjectToArray.ts' const Feedback = (correct: boolean) => -const Question = ({ id, question, answers, quizType }: QuizQuestion) => { +const Explanation = (explanation: string) => {explanation} + +const QuestionExplanation =

{'Question Explanation'}

+ +const Question = ({ id, question, answers, quizType, explanations }: QuizQuestion) => { const [selectedAnswer, setSelectedAnswer] = createSignal(null) const [selectedAnswers, setSelectedAnswers] = createSignal<{ [key: string]: boolean } | Record>({}) const [isAnswerCorrect, setIsAnswerCorrect] = createSignal(false) + const [explanation, setExplanation] = createSignal('') + const [explanationIdx, setExplanationIdx] = createSignal(null) const [submitted, setSubmitted] = createSignal(false) @@ -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) }) }) @@ -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 @@ -71,7 +81,10 @@ const Question = ({ id, question, answers, quizType }: QuizQuestion) => { return (
  • - +
  • ) } @@ -84,6 +97,7 @@ const Question = ({ id, question, answers, quizType }: QuizQuestion) => { + ) } diff --git a/frontend/tests/pages/quiz-taking-page.ts b/frontend/tests/pages/quiz-taking-page.ts index 553b121..9506d55 100644 --- a/frontend/tests/pages/quiz-taking-page.ts +++ b/frontend/tests/pages/quiz-taking-page.ts @@ -26,6 +26,8 @@ export default class QuizTakingPage { feedbackLocator = () => this.page.locator('p.feedback') + explanationLocator = () => this.page.locator('span.explanation') + getFeedback = async () => { return this.page.locator('.feedback').innerText() } @@ -33,4 +35,8 @@ export default class QuizTakingPage { getQuestions = async () => { return this.page.locator('.quiz-questions li').allTextContents() } + + questionExplanationLocator = () => this.page.locator('p.questionExplanation') + + getUrl = () => this.page.url() } diff --git a/frontend/tests/steps/create-quiz.ts b/frontend/tests/steps/create-quiz.ts index 5fd188d..4098d98 100644 --- a/frontend/tests/steps/create-quiz.ts +++ b/frontend/tests/steps/create-quiz.ts @@ -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 () => { diff --git a/frontend/tests/steps/multiple-choise-feedback-per-answer.ts b/frontend/tests/steps/multiple-choise-feedback-per-answer.ts index 3260212..3313df1 100644 --- a/frontend/tests/steps/multiple-choise-feedback-per-answer.ts +++ b/frontend/tests/steps/multiple-choise-feedback-per-answer.ts @@ -9,27 +9,11 @@ interface MultipleChoiceWorld { const world = worldAs() -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) diff --git a/frontend/tests/steps/quiz-question.ts b/frontend/tests/steps/quiz-question.ts index 4fc76b4..1a77bc5 100644 --- a/frontend/tests/steps/quiz-question.ts +++ b/frontend/tests/steps/quiz-question.ts @@ -8,6 +8,7 @@ interface QuizQuestionData { readonly question: string readonly answers: readonly string[] readonly correctAnswer: number + readonly explanations: readonly string[] } interface QuizQuestion { @@ -15,11 +16,13 @@ interface QuizQuestion { 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 @@ -56,18 +59,21 @@ Before(() => { Given( 'a quiz question {string} bookmarked as {string} with answers', - async (question: string, bookmark: string, table: TableOf) => { + async (question: string, bookmark: string, answerRawTable: TableOf) => { 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) => { @@ -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() }) @@ -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) +}) diff --git a/specs/DetailedFeedbackPerAnswer.feature b/specs/DetailedFeedbackPerAnswer.feature new file mode 100644 index 0000000..c243b5b --- /dev/null +++ b/specs/DetailedFeedbackPerAnswer.feature @@ -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 "" quiz-taking page + And I select the answer "" + And I submit the quiz + Then I should see "" + And I should see the 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 | diff --git a/specs/MultipleChoiseFeedbackPerAnswer.feature b/specs/MultipleChoiseFeedbackPerAnswer.feature index 9181bc2..0dc2ff1 100644 --- a/specs/MultipleChoiseFeedbackPerAnswer.feature +++ b/specs/MultipleChoiseFeedbackPerAnswer.feature @@ -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 -# 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 # # Examples: diff --git a/specs/QuestionExplanation.feature b/specs/QuestionExplanation.feature new file mode 100644 index 0000000..a046128 --- /dev/null +++ b/specs/QuestionExplanation.feature @@ -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 "" quiz-taking page + And I select the answer "" + And I submit the quiz + Then I should see "" + And I should see 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 |