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

Feat/edit or delete question in a note (less in action) #1012

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
19 changes: 19 additions & 0 deletions e2e_test/features/assessment/quiz_questions_management.feature
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ Feature: Quiz Question Management
| What does a cow say? | moo |
| What do you call a cow with not leg? | Ground beef |

Scenario: Manually edit a question to the note successfully
When I add the following question for the note "The cow joke":
| Stem | Choice 0 | Choice 1 | Choice 2 | Correct Choice Index |
| What do you call a cow with not leg? | Ground beef | Cowboy | Oxford | 0 |
When I edit the question "What do you call a cow with not leg?" for the note "The cow joke":
| Stem | Choice 0 | Choice 1 | Choice 2 | Correct Choice Index |
| What do you call a cow with not leg? | Ground beef | Cowboy | Oxford | 1 |
Then I should see the questions in the question list of the note "The cow joke":
| Question | Correct Choice |
| What does a cow say? | moo |
| What do you call a cow with not leg? | Cowboy |

Scenario: Manually delete a question to the note successfully
When I add the following question for the note "The cow joke":
| Stem | Choice 0 | Choice 1 | Choice 2 | Correct Choice Index |
| What do you call a cow with not leg? | Ground beef | Cowboy | Oxford | 0 |
When I delete the question "What do you call a cow with not leg?" for the note "The cow joke"
Then I should see no question named "What do you call a cow with not leg?" in the question list of the note "The cow joke"

