Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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: '<p>Which SQL keyword marks a missing value?</p>',
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('<qti-assessment-item><oops>')).toEqual([
{ code: ValidationError.PARSE_ERROR },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <qti-value> element
* @param {string|null} baseType - One of BaseType.* constants
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading