Adding the ability to use catch-all listeners

This commit is contained in:
Jonathan Putney
2020-05-25 17:06:51 -04:00
parent db1477fedc
commit f791aeec7b
8 changed files with 80 additions and 31 deletions

1
.gitignore vendored
View File

@@ -66,3 +66,4 @@ typings/
.DS_Store
test-results.xml
/.vscode/

7
.mocharc.yml Normal file
View File

@@ -0,0 +1,7 @@
recursive: true
require: '@babel/register'
spec: 'test/**/*.spec.js'
ui: 'bdd'
watch: true
watch-files:
- 'test/**/*.js'

View File

@@ -48,7 +48,7 @@ The APIs include several settings to customize the functionality of each API:
| `autocommitSeconds` | 60 | int | Number of seconds to wait before autocommiting. Timer is restarted if another value is set. |
| `lmsCommitUrl` | false | url | The URL endpoint of the LMS where data should be sent upon commit. If no value is provided, modules will run as usual, but all method calls with just be logged to the console. |
| `dataCommitFormat` | `json` | `json`, `flattened`, `params` | `json` will send a JSON object to the lmsCommitUrl in the format of <br>`{'cmi': {'core': {...}}`<br><br> `flattened` will send the data in the format <br>`{'cmi.core.exit': 'suspend', 'cmi.core.mode': 'normal'...}`<br><br> `params` will send the data as <br>`?cmi.core.exit=suspend&cmi.core.mode=normal...` |
| `commitRequestType` | 'application/json;charset=UTF-8' | string | This setting is provided in case your LMS expects a different content type or character set. |
| `commitRequestDataType` | 'application/json;charset=UTF-8' | string | This setting is provided in case your LMS expects a different content type or character set. |
| `autoProgress` | false | true/false | In case Sequencing is being used, you can tell the API to automatically throw the `SequenceNext` event.|
| `logLevel` | 4 | int<br><br>1 => DEBUG<br>2 => INFO<br>3 => WARN<br>4 => ERROR<br>5 => NONE | By default, the APIs only log error messages. |
| `mastery_override` | false | true/false | (SCORM 1.2) Used to override a module's `cmi.core.lesson_status` so that a pass/fail is determined based on a mastery score and the user's raw score, rather than using whatever status is provided by the module. An example of this would be if a module is published using a `Complete/Incomplete` final status, but the LMS always wants to receive a `Passed/Failed` for quizzes, then we can use this setting to override the given final status. |

View File