@usingMockedOpenAiService
Scenario: Can generate the question by AI
Given OpenAI now generates this question:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
export const addQuestionPage = () => {
cy.findByRole('button', { name: 'Add Question' }).click()
export const addEditQuestionPage = () => {
return {
fillQuestion(row: Record<string, string>) {
cy.findByRole('button', { name: '+' }).click()
;[
'Stem',
'Choice 0',
Expand All @@ -15,14 +13,26 @@ export const addQuestionPage = () => {
}
})
},

editQuestion(question: string, row: Record<string, string>) {
cy.findByText(question).parent('tr').contains('Edit').click()
this.fillQuestion(row)
cy.findByRole('button', { name: 'Submit' }).click()
},
addQuestion(row: Record<string, string>) {
cy.findByRole('button', { name: 'Add Question' }).click()
cy.findByRole('button', { name: '+' }).click()
this.fillQuestion(row)
cy.findByRole('button', { name: 'Submit' }).click()
},
generateQuestionByAI() {
cy.findByRole('button', { name: 'Add Question' }).click()
cy.findByRole('button', { name: '+' }).click()
cy.findByRole('button', { name: 'Generate by AI' }).click()
},
refineQuestion(row: Record<string, string>) {
cy.findByRole('button', { name: 'Add Question' }).click()
cy.findByRole('button', { name: '+' }).click()
this.fillQuestion(row)
cy.findByRole('button', { name: 'Refine' }).click()
},
Expand Down
17 changes: 15 additions & 2 deletions e2e_test/start/pageObjects/notePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,18 @@ export const assumeNotePage = (noteTopic?: string) => {
return questionListPage()
},
addQuestion(row: Record<string, string>) {
this.openQuestionList().addQuestionPage().addQuestion(row)
this.openQuestionList().addEditQuestionPage().addQuestion(row)
},

editQuestion(question: string, row: Record<string, string>) {
this.openQuestionList().addEditQuestionPage().editQuestion(question, row)
},
deleteQuestion(question) {
this.openQuestionList()
cy.findByText(question).parent('tr').contains('Delete').click()
},
refineQuestion(row: Record<string, string>) {
this.openQuestionList().addQuestionPage().refineQuestion(row)
this.openQuestionList().addEditQuestionPage().refineQuestion(row)
},
toggleApproval(question: string) {
this.openQuestionList()
Expand All @@ -206,6 +214,11 @@ export const assumeNotePage = (noteTopic?: string) => {
expectQuestionsInList(expectedQuestions: Record<string, string>[]) {
this.openQuestionList().expectQuestion(expectedQuestions)
},
expectQuestionNotInList(question: string) {
this.openQuestionList()
cy.pageIsNotLoading()
cy.findByText(question).should('not.exist')
},
aiSuggestDetailsForNote: () => {
cy.on('uncaught:exception', () => {
return false
Expand Down
4 changes: 2 additions & 2 deletions e2e_test/start/pageObjects/questionListPage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { addQuestionPage } from './addQuestionPage'
import { addEditQuestionPage } from './addEditQuestionPage'

export const questionListPage = () => {
return {
addQuestionPage,
addEditQuestionPage,
expectQuestion(expectedQuestions: Record<string, string>[]) {
expectedQuestions.forEach((row) => {
cy.findByText(row.Question!)
Expand Down
25 changes: 24 additions & 1 deletion e2e_test/step_definitions/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,29 @@ When(
}
)

When(
'I edit the question {string} for the note {string}:',
(quizQuestion: string, noteTopic: string, data: DataTable) => {
start
.jumpToNotePage(noteTopic)
.editQuestion(quizQuestion, data.hashes()[0]!)
}
)

When(
'I delete the question {string} for the note {string}',
(quizQuestion: string, noteTopic: string) => {
start.jumpToNotePage(noteTopic).deleteQuestion(quizQuestion)
}
)

Then(
'I should see no question named {string} in the question list of the note {string}',
(quizQuestion: string, noteTopic: string) => {
start.jumpToNotePage(noteTopic).expectQuestionNotInList(quizQuestion)
}
)

Given(
'I toggle the approval of the question {string} of the topic {string}',
(quizQuestion: string, noteTopic: string) => {
Expand All @@ -515,7 +538,7 @@ When('I generate question by AI for note {string}', (noteName: string) => {
start
.jumpToNotePage(noteName)
.openQuestionList()
.addQuestionPage()
.addEditQuestionPage()
.generateQuestionByAI()
})

Expand Down
2 changes: 1 addition & 1 deletion frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ declare module 'vue' {
NoteAccessoryAsync: typeof import('./src/components/notes/accessory/NoteAccessoryAsync.vue')['default']
NoteAccessoryDisplay: typeof import('./src/components/notes/accessory/NoteAccessoryDisplay.vue')['default']
NoteAccessoryToolbar: typeof import('./src/components/notes/accessory/NoteAccessoryToolbar.vue')['default']
NoteAddQuestion: typeof import('./src/components/notes/NoteAddQuestion.vue')['default']
NoteAddOrEditQuestion: typeof import('./src/components/notes/NoteAddOrEditQuestion.vue')['default']
NotebookAssistantManagementDialog: typeof import('./src/components/notebook/NotebookAssistantManagementDialog.vue')['default']
NotebookBazaarViewCards: typeof import('./src/components/bazaar/NotebookBazaarViewCards.vue')['default']
NotebookButtons: typeof import('./src/components/notebook/NotebookButtons.vue')['default']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,36 @@ const props = defineProps({
type: Object as PropType<Note>,
required: true,
},
question: {
type: Object as PropType<QuizQuestionAndAnswer>,
required: false,
},
})

const quizQuestionAndAnswer = ref<QuizQuestionAndAnswer>({
correctAnswerIndex: 0,
quizQuestion: {
multipleChoicesQuestion: {
stem: "",
choices: ["", ""],
},
},
} as QuizQuestionAndAnswer)
const quizQuestionAndAnswer = ref<QuizQuestionAndAnswer>(
props.question
? {
...props.question,
quizQuestion: {
...props.question.quizQuestion,
multipleChoicesQuestion: {
...props.question.quizQuestion.multipleChoicesQuestion,
choices: [
...props.question.quizQuestion.multipleChoicesQuestion.choices,
],
},
},
}
: ({
correctAnswerIndex: 0,
quizQuestion: {
multipleChoicesQuestion: {
stem: "",
choices: ["", ""],
},
},
} as QuizQuestionAndAnswer)
)

const minimumNumberOfChoices = 2
const maximumNumberOfChoices = 10
Expand Down
45 changes: 39 additions & 6 deletions frontend/src/components/notes/Questions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@
<PopButton btn-class="btn btn-primary" title="Add Question">
<!-- prettier-ignore -->
<template #default="{ closer }">
<NoteAddQuestion
<NoteAddOrEditQuestion
v-bind="{ note }"
@close-dialog="
closer($event);
questionAdded($event);
closer();
questionAddedOrEdited($event);
"
/>
</template>
</PopButton>
<table class="question-table mt-2">
<thead>
<tr>
<th>Actions</th>
<th>Approved</th>
<th>Question Text</th>
<th>A</th>
Expand All @@ -28,6 +29,23 @@
v-for="(question, outerIndex) in questions"
:key="question.quizQuestion.multipleChoicesQuestion.stem"
>
<td>
<button class="btn btn-danger" title="Delete Question" @click=deleteQuestion(question.id)>
Delete
</button>
<PopButton btn-class="btn btn-primary" title="Edit">
<!-- prettier-ignore -->
<template #default="{ closer }">
<NoteAddOrEditQuestion
v-bind="{ note, question }"
@close-dialog="
closer();
questionAddedOrEdited($event);
"
/>
</template>
</PopButton>
</td>
<td>
<input
:id="'checkbox-' + outerIndex"
Expand Down Expand Up @@ -61,7 +79,7 @@
import { PropType, onMounted, ref } from "vue"
import { Note, QuizQuestionAndAnswer } from "@/generated/backend"
import useLoadingApi from "@/managedApi/useLoadingApi"
import NoteAddQuestion from "./NoteAddQuestion.vue"
import NoteAddOrEditQuestion from "./NoteAddOrEditQuestion.vue"
import PopButton from "../commons/Popups/PopButton.vue"

const { managedApi } = useLoadingApi()
Expand All @@ -78,12 +96,27 @@ const fetchQuestions = async () => {
props.note.id
)
}
const questionAdded = (newQuestion: QuizQuestionAndAnswer) => {
const questionAddedOrEdited = (newQuestion: QuizQuestionAndAnswer) => {
if (newQuestion == null) {
return
}
questions.value.push(newQuestion)

const index = questions.value.findIndex((q) => {
return q.id === newQuestion.id
})
if (index === -1) {
questions.value.push(newQuestion)
} else {
questions.value.splice(index, 1, newQuestion)
}
}

const deleteQuestion = async (questionId?: number) => {
if (questionId) {
console.log("Delete not implemented yet!")
}
}

const toggleApproval = async (questionId?: number) => {
if (questionId) {
await managedApi.restQuizQuestionController.toggleApproval(questionId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import NoteAddQuestion from "@/components/notes/NoteAddQuestion.vue"
import NoteAddEditQuestion from "@/components/notes/NoteAddOrEditQuestion.vue"
import { userEvent } from "@testing-library/user-event"
import { screen } from "@testing-library/vue"
import { flushPromises } from "@vue/test-utils"
import makeMe from "../fixtures/makeMe"
import helper from "../helpers"
import { QuizQuestionAndAnswer } from "@/generated/backend"

const note = makeMe.aNoteRealm.please()
const createWrapper = async () => {
const createWrapper = async (questionInput?: QuizQuestionAndAnswer) => {
helper
.component(NoteAddQuestion)
.component(NoteAddEditQuestion)
.withProps({
note: note.note,
question: questionInput,
})
.render()
await flushPromises()
}

describe("NoteAddQuestion", () => {
describe("NoteAddEditQuestion", () => {
interface Case {
question: Record<string, string>
questionInput?: QuizQuestionAndAnswer
expectedRefineButton: boolean
expectedGenerateButton: boolean
}
Expand All @@ -38,9 +41,26 @@ describe("NoteAddQuestion", () => {
expectedRefineButton: true,
expectedGenerateButton: false,
},
{
question: {},
questionInput: {
id: 100,
quizQuestion: {
id: 1000,
multipleChoicesQuestion: {
stem: "Should I participate in a LeSS course?",
choices: ["yes", "no", "maybe"],
},
correctAnswerIndex: 0,
approved: true,
},
},
expectedRefineButton: true,
expectedGenerateButton: false,
},
].forEach(async (testCase: Case) => {
it("only allow generation when no changes", async () => {
await createWrapper()
await createWrapper(testCase.questionInput)
// eslint-disable-next-line no-restricted-syntax
for (const key of Object.keys(testCase.question)) {
// eslint-disable-next-line no-await-in-loop
Expand Down