From 2f5c9804c94fcc7090fca50fabe0f82662333208 Mon Sep 17 00:00:00 2001 From: Jonathan Putney Date: Mon, 11 Nov 2019 15:05:02 -0500 Subject: [PATCH] Fixing eslint issues, and still working on test cases --- .babelrc | 3 +- .circleci/config.yml | 10 +- .eslintrc.js | 9 +- package-lock.json | 25 + package.json | 2 + src/BaseAPI.js | 169 ++- src/Scorm12API.js | 193 +-- src/Scorm2004API.js | 84 +- src/cmi/aicc_cmi.js | 55 +- src/cmi/common.js | 250 ++-- src/cmi/scorm12_cmi.js | 1397 ++++++++++++++------ src/cmi/scorm2004_cmi.js | 1906 ++++++++++++++++++++------- src/constants/api_constants.js | 2 +- src/constants/language_constants.js | 2 +- src/regex.js | 126 +- src/utilities.js | 55 +- test/Scorm12API.spec.js | 283 ---- test/cmi/scorm12_cmi.spec.js | 389 ++++++ test/utilities.spec.js | 425 +++--- 19 files changed, 3667 insertions(+), 1718 deletions(-) delete mode 100644 test/Scorm12API.spec.js create mode 100644 test/cmi/scorm12_cmi.spec.js diff --git a/.babelrc b/.babelrc index 0d8350c..754eb68 100644 --- a/.babelrc +++ b/.babelrc @@ -5,6 +5,7 @@ ], "plugins": [ "@babel/plugin-proposal-class-properties", - "@babel/plugin-proposal-private-methods" + "@babel/plugin-proposal-private-methods", + "@babel/plugin-proposal-optional-chaining" ] } \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index f110a3d..6c6551a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,11 +49,11 @@ jobs: when: always # Run eslint - # - run: - # name: eslint - # command: | - # ./node_modules/.bin/eslint ./ --format junit --output-file ./reports/eslint/eslint.xml - # when: always + - run: + name: eslint + command: | + ./node_modules/.bin/eslint ./ --format junit --output-file ./reports/eslint/eslint.xml + when: always # Run coverage report for Code Climate - run: diff --git a/.eslintrc.js b/.eslintrc.js index 2b236b8..63430f0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - parser: "babel-eslint", + parser: 'babel-eslint', env: { browser: true, es6: true, @@ -10,18 +10,19 @@ module.exports = { SharedArrayBuffer: 'readonly', }, parserOptions: { - sourceType: "module", + sourceType: 'module', allowImportExportEverywhere: false, classPrivateMethods: true, ecmaFeatures: { globalReturn: false, }, babelOptions: { - configFile: "./.babelrc", + configFile: './.babelrc', }, }, rules: { - camelcase: 'off', + 'camelcase': 'off', 'max-len': 'off', + 'no-unused-vars': 'off', }, }; diff --git a/package-lock.json b/package-lock.json index 0203cba..ffd1cb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -423,6 +423,16 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" } }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.6.0.tgz", + "integrity": "sha512-kj4gkZ6qUggkprRq3Uh5KP8XnE1MdIO0J7MhdDX8+rAbB6dJ2UrensGIS+0NPZAaaJ1Vr0PN6oLUgXMU1uMcSg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-chaining": "^7.2.0" + } + }, "@babel/plugin-proposal-private-methods": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.6.0.tgz", @@ -497,6 +507,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.2.0.tgz", + "integrity": "sha512-HtGCtvp5Uq/jH/WNUPkK6b7rufnCPLLlDAFN7cmACoIjaOOiXxUt3SswU5loHqrhtqTsa/WoLQ1OQ1AGuZqaWA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-syntax-top-level-await": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.7.0.tgz", @@ -960,6 +979,12 @@ "to-fast-properties": "^2.0.0" } }, + "@types/chai": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.4.tgz", + "integrity": "sha512-7qvf9F9tMTzo0akeswHPGqgUx/gIaJqrOEET/FCD8CFRkSUHlygQiM5yB6OvjrtdxBVLSyw7COJubsFYs0683g==", + "dev": true + }, "acorn": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", diff --git a/package.json b/package.json index 06ce77a..ca5e318 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,12 @@ "@babel/core": "^7.7.2", "@babel/node": "^7.7.0", "@babel/plugin-proposal-class-properties": "^7.7.0", + "@babel/plugin-proposal-optional-chaining": "^7.6.0", "@babel/plugin-proposal-private-methods": "^7.6.0", "@babel/preset-env": "^7.7.1", "@babel/preset-flow": "^7.0.0", "@babel/register": "^7.7.0", + "@types/chai": "^4.2.4", "babel-eslint": "^11.0.0-beta.0", "chai": "^4.2.0", "eslint": "^6.6.0", diff --git a/src/BaseAPI.js b/src/BaseAPI.js index 4bd4d80..73a443c 100644 --- a/src/BaseAPI.js +++ b/src/BaseAPI.js @@ -134,7 +134,7 @@ export default class BaseAPI { * @param {string} callbackName * @param {boolean} checkTerminated * @param {string} CMIElement - * @param {any} value + * @param {*} value * @return {string} */ setValue( @@ -204,7 +204,7 @@ export default class BaseAPI { * Returns the errorNumber error description * * @param {string} callbackName - * @param {number} CMIErrorCode + * @param {(string|number)} CMIErrorCode * @return {string} */ getErrorString(callbackName: String, CMIErrorCode) { @@ -224,8 +224,8 @@ export default class BaseAPI { /** * Returns a comprehensive description of the errorNumber error. * - * @param callbackName - * @param CMIErrorCode + * @param {string} callbackName + * @param {(string|number)} CMIErrorCode * @return {string} */ getDiagnostic(callbackName: String, CMIErrorCode) { @@ -243,10 +243,16 @@ export default class BaseAPI { } /** - * Checks the LMS state and ensures it has been initialized + * Checks the LMS state and ensures it has been initialized. + * + * @param {boolean} checkTerminated + * @param {number} beforeInitError + * @param {number} afterTermError + * @return {boolean} */ checkState( - checkTerminated: boolean, beforeInitError: number, + checkTerminated: boolean, + beforeInitError: number, afterTermError?: number) { if (_self.isNotInitialized()) { _self.throwSCORMError(beforeInitError); @@ -262,13 +268,15 @@ export default class BaseAPI { /** * Logging for all SCORM actions * - * @param functionName - * @param CMIElement - * @param logMessage - * @param messageLevel + * @param {string} functionName + * @param {string} CMIElement + * @param {string} logMessage + * @param {number}messageLevel */ apiLog( - functionName: String, CMIElement: String, logMessage: String, + functionName: String, + CMIElement: String, + logMessage: String, messageLevel: number) { logMessage = _self.formatMessage(functionName, CMIElement, logMessage); @@ -287,21 +295,12 @@ export default class BaseAPI { } } - /** - * Clears the last SCORM error code on success - */ - clearSCORMError(success: String) { - if (success !== api_constants.SCORM_FALSE) { - _self.lastErrorCode = 0; - } - } - /** * Formats the SCORM messages for easy reading * - * @param functionName - * @param CMIElement - * @param message + * @param {string} functionName + * @param {string} CMIElement + * @param {string} message * @return {string} */ formatMessage(functionName: String, CMIElement: String, message: String) { @@ -340,8 +339,9 @@ export default class BaseAPI { /** * Checks to see if {str} contains {tester} * - * @param str String to check against - * @param tester String to check for + * @param {string} str String to check against + * @param {string} tester String to check for + * @return {boolean} */ stringContains(str: String, tester: String) { return str.indexOf(tester) > -1; @@ -350,6 +350,10 @@ export default class BaseAPI { /** * Returns the message that corresponds to errorNumber * APIs that inherit BaseAPI should override this function + * + * @param {(string|number)} _errorNumber + * @param {boolean} _detail + * @return {string} */ getLmsErrorMessageDetails(_errorNumber, _detail) { return 'No error'; @@ -358,6 +362,9 @@ export default class BaseAPI { /** * Gets the value for the specific element. * APIs that inherit BaseAPI should override this function + * + * @param {string} _CMIElement + * @return {string} */ getCMIValue(_CMIElement) { return ''; @@ -366,11 +373,24 @@ export default class BaseAPI { /** * Sets the value for the specific element. * APIs that inherit BaseAPI should override this function + * + * @param {string} _CMIElement + * @param {any} _value */ setCMIValue(_CMIElement, _value) { - return ''; + // just a stub method } + /** + * Shared API method to set a valid for a given element. + * + * @param {string} methodName + * @param {boolean} scorm2004 + * @param {string} CMIElement + * @param {*} value + * @return {string} + * @private + */ _commonSetCMIValue( methodName: String, scorm2004: boolean, CMIElement, value) { if (!CMIElement || CMIElement === '') { @@ -393,7 +413,7 @@ export default class BaseAPI { if (scorm2004 && (attribute.substr(0, 8) === '{target=') && (typeof refObject._isTargetValid == 'function')) { _self.throwSCORMError(_self.#error_codes.READ_ONLY_ELEMENT); - } else if (!refObject.hasOwnProperty(attribute)) { + } else if (!{}.hasOwnProperty.call(refObject, attribute)) { _self.throwSCORMError(invalidErrorCode, invalidErrorMessage); } else { if (_self.stringContains(CMIElement, '.correct_responses')) { @@ -412,7 +432,7 @@ export default class BaseAPI { break; } - if (refObject.prototype === CMIArray) { + if (refObject instanceof CMIArray) { const index = parseInt(structure[i + 1], 10); // SCO is trying to set an item on an array @@ -448,24 +468,34 @@ export default class BaseAPI { return returnValue; } + /** + * Abstract method for validating that a response is correct. + * + * @param {string} _CMIElement + * @param {*} _value + */ validateCorrectResponse(_CMIElement, _value) { - return false; + // just a stub method } /** * Gets or builds a new child element to add to the array. - * APIs that inherit BaseAPI should override this method + * APIs that inherit BaseAPI should override this method. + * + * @param {string} _CMIElement - unused + * @param {*} _value - unused + * @return {*} */ - getChildElement(_CMIElement) { + getChildElement(_CMIElement, _value) { return null; } /** * Gets a value from the CMI Object * - * @param methodName - * @param scorm2004 - * @param CMIElement + * @param {string} methodName + * @param {boolean} scorm2004 + * @param {string} CMIElement * @return {*} */ _commonGetCMIValue(methodName: String, scorm2004: boolean, CMIElement) { @@ -482,7 +512,7 @@ export default class BaseAPI { if (!scorm2004) { if (i === structure.length - 1) { - if (!refObject.hasOwnProperty(attribute)) { + if (!{}.hasOwnProperty.call(refObject, attribute)) { _self.throwSCORMError(101, 'getCMIValue did not find a value for: ' + CMIElement); } @@ -493,7 +523,7 @@ export default class BaseAPI { const target = String(attribute). substr(8, String(attribute).length - 9); return refObject._isTargetValid(target); - } else if (!refObject.hasOwnProperty(attribute)) { + } else if (!{}.hasOwnProperty.call(refObject, attribute)) { _self.throwSCORMError(401, 'The data model element passed to GetValue (' + CMIElement + ') is not a valid SCORM data model element.'); @@ -520,6 +550,8 @@ export default class BaseAPI { /** * Returns true if the API's current state is STATE_INITIALIZED + * + * @return {boolean} */ isInitialized() { return _self.currentState === api_constants.STATE_INITIALIZED; @@ -527,6 +559,8 @@ export default class BaseAPI { /** * Returns true if the API's current state is STATE_NOT_INITIALIZED + * + * @return {boolean} */ isNotInitialized() { return _self.currentState === api_constants.STATE_NOT_INITIALIZED; @@ -534,6 +568,8 @@ export default class BaseAPI { /** * Returns true if the API's current state is STATE_TERMINATED + * + * @return {boolean} */ isTerminated() { return _self.currentState === api_constants.STATE_TERMINATED; @@ -542,8 +578,8 @@ export default class BaseAPI { /** * Provides a mechanism for attaching to a specific SCORM event * - * @param listenerName - * @param callback + * @param {string} listenerName + * @param {function} callback */ on(listenerName: String, callback: function) { if (!callback) return; @@ -571,9 +607,9 @@ export default class BaseAPI { /** * Processes any 'on' listeners that have been created * - * @param functionName - * @param CMIElement - * @param value + * @param {string} functionName + * @param {string} CMIElement + * @param {*} value */ processListeners(functionName: String, CMIElement: String, value: any) { for (let i = 0; i < _self.listenerArray.length; i++) { @@ -591,8 +627,8 @@ export default class BaseAPI { /** * Throws a SCORM error * - * @param errorNumber - * @param message + * @param {number} errorNumber + * @param {string} message */ throwSCORMError(errorNumber: number, message: String) { if (!message) { @@ -605,20 +641,34 @@ export default class BaseAPI { _self.lastErrorCode = String(errorNumber); } + /** + * Clears the last SCORM error code on success. + * + * @param {string} success + */ + clearSCORMError(success: String) { + if (success !== api_constants.SCORM_FALSE) { + _self.lastErrorCode = 0; + } + } + /** * Loads CMI data from a JSON object. + * + * @param {object} json + * @param {string} CMIElement */ loadFromJSON(json, CMIElement) { if (!_self.isNotInitialized()) { console.error( - 'loadFromJSON can only be called before the call to LMSInitialize.'); + 'loadFromJSON can only be called before the call to lmsInitialize.'); return; } CMIElement = CMIElement || 'cmi'; for (const key in json) { - if (json.hasOwnProperty(key) && json[key]) { + if ({}.hasOwnProperty.call(json, key) && json[key]) { const currentCMIElement = CMIElement + '.' + key; const value = json[key]; @@ -636,6 +686,11 @@ export default class BaseAPI { } } + /** + * Render the CMI object to JSON for sending to an LMS. + * + * @return {string} + */ renderCMIToJSON() { const cmi = _self.cmi; // Do we want/need to return fields that have no set value? @@ -646,8 +701,8 @@ export default class BaseAPI { /** * Check if the value matches the proper format. If not, throw proper error code. * - * @param value - * @param regexPattern + * @param {string} value + * @param {string} regexPattern * @return {boolean} */ checkValidFormat(value: String, regexPattern: String) { @@ -662,8 +717,8 @@ export default class BaseAPI { /** * Check if the value matches the proper range. If not, throw proper error code. * - * @param value - * @param rangePattern + * @param {*} value + * @param {string} rangePattern * @return {boolean} */ checkValidRange(value: any, rangePattern: String) { @@ -686,7 +741,7 @@ export default class BaseAPI { /** * Throws a SCORM error * - * @param when the number of milliseconds to wait before committing + * @param {number} when - the number of milliseconds to wait before committing */ scheduleCommit(when: number) { _self.#timeout = new ScheduledCommit(this, when); @@ -703,16 +758,27 @@ export default class BaseAPI { } } +/** + * Private class that wraps a timeout call to the commit() function + */ class ScheduledCommit { #API; #cancelled: false; #timeout; + /** + * Constructor for ScheduledCommit + * @param {BaseAPI} API + * @param {number} when + */ constructor(API: any, when: number) { _self.#API = API; _self.#timeout = setTimeout(_self.#wrapper, when); } + /** + * Cancel any currently scheduled commit + */ cancel() { _self.#cancelled = true; if (_self.#timeout) { @@ -720,6 +786,9 @@ class ScheduledCommit { } } + /** + * Wrap the API commit call to check if the call has already been cancelled + */ #wrapper = () => { if (!_self.#cancelled) { _self.#API.commit(); diff --git a/src/Scorm12API.js b/src/Scorm12API.js index 126a9e5..01554ac 100644 --- a/src/Scorm12API.js +++ b/src/Scorm12API.js @@ -13,134 +13,170 @@ 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 + */ export default class Scorm12API extends BaseAPI { + /** + * Constructor for SCORM 1.2 API + */ constructor() { super(scorm12_error_codes); + _self = this; - this.cmi = new CMI(this); + _self.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; } /** - * @return {string} bool - */ - LMSInitialize() { - return this.initialize('LMSInitialize', 'LMS was already initialized!', + * lmsInitialize function from SCORM 1.2 Spec + * + * @return {string} bool + */ + lmsInitialize() { + return _self.initialize('LMSInitialize', 'LMS was already initialized!', 'LMS is already finished!'); } /** - * @return {string} bool - */ - LMSFinish() { - return this.terminate('LMSFinish', false); + * LMSFinish function from SCORM 1.2 Spec + * + * @return {string} bool + */ + lmsFinish() { + return _self.terminate('LMSFinish', false); } /** - * @param CMIElement - * @return {string} - */ - LMSGetValue(CMIElement) { - return this.getValue('LMSGetValue', false, CMIElement); + * LMSGetValue function from SCORM 1.2 Spec + * + * @param {string} CMIElement + * @return {string} + */ + lmsGetValue(CMIElement) { + return _self.getValue('LMSGetValue', false, CMIElement); } /** - * @param CMIElement - * @param value - * @return {string} - */ - LMSSetValue(CMIElement, value) { - return this.setValue('LMSSetValue', false, CMIElement, value); + * LMSSetValue function from SCORM 1.2 Spec + * + * @param {string} CMIElement + * @param {*} value + * @return {string} + */ + lmsSetValue(CMIElement, value) { + return _self.setValue('LMSSetValue', false, CMIElement, value); } /** - * Orders LMS to store all content parameters - * - * @return {string} bool - */ - LMSCommit() { - return this.commit('LMSCommit', false); + * LMSCommit function from SCORM 1.2 Spec + * + * @return {string} bool + */ + lmsCommit() { + return _self.commit('LMSCommit', false); } /** - * Returns last error code - * - * @return {string} - */ - LMSGetLastError() { - return this.getLastError('LMSGetLastError'); + * LMSGetLastError function from SCORM 1.2 Spec + * + * @return {string} + */ + lmsGetLastError() { + return _self.getLastError('LMSGetLastError'); } /** - * Returns the errorNumber error description - * - * @param CMIErrorCode - * @return {string} - */ - LMSGetErrorString(CMIErrorCode) { - return this.getErrorString('LMSGetErrorString', CMIErrorCode); + * LMSGetErrorString function from SCORM 1.2 Spec + * + * @param {string} CMIErrorCode + * @return {string} + */ + lmsGetErrorString(CMIErrorCode) { + return _self.getErrorString('LMSGetErrorString', CMIErrorCode); } /** - * Returns a comprehensive description of the errorNumber error. - * - * @param CMIErrorCode - * @return {string} - */ - LMSGetDiagnostic(CMIErrorCode) { - return this.getDiagnostic('LMSGetDiagnostic', CMIErrorCode); + * LMSGetDiagnostic function from SCORM 1.2 Spec + * + * @param {string} CMIErrorCode + * @return {string} + */ + lmsGetDiagnostic(CMIErrorCode) { + return _self.getDiagnostic('LMSGetDiagnostic', CMIErrorCode); } /** - * Sets a value on the CMI Object - * - * @param CMIElement - * @param value - * @return {string} - */ + * Sets a value on the CMI Object + * + * @param {string} CMIElement + * @param {*} value + */ setCMIValue(CMIElement, value) { - this._commonSetCMIValue('LMSSetValue', false, CMIElement, value); + _self._commonSetCMIValue('LMSSetValue', false, CMIElement, value); } /** - * Gets a value from the CMI Object - * - * @param CMIElement - * @return {*} - */ + * Gets a value from the CMI Object + * + * @param {string} CMIElement + * @return {*} + */ getCMIValue(CMIElement) { - return this._commonGetCMIValue('getCMIValue', false, CMIElement); + return _self._commonGetCMIValue('getCMIValue', false, CMIElement); } /** * Gets or builds a new child element to add to the array. * - * @param CMIElement - * @param value + * @param {string} CMIElement + * @param {*} value + * @return {object} */ getChildElement(CMIElement, value) { let newChild; - if (this.stringContains(CMIElement, 'cmi.objectives')) { + if (_self.stringContains(CMIElement, 'cmi.objectives')) { newChild = new CMIObjectivesObject(this); - } else if (this.stringContains(CMIElement, '.correct_responses')) { + } else if (_self.stringContains(CMIElement, '.correct_responses')) { newChild = new CMIInteractionsCorrectResponsesObject(this); - } else if (this.stringContains(CMIElement, '.objectives')) { + } else if (_self.stringContains(CMIElement, '.objectives')) { newChild = new CMIInteractionsObjectivesObject(this); - } else if (this.stringContains(CMIElement, 'cmi.interactions')) { + } else if (_self.stringContains(CMIElement, 'cmi.interactions')) { newChild = new CMIInteractionsObject(this); } return newChild; } + /** + * Validates Correct Response values + * + * @param {string} CMIElement + * @param {*} value + * @return {boolean} + */ validateCorrectResponse(CMIElement, value) { return true; } /** - * Returns the message that corresponds to errorNumber. - */ + * Returns the message that corresponds to errorNumber. + * + * @param {*} errorNumber + * @param {boolean }detail + * @return {string} + */ getLmsErrorMessageDetails(errorNumber, detail) { let basicMessage = 'No Error'; let detailMessage = 'No Error'; @@ -156,25 +192,26 @@ export default class Scorm12API extends BaseAPI { } /** - * Adds the current session time to the existing total time. - */ + * Adds the current session time to the existing total time. + * + * @return {string} + */ getCurrentTotalTime() { const timeRegex = new RegExp(scorm12_regex.CMITime); - const totalTime = this.cmi.core.total_time; - const sessionTime = this.cmi.core.session_time; + const totalTime = _self.cmi.core.total_time; + const sessionTime = _self.cmi.core.session_time; - const totalSeconds = Utilities.getTimeAsSeconds(totalTime, timeRegex); - const sessionSeconds = Utilities.getTimeAsSeconds(sessionTime, timeRegex); - - return Utilities.getSecondsAsHHMMSS(totalSeconds + sessionSeconds); + return Utilities.addHHMMSSTimeStrings(totalTime, sessionTime, timeRegex); } /** - * Replace the whole API with another - */ + * Replace the whole API with another + * + * @param {Scorm12API} newAPI + */ replaceWithAnotherScormAPI(newAPI) { // Data Model - this.cmi = newAPI.cmi; + _self.cmi = newAPI.cmi; } } diff --git a/src/Scorm2004API.js b/src/Scorm2004API.js index 94920fb..f6c72d7 100644 --- a/src/Scorm2004API.js +++ b/src/Scorm2004API.js @@ -24,7 +24,7 @@ let _self; * API class for SCORM 2004 */ class Scorm2004API extends BaseAPI { - version: '1.0'; + #version: '1.0'; /** * Constructor for SCORM 2004 API @@ -36,28 +36,36 @@ class Scorm2004API extends BaseAPI { _self.cmi = new CMI(_self); _self.adl = new ADL(_self); - // Rename functions to match 2004 Spec - _self.Initialize = _self.LMSInitialize; - _self.Terminate = _self.LMSTerminate; - _self.GetValue = _self.LMSGetValue; - _self.SetValue = _self.LMSSetValue; - _self.Commit = _self.LMSCommit; - _self.GetLastError = _self.LMSGetLastError; - _self.GetErrorString = _self.LMSGetErrorString; - _self.GetDiagnostic = _self.LMSGetDiagnostic; + // Rename functions to match 2004 Spec and expose to modules + _self.Initialize = _self.lmsInitialize; + _self.Terminate = _self.lmsTerminate; + _self.GetValue = _self.lmsGetValue; + _self.SetValue = _self.lmsSetValue; + _self.Commit = _self.lmsCommit; + _self.GetLastError = _self.lmsGetLastError; + _self.GetErrorString = _self.lmsGetErrorString; + _self.GetDiagnostic = _self.lmsGetDiagnostic; + } + + /** + * Getter for #version + * @return {string} + */ + get version() { + return this.#version; } /** * @return {string} bool */ - LMSInitialize() { + lmsInitialize() { return _self.initialize('Initialize'); } /** * @return {string} bool */ - LMSTerminate() { + lmsTerminate() { return _self.terminate('Terminate', true); } @@ -65,7 +73,7 @@ class Scorm2004API extends BaseAPI { * @param {string} CMIElement * @return {string} */ - LMSGetValue(CMIElement) { + lmsGetValue(CMIElement) { return _self.getValue('GetValue', true, CMIElement); } @@ -74,7 +82,7 @@ class Scorm2004API extends BaseAPI { * @param {any} value * @return {string} */ - LMSSetValue(CMIElement, value) { + lmsSetValue(CMIElement, value) { return _self.setValue('SetValue', true, CMIElement, value); } @@ -83,7 +91,7 @@ class Scorm2004API extends BaseAPI { * * @return {string} bool */ - LMSCommit() { + lmsCommit() { return _self.commit('Commit'); } @@ -92,27 +100,27 @@ class Scorm2004API extends BaseAPI { * * @return {string} */ - LMSGetLastError() { + lmsGetLastError() { return _self.getLastError('GetLastError'); } /** * Returns the errorNumber error description * - * @param CMIErrorCode + * @param {(string|number)} CMIErrorCode * @return {string} */ - LMSGetErrorString(CMIErrorCode) { + lmsGetErrorString(CMIErrorCode) { return _self.getErrorString('GetErrorString', CMIErrorCode); } /** * Returns a comprehensive description of the errorNumber error. * - * @param CMIErrorCode + * @param {(string|number)} CMIErrorCode * @return {string} */ - LMSGetDiagnostic(CMIErrorCode) { + lmsGetDiagnostic(CMIErrorCode) { return _self.getDiagnostic('GetDiagnostic', CMIErrorCode); } @@ -160,7 +168,7 @@ class Scorm2004API extends BaseAPI { const response_type = correct_responses[interaction_type]; let nodes = []; if (response_type.delimiter !== '') { - nodes = value.split(response_type.delimiter); + nodes = String(value).split(response_type.delimiter); } else { nodes[0] = value; } @@ -191,7 +199,7 @@ class Scorm2004API extends BaseAPI { /** * Validate correct response. * @param {string} CMIElement - * @param {any} value + * @param {*} value */ validateCorrectResponse(CMIElement, value) { const parts = CMIElement.split('.'); @@ -215,7 +223,7 @@ class Scorm2004API extends BaseAPI { response_type.limit) { let nodes = []; if (response_type.delimiter !== '') { - nodes = value.split(response_type.delimiter); + nodes = String(value).split(response_type.delimiter); } else { nodes[0] = value; } @@ -258,8 +266,8 @@ class Scorm2004API extends BaseAPI { /** * Returns the message that corresponds to errorNumber. * - * @param {string,number} errorNumber - * @param {string} detail + * @param {(string|number)} errorNumber + * @param {boolean} detail * @return {string} */ getLmsErrorMessageDetails(errorNumber, detail) { @@ -276,6 +284,13 @@ class Scorm2004API extends BaseAPI { return detail ? detailMessage : basicMessage; } + /** + * Check to see if a correct_response value has been duplicated + * @param {CMIArray} correct_response + * @param {number} current_index + * @param {*} value + * @return {boolean} + */ #checkDuplicatedPattern = (correct_response, current_index, value) => { let found = false; const count = correct_response._count; @@ -287,6 +302,12 @@ class Scorm2004API extends BaseAPI { return found; }; + /** + * Checks for a valid correct_response value + * @param {string} interaction_type + * @param {Array} nodes + * @param {*} value + */ #checkCorrectResponseValue = (interaction_type, nodes, value) => { const response = correct_responses[interaction_type]; const formatRegex = new RegExp(response.format); @@ -334,6 +355,11 @@ class Scorm2004API extends BaseAPI { } }; + /** + * Remove prefixes from correct_response + * @param {string} node + * @return {*} + */ #removeCorrectResponsePrefixes = (node) => { let seenOrder = false; let seenCase = false; @@ -404,11 +430,7 @@ class Scorm2004API extends BaseAPI { const totalTime = _self.cmi.total_time; const sessionTime = _self.cmi.session_time; - const durationRegex = scorm2004_regex.CMITimespan; - const totalSeconds = Util.getDurationAsSeconds(totalTime, durationRegex); - const sessionSeconds = Util.getDurationAsSeconds(sessionTime, - durationRegex); - - return Util.getSecondsAsISODuration(totalSeconds + sessionSeconds); + return Util.addTwoDurations(totalTime, sessionTime, + scorm2004_regex.CMITimespan); } } diff --git a/src/cmi/aicc_cmi.js b/src/cmi/aicc_cmi.js index 74e045c..726dfda 100644 --- a/src/cmi/aicc_cmi.js +++ b/src/cmi/aicc_cmi.js @@ -39,17 +39,18 @@ class CMIEvaluation extends BaseCMI { */ constructor(API) { super(API); - } - comments = new class extends CMIArray { - /** - * Constructor for AICC Evaluation Comments object - * @param {AICC} API - */ - constructor(API) { - super(API, constants.comments_children, 402); - } - }; + 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); + } } /** @@ -62,6 +63,16 @@ 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); } #tries_during_lesson = ''; @@ -84,34 +95,20 @@ class AICCCMIStudentData extends Scorm12CMI.CMIStudentData { this.#tries_during_lesson = tries_during_lesson : throwReadOnlyError(); } - - tries = new class extends CMIArray { - /** - * Constructor for inline Tries Array class - * @param {AICC} API - */ - constructor(API) { - super(API, aicc_constants.tries_children); - } - }; } -let _self; - /** * Class for AICC Tries */ export class CMITriesObject extends BaseCMI { - #API; - /** * Constructor for AICC Tries object * @param {AICC} API */ constructor(API) { super(API); - this.#API = API; - _self = this; + + this.score = new CMIScore(API); } #status = ''; @@ -152,8 +149,6 @@ export class CMITriesObject extends BaseCMI { this.#time = time; } } - - score = new CMIScore(_self.#API); } /** @@ -246,7 +241,9 @@ export class NAV extends BaseCMI { * @return {string} */ get event() { - return (!this.jsonString) ? this.API.throwSCORMError(404) : this.#event; + return (!this.jsonString) ? + this.API.throwSCORMError(scorm12_error_codes.WRITE_ONLY_ELEMENT) : + this.#event; } /** diff --git a/src/cmi/common.js b/src/cmi/common.js index 5214ca7..11ee0ee 100644 --- a/src/cmi/common.js +++ b/src/cmi/common.js @@ -1,78 +1,154 @@ +// @flow import {scorm12_constants} from '../constants/api_constants'; import {scorm12_error_codes} from '../constants/error_codes'; +/** + * Base class for API cmi objects + */ export class BaseCMI { - jsonString = false; - API; + jsonString = false; + API; - constructor(API: any) { - this.API = API; - } + /** + * Constructor for base cmi + * @param {BaseAPI} API + */ + constructor(API: any) { + this.API = API; + } } +/** + * Base class for cmi *.score objects + */ export class CMIScore extends BaseCMI { + /** + * Constructor for *.score + * @param {BaseAPI} API + * @param {string} score_children + * @param {string} score_range + * @param {number} invalidErrorCode + */ constructor(API, score_children?, score_range?, invalidErrorCode) { super(API); - this.#_children = score_children? score_children : scorm12_constants.score_children; - this.#_score_range = score_range? score_range : false; - this.#_invalid_error_code = invalidErrorCode ? invalidErrorCode : scorm12_error_codes.INVALID_SET_VALUE; + this.#_children = score_children ? + score_children : + scorm12_constants.score_children; + this.#_score_range = score_range ? score_range : false; + this.#_invalid_error_code = invalidErrorCode ? + invalidErrorCode : + scorm12_error_codes.INVALID_SET_VALUE; } - #_children; - #_score_range; - #_invalid_error_code; - #raw = ''; - #min = ''; - #max = '100'; + #_children; + #_score_range; + #_invalid_error_code; + #raw = ''; + #min = ''; + #max = '100'; - get _children() { - return this.#_children; - } - set _children(_children) { - this.API.throwSCORMError(this.#_invalid_error_code); - } + /** + * Getter for _children + * @return {string} + * @private + */ + get _children() { + return this.#_children; + } - get raw() { - return this.#raw; - } - set raw(raw) { - if (this.API.checkValidFormat(raw, scorm12_constants.CMIDecimal) && - (!this.#_score_range || this.API.checkValidRange(raw, this.#_score_range))) { - this.#raw = raw; - } - } + /** + * Setter for _children. Just throws an error. + * @param {string} _children + * @private + */ + set _children(_children) { + this.API.throwSCORMError(this.#_invalid_error_code); + } - get min() { - return this.#min; - } - set min(min) { - if (this.API.checkValidFormat(min, scorm12_constants.CMIDecimal) && - (!this.#_score_range || this.API.checkValidRange(min, this.#_score_range))) { - this.#min = min; - } - } + /** + * Getter for #raw + * @return {string} + */ + get raw() { + return this.#raw; + } - get max() { - return this.#max; - } - set max(max) { - if (this.API.checkValidFormat(max, scorm12_constants.CMIDecimal) && - (!this.#_score_range || this.API.checkValidRange(max, this.#_score_range))) { - this.#max = max; - } + /** + * Setter for #raw + * @param {string} raw + */ + set raw(raw) { + if (this.API.checkValidFormat(raw, scorm12_constants.CMIDecimal) && + (!this.#_score_range || + this.API.checkValidRange(raw, this.#_score_range))) { + this.#raw = raw; } + } - toJSON = () => { - return { - 'raw': this.raw, - 'min': this.min, - 'max': this.max, - }; + /** + * Getter for #min + * @return {string} + */ + get min() { + return this.#min; + } + + /** + * Setter for #min + * @param {string} min + */ + set min(min) { + if (this.API.checkValidFormat(min, scorm12_constants.CMIDecimal) && + (!this.#_score_range || + this.API.checkValidRange(min, this.#_score_range))) { + this.#min = min; } + } + + /** + * Getter for #max + * @return {string} + */ + get max() { + return this.#max; + } + + /** + * Setter for #max + * @param {string} max + */ + set max(max) { + if (this.API.checkValidFormat(max, scorm12_constants.CMIDecimal) && + (!this.#_score_range || + this.API.checkValidRange(max, this.#_score_range))) { + this.#max = max; + } + } + + /** + * toJSON for *.score + * @return {{min: string, max: string, raw: string}} + */ + toJSON() { + return { + 'raw': this.raw, + 'min': this.min, + 'max': this.max, + }; + } } +/** + * Base class for cmi *.n objects + */ export class CMIArray extends BaseCMI { + /** + * Constructor cmi *.n arrays + * @param {BaseAPI} API + * @param {string} children + * @param {number} errorCode + */ constructor({API, children, errorCode}) { super(API); this.#_children = children; @@ -80,30 +156,56 @@ export class CMIArray extends BaseCMI { this.childArray = []; } - #errorCode; - #_children; + #errorCode; + #_children; - get _children() { - return this.#_children; - } - set _children(_children) { - this.API.throwSCORMError(this.#errorCode); - } + /** + * Getter for _children + * @return {*} + * @private + */ + get _children() { + return this.#_children; + } - get _count() { - return this.childArray.length; - } - set _count(_count) { - this.API.throwSCORMError(this.#errorCode); - } + /** + * Setter for _children. Just throws an error. + * @param {string} _children + * @private + */ + set _children(_children) { + this.API.throwSCORMError(this.#errorCode); + } - toJSON = () => { - this.jsonString = true; - const result = {}; - for (let i = 0; i < this.childArray.length; i++) { - result[i + ''] = this.childArray[i]; - } - delete this.jsonString; - return result; + /** + * Getter for _count + * @return {number} + * @private + */ + get _count() { + return this.childArray.length; + } + + /** + * Setter for _count. Just throws an error. + * @param {number} _count + * @private + */ + set _count(_count) { + this.API.throwSCORMError(this.#errorCode); + } + + /** + * toJSON for *.n arrays + * @return {object} + */ + toJSON() { + this.jsonString = true; + const result = {}; + for (let i = 0; i < this.childArray.length; i++) { + result[i + ''] = this.childArray[i]; } + delete this.jsonString; + return result; + } } diff --git a/src/cmi/scorm12_cmi.js b/src/cmi/scorm12_cmi.js index a3f05b8..f8ef781 100644 --- a/src/cmi/scorm12_cmi.js +++ b/src/cmi/scorm12_cmi.js @@ -1,3 +1,4 @@ +// @flow import {BaseCMI, CMIArray, CMIScore} from './common'; import {scorm12_constants} from '../constants/api_constants'; import {scorm12_error_codes} from '../constants/error_codes'; @@ -6,260 +7,479 @@ import {scorm12_regex} from '../regex'; const constants = scorm12_constants; const regex = scorm12_regex; +/** + * Helper method for throwing Read Only error + * @param {Scorm12API} API + */ function throwReadOnlyError(API) { API.throwSCORMError(scorm12_error_codes.READ_ONLY_ELEMENT); } +/** + * Helper method for throwing Write Only error + * @param {Scorm12API} API + */ function throwWriteOnlyError(API) { API.throwSCORMError(scorm12_error_codes.WRITE_ONLY_ELEMENT); } +/** + * Helper method for throwing Invalid Set error + * @param {Scorm12API} API + */ function throwInvalidValueError(API) { API.throwSCORMError(scorm12_error_codes.INVALID_SET_VALUE); } +/** + * Class representing the cmi object for SCORM 1.2 + */ export class CMI extends BaseCMI { - #_children = ''; - #_version = '3.4'; - #suspend_data = ''; - #launch_data = ''; - #comments = ''; - #comments_from_lms = ''; + #_children = ''; + #_version = '3.4'; + #suspend_data = ''; + #launch_data = ''; + #comments = ''; + #comments_from_lms = ''; - student_data = null; + student_data = null; - constructor(API, cmi_children, student_data) { - super(API); + /** + * Constructor for the SCORM 1.2 cmi object + * @param {Scorm12API} API + * @param {string} cmi_children + * @param {string} student_data + */ + constructor(API, cmi_children, student_data) { + super(API); - this.#_children = cmi_children ? cmi_children : constants.cmi_children; - this.core = new CMICore(API); - this.objectives = new CMIObjectives(API); - this.student_data = student_data ? - student_data : - new CMIStudentData(API); - this.student_preference = new CMIStudentPreference(API); - this.interactions = new CMIInteractions(API); - } + this.#_children = cmi_children ? cmi_children : constants.cmi_children; + this.core = new CMICore(API); + this.objectives = new CMIObjectives(API); + this.student_data = student_data ? + student_data : + new CMIStudentData(API); + this.student_preference = new CMIStudentPreference(API); + this.interactions = new CMIInteractions(API); + } - toJSON = () => { - this.jsonString = true; - const result = { - 'suspend_data': this.suspend_data, - 'launch_data': this.launch_data, - 'comments': this.comments, - 'comments_from_lms': this.comments_from_lms, - 'core': this.core, - 'objectives': this.objectives, - 'student_data': this.student_data, - 'student_preference': this.student_preference, - 'interactions': this.interactions, - }; - delete this.jsonString; - return result; + /** + * toJSON for cmi + * + * @return { + * { + * suspend_data: string, + * launch_data: string, + * comments: string, + * comments_from_lms: string, + * core: CMICore, + * objectives: CMIObjectives, + * student_data: CMIStudentData, + * student_preference: CMIStudentPreference, + * interactions: CMIInteractions + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'suspend_data': this.suspend_data, + 'launch_data': this.launch_data, + 'comments': this.comments, + 'comments_from_lms': this.comments_from_lms, + 'core': this.core, + 'objectives': this.objectives, + 'student_data': this.student_data, + 'student_preference': this.student_preference, + 'interactions': this.interactions, }; + delete this.jsonString; + return result; + } - get _version() { - return this.#_version; - } + /** + * Getter for #_version + * @return {string} + * @private + */ + get _version() { + return this.#_version; + } - set _version(_version) { - throwInvalidValueError(this.API); - } + /** + * Setter for #_version. Just throws an error. + * @param {string} _version + * @private + */ + set _version(_version) { + throwInvalidValueError(this.API); + } - get _children() { - return this.#_children; - } + /** + * Getter for #_children + * @return {string} + * @private + */ + get _children() { + return this.#_children; + } - set _children(_children) { - throwInvalidValueError(this.API); - } + /** + * Setter for #_version. Just throws an error. + * @param {string} _children + * @private + */ + set _children(_children) { + throwInvalidValueError(this.API); + } - get suspend_data() { - return this.#suspend_data; - } - set suspend_data(suspend_data) { - if (this.API.checkValidFormat(suspend_data, regex.CMIString4096)) { - this.#suspend_data = suspend_data; - } - } + /** + * Getter for #suspend_data + * @return {string} + */ + get suspend_data() { + return this.#suspend_data; + } - get launch_data() { - return this.#launch_data; + /** + * Setter for #suspend_data + * @param {string} suspend_data + */ + set suspend_data(suspend_data) { + if (this.API.checkValidFormat(suspend_data, regex.CMIString4096)) { + this.#suspend_data = suspend_data; } + } - set launch_data(launch_data) { - this.API.isNotInitialized() ? - this.#launch_data = launch_data : - throwReadOnlyError(this.API); - } + /** + * Getter for #launch_data + * @return {string} + */ + get launch_data() { + return this.#launch_data; + } - get comments() { - return this.#comments; - } - set comments(comments) { - if (this.API.checkValidFormat(comments, regex.CMIString4096)) { - this.#comments = comments; - } - } + /** + * Setter for #launch_data. Can only be called before API initialization. + * @param {string} launch_data + */ + set launch_data(launch_data) { + this.API.isNotInitialized() ? + this.#launch_data = launch_data : + throwReadOnlyError(this.API); + } - get comments_from_lms() { - return this.#comments_from_lms; - } + /** + * Getter for #comments + * @return {string} + */ + get comments() { + return this.#comments; + } - set comments_from_lms(comments_from_lms) { - this.API.isNotInitialized() ? - this.#comments_from_lms = comments_from_lms : - throwReadOnlyError(this.API); + /** + * Setter for #comments + * @param {string} comments + */ + set comments(comments) { + if (this.API.checkValidFormat(comments, regex.CMIString4096)) { + this.#comments = comments; } + } + + /** + * Getter for #comments_from_lms + * @return {string} + */ + get comments_from_lms() { + return this.#comments_from_lms; + } + + /** + * Setter for #comments_from_lms. Can only be called before API initialization. + * @param {string} comments_from_lms + */ + set comments_from_lms(comments_from_lms) { + this.API.isNotInitialized() ? + this.#comments_from_lms = comments_from_lms : + throwReadOnlyError(this.API); + } } +/** + * Class representing the cmi.core object + */ class CMICore extends BaseCMI { + /** + * Constructor for cmi.core + * @param {Scorm12API} API + */ constructor(API) { super(API); - this.score = new CMIScore(API, constants.score_children, + this.score = new Scorm12CMIScore(API, constants.score_children, regex.score_range); } - #_children = constants.core_children; - #student_id = ''; - #student_name = ''; - #lesson_location = ''; - #credit = ''; - #lesson_status = ''; - #entry = ''; - #total_time = ''; - #lesson_mode = 'normal'; - #exit = ''; - #session_time = '00:00:00'; + #_children = constants.core_children; + #student_id = ''; + #student_name = ''; + #lesson_location = ''; + #credit = ''; + #lesson_status = ''; + #entry = ''; + #total_time = ''; + #lesson_mode = 'normal'; + #exit = ''; + #session_time = '00:00:00'; - get _children() { - return this.#_children; - } + /** + * Getter for #_children + * @return {string} + * @private + */ + get _children() { + return this.#_children; + } - set _children(_children) { - throwInvalidValueError(this.API); - } + /** + * Setter for #_children. Just throws an error. + * @param {string} _children + * @private + */ + set _children(_children) { + throwInvalidValueError(this.API); + } - get student_id() { - return this.#student_id; - } + /** + * Getter for #student_id + * @return {string} + */ + get student_id() { + return this.#student_id; + } - set student_id(student_id) { - this.API.isNotInitialized() ? - this.#student_id = student_id : - throwReadOnlyError(this.API); - } + /** + * Setter for #student_id. Can only be called before API initialization. + * @param {string} student_id + */ + set student_id(student_id) { + this.API.isNotInitialized() ? + this.#student_id = student_id : + throwReadOnlyError(this.API); + } - get student_name() { - return this.#student_name; - } + /** + * Getter for #student_name + * @return {string} + */ + get student_name() { + return this.#student_name; + } - set student_name(student_name) { - this.API.isNotInitialized() ? - this.#student_name = student_name : - throwReadOnlyError(this.API); - } + /** + * Setter for #student_name. Can only be called before API initialization. + * @param {string} student_name + */ + set student_name(student_name) { + this.API.isNotInitialized() ? + this.#student_name = student_name : + throwReadOnlyError(this.API); + } - get lesson_location() { - return this.#lesson_location; - } - set lesson_location(lesson_location) { - if (this.API.checkValidFormat(lesson_location, regex.CMIString256)) { - this.#lesson_location = lesson_location; - } - } + /** + * Getter for #lesson_location + * @return {string} + */ + get lesson_location() { + return this.#lesson_location; + } - get credit() { - return this.#credit; + /** + * Setter for #lesson_location + * @param {string} lesson_location + */ + set lesson_location(lesson_location) { + if (this.API.checkValidFormat(lesson_location, regex.CMIString256)) { + this.#lesson_location = lesson_location; } + } - set credit(credit) { - this.API.isNotInitialized() ? - this.#credit = credit : - throwReadOnlyError(this.API); - } + /** + * Getter for #credit + * @return {string} + */ + get credit() { + return this.#credit; + } - get lesson_status() { - return this.#lesson_status; - } - set lesson_status(lesson_status) { - if (this.API.checkValidFormat(lesson_status, regex.CMIStatus)) { - this.#lesson_status = lesson_status; - } - } + /** + * Setter for #credit. Can only be called before API initialization. + * @param {string} credit + */ + set credit(credit) { + this.API.isNotInitialized() ? + this.#credit = credit : + throwReadOnlyError(this.API); + } - get entry() { - return this.#entry; - } + /** + * Getter for #lesson_status + * @return {string} + */ + get lesson_status() { + return this.#lesson_status; + } - set entry(entry) { - this.API.isNotInitialized() ? - this.#entry = entry : - throwReadOnlyError(this.API); + /** + * Setter for #lesson_status + * @param {string} lesson_status + */ + set lesson_status(lesson_status) { + if (this.API.checkValidFormat(lesson_status, regex.CMIStatus)) { + this.#lesson_status = lesson_status; } + } - get total_time() { - return this.#total_time; - } + /** + * Getter for #entry + * @return {string} + */ + get entry() { + return this.#entry; + } - set total_time(total_time) { - this.API.isNotInitialized() ? - this.#total_time = total_time : - throwReadOnlyError(this.API); - } + /** + * Setter for #entry. Can only be called before API initialization. + * @param {string} entry + */ + set entry(entry) { + this.API.isNotInitialized() ? + this.#entry = entry : + throwReadOnlyError(this.API); + } - get lesson_mode() { - return this.#lesson_mode; - } + /** + * Getter for #total_time + * @return {string} + */ + get total_time() { + return this.#total_time; + } - set lesson_mode(lesson_mode) { - this.API.isNotInitialized() ? - this.#lesson_mode = lesson_mode : - throwReadOnlyError(this.API); - } + /** + * Setter for #total_time. Can only be called before API initialization. + * @param {string} total_time + */ + set total_time(total_time) { + this.API.isNotInitialized() ? + this.#total_time = total_time : + throwReadOnlyError(this.API); + } - get exit() { - return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#exit; - } - set exit(exit) { - if (this.API.checkValidFormat(exit, regex.CMIExit)) { - this.#exit = exit; - } - } + /** + * Getter for #lesson_mode + * @return {string} + */ + get lesson_mode() { + return this.#lesson_mode; + } - get session_time() { - return (!this.jsonString) ? - throwWriteOnlyError(this.API) : - this.#session_time; - } - set session_time(session_time) { - if (this.API.checkValidFormat(session_time, regex.CMITimespan)) { - this.#session_time = session_time; - } - } + /** + * Setter for #lesson_mode. Can only be called before API initialization. + * @param {string} lesson_mode + */ + set lesson_mode(lesson_mode) { + this.API.isNotInitialized() ? + this.#lesson_mode = lesson_mode : + throwReadOnlyError(this.API); + } - toJSON = () => { - this.jsonString = true; - const result = { - 'student_id': this.student_id, - 'student_name': this.student_name, - 'lesson_location': this.lesson_location, - 'credit': this.credit, - 'lesson_status': this.lesson_status, - 'entry': this.entry, - 'total_time': this.total_time, - 'lesson_mode': this.lesson_mode, - 'exit': this.exit, - 'session_time': this.session_time, - 'score': this.score, - }; - delete this.jsonString; - return result; + /** + * Getter for #exit. Should only be called during JSON export. + * @return {*} + */ + get exit() { + return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#exit; + } + + /** + * Setter for #exit + * @param {string} exit + */ + set exit(exit) { + if (this.API.checkValidFormat(exit, regex.CMIExit)) { + this.#exit = exit; } + } + + /** + * Getter for #session_time. Should only be called during JSON export. + * @return {*} + */ + get session_time() { + return (!this.jsonString) ? + throwWriteOnlyError(this.API) : + this.#session_time; + } + + /** + * Setter for #session_time + * @param {string} session_time + */ + set session_time(session_time) { + if (this.API.checkValidFormat(session_time, regex.CMITimespan)) { + this.#session_time = session_time; + } + } + + /** + * toJSON for cmi.core + * + * @return { + * { + * student_name: string, + * entry: string, + * exit: string, + * score: Scorm12CMIScore, + * student_id: string, + * lesson_mode: string, + * lesson_location: string, + * lesson_status: string, + * credit: string, + * total_time: string, + * session_time: * + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'student_id': this.student_id, + 'student_name': this.student_name, + 'lesson_location': this.lesson_location, + 'credit': this.credit, + 'lesson_status': this.lesson_status, + 'entry': this.entry, + 'total_time': this.total_time, + 'lesson_mode': this.lesson_mode, + 'exit': this.exit, + 'session_time': this.session_time, + 'score': this.score, + }; + delete this.jsonString; + return result; + } } +/** + * Class representing SCORM 1.2's cmi.objectives object + */ class CMIObjectives extends CMIArray { + /** + * Constructor for cmi.objectives + * @param {Scorm12API} API + */ constructor(API) { super({ API: API, @@ -269,143 +489,267 @@ class CMIObjectives extends CMIArray { } } - +/** + * Class representing SCORM 1.2's cmi.student_data object + */ export class CMIStudentData extends BaseCMI { - #_children; - #mastery_score = ''; - #max_time_allowed = ''; - #time_limit_action = ''; + #_children; + #mastery_score = ''; + #max_time_allowed = ''; + #time_limit_action = ''; - constructor(API, student_data_children) { - super(API); + /** + * Constructor for cmi.student_data + * @param {Scorm12API} API + * @param {string} student_data_children + */ + constructor(API, student_data_children) { + super(API); - this.#_children = student_data_children ? - student_data_children : - constants.student_data_children; - } + this.#_children = student_data_children ? + student_data_children : + constants.student_data_children; + } - get _children() { - return this.#_children; - } + /** + * Getter for #_children + * @return {*} + * @private + */ + get _children() { + return this.#_children; + } - set _children(_children) { - throwInvalidValueError(this.API); - } + /** + * Setter for #_children. Just throws an error. + * @param {string} _children + * @private + */ + set _children(_children) { + throwInvalidValueError(this.API); + } - get mastery_score() { - return this.#mastery_score; - } + /** + * Getter for #master_score + * @return {string} + */ + get mastery_score() { + return this.#mastery_score; + } - set mastery_score(mastery_score) { - this.API.isNotInitialized() ? - this.#mastery_score = mastery_score : - throwReadOnlyError(this.API); - } + /** + * Setter for #master_score. Can only be called before API initialization. + * @param {string} mastery_score + */ + set mastery_score(mastery_score) { + this.API.isNotInitialized() ? + this.#mastery_score = mastery_score : + throwReadOnlyError(this.API); + } - get max_time_allowed() { - return this.#max_time_allowed; - } + /** + * Getter for #max_time_allowed + * @return {string} + */ + get max_time_allowed() { + return this.#max_time_allowed; + } - set max_time_allowed(max_time_allowed) { - this.API.isNotInitialized() ? - this.#max_time_allowed = max_time_allowed : - throwReadOnlyError(this.API); - } + /** + * Setter for #max_time_allowed. Can only be called before API initialization. + * @param {string} max_time_allowed + */ + set max_time_allowed(max_time_allowed) { + this.API.isNotInitialized() ? + this.#max_time_allowed = max_time_allowed : + throwReadOnlyError(this.API); + } - get time_limit_action() { - return this.#time_limit_action; - } + /** + * Getter for #time_limit_action + * @return {string} + */ + get time_limit_action() { + return this.#time_limit_action; + } - set time_limit_action(time_limit_action) { - this.API.isNotInitialized() ? - this.#time_limit_action = time_limit_action : - throwReadOnlyError(this.API); - } + /** + * Setter for #time_limit_action. Can only be called before API initialization. + * @param {string} time_limit_action + */ + set time_limit_action(time_limit_action) { + this.API.isNotInitialized() ? + this.#time_limit_action = time_limit_action : + throwReadOnlyError(this.API); + } - toJSON = () => { - this.jsonString = true; - const result = { - 'mastery_score': this.mastery_score, - 'max_time_allowed': this.max_time_allowed, - 'time_limit_action': this.time_limit_action, - }; - delete this.jsonString; - return result; - } + /** + * toJSON for cmi.student_data + * + * @return { + * { + * max_time_allowed: string, + * time_limit_action: string, + * mastery_score: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'mastery_score': this.mastery_score, + 'max_time_allowed': this.max_time_allowed, + 'time_limit_action': this.time_limit_action, + }; + delete this.jsonString; + return result; + } } +/** + * Class representing SCORM 1.2's cmi.student_preference object + */ class CMIStudentPreference extends BaseCMI { + /** + * Constructor for cmi.student_preference + * @param {Scorm12API} API + */ constructor(API) { super(API); } - #_children = constants.student_preference_children; - #audio = ''; - #language = ''; - #speed = ''; - #text = ''; + #_children = constants.student_preference_children; + #audio = ''; + #language = ''; + #speed = ''; + #text = ''; - get _children() { - return this.#_children; - } + /** + * Getter for #_children + * @return {string} + * @private + */ + get _children() { + return this.#_children; + } - set _children(_children) { - throwInvalidValueError(this.API); - } + /** + * Setter for #_children. Just throws an error. + * @param {string} _children + * @private + */ + set _children(_children) { + throwInvalidValueError(this.API); + } - get audio() { - return this.#audio; - } - set audio(audio) { - if (this.API.checkValidFormat(audio, regex.CMISInteger) && - this.API.checkValidRange(audio, regex.audio_range)) { - this.#audio = audio; - } - } + /** + * Getter for #audio + * @return {string} + */ + get audio() { + return this.#audio; + } - get language() { - return this.#language; - } - set language(language) { - if (this.API.checkValidFormat(language, regex.CMIString256)) { - this.#language = language; - } + /** + * Setter for #audio + * @param {string} audio + */ + set audio(audio) { + if (this.API.checkValidFormat(audio, regex.CMISInteger) && + this.API.checkValidRange(audio, regex.audio_range)) { + this.#audio = audio; } + } - get speed() { - return this.#speed; - } - set speed(speed) { - if (this.API.checkValidFormat(speed, regex.CMISInteger) && - this.API.checkValidRange(speed, regex.speed_range)) { - this.#speed = speed; - } - } + /** + * Getter for #language + * @return {string} + */ + get language() { + return this.#language; + } - get text() { - return this.#text; - } - set text(text) { - if (this.API.checkValidFormat(text, regex.CMISInteger) && - this.API.checkValidRange(text, regex.text_range)) { - this.#text = text; - } + /** + * Setter for #language + * @param {string} language + */ + set language(language) { + if (this.API.checkValidFormat(language, regex.CMIString256)) { + this.#language = language; } + } - toJSON = () => { - this.jsonString = true; - const result = { - 'audio': this.audio, - 'language': this.language, - 'speed': this.speed, - 'text': this.text, - }; - delete this.jsonString; - return result; + /** + * Getter for #speed + * @return {string} + */ + get speed() { + return this.#speed; + } + + /** + * Setter for #speed + * @param {string} speed + */ + set speed(speed) { + if (this.API.checkValidFormat(speed, regex.CMISInteger) && + this.API.checkValidRange(speed, regex.speed_range)) { + this.#speed = speed; } + } + + /** + * Getter for #text + * @return {string} + */ + get text() { + return this.#text; + } + + /** + * Setter for #text + * @param {string} text + */ + set text(text) { + if (this.API.checkValidFormat(text, regex.CMISInteger) && + this.API.checkValidRange(text, regex.text_range)) { + this.#text = text; + } + } + + /** + * toJSON for cmi.student_preference + * + * @return { + * { + * audio: string, + * language: string, + * speed: string, + * text: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'audio': this.audio, + 'language': this.language, + 'speed': this.speed, + 'text': this.text, + }; + delete this.jsonString; + return result; + } } +/** + * Class representing SCORM 1.2's cmi.interactions object + */ class CMIInteractions extends CMIArray { + /** + * Constructor for cmi.interactions + * @param {Scorm12API} API + */ constructor(API) { super({ API: API, @@ -415,7 +759,14 @@ class CMIInteractions extends CMIArray { } } +/** + * Class representing SCORM 1.2's cmi.interactions.n object + */ export class CMIInteractionsObject extends BaseCMI { + /** + * Constructor for cmi.interactions.n object + * @param {Scorm12API} API + */ constructor(API) { super(API); @@ -431,192 +782,362 @@ export class CMIInteractionsObject extends BaseCMI { }); } - #id: ""; - #time: ""; - #type: ""; - #weighting: ""; - #student_response: ""; - #result: ""; - #latency: ""; + #id: ''; + #time: ''; + #type: ''; + #weighting: ''; + #student_response: ''; + #result: ''; + #latency: ''; - get id() { - return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#id; - } - set id(id) { - if (this.API.checkValidFormat(id, regex.CMIIdentifier)) { - this.#id = id; - } - } + /** + * Getter for #id. Should only be called during JSON export. + * @return {*} + */ + get id() { + return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#id; + } - get time() { - return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#time; - } - set time(time) { - if (this.API.checkValidFormat(time, regex.CMITime)) { - this.#time = time; - } + /** + * Setter for #id + * @param {string} id + */ + set id(id) { + if (this.API.checkValidFormat(id, regex.CMIIdentifier)) { + this.#id = id; } + } - get type() { - return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#type; - } - set type(type) { - if (this.API.checkValidFormat(type, regex.CMIType)) { - this.#type = type; - } - } + /** + * Getter for #time. Should only be called during JSON export. + * @return {*} + */ + get time() { + return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#time; + } - get weighting() { - return (!this.jsonString) ? - throwWriteOnlyError(this.API) : - this.#weighting; - } - set weighting(weighting) { - if (this.API.checkValidFormat(weighting, regex.CMIDecimal) && - this.API.checkValidRange(weighting, regex.weighting_range)) { - this.#weighting = weighting; - } + /** + * Setter for #time + * @param {string} time + */ + set time(time) { + if (this.API.checkValidFormat(time, regex.CMITime)) { + this.#time = time; } + } - get student_response() { - return (!this.jsonString) ? - throwWriteOnlyError(this.API) : - this.#student_response; - } - set student_response(student_response) { - if (this.API.checkValidFormat(student_response, regex.CMIFeedback)) { - this.#student_response = student_response; - } - } + /** + * Getter for #type. Should only be called during JSON export. + * @return {*} + */ + get type() { + return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#type; + } - get result() { - return (!this.jsonString) ? - throwWriteOnlyError(this.API) : - this.#result; - } - set result(result) { - if (this.API.checkValidFormat(result, regex.CMIResult)) { - this.#result = result; - } + /** + * Setter for #type + * @param {string} type + */ + set type(type) { + if (this.API.checkValidFormat(type, regex.CMIType)) { + this.#type = type; } + } - get latency() { - return (!this.jsonString) ? - throwWriteOnlyError(this.API) : - this.#latency; - } - set latency(latency) { - if (this.API.checkValidFormat(latency, regex.CMITimespan)) { - this.#latency = latency; - } - } + /** + * Getter for #weighting. Should only be called during JSON export. + * @return {*} + */ + get weighting() { + return (!this.jsonString) ? + throwWriteOnlyError(this.API) : + this.#weighting; + } - toJSON = () => { - this.jsonString = true; - const result = { - 'id': this.id, - 'time': this.time, - 'type': this.type, - 'weighting': this.weighting, - 'student_response': this.student_response, - 'result': this.result, - 'latency': this.latency, - 'objectives': this.objectives, - 'correct_responses': this.correct_responses, - }; - delete this.jsonString; - return result; + /** + * Setter for #weighting + * @param {string} weighting + */ + set weighting(weighting) { + if (this.API.checkValidFormat(weighting, regex.CMIDecimal) && + this.API.checkValidRange(weighting, regex.weighting_range)) { + this.#weighting = weighting; } + } + + /** + * Getter for #student_response. Should only be called during JSON export. + * @return {*} + */ + get student_response() { + return (!this.jsonString) ? + throwWriteOnlyError(this.API) : + this.#student_response; + } + + /** + * Setter for #student_response + * @param {string} student_response + */ + set student_response(student_response) { + if (this.API.checkValidFormat(student_response, regex.CMIFeedback)) { + this.#student_response = student_response; + } + } + + /** + * Getter for #result. Should only be called during JSON export. + * @return {*} + */ + get result() { + return (!this.jsonString) ? + throwWriteOnlyError(this.API) : + this.#result; + } + + /** + * Setter for #result + * @param {string} result + */ + set result(result) { + if (this.API.checkValidFormat(result, regex.CMIResult)) { + this.#result = result; + } + } + + /** + * Getter for #latency. Should only be called during JSON export. + * @return {*} + */ + get latency() { + return (!this.jsonString) ? + throwWriteOnlyError(this.API) : + this.#latency; + } + + /** + * Setter for #latency + * @param {string} latency + */ + set latency(latency) { + if (this.API.checkValidFormat(latency, regex.CMITimespan)) { + this.#latency = latency; + } + } + + /** + * toJSON for cmi.interactions.n + * + * @return { + * { + * id: string, + * time: string, + * type: string, + * weighting: string, + * student_response: string, + * result: string, + * latency: string, + * objectives: CMIArray, + * correct_responses: CMIArray + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'id': this.id, + 'time': this.time, + 'type': this.type, + 'weighting': this.weighting, + 'student_response': this.student_response, + 'result': this.result, + 'latency': this.latency, + 'objectives': this.objectives, + 'correct_responses': this.correct_responses, + }; + delete this.jsonString; + return result; + } } +/** + * Class representing SCORM 1.2's cmi.objectives.n object + */ export class CMIObjectivesObject extends BaseCMI { + /** + * Constructor for cmi.objectives.n + * @param {Scorm12API} API + */ constructor(API) { super(API); - this.score = new CMIScore(API); + this.score = new Scorm12CMIScore(API); } - #id: ""; - #status: ""; + #id: ''; + #status: ''; - get id() { - return this.#id; - } - set id(id) { - if (this.API.checkValidFormat(id, regex.CMIIdentifier)) { - this.#id = id; - } - } + /** + * Getter for #id + * @return {""} + */ + get id() { + return this.#id; + } - get status() { - return this.#status; - } - set status(status) { - if (this.API.checkValidFormat(status, regex.CMIStatus2)) { - this.#status = status; - } + /** + * Setter for #id + * @param {string} id + */ + set id(id) { + if (this.API.checkValidFormat(id, regex.CMIIdentifier)) { + this.#id = id; } + } - toJSON = () => { - this.jsonString = true; - const result = { - 'id': this.id, - 'status': this.status, - 'score': this.score, - }; - delete this.jsonString; - return result; + /** + * Getter for #status + * @return {""} + */ + get status() { + return this.#status; + } + + /** + * Setter for #status + * @param {string} status + */ + set status(status) { + if (this.API.checkValidFormat(status, regex.CMIStatus2)) { + this.#status = status; } + } + + /** + * toJSON for cmi.objectives.n + * @return { + * { + * id: string, + * status: string, + * score: Scorm12CMIScore + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'id': this.id, + 'status': this.status, + 'score': this.score, + }; + delete this.jsonString; + return result; + } } +/** + * Class representing SCORM 1.2's cmi.interactions.n.objectives.n object + */ export class CMIInteractionsObjectivesObject extends BaseCMI { + /** + * Constructor for cmi.interactions.n.objectives.n + * @param {Scorm12API} API + */ constructor(API) { super(API); } - #id: ""; + #id: ''; - get id() { - return this.#id; - } - set id(id) { - if (this.API.checkValidFormat(id, regex.CMIIdentifier)) { - this.#id = id; - } - } + /** + * Getter for #id + * @return {""} + */ + get id() { + return this.#id; + } - toJSON = () => { - this.jsonString = true; - const result = { - 'id': this.id, - }; - delete this.jsonString; - return result; + /** + * Setter for #id + * @param {string} id + */ + set id(id) { + if (this.API.checkValidFormat(id, regex.CMIIdentifier)) { + this.#id = id; } + } + + /** + * toJSON for cmi.interactions.n.objectives.n + * @return { + * { + * id: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'id': this.id, + }; + delete this.jsonString; + return result; + } } +/** + * Class representing SCORM 1.2's cmi.interactions.correct_responses.n object + */ export class CMIInteractionsCorrectResponsesObject extends BaseCMI { + /** + * Constructor for cmi.interactions.correct_responses.n + * @param {Scorm12API} API + */ constructor(API) { super(API); } - #pattern: ""; + #pattern: ''; - get pattern() { - return this.#pattern; - } - set pattern(pattern) { - if (this.API.checkValidFormat(pattern, regex.CMIFeedback)) { - this.#pattern = pattern; - } - } + /** + * Getter for #pattern + * @return {""} + */ + get pattern() { + return this.#pattern; + } - toJSON = () => { - this.jsonString = true; - const result = { - 'pattern': this.pattern, - }; - delete this.jsonString; - return result; + /** + * Setter for #pattern + * @param {string} pattern + */ + set pattern(pattern) { + if (this.API.checkValidFormat(pattern, regex.CMIFeedback)) { + this.#pattern = pattern; } + } + + /** + * toJSON for cmi.interactions.correct_responses.n + * @return { + * { + * pattern: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'pattern': this.pattern, + }; + delete this.jsonString; + return result; + } +} + +/** + * Basic extension of CMIScore + */ +class Scorm12CMIScore extends CMIScore { + toJSON = super.toJSON; } diff --git a/src/cmi/scorm2004_cmi.js b/src/cmi/scorm2004_cmi.js index 8fa95ed..94ac398 100644 --- a/src/cmi/scorm2004_cmi.js +++ b/src/cmi/scorm2004_cmi.js @@ -1,525 +1,1493 @@ +// @flow import {BaseCMI, CMIArray, CMIScore} from './common'; -import {learner_responses, scorm2004_constants, scorm2004_error_codes} from "../constants/api_constants"; -import {scorm2004_regex} from "../regex"; +import {scorm2004_constants} from '../constants/api_constants'; +import {scorm2004_regex} from '../regex'; +import {scorm2004_error_codes} from '../constants/error_codes'; +import {learner_responses} from '../constants/response_constants'; const constants = scorm2004_constants; const regex = scorm2004_regex; +/** + * Helper method for throwing Read Only error + * @param {Scorm2004API} API + */ function throwReadOnlyError(API) { - API.throwSCORMError(scorm2004_error_codes.READ_ONLY_ELEMENT); + API.throwSCORMError(scorm2004_error_codes.READ_ONLY_ELEMENT); } +/** + * Helper method for throwing Write Only error + * @param {Scorm2004API} API + */ function throwWriteOnlyError(API) { - API.throwSCORMError(scorm2004_error_codes.WRITE_ONLY_ELEMENT); + API.throwSCORMError(scorm2004_error_codes.WRITE_ONLY_ELEMENT); } +/** + * Helper method for throwing Type Mismatch error + * @param {Scorm2004API} API + */ +function throwTypeMismatchError(API) { + API.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH); +} + +/** + * Class representing cmi object for SCORM 2004 + */ export class CMI extends BaseCMI { - constructor(API) { - super(API); + /** + * Constructor for the SCORM 2004 cmi object + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); - this.learner_preference = new CMILearnerPreference(API); + this.learner_preference = new CMILearnerPreference(API); + this.score = new Scorm2004CMIScore(API); + this.comments_from_learner = new CMICommentsFromLearner(API); + this.comments_from_lms = new CMICommentsFromLMS(API); + this.interactions = new CMIInteractions(API); + this.objectives = new CMIObjectives(API); + } + + #_version = '1.0'; + #_children = constants.cmi_children; + #completion_status = 'unknown'; + #completion_threshold = ''; + #credit = 'credit'; + #entry = ''; + #exit = ''; + #launch_data = ''; + #learner_id = ''; + #learner_name = ''; + #location = ''; + #max_time_allowed = ''; + #mode = 'normal'; + #progress_measure = ''; + #scaled_passing_score = ''; + #session_time = 'PT0H0M0S'; + #success_status = 'unknown'; + #suspend_data = ''; + #time_limit_action = 'continue,no message'; + #total_time = '0'; + + /** + * Getter for #_version + * @return {string} + * @private + */ + get _version() { + return this.#_version; + } + + /** + * Setter for #_version. Just throws an error. + * @param {string} _version + * @private + */ + set _version(_version) { + throwReadOnlyError(this.API); + } + + /** + * Getter for #_children + * @return {string} + * @private + */ + get _children() { + return this.#_children; + } + + /** + * Setter for #_children. Just throws an error. + * @param {number} _children + * @private + */ + set _children(_children) { + throwReadOnlyError(this.API); + } + + /** + * Getter for #completion_status + * @return {string} + */ + get completion_status() { + return this.#completion_status; + } + + /** + * Setter for #completion_status + * @param {string} completion_status + */ + set completion_status(completion_status) { + if (this.API.checkValidFormat(completion_status, regex.CMICStatus)) { + this.#completion_status = completion_status; } + } - #_version: "1.0"; - #_children = constants.cmi_children; - #completion_status: "unknown"; - #completion_threshold: ""; - #credit: "credit"; - #entry: ""; - #exit: ""; - #launch_data: ""; - #learner_id: ""; - #learner_name: ""; - #location: ""; - #max_time_allowed: ""; - #mode: "normal"; - #progress_measure: ""; - #scaled_passing_score: ""; - #session_time: "PT0H0M0S"; - #success_status: "unknown"; - #suspend_data: ""; - #time_limit_action: "continue,no message"; - #total_time: "0"; + /** + * Getter for #completion_threshold + * @return {string} + */ + get completion_threshold() { + return this.#completion_threshold; + } - get _version() { return this.#_version; } - set _version(_version) { throwReadOnlyError(); } + /** + * Setter for #completion_threshold. Can only be called before API initialization. + * @param {string} completion_threshold + */ + set completion_threshold(completion_threshold) { + this.API.isNotInitialized() ? + this.#completion_threshold = completion_threshold : + throwReadOnlyError(this.API); + } - get _children() { return this.#_children; } - set _children(_children) { throwReadOnlyError(); } + /** + * Setter for #credit + * @return {string} + */ + get credit() { + return this.#credit; + } - get completion_status() { return this.#completion_status; } - set completion_status(completion_status) { - if(this.API.checkValidFormat(completion_status, regex.CMICStatus)) { - this.#completion_status = completion_status; - } + /** + * Setter for #credit. Can only be called before API initialization. + * @param {string} credit + */ + set credit(credit) { + this.API.isNotInitialized() ? + this.#credit = credit : + throwReadOnlyError(this.API); + } + + /** + * Getter for #entry + * @return {string} + */ + get entry() { + return this.#entry; + } + + /** + * Setter for #entry. Can only be called before API initialization. + * @param {string} entry + */ + set entry(entry) { + this.API.isNotInitialized() ? + this.#entry = entry : + throwReadOnlyError(this.API); + } + + /** + * Getter for #exit. Should only be called during JSON export. + * @return {string} + */ + get exit() { + return (!this.jsonString) ? throwWriteOnlyError() : this.#exit; + } + + /** + * Getter for #exit + * @param {string} exit + */ + set exit(exit) { + if (this.API.checkValidFormat(exit, regex.CMIExit)) { + this.#exit = exit; } + } - get completion_threshold() { return this.#completion_threshold; } - set completion_threshold(completion_threshold) { this.API.isNotInitialized() ? this.#completion_threshold = completion_threshold : throwReadOnlyError(); } + /** + * Getter for #launch_data + * @return {string} + */ + get launch_data() { + return this.#launch_data; + } - get credit() { return this.#credit; } - set credit(credit) { this.API.isNotInitialized() ? this.#credit = credit : throwReadOnlyError(); } + /** + * Setter for #launch_data. Can only be called before API initialization. + * @param {string} launch_data + */ + set launch_data(launch_data) { + this.API.isNotInitialized() ? + this.#launch_data = launch_data : + throwReadOnlyError(this.API); + } - get entry() { return this.#entry; } - set entry(entry) { this.API.isNotInitialized() ? this.#entry = entry : throwReadOnlyError(); } + /** + * Getter for #learner_id + * @return {string} + */ + get learner_id() { + return this.#learner_id; + } - get exit() { return (!this.jsonString) ? throwWriteOnlyError() : this.#exit; } - set exit(exit) { - if(this.API.checkValidFormat(exit, regex.CMIExit)) { - this.#exit = exit; - } + /** + * Setter for #learner_id. Can only be called before API initialization. + * @param {string} learner_id + */ + set learner_id(learner_id) { + this.API.isNotInitialized() ? + this.#learner_id = learner_id : + throwReadOnlyError(this.API); + } + + /** + * Getter for #learner_name + * @return {string} + */ + get learner_name() { + return this.#learner_name; + } + + /** + * Setter for #learner_name. Can only be called before API initialization. + * @param {string} learner_name + */ + set learner_name(learner_name) { + this.API.isNotInitialized() ? + this.#learner_name = learner_name : + throwReadOnlyError(this.API); + } + + /** + * Getter for #location + * @return {string} + */ + get location() { + return this.#location; + } + + /** + * Setter for #location + * @param {string} location + */ + set location(location) { + if (this.API.checkValidFormat(location, regex.CMIString1000)) { + this.#location = location; } + } - get launch_data() { return this.#launch_data; } - set launch_data(launch_data) { this.API.isNotInitialized() ? this.#launch_data = launch_data : throwReadOnlyError(); } + /** + * Getter for #max_time_allowed + * @return {string} + */ + get max_time_allowed() { + return this.#max_time_allowed; + } - get learner_id() { return this.#learner_id; } - set learner_id(learner_id) { this.API.isNotInitialized() ? this.#learner_id = learner_id : throwReadOnlyError(); } + /** + * Setter for #max_time_allowed. Can only be called before API initialization. + * @param {string} max_time_allowed + */ + set max_time_allowed(max_time_allowed) { + this.API.isNotInitialized() ? + this.#max_time_allowed = max_time_allowed : + throwReadOnlyError(this.API); + } - get learner_name() { return this.#learner_name; } - set learner_name(learner_name) { this.API.isNotInitialized() ? this.#learner_name = learner_name : throwReadOnlyError(); } + /** + * Getter for #mode + * @return {string} + */ + get mode() { + return this.#mode; + } - get location() { return this.#location; } - set location(location) { - if(this.API.checkValidFormat(location, regex.CMIString1000)) { - this.#location = location; - } + /** + * Setter for #mode. Can only be called before API initialization. + * @param {string} mode + */ + set mode(mode) { + this.API.isNotInitialized() ? + this.#mode = mode : + throwReadOnlyError(this.API); + } + + /** + * Getter for #progress_measure + * @return {string} + */ + get progress_measure() { + return this.#progress_measure; + } + + /** + * Setter for #progress_measure + * @param {string} progress_measure + */ + set progress_measure(progress_measure) { + if (this.API.checkValidFormat(progress_measure, regex.CMIDecimal) && + this.API.checkValidRange(progress_measure, regex.progress_range)) { + this.#progress_measure = progress_measure; } + } - get max_time_allowed() { return this.#max_time_allowed; } - set max_time_allowed(max_time_allowed) { this.API.isNotInitialized() ? this.#max_time_allowed = max_time_allowed : throwReadOnlyError(); } + /** + * Getter for #scaled_passing_score + * @return {string} + */ + get scaled_passing_score() { + return this.#scaled_passing_score; + } - get mode() { return this.#mode; } - set mode(mode) { this.API.isNotInitialized() ? this.#mode = mode : throwReadOnlyError(); } + /** + * Setter for #scaled_passing_score. Can only be called before API initialization. + * @param {string} scaled_passing_score + */ + set scaled_passing_score(scaled_passing_score) { + this.API.isNotInitialized() ? + this.#scaled_passing_score = scaled_passing_score : + throwReadOnlyError(this.API); + } - get progress_measure() { return this.#progress_measure; } - set progress_measure(progress_measure) { - if(this.API.checkValidFormat(progress_measure, regex.CMIDecimal) - && this.API.checkValidRange(progress_measure, regex.progress_range)) { - this.#progress_measure = progress_measure; - } + /** + * Getter for #session_time. Should only be called during JSON export. + * @return {string} + */ + get session_time() { + return (!this.jsonString) ? + this.API.throwSCORMError(405) : + this.#session_time; + } + + /** + * Setter for #session_time + * @param {string} session_time + */ + set session_time(session_time) { + if (this.API.checkValidFormat(session_time, regex.CMITimespan)) { + this.#session_time = session_time; } + } - get scaled_passing_score() { return this.#scaled_passing_score; } - set scaled_passing_score(scaled_passing_score) { this.API.isNotInitialized() ? this.#scaled_passing_score = scaled_passing_score : throwReadOnlyError(); } + /** + * Getter for #success_status + * @return {string} + */ + get success_status() { + return this.#success_status; + } - get session_time() { return (!this.jsonString) ? this.API.throwSCORMError(405) : this.#session_time; } - set session_time(session_time) { - if(this.API.checkValidFormat(session_time, regex.CMITimespan)) { - this.#session_time = session_time; - } + /** + * Setter for #success_status + * @param {string} success_status + */ + set success_status(success_status) { + if (this.API.checkValidFormat(success_status, regex.CMISStatus)) { + this.#success_status = success_status; } - - get success_status() { return this.#success_status; } - set success_status(success_status) { - if(this.API.checkValidFormat(success_status, regex.CMISStatus)) { - this.#success_status = success_status; - } + } + + /** + * Getter for #suspend_data + * @return {string} + */ + get suspend_data() { + return this.#suspend_data; + } + + /** + * Setter for #suspend_data + * @param {string} suspend_data + */ + set suspend_data(suspend_data) { + if (this.API.checkValidFormat(suspend_data, regex.CMIString64000)) { + this.#suspend_data = suspend_data; } + } - get suspend_data() { return this.#suspend_data; } - set suspend_data(suspend_data) { - if(this.API.checkValidFormat(suspend_data, regex.CMIString64000)) { - this.#suspend_data = suspend_data; - } - } + /** + * Getter for #time_limit_action + * @return {string} + */ + get time_limit_action() { + return this.#time_limit_action; + } - get time_limit_action() { return this.#time_limit_action; } - set time_limit_action(time_limit_action) { this.API.isNotInitialized() ? this.#time_limit_action = time_limit_action : throwReadOnlyError(); } + /** + * Setter for #time_limit_action. Can only be called before API initialization. + * @param {string} time_limit_action + */ + set time_limit_action(time_limit_action) { + this.API.isNotInitialized() ? + this.#time_limit_action = time_limit_action : + throwReadOnlyError(this.API); + } - get total_time() { return this.#total_time; } - set total_time(total_time) { this.API.isNotInitialized() ? this.#total_time = total_time : throwReadOnlyError(); } + /** + * Getter for #total_time + * @return {string} + */ + get total_time() { + return this.#total_time; + } - comments_from_learner = new class extends CMIArray { - constructor(API) { - super({ - API: API, - children: constants.comments_children, - errorCode: 404 - }); - } - }; - - comments_from_lms = new class extends CMIArray { - constructor(API) { - super({ - API: API, - children: constants.comments_children, - errorCode: 404 - }); - } - }; - - interactions = new class extends CMIArray { - constructor(API) { - super({ - API: API, - children: constants.interactions_children, - errorCode: 404 - }); - } - }; - - objectives = new class extends CMIArray { - constructor(API) { - super({ - API: API, - children: constants.objectives_children, - errorCode: 404 - }); - } + /** + * Setter for #total_time. Can only be called before API initialization. + * @param {string} total_time + */ + set total_time(total_time) { + this.API.isNotInitialized() ? + this.#total_time = total_time : + throwReadOnlyError(this.API); + } + + /** + * toJSON for cmi + * + * @return { + * { + * comments_from_learner: CMICommentsFromLearner, + * comments_from_lms: CMICommentsFromLMS, + * completion_status: string, + * completion_threshold: string, + * credit: string, + * entry: string, + * exit: string, + * interactions: CMIInteractions, + * launch_data: string, + * learner_id: string, + * learner_name: string, + * learner_preference: CMILearnerPreference, + * location: string, + * max_time_allowed: string, + * mode: string, + * objectives: CMIObjectives, + * progress_measure: string, + * scaled_passing_score: string, + * score: Scorm2004CMIScore, + * session_time: string, + * success_status: string, + * suspend_data: string, + * time_limit_action: string, + * total_time: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'comments_from_learner': this.comments_from_learner, + 'comments_from_lms': this.comments_from_lms, + 'completion_status': this.completion_status, + 'completion_threshold': this.completion_threshold, + 'credit': this.credit, + 'entry': this.entry, + 'exit': this.exit, + 'interactions': this.interactions, + 'launch_data': this.launch_data, + 'learner_id': this.learner_id, + 'learner_name': this.learner_name, + 'learner_preference': this.learner_preference, + 'location': this.location, + 'max_time_allowed': this.max_time_allowed, + 'mode': this.mode, + 'objectives': this.objectives, + 'progress_measure': this.progress_measure, + 'scaled_passing_score': this.scaled_passing_score, + 'score': this.score, + 'session_time': this.session_time, + 'success_status': this.success_status, + 'suspend_data': this.suspend_data, + 'time_limit_action': this.time_limit_action, + 'total_time': this.total_time, }; + delete this.jsonString; + return result; + } } +/** + * Class for SCORM 2004's cmi.learner_preference object + */ class CMILearnerPreference extends BaseCMI { - constructor(API) { - super(API); + /** + * Constructor for cmi.learner_preference + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); + } + + #_children = constants.student_preference_children; + #audio_level = '1'; + #language = ''; + #delivery_speed = '1'; + #audio_captioning = '0'; + + /** + * Getter for #_children + * @return {string} + * @private + */ + get _children() { + return this.#_children; + } + + /** + * Setter for #_children. Just throws an error. + * @param {string} _children + * @private + */ + set _children(_children) { + throwReadOnlyError(this.API); + } + + /** + * Getter for #audio_level + * @return {string} + */ + get audio_level() { + return this.#audio_level; + } + + /** + * Setter for #audio_level + * @param {string} audio_level + */ + set audio_level(audio_level) { + if (this.API.checkValidFormat(audio_level, regex.CMIDecimal) && + this.API.checkValidRange(audio_level, regex.audio_range)) { + this.#audio_level = audio_level; } + } - #_children: constants.student_preference_children; - #audio_level: "1"; - #language: ""; - #delivery_speed: "1"; - #audio_captioning: "0"; + /** + * Getter for #language + * @return {string} + */ + get language() { + return this.#language; + } - get _children() { return this.#_children; } - set _children(_children) { throwReadOnlyError(); } - - get audio_level() { return this.#audio_level; } - set audio_level(audio_level) { - if(this.API.checkValidFormat(audio_level, regex.CMIDecimal) - && this.API.checkValidRange(audio_level, regex.audio_range)) { - this.#audio_level = audio_level; - } + /** + * Setter for #language + * @param {string} language + */ + set language(language) { + if (this.API.checkValidFormat(language, regex.CMILang)) { + this.#language = language; } + } - get language() { return this.#language; } - set language(language) { - if(this.API.checkValidFormat(language, regex.CMILang)) { - this.#language = language; - } + /** + * Getter for #delivery_speed + * @return {string} + */ + get delivery_speed() { + return this.#delivery_speed; + } + + /** + * Setter for #delivery_speed + * @param {string} delivery_speed + */ + set delivery_speed(delivery_speed) { + if (this.API.checkValidFormat(delivery_speed, regex.CMIDecimal) && + this.API.checkValidRange(delivery_speed, regex.speed_range)) { + this.#delivery_speed = delivery_speed; } + } - get delivery_speed() { return this.#delivery_speed; } - set delivery_speed(delivery_speed) { - if(this.API.checkValidFormat(delivery_speed, regex.CMIDecimal) - && this.API.checkValidRange(delivery_speed, regex.speed_range)) { - this.#delivery_speed = delivery_speed; - } + /** + * Getter for #audio_captioning + * @return {string} + */ + get audio_captioning() { + return this.#audio_captioning; + } + + /** + * Setter for #audio_captioning + * @param {string} audio_captioning + */ + set audio_captioning(audio_captioning) { + if (this.API.checkValidFormat(audio_captioning, regex.CMISInteger) && + this.API.checkValidRange(audio_captioning, regex.text_range)) { + this.#audio_captioning = audio_captioning; } + } - get audio_captioning() { return this.#audio_captioning; } - set audio_captioning(audio_captioning) { - if(this.API.checkValidFormat(audio_captioning, regex.CMISInteger) - && this.API.checkValidRange(audio_captioning, regex.text_range)) { - this.#audio_captioning = audio_captioning; - } - } -} - -export class CMIInteractionsObject extends BaseCMI { - constructor(API) { - super(API); - - this.objectives = new CMIArray({ - API: API, - errorCode: 404, - children: constants.objectives_children - }); - this.correct_responses = new CMIArray({ - API: API, - errorCode: 404, - children: constants.correct_responses_children - }); - } - - #id: ""; - #type: ""; - #timestamp: ""; - #weighting: ""; - #learner_response: ""; - #result: ""; - #latency: ""; - #description: ""; - - get id() { return this.#id; } - set id(id) { - if(this.API.checkValidFormat(id, regex.CMILongIdentifier)) { - this.#id = id; - } - } - - get type() { return this.#type; } - set type(type) { - if(this.API.checkValidFormat(type, regex.CMIType)) { - this.#type = type; - } - } - - get timestamp() { return this.#timestamp; } - set timestamp(timestamp) { - if(this.API.checkValidFormat(timestamp, regex.CMITime)) { - this.#timestamp = timestamp; - } - } - - get weighting() { return this.#weighting; } - set weighting(weighting) { - if (this.API.checkValidFormat(weighting, regex.CMIDecimal)) { - this.#weighting = weighting; - } - } - - get learner_response() { return this.#learner_response; } - set learner_response(learner_response) { - if(typeof this.type === 'undefined') { - this.API.throwSCORMError(this.API.error.DEPENDENCY_NOT_ESTABLISHED); - } else { - let nodes = []; - let response_type = learner_responses[this.type]; - if(response_type.delimiter !== '') { - nodes = learner_response.split(response_type.delimiter); - } else { - nodes[0] = learner_response; - } - - if((nodes.length > 0) && (nodes.length <= response_type.max)) { - const formatRegex = new RegExp(response_type.format); - for(let i = 0; (i < nodes.length) && (this.API.lastErrorCode === 0); i++) { - if(typeof response_type.delimiter2 !== 'undefined') { - let values = nodes[i].split(response_type.delimiter2); - if(values.length === 2) { - if(!values[0].match(formatRegex)) { - this.API.throwSCORMError(this.API.error.TYPE_MISMATCH); - } else { - if(!values[1].match(new RegExp(response_type.format2))) { - this.API.throwSCORMError(this.API.error.TYPE_MISMATCH); - } - } - } else { - this.API.throwSCORMError(this.API.error.TYPE_MISMATCH); - } - } else { - if(!nodes[i].match(formatRegex)) { - this.API.throwSCORMError(this.API.error.TYPE_MISMATCH); - } else { - if(nodes[i] !== '' && response_type.unique) { - for(let j = 0; (j < i) && this.API.lastErrorCode === 0; j++) { - if(nodes[i] === nodes[j]) { - this.API.throwSCORMError(this.API.error.TYPE_MISMATCH); - } - } - } - } - } - } - } else { - this.API.throwSCORMError(this.API.error.GENERAL_SET_FAILURE); - } - } - } - - get result() { return this.#result; } - set result(result) { - if(this.API.checkValidFormat(result, regex.CMIResult)) { - this.#result = result; - } - } - - get latency() { return this.#latency; } - set latency(latency) { - if(this.API.checkValidFormat(latency, regex.CMITimespan)) { - this.#latency = latency; - } - } - - get description() { return this.#description; } - set description(description) { - if(this.API.checkValidFormat(description, regex.CMILangString250)) { - this.#description = description; - } - } -} - -export class CMIObjectivesObject extends BaseCMI { - constructor(API) { - super(API); - } - - #id: ""; - #success_status: "unknown"; // Allowed values: "passed", "failed", "unknown" - #completion_status: "unknown"; // Allowed values: "completed", "incomplete", "not attempted", "unknown" - #progress_measure: ""; // Data type: real (10,7). Range: 0.0 to 1.0 - #description: ""; // SPM 250 characters - - get id() { return this.#id; } - set id(id) { - if(this.API.checkValidFormat(id, regex.CMILongIdentifier)) { - this.#id = id; - } - } - - get success_status() { return this.#success_status; } - set success_status(success_status) { - if(this.API.checkValidFormat(success_status, regex.CMISStatus)) { - this.#success_status = success_status; - } - } - - get completion_status() { return this.#completion_status; } - set completion_status(completion_status) { - if(this.API.checkValidFormat(completion_status, regex.CMICStatus)) { - this.#completion_status = completion_status; - } - } - - get progress_measure() { return this.#progress_measure; } - set progress_measure(progress_measure) { - if(this.API.checkValidFormat(progress_measure, regex.CMIDecimal) - && this.API.checkValidRange(progress_measure, regex.progress_range)) { - this.#progress_measure = progress_measure; - } - } - - get description() { return this.#description; } - set description(description) { - if(this.API.checkValidFormat(description, regex.CMILangString250)) { - this.#description = description; - } - } - - score = new Scorm2004CMIScore(this.API); -} - -class Scorm2004CMIScore extends CMIScore { - constructor(API) { - super(API, constants.score_children); - this.max = ""; - } - - #scaled: ""; - - get scaled() { return this.#scaled; } - set scaled(scaled) { - if(this.API.checkValidFormat(scaled, regex.CMIDecimal) - && this.API.checkValidRange(scaled, regex.scaled_range)) { - this.#scaled = scaled; - } - } -} - -export class CMICommentsFromLearnerObject extends BaseCMI { - constructor(API) { - super(API); - } - #comment = ""; - #location = ""; - #timestamp = ""; - - get comment() { return this.#comment; } - set comment(comment) { - if(this.API.checkValidFormat(comment, regex.CMILangString4000)) { - this.#comment = comment; - } - } - - get location() { return this.#location; } - set location(location) { - if(this.API.checkValidFormat(location, regex.CMIString250)) { - this.#location = location; - } - } - - get timestamp() { return this.#timestamp; } - set timestamp(timestamp) { - if(this.API.checkValidFormat(timestamp, regex.CMITime)) { - this.#timestamp = timestamp; - } - } -} - -export class CMICommentsFromLMSObject extends CMICommentsFromLearnerObject { - constructor(API) { - super(API); - } - - set comment(comment) { this.API.isNotInitialized() ? this.comment = comment : throwReadOnlyError(); } - - set location(location) { this.API.isNotInitialized() ? this.location = location : throwReadOnlyError(); } - - set timestamp(timestamp) { this.API.isNotInitialized() ? this.timestamp = timestamp : throwReadOnlyError(); } -} - -export class CMIInteractionsObjectivesObject extends BaseCMI { - constructor(API) { - super(API); - } - - #id: ""; - - get id() { return this.#id; } - set id(id) { - if(this.API.checkValidFormat(id, regex.CMILongIdentifier)) { - this.#id = id; - } - } -} - -export class CMIInteractionsCorrectResponsesObject extends BaseCMI { - constructor(API) { - super(API); - } - - #pattern: ""; - - get pattern() { return this.#pattern; } - set pattern(pattern) { - if(this.API.checkValidFormat(pattern, regex.CMIFeedback)) { - this.#pattern = pattern; - } - } -} - -export class ADL extends BaseCMI { - constructor(API) { - super(API); - } - - nav = new class extends BaseCMI { - constructor(API) { - super(API); - } - - #request = "_none_"; // Allowed values: "continue", "previous", "choice", "jump", "exit", "exitAll", "abandon", "abandonAll", "_none_" - - get request() { return this.#request; } - set request(request) { - if(this.API.checkValidFormat(request, regex.NAVEvent)) { - this.#request = request; - } - } - - request_valid = new class extends BaseCMI { - constructor(API) { - super(API); - } - - #continue = "unknown"; // Allowed values: "true", "false", "unknown" - #previous = "unknown"; // Allowed values: "true", "false", "unknown" - - get continue() { return this.#continue; } - set continue(_) { API.throwSCORMError(404); } - - get previous() { return this.#previous; } - set previous(_) { API.throwSCORMError(404); } - - choice = class { - _isTargetValid = (_target) => "unknown"; - }; - - jump = class { - _isTargetValid = (_target) => "unknown"; - }; - } + /** + * toJSON for cmi.learner_preference + * + * @return { + * { + * audio_level: string, + * language: string, + * delivery_speed: string, + * audio_captioning: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'audio_level': this.audio_level, + 'language': this.language, + 'delivery_speed': this.delivery_speed, + 'audio_captioning': this.audio_captioning, }; + delete this.jsonString; + return result; + } +} + +/** + * Class representing SCORM 2004's cmi.interactions object + */ +class CMIInteractions extends CMIArray { + /** + * Constructor for cmi.objectives Array + * @param {Scorm2004API} API + */ + constructor(API) { + super({ + API: API, + children: constants.objectives_children, + errorCode: scorm2004_error_codes.READ_ONLY_ELEMENT, + }); + } +} + +/** + * Class representing SCORM 2004's cmi.objectives object + */ +class CMIObjectives extends CMIArray { + /** + * Constructor for cmi.objectives Array + * @param {Scorm2004API} API + */ + constructor(API) { + super({ + API: API, + children: constants.objectives_children, + errorCode: scorm2004_error_codes.READ_ONLY_ELEMENT, + }); + } +} + +/** + * Class representing SCORM 2004's cmi.comments_from_lms object + */ +class CMICommentsFromLMS extends CMIArray { + /** + * Constructor for cmi.comments_from_lms Array + * @param {Scorm2004API} API + */ + constructor(API) { + super({ + API: API, + children: constants.comments_children, + errorCode: scorm2004_error_codes.READ_ONLY_ELEMENT, + }); + } +} + +/** + * Class representing SCORM 2004's cmi.comments_from_learner object + */ +class CMICommentsFromLearner extends CMIArray { + /** + * Constructor for cmi.comments_from_learner Array + * @param {Scorm2004API} API + */ + constructor(API) { + super({ + API: API, + children: constants.comments_children, + errorCode: scorm2004_error_codes.READ_ONLY_ELEMENT, + }); + } +} + +/** + * Class for SCORM 2004's cmi.interaction.n object + */ +export class CMIInteractionsObject extends BaseCMI { + /** + * Constructor for cmi.interaction.n + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); + + this.objectives = new CMIArray({ + API: API, + errorCode: scorm2004_error_codes.READ_ONLY_ELEMENT, + children: constants.objectives_children, + }); + this.correct_responses = new CMIArray({ + API: API, + errorCode: scorm2004_error_codes.READ_ONLY_ELEMENT, + children: constants.correct_responses_children, + }); + } + + #id = ''; + #type = ''; + #timestamp = ''; + #weighting = ''; + #learner_response = ''; + #result = ''; + #latency = ''; + #description = ''; + + /** + * Getter for #id + * @return {string} + */ + get id() { + return this.#id; + } + + /** + * Setter for #id + * @param {string} id + */ + set id(id) { + if (this.API.checkValidFormat(id, regex.CMILongIdentifier)) { + this.#id = id; + } + } + + /** + * Getter for #type + * @return {string} + */ + get type() { + return this.#type; + } + + /** + * Setter for #type + * @param {string} type + */ + set type(type) { + if (this.API.checkValidFormat(type, regex.CMIType)) { + this.#type = type; + } + } + + /** + * Getter for #timestamp + * @return {string} + */ + get timestamp() { + return this.#timestamp; + } + + /** + * Setter for #timestamp + * @param {string} timestamp + */ + set timestamp(timestamp) { + if (this.API.checkValidFormat(timestamp, regex.CMITime)) { + this.#timestamp = timestamp; + } + } + + /** + * Getter for #weighting + * @return {string} + */ + get weighting() { + return this.#weighting; + } + + /** + * Setter for #weighting + * @param {string} weighting + */ + set weighting(weighting) { + if (this.API.checkValidFormat(weighting, regex.CMIDecimal)) { + this.#weighting = weighting; + } + } + + /** + * Getter for #learner_response + * @return {string} + */ + get learner_response() { + return this.#learner_response; + } + + /** + * Setter for #learner_response. Does type validation to make sure response + * matches SCORM 2004's spec + * @param {string} learner_response + */ + set learner_response(learner_response) { + if (typeof this.type === 'undefined') { + this.API.throwSCORMError(this.API.error.DEPENDENCY_NOT_ESTABLISHED); + } else { + let nodes = []; + const response_type = learner_responses[this.type]; + if (response_type.delimiter !== '') { + nodes = learner_response.split(response_type.delimiter); + } else { + nodes[0] = learner_response; + } + + if ((nodes.length > 0) && (nodes.length <= response_type.max)) { + const formatRegex = new RegExp(response_type.format); + for (let i = 0; (i < nodes.length) && + (this.API.lastErrorCode === 0); i++) { + if (typeof response_type.delimiter2 !== 'undefined') { + const values = nodes[i].split(response_type.delimiter2); + if (values.length === 2) { + if (!values[0].match(formatRegex)) { + throwTypeMismatchError(this.API); + } else { + if (!values[1].match(new RegExp(response_type.format2))) { + throwTypeMismatchError(this.API); + } + } + } else { + throwTypeMismatchError(this.API); + } + } else { + if (!nodes[i].match(formatRegex)) { + throwTypeMismatchError(this.API); + } else { + if (nodes[i] !== '' && response_type.unique) { + for (let j = 0; (j < i) && this.API.lastErrorCode === 0; j++) { + if (nodes[i] === nodes[j]) { + throwTypeMismatchError(this.API); + } + } + } + } + } + } + } else { + this.API.throwSCORMError(this.API.error.GENERAL_SET_FAILURE); + } + } + } + + /** + * Getter for #result + * @return {string} + */ + get result() { + return this.#result; + } + + /** + * Setter for #result + * @param {string} result + */ + set result(result) { + if (this.API.checkValidFormat(result, regex.CMIResult)) { + this.#result = result; + } + } + + /** + * Getter for #latency + * @return {string} + */ + get latency() { + return this.#latency; + } + + /** + * Setter for #latency + * @param {string} latency + */ + set latency(latency) { + if (this.API.checkValidFormat(latency, regex.CMITimespan)) { + this.#latency = latency; + } + } + + /** + * Getter for #description + * @return {string} + */ + get description() { + return this.#description; + } + + /** + * Setter for #description + * @param {string} description + */ + set description(description) { + if (this.API.checkValidFormat(description, regex.CMILangString250)) { + this.#description = description; + } + } + + /** + * toJSON for cmi.interactions.n + * + * @return { + * { + * id: string, + * type: string, + * objectives: CMIArray, + * timestamp: string, + * correct_responses: CMIArray, + * weighting: string, + * learner_response: string, + * result: string, + * latency: string, + * description: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'id': this.id, + 'type': this.type, + 'objectives': this.objectives, + 'timestamp': this.timestamp, + 'weighting': this.weighting, + 'learner_response': this.learner_response, + 'result': this.result, + 'latency': this.latency, + 'description': this.description, + 'correct_responses': this.correct_responses, + }; + delete this.jsonString; + return result; + } +} + +/** + * Class for SCORM 2004's cmi.objectives.n object + */ +export class CMIObjectivesObject extends BaseCMI { + /** + * Constructor for cmi.objectives.n + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); + + this.score = new Scorm2004CMIScore(API); + } + + #id = ''; + #success_status = 'unknown'; + #completion_status = 'unknown'; + #progress_measure = ''; + #description = ''; + + /** + * Getter for #id + * @return {string} + */ + get id() { + return this.#id; + } + + /** + * Setter for #id + * @param {string} id + */ + set id(id) { + if (this.API.checkValidFormat(id, regex.CMILongIdentifier)) { + this.#id = id; + } + } + + /** + * Getter for #success_status + * @return {string} + */ + get success_status() { + return this.#success_status; + } + + /** + * Setter for #success_status + * @param {string} success_status + */ + set success_status(success_status) { + if (this.API.checkValidFormat(success_status, regex.CMISStatus)) { + this.#success_status = success_status; + } + } + + /** + * Getter for #completion_status + * @return {string} + */ + get completion_status() { + return this.#completion_status; + } + + /** + * Setter for #completion_status + * @param {string} completion_status + */ + set completion_status(completion_status) { + if (this.API.checkValidFormat(completion_status, regex.CMICStatus)) { + this.#completion_status = completion_status; + } + } + + /** + * Getter for #progress_measure + * @return {string} + */ + get progress_measure() { + return this.#progress_measure; + } + + /** + * Setter for #progress_measure + * @param {string} progress_measure + */ + set progress_measure(progress_measure) { + if (this.API.checkValidFormat(progress_measure, regex.CMIDecimal) && + this.API.checkValidRange(progress_measure, regex.progress_range)) { + this.#progress_measure = progress_measure; + } + } + + /** + * Getter for #description + * @return {string} + */ + get description() { + return this.#description; + } + + /** + * Setter for #description + * @param {string} description + */ + set description(description) { + if (this.API.checkValidFormat(description, regex.CMILangString250)) { + this.#description = description; + } + } + + /** + * toJSON for cmi.objectives.n + * + * @return { + * { + * id: string, + * success_status: string, + * completion_status: string, + * progress_measure: string, + * description: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'id': this.id, + 'success_status': this.success_status, + 'completion_status': this.completion_status, + 'progress_measure': this.progress_measure, + 'description': this.description, + }; + delete this.jsonString; + return result; + } +} + +/** + * Class for SCORM 2004's cmi *.score object + */ +class Scorm2004CMIScore extends CMIScore { + /** + * Constructor for cmi *.score + * @param {Scorm2004API} API + */ + constructor(API) { + super(API, constants.score_children); + + this.max = ''; + } + + #scaled = ''; + + /** + * Getter for #scaled + * @return {string} + */ + get scaled() { + return this.#scaled; + } + + /** + * Setter for #scaled + * @param {string} scaled + */ + set scaled(scaled) { + if (this.API.checkValidFormat(scaled, regex.CMIDecimal) && + this.API.checkValidRange(scaled, regex.scaled_range)) { + this.#scaled = scaled; + } + } + + /** + * toJSON for cmi *.score + * + * @return { + * { + * scaled: string, + * raw: string, + * min: string, + * max: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'scaled': this.scaled, + 'raw': this.raw, + 'min': this.min, + 'max': this.max, + }; + delete this.jsonString; + return result; + } +} + +/** + * Class representing SCORM 2004's cmi.comments_from_learner.n object + */ +export class CMICommentsFromLearnerObject extends BaseCMI { + /** + * Constructor for cmi.comments_from_learner.n + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); + } + + #comment = ''; + #location = ''; + #timestamp = ''; + + /** + * Getter for #comment + * @return {string} + */ + get comment() { + return this.#comment; + } + + /** + * Setter for #comment + * @param {string} comment + */ + set comment(comment) { + if (this.API.checkValidFormat(comment, regex.CMILangString4000)) { + this.#comment = comment; + } + } + + /** + * Getter for #location + * @return {string} + */ + get location() { + return this.#location; + } + + /** + * Setter for #location + * @param {string} location + */ + set location(location) { + if (this.API.checkValidFormat(location, regex.CMIString250)) { + this.#location = location; + } + } + + /** + * Getter for #timestamp + * @return {string} + */ + get timestamp() { + return this.#timestamp; + } + + /** + * Setter for #timestamp + * @param {string} timestamp + */ + set timestamp(timestamp) { + if (this.API.checkValidFormat(timestamp, regex.CMITime)) { + this.#timestamp = timestamp; + } + } +} + +/** + * Class representing SCORM 2004's cmi.comments_from_lms.n object + */ +export class CMICommentsFromLMSObject extends CMICommentsFromLearnerObject { + /** + * Constructor for cmi.comments_from_lms.n + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); + } + + /** + * Setter for #comment. Can only be called before API initialization. + * @param {string} comment + */ + set comment(comment) { + this.API.isNotInitialized() ? + this.comment = comment : + throwReadOnlyError(this.API); + } + + /** + * Setter for #location. Can only be called before API initialization. + * @param {string} location + */ + set location(location) { + this.API.isNotInitialized() ? + this.location = location : + throwReadOnlyError(this.API); + } + + /** + * Setter for #timestamp. Can only be called before API initialization. + * @param {string} timestamp + */ + set timestamp(timestamp) { + this.API.isNotInitialized() ? + this.timestamp = timestamp : + throwReadOnlyError(this.API); + } +} + +/** + * Class representing SCORM 2004's cmi.interactions.n.objectives.n object + */ +export class CMIInteractionsObjectivesObject extends BaseCMI { + /** + * Constructor for cmi.interactions.n.objectives.n + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); + } + + #id = ''; + + /** + * Getter for #id + * @return {string} + */ + get id() { + return this.#id; + } + + /** + * Setter for #id + * @param {string} id + */ + set id(id) { + if (this.API.checkValidFormat(id, regex.CMILongIdentifier)) { + this.#id = id; + } + } +} + +/** + * Class representing SCORM 2004's cmi.interactions.n.correct_responses.n object + */ +export class CMIInteractionsCorrectResponsesObject extends BaseCMI { + /** + * Constructor for cmi.interactions.n.correct_responses.n + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); + } + + #pattern = ''; + + /** + * Getter for #pattern + * @return {string} + */ + get pattern() { + return this.#pattern; + } + + /** + * Setter for #pattern + * @param {string} pattern + */ + set pattern(pattern) { + if (this.API.checkValidFormat(pattern, regex.CMIFeedback)) { + this.#pattern = pattern; + } + } +} + +/** + * Class representing SCORM 2004's adl object + */ +export class ADL extends BaseCMI { + /** + * Constructor for adl + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); + + this.nav = new class extends BaseCMI { + /** + * Constructor for adl.nav + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); + + this.request_valid = new class extends BaseCMI { + /** + * Constructor for adl.nav.request_valid + * @param {Scorm2004API} API + */ + constructor(API) { + super(API); + } + + #continue = 'unknown'; + #previous = 'unknown'; + + /** + * Getter for #continue + * @return {string} + */ + get continue() { + return this.#continue; + } + + /** + * Setter for #continue. Just throws an error. + * @param {*} _ + */ + set continue(_) { + throwReadOnlyError(this.API); + } + + /** + * Getter for #previous + * @return {string} + */ + get previous() { + return this.#previous; + } + + /** + * Setter for #previous. Just throws an error. + * @param {*} _ + */ + set previous(_) { + throwReadOnlyError(this.API); + } + + choice = class { + _isTargetValid = (_target) => 'unknown'; + }(); + + jump = class { + _isTargetValid = (_target) => 'unknown'; + }(); + + /** + * toJSON for adl.nav.request_valid + * + * @return { + * { + * previous: string, + * continue: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'previous': this.previous, + 'continue': this.continue, + }; + delete this.jsonString; + return result; + } + }(API); + } + + #request = '_none_'; + + /** + * Getter for #request + * @return {string} + */ + get request() { + return this.#request; + } + + /** + * Setter for #request + * @param {string} request + */ + set request(request) { + if (this.API.checkValidFormat(request, regex.NAVEvent)) { + this.#request = request; + } + } + + /** + * toJSON for adl.nav + * + * @return { + * { + * request: string + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'request': this.request, + }; + delete this.jsonString; + return result; + } + }(API); + } + + /** + * toJSON for adl + * @return { + * { + * nav: { + * request: string + * } + * } + * } + */ + toJSON() { + this.jsonString = true; + const result = { + 'nav': this.nav, + }; + delete this.jsonString; + return result; + } } diff --git a/src/constants/api_constants.js b/src/constants/api_constants.js index 998be0a..0daead5 100644 --- a/src/constants/api_constants.js +++ b/src/constants/api_constants.js @@ -30,7 +30,7 @@ export const scorm12_constants = { }, '301': { basicMessage: 'Not initialized', - detailMessage: 'Indicates that an API call was made before the call to LMSInitialize.', + detailMessage: 'Indicates that an API call was made before the call to lmsInitialize.', }, '401': { basicMessage: 'Not implemented error', diff --git a/src/constants/language_constants.js b/src/constants/language_constants.js index d31e65e..099bb0f 100644 --- a/src/constants/language_constants.js +++ b/src/constants/language_constants.js @@ -71,4 +71,4 @@ export const valid_languages = { 'ukr': 'ukr', 'urd': 'urd', 'uzb': 'uzb', 'ven': 'ven', 'vie': 'vie', 'vol': 'vol', 'wln': 'wln', 'wol': 'wol', 'xho': 'xho', 'yid': 'yid', 'yor': 'yor', 'zha': 'zha', 'chi': 'chi', 'zho': 'zho', 'zul': 'zul', -}; \ No newline at end of file +}; diff --git a/src/regex.js b/src/regex.js index f9c32af..47359b0 100644 --- a/src/regex.js +++ b/src/regex.js @@ -1,76 +1,76 @@ // @flow export const scorm12_regex = { - CMIString256: '^.{0,255}$', - CMIString4096: '^.{0,4096}$', - CMITime: '^([0-2]{1}[0-9]{1}):([0-5]{1}[0-9]{1}):([0-5]{1}[0-9]{1})(\.[0-9]{1,6})?$', - CMITimespan: '^([0-9]{2,4}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,2})?$', - CMIInteger: '^\\d+$', - CMISInteger: '^-?([0-9]+)$', - CMIDecimal: '^-?([0-9]{0,3})(\.[0-9]*)?$', - CMIIdentifier: '^[\\u0021-\\u007E]{0,255}$', - CMIFeedback: '^.{0,255}$', // This must be redefined - CMIIndex: '[._](\\d+).', + CMIString256: '^.{0,255}$', + CMIString4096: '^.{0,4096}$', + CMITime: '^([0-2]{1}[0-9]{1}):([0-5]{1}[0-9]{1}):([0-5]{1}[0-9]{1})(\.[0-9]{1,6})?$', // eslint-disable-line + CMITimespan: '^([0-9]{2,}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,2})?$', // eslint-disable-line + CMIInteger: '^\\d+$', + CMISInteger: '^-?([0-9]+)$', + CMIDecimal: '^-?([0-9]{0,3})(\.[0-9]*)?$', // eslint-disable-line + CMIIdentifier: '^[\\u0021-\\u007E]{0,255}$', + CMIFeedback: '^.{0,255}$', // This must be redefined + CMIIndex: '[._](\\d+).', - // Vocabulary Data Type Definition - CMIStatus: '^passed$|^completed$|^failed$|^incomplete$|^browsed$', - CMIStatus2: '^passed$|^completed$|^failed$|^incomplete$|^browsed$|^not attempted$', - CMIExit: '^time-out$|^suspend$|^logout$|^$', - CMIType: '^true-false$|^choice$|^fill-in$|^matching$|^performance$|^sequencing$|^likert$|^numeric$', - CMIResult: '^correct$|^wrong$|^unanticipated$|^neutral$|^([0-9]{0,3})?(\.[0-9]*)?$', - NAVEvent: '^previous$|^continue$', + // Vocabulary Data Type Definition + CMIStatus: '^passed$|^completed$|^failed$|^incomplete$|^browsed$', + CMIStatus2: '^passed$|^completed$|^failed$|^incomplete$|^browsed$|^not attempted$', + CMIExit: '^time-out$|^suspend$|^logout$|^$', + CMIType: '^true-false$|^choice$|^fill-in$|^matching$|^performance$|^sequencing$|^likert$|^numeric$', + CMIResult: '^correct$|^wrong$|^unanticipated$|^neutral$|^([0-9]{0,3})?(\.[0-9]*)?$', // eslint-disable-line + NAVEvent: '^previous$|^continue$', - // Data ranges - score_range: '0#100', - audio_range: '-1#100', - speed_range: '-100#100', - weighting_range: '-100#100', - text_range: '-1#1', + // Data ranges + score_range: '0#100', + audio_range: '-1#100', + speed_range: '-100#100', + weighting_range: '-100#100', + text_range: '-1#1', }; export const aicc_regex = { - ...scorm12_regex, ...{ - CMIIdentifier: '^\\w{1,255}$', - } + ...scorm12_regex, ...{ + CMIIdentifier: '^\\w{1,255}$', + }, }; export const scorm2004_regex = { - CMIString200: '^[\\u0000-\\uFFFF]{0,200}$', - CMIString250: '^[\\u0000-\\uFFFF]{0,250}$', - CMIString1000: '^[\\u0000-\\uFFFF]{0,1000}$', - CMIString4000: '^[\\u0000-\\uFFFF]{0,4000}$', - CMIString64000: '^[\\u0000-\\uFFFF]{0,64000}$', - CMILang: '^([a-zA-Z]{2,3}|i|x)(\-[a-zA-Z0-9\-]{2,8})?$|^$', - CMILangString250: '^(\{lang=([a-zA-Z]{2,3}|i|x)(\-[a-zA-Z0-9\-]{2,8})?\})?([^\{].{0,250}$)?', - CMILangcr: '^((\{lang=([a-zA-Z]{2,3}|i|x)?(\-[a-zA-Z0-9\-]{2,8})?\}))(.*?)$', - CMILangString250cr: '^((\{lang=([a-zA-Z]{2,3}|i|x)?(\-[a-zA-Z0-9\-]{2,8})?\})?(.{0,250})?)?$', - CMILangString4000: '^(\{lang=([a-zA-Z]{2,3}|i|x)(\-[a-zA-Z0-9\-]{2,8})?\})?([^\{].{0,4000}$)?', - CMITime: '^(19[7-9]{1}[0-9]{1}|20[0-2]{1}[0-9]{1}|203[0-8]{1})((-(0[1-9]{1}|1[0-2]{1}))((-(0[1-9]{1}|[1-2]{1}[0-9]{1}|3[0-1]{1}))(T([0-1]{1}[0-9]{1}|2[0-3]{1})((:[0-5]{1}[0-9]{1})((:[0-5]{1}[0-9]{1})((\\.[0-9]{1,2})((Z|([+|-]([0-1]{1}[0-9]{1}|2[0-3]{1})))(:[0-5]{1}[0-9]{1})?)?)?)?)?)?)?)?$', - CMITimespan: '^P(?:([.,\\d]+)Y)?(?:([.,\\d]+)M)?(?:([.,\\d]+)W)?(?:([.,\\d]+)D)?(?:T?(?:([.,\\d]+)H)?(?:([.,\\d]+)M)?(?:([.,\\d]+)S)?)?$', - CMIInteger: '^\\d+$', - CMISInteger: '^-?([0-9]+)$', - CMIDecimal: '^-?([0-9]{1,5})(\\.[0-9]{1,18})?$', - CMIIdentifier: '^\\S{1,250}[a-zA-Z0-9]$', - CMIShortIdentifier: '^[\\w\.]{1,250}$', - CMILongIdentifier: '^(?:(?!urn:)\\S{1,4000}|urn:[A-Za-z0-9-]{1,31}:\\S{1,4000})$', - CMIFeedback: '^.*$', // This must be redefined - CMIIndex: '[._](\\d+).', - CMIIndexStore: '.N(\\d+).', + CMIString200: '^[\\u0000-\\uFFFF]{0,200}$', + CMIString250: '^[\\u0000-\\uFFFF]{0,250}$', + CMIString1000: '^[\\u0000-\\uFFFF]{0,1000}$', + CMIString4000: '^[\\u0000-\\uFFFF]{0,4000}$', + CMIString64000: '^[\\u0000-\\uFFFF]{0,64000}$', + CMILang: '^([a-zA-Z]{2,3}|i|x)(\-[a-zA-Z0-9\-]{2,8})?$|^$', // eslint-disable-line + CMILangString250: '^(\{lang=([a-zA-Z]{2,3}|i|x)(\-[a-zA-Z0-9\-]{2,8})?\})?([^\{].{0,250}$)?', // eslint-disable-line + CMILangcr: '^((\{lang=([a-zA-Z]{2,3}|i|x)?(\-[a-zA-Z0-9\-]{2,8})?\}))(.*?)$', // eslint-disable-line + CMILangString250cr: '^((\{lang=([a-zA-Z]{2,3}|i|x)?(\-[a-zA-Z0-9\-]{2,8})?\})?(.{0,250})?)?$', // eslint-disable-line + CMILangString4000: '^(\{lang=([a-zA-Z]{2,3}|i|x)(\-[a-zA-Z0-9\-]{2,8})?\})?([^\{].{0,4000}$)?', // eslint-disable-line + CMITime: '^(19[7-9]{1}[0-9]{1}|20[0-2]{1}[0-9]{1}|203[0-8]{1})((-(0[1-9]{1}|1[0-2]{1}))((-(0[1-9]{1}|[1-2]{1}[0-9]{1}|3[0-1]{1}))(T([0-1]{1}[0-9]{1}|2[0-3]{1})((:[0-5]{1}[0-9]{1})((:[0-5]{1}[0-9]{1})((\\.[0-9]{1,2})((Z|([+|-]([0-1]{1}[0-9]{1}|2[0-3]{1})))(:[0-5]{1}[0-9]{1})?)?)?)?)?)?)?)?$', + CMITimespan: '^P(?:([.,\\d]+)Y)?(?:([.,\\d]+)M)?(?:([.,\\d]+)W)?(?:([.,\\d]+)D)?(?:T?(?:([.,\\d]+)H)?(?:([.,\\d]+)M)?(?:([.,\\d]+)S)?)?$', + CMIInteger: '^\\d+$', + CMISInteger: '^-?([0-9]+)$', + CMIDecimal: '^-?([0-9]{1,5})(\\.[0-9]{1,18})?$', + CMIIdentifier: '^\\S{1,250}[a-zA-Z0-9]$', + CMIShortIdentifier: '^[\\w\.]{1,250}$', // eslint-disable-line + CMILongIdentifier: '^(?:(?!urn:)\\S{1,4000}|urn:[A-Za-z0-9-]{1,31}:\\S{1,4000})$', + CMIFeedback: '^.*$', // This must be redefined + CMIIndex: '[._](\\d+).', + CMIIndexStore: '.N(\\d+).', - // Vocabulary Data Type Definition - CMICStatus: '^completed$|^incomplete$|^not attempted$|^unknown$', - CMISStatus: '^passed$|^failed$|^unknown$', - CMIExit: '^time-out$|^suspend$|^logout$|^normal$|^$', - CMIType: '^true-false$|^choice$|^(long-)?fill-in$|^matching$|^performance$|^sequencing$|^likert$|^numeric$|^other$', - CMIResult: '^correct$|^incorrect$|^unanticipated$|^neutral$|^-?([0-9]{1,4})(\\.[0-9]{1,18})?$', - NAVEvent: '^previous$|^continue$|^exit$|^exitAll$|^abandon$|^abandonAll$|^suspendAll$|^\{target=\\S{0,200}[a-zA-Z0-9]\}choice|jump$', - NAVBoolean: '^unknown$|^true$|^false$', - NAVTarget: '^previous$|^continue$|^choice.{target=\\S{0,200}[a-zA-Z0-9]}$', + // Vocabulary Data Type Definition + CMICStatus: '^completed$|^incomplete$|^not attempted$|^unknown$', + CMISStatus: '^passed$|^failed$|^unknown$', + CMIExit: '^time-out$|^suspend$|^logout$|^normal$|^$', + CMIType: '^true-false$|^choice$|^(long-)?fill-in$|^matching$|^performance$|^sequencing$|^likert$|^numeric$|^other$', + CMIResult: '^correct$|^incorrect$|^unanticipated$|^neutral$|^-?([0-9]{1,4})(\\.[0-9]{1,18})?$', + NAVEvent: '^previous$|^continue$|^exit$|^exitAll$|^abandon$|^abandonAll$|^suspendAll$|^\{target=\\S{0,200}[a-zA-Z0-9]\}choice|jump$', // eslint-disable-line + NAVBoolean: '^unknown$|^true$|^false$', + NAVTarget: '^previous$|^continue$|^choice.{target=\\S{0,200}[a-zA-Z0-9]}$', - // Data ranges - scaled_range: '-1#1', - audio_range: '0#*', - speed_range: '0#*', - text_range: '-1#1', - progress_range: '0#1', -}; \ No newline at end of file + // Data ranges + scaled_range: '-1#1', + audio_range: '0#*', + speed_range: '0#*', + text_range: '-1#1', + progress_range: '0#1', +}; diff --git a/src/utilities.js b/src/utilities.js index 11fad7f..9e58dbc 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -1,5 +1,5 @@ // @flow -export const SECONDS_PER_SECOND = 1; +export const SECONDS_PER_SECOND = 1.0; export const SECONDS_PER_MINUTE = 60; export const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE; export const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR; @@ -27,11 +27,12 @@ export function getSecondsAsHHMMSS(totalSeconds: Number) { const dateObj = new Date(totalSeconds * 1000); const minutes = dateObj.getUTCMinutes(); - const seconds = dateObj.getSeconds(); + // make sure we add any possible decimal value + const seconds = dateObj.getSeconds() + (totalSeconds % 1.0); return hours.toString().padStart(2, '0') + ':' + - minutes.toString().padStart(2, '0') + ':' + - seconds.toString().padStart(2, '0'); + minutes.toString().padStart(2, '0') + ':' + + seconds.toString().padStart(2, '0'); } /** @@ -50,9 +51,14 @@ export function getSecondsAsISODuration(seconds: Number) { let remainder = seconds; designations.forEach(([sign, current_seconds]) => { - const value = Math.floor(remainder / current_seconds); + let value = Math.floor(remainder / current_seconds); remainder = remainder % current_seconds; + // If we have anything left in the remainder, and we're currently adding + // seconds to the duration, go ahead and add the decimal to the seconds + if (sign === 'S' && remainder > 0) { + value += remainder; + } if (value) { duration += `${value}${sign}`; @@ -104,10 +110,45 @@ export function getDurationAsSeconds(duration: String, durationRegex: RegExp) { anchor.setHours(anchor.getHours() + Number(hours || 0)); anchor.setMinutes(anchor.getMinutes() + Number(minutes || 0)); anchor.setSeconds(anchor.getSeconds() + Number(seconds || 0)); - if (seconds) { + if (seconds && String(seconds).indexOf('.') > 0) { const milliseconds = Number(Number(seconds) % 1).toFixed(6) * 1000.0; anchor.setMilliseconds(anchor.getMilliseconds() + milliseconds); } - return (anchor - now) / 1000.0; + return ((anchor * 1.0) - now) / 1000.0; +} + +/** + * Adds together two ISO8601 Duration strings + * + * @param {string} first + * @param {string} second + * @param {RegExp} durationRegex + * @return {string} + */ +export function addTwoDurations( + first: String, + second: String, + durationRegex: RegExp) { + const firstSeconds = getDurationAsSeconds(first, durationRegex); + const secondSeconds = getDurationAsSeconds(second, durationRegex); + + return getSecondsAsISODuration(firstSeconds + secondSeconds); +} + +/** + * Add together two HH:MM:SS.DD strings + * + * @param {string} first + * @param {string} second + * @param {RegExp} timeRegex + * @return {string} + */ +export function addHHMMSSTimeStrings( + first: String, + second: String, + timeRegex: RegExp) { + const firstSeconds = getTimeAsSeconds(first, timeRegex); + const secondSeconds = getTimeAsSeconds(second, timeRegex); + return getSecondsAsHHMMSS(firstSeconds + secondSeconds); } diff --git a/test/Scorm12API.spec.js b/test/Scorm12API.spec.js deleted file mode 100644 index 790a1e7..0000000 --- a/test/Scorm12API.spec.js +++ /dev/null @@ -1,283 +0,0 @@ -import {expect, assert} from 'chai'; -import {describe, it, before, beforeEach, after, afterEach} from 'mocha'; -import Scorm12API from '../src/Scorm12API'; -import {scorm12_constants} from '../src/constants/api_constants'; -import {scorm12_error_codes} from '../src/constants/error_codes'; - -let API; - -const checkFieldConstraintSize = (fieldName: String, limit: Number, expectedValue: String = '') => { - 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: String, expectedValue: String = '') => { - 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: String, expectedValue: String = '') => { - 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: String, expectedValue: String = '') => { - describe(`Field: ${fieldName}`, () => { - it(`Should be able to read from ${fieldName}`, () => { - expect(eval(`API.${fieldName}`)).to.equal(expectedValue); - }); - }); -}; - -const checkWriteOnly = (fieldName: String, valueToTest: String = '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: String, valueToTest: String = 'xxx') => { - describe(`Field: ${fieldName}`, () => { - it(`Should successfully write to ${fieldName}`, () => { - eval(`API.${fieldName} = '${valueToTest}'`); - expect(API.lastErrorCode).to.equal(0); - }); - }); -}; - -const checkValidValues = (fieldName: String, expectedError: Number, validValues: Array, invalidValues: Array) => { - describe(`Field: ${fieldName}`, () => { - for (const idx in validValues) { - it(`Should successfully write '${validValues[idx]}' to ${fieldName}`, () => { - eval(`API.${fieldName} = '${validValues[idx]}'`); - expect(API.lastErrorCode).to.equal(0); - }); - } - - for (const idx in invalidValues) { - it(`Should fail to write '${invalidValues[idx]}' to ${fieldName}`, () => { - eval(`API.${fieldName} = '${invalidValues[idx]}'`); - expect(API.lastErrorCode).to.equal(expectedError + ''); - }); - } - }); -}; - -describe('SCORM 1.2 API 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()) - ).to.hasOwnProperty('core'); - }); - - /** - * Base CMI Properties - */ - checkInvalidSet('cmi._version', '3.4'); - checkInvalidSet('cmi._children', scorm12_constants.cmi_children); - checkFieldConstraintSize('cmi.suspend_data', 4096); - checkReadOnly('cmi.launch_data'); - checkFieldConstraintSize('cmi.comments', 4096); - checkReadOnly('cmi.comments_from_lms'); - - /** - * cmi.core Properties - */ - checkInvalidSet('cmi.core._children', scorm12_constants.core_children); - checkReadOnly('cmi.core.student_id'); - checkReadOnly('cmi.core.student_name'); - checkFieldConstraintSize('cmi.core.lesson_location', 255); - checkReadOnly('cmi.core.credit'); - checkRead('cmi.core.lesson_status'); - checkValidValues('cmi.core.lesson_status', scorm12_error_codes.TYPE_MISMATCH, [ - 'passed', - 'completed', - 'failed', - 'incomplete', - 'browsed', - ], [ - 'Passed', - 'P', - 'F', - 'p', - 'true', - 'false', - 'complete', - ]); - checkReadOnly('cmi.core.entry'); - checkReadOnly('cmi.core.total_time'); - checkReadOnly('cmi.core.lesson_mode', 'normal'); - checkWrite('cmi.core.exit', 'suspend'); - checkValidValues('cmi.core.exit', scorm12_error_codes.TYPE_MISMATCH, [ - 'time-out', - 'suspend', - 'logout', - ], [ - 'complete', - 'exit', - ]); - checkWriteOnly('cmi.core.session_time', '00:00:00'); - checkValidValues('cmi.core.session_time', scorm12_error_codes.TYPE_MISMATCH, [ - '10:06:57', - '00:00:01.56', - '23:59:59', - '47:59:59', - ], [ - '06:5:13', - '23:59:59.123', - 'P1DT23H59M59S', - ]); - - /** - * cmi.core.score Properties - */ - checkInvalidSet('cmi.core.score._children', scorm12_constants.score_children); - checkValidValues('cmi.core.score.raw', scorm12_error_codes.VALUE_OUT_OF_RANGE, [ - '0', - '25.1', - '50.5', - '75', - '100', - ], [ - '-1', - '101', - ]); - checkValidValues('cmi.core.score.min', scorm12_error_codes.VALUE_OUT_OF_RANGE, [ - '0', - '25.1', - '50.5', - '75', - '100', - ], [ - '-1', - '101', - ]); - checkValidValues('cmi.core.score.max', scorm12_error_codes.VALUE_OUT_OF_RANGE, [ - '0', - '25.1', - '50.5', - '75', - '100', - ], [ - '-1', - '101', - ]); - - /** - * cmi.objectives Properties - */ - checkInvalidSet('cmi.objectives._children', scorm12_constants.objectives_children); - checkInvalidSet('cmi.objectives._count', 0); - - /** - * cmi.student_data Properties - */ - checkInvalidSet('cmi.student_data._children', scorm12_constants.student_data_children); - checkReadOnly('cmi.student_data.mastery_score'); - checkReadOnly('cmi.student_data.max_time_allowed'); - checkReadOnly('cmi.student_data.time_limit_action'); - - /** - * cmi.student_preference Properties - */ - checkInvalidSet('cmi.student_preference._children', scorm12_constants.student_preference_children); - checkValidValues('cmi.student_preference.audio', scorm12_error_codes.TYPE_MISMATCH, [ - '1', - '-1', - '50', - '100', - ], [ - 'invalid', - 'a100', - ]); - checkValidValues('cmi.student_preference.audio', scorm12_error_codes.VALUE_OUT_OF_RANGE, [], [ - '101', - '5000000', - '-500', - ]); - checkFieldConstraintSize('cmi.student_preference.language', 255); - checkValidValues('cmi.student_preference.speed', scorm12_error_codes.TYPE_MISMATCH, [ - '1', - '-100', - '50', - '100', - ], [ - 'invalid', - 'a100', - ]); - checkValidValues('cmi.student_preference.speed', scorm12_error_codes.VALUE_OUT_OF_RANGE, [], [ - '101', - '-101', - '5000000', - '-500', - ]); - checkValidValues('cmi.student_preference.text', scorm12_error_codes.TYPE_MISMATCH, [ - '1', - '-1', - ], [ - 'invalid', - 'a100', - ]); - checkValidValues('cmi.student_preference.text', scorm12_error_codes.VALUE_OUT_OF_RANGE, [], [ - '2', - '-2', - ]); - - /** - * cmi.interactions Properties - */ - checkInvalidSet('cmi.interactions._children', scorm12_constants.interactions_children); - checkInvalidSet('cmi.interactions._count', 0); - }); - }); -}); diff --git a/test/cmi/scorm12_cmi.spec.js b/test/cmi/scorm12_cmi.spec.js new file mode 100644 index 0000000..951e75e --- /dev/null +++ b/test/cmi/scorm12_cmi.spec.js @@ -0,0 +1,389 @@ +import {expect, assert} from 'chai'; +import {describe, it, beforeEach, afterEach} from 'mocha'; +import Scorm12API from '../../src/Scorm12API'; +import {scorm12_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 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('SCORM 1.2 API 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; + }); + + /** + * 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}); + 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: scorm12_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: 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, + }); + checkReadOnly({fieldName: 'cmi.student_data.mastery_score'}); + checkReadOnly({fieldName: 'cmi.student_data.max_time_allowed'}); + checkReadOnly({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}); + }); + }); +}); diff --git a/test/utilities.spec.js b/test/utilities.spec.js index 2603aff..3a8dd7e 100644 --- a/test/utilities.spec.js +++ b/test/utilities.spec.js @@ -1,220 +1,277 @@ -import { expect } from 'chai'; -import {describe, it} from "mocha"; -import * as Utilities from '../src/utilities'; -import {scorm12_regex, scorm2004_regex} from "../src/regex"; +import {expect} from 'chai'; +import {describe, it} from 'mocha'; +import * as Utilities from '../src/utilities'; +import {scorm12_regex, scorm2004_regex} from '../src/regex'; describe('Utility Tests', () => { - describe('function getSecondsAsHHMMSS()', () => { - it('10 returns 00:00:10', () => { - expect( - Utilities.getSecondsAsHHMMSS(10) - ).to.equal('00:00:10'); - }); + describe('getSecondsAsHHMMSS()', () => { + it('10 returns 00:00:10', () => { + expect( + Utilities.getSecondsAsHHMMSS(10), + ).to.equal('00:00:10'); + }); - it('60 returns 00:01:00', () => { - expect( - Utilities.getSecondsAsHHMMSS(60) - ).to.equal('00:01:00'); - }); + it('60 returns 00:01:00', () => { + expect( + Utilities.getSecondsAsHHMMSS(60), + ).to.equal('00:01:00'); + }); - it('3600 returns 01:00:00', () => { - expect( - Utilities.getSecondsAsHHMMSS(3600) - ).to.equal('01:00:00'); - }); + it('3600 returns 01:00:00', () => { + expect( + Utilities.getSecondsAsHHMMSS(3600), + ).to.equal('01:00:00'); + }); - it('70 returns 00:01:10', () => { - expect( - Utilities.getSecondsAsHHMMSS(70) - ).to.equal('00:01:10'); - }); + it('70 returns 00:01:10', () => { + expect( + Utilities.getSecondsAsHHMMSS(70), + ).to.equal('00:01:10'); + }); - it('3670 returns 01:01:10', () => { - expect( - Utilities.getSecondsAsHHMMSS(3670) - ).to.equal('01:01:10'); - }); + it('3670 returns 01:01:10', () => { + expect( + Utilities.getSecondsAsHHMMSS(3670), + ).to.equal('01:01:10'); + }); - it('90000 returns 25:00:00, check for hours greater than 24', () => { - expect( - Utilities.getSecondsAsHHMMSS(90000) - ).to.equal('25:00:00'); - }); + it('90000 returns 25:00:00, check for hours greater than 24', () => { + expect( + Utilities.getSecondsAsHHMMSS(90000), + ).to.equal('25:00:00'); + }); - it('-3600 returns 00:00:00, negative time not allowed in SCORM session times', () => { - expect( - Utilities.getSecondsAsHHMMSS(-3600) - ).to.equal('00:00:00'); - }); + it('-3600 returns 00:00:00, negative time not allowed in SCORM session times', + () => { + expect( + Utilities.getSecondsAsHHMMSS(-3600), + ).to.equal('00:00:00'); + }); - it('Empty seconds returns 00:00:00', () => { - expect( - Utilities.getSecondsAsHHMMSS(null) - ).to.equal('00:00:00'); - }); - }); + it('Empty seconds returns 00:00:00', () => { + expect( + Utilities.getSecondsAsHHMMSS(null), + ).to.equal('00:00:00'); + }); + }); - describe('function getSecondsAsISODuration()', () => { - it('10 returns P10S', () => { - expect( - Utilities.getSecondsAsISODuration(10) - ).to.equal('P10S'); - }); + describe('getSecondsAsISODuration()', () => { + it('10 returns P10S', () => { + expect( + Utilities.getSecondsAsISODuration(10), + ).to.equal('P10S'); + }); - it('60 returns P1M', () => { - expect( - Utilities.getSecondsAsISODuration(60) - ).to.equal('P1M'); - }); + it('60 returns P1M', () => { + expect( + Utilities.getSecondsAsISODuration(60), + ).to.equal('P1M'); + }); - it('3600 returns P1H', () => { - expect( - Utilities.getSecondsAsISODuration(3600) - ).to.equal('P1H'); - }); + it('3600 returns P1H', () => { + expect( + Utilities.getSecondsAsISODuration(3600), + ).to.equal('P1H'); + }); - it('70 returns P1M10S', () => { - expect( - Utilities.getSecondsAsISODuration(70) - ).to.equal('P1M10S'); - }); + it('70 returns P1M10S', () => { + expect( + Utilities.getSecondsAsISODuration(70), + ).to.equal('P1M10S'); + }); - it('3670 returns P1H1M10S', () => { - expect( - Utilities.getSecondsAsISODuration(3670) - ).to.equal('P1H1M10S'); - }); + it('3670 returns P1H1M10S', () => { + expect( + Utilities.getSecondsAsISODuration(3670), + ).to.equal('P1H1M10S'); + }); - it('90000 returns P1D1H', () => { - expect( - Utilities.getSecondsAsISODuration(90000) - ).to.equal('P1D1H'); - }); + it('90000 returns P1D1H', () => { + expect( + Utilities.getSecondsAsISODuration(90000), + ).to.equal('P1D1H'); + }); - it('90061 returns P1D1H1M1S', () => { - expect( - Utilities.getSecondsAsISODuration(90061) - ).to.equal('P1D1H1M1S'); - }); + it('90061 returns P1D1H1M1S', () => { + expect( + Utilities.getSecondsAsISODuration(90061), + ).to.equal('P1D1H1M1S'); + }); - it('-3600 returns P0S, negative time not allowed in SCORM session times', () => { - expect( - Utilities.getSecondsAsISODuration(-3600) - ).to.equal('P0S'); - }); + it('-3600 returns P0S, negative time not allowed in SCORM session times', + () => { + expect( + Utilities.getSecondsAsISODuration(-3600), + ).to.equal('P0S'); + }); - it('Empty seconds returns P0S', () => { - expect( - Utilities.getSecondsAsISODuration(null) - ).to.equal('P0S'); - }); - }); + it('Empty seconds returns P0S', () => { + expect( + Utilities.getSecondsAsISODuration(null), + ).to.equal('P0S'); + }); + }); - describe('function getTimeAsSeconds()', () => { - it('00:00:10 returns 10', () => { - expect( - Utilities.getTimeAsSeconds('00:00:10', scorm12_regex.CMITimespan) - ).to.equal(10); - }); + describe('getTimeAsSeconds()', () => { + it('00:00:10 returns 10', () => { + expect( + Utilities.getTimeAsSeconds('00:00:10', scorm12_regex.CMITimespan), + ).to.equal(10); + }); - it('00:01:10 returns 70', () => { - expect( - Utilities.getTimeAsSeconds('00:01:10', scorm12_regex.CMITimespan) - ).to.equal(70); - }); + it('00:01:10 returns 70', () => { + expect( + Utilities.getTimeAsSeconds('00:01:10', scorm12_regex.CMITimespan), + ).to.equal(70); + }); - it('01:01:10 returns 3670', () => { - expect( - Utilities.getTimeAsSeconds('01:01:10', scorm12_regex.CMITimespan) - ).to.equal(3670); - }); + it('01:01:10 returns 3670', () => { + expect( + Utilities.getTimeAsSeconds('01:01:10', scorm12_regex.CMITimespan), + ).to.equal(3670); + }); - it('100:00:00 returns 3670', () => { - expect( - Utilities.getTimeAsSeconds('100:00:00', scorm12_regex.CMITimespan) - ).to.equal(360000); - }); + it('100:00:00 returns 3670', () => { + expect( + Utilities.getTimeAsSeconds('100:00:00', scorm12_regex.CMITimespan), + ).to.equal(360000); + }); - it('-01:00:00 returns 0', () => { - expect( - Utilities.getTimeAsSeconds('-01:00:00', scorm12_regex.CMITimespan) - ).to.equal(0); - }); + it('-01:00:00 returns 0', () => { + expect( + Utilities.getTimeAsSeconds('-01:00:00', scorm12_regex.CMITimespan), + ).to.equal(0); + }); - it('Number value returns 0', () => { - expect( - Utilities.getTimeAsSeconds(999, scorm12_regex.CMITimespan) - ).to.equal(0); - }); + it('Number value returns 0', () => { + expect( + Utilities.getTimeAsSeconds(999, scorm12_regex.CMITimespan), + ).to.equal(0); + }); - it('boolean value returns 0', () => { - expect( - Utilities.getTimeAsSeconds(true, scorm12_regex.CMITimespan) - ).to.equal(0); - }); + it('boolean value returns 0', () => { + expect( + Utilities.getTimeAsSeconds(true, scorm12_regex.CMITimespan), + ).to.equal(0); + }); - it('Empty value returns 0', () => { - expect( - Utilities.getTimeAsSeconds(null, scorm12_regex.CMITimespan) - ).to.equal(0); - }); - }); + it('Empty value returns 0', () => { + expect( + Utilities.getTimeAsSeconds(null, scorm12_regex.CMITimespan), + ).to.equal(0); + }); + }); - describe('function getDurationAsSeconds()', () => { - it('P0S returns 0', () => { - expect( - Utilities.getDurationAsSeconds('P0S', scorm2004_regex.CMITimespan) - ).to.equal(0); - }); + describe('getDurationAsSeconds()', () => { + it('P0S returns 0', () => { + expect( + Utilities.getDurationAsSeconds('P0S', scorm2004_regex.CMITimespan), + ).to.equal(0); + }); - it('P70S returns 70', () => { - expect( - Utilities.getDurationAsSeconds('P70S', scorm2004_regex.CMITimespan) - ).to.equal(70); - }); + it('P70S returns 70', () => { + expect( + Utilities.getDurationAsSeconds('P70S', scorm2004_regex.CMITimespan), + ).to.equal(70); + }); - it('PT1M10S returns 70', () => { - expect( - Utilities.getDurationAsSeconds('PT1M10S', scorm2004_regex.CMITimespan) - ).to.equal(70); - }); + it('PT1M10S returns 70', () => { + expect( + Utilities.getDurationAsSeconds('PT1M10S', + scorm2004_regex.CMITimespan), + ).to.equal(70); + }); - it('P1D returns 86400', () => { - expect( - Utilities.getDurationAsSeconds('P1D', scorm2004_regex.CMITimespan) - ).to.equal(86400); - }); + it('P1D returns 86400', () => { + expect( + Utilities.getDurationAsSeconds('P1D', scorm2004_regex.CMITimespan), + ).to.equal(86400); + }); - it('P1M returns number of seconds for one month from now', () => { - const now = new Date(); - let oneMonthFromNow = new Date(now); - oneMonthFromNow.setMonth(oneMonthFromNow.getMonth() + 1); + it('P1M returns number of seconds for one month from now', () => { + const now = new Date(); + const oneMonthFromNow = new Date(now); + oneMonthFromNow.setMonth(oneMonthFromNow.getMonth() + 1); - expect( - Utilities.getDurationAsSeconds('P1M', scorm2004_regex.CMITimespan) - ).to.equal((oneMonthFromNow - now) / 1000.0); - }); + expect( + Utilities.getDurationAsSeconds('P1M', scorm2004_regex.CMITimespan), + ).to.equal((oneMonthFromNow - now) / 1000.0); + }); - it('P1Y returns number of seconds for one year from now', () => { - const now = new Date(); - let oneYearFromNow = new Date(now); - oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1); + it('P1Y returns number of seconds for one year from now', () => { + const now = new Date(); + const oneYearFromNow = new Date(now); + oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1); - expect( - Utilities.getDurationAsSeconds('P1Y', scorm2004_regex.CMITimespan) - ).to.equal((oneYearFromNow - now) / 1000.0); - }); + expect( + Utilities.getDurationAsSeconds('P1Y', scorm2004_regex.CMITimespan), + ).to.equal((oneYearFromNow - now) / 1000.0); + }); - it('Invalid duration returns 0', () => { - expect( - Utilities.getDurationAsSeconds('T1M10S', scorm2004_regex.CMITimespan) - ).to.equal(0); - }); + it('Invalid duration returns 0', () => { + expect( + Utilities.getDurationAsSeconds('T1M10S', scorm2004_regex.CMITimespan), + ).to.equal(0); + }); - it('Empty duration returns 0', () => { - expect( - Utilities.getDurationAsSeconds(null, scorm2004_regex.CMITimespan) - ).to.equal(0); - }); - }); -}); \ No newline at end of file + it('Empty duration returns 0', () => { + expect( + Utilities.getDurationAsSeconds(null, scorm2004_regex.CMITimespan), + ).to.equal(0); + }); + }); + + describe('addTwoDurations()', () => { + it('P1H5M30.5S plus PT15M10S equals P1H20M40.5S', () => { + expect( + Utilities.addTwoDurations('P1H5M30.5S', 'PT15M10S', + scorm2004_regex.CMITimespan), + ).to.equal('P1H20M40.5S'); + }); + it('P1Y364D plus P2D1H45M52S equals P732D1H45M52S', () => { + expect( + Utilities.addTwoDurations('P1Y364D', 'P2D1H45M52S', + scorm2004_regex.CMITimespan), + ).to.equal('P732D1H45M52S'); + }); + it('Invalid plus valid equals valid', () => { + expect( + Utilities.addTwoDurations('NOT A VALID DURATION', 'P1H30M45S', + scorm2004_regex.CMITimespan), + ).to.equal('P1H30M45S'); + }); + it('Valid plus invalid equals valid', () => { + expect( + Utilities.addTwoDurations('P1H30M45S', 'NOT A VALID DURATION', + scorm2004_regex.CMITimespan), + ).to.equal('P1H30M45S'); + }); + }); + + describe('addHHMMSSTimeStrings()', () => { + it('01:05:30.5 plus 00:15:10 equals 01:20:40.5', () => { + expect( + Utilities.addHHMMSSTimeStrings('01:05:30.5', '00:15:10', + scorm12_regex.CMITimespan), + ).to.equal('01:20:40.5'); + }); + it('17496:00:00 plus 49:35:52 equals 17545:35:52', () => { + expect( + Utilities.addHHMMSSTimeStrings('17496:00:00', '49:35:52', + scorm12_regex.CMITimespan), + ).to.equal('17545:35:52'); + }); + it('Invalid plus valid equals valid', () => { + expect( + Utilities.addHHMMSSTimeStrings('-00:15:10', '01:05:30.5', + scorm12_regex.CMITimespan), + ).to.equal('01:05:30.5'); + }); + it('Valid plus invalid equals valid', () => { + expect( + Utilities.addHHMMSSTimeStrings('01:05:30.5', 'NOT A VALID DURATION', + scorm12_regex.CMITimespan), + ).to.equal('01:05:30.5'); + }); + }); +});