From 83b861ce7395b56d4053f3d6b4cc8bb5a744fdb1 Mon Sep 17 00:00:00 2001 From: sjschlapbach Date: Tue, 7 Jan 2025 17:32:14 +0100 Subject: [PATCH 1/3] wip: extend questions workflow to cover creation and editing of selection questions --- .../manipulation/options/SelectionOptions.tsx | 50 +++--- .../cypress/e2e/D-questions-workflow.cy.ts | 161 ++++++++++++++++++ .../cypress/e2e/K-resources-workflow.cy.ts | 6 + packages/i18n/messages/de.ts | 2 + packages/i18n/messages/en.ts | 2 + packages/prisma/src/data/data/TEST.ts | 6 + 6 files changed, 208 insertions(+), 19 deletions(-) diff --git a/apps/frontend-manage/src/components/questions/manipulation/options/SelectionOptions.tsx b/apps/frontend-manage/src/components/questions/manipulation/options/SelectionOptions.tsx index eec178f7db..c125941e8b 100644 --- a/apps/frontend-manage/src/components/questions/manipulation/options/SelectionOptions.tsx +++ b/apps/frontend-manage/src/components/questions/manipulation/options/SelectionOptions.tsx @@ -6,6 +6,7 @@ import { FormikSelectField, FormikSwitchField, FormLabel, + UserNotification, } from '@uzh-bf/design-system' import { useField } from 'formik' import { useTranslations } from 'next-intl' @@ -65,7 +66,7 @@ function SelectionOptions({ values }: SelectionOptionsProps) { // udpate the selected correct answers if the answer collection changes useEffect(() => { - if (!field.value) { + if (!field.value || !collectionAnswers || collectionAnswers.length === 0) { return } @@ -82,6 +83,16 @@ function SelectionOptions({ values }: SelectionOptionsProps) { return } + if (collections.length === 0) { + return ( + + ) + } + return (
@@ -96,7 +107,7 @@ function SelectionOptions({ values }: SelectionOptionsProps) { label: collection.name, value: String(collection.id), data: { - cy: `select-answer-collection-${collection.id}`, + cy: `select-answer-collection-${collection.name}`, }, }))} data={{ cy: 'select-answer-collection' }} @@ -135,23 +146,24 @@ function SelectionOptions({ values }: SelectionOptionsProps) { tooltip={t('manage.questionForms.correctAnswerOptionsTooltip')} labelType="small" /> - 'w-full', + }} + onChange={(newValue) => + helpers.setValue(newValue.map((tag) => tag.value)) + } + placeholder={t('manage.questionForms.selectAnswerOptions')} + noOptionsMessage={() => + t('manage.questionForms.noMatchingOptionFound') + } + /> +
) : null} diff --git a/cypress/cypress/e2e/D-questions-workflow.cy.ts b/cypress/cypress/e2e/D-questions-workflow.cy.ts index 8ca2183c16..ef99952e71 100644 --- a/cypress/cypress/e2e/D-questions-workflow.cy.ts +++ b/cypress/cypress/e2e/D-questions-workflow.cy.ts @@ -65,6 +65,19 @@ const FTSampleSolution = [ 'Sample Solution 3', ] +const SETitle = 'Selection Question Title' +const SEContent = 'Selection Question Text' +const SEExplanation = 'Selection Question Explanation' +const SECollection = 'Private Collection (Vegetables)' // from seed (access otherwise is tested in resources workflow) +const SEInputs = 2 +const SESolutions = ['Cabbage', 'Cucumber'] +const SETitleEdited = 'Selection Question Title Edited' +const SEContentEdited = 'Selection Question Text Edited' +const SEExplanationEdited = 'Selection Question Explanation Edited' +const SECollectionEdited = 'Public Collection (Fruits)' // from seed +const SEInputsEdited = 1 +const SESolutionsEdited = ['Apple', 'Banana'] + describe('Create different types of elements (with and without sample solution) and edit them', () => { beforeEach(() => { cy.loginLecturer() @@ -1054,6 +1067,154 @@ describe('Create different types of elements (with and without sample solution) cy.get('[data-cy="close-question-modal"]').click() }) + it('Create a Selection question', () => { + cy.get('[data-cy="create-question"]').click() + cy.get('[data-cy="select-question-type"]') + .should('exist') + .contains(messages.shared.SC.typeLabel) + cy.get('[data-cy="select-question-type"]').click() + cy.get( + `[data-cy="select-question-type-${messages.shared.SELECTION.typeLabel}"]` + ).click() + cy.get('[data-cy="select-question-type"]') + .should('exist') + .contains(messages.shared.SELECTION.typeLabel) + cy.get('[data-cy="save-new-question"]').should('be.disabled') + + cy.get('[data-cy="insert-question-title"]').click().type(SETitle) + cy.get('[data-cy="save-new-question"]').should('be.disabled') + cy.get('[data-cy="select-question-status"]').click() + cy.get( + `[data-cy="select-question-status-${messages.shared.READY.statusLabel}"]` + ).click() + cy.get('[data-cy="insert-question-text"]').realClick().type(SEContent) + cy.get('[data-cy="insert-question-explanation"]') + .realClick() + .type(SEExplanation) + cy.get('[data-cy="save-new-question"]').should('be.disabled') + + cy.get('[data-cy="select-answer-collection"]').contains( + messages.manage.questionForms.selectCollection + ) + cy.get('[data-cy="select-answer-collection"]').click() + cy.get(`[data-cy="select-answer-collection-${SECollection}"]`).click() + cy.get('[data-cy="select-answer-collection"]').contains(SECollection) + cy.get('[data-cy="save-new-question"]').should('be.disabled') + cy.get('[data-cy="configure-number-of-inputs"]') + .click() + .type(String(SEInputs)) + cy.get('[data-cy="save-new-question"]').click() + cy.wait(500) + + cy.get(`[data-cy="element-item-${SETitle}"]`).contains(SEContent) + cy.get(`[data-cy="element-item-${SETitle}"]`).contains(SETitle) + cy.get(`[data-cy="element-item-${SETitle}"]`).contains( + messages.shared.READY.statusLabel + ) + + cy.get(`[data-cy="edit-question-${SETitle}"]`).click() + // TODO: check that preview of selection question is visible and correct + }) + + it('Verify that the correct content has been saved', () => { + cy.get(`[data-cy="edit-question-${SETitle}"]`).click() + cy.get('[data-cy="insert-question-title"]').should('have.value', SETitle) + cy.get('[data-cy="select-question-status"]').contains( + messages.shared.READY.statusLabel + ) + cy.get('[data-cy="insert-question-text"]').realClick().contains(SEContent) + cy.get('[data-cy="insert-question-explanation"]') + .realClick() + .contains(SEExplanation) + cy.get('[data-cy="select-answer-collection"]').contains(SECollection) + cy.get('[data-cy="configure-number-of-inputs"]').should( + 'have.value', + SEInputs + ) + cy.get('[data-cy="close-question-modal"]').click() + }) + + it('Add a sample solution to the created selection question', () => { + cy.get(`[data-cy="edit-question-${SETitle}"]`).click() + cy.get('[data-cy="insert-question-title"]').should('have.value', SETitle) + + cy.get('[data-cy="configure-sample-solution"]').click() + cy.get('[data-cy="save-new-question"]').should('be.disabled') // at least one correct answer is required + cy.get('[data-cy="choose-correct-answer-options"]').click() + cy.findByText(SESolutions[0]).realClick() + cy.get('[data-cy="choose-correct-answer-options"]').contains(SESolutions[0]) + cy.get('[data-cy="save-new-question"]').should('be.disabled') // number of solutions needs to be >= number of inputs + SESolutions.slice(1).forEach((solution) => { + cy.get('[data-cy="choose-correct-answer-options"]').click() + cy.findByText(solution).realClick() + cy.get('[data-cy="choose-correct-answer-options"]').contains(solution) + }) + cy.get('[data-cy="save-new-question"]').click() + }) + + it('Verify that the sample solution has been stored correctly for the modified selection question', () => { + cy.get(`[data-cy="edit-question-${SETitle}"]`).click() + cy.get('[data-cy="insert-question-title"]').should('have.value', SETitle) + + SESolutions.forEach((solution) => { + cy.get('[data-cy="choose-correct-answer-options"]').contains(solution) + }) + cy.get('[data-cy="close-question-modal"]').click() + }) + + it('Edit the selection question and change the answer collection (including new sample solutions)', () => { + cy.get(`[data-cy="edit-question-${SETitle}"]`).click() + cy.get('[data-cy="insert-question-title"]') + .click() + .clear() + .type(SETitleEdited) + cy.get('[data-cy="insert-question-text"]') + .realClick() + .clear() + .type(SEContentEdited) + cy.get('[data-cy="insert-question-explanation"]') + .realClick() + .clear() + .type(SEExplanationEdited) + + cy.get('[data-cy="select-answer-collection"]').click() + cy.get(`[data-cy="select-answer-collection-${SECollectionEdited}"]`).click() + cy.get('[data-cy="select-answer-collection"]').contains(SECollectionEdited) + cy.get('[data-cy="save-new-question"]').should('be.disabled') // answer options are cleared on collection change + cy.get('[data-cy="configure-number-of-inputs"]') + .click() + .clear() + .type(String(SEInputsEdited)) + SESolutionsEdited.forEach((solution) => { + cy.get('[data-cy="choose-correct-answer-options"]').click() + cy.findByText(solution).realClick() + }) + + cy.get('[data-cy="save-new-question"]').click() + }) + + it.only('Verify that the edited state of the selection question persists', () => { + cy.get(`[data-cy="edit-question-${SETitleEdited}"]`).click() + cy.get('[data-cy="insert-question-title"]').should( + 'have.value', + SETitleEdited + ) + cy.get('[data-cy="insert-question-text"]') + .realClick() + .contains(SEContentEdited) + cy.get('[data-cy="insert-question-explanation"]') + .realClick() + .contains(SEExplanationEdited) + cy.get('[data-cy="select-answer-collection"]').contains(SECollectionEdited) + cy.get('[data-cy="configure-number-of-inputs"]').should( + 'have.value', + SEInputsEdited + ) + SESolutionsEdited.forEach((solution) => { + cy.get('[data-cy="choose-correct-answer-options"]').contains(solution) + }) + }) + it('Create a new question, duplicates it and then deletes the duplicate again', () => { const randomNumber = uuid() const questionTitle = 'A Single Choice ' + randomNumber diff --git a/cypress/cypress/e2e/K-resources-workflow.cy.ts b/cypress/cypress/e2e/K-resources-workflow.cy.ts index 0d12740ff0..992e2c76fb 100644 --- a/cypress/cypress/e2e/K-resources-workflow.cy.ts +++ b/cypress/cypress/e2e/K-resources-workflow.cy.ts @@ -216,6 +216,8 @@ describe('Create, edit and share answer collections', () => { }) }) + // TODO: verify that all three answer collections are available when creating a new selection question + it('Request access to the restricted answer catalogue for user pro1', () => { cy.loginIndividualCatalyst() cy.get('[data-cy="resources"]').click() @@ -373,6 +375,8 @@ describe('Create, edit and share answer collections', () => { }) }) + // TODO: check that the restricted answer catalogue can be used in selection questions for user pro1 + it('Verify that user pro2 does not have access to the restricted answer catalogue', () => { cy.loginInstitutionalCatalyst() cy.get('[data-cy="resources"]').click() @@ -381,6 +385,8 @@ describe('Create, edit and share answer collections', () => { ) }) + // TODO: check that the no answer catalogue is available user pro2 and that the corresponding message is shown + it('Import the public answer catalogue for user pro1 and verify access to it', () => { cy.loginIndividualCatalyst() cy.get('[data-cy="resources"]').click() diff --git a/packages/i18n/messages/de.ts b/packages/i18n/messages/de.ts index 18d6bced0d..82707a9b36 100644 --- a/packages/i18n/messages/de.ts +++ b/packages/i18n/messages/de.ts @@ -994,6 +994,8 @@ Da die KlickerUZH-App noch nicht im iOS-App-Store verfügbar ist, folgen Sie die 'Nehmen Sie hier optionale Einstellungen für die numerische Frage vor. Bitte beachten Sie, dass der Antwortbereich von numerischen Fragen auf das Intervall [-1e30,1e30] begrenzt ist. Sollten Sie grössere Zahlen benötigen, verwenden Sie bitte eine Freitext-Frage.', SELECTIONOptionsTooltip: 'Wählen Sie hier die Antwort-Sammlung aus welcher die Studierenden die korrekten Antworten auswählen sollen.', + SEAnswerCollectionRequired: + "Zur Erstellung von Auswahl-Fragen benötigen Sie Zugriff auf mindestens eine Antwort-Sammlung!. Sie können diese entweder unter dem Reiter 'Ressourcen' selbst erstellen oder dort eine bestehende Sammlung anderer Nutzer importieren.", selectCollection: 'Sammlung auswählen...', answerCollection: 'Antwort-Sammlung', numberOfInputs: 'Anzahl Eingabefelder', diff --git a/packages/i18n/messages/en.ts b/packages/i18n/messages/en.ts index 996e09af82..82e95175be 100644 --- a/packages/i18n/messages/en.ts +++ b/packages/i18n/messages/en.ts @@ -995,6 +995,8 @@ Since the KlickerUZH app is not yet available on the iOS App Store, follow these 'Enter optional settings for the numerical question here. Please note that the range of numbers for numerical questions is limited to the interval [-1e30,1e30] for technical reasons. Should you require to use larger numbers, please use a free text question instead.', SELECTIONOptionsTooltip: 'Please select the answer collection from which the students should select the correct answers.', + SEAnswerCollectionRequired: + 'To create selection questions, you need access to at least one answer collection! You can either create one yourself under the "Resources" tab or import an existing collection from other users there.', answerCollection: 'Answer collection', selectCollection: 'Select collection...', numberOfInputs: 'Number of inputs', diff --git a/packages/prisma/src/data/data/TEST.ts b/packages/prisma/src/data/data/TEST.ts index 9659445980..05329cce11 100644 --- a/packages/prisma/src/data/data/TEST.ts +++ b/packages/prisma/src/data/data/TEST.ts @@ -539,6 +539,12 @@ export const ANSWER_COLLECTIONS = [ { value: 'Dill', }, + { + value: 'Cucumber', + }, + { + value: 'Carrot', + }, ], }, { From bd28237089d1c235f255830dacb3a56e5292a9b2 Mon Sep 17 00:00:00 2001 From: sjschlapbach Date: Tue, 7 Jan 2025 17:39:27 +0100 Subject: [PATCH 2/3] chore(cypress): extend resources test workflow to include validation of collection availability --- .../cypress/e2e/D-questions-workflow.cy.ts | 2 +- .../cypress/e2e/K-resources-workflow.cy.ts | 68 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/cypress/cypress/e2e/D-questions-workflow.cy.ts b/cypress/cypress/e2e/D-questions-workflow.cy.ts index ef99952e71..0309ae53b0 100644 --- a/cypress/cypress/e2e/D-questions-workflow.cy.ts +++ b/cypress/cypress/e2e/D-questions-workflow.cy.ts @@ -1193,7 +1193,7 @@ describe('Create different types of elements (with and without sample solution) cy.get('[data-cy="save-new-question"]').click() }) - it.only('Verify that the edited state of the selection question persists', () => { + it('Verify that the edited state of the selection question persists', () => { cy.get(`[data-cy="edit-question-${SETitleEdited}"]`).click() cy.get('[data-cy="insert-question-title"]').should( 'have.value', diff --git a/cypress/cypress/e2e/K-resources-workflow.cy.ts b/cypress/cypress/e2e/K-resources-workflow.cy.ts index 992e2c76fb..5959a44a38 100644 --- a/cypress/cypress/e2e/K-resources-workflow.cy.ts +++ b/cypress/cypress/e2e/K-resources-workflow.cy.ts @@ -216,7 +216,23 @@ describe('Create, edit and share answer collections', () => { }) }) - // TODO: verify that all three answer collections are available when creating a new selection question + it('Verify that all three answer collections can be used in selection questions by owner', () => { + cy.loginLecturer() + cy.get('[data-cy="create-question"]').click() + cy.get('[data-cy="select-question-type"]').click() + cy.get( + `[data-cy="select-question-type-${messages.shared.SELECTION.typeLabel}"]` + ).click() + + cy.get('[data-cy="select-answer-collection"]').click() + cy.get(`[data-cy="select-answer-collection-${publicName}"]`).should('exist') + cy.get(`[data-cy="select-answer-collection-${restrictedName}"]`).should( + 'exist' + ) + cy.get(`[data-cy="select-answer-collection-${privateNameNew}"]`).should( + 'exist' + ) + }) it('Request access to the restricted answer catalogue for user pro1', () => { cy.loginIndividualCatalyst() @@ -375,7 +391,25 @@ describe('Create, edit and share answer collections', () => { }) }) - // TODO: check that the restricted answer catalogue can be used in selection questions for user pro1 + it('Verify that only the shared and restricted answer catalogue is available during question creation for user pro1', () => { + cy.loginIndividualCatalyst() + cy.get('[data-cy="create-question"]').click() + cy.get('[data-cy="select-question-type"]').click() + cy.get( + `[data-cy="select-question-type-${messages.shared.SELECTION.typeLabel}"]` + ).click() + + cy.get('[data-cy="select-answer-collection"]').click() + cy.get(`[data-cy="select-answer-collection-${publicName}"]`).should( + 'not.exist' + ) + cy.get(`[data-cy="select-answer-collection-${restrictedName}"]`).should( + 'exist' + ) + cy.get(`[data-cy="select-answer-collection-${privateNameNew}"]`).should( + 'not.exist' + ) + }) it('Verify that user pro2 does not have access to the restricted answer catalogue', () => { cy.loginInstitutionalCatalyst() @@ -385,7 +419,17 @@ describe('Create, edit and share answer collections', () => { ) }) - // TODO: check that the no answer catalogue is available user pro2 and that the corresponding message is shown + it('Verify that no answer catalogue is available for user pro2', () => { + cy.loginInstitutionalCatalyst() + cy.get('[data-cy="create-question"]').click() + cy.get('[data-cy="select-question-type"]').click() + cy.get( + `[data-cy="select-question-type-${messages.shared.SELECTION.typeLabel}"]` + ).click() + + cy.get('[data-cy="select-answer-collection"]').should('not.exist') + cy.findByText(messages.manage.questionForms.SEAnswerCollectionRequired) + }) it('Import the public answer catalogue for user pro1 and verify access to it', () => { cy.loginIndividualCatalyst() @@ -436,6 +480,24 @@ describe('Create, edit and share answer collections', () => { cy.get('[data-cy="close-viewing-collection-modal"]').click() }) + it('Verify that imported public answer collection is also available for during question creation user pro1', () => { + cy.loginIndividualCatalyst() + cy.get('[data-cy="create-question"]').click() + cy.get('[data-cy="select-question-type"]').click() + cy.get( + `[data-cy="select-question-type-${messages.shared.SELECTION.typeLabel}"]` + ).click() + + cy.get('[data-cy="select-answer-collection"]').click() + cy.get(`[data-cy="select-answer-collection-${publicName}"]`).should('exist') + cy.get(`[data-cy="select-answer-collection-${restrictedName}"]`).should( + 'exist' + ) + cy.get(`[data-cy="select-answer-collection-${privateNameNew}"]`).should( + 'not.exist' + ) + }) + it('Login again as user pro1 and verify that the answer catalogues are still visible', () => { cy.loginIndividualCatalyst() cy.get('[data-cy="resources"]').click() From 8ac6284ab8f626c8ce2da5354d995ee9af176e01 Mon Sep 17 00:00:00 2001 From: sjschlapbach Date: Tue, 7 Jan 2025 17:42:29 +0100 Subject: [PATCH 3/3] fix: delete selection question at the end of the selection workflow to ensure full functionality when running workflow multiple times --- cypress/cypress/e2e/D-questions-workflow.cy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cypress/cypress/e2e/D-questions-workflow.cy.ts b/cypress/cypress/e2e/D-questions-workflow.cy.ts index 0309ae53b0..4802440068 100644 --- a/cypress/cypress/e2e/D-questions-workflow.cy.ts +++ b/cypress/cypress/e2e/D-questions-workflow.cy.ts @@ -1287,5 +1287,9 @@ describe('Create different types of elements (with and without sample solution) cy.get(`[data-cy="delete-question-${FTTitleEdited}"]`).click() cy.get('[data-cy="confirm-question-deletion"]').click() cy.get(`[data-cy="element-item-${FTTitleEdited}"]`).should('not.exist') + + cy.get(`[data-cy="delete-question-${SETitleEdited}"]`).click() + cy.get('[data-cy="confirm-question-deletion"]').click() + cy.get(`[data-cy="element-item-${SETitleEdited}"]`).should('not.exist') }) })