This commit is contained in:
Jonathan Putney
2020-01-09 09:37:58 -05:00
17 changed files with 2031 additions and 534 deletions

86
dist/scorm-again.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2270
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,23 +7,23 @@
"test": "test"
},
"devDependencies": {
"@babel/cli": "^7.7.0",
"@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.5",
"babel-eslint": "^11.0.0-beta.0",
"@babel/cli": "^7.7.7",
"@babel/core": "^7.7.7",
"@babel/node": "^7.7.7",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-optional-chaining": "^7.7.5",
"@babel/plugin-proposal-private-methods": "^7.7.4",
"@babel/preset-env": "^7.7.7",
"@babel/preset-flow": "^7.7.4",
"@babel/register": "^7.7.7",
"@types/chai": "^4.2.7",
"babel-eslint": "^11.0.0-beta.2",
"babelify": "^10.0.0",
"browserify": "^16.5.0",
"chai": "^4.2.0",
"eslint": "^6.6.0",
"eslint": "^6.8.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-import": "^2.19.1",
"grunt": "^1.0.4",
"grunt-browserify": "^5.3.0",
"grunt-cli": "^1.3.2",

View File

@@ -116,7 +116,7 @@ export default class BaseAPI {
result.result : global_constants.SCORM_FALSE;
if (checkTerminated) this.lastErrorCode = 0;
returnValue = global_constants.SCORM_TRUE;
this.processListeners(callbackName);
}
@@ -171,6 +171,9 @@ export default class BaseAPI {
checkTerminated: boolean,
CMIElement,
value) {
if (value !== undefined) {
value = String(value);
}
let returnValue = global_constants.SCORM_FALSE;
if (this.checkState(checkTerminated, this.#error_codes.STORE_BEFORE_INIT,
@@ -183,6 +186,7 @@ export default class BaseAPI {
this.lastErrorCode = e.errorCode;
returnValue = global_constants.SCORM_FALSE;
} else {
console.error(e.getMessage());
this.throwSCORMError(this.#error_codes.GENERAL);
}
}
@@ -808,13 +812,14 @@ export default class BaseAPI {
return;
}
CMIElement = CMIElement || 'cmi';
CMIElement = CMIElement !== undefined ? CMIElement : 'cmi';
this.startingData = json;
// could this be refactored down to flatten(json) then setCMIValue on each?
for (const key in json) {
if ({}.hasOwnProperty.call(json, key) && json[key]) {
const currentCMIElement = CMIElement + '.' + key;
const currentCMIElement = (CMIElement ? CMIElement + '.' : '') + key;
const value = json[key];
if (value['childArray']) {

View File

@@ -503,16 +503,19 @@ export default class Scorm2004API extends BaseAPI {
if (this.cmi.credit === 'credit') {
if (this.cmi.completion_threshold && this.cmi.progress_measure) {
if (this.cmi.progress_measure >= this.cmi.completion_threshold) {
console.debug('Setting Completion Status: Completed');
this.cmi.completion_status = 'completed';
} else {
console.debug('Setting Completion Status: Incomplete');
this.cmi.completion_status = 'incomplete';
}
}
if (this.cmi.scaled_passing_score !== null &&
this.cmi.score.scaled !== '') {
if (this.cmi.scaled_passing_score && this.cmi.score.scaled) {
if (this.cmi.score.scaled >= this.cmi.scaled_passing_score) {
console.debug('Setting Success Status: Passed');
this.cmi.success_status = 'passed';
} else {
console.debug('Setting Success Status: Failed');
this.cmi.success_status = 'failed';
}
}

View File

@@ -362,7 +362,7 @@ class CMICore extends BaseCMI {
* @param {string} lesson_location
*/
set lesson_location(lesson_location) {
if (check12ValidFormat(lesson_location, regex.CMIString256)) {
if (check12ValidFormat(lesson_location, regex.CMIString256, true)) {
this.#lesson_location = lesson_location;
}
}
@@ -462,7 +462,7 @@ class CMICore extends BaseCMI {
* @param {string} exit
*/
set exit(exit) {
if (check12ValidFormat(exit, regex.CMIExit)) {
if (check12ValidFormat(exit, regex.CMIExit, true)) {
this.#exit = exit;
}
}
@@ -494,7 +494,7 @@ class CMICore extends BaseCMI {
return Utilities.addHHMMSSTimeStrings(
this.#total_time,
this.#session_time,
new RegExp(scorm12_regex.CMITimespan)
new RegExp(scorm12_regex.CMITimespan),
);
}
@@ -512,7 +512,6 @@ class CMICore extends BaseCMI {
* lesson_location: string,
* lesson_status: string,
* credit: string,
* total_time: string,
* session_time: *
* }
* }
@@ -526,7 +525,6 @@ class CMICore extends BaseCMI {
'credit': this.credit,
'lesson_status': this.lesson_status,
'entry': this.entry,
'total_time': this.getCurrentTotalTime(),
'lesson_mode': this.lesson_mode,
'exit': this.exit,
'session_time': this.session_time,

View File

@@ -235,7 +235,7 @@ export class CMI extends BaseCMI {
* @param {string} exit
*/
set exit(exit) {
if (check2004ValidFormat(exit, regex.CMIExit)) {
if (check2004ValidFormat(exit, regex.CMIExit, true)) {
this.#exit = exit;
}
}
@@ -507,8 +507,7 @@ export class CMI extends BaseCMI {
* session_time: string,
* success_status: string,
* suspend_data: string,
* time_limit_action: string,
* total_time: string
* time_limit_action: string
* }
* }
*/
@@ -538,7 +537,6 @@ export class CMI extends BaseCMI {
'success_status': this.success_status,
'suspend_data': this.suspend_data,
'time_limit_action': this.time_limit_action,
'total_time': this.getCurrentTotalTime(),
};
delete this.jsonString;
return result;

View File

@@ -28,11 +28,21 @@ export function getSecondsAsHHMMSS(totalSeconds: Number) {
const dateObj = new Date(totalSeconds * 1000);
const minutes = dateObj.getUTCMinutes();
// make sure we add any possible decimal value
const seconds = dateObj.getSeconds() + (totalSeconds % 1.0);
const seconds = dateObj.getSeconds();
const ms = totalSeconds % 1.0;
let msStr = '';
if (countDecimals(ms) > 0) {
if (countDecimals(ms) > 2) {
msStr = ms.toFixed(2);
} else {
msStr = String(ms);
}
msStr = '.' + msStr.split('.')[1];
}
return hours.toString().padStart(2, '0') + ':' +
minutes.toString().padStart(2, '0') + ':' +
seconds.toString().padStart(2, '0');
seconds.toString().padStart(2, '0') + msStr;
}
/**
@@ -119,7 +129,6 @@ export function getDurationAsSeconds(duration: String, durationRegex: RegExp) {
const milliseconds = Number(Number(seconds) % 1).toFixed(6) * 1000.0;
anchor.setMilliseconds(anchor.getMilliseconds() + milliseconds);
}
return ((anchor * 1.0) - now) / 1000.0;
}
@@ -220,3 +229,13 @@ export function unflatten(data) {
}
return result[''] || result;
}
/**
* Counts the number of decimal places
* @param {number} num
* @return {number}
*/
export function countDecimals(num: number) {
if (Math.floor(num) === num) return 0;
return num.toString().split('.')[1].length || 0;
}

View File

@@ -3,6 +3,7 @@ import {describe, it} from 'mocha';
import Scorm12API from '../src/Scorm12API';
import * as h from './api_helpers';
import {scorm12_error_codes} from '../src/constants/error_codes';
import {scorm12_values} from '../src/constants/field_values';
const api = (settings = {}) => {
const API = new Scorm12API(settings);
@@ -16,6 +17,27 @@ const apiInitialized = (settings = {}) => {
};
describe('SCORM 1.2 API Tests', () => {
describe('LMSSetValue()', () => {
h.checkValidValues({
api: apiInitialized(),
fieldName: 'cmi.core.score.raw',
validValues: scorm12_values.validScoreRange,
invalidValues: scorm12_values.invalidScoreRange,
});
h.checkValidValues({
api: apiInitialized(),
fieldName: 'cmi.core.score.min',
validValues: scorm12_values.validScoreRange,
invalidValues: scorm12_values.invalidScoreRange,
});
h.checkValidValues({
api: apiInitialized(),
fieldName: 'cmi.core.score.max',
validValues: scorm12_values.validScoreRange,
invalidValues: scorm12_values.invalidScoreRange,
});
});
describe('setCMIValue()', () => {
describe('Invalid Sets - Should Always Fail', () => {
h.checkSetCMIValue({
@@ -280,7 +302,7 @@ describe('SCORM 1.2 API Tests', () => {
scorm12API.cmi.core.session_time = '23:59:59';
const cmiExport = scorm12API.renderCommitCMI(true);
expect(
cmiExport.cmi.core.total_time
cmiExport.cmi.core.total_time,
).to.equal('36:34:55');
});
});

View File

@@ -17,6 +17,33 @@ const apiInitialized = () => {
};
describe('SCORM 2004 API Tests', () => {
describe('SetValue()', () => {
h.checkValidValues({
api: apiInitialized(),
fieldName: 'cmi.score.scaled',
validValues: scorm2004_values.validScaledRange,
invalidValues: scorm2004_values.invalidScaledRange,
});
h.checkValidValues({
api: apiInitialized(),
fieldName: 'cmi.score.raw',
validValues: scorm2004_values.validScoreRange,
invalidValues: scorm2004_values.invalidScoreRange,
});
h.checkValidValues({
api: apiInitialized(),
fieldName: 'cmi.score.min',
validValues: scorm2004_values.validScoreRange,
invalidValues: scorm2004_values.invalidScoreRange,
});
h.checkValidValues({
api: apiInitialized(),
fieldName: 'cmi.score.max',
validValues: scorm2004_values.validScoreRange,
invalidValues: scorm2004_values.invalidScoreRange,
});
});
describe('setCMIValue()', () => {
describe('Invalid Sets - Should Always Fail', () => {
h.checkSetCMIValue({

View File

@@ -1,6 +1,36 @@
import {describe, it} from 'mocha';
import {expect} from 'chai';
export const checkValidValues = (
{
api,
fieldName,
validValues,
invalidValues,
}) => {
describe(`Field: ${fieldName}`, () => {
for (const idx in validValues) {
if ({}.hasOwnProperty.call(validValues, idx)) {
it(`Should successfully write '${validValues[idx]}' to ${fieldName}`,
() => {
expect(api.lmsSetValue(fieldName, validValues[idx])).
to.equal('true');
});
}
}
for (const idx in invalidValues) {
if ({}.hasOwnProperty.call(invalidValues, idx)) {
it(`Should fail to write '${invalidValues[idx]}' to ${fieldName}`,
() => {
expect(api.lmsSetValue(fieldName, invalidValues[idx])).
to.equal('false');
});
}
}
});
};
export const checkLMSSetValue = (
{
api,

View File

@@ -592,7 +592,7 @@ describe('SCORM 1.2 CMI Tests', () => {
).
to.
equal(
'{"suspend_data":"","launch_data":"","comments":"","comments_from_lms":"","core":{"student_id":"","student_name":"","lesson_location":"","credit":"","lesson_status":"not attempted","entry":"","total_time":"00:00:00","lesson_mode":"normal","exit":"","session_time":"00:00:00","score":{"raw":"","min":"","max":"100"}},"objectives":{"0":{"id":"","status":"","score":{"raw":"","min":"","max":"100"}}},"student_data":{"mastery_score":"","max_time_allowed":"","time_limit_action":""},"student_preference":{"audio":"","language":"","speed":"","text":""},"interactions":{"0":{"id":"","time":"","type":"","weighting":"","student_response":"","result":"","latency":"","objectives":{},"correct_responses":{}}}}');
'{"suspend_data":"","launch_data":"","comments":"","comments_from_lms":"","core":{"student_id":"","student_name":"","lesson_location":"","credit":"","lesson_status":"not attempted","entry":"","lesson_mode":"normal","exit":"","session_time":"00:00:00","score":{"raw":"","min":"","max":"100"}},"objectives":{"0":{"id":"","status":"","score":{"raw":"","min":"","max":"100"}}},"student_data":{"mastery_score":"","max_time_allowed":"","time_limit_action":""},"student_preference":{"audio":"","language":"","speed":"","text":""},"interactions":{"0":{"id":"","time":"","type":"","weighting":"","student_response":"","result":"","latency":"","objectives":{},"correct_responses":{}}}}');
});
});

View File

@@ -402,7 +402,7 @@ describe('SCORM 2004 CMI Tests', () => {
).
to.
equal(
'{"comments_from_learner":{},"comments_from_lms":{},"completion_status":"unknown","completion_threshold":"","credit":"credit","entry":"","exit":"","interactions":{"0":{"id":"","type":"","objectives":{},"timestamp":"","weighting":"","learner_response":"","result":"","latency":"","description":"","correct_responses":{}}},"launch_data":"","learner_id":"","learner_name":"","learner_preference":{"audio_level":"1","language":"","delivery_speed":"1","audio_captioning":"0"},"location":"","max_time_allowed":"","mode":"normal","objectives":{"0":{"id":"","success_status":"unknown","completion_status":"unknown","progress_measure":"","description":"","score":{"scaled":"","raw":"","min":"","max":""}}},"progress_measure":"","scaled_passing_score":"","score":{"scaled":"","raw":"","min":"","max":""},"session_time":"PT0H0M0S","success_status":"unknown","suspend_data":"","time_limit_action":"continue,no message","total_time":"PT0S"}');
'{"comments_from_learner":{},"comments_from_lms":{},"completion_status":"unknown","completion_threshold":"","credit":"credit","entry":"","exit":"","interactions":{"0":{"id":"","type":"","objectives":{},"timestamp":"","weighting":"","learner_response":"","result":"","latency":"","description":"","correct_responses":{}}},"launch_data":"","learner_id":"","learner_name":"","learner_preference":{"audio_level":"1","language":"","delivery_speed":"1","audio_captioning":"0"},"location":"","max_time_allowed":"","mode":"normal","objectives":{"0":{"id":"","success_status":"unknown","completion_status":"unknown","progress_measure":"","description":"","score":{"scaled":"","raw":"","min":"","max":""}}},"progress_measure":"","scaled_passing_score":"","score":{"scaled":"","raw":"","min":"","max":""},"session_time":"PT0H0M0S","success_status":"unknown","suspend_data":"","time_limit_action":"continue,no message"}');
});
});
@@ -712,7 +712,7 @@ describe('SCORM 2004 CMI Tests', () => {
).
to.
equal(
'{"comments_from_learner":{},"comments_from_lms":{},"completion_status":"unknown","completion_threshold":"","credit":"credit","entry":"","exit":"","interactions":{"0":{"id":"","type":"","objectives":{},"timestamp":"","weighting":"","learner_response":"","result":"","latency":"","description":"","correct_responses":{}}},"launch_data":"","learner_id":"","learner_name":"","learner_preference":{"audio_level":"1","language":"","delivery_speed":"1","audio_captioning":"0"},"location":"","max_time_allowed":"","mode":"normal","objectives":{"0":{"id":"","success_status":"unknown","completion_status":"unknown","progress_measure":"","description":"","score":{"scaled":"","raw":"","min":"","max":""}}},"progress_measure":"","scaled_passing_score":"","score":{"scaled":"","raw":"","min":"","max":""},"session_time":"PT0H0M0S","success_status":"unknown","suspend_data":"","time_limit_action":"continue,no message","total_time":"PT0S"}');
'{"comments_from_learner":{},"comments_from_lms":{},"completion_status":"unknown","completion_threshold":"","credit":"credit","entry":"","exit":"","interactions":{"0":{"id":"","type":"","objectives":{},"timestamp":"","weighting":"","learner_response":"","result":"","latency":"","description":"","correct_responses":{}}},"launch_data":"","learner_id":"","learner_name":"","learner_preference":{"audio_level":"1","language":"","delivery_speed":"1","audio_captioning":"0"},"location":"","max_time_allowed":"","mode":"normal","objectives":{"0":{"id":"","success_status":"unknown","completion_status":"unknown","progress_measure":"","description":"","score":{"scaled":"","raw":"","min":"","max":""}}},"progress_measure":"","scaled_passing_score":"","score":{"scaled":"","raw":"","min":"","max":""},"session_time":"PT0H0M0S","success_status":"unknown","suspend_data":"","time_limit_action":"continue,no message"}');
});
});

View File

@@ -89,6 +89,7 @@ export const scorm12_values = {
'time-out',
'suspend',
'logout',
'',
],
invalidExit: [
'close',
@@ -132,7 +133,12 @@ export const scorm12_values = {
validScoreRange: [
'1',
'50.25',
'70',
'100',
1,
50.25,
70,
100,
],
invalidScoreRange: [
'invalid',
@@ -224,6 +230,7 @@ export const scorm2004_values = {
'suspend',
'logout',
'normal',
'',
],
invalidExit: [
'close',

View File

@@ -224,9 +224,9 @@ describe('Utility Tests', () => {
describe('addTwoDurations()', () => {
it('P1H5M30.5S plus PT15M10S equals P1H20M40.5S', () => {
expect(
Utilities.addTwoDurations('PT1H5M30.5S', 'PT15M10S',
Utilities.addTwoDurations('PT1H5M30.5S', 'PT15M30S',
scorm2004_regex.CMITimespan),
).to.equal('PT1H20M40.5S');
).to.equal('PT1H21M0.5S');
});
it('P1Y364D plus P2DT1H45M52S equals P732DT1H45M52S', () => {
expect(
@@ -251,9 +251,9 @@ describe('Utility Tests', () => {
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',
Utilities.addHHMMSSTimeStrings('01:05:30.5', '00:15:30',
scorm12_regex.CMITimespan),
).to.equal('01:20:40.5');
).to.equal('01:21:00.5');
});
it('17496:00:00 plus 49:35:52 equals 17545:35:52', () => {
expect(