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..2aff6f8a7e 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,7 @@ :interaction="interaction" :mode="mode" :showAnswers="showAnswers" + @update:interaction="interaction => $emit('update:interaction', interaction)" /> @@ -30,8 +31,8 @@ name: 'InteractionSection', setup(props, { emit }) { - const bodyXmlRef = computed(() => props.interaction?.bodyXml); - const { descriptor, questionType, parseError } = useInteractionDescriptor(bodyXmlRef); + const interactionRef = computed(() => props.interaction); + const { descriptor, questionType, parseError } = useInteractionDescriptor(interactionRef); watch( questionType, @@ -67,7 +68,7 @@ }, }, - emits: ['update:questionType'], + emits: ['update:questionType', 'update:interaction'], }; 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..00d8eb76d2 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,7 @@ :mode="mode" :showAnswers="showAnswers" @update:questionType="type => (currentQuestionType = type)" + @update:interaction="onUpdateInteraction" />
- import { computed, ref } from 'vue';
+ import { computed, ref, watch } from 'vue';
import { qtiEditorStrings } from '../../qtiEditorStrings';
import { QuestionType } from '../../constants';
import useQtiItem from '../../composables/useQtiItem';
+ import { assembleItemXml } from '../../serialization/assembleItem';
import InteractionSection from '../InteractionSection/index.vue';
export default {
@@ -70,7 +72,7 @@
components: { InteractionSection },
- setup(props) {
+ setup(props, { emit }) {
const {
questionNumberLabel$,
questionNumberAndTypeLabel$,
@@ -79,7 +81,7 @@
unknownTypeLabel$,
} = qtiEditorStrings;
- const { interactions } = useQtiItem(props.item.raw_data);
+ const { identifier, title, language, interactions } = useQtiItem(props.item.raw_data);
const questionNumberLabel = computed(() =>
questionNumberLabel$({
@@ -116,6 +118,46 @@
}),
);
+ /**
+ * Track the current bodyXml and responseDeclarations for the interaction.
+ * Initialised from the parsed item; updated atomically when the editor
+ * emits update:interaction.
+ *
+ * Note: We are currently using only the first interaction, but this
+ * architecture is designed to be expansible to multiple interactions
+ * per question in the future.
+ */
+ const currentBodyXml = ref(
+ interactions.value.length > 0 ? interactions.value[0].bodyXml : '',
+ );
+ const currentResponseDeclarations = ref(
+ interactions.value.length > 0 ? interactions.value[0].responseDeclarations : [],
+ );
+
+ const rawData = computed(() =>
+ assembleItemXml({
+ identifier: identifier.value,
+ title: title.value,
+ language: language.value,
+ bodyXml: currentBodyXml.value,
+ responseDeclarations: currentResponseDeclarations.value,
+ }),
+ );
+
+ // Emit only when the assembled XML actually changes after initial mount
+ watch(rawData, newVal => {
+ if (process.env.NODE_ENV === 'development') {
+ // eslint-disable-next-line no-console
+ console.log('[QTIItemEditor] assembled XML:\n', newVal);
+ }
+ emit('update:rawData', newVal);
+ });
+
+ function onUpdateInteraction({ bodyXml, responseDeclarations }) {
+ currentBodyXml.value = bodyXml;
+ currentResponseDeclarations.value = responseDeclarations;
+ }
+
return {
currentQuestionType,
interactions,
@@ -123,6 +165,7 @@
questionNumberAndTypeLabel,
closeBtnLabel$,
questionContentPlaceholder$,
+ onUpdateInteraction,
};
},
@@ -158,7 +201,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..58808bc72c
--- /dev/null
+++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/ValidationMessage/index.vue
@@ -0,0 +1,33 @@
+
+
+
New prompt
'); + expect(state.value.prompt).toBe('New prompt
'); + }); + }); + + describe('setChoiceContent()', () => { + it('updates content for the target choice only', () => { + const { state, setChoiceContent } = setup([ + makeAnswer({ id: 'a', content: 'Old' }), + makeAnswer({ id: 'b', content: 'Unchanged' }), + ]); + setChoiceContent('a', 'New content'); + expect(state.value.choices.find(a => a.id === 'a').content).toBe('New content'); + expect(state.value.choices.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); + }); + }); +}); 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..f9619c45f2 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/composables/__tests__/useInteraction.spec.js @@ -0,0 +1,169 @@ +import { ref, nextTick } 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: '