diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/Editor.vue b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/Editor.vue index 174e989a49..aa113da68e 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/Editor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/Editor.vue @@ -248,7 +248,7 @@ import ClickableRegion from '../../components/ClickableRegion/index.vue'; import ShuffledResponsePool from '../../components/ShuffledResponsePool/index.vue'; import EditableChipList from '../../components/EditableChipList/index.vue'; - import { hasRichTextContent } from '../../utils/richText'; + import { choiceKey, hasRichTextContent } from '../../utils/richText'; import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor'; import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService'; @@ -537,21 +537,25 @@ () => new Set( flatten(state.value.pairs) - .filter(choice => hasRichTextContent(choice.content)) - .map(choice => richTextComparisonKey(choice.content)), + .map(choice => choiceKey(choice.content)) + .filter(Boolean), ), ); - // One option per association the learner can make: the emitted XML folds - // choices holding the same answer into a single option whose match-max is - // the number of occurrences walked here. - const poolChoices = computed(() => - [...flatten(state.value.pairs), ...state.value.distractors].map(choice => ({ - content: choice.content, - isCorrect: - props.showAnswers && pairedTexts.value.has(richTextComparisonKey(choice.content)), - })), - ); + // Kolibri's pool shows each choice of the interaction once. + const poolChoices = computed(() => { + const keys = new Set(); + const keyById = new Map(); + const choices = []; + for (const { id, content } of [...flatten(state.value.pairs), ...state.value.distractors]) { + const key = choiceKey(content); + if (key ? keys.has(key) : keyById.get(id) === '') continue; + if (key) keys.add(key); + if (!keyById.has(id)) keyById.set(id, key); + choices.push({ content, isCorrect: props.showAnswers && pairedTexts.value.has(key) }); + } + return choices; + }); return { EditorImageProcessor, diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/Editor.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/Editor.spec.js index fc5e8dfdb9..c85198d4c9 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/Editor.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/Editor.spec.js @@ -11,6 +11,7 @@ import { mockInteractionBlockWithDecl as blockWithDecl, } from '../../../utils/testingFixtures'; import { QuestionType } from '../../../constants'; +import { parseXML } from '../../../serialization/xml'; import { qtiEditorStrings as tr } from '../../../qtiEditorStrings'; jest.mock('shared/views/TipTapEditor/TipTapEditor/TipTapEditor'); @@ -60,6 +61,37 @@ const SHARED_CHOICE_DECL_XML = ` `; +const PAIRED_DISTRACTOR_XML = ` +

Match each character to his adversary.

+ Antonio + Prospero + Antonio +
`; + +const BLANK_CHOICES_XML = ` +

Match each character to his adversary.

+ + Prospero + Capulet + +
`; + +const THRICE_SHARED_CHOICE_XML = ` +

Match each character to his adversary.

+ Antonio + Prospero + Capulet + Montague +
`; + +const THRICE_SHARED_CHOICE_DECL_XML = ` + + choice_aaa11111 choice_bbb22222 + choice_aaa11111 choice_ccc33333 + choice_aaa11111 choice_ddd44444 + +`; + // The mock TipTapEditor renders a