diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/__tests__/validateItem.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/__tests__/validateItem.spec.js
index a3c397f44b..1bedf94a2b 100644
--- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/__tests__/validateItem.spec.js
+++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/__tests__/validateItem.spec.js
@@ -1,6 +1,7 @@
import { validateItemShape, validateQtiItem } from '../validateItem';
import { QuestionType, ValidationError } from '../constants';
import { assembleItemXml } from '../serialization/assembleItem';
+import { parseItem } from '../serialization/parseItem';
import { textEntryInteractionDescriptor } from '../interactions/textEntry/Descriptor';
import { validateTextEntryInteraction } from '../interactions/textEntry/validation';
import {
@@ -174,6 +175,27 @@ describe('validateQtiItem', () => {
});
});
+ describe('text entry answers', () => {
+ it.each([false, true])('reopens and accepts NULL with caseSensitive %s', caseSensitive => {
+ const { bodyXml, responseDeclarations } = textEntryInteractionDescriptor.buildXML(
+ {
+ prompt: '
Which SQL keyword marks a missing value?
',
+ answers: [{ id: 'a0', value: 'NULL', caseSensitive }],
+ expectedLength: 0,
+ },
+ QuestionType.TEXT_ENTRY,
+ );
+ const xml = assembleItemXml({ identifier: 'item', title: '', bodyXml, responseDeclarations });
+ const [interaction] = parseItem(xml).interactions;
+
+ expect(validateQtiItem(xml)).toEqual([]);
+ expect(
+ textEntryInteractionDescriptor.parse(interaction.bodyXml, interaction.responseDeclarations)
+ .answers,
+ ).toEqual([{ id: expect.any(String), value: 'NULL', caseSensitive }]);
+ });
+ });
+
it('reports unparseable XML', () => {
expect(validateQtiItem('')).toEqual([
{ code: ValidationError.PARSE_ERROR },
diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/textEntry/__tests__/parse.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/textEntry/__tests__/parse.spec.js
index 2a9d4e7673..25be823101 100644
--- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/textEntry/__tests__/parse.spec.js
+++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/textEntry/__tests__/parse.spec.js
@@ -344,7 +344,7 @@ describe('_extractAnswers', () => {
expect(errorSpy).toHaveBeenCalled();
});
- it.each(['1,234', '1.2.3', '1e400', 'Infinity', '0x10', '', 'NULL'])(
+ it.each(['1,234', '1.2.3', '1e400', 'Infinity', '0x10', ''])(
'reads answers past default value %j, which fromXML accepts',
defaultValue => {
const declXml = `
diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/serialization/qti/QTIDeclaration.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/serialization/qti/QTIDeclaration.js
index c681a9ee37..9f9fbe0016 100644
--- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/serialization/qti/QTIDeclaration.js
+++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/serialization/qti/QTIDeclaration.js
@@ -202,7 +202,7 @@ export class QTIDeclaration {
*
* Follows the Kolibri QTI implementation — each base-type maps to a
* specific JS primitive or 2-element array (point/pair/directedPair).
- * Returns null for empty, null, undefined, or 'NULL' inputs.
+ * Returns null for empty, null, or undefined inputs.
*
* @param {string|null|undefined} raw - Raw text from a element
* @param {string|null} baseType - One of BaseType.* constants
@@ -211,7 +211,7 @@ export class QTIDeclaration {
*/
static coerceValue(raw, baseType) {
// QTI treats empty / null as NULL — see qti-is-null spec
- if (raw === null || raw === undefined || raw === '' || raw === 'NULL') {
+ if (raw === null || raw === undefined || raw === '') {
return null;
}
diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/serialization/qti/__tests__/QTIDeclaration.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/serialization/qti/__tests__/QTIDeclaration.spec.js
index 2b95a31991..e3c7b87c41 100644
--- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/serialization/qti/__tests__/QTIDeclaration.spec.js
+++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/serialization/qti/__tests__/QTIDeclaration.spec.js
@@ -322,9 +322,13 @@ describe('QTIDeclaration full XML output (QTI compatibility)', () => {
describe('QTIDeclaration.coerceValue', () => {
describe('null / empty handling', () => {
- it.each([null, undefined, '', 'NULL'])('returns null for %p', raw => {
+ it.each([null, undefined, ''])('returns null for %p', raw => {
expect(QTIDeclaration.coerceValue(raw, 'identifier')).toBeNull();
});
+
+ it('keeps the string "NULL"', () => {
+ expect(QTIDeclaration.coerceValue('NULL', 'string')).toBe('NULL');
+ });
});
describe('boolean', () => {