From 8eb2d97513e85bb0540466bb716ea6b6adb678fa Mon Sep 17 00:00:00 2001 From: Emmanuel Gomez Date: Fri, 23 Aug 2024 12:02:55 -0700 Subject: [PATCH] cleanup: Separate `Context...`/`ResultExtension`, run `prettier`. --- src/AbstractCmi5.ts | 5 +- src/Cmi5Statements.ts | 31 +++-- src/constants/Cmi5ContextExtension.ts | 4 + src/constants/Cmi5Extension.ts | 5 - src/constants/Cmi5InteractionType.ts | 3 +- src/constants/Cmi5ResultExtension.ts | 4 + src/constants/index.ts | 3 +- test/__tests__/cmi5-statements.spec.ts | 180 ++++++++++++++----------- 8 files changed, 132 insertions(+), 103 deletions(-) create mode 100644 src/constants/Cmi5ContextExtension.ts delete mode 100644 src/constants/Cmi5Extension.ts create mode 100644 src/constants/Cmi5ResultExtension.ts diff --git a/src/AbstractCmi5.ts b/src/AbstractCmi5.ts index 1e1ea84..71a463a 100644 --- a/src/AbstractCmi5.ts +++ b/src/AbstractCmi5.ts @@ -45,7 +45,10 @@ import { export * from "./interfaces"; -function _applyTransform(mergedStatement: Statement, options: SendStatementOptions) { +function _applyTransform( + mergedStatement: Statement, + options: SendStatementOptions +) { return options && typeof options.transform === "function" ? options.transform(mergedStatement) : mergedStatement; diff --git a/src/Cmi5Statements.ts b/src/Cmi5Statements.ts index 49f50c1..6a48fe1 100644 --- a/src/Cmi5Statements.ts +++ b/src/Cmi5Statements.ts @@ -26,11 +26,13 @@ import { } from "./interfaces"; import { Cmi5ContextActivity, - Cmi5DefinedVerbs, Cmi5Extension, Cmi5InteractionIRI, + Cmi5DefinedVerbs, + Cmi5ContextExtension, + Cmi5InteractionIRI, Cmi5InteractionType, + Cmi5ResultExtension, } from "./constants"; - export function Cmi5DefinedStatement( ctx: LaunchContext, statement: Partial @@ -116,7 +118,7 @@ export function Cmi5PassStatement( score?: ResultScore | number, objectiveOrOptions?: ObjectiveActivity | PassOptions ): Statement { - const masteryScore = ctx.launchData.masteryScore + const masteryScore = ctx.launchData.masteryScore; // 10.0 xAPI State Data Model - https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#100-xapi-state-data-model if (ctx.launchData.launchMode !== "Normal") throw new Error("Can only send PASSED when launchMode is 'Normal'"); @@ -148,7 +150,7 @@ export function Cmi5PassStatement( ...(objective ? { parent: [objective] } : {}), }, ...(masteryScore - ? { extensions: { [Cmi5Extension.MASTERY_SCORE]: masteryScore } } + ? { extensions: { [Cmi5ContextExtension.MASTERY_SCORE]: masteryScore } } : {}), }, }); @@ -156,7 +158,7 @@ export function Cmi5PassStatement( export function Cmi5FailStatement( ctx: LaunchContext, - score?: ResultScore | number, + score?: ResultScore | number ): Statement { // 10.0 xAPI State Data Model - https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#100-xapi-state-data-model if (ctx.launchData.launchMode !== "Normal") @@ -182,8 +184,7 @@ export function Cmi5FailStatement( ...(ctx.launchData.masteryScore ? { extensions: { - [Cmi5Extension.MASTERY_SCORE]: - ctx.launchData.masteryScore, + [Cmi5ContextExtension.MASTERY_SCORE]: ctx.launchData.masteryScore, }, } : {}), @@ -215,7 +216,7 @@ export function Cmi5ProgressStatement( }, result: { extensions: { - [Cmi5Extension.PROGRESS]: percent, + [Cmi5ResultExtension.PROGRESS]: percent, }, }, }); @@ -615,9 +616,7 @@ export function Cmi5InteractionOtherStatement( { type: Cmi5InteractionIRI, interactionType: "other", - ...(correctAnswer - ? { correctResponsesPattern: [correctAnswer] } - : {}), + ...(correctAnswer ? { correctResponsesPattern: [correctAnswer] } : {}), ...(name ? { name } : {}), ...(description ? { description } : {}), }, @@ -663,7 +662,6 @@ export function Cmi5InteractionStatement( }); } - // Helper/utility functions // Formatting @@ -724,8 +722,9 @@ function _isNumericRange(candidate: unknown): candidate is NumericRange { ); } -function _didMeetMasteryScore(masteryScore: number, rScore?: ResultScore): boolean { - return rScore && - _isNumber(rScore.scaled) && - rScore.scaled >= masteryScore; +function _didMeetMasteryScore( + masteryScore: number, + rScore?: ResultScore +): boolean { + return rScore && _isNumber(rScore.scaled) && rScore.scaled >= masteryScore; } diff --git a/src/constants/Cmi5ContextExtension.ts b/src/constants/Cmi5ContextExtension.ts new file mode 100644 index 0000000..722d689 --- /dev/null +++ b/src/constants/Cmi5ContextExtension.ts @@ -0,0 +1,4 @@ +export class Cmi5ContextExtension { + public static readonly MASTERY_SCORE = + "https://w3id.org/xapi/cmi5/context/extensions/masteryscore"; +} diff --git a/src/constants/Cmi5Extension.ts b/src/constants/Cmi5Extension.ts deleted file mode 100644 index 33d9baf..0000000 --- a/src/constants/Cmi5Extension.ts +++ /dev/null @@ -1,5 +0,0 @@ -// 9.3 Verbs - https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#93-verbs -export class Cmi5Extension { - public static readonly MASTERY_SCORE = "https://w3id.org/xapi/cmi5/context/extensions/masteryscore"; - public static readonly PROGRESS = "https://w3id.org/xapi/cmi5/result/extensions/progress"; -} diff --git a/src/constants/Cmi5InteractionType.ts b/src/constants/Cmi5InteractionType.ts index 0445081..e9d5efc 100644 --- a/src/constants/Cmi5InteractionType.ts +++ b/src/constants/Cmi5InteractionType.ts @@ -1,4 +1,5 @@ -export const Cmi5InteractionIRI = "http://adlnet.gov/expapi/activities/cmi.interaction" as const; +export const Cmi5InteractionIRI = + "http://adlnet.gov/expapi/activities/cmi.interaction" as const; export class Cmi5InteractionType { public static readonly TRUE_FALSE = "true-false"; diff --git a/src/constants/Cmi5ResultExtension.ts b/src/constants/Cmi5ResultExtension.ts new file mode 100644 index 0000000..43eb00d --- /dev/null +++ b/src/constants/Cmi5ResultExtension.ts @@ -0,0 +1,4 @@ +export class Cmi5ResultExtension { + public static readonly PROGRESS = + "https://w3id.org/xapi/cmi5/result/extensions/progress"; +} diff --git a/src/constants/index.ts b/src/constants/index.ts index ad867c0..345104b 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,4 +1,5 @@ export * from "./Cmi5ContextActivity"; +export * from "./Cmi5ContextExtension"; export * from "./Cmi5DefinedVerbs"; -export * from "./Cmi5Extension"; export * from "./Cmi5InteractionType"; +export * from "./Cmi5ResultExtension"; diff --git a/test/__tests__/cmi5-statements.spec.ts b/test/__tests__/cmi5-statements.spec.ts index f638e7f..9933ad5 100644 --- a/test/__tests__/cmi5-statements.spec.ts +++ b/test/__tests__/cmi5-statements.spec.ts @@ -1,9 +1,16 @@ -import { LaunchContext, LaunchData, LaunchParameters } from "../../src/interfaces"; +import { + LaunchContext, + LaunchData, + LaunchParameters, +} from "../../src/interfaces"; import { beforeEach } from "node:test"; import { randomUUID } from "node:crypto"; import Cmi5 from "../../src/Cmi5"; import { Cmi5DefinedVerbs } from "../../src/constants"; -import { Cmi5CompleteStatement, Cmi5PassStatement } from "../../src/Cmi5Statements"; +import { + Cmi5CompleteStatement, + Cmi5PassStatement, +} from "../../src/Cmi5Statements"; import { ResultScore } from "@xapi/xapi"; describe("Cmi5 Statements", () => { @@ -13,17 +20,17 @@ describe("Cmi5 Statements", () => { endpoint: "http://fake-lrs.example.com", fetch: "http://fake-fetch.lms.example.com", registration: randomUUID(), - } + }; const DEFAULT_LAUNCH_DATA: LaunchData = { contextTemplate: {}, launchMode: "Normal", moveOn: "CompletedAndPassed", - } + }; const DEFAULT_LAUNCH_CONTEXT: LaunchContext = { initializedDate: new Date(), launchParameters: DEFAULT_LAUNCH_PARAMETERS, launchData: DEFAULT_LAUNCH_DATA, - } + }; describe("Cmi5CompleteStatement", () => { [ @@ -33,28 +40,32 @@ describe("Cmi5 Statements", () => { it(`calculates duration as time since initialized (${ex.seconds}s=${ex.expectedDuration})`, () => { const ctx: LaunchContext = { ...DEFAULT_LAUNCH_CONTEXT, - initializedDate: new Date(Date.now() - (ex.seconds * 1000)), - } + initializedDate: new Date(Date.now() - ex.seconds * 1000), + }; const statement = Cmi5CompleteStatement(ctx); expect(statement.verb).toEqual(Cmi5DefinedVerbs.COMPLETED); expect(statement.result.duration).toEqual(ex.expectedDuration); }); }); - ["Browse", "Review", null].forEach((launchMode: LaunchData["launchMode"]) => { - it(`throws exception if launchMode is '${launchMode}'`, async () => { - const ctx: LaunchContext = { - ...DEFAULT_LAUNCH_CONTEXT, - launchData: { - ...DEFAULT_LAUNCH_DATA, - launchMode: launchMode, - }, - } - expect(() => Cmi5CompleteStatement(ctx)).toThrow(expect.objectContaining({ - message: "Can only send COMPLETED when launchMode is 'Normal'", - })); - }); - }); + ["Browse", "Review", null].forEach( + (launchMode: LaunchData["launchMode"]) => { + it(`throws exception if launchMode is '${launchMode}'`, async () => { + const ctx: LaunchContext = { + ...DEFAULT_LAUNCH_CONTEXT, + launchData: { + ...DEFAULT_LAUNCH_DATA, + launchMode: launchMode, + }, + }; + expect(() => Cmi5CompleteStatement(ctx)).toThrow( + expect.objectContaining({ + message: "Can only send COMPLETED when launchMode is 'Normal'", + }) + ); + }); + } + ); }); describe("Cmi5PassStatement", () => { @@ -78,14 +89,16 @@ describe("Cmi5 Statements", () => { launchData: { ...DEFAULT_LAUNCH_DATA, masteryScore: 0.5, - } + }, }; describe("when `resultScore` is greater than `masteryScore`", () => { it("returns `statement.result.success === true` ", async () => { expect(ctx.launchData.masteryScore).toBeGreaterThan(0); const resultScore: ResultScore = { scaled: 0.9 }; - expect(resultScore.scaled).toBeGreaterThan(ctx.launchData.masteryScore); + expect(resultScore.scaled).toBeGreaterThan( + ctx.launchData.masteryScore + ); const statement = Cmi5PassStatement(ctx, resultScore); expect(statement.result.success).toEqual(true); }); @@ -93,7 +106,9 @@ describe("Cmi5 Statements", () => { it("returns a `ResultScore` when given a `ResultScore`", async () => { expect(ctx.launchData.masteryScore).toBeGreaterThan(0); const resultScore: ResultScore = { scaled: 0.9 }; - expect(resultScore.scaled).toBeGreaterThan(ctx.launchData.masteryScore); + expect(resultScore.scaled).toBeGreaterThan( + ctx.launchData.masteryScore + ); const statement = Cmi5PassStatement(ctx, resultScore); expect(statement.result.score).toEqual(resultScore); }); @@ -121,12 +136,12 @@ describe("Cmi5 Statements", () => { it(`sends duration as time since initialized (${ex.seconds}=${ex.expectedDuration})`, async () => { const ctx: LaunchContext = { ...DEFAULT_LAUNCH_CONTEXT, - initializedDate: new Date(Date.now() - (ex.seconds * 1000)), + initializedDate: new Date(Date.now() - ex.seconds * 1000), launchData: { ...DEFAULT_LAUNCH_DATA, masteryScore: 0.5, }, - } + }; expect(ctx.launchData.masteryScore).toBeGreaterThan(0); const scaledScore = 0.9; expect(scaledScore).toBeGreaterThan(0); @@ -141,69 +156,76 @@ describe("Cmi5 Statements", () => { expect(ctx.launchData.masteryScore).toBeGreaterThan(0); const resultScore = 0.4; expect(resultScore).toBeLessThan(ctx.launchData.masteryScore); - expect(() => Cmi5PassStatement(ctx, resultScore)).toThrow(expect.objectContaining({ - message: "Learner has not met Mastery Score", - })); + expect(() => Cmi5PassStatement(ctx, resultScore)).toThrow( + expect.objectContaining({ + message: "Learner has not met Mastery Score", + }) + ); }); }); describe("when `resultScore` is not provided", () => { it("throws an error that learner has not met mastery score", async () => { expect(ctx.launchData.masteryScore).toBeGreaterThan(0); - expect(() => Cmi5PassStatement(ctx)).toThrow(expect.objectContaining({ - message: "Learner has not met Mastery Score", - })); + expect(() => Cmi5PassStatement(ctx)).toThrow( + expect.objectContaining({ + message: "Learner has not met Mastery Score", + }) + ); }); }); }); - }); - // it("posts a PASSED statement with no result score when score not passed", async () => { - // mockCmi5.fakeLaunchData = { - // ...mockCmi5.fakeLaunchData, - // masteryScore: undefined, - // }; - // await initialize(mockCmi5); - // Cmi5.instance.pass(); - // expect(mockCmi5.mockXapiSendStatement).toHaveBeenCalledWith( - // expectActivityStatement(Cmi5.instance, Cmi5DefinedVerbs.PASSED, { - // result: expect.not.objectContaining({ - // score: expect.anything(), - // }), - // }) - // ); - // }); - // - // it("throws if passed score is beneath masteryScore", async () => { - // await initialize(mockCmi5); - // let exception; - // try { - // await Cmi5.instance.pass(0.1); - // } catch (err) { - // exception = err; - // } - // expect(exception).toEqual( - // expect.objectContaining({ - // message: "Learner has not met Mastery Score", - // }) - // ); - // }); - // - - ["Browse", "Review", null].forEach((launchMode: LaunchData["launchMode"]) => { - it(`throws exception if launchMode is '${launchMode}'`, async () => { - const ctx: LaunchContext = { - ...DEFAULT_LAUNCH_CONTEXT, - launchData: { - ...DEFAULT_LAUNCH_DATA, - launchMode: launchMode, - }, - } - expect(() => Cmi5PassStatement(ctx)).toThrow(expect.objectContaining({ - message: "Can only send PASSED when launchMode is 'Normal'", - })); - }); - }); + // it("posts a PASSED statement with no result score when score not passed", async () => { + // mockCmi5.fakeLaunchData = { + // ...mockCmi5.fakeLaunchData, + // masteryScore: undefined, + // }; + // await initialize(mockCmi5); + // Cmi5.instance.pass(); + // expect(mockCmi5.mockXapiSendStatement).toHaveBeenCalledWith( + // expectActivityStatement(Cmi5.instance, Cmi5DefinedVerbs.PASSED, { + // result: expect.not.objectContaining({ + // score: expect.anything(), + // }), + // }) + // ); + // }); + // + // it("throws if passed score is beneath masteryScore", async () => { + // await initialize(mockCmi5); + // let exception; + // try { + // await Cmi5.instance.pass(0.1); + // } catch (err) { + // exception = err; + // } + // expect(exception).toEqual( + // expect.objectContaining({ + // message: "Learner has not met Mastery Score", + // }) + // ); + // }); + // + + ["Browse", "Review", null].forEach( + (launchMode: LaunchData["launchMode"]) => { + it(`throws exception if launchMode is '${launchMode}'`, async () => { + const ctx: LaunchContext = { + ...DEFAULT_LAUNCH_CONTEXT, + launchData: { + ...DEFAULT_LAUNCH_DATA, + launchMode: launchMode, + }, + }; + expect(() => Cmi5PassStatement(ctx)).toThrow( + expect.objectContaining({ + message: "Can only send PASSED when launchMode is 'Normal'", + }) + ); + }); + } + ); }); });