@@ -12,6 +12,15 @@ module.exports = function(grunt) {
src: ['test/**/*.spec.js'],
},
},
watch: {
scripts: {
files: ['src/**/*.js'],
tasks: ['browserify:development'],
options: {
spawn: false,
},
},
},
browserify: {
development: {
src: [
@@ -69,6 +78,7 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.registerTask('test', 'mochaTest');

View File

@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test Page</title>
<script src="dist/scorm-again.js" type="application/javascript"></script>
</head>
<body>
</body>
</html>

View File

@@ -27,6 +27,7 @@
"grunt": "^1.1.0",
"grunt-browserify": "^5.3.0",
"grunt-cli": "^1.3.2",
"grunt-contrib-watch": "^1.1.0",
"grunt-mocha-test": "^0.13.3",
"jsdoc": "^3.6.4",
"jsdoc-babel": "^0.5.0",
@@ -38,7 +39,9 @@
"nyc": "^15.0.1"
},
"scripts": {
"test": "./node_modules/.bin/mocha --require @babel/register --bdd --recursive --reporter list"
"test": "./node_modules/.bin/mocha --require @babel/register --bdd --recursive --reporter list",
"compile": "./node_modules/.bin/grunt default",
"fix": "./node_modules/.bin/eslint ./src --fix"
},
"repository": {
"type": "git",
@@ -55,5 +58,7 @@
"url": "https://github.com/jcputney/scorm-again/issues"
},
"homepage": "https://github.com/jcputney/scorm-again",
"dependencies": {}
"dependencies": {
"@types/mocha": "^7.0.2"
}
}

View File

@@ -17,12 +17,28 @@ export default class BaseAPI {
#error_codes;
#settings = {
autocommit: false,
autocommitSeconds: 60,
autocommitSeconds: 10,
lmsCommitUrl: false,
dataCommitFormat: 'json', // valid formats are 'json' or 'flattened', 'params'
commitRequestDataType: 'application/json;charset=UTF-8',
autoProgress: false,
logLevel: global_constants.LOG_LEVEL_ERROR,
responseHandler: function(xhr) {
let result;
if (typeof xhr !== 'undefined') {
result = JSON.parse(xhr.responseText);
if (result === null || !{}.hasOwnProperty.call(result, 'result')) {
result = {};
if (xhr.status === 200) {
result.result = global_constants.SCORM_TRUE;
} else {
result.result = global_constants.SCORM_FALSE;
result.errorCode = 101;
}
}
}
return result;
},
};
cmi;
startingData: {};
@@ -112,11 +128,11 @@ export default class BaseAPI {
this.currentState = global_constants.STATE_TERMINATED;
const result = this.storeData(true);
if (result.errorCode && result.errorCode > 0) {
if (typeof result.errorCode !== 'undefined' && result.errorCode > 0) {
this.throwSCORMError(result.errorCode);
}
returnValue = result.result ?
result.result : global_constants.SCORM_FALSE;
result.result : global_constants.SCORM_FALSE;
if (checkTerminated) this.lastErrorCode = 0;
@@ -240,7 +256,7 @@ export default class BaseAPI {
this.throwSCORMError(result.errorCode);
}
returnValue = result.result ?
result.result : global_constants.SCORM_FALSE;
result.result : global_constants.SCORM_FALSE;
this.apiLog(callbackName, 'HttpRequest', ' Result: ' + returnValue,
global_constants.LOG_LEVEL_DEBUG);
@@ -436,9 +452,9 @@ export default class BaseAPI {
*/
_checkObjectHasProperty(refObject, attribute: String) {
return Object.hasOwnProperty.call(refObject, attribute) ||
Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(refObject), attribute) ||
(attribute in refObject);
Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(refObject), attribute) ||
(attribute in refObject);
}
/**
@@ -502,15 +518,15 @@ export default class BaseAPI {
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
const invalidErrorCode = scorm2004 ?
this.#error_codes.UNDEFINED_DATA_MODEL :
this.#error_codes.GENERAL;
this.#error_codes.UNDEFINED_DATA_MODEL :
this.#error_codes.GENERAL;
for (let i = 0; i < structure.length; i++) {
const attribute = structure[i];
if (i === structure.length - 1) {
if (scorm2004 && (attribute.substr(0, 8) === '{target=') &&
(typeof refObject._isTargetValid == 'function')) {
(typeof refObject._isTargetValid == 'function')) {
this.throwSCORMError(this.#error_codes.READ_ONLY_ELEMENT);
} else if (!this._checkObjectHasProperty(refObject, attribute)) {
this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
@@ -616,8 +632,8 @@ export default class BaseAPI {
const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
const invalidErrorCode = scorm2004 ?
this.#error_codes.UNDEFINED_DATA_MODEL :
this.#error_codes.GENERAL;
this.#error_codes.UNDEFINED_DATA_MODEL :
this.#error_codes.GENERAL;
for (let i = 0; i < structure.length; i++) {
attribute = structure[i];
@@ -631,7 +647,7 @@ export default class BaseAPI {
}
} else {
if ((String(attribute).substr(0, 8) === '{target=') &&
(typeof refObject._isTargetValid == 'function')) {
(typeof refObject._isTargetValid == 'function')) {
const target = String(attribute).
substr(8, String(attribute).length - 9);
return refObject._isTargetValid(target);
@@ -745,11 +761,17 @@ export default class BaseAPI {
* @param {*} value
*/
processListeners(functionName: String, CMIElement: String, value: any) {
this.apiLog(functionName, CMIElement, value);
for (let i = 0; i < this.listenerArray.length; i++) {
const listener = this.listenerArray[i];
const functionsMatch = listener.functionName === functionName;
const listenerHasCMIElement = !!listener.CMIElement;
const CMIElementsMatch = listener.CMIElement === CMIElement;
let CMIElementsMatch = false;
if (CMIElement && listener.CMIElement && listener.CMIElement.substring(listener.CMIElement.length - 1) === '*') {
CMIElementsMatch = CMIElement.indexOf(listener.CMIElement.substring(0, listener.CMIElement.length - 1)) === 0;
} else {
CMIElementsMatch = listener.CMIElement === CMIElement;
}
if (functionsMatch && (!listenerHasCMIElement || CMIElementsMatch)) {
listener.callback(CMIElement, value);
@@ -901,17 +923,29 @@ export default class BaseAPI {
} else {
httpReq.setRequestHeader('Content-Type',
this.settings.commitRequestDataType);
httpReq.responseType = 'json';
httpReq.send(JSON.stringify(params));
}
} catch (e) {
return genericError;
}
let result;
try {
return JSON.parse(httpReq.responseText);
if (typeof this.settings.responseHandler === 'function') {
result = this.settings.responseHandler(httpReq);
} else {
result = JSON.parse(httpReq.responseText);
}
} catch (e) {
return genericError;
}
if (typeof result === 'undefined') {
return genericError;
}
return result;
}
/**

View File

@@ -554,10 +554,13 @@ export default class Scorm2004API extends BaseAPI {
}
const result = this.processHttpRequest(this.settings.lmsCommitUrl,
commitObject);
// check if this is a sequencing call, and then call the necessary JS
if (navRequest && result.navRequest !== undefined &&
{
if (navRequest && result.navRequest !== undefined &&
result.navRequest !== '') {
Function(`"use strict";(() => { ${result.navRequest} })()`)();
Function(`"use strict";(() => { ${result.navRequest} })()`)();
}
}
return result;
} else {