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', () => {