diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/InteractionSection/__tests__/InteractionSection.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/InteractionSection/__tests__/InteractionSection.spec.js index 9261242d32..c5f008646e 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/InteractionSection/__tests__/InteractionSection.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/InteractionSection/__tests__/InteractionSection.spec.js @@ -9,6 +9,8 @@ import { mockInteractionBlock as interactionBlock, } from '../../../utils/testingFixtures'; +jest.mock('shared/views/TipTapEditor/TipTapEditor/TipTapEditor'); + const renderSection = (props = {}) => render(InteractionSection, { props: { mode: 'edit', ...props }, diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/InteractionSection/index.vue b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/InteractionSection/index.vue index ee9137e245..4bf3efb30f 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/InteractionSection/index.vue +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/InteractionSection/index.vue @@ -15,6 +15,8 @@ :interaction="interaction" :mode="mode" :showAnswers="showAnswers" + @update:bodyXml="xml => $emit('update:bodyXml', xml)" + @update:responseDeclarations="decls => $emit('update:responseDeclarations', decls)" /> @@ -67,7 +69,7 @@ }, }, - emits: ['update:questionType'], + emits: ['update:questionType', 'update:bodyXml', 'update:responseDeclarations'], }; diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/QTIItemEditor/index.vue b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/QTIItemEditor/index.vue index e21b5139f1..27f4748601 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/QTIItemEditor/index.vue +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/QTIItemEditor/index.vue @@ -33,6 +33,8 @@ :mode="mode" :showAnswers="showAnswers" @update:questionType="type => (currentQuestionType = type)" + @update:bodyXml="onBodyXmlUpdate" + @update:responseDeclarations="onResponseDeclarationsUpdate" />
questionNumberLabel$({
@@ -116,6 +119,38 @@
}),
);
+ /**
+ * Track the current bodyXml and responseDeclarations for the interaction.
+ * Initialized from the parsed item; updated when the editor emits changes.
+ */
+ const currentBodyXml = ref(
+ interactions.value.length > 0 ? interactions.value[0].bodyXml : '',
+ );
+ const currentResponseDeclarations = ref(
+ interactions.value.length > 0 ? interactions.value[0].responseDeclarations : [],
+ );
+
+ function onBodyXmlUpdate(xml) {
+ currentBodyXml.value = xml;
+ emitRawData();
+ }
+
+ function onResponseDeclarationsUpdate(decls) {
+ currentResponseDeclarations.value = decls;
+ emitRawData();
+ }
+
+ function emitRawData() {
+ const newRawData = reassembleItemXml({
+ identifier: identifier.value,
+ title: title.value,
+ language: language.value,
+ bodyXml: currentBodyXml.value,
+ responseDeclarations: currentResponseDeclarations.value,
+ });
+ emit('update:rawData', newRawData);
+ }
+
return {
currentQuestionType,
interactions,
@@ -123,6 +158,8 @@
questionNumberAndTypeLabel,
closeBtnLabel$,
questionContentPlaceholder$,
+ onBodyXmlUpdate,
+ onResponseDeclarationsUpdate,
};
},
@@ -158,7 +195,7 @@
},
},
- emits: ['close'],
+ emits: ['close', 'update:rawData'],
};
diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/ValidationMessage/index.vue b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/ValidationMessage/index.vue
new file mode 100644
index 0000000000..dc98110055
--- /dev/null
+++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/ValidationMessage/index.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/composables/__tests__/useChoiceInteraction.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/composables/__tests__/useChoiceInteraction.spec.js
new file mode 100644
index 0000000000..0b48c4d064
--- /dev/null
+++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/composables/__tests__/useChoiceInteraction.spec.js
@@ -0,0 +1,197 @@
+import { ref } from 'vue';
+import { useChoiceInteraction } from '../useChoiceInteraction';
+import { QuestionType } from '../../constants';
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function makeAnswer(overrides = {}) {
+ return { id: 'choice_a', content: 'A', correct: false, fixed: false, ...overrides };
+}
+
+function makeBlock(answers, questionType = QuestionType.SINGLE_SELECT) {
+ const maxChoices = questionType === QuestionType.SINGLE_SELECT ? 1 : 2;
+ const correctIds = answers.filter(a => a.correct).map(a => a.id);
+
+ const bodyXml = `
New prompt
'); + expect(state.value.prompt).toBe('New prompt
'); + }); + }); + + describe('setChoiceContent()', () => { + it('updates content for the target answer only', () => { + const { state, setChoiceContent } = setup([ + makeAnswer({ id: 'a', content: 'Old' }), + makeAnswer({ id: 'b', content: 'Unchanged' }), + ]); + setChoiceContent('a', 'New content'); + expect(state.value.answers.find(a => a.id === 'a').content).toBe('New content'); + expect(state.value.answers.find(a => a.id === 'b').content).toBe('Unchanged'); + }); + }); + + describe('setShuffle()', () => { + it('updates the shuffle flag', () => { + const { state, setShuffle } = setup([makeAnswer({ id: 'a' }), makeAnswer({ id: 'b' })]); + setShuffle(true); + expect(state.value.shuffle).toBe(true); + }); + }); + + describe('setOrientation()', () => { + it('updates the orientation field', () => { + const { state, setOrientation } = setup([makeAnswer({ id: 'a' }), makeAnswer({ id: 'b' })]); + setOrientation('horizontal'); + expect(state.value.orientation).toBe('horizontal'); + }); + }); + + describe('setMaxChoices()', () => { + it('updates the maxChoices field', () => { + const { state, setMaxChoices } = setup([makeAnswer({ id: 'a' }), makeAnswer({ id: 'b' })]); + setMaxChoices(3); + expect(state.value.maxChoices).toBe(3); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/composables/__tests__/useInteraction.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/composables/__tests__/useInteraction.spec.js new file mode 100644 index 0000000000..0196a9c215 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/composables/__tests__/useInteraction.spec.js @@ -0,0 +1,139 @@ +import { ref } from 'vue'; +import { useInteraction } from '../useInteraction'; + +// --------------------------------------------------------------------------- +// Minimal descriptor stub +// --------------------------------------------------------------------------- + +function makeDescriptor({ parseReturn = {}, buildReturn = null, validateReturn = [] } = {}) { + return { + parse: jest.fn(() => parseReturn), + buildXML: jest.fn(() => buildReturn ?? { bodyXml: '