diff --git a/src/AICC.js b/src/AICC.js index 98ecfa5..9c78f0a 100644 --- a/src/AICC.js +++ b/src/AICC.js @@ -1,17 +1,23 @@ // @flow import Scorm12API from './Scorm12API'; -import {CMIEvaluationCommentsObject, CMITriesObject, NAV} from './cmi/aicc_cmi'; +import { + CMI, + CMIEvaluationCommentsObject, + CMITriesObject, + NAV, +} from './cmi/aicc_cmi'; /** * The AICC API class */ -class AICC extends Scorm12API { +export default class AICC extends Scorm12API { /** * Constructor to create AICC API object */ constructor() { super(); + this.cmi = new CMI(this); this.nav = new NAV(this); } diff --git a/src/BaseAPI.js b/src/BaseAPI.js index 73a443c..5cedf11 100644 --- a/src/BaseAPI.js +++ b/src/BaseAPI.js @@ -389,7 +389,6 @@ export default class BaseAPI { * @param {string} CMIElement * @param {*} value * @return {string} - * @private */ _commonSetCMIValue( methodName: String, scorm2004: boolean, CMIElement, value) { diff --git a/src/Scorm12API.js b/src/Scorm12API.js index 01554ac..cb2c2e0 100644 --- a/src/Scorm12API.js +++ b/src/Scorm12API.js @@ -13,7 +13,6 @@ import {scorm12_error_codes} from './constants/error_codes'; import {scorm12_regex} from './regex'; const constants = scorm12_constants; -let _self; /** * API class for SCORM 1.2 @@ -24,18 +23,17 @@ export default class Scorm12API extends BaseAPI { */ constructor() { super(scorm12_error_codes); - _self = this; - _self.cmi = new CMI(this); + this.cmi = new CMI(this); // Rename functions to match 1.2 Spec and expose to modules - _self.LMSInitialize = _self.lmsInitialize; - _self.LMSFinish = _self.lmsFinish; - _self.LMSGetValue = _self.lmsGetValue; - _self.LMSSetValue = _self.lmsSetValue; - _self.LMSCommit = _self.lmsCommit; - _self.LMSGetLastError = _self.lmsGetLastError; - _self.LMSGetErrorString = _self.lmsGetErrorString; - _self.LMSGetDiagnostic = _self.lmsGetDiagnostic; + this.LMSInitialize = this.lmsInitialize; + this.LMSFinish = this.lmsFinish; + this.LMSGetValue = this.lmsGetValue; + this.LMSSetValue = this.lmsSetValue; + this.LMSCommit = this.lmsCommit; + this.LMSGetLastError = this.lmsGetLastError; + this.LMSGetErrorString = this.lmsGetErrorString; + this.LMSGetDiagnostic = this.lmsGetDiagnostic; } /** @@ -44,7 +42,7 @@ export default class Scorm12API extends BaseAPI { * @return {string} bool */ lmsInitialize() { - return _self.initialize('LMSInitialize', 'LMS was already initialized!', + return this.initialize('LMSInitialize', 'LMS was already initialized!', 'LMS is already finished!'); } @@ -54,7 +52,7 @@ export default class Scorm12API extends BaseAPI { * @return {string} bool */ lmsFinish() { - return _self.terminate('LMSFinish', false); + return this.terminate('LMSFinish', false); } /** @@ -64,7 +62,7 @@ export default class Scorm12API extends BaseAPI { * @return {string} */ lmsGetValue(CMIElement) { - return _self.getValue('LMSGetValue', false, CMIElement); + return this.getValue('LMSGetValue', false, CMIElement); } /** @@ -75,7 +73,7 @@ export default class Scorm12API extends BaseAPI { * @return {string} */ lmsSetValue(CMIElement, value) { - return _self.setValue('LMSSetValue', false, CMIElement, value); + return this.setValue('LMSSetValue', false, CMIElement, value); } /** @@ -84,7 +82,7 @@ export default class Scorm12API extends BaseAPI { * @return {string} bool */ lmsCommit() { - return _self.commit('LMSCommit', false); + return this.commit('LMSCommit', false); } /** @@ -93,7 +91,7 @@ export default class Scorm12API extends BaseAPI { * @return {string} */ lmsGetLastError() { - return _self.getLastError('LMSGetLastError'); + return this.getLastError('LMSGetLastError'); } /** @@ -103,7 +101,7 @@ export default class Scorm12API extends BaseAPI { * @return {string} */ lmsGetErrorString(CMIErrorCode) { - return _self.getErrorString('LMSGetErrorString', CMIErrorCode); + return this.getErrorString('LMSGetErrorString', CMIErrorCode); } /** @@ -113,7 +111,7 @@ export default class Scorm12API extends BaseAPI { * @return {string} */ lmsGetDiagnostic(CMIErrorCode) { - return _self.getDiagnostic('LMSGetDiagnostic', CMIErrorCode); + return this.getDiagnostic('LMSGetDiagnostic', CMIErrorCode); } /** @@ -123,7 +121,7 @@ export default class Scorm12API extends BaseAPI { * @param {*} value */ setCMIValue(CMIElement, value) { - _self._commonSetCMIValue('LMSSetValue', false, CMIElement, value); + this._commonSetCMIValue('LMSSetValue', false, CMIElement, value); } /** @@ -133,7 +131,7 @@ export default class Scorm12API extends BaseAPI { * @return {*} */ getCMIValue(CMIElement) { - return _self._commonGetCMIValue('getCMIValue', false, CMIElement); + return this._commonGetCMIValue('getCMIValue', false, CMIElement); } /** @@ -146,13 +144,13 @@ export default class Scorm12API extends BaseAPI { getChildElement(CMIElement, value) { let newChild; - if (_self.stringContains(CMIElement, 'cmi.objectives')) { + if (this.stringContains(CMIElement, 'cmi.objectives')) { newChild = new CMIObjectivesObject(this); - } else if (_self.stringContains(CMIElement, '.correct_responses')) { + } else if (this.stringContains(CMIElement, '.correct_responses')) { newChild = new CMIInteractionsCorrectResponsesObject(this); - } else if (_self.stringContains(CMIElement, '.objectives')) { + } else if (this.stringContains(CMIElement, '.objectives')) { newChild = new CMIInteractionsObjectivesObject(this); - } else if (_self.stringContains(CMIElement, 'cmi.interactions')) { + } else if (this.stringContains(CMIElement, 'cmi.interactions')) { newChild = new CMIInteractionsObject(this); } @@ -199,8 +197,8 @@ export default class Scorm12API extends BaseAPI { getCurrentTotalTime() { const timeRegex = new RegExp(scorm12_regex.CMITime); - const totalTime = _self.cmi.core.total_time; - const sessionTime = _self.cmi.core.session_time; + const totalTime = this.cmi.core.total_time; + const sessionTime = this.cmi.core.session_time; return Utilities.addHHMMSSTimeStrings(totalTime, sessionTime, timeRegex); } @@ -212,6 +210,6 @@ export default class Scorm12API extends BaseAPI { */ replaceWithAnotherScormAPI(newAPI) { // Data Model - _self.cmi = newAPI.cmi; + this.cmi = newAPI.cmi; } } diff --git a/src/cmi/aicc_cmi.js b/src/cmi/aicc_cmi.js index 726dfda..f090e3f 100644 --- a/src/cmi/aicc_cmi.js +++ b/src/cmi/aicc_cmi.js @@ -1,7 +1,8 @@ import * as Scorm12CMI from './scorm12_cmi'; import {BaseCMI, CMIArray, CMIScore} from './common'; -import {aicc_constants, scorm12_error_codes} from '../constants/api_constants'; +import {aicc_constants} from '../constants/api_constants'; import {aicc_regex} from '../regex'; +import {scorm12_error_codes} from '../constants/error_codes'; const constants = aicc_constants; const regex = aicc_regex; @@ -23,8 +24,9 @@ export class CMI extends Scorm12CMI.CMI { * @param {AICC} API */ constructor(API) { - super(API, constants.cmi_children, new AICCCMIStudentData(API)); + super(API, constants.cmi_children); + this.student_data = new AICCCMIStudentData(API); this.evaluation = new CMIEvaluation(API); } } @@ -40,16 +42,21 @@ class CMIEvaluation extends BaseCMI { constructor(API) { super(API); - this.comments = new class extends CMIArray { - /** - * Constructor for AICC Evaluation Comments object - * @param {AICC} API - */ - constructor(API) { - super(API, constants.comments_children, - scorm12_error_codes.INVALID_SET_VALUE); - } - }(API); + this.comments = new CMIEvaluationComments(API); + } +} + +/** + * Class representing AICC's cmi.evaluation.comments object + */ +class CMIEvaluationComments extends CMIArray { + /** + * Constructor for AICC Evaluation Comments object + * @param {AICC} API + */ + constructor(API) { + super(API, constants.comments_children, + scorm12_error_codes.INVALID_SET_VALUE); } } @@ -64,15 +71,7 @@ class AICCCMIStudentData extends Scorm12CMI.CMIStudentData { constructor(API) { super(API, constants.student_data_children); - this.tries = new class extends CMIArray { - /** - * Constructor for inline Tries Array class - * @param {AICC} API - */ - constructor(API) { - super(API, aicc_constants.tries_children); - } - }(API); + this.tries = new CMITries(API); } #tries_during_lesson = ''; @@ -93,7 +92,20 @@ class AICCCMIStudentData extends Scorm12CMI.CMIStudentData { set tries_during_lesson(tries_during_lesson) { this.API.isNotInitialized() ? this.#tries_during_lesson = tries_during_lesson : - throwReadOnlyError(); + throwReadOnlyError(this.API); + } +} + +/** + * Class representing the AICC cmi.student_data.tries object + */ +export class CMITries extends CMIArray { + /** + * Constructor for inline Tries Array class + * @param {AICC} API + */ + constructor(API) { + super(API, aicc_constants.tries_children); } } diff --git a/test/cmi/aicc_cmi.spec.js b/test/cmi/aicc_cmi.spec.js new file mode 100644 index 0000000..0975e8f --- /dev/null +++ b/test/cmi/aicc_cmi.spec.js @@ -0,0 +1,670 @@ +import {expect, assert} from 'chai'; +import {describe, it, beforeEach, afterEach} from 'mocha'; +import AICC from '../../src/AICC'; +import {aicc_constants} from '../../src/constants/api_constants'; +import {scorm12_error_codes} from '../../src/constants/error_codes'; + +let API; + +const checkFieldConstraintSize = ({fieldName, limit, expectedValue = ''}) => { + describe(`Field: ${fieldName}`, () => { + it(`Should be able to read from ${fieldName}`, () => { + expect(eval(`API.${fieldName}`)).to.equal(expectedValue); + }); + + it(`Should be able to write upto ${limit} characters to ${fieldName}`, + () => { + eval(`API.${fieldName} = 'x'.repeat(${limit})`); + expect(0).to.equal(API.lastErrorCode); + }); + + it(`Should fail to write more than ${limit} characters to ${fieldName}`, + () => { + eval(`API.${fieldName} = 'x'.repeat(${limit + 1})`); + expect(scorm12_error_codes.TYPE_MISMATCH + ''). + to. + equal(API.lastErrorCode); + }); + }); +}; + +const checkInvalidSet = ({fieldName, expectedValue = ''}) => { + describe(`Field: ${fieldName}`, () => { + it(`Should be able to read from ${fieldName}`, () => { + expect(eval(`API.${fieldName}`)).to.equal(expectedValue); + }); + + it(`Should fail to write to ${fieldName}`, () => { + eval(`API.${fieldName} = 'xxx'`); + expect(API.lastErrorCode). + to. + equal(scorm12_error_codes.INVALID_SET_VALUE + ''); + }); + }); +}; + +const checkReadOnly = ({fieldName, expectedValue = ''}) => { + describe(`Field: ${fieldName}`, () => { + it(`Should be able to read from ${fieldName}`, () => { + expect(eval(`API.${fieldName}`)).to.equal(expectedValue); + }); + + it(`Should fail to write to ${fieldName}`, () => { + eval(`API.${fieldName} = 'xxx'`); + expect(API.lastErrorCode). + to. + equal(scorm12_error_codes.READ_ONLY_ELEMENT + ''); + }); + }); +}; + +const checkRead = ({fieldName, expectedValue = ''}) => { + describe(`Field: ${fieldName}`, () => { + it(`Should be able to read from ${fieldName}`, () => { + expect(eval(`API.${fieldName}`)).to.equal(expectedValue); + }); + }); +}; + +const checkReadAndWrite = ({fieldName, expectedValue = '', valueToTest = 'xxx'}) => { + describe(`Field: ${fieldName}`, () => { + it(`Should be able to read from ${fieldName}`, () => { + expect(eval(`API.${fieldName}`)).to.equal(expectedValue); + }); + + it(`Should successfully write to ${fieldName}`, () => { + eval(`API.${fieldName} = '${valueToTest}'`); + expect(API.lastErrorCode).to.equal(0); + }); + }); +}; + +const checkWriteOnly = ({fieldName, valueToTest = 'xxx'}) => { + describe(`Field: ${fieldName}`, () => { + it(`Should fail to read from ${fieldName}`, () => { + eval(`API.${fieldName}`); + expect(API.lastErrorCode). + to. + equal(scorm12_error_codes.WRITE_ONLY_ELEMENT + ''); + }); + + it(`Should successfully write to ${fieldName}`, () => { + eval(`API.${fieldName} = '${valueToTest}'`); + expect(API.lastErrorCode).to.equal(0); + }); + }); +}; + +const checkWrite = ({fieldName, valueToTest = 'xxx'}) => { + describe(`Field: ${fieldName}`, () => { + it(`Should successfully write to ${fieldName}`, () => { + eval(`API.${fieldName} = '${valueToTest}'`); + expect(API.lastErrorCode).to.equal(0); + }); + }); +}; + +const checkValidValues = ({fieldName, expectedError, validValues, invalidValues}) => { + describe(`Field: ${fieldName}`, () => { + for (const idx in validValues) { + if ({}.hasOwnProperty.call(validValues, idx)) { + it(`Should successfully write '${validValues[idx]}' to ${fieldName}`, + () => { + eval(`API.${fieldName} = '${validValues[idx]}'`); + expect(API.lastErrorCode).to.equal(0); + }); + } + } + + for (const idx in invalidValues) { + if ({}.hasOwnProperty.call(invalidValues, idx)) { + it(`Should fail to write '${invalidValues[idx]}' to ${fieldName}`, + () => { + eval(`API.${fieldName} = '${invalidValues[idx]}'`); + expect(API.lastErrorCode).to.equal(expectedError + ''); + }); + } + } + }); +}; + +describe('AICC CMI Tests', () => { + describe('CMI Spec Tests', () => { + beforeEach('Create the API object', () => { + API = new AICC(); + API.lmsInitialize(); + }); + afterEach('Destroy API object', () => { + API = null; + }); + + it('lmsInitialize should create CMI object', () => { + assert(API.cmi !== undefined, 'CMI object is created'); + }); + + it('Exporting CMI to JSON produces proper Object', () => { + expect( + JSON.parse(API.renderCMIToJSON()).cmi?.core !== undefined, + ).to.be.true; + }); + + describe('Pre-Initialize Tests', () => { + beforeEach('Create the API object', () => { + API = new AICC(); + }); + afterEach('Destroy API object', () => { + API = null; + }); + + /** + * Base CMI Properties + */ + checkInvalidSet({fieldName: 'cmi._version', expectedValue: '3.4'}); + checkInvalidSet({ + fieldName: 'cmi._children', + expectedValue: aicc_constants.cmi_children, + }); + checkFieldConstraintSize({fieldName: 'cmi.suspend_data', limit: 4096}); + checkReadAndWrite({fieldName: 'cmi.launch_data'}); + checkFieldConstraintSize({fieldName: 'cmi.comments', limit: 4096}); + checkReadAndWrite({fieldName: 'cmi.comments_from_lms'}); + + /** + * cmi.core Properties + */ + checkInvalidSet({ + fieldName: 'cmi.core._children', + expectedValue: aicc_constants.core_children, + }); + checkReadAndWrite({fieldName: 'cmi.core.student_id'}); + checkReadAndWrite({fieldName: 'cmi.core.student_name'}); + checkFieldConstraintSize({ + fieldName: 'cmi.core.lesson_location', + limit: 255, + }); + checkReadAndWrite({fieldName: 'cmi.core.credit'}); + checkRead({fieldName: 'cmi.core.lesson_status'}); + checkValidValues({ + fieldName: 'cmi.core.lesson_status', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + 'passed', + 'completed', + 'failed', + 'incomplete', + 'browsed', + ], + invalidValues: [ + 'Passed', + 'P', + 'F', + 'p', + 'true', + 'false', + 'complete', + ], + }); + checkReadAndWrite({fieldName: 'cmi.core.entry'}); + checkReadAndWrite({fieldName: 'cmi.core.total_time'}); + checkReadAndWrite( + {fieldName: 'cmi.core.lesson_mode', expectedValue: 'normal'}); + checkWrite({fieldName: 'cmi.core.exit', valueToTest: 'suspend'}); + checkValidValues({ + fieldName: 'cmi.core.exit', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + 'time-out', + 'suspend', + 'logout', + ], invalidValues: [ + 'complete', + 'exit', + ], + }); + checkWriteOnly({ + fieldName: 'cmi.core.session_time', + valueToTest: '00:00:00', + }); + checkValidValues({ + fieldName: 'cmi.core.session_time', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '10:06:57', + '00:00:01.56', + '23:59:59', + '47:59:59', + ], + invalidValues: [ + '06:5:13', + '23:59:59.123', + 'P1DT23H59M59S', + ], + }); + + /** + * cmi.core.score Properties + */ + checkInvalidSet({ + fieldName: 'cmi.core.score._children', + expectedValue: aicc_constants.score_children, + }); + checkValidValues({ + fieldName: 'cmi.core.score.raw', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [ + '0', + '25.1', + '50.5', + '75', + '100', + ], + invalidValues: [ + '-1', + '101', + ], + }); + checkValidValues({ + fieldName: 'cmi.core.score.min', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [ + '0', + '25.1', + '50.5', + '75', + '100', + ], + invalidValues: [ + '-1', + '101', + ], + }); + checkValidValues({ + fieldName: 'cmi.core.score.max', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [ + '0', + '25.1', + '50.5', + '75', + '100', + ], + invalidValues: [ + '-1', + '101', + ], + }); + + /** + * cmi.objectives Properties + */ + checkInvalidSet({ + fieldName: 'cmi.objectives._children', + expectedValue: aicc_constants.objectives_children, + }); + checkInvalidSet({fieldName: 'cmi.objectives._count', expectedValue: 0}); + + /** + * cmi.student_data Properties + */ + checkInvalidSet({ + fieldName: 'cmi.student_data._children', + expectedValue: aicc_constants.student_data_children, + }); + checkReadAndWrite({fieldName: 'cmi.student_data.mastery_score'}); + checkReadAndWrite({fieldName: 'cmi.student_data.max_time_allowed'}); + checkReadAndWrite({fieldName: 'cmi.student_data.time_limit_action'}); + checkReadAndWrite({fieldName: 'cmi.student_data.tries_during_lesson'}); + + /** + * cmi.student_preference Properties + */ + checkInvalidSet({ + fieldName: 'cmi.student_preference._children', + expectedValue: aicc_constants.student_preference_children, + }); + checkValidValues({ + fieldName: 'cmi.student_preference.audio', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '1', + '-1', + '50', + '100', + ], + invalidValues: [ + 'invalid', + 'a100', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.audio', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [], + invalidValues: [ + '101', + '5000000', + '-500', + ], + }); + checkFieldConstraintSize({ + fieldName: 'cmi.student_preference.language', + limit: 255, + }); + checkValidValues({ + fieldName: 'cmi.student_preference.speed', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '1', + '-100', + '50', + '100', + ], + invalidValues: [ + 'invalid', + 'a100', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.speed', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [], + invalidValues: [ + '101', + '-101', + '5000000', + '-500', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.text', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '1', + '-1', + ], + invalidValues: [ + 'invalid', + 'a100', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.text', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [], + invalidValues: [ + '2', + '-2', + ], + }); + + /** + * cmi.interactions Properties + */ + checkInvalidSet({ + fieldName: 'cmi.interactions._children', + expectedValue: aicc_constants.interactions_children, + }); + checkInvalidSet({fieldName: 'cmi.interactions._count', expectedValue: 0}); + }); + + describe('Post-Initialize Tests', () => { + beforeEach('Create the API object', () => { + API = new AICC(); + API.lmsInitialize(); + }); + afterEach('Destroy API object', () => { + API = null; + }); + + /** + * Base CMI Properties + */ + checkInvalidSet({fieldName: 'cmi._version', expectedValue: '3.4'}); + checkInvalidSet({ + fieldName: 'cmi._children', + expectedValue: aicc_constants.cmi_children, + }); + checkFieldConstraintSize({fieldName: 'cmi.suspend_data', limit: 4096}); + checkReadOnly({fieldName: 'cmi.launch_data'}); + checkFieldConstraintSize({fieldName: 'cmi.comments', limit: 4096}); + checkReadOnly({fieldName: 'cmi.comments_from_lms'}); + + /** + * cmi.core Properties + */ + checkInvalidSet({ + fieldName: 'cmi.core._children', + expectedValue: aicc_constants.core_children, + }); + checkReadOnly({fieldName: 'cmi.core.student_id'}); + checkReadOnly({fieldName: 'cmi.core.student_name'}); + checkFieldConstraintSize({ + fieldName: 'cmi.core.lesson_location', + limit: 255, + }); + checkReadOnly({fieldName: 'cmi.core.credit'}); + checkRead({fieldName: 'cmi.core.lesson_status'}); + checkValidValues({ + fieldName: 'cmi.core.lesson_status', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + 'passed', + 'completed', + 'failed', + 'incomplete', + 'browsed', + ], + invalidValues: [ + 'Passed', + 'P', + 'F', + 'p', + 'true', + 'false', + 'complete', + ], + }); + checkReadOnly({fieldName: 'cmi.core.entry'}); + checkReadOnly({fieldName: 'cmi.core.total_time'}); + checkReadOnly( + {fieldName: 'cmi.core.lesson_mode', expectedValue: 'normal'}); + checkWrite({fieldName: 'cmi.core.exit', valueToTest: 'suspend'}); + checkValidValues({ + fieldName: 'cmi.core.exit', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + 'time-out', + 'suspend', + 'logout', + ], invalidValues: [ + 'complete', + 'exit', + ], + }); + checkWriteOnly({ + fieldName: 'cmi.core.session_time', + valueToTest: '00:00:00', + }); + checkValidValues({ + fieldName: 'cmi.core.session_time', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '10:06:57', + '00:00:01.56', + '23:59:59', + '47:59:59', + ], + invalidValues: [ + '06:5:13', + '23:59:59.123', + 'P1DT23H59M59S', + ], + }); + + /** + * cmi.core.score Properties + */ + checkInvalidSet({ + fieldName: 'cmi.core.score._children', + expectedValue: aicc_constants.score_children, + }); + checkValidValues({ + fieldName: 'cmi.core.score.raw', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [ + '0', + '25.1', + '50.5', + '75', + '100', + ], + invalidValues: [ + '-1', + '101', + ], + }); + checkValidValues({ + fieldName: 'cmi.core.score.min', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [ + '0', + '25.1', + '50.5', + '75', + '100', + ], + invalidValues: [ + '-1', + '101', + ], + }); + checkValidValues({ + fieldName: 'cmi.core.score.max', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [ + '0', + '25.1', + '50.5', + '75', + '100', + ], + invalidValues: [ + '-1', + '101', + ], + }); + + /** + * cmi.objectives Properties + */ + checkInvalidSet({ + fieldName: 'cmi.objectives._children', + expectedValue: aicc_constants.objectives_children, + }); + checkInvalidSet({fieldName: 'cmi.objectives._count', expectedValue: 0}); + + /** + * cmi.student_data Properties + */ + checkInvalidSet({ + fieldName: 'cmi.student_data._children', + expectedValue: aicc_constants.student_data_children, + }); + checkReadOnly({fieldName: 'cmi.student_data.mastery_score'}); + checkReadOnly({fieldName: 'cmi.student_data.max_time_allowed'}); + checkReadOnly({fieldName: 'cmi.student_data.time_limit_action'}); + checkReadOnly({fieldName: 'cmi.student_data.tries_during_lesson'}); + + /** + * cmi.student_preference Properties + */ + checkInvalidSet({ + fieldName: 'cmi.student_preference._children', + expectedValue: aicc_constants.student_preference_children, + }); + checkValidValues({ + fieldName: 'cmi.student_preference.audio', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '1', + '-1', + '50', + '100', + ], + invalidValues: [ + 'invalid', + 'a100', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.audio', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [], + invalidValues: [ + '101', + '5000000', + '-500', + ], + }); + checkFieldConstraintSize({ + fieldName: 'cmi.student_preference.language', + limit: 255, + }); + checkValidValues({ + fieldName: 'cmi.student_preference.speed', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '1', + '-100', + '50', + '100', + ], + invalidValues: [ + 'invalid', + 'a100', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.speed', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [], + invalidValues: [ + '101', + '-101', + '5000000', + '-500', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.text', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '1', + '-1', + ], + invalidValues: [ + 'invalid', + 'a100', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.text', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [], + invalidValues: [ + '2', + '-2', + ], + }); + + /** + * cmi.interactions Properties + */ + checkInvalidSet({ + fieldName: 'cmi.interactions._children', + expectedValue: aicc_constants.interactions_children, + }); + checkInvalidSet({fieldName: 'cmi.interactions._count', expectedValue: 0}); + }); + }); +}); diff --git a/test/cmi/scorm12_cmi.spec.js b/test/cmi/scorm12_cmi.spec.js index 951e75e..bb73333 100644 --- a/test/cmi/scorm12_cmi.spec.js +++ b/test/cmi/scorm12_cmi.spec.js @@ -66,6 +66,19 @@ const checkRead = ({fieldName, expectedValue = ''}) => { }); }; +const checkReadAndWrite = ({fieldName, expectedValue = '', valueToTest = 'xxx'}) => { + describe(`Field: ${fieldName}`, () => { + it(`Should be able to read from ${fieldName}`, () => { + expect(eval(`API.${fieldName}`)).to.equal(expectedValue); + }); + + it(`Should successfully write to ${fieldName}`, () => { + eval(`API.${fieldName} = '${valueToTest}'`); + expect(API.lastErrorCode).to.equal(0); + }); + }); +}; + const checkWriteOnly = ({fieldName, valueToTest = 'xxx'}) => { describe(`Field: ${fieldName}`, () => { it(`Should fail to read from ${fieldName}`, () => { @@ -115,9 +128,285 @@ const checkValidValues = ({fieldName, expectedError, validValues, invalidValues} }); }; -describe('SCORM 1.2 API Tests', () => { +describe('SCORM 1.2 CMI Tests', () => { describe('CMI Spec Tests', () => { - describe('Post-lmsInitialize Tests', () => { + beforeEach('Create the API object', () => { + API = new Scorm12API(); + API.lmsInitialize(); + }); + afterEach('Destroy API object', () => { + API = null; + }); + + it('lmsInitialize should create CMI object', () => { + assert(API.cmi !== undefined, 'CMI object is created'); + }); + + it('Exporting CMI to JSON produces proper Object', () => { + expect( + JSON.parse(API.renderCMIToJSON()).cmi?.core !== undefined, + ).to.be.true; + }); + + describe('Pre-Initialize Tests', () => { + beforeEach('Create the API object', () => { + API = new Scorm12API(); + }); + afterEach('Destroy API object', () => { + API = null; + }); + + /** + * Base CMI Properties + */ + checkInvalidSet({fieldName: 'cmi._version', expectedValue: '3.4'}); + checkInvalidSet({ + fieldName: 'cmi._children', + expectedValue: scorm12_constants.cmi_children, + }); + checkFieldConstraintSize({fieldName: 'cmi.suspend_data', limit: 4096}); + checkReadAndWrite({fieldName: 'cmi.launch_data'}); + checkFieldConstraintSize({fieldName: 'cmi.comments', limit: 4096}); + checkReadAndWrite({fieldName: 'cmi.comments_from_lms'}); + + /** + * cmi.core Properties + */ + checkInvalidSet({ + fieldName: 'cmi.core._children', + expectedValue: scorm12_constants.core_children, + }); + checkReadAndWrite({fieldName: 'cmi.core.student_id'}); + checkReadAndWrite({fieldName: 'cmi.core.student_name'}); + checkFieldConstraintSize({ + fieldName: 'cmi.core.lesson_location', + limit: 255, + }); + checkReadAndWrite({fieldName: 'cmi.core.credit'}); + checkRead({fieldName: 'cmi.core.lesson_status'}); + checkValidValues({ + fieldName: 'cmi.core.lesson_status', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + 'passed', + 'completed', + 'failed', + 'incomplete', + 'browsed', + ], + invalidValues: [ + 'Passed', + 'P', + 'F', + 'p', + 'true', + 'false', + 'complete', + ], + }); + checkReadAndWrite({fieldName: 'cmi.core.entry'}); + checkReadAndWrite({fieldName: 'cmi.core.total_time'}); + checkReadAndWrite( + {fieldName: 'cmi.core.lesson_mode', expectedValue: 'normal'}); + checkWrite({fieldName: 'cmi.core.exit', valueToTest: 'suspend'}); + checkValidValues({ + fieldName: 'cmi.core.exit', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + 'time-out', + 'suspend', + 'logout', + ], invalidValues: [ + 'complete', + 'exit', + ], + }); + checkWriteOnly({ + fieldName: 'cmi.core.session_time', + valueToTest: '00:00:00', + }); + checkValidValues({ + fieldName: 'cmi.core.session_time', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '10:06:57', + '00:00:01.56', + '23:59:59', + '47:59:59', + ], + invalidValues: [ + '06:5:13', + '23:59:59.123', + 'P1DT23H59M59S', + ], + }); + + /** + * cmi.core.score Properties + */ + checkInvalidSet({ + fieldName: 'cmi.core.score._children', + expectedValue: scorm12_constants.score_children, + }); + checkValidValues({ + fieldName: 'cmi.core.score.raw', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [ + '0', + '25.1', + '50.5', + '75', + '100', + ], + invalidValues: [ + '-1', + '101', + ], + }); + checkValidValues({ + fieldName: 'cmi.core.score.min', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [ + '0', + '25.1', + '50.5', + '75', + '100', + ], + invalidValues: [ + '-1', + '101', + ], + }); + checkValidValues({ + fieldName: 'cmi.core.score.max', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [ + '0', + '25.1', + '50.5', + '75', + '100', + ], + invalidValues: [ + '-1', + '101', + ], + }); + + /** + * cmi.objectives Properties + */ + checkInvalidSet({ + fieldName: 'cmi.objectives._children', + expectedValue: scorm12_constants.objectives_children, + }); + checkInvalidSet({fieldName: 'cmi.objectives._count', expectedValue: 0}); + + /** + * cmi.student_data Properties + */ + checkInvalidSet({ + fieldName: 'cmi.student_data._children', + expectedValue: scorm12_constants.student_data_children, + }); + checkReadAndWrite({fieldName: 'cmi.student_data.mastery_score'}); + checkReadAndWrite({fieldName: 'cmi.student_data.max_time_allowed'}); + checkReadAndWrite({fieldName: 'cmi.student_data.time_limit_action'}); + + /** + * cmi.student_preference Properties + */ + checkInvalidSet({ + fieldName: 'cmi.student_preference._children', + expectedValue: scorm12_constants.student_preference_children, + }); + checkValidValues({ + fieldName: 'cmi.student_preference.audio', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '1', + '-1', + '50', + '100', + ], + invalidValues: [ + 'invalid', + 'a100', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.audio', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [], + invalidValues: [ + '101', + '5000000', + '-500', + ], + }); + checkFieldConstraintSize({ + fieldName: 'cmi.student_preference.language', + limit: 255, + }); + checkValidValues({ + fieldName: 'cmi.student_preference.speed', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '1', + '-100', + '50', + '100', + ], + invalidValues: [ + 'invalid', + 'a100', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.speed', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [], + invalidValues: [ + '101', + '-101', + '5000000', + '-500', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.text', + expectedError: scorm12_error_codes.TYPE_MISMATCH, + validValues: [ + '1', + '-1', + ], + invalidValues: [ + 'invalid', + 'a100', + ], + }); + checkValidValues({ + fieldName: 'cmi.student_preference.text', + expectedError: scorm12_error_codes.VALUE_OUT_OF_RANGE, + validValues: [], + invalidValues: [ + '2', + '-2', + ], + }); + + /** + * cmi.interactions Properties + */ + checkInvalidSet({ + fieldName: 'cmi.interactions._children', + expectedValue: scorm12_constants.interactions_children, + }); + checkInvalidSet({fieldName: 'cmi.interactions._count', expectedValue: 0}); + }); + + describe('Post-Initialize Tests', () => { beforeEach('Create the API object', () => { API = new Scorm12API(); API.lmsInitialize(); @@ -126,16 +415,6 @@ describe('SCORM 1.2 API Tests', () => { API = null; }); - it('lmsInitialize should create CMI object', () => { - assert(API.cmi !== undefined, 'CMI object is created'); - }); - - it('Exporting CMI to JSON produces proper Object', () => { - expect( - JSON.parse(API.renderCMIToJSON()).cmi?.core !== undefined, - ).to.be.true; - }); - /** * Base CMI Properties */