• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xapijs / cmi5 / 11203443773

06 Oct 2024 04:27PM UTC coverage: 69.043% (+1.7%) from 67.366%
11203443773

push

github

web-flow
Merge pull request #274 from pelotech/feat/use-async-await

refactor: Extract statement construction functions, use `async`/`await`, begin extracting tests.

119 of 222 branches covered (53.6%)

Branch coverage included in aggregate %.

145 of 199 new or added lines in 7 files covered. (72.86%)

1 existing line in 1 file now uncovered.

249 of 311 relevant lines covered (80.06%)

21.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.9
/src/AbstractCmi5.ts
1
import axios, { AxiosPromise } from "axios";
1✔
2
import XAPI, {
1✔
3
  InteractionActivityDefinition,
4
  InteractionComponent,
5
  LanguageMap,
6
  ObjectiveActivity,
7
  ResultScore,
8
  Statement,
9
} from "@xapi/xapi";
10
import {
11
  AuthTokenResponse,
12
  LaunchData,
13
  LaunchParameters,
14
  LearnerPreferences,
15
  MoveOnOptions,
16
  NumericCriteria,
17
  PassOptions,
18
  Performance,
19
  PerformanceCriteria,
20
  Period,
21
  SendStatementOptions,
22
} from "./interfaces";
23
import { Cmi5DefinedVerbs } from "./constants";
1✔
24
import {
1✔
25
  Cmi5CompleteStatement,
26
  Cmi5DefinedStatement,
27
  Cmi5FailStatement,
28
  Cmi5InteractionChoiceStatement,
29
  Cmi5InteractionFillInStatement,
30
  Cmi5InteractionLikertStatement,
31
  Cmi5InteractionLongFillInStatement,
32
  Cmi5InteractionMatchingStatement,
33
  Cmi5InteractionNumericStatement,
34
  Cmi5InteractionOtherStatement,
35
  Cmi5InteractionPerformanceStatement,
36
  Cmi5InteractionSequencingStatement,
37
  Cmi5InteractionStatement,
38
  Cmi5InteractionTrueFalseStatement,
39
  Cmi5MoveOnStatements,
40
  Cmi5MoveOnStatementSendOptions,
41
  Cmi5PassStatement,
42
  Cmi5ProgressStatement,
43
  Cmi5TerminateStatement,
44
} from "./Cmi5Statements";
45

46
export * from "./interfaces";
1✔
47

48
function _applyTransform(
49
  mergedStatement: Statement,
50
  options: SendStatementOptions
51
) {
52
  return options && typeof options.transform === "function"
80✔
53
    ? options.transform(mergedStatement)
54
    : mergedStatement;
55
}
56

57
/**
58
 * Experience API cmi5 Profile (Quartz - 1st Edition)
59
 * Reference: https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md
60
 */
61
export default class AbstractCmi5 {
1✔
62
  private _launchParameters: LaunchParameters;
63
  private _launchData!: LaunchData;
64
  private _learnerPreferences!: LearnerPreferences;
65
  private _initializedDate!: Date;
66
  private _authToken: string | null = null;
55✔
67
  private _xapi: XAPI;
68

69
  constructor(launchParameters: LaunchParameters) {
70
    this._launchParameters = launchParameters;
55✔
71
    if (!this._launchParameters.fetch) {
55!
UNCOV
72
      throw Error("Unable to construct, no `fetch` parameter found in URL.");
×
73
    } else if (!this._launchParameters.endpoint) {
55!
74
      throw Error("Unable to construct, no `endpoint` parameter found in URL");
×
75
    } else if (!this._launchParameters.actor) {
55!
76
      throw Error("Unable to construct, no `actor` parameter found in URL.");
×
77
    } else if (!this._launchParameters.activityId) {
55!
78
      throw Error(
×
79
        "Unable to construct, no `activityId` parameter found in URL."
80
      );
81
    } else if (!this._launchParameters.registration) {
55!
82
      throw Error(
×
83
        "Unable to construct, no `registration` parameter found in URL."
84
      );
85
    }
86
  }
87

88
  public get xapi(): XAPI | null {
89
    return this._xapi;
×
90
  }
91

92
  public get isAuthenticated(): boolean {
93
    return Boolean(this._xapi);
1✔
94
  }
95

96
  public get launchParameters(): LaunchParameters | null {
97
    return this._launchParameters;
240✔
98
  }
99

100
  public getLaunchParameters(): LaunchParameters {
101
    return this._launchParameters;
50✔
102
  }
103

104
  public get launchData(): LaunchData {
105
    return this._launchData;
166✔
106
  }
107

108
  public getLaunchData(): LaunchData {
109
    return this._launchData;
1✔
110
  }
111

112
  // Best Practice #17 – Persist AU Session State - https://aicc.github.io/CMI-5_Spec_Current/best_practices/
113
  public getAuthToken(): string {
NEW
114
    return this._authToken;
×
115
  }
116

117
  public get initializedDate(): Date {
118
    return this._initializedDate;
36✔
119
  }
120

121
  public getInitializedDate(): Date {
NEW
122
    return this._initializedDate;
×
123
  }
124

125
  // 11.0 xAPI Agent Profile Data Model - https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#110-xapi-agent-profile-data-model
126
  public getLearnerPreferences(): LearnerPreferences {
127
    return this._learnerPreferences;
4✔
128
  }
129

130
  // "cmi5 defined" Statements
131
  public async initialize(sessionState?: {
132
    authToken: string;
133
    initializedDate: Date;
134
  }): AxiosPromise<string[] | void> {
135
    // Best Practice #17 – Persist AU Session State - https://aicc.github.io/CMI-5_Spec_Current/best_practices/
136
    const authToken = sessionState
52!
137
      ? sessionState.authToken
138
      : await this.getAuthTokenFromLMS(this._launchParameters.fetch);
139
    this._authToken = authToken;
48✔
140
    this._xapi = new XAPI({
48✔
141
      endpoint: this._launchParameters.endpoint,
142
      auth: `Basic ${authToken}`,
143
    });
144
    this._launchData = await this.getLaunchDataFromLMS();
48✔
145
    this._learnerPreferences = await this.getLearnerPreferencesFromLMS();
44✔
146

147
    if (sessionState) {
44!
148
      // Best Practice #17 – Persist AU Session State - https://aicc.github.io/CMI-5_Spec_Current/best_practices/
NEW
149
      this._initializedDate = sessionState.initializedDate;
×
150
    } else {
151
      this._initializedDate = new Date();
44✔
152
      // 9.3.2 Initialized - https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#932-initialized
153
      const statement = Cmi5DefinedStatement(this, {
44✔
154
        verb: Cmi5DefinedVerbs.INITIALIZED,
155
      });
156
      return this.sendXapiStatement(statement);
44✔
157
    }
158
  }
159

160
  public complete(options?: SendStatementOptions): AxiosPromise<string[]> {
161
    const statement = Cmi5CompleteStatement(this);
7✔
162
    return this.sendXapiStatement(statement, options);
4✔
163
  }
164

165
  public pass(
166
    score?: ResultScore | number,
167
    objectiveOrOptions?: ObjectiveActivity | PassOptions
168
  ): AxiosPromise<string[]> {
169
    const statement = Cmi5PassStatement(this, score, objectiveOrOptions);
11✔
170
    return this.sendXapiStatement(statement, objectiveOrOptions as PassOptions);
7✔
171
  }
172

173
  public fail(
174
    score?: ResultScore | number,
175
    options?: SendStatementOptions
176
  ): AxiosPromise<string[]> {
177
    const statement = Cmi5FailStatement(this, score);
8✔
178
    return this.sendXapiStatement(statement, options);
5✔
179
  }
180

181
  public terminate(): AxiosPromise<string[]> {
182
    const statement = Cmi5TerminateStatement(this);
2✔
183
    return this.sendXapiStatement(statement);
2✔
184
  }
185

186
  // "cmi5 allowed" Statements
187
  public progress(percent: number): AxiosPromise<string[]> {
NEW
188
    const statement = Cmi5ProgressStatement(this, percent);
×
NEW
189
    return this.sendXapiStatement(statement);
×
190
  }
191

192
  /* eslint-disable @typescript-eslint/no-unused-vars */
193
  public interactionTrueFalse(
194
    testId: string,
195
    questionId: string,
196
    answer: boolean,
197
    correctAnswer?: boolean,
198
    name?: LanguageMap,
199
    description?: LanguageMap,
200
    success?: boolean,
201
    duration?: Period,
202
    objective?: ObjectiveActivity
203
  ): AxiosPromise<string[]> {
204
    /* eslint-disable prefer-rest-params */
205
    // @ts-expect-error TS doesn't like spreading arguments
NEW
206
    const statement = Cmi5InteractionTrueFalseStatement(this, ...arguments);
×
207
    /* eslint-enable prefer-rest-params */
NEW
208
    return this.sendXapiStatement(statement);
×
209
  }
210
  /* eslint-enable @typescript-eslint/no-unused-vars */
211

212
  /* eslint-disable @typescript-eslint/no-unused-vars */
213
  public interactionChoice(
214
    testId: string,
215
    questionId: string,
216
    answerIds: string[],
217
    correctAnswerIds?: string[],
218
    choices?: InteractionComponent[],
219
    name?: LanguageMap,
220
    description?: LanguageMap,
221
    success?: boolean,
222
    duration?: Period,
223
    objective?: ObjectiveActivity
224
    /* eslint-enable @typescript-eslint/no-unused-vars */
225
  ): AxiosPromise<string[]> {
226
    /* eslint-disable prefer-rest-params */
227
    // @ts-expect-error TS doesn't like spreading arguments
NEW
228
    const statement = Cmi5InteractionChoiceStatement(this, ...arguments);
×
229
    /* eslint-enable prefer-rest-params */
NEW
230
    return this.sendXapiStatement(statement);
×
231
  }
232

233
  /* eslint-disable @typescript-eslint/no-unused-vars */
234
  public interactionFillIn(
235
    testId: string,
236
    questionId: string,
237
    answers: string[],
238
    correctAnswers?: string[],
239
    name?: LanguageMap,
240
    description?: LanguageMap,
241
    success?: boolean,
242
    duration?: Period,
243
    objective?: ObjectiveActivity
244
    /* eslint-enable @typescript-eslint/no-unused-vars */
245
  ): AxiosPromise<string[]> {
246
    /* eslint-disable prefer-rest-params */
247
    // @ts-expect-error TS doesn't like spreading arguments
NEW
248
    const statement = Cmi5InteractionFillInStatement(this, ...arguments);
×
249
    /* eslint-enable prefer-rest-params */
NEW
250
    return this.sendXapiStatement(statement);
×
251
  }
252

253
  /* eslint-disable @typescript-eslint/no-unused-vars */
254
  public interactionLongFillIn(
255
    testId: string,
256
    questionId: string,
257
    answers: string[],
258
    correctAnswers?: string[],
259
    name?: LanguageMap,
260
    description?: LanguageMap,
261
    success?: boolean,
262
    duration?: Period,
263
    objective?: ObjectiveActivity
264
    /* eslint-enable @typescript-eslint/no-unused-vars */
265
  ): AxiosPromise<string[]> {
266
    /* eslint-disable prefer-rest-params */
267
    // @ts-expect-error TS doesn't like spreading arguments
NEW
268
    const statement = Cmi5InteractionLongFillInStatement(this, ...arguments);
×
269
    /* eslint-enable prefer-rest-params */
NEW
270
    return this.sendXapiStatement(statement);
×
271
  }
272

273
  /* eslint-disable @typescript-eslint/no-unused-vars */
274
  public interactionLikert(
275
    testId: string,
276
    questionId: string,
277
    answerId: string,
278
    correctAnswerId?: string,
279
    scale?: InteractionComponent[],
280
    name?: LanguageMap,
281
    description?: LanguageMap,
282
    success?: boolean,
283
    duration?: Period,
284
    objective?: ObjectiveActivity
285
    /* eslint-enable @typescript-eslint/no-unused-vars */
286
  ): AxiosPromise<string[]> {
287
    /* eslint-disable prefer-rest-params */
288
    // @ts-expect-error TS doesn't like spreading arguments
NEW
289
    const statement = Cmi5InteractionLikertStatement(this, ...arguments);
×
290
    /* eslint-enable prefer-rest-params */
NEW
291
    return this.sendXapiStatement(statement);
×
292
  }
293

294
  /* eslint-disable @typescript-eslint/no-unused-vars */
295
  public interactionMatching(
296
    testId: string,
297
    questionId: string,
298
    answers: { [sourceId: string]: string },
299
    correctAnswers?: { [sourceId: string]: string },
300
    source?: InteractionComponent[],
301
    target?: InteractionComponent[],
302
    name?: LanguageMap,
303
    description?: LanguageMap,
304
    success?: boolean,
305
    duration?: Period,
306
    objective?: ObjectiveActivity
307
    /* eslint-enable @typescript-eslint/no-unused-vars */
308
  ): AxiosPromise<string[]> {
309
    /* eslint-disable prefer-rest-params */
310
    // @ts-expect-error TS doesn't like spreading arguments
NEW
311
    const statement = Cmi5InteractionMatchingStatement(this, ...arguments);
×
312
    /* eslint-enable prefer-rest-params */
NEW
313
    return this.sendXapiStatement(statement);
×
314
  }
315

316
  /* eslint-disable @typescript-eslint/no-unused-vars */
317
  public interactionPerformance(
318
    testId: string,
319
    questionId: string,
320
    answers: Performance,
321
    correctAnswers?: PerformanceCriteria[],
322
    steps?: InteractionComponent[],
323
    name?: LanguageMap,
324
    description?: LanguageMap,
325
    success?: boolean,
326
    duration?: Period,
327
    objective?: ObjectiveActivity
328
    /* eslint-enable @typescript-eslint/no-unused-vars */
329
  ): AxiosPromise<string[]> {
330
    /* eslint-disable prefer-rest-params */
331
    // @ts-expect-error TS doesn't like spreading arguments
NEW
332
    const statement = Cmi5InteractionPerformanceStatement(this, ...arguments);
×
333
    /* eslint-enable prefer-rest-params */
NEW
334
    return this.sendXapiStatement(statement);
×
335
  }
336

337
  /* eslint-disable @typescript-eslint/no-unused-vars */
338
  public interactionSequencing(
339
    testId: string,
340
    questionId: string,
341
    answerIds: string[],
342
    correctAnswerIds: string[],
343
    choices?: InteractionComponent[],
344
    name?: LanguageMap,
345
    description?: LanguageMap,
346
    success?: boolean,
347
    duration?: Period,
348
    objective?: ObjectiveActivity
349
    /* eslint-enable @typescript-eslint/no-unused-vars */
350
  ): AxiosPromise<string[]> {
351
    /* eslint-disable prefer-rest-params */
352
    // @ts-expect-error TS doesn't like spreading arguments
NEW
353
    const statement = Cmi5InteractionSequencingStatement(this, ...arguments);
×
354
    /* eslint-enable prefer-rest-params */
NEW
355
    return this.sendXapiStatement(statement);
×
356
  }
357

358
  /* eslint-disable @typescript-eslint/no-unused-vars */
359
  public interactionNumeric(
360
    testId: string,
361
    questionId: string,
362
    answer: number,
363
    correctAnswer: NumericCriteria,
364
    name?: LanguageMap,
365
    description?: LanguageMap,
366
    success?: boolean,
367
    duration?: Period,
368
    objective?: ObjectiveActivity
369
    /* eslint-enable @typescript-eslint/no-unused-vars */
370
  ): AxiosPromise<string[]> {
371
    /* eslint-disable prefer-rest-params */
372
    // @ts-expect-error TS doesn't like spreading arguments
NEW
373
    const statement = Cmi5InteractionNumericStatement(this, ...arguments);
×
374
    /* eslint-enable prefer-rest-params */
NEW
375
    return this.sendXapiStatement(statement);
×
376
  }
377

378
  /* eslint-disable @typescript-eslint/no-unused-vars */
379
  public interactionOther(
380
    testId: string,
381
    questionId: string,
382
    answer: string,
383
    correctAnswer: string,
384
    name?: LanguageMap,
385
    description?: LanguageMap,
386
    success?: boolean,
387
    duration?: Period,
388
    objective?: ObjectiveActivity
389
    /* eslint-enable @typescript-eslint/no-unused-vars */
390
  ): AxiosPromise<string[]> {
391
    /* eslint-disable prefer-rest-params */
392
    // @ts-expect-error TS doesn't like spreading arguments
NEW
393
    const statement = Cmi5InteractionOtherStatement(this, ...arguments);
×
394
    /* eslint-enable prefer-rest-params */
NEW
395
    return this.sendXapiStatement(statement);
×
396
  }
397

398
  /* eslint-disable @typescript-eslint/no-unused-vars */
399
  public interaction(
400
    testId: string,
401
    questionId: string,
402
    response: string,
403
    interactionDefinition: InteractionActivityDefinition,
404
    success?: boolean,
405
    duration?: Period,
406
    objective?: ObjectiveActivity
407
    /* eslint-enable @typescript-eslint/no-unused-vars */
408
  ): AxiosPromise<string[]> {
409
    /* eslint-disable prefer-rest-params */
410
    // @ts-expect-error TS doesn't like spreading arguments
NEW
411
    const statement = Cmi5InteractionStatement(this, ...arguments);
×
412
    /* eslint-enable prefer-rest-params */
NEW
413
    return this.sendXapiStatement(statement);
×
414
  }
415

416
  public async moveOn(options?: MoveOnOptions): Promise<string[]> {
417
    const moveOnStatements = Cmi5MoveOnStatements(this, options);
7✔
418
    const sendOptions = Cmi5MoveOnStatementSendOptions(this, options);
7✔
419
    const newStatementIds: string[] = [];
7✔
420
    for (const statement of moveOnStatements) {
7✔
421
      await this.sendXapiStatement(statement, sendOptions);
18✔
422
      newStatementIds.push(statement.id);
18✔
423
    }
424
    return newStatementIds;
7✔
425
  }
426

427
  private async getAuthTokenFromLMS(fetchUrl: string): Promise<string> {
428
    const response = await axios.post<AuthTokenResponse>(fetchUrl);
52✔
429
    return response.data["auth-token"];
48✔
430
  }
431

432
  private async getLaunchDataFromLMS(): Promise<LaunchData> {
433
    const launchDataResponse = await (this._xapi.getState({
48✔
434
      agent: this._launchParameters.actor,
435
      activityId: this._launchParameters.activityId,
436
      stateId: "LMS.LaunchData",
437
      registration: this._launchParameters.registration,
438
    }) as AxiosPromise<LaunchData>);
439
    return launchDataResponse.data;
44✔
440
  }
441

442
  private async getLearnerPreferencesFromLMS(): Promise<LearnerPreferences> {
443
    try {
44✔
444
      const learnerPrefResponse = await (this._xapi.getAgentProfile({
44✔
445
        agent: this._launchParameters.actor,
446
        profileId: "cmi5LearnerPreferences",
447
      }) as AxiosPromise<LearnerPreferences>);
448
      return learnerPrefResponse.data;
40✔
449
    } catch (err) {
450
      return {};
4✔
451
    }
452
  }
453

454
  public async sendXapiStatement(
455
    statement: Statement,
456
    options?: SendStatementOptions
457
  ): AxiosPromise<string[]> {
458
    const sendStatement = _applyTransform(statement, options);
80✔
459
    return this._xapi.sendStatement({
80✔
460
      statement: sendStatement,
461
    });
462
  }
463
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc