tessera-learn 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +1 -0
  2. package/dist/audit-BBJpQGqb.js +204 -0
  3. package/dist/audit-BBJpQGqb.js.map +1 -0
  4. package/dist/plugin/a11y-cli.d.ts +1 -0
  5. package/dist/plugin/a11y-cli.js +36 -0
  6. package/dist/plugin/a11y-cli.js.map +1 -0
  7. package/dist/plugin/cli.js +2 -1
  8. package/dist/plugin/cli.js.map +1 -1
  9. package/dist/plugin/index.d.ts +16 -1
  10. package/dist/plugin/index.d.ts.map +1 -1
  11. package/dist/plugin/index.js +85 -10
  12. package/dist/plugin/index.js.map +1 -1
  13. package/dist/{validation-D9DXlqNP.js → validation-B-xTvM9B.js} +342 -18
  14. package/dist/validation-B-xTvM9B.js.map +1 -0
  15. package/package.json +17 -2
  16. package/src/components/Accordion.svelte +3 -1
  17. package/src/components/AccordionItem.svelte +1 -5
  18. package/src/components/Audio.svelte +17 -3
  19. package/src/components/Callout.svelte +5 -1
  20. package/src/components/Carousel.svelte +24 -8
  21. package/src/components/DefaultLayout.svelte +41 -12
  22. package/src/components/FillInTheBlank.svelte +16 -6
  23. package/src/components/Image.svelte +12 -3
  24. package/src/components/LockedBanner.svelte +2 -1
  25. package/src/components/Matching.svelte +48 -19
  26. package/src/components/MediaTracks.svelte +21 -0
  27. package/src/components/MultipleChoice.svelte +33 -13
  28. package/src/components/Quiz.svelte +61 -20
  29. package/src/components/ResultIcon.svelte +20 -4
  30. package/src/components/RevealModal.svelte +25 -22
  31. package/src/components/Sorting.svelte +61 -26
  32. package/src/components/Transcript.svelte +37 -0
  33. package/src/components/Video.svelte +21 -18
  34. package/src/components/util.ts +3 -1
  35. package/src/components/video-embed.ts +25 -0
  36. package/src/index.ts +2 -7
  37. package/src/plugin/a11y/audit.ts +299 -0
  38. package/src/plugin/a11y/contrast.ts +67 -0
  39. package/src/plugin/a11y-cli.ts +35 -0
  40. package/src/plugin/cli.ts +4 -1
  41. package/src/plugin/export.ts +42 -14
  42. package/src/plugin/index.ts +216 -44
  43. package/src/plugin/manifest.ts +62 -22
  44. package/src/plugin/validation.ts +736 -122
  45. package/src/runtime/App.svelte +119 -48
  46. package/src/runtime/LoadingBar.svelte +12 -3
  47. package/src/runtime/Sidebar.svelte +24 -8
  48. package/src/runtime/access.ts +15 -3
  49. package/src/runtime/adapters/cmi5.ts +55 -33
  50. package/src/runtime/adapters/index.ts +22 -10
  51. package/src/runtime/adapters/retry.ts +25 -20
  52. package/src/runtime/adapters/scorm-base.ts +19 -15
  53. package/src/runtime/adapters/scorm12.ts +7 -8
  54. package/src/runtime/adapters/scorm2004.ts +11 -14
  55. package/src/runtime/adapters/web.ts +1 -1
  56. package/src/runtime/hooks.svelte.ts +152 -326
  57. package/src/runtime/interaction-format.ts +30 -12
  58. package/src/runtime/interaction.ts +44 -11
  59. package/src/runtime/navigation.svelte.ts +27 -11
  60. package/src/runtime/persistence.ts +2 -2
  61. package/src/runtime/progress.svelte.ts +13 -9
  62. package/src/runtime/quiz-engine.svelte.ts +361 -0
  63. package/src/runtime/quiz-policy.ts +9 -3
  64. package/src/runtime/types.ts +24 -2
  65. package/src/runtime/xapi/agent-rules.ts +4 -1
  66. package/src/runtime/xapi/client.ts +5 -5
  67. package/src/runtime/xapi/derive-actor.ts +2 -2
  68. package/src/runtime/xapi/publisher.ts +32 -29
  69. package/src/runtime/xapi/setup.ts +18 -15
  70. package/src/runtime/xapi/validation.ts +15 -6
  71. package/src/virtual.d.ts +4 -1
  72. package/styles/base.css +32 -11
  73. package/styles/layout.css +39 -18
  74. package/styles/theme.css +15 -3
  75. package/dist/validation-D9DXlqNP.js.map +0 -1
@@ -0,0 +1,361 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ import type { Interaction } from './interaction.js';
3
+ import type { QuizConfig } from './types.js';
4
+ import {
5
+ resolveFeedbackMode,
6
+ resolveRetryStrategy,
7
+ type QuizQuestionResult,
8
+ type FeedbackModePredicate,
9
+ type RetryStrategyPredicate,
10
+ } from './quiz-policy.js';
11
+ import type {
12
+ UseQuizInternalHandle,
13
+ UseQuizQuestionApi,
14
+ QuestionInternal,
15
+ Question,
16
+ } from './hooks.svelte.js';
17
+
18
+ /**
19
+ * Dependencies injected into {@link QuizEngine} so the engine itself stays
20
+ * framework- and DOM-free. The Svelte wrapper (`useQuiz`) provides the two
21
+ * callbacks that bridge to the host element and the LMS adapter.
22
+ */
23
+ export interface QuizEngineDeps {
24
+ quizConfig: QuizConfig;
25
+ /**
26
+ * Live accessor for the resolved passing threshold (config + LMS mastery
27
+ * override). Read on each access rather than captured, because the cmi5/SCORM
28
+ * mastery override mutates `pageContext.passingScore` after `useQuiz()` may
29
+ * already have run.
30
+ */
31
+ passingScore: () => number;
32
+ /** Wraps `adapterCtx.adapter.reportInteraction`; a no-op when there is no adapter. */
33
+ report: (
34
+ id: string,
35
+ interaction: Interaction,
36
+ correct: boolean | null,
37
+ ) => void;
38
+ /**
39
+ * Wraps the host-element `CustomEvent` dispatch. Returns `false` when the host
40
+ * element is null (the engine treats that as "no LMS bridge listener").
41
+ */
42
+ dispatch: (name: string, detail?: unknown) => boolean;
43
+ }
44
+
45
+ interface InternalQuestion {
46
+ id: string;
47
+ weight: number;
48
+ checkAnswer: (answer?: unknown) => boolean;
49
+ reset?: () => void;
50
+ interaction?: () => Interaction;
51
+ render: unknown;
52
+ }
53
+
54
+ /**
55
+ * The quiz engine: all reactive state, scoring, retry/feedback policy and the
56
+ * register/submit/retry lifecycle that used to live inside the `useQuiz`
57
+ * closure. Directly instantiable (and unit-testable) because the only two
58
+ * side-effecting touchpoints — DOM events and LMS reporting — are injected.
59
+ */
60
+ export class QuizEngine implements UseQuizInternalHandle {
61
+ #deps: QuizEngineDeps;
62
+ #feedbackPredicate: FeedbackModePredicate;
63
+ #retryPredicate: RetryStrategyPredicate;
64
+ #maxAttempts: number;
65
+
66
+ #internalQuestions = $state<InternalQuestion[]>([]);
67
+ #questionHandles = $state<QuestionInternal[]>([]);
68
+ #answers = new Map<number, unknown>();
69
+ #reportedAnswers = new Map<number, string>();
70
+ #answersVersion = $state(0);
71
+ #submitted = $state(false);
72
+ #reviewing = $state(false);
73
+ #score = $state(0);
74
+ #attemptCount = $state(0);
75
+ #submitCalled = false; // plain field, not $state — only the wrapper's onDestroy reads it
76
+ #feedbackShown = new SvelteSet<number>();
77
+ #lockedCorrect = new SvelteSet<number>();
78
+ #seenIds = new Set<string>();
79
+
80
+ constructor(deps: QuizEngineDeps) {
81
+ this.#deps = deps;
82
+ this.#maxAttempts = deps.quizConfig.maxAttempts ?? Infinity;
83
+ this.#feedbackPredicate = resolveFeedbackMode(deps.quizConfig);
84
+ this.#retryPredicate = resolveRetryStrategy(deps.quizConfig);
85
+ }
86
+
87
+ // Derived values are plain getters over $state, mirroring ProgressState (which
88
+ // has no $derived). Getters recompute on read with or without a tracking
89
+ // effect, so engine-construction tests need no $effect.root.
90
+ get #totalQuestions(): number {
91
+ return this.#internalQuestions.length;
92
+ }
93
+
94
+ get #allAnswered(): boolean {
95
+ void this.#answersVersion;
96
+ return (
97
+ this.#totalQuestions > 0 && this.#answers.size >= this.#totalQuestions
98
+ );
99
+ }
100
+
101
+ get state(): 'answering' | 'submitted' | 'reviewing' {
102
+ return this.#reviewing
103
+ ? 'reviewing'
104
+ : this.#submitted
105
+ ? 'submitted'
106
+ : 'answering';
107
+ }
108
+
109
+ get questions(): ReadonlyArray<Question> {
110
+ return this.#questionHandles;
111
+ }
112
+
113
+ get canSubmit(): boolean {
114
+ return !this.#submitted && this.#allAnswered;
115
+ }
116
+
117
+ get canRetry(): boolean {
118
+ return this.#submitted && this.#attemptCount < this.#maxAttempts;
119
+ }
120
+
121
+ get score(): number {
122
+ return this.#score;
123
+ }
124
+
125
+ get passingScore(): number {
126
+ return this.#deps.passingScore();
127
+ }
128
+
129
+ get attemptCount(): number {
130
+ return this.#attemptCount;
131
+ }
132
+
133
+ /** Dev-warning inputs the wrapper reads in onDestroy. */
134
+ get stats(): {
135
+ questionsCount: number;
136
+ answersCount: number;
137
+ submitCalled: boolean;
138
+ } {
139
+ return {
140
+ questionsCount: this.#internalQuestions.length,
141
+ answersCount: this.#answers.size,
142
+ submitCalled: this.#submitCalled,
143
+ };
144
+ }
145
+
146
+ registerQuestion(api: UseQuizQuestionApi): QuestionInternal {
147
+ if (this.#seenIds.has(api.id)) {
148
+ console.warn(
149
+ `[tessera] useQuiz: duplicate question id "${api.id}" — ` +
150
+ 'each question id must be unique within a quiz (LMS interaction records key by id).',
151
+ );
152
+ }
153
+ this.#seenIds.add(api.id);
154
+ const internal: InternalQuestion = {
155
+ id: api.id,
156
+ weight: typeof api.weight === 'number' && api.weight > 0 ? api.weight : 1,
157
+ checkAnswer: api.checkAnswer,
158
+ reset: api.reset,
159
+ interaction: api.interaction,
160
+ render: undefined,
161
+ };
162
+ this.#internalQuestions.push(internal);
163
+ const handle = this.#makeQuestionHandle(this.#internalQuestions.length - 1);
164
+ this.#questionHandles.push(handle);
165
+ return handle;
166
+ }
167
+
168
+ setAnswer(index: number, answer: unknown): void {
169
+ this.#answers.set(index, answer);
170
+ this.#answersVersion++;
171
+ this.#deps.dispatch('tessera-quiz-question-answered', { index });
172
+ }
173
+
174
+ getAnswer(index: number): unknown {
175
+ void this.#answersVersion;
176
+ return this.#answers.get(index);
177
+ }
178
+
179
+ setRender(index: number, render: unknown): void {
180
+ if (this.#internalQuestions[index])
181
+ this.#internalQuestions[index].render = render;
182
+ }
183
+
184
+ getRender(index: number): unknown {
185
+ return this.#internalQuestions[index]?.render;
186
+ }
187
+
188
+ feedbackVisible(index: number): boolean {
189
+ if (this.#deps.quizConfig.feedbackMode === 'never') return false;
190
+ return this.#feedbackPredicate({
191
+ questionIndex: index,
192
+ submitted: this.#submitted,
193
+ reviewing: this.#reviewing,
194
+ hasAnswered: this.#answers.has(index),
195
+ revealed: this.#feedbackShown.has(index),
196
+ attemptCount: this.#attemptCount,
197
+ });
198
+ }
199
+
200
+ revealFeedbackByIndex(index: number): void {
201
+ if (this.#deps.quizConfig.feedbackMode === 'never') return;
202
+ this.#feedbackShown.add(index);
203
+ }
204
+
205
+ isLockedCorrect(index: number): boolean {
206
+ return this.#lockedCorrect.has(index);
207
+ }
208
+
209
+ revealFeedback(q: Question): void {
210
+ const index = this.#internalQuestions.findIndex((iq) => iq.id === q.id);
211
+ if (index >= 0) this.revealFeedbackByIndex(index);
212
+ }
213
+
214
+ submit(): void {
215
+ this.#submitCalled = true;
216
+ if (this.#submitted) return;
217
+ if (!this.#allAnswered) return;
218
+ // Combined null-host guard + before-submit dispatch: dispatch() returns false
219
+ // when the host element is null, which is the silent-LMS-dropout case.
220
+ if (!this.#deps.dispatch('tessera-quiz-before-submit')) {
221
+ console.warn(
222
+ '[tessera] useQuiz: submit() ran but the host element was null — no LMS bridge ' +
223
+ 'listener exists, so this score will not be persisted. Make sure your custom ' +
224
+ 'quiz shell binds the element it passes to useQuiz({ element: () => ... }).',
225
+ );
226
+ return;
227
+ }
228
+
229
+ for (let i = 0; i < this.#internalQuestions.length; i++) this.#commit(i);
230
+
231
+ const { rounded } = this.#computeScore();
232
+ this.#score = rounded;
233
+ this.#submitted = true;
234
+ this.#attemptCount++;
235
+
236
+ this.#deps.dispatch('tessera-quiz-complete', { score: rounded });
237
+ }
238
+
239
+ startReview(): void {
240
+ if (!this.#submitted) return;
241
+ this.#reviewing = true;
242
+ }
243
+
244
+ exitReview(): void {
245
+ this.#reviewing = false;
246
+ }
247
+
248
+ retry(): void {
249
+ if (!this.canRetry) return;
250
+ const results: QuizQuestionResult[] = [];
251
+ for (let i = 0; i < this.#internalQuestions.length; i++) {
252
+ const a = this.#answers.has(i) ? this.#answers.get(i) : undefined;
253
+ results.push({
254
+ interaction:
255
+ this.#internalQuestions[i].interaction?.() ?? ({} as never),
256
+ correct: this.#internalQuestions[i].checkAnswer(a),
257
+ weight: this.#internalQuestions[i].weight,
258
+ });
259
+ }
260
+ const newLocked = this.#retryPredicate(results);
261
+ const preserved = new Map<number, unknown>();
262
+ for (const i of newLocked) {
263
+ if (this.#answers.has(i)) preserved.set(i, this.#answers.get(i));
264
+ }
265
+ this.#lockedCorrect.clear();
266
+ for (const i of newLocked) this.#lockedCorrect.add(i);
267
+ this.#answers.clear();
268
+ this.#reportedAnswers.clear();
269
+ for (const [i, a] of preserved) this.#answers.set(i, a);
270
+ for (let i = 0; i < this.#internalQuestions.length; i++) {
271
+ if (!newLocked.has(i) && this.#internalQuestions[i].reset)
272
+ this.#internalQuestions[i].reset!();
273
+ }
274
+ this.#answersVersion++;
275
+ this.#feedbackShown.clear();
276
+ this.#submitted = false;
277
+ this.#reviewing = false;
278
+ this.#score = 0;
279
+ this.#deps.dispatch('tessera-quiz-retry');
280
+ }
281
+
282
+ #commit(index: number): void {
283
+ const q = this.#internalQuestions[index];
284
+ if (!q || typeof q.interaction !== 'function') return;
285
+ const interaction = q.interaction();
286
+ if (!interaction) return;
287
+ const fingerprint = JSON.stringify(interaction);
288
+ if (this.#reportedAnswers.get(index) === fingerprint) return;
289
+ const answer = this.#answers.has(index)
290
+ ? this.#answers.get(index)
291
+ : undefined;
292
+ this.#deps.report(q.id, interaction, q.checkAnswer(answer));
293
+ this.#reportedAnswers.set(index, fingerprint);
294
+ }
295
+
296
+ #computeScore(): { rounded: number; correctCount: number } {
297
+ let weighted = 0;
298
+ let totalWeight = 0;
299
+ let correctCount = 0;
300
+ for (let i = 0; i < this.#internalQuestions.length; i++) {
301
+ const q = this.#internalQuestions[i];
302
+ const a = this.#answers.has(i) ? this.#answers.get(i) : undefined;
303
+ const ok = q.checkAnswer(a);
304
+ totalWeight += q.weight;
305
+ if (ok) {
306
+ weighted += q.weight;
307
+ correctCount++;
308
+ }
309
+ }
310
+ if (totalWeight === 0) return { rounded: 0, correctCount: 0 };
311
+ return {
312
+ rounded: Math.round((weighted / totalWeight) * 100),
313
+ correctCount,
314
+ };
315
+ }
316
+
317
+ #makeQuestionHandle(i: number): QuestionInternal {
318
+ const engine = this;
319
+ return {
320
+ get id() {
321
+ return engine.#internalQuestions[i].id;
322
+ },
323
+ get submitted() {
324
+ return engine.#submitted;
325
+ },
326
+ get correct() {
327
+ if (!engine.#submitted) return null;
328
+ const a = engine.#answers.has(i) ? engine.#answers.get(i) : undefined;
329
+ return engine.#internalQuestions[i].checkAnswer(a);
330
+ },
331
+ get answer() {
332
+ return engine.getAnswer(i);
333
+ },
334
+ get feedbackVisible() {
335
+ return engine.feedbackVisible(i);
336
+ },
337
+ get locked() {
338
+ return (
339
+ engine.#submitted ||
340
+ engine.feedbackVisible(i) ||
341
+ engine.isLockedCorrect(i)
342
+ );
343
+ },
344
+ get isLockedCorrect() {
345
+ return engine.isLockedCorrect(i);
346
+ },
347
+ get render() {
348
+ return engine.getRender(i);
349
+ },
350
+ setAnswer(a: unknown) {
351
+ engine.setAnswer(i, a);
352
+ },
353
+ commit() {
354
+ engine.#commit(i);
355
+ },
356
+ setRender(r: unknown) {
357
+ engine.setRender(i, r);
358
+ },
359
+ };
360
+ }
361
+ }
@@ -34,7 +34,9 @@ export interface FeedbackVisibilityState {
34
34
  }
35
35
 
36
36
  export type FeedbackModePredicate = (state: FeedbackVisibilityState) => boolean;
37
- export type RetryStrategyPredicate = (results: QuizQuestionResult[]) => Set<number>;
37
+ export type RetryStrategyPredicate = (
38
+ results: QuizQuestionResult[],
39
+ ) => Set<number>;
38
40
 
39
41
  /**
40
42
  * Resolve the configured feedback policy into the "should this question's
@@ -43,7 +45,9 @@ export type RetryStrategyPredicate = (results: QuizQuestionResult[]) => Set<numb
43
45
  * - `'review'` (default) — visible only while reviewing.
44
46
  * - `'never'` — never visible (`useQuiz` short-circuits before calling here).
45
47
  */
46
- export function resolveFeedbackMode(cfg: QuizConfig | undefined | null): FeedbackModePredicate {
48
+ export function resolveFeedbackMode(
49
+ cfg: QuizConfig | undefined | null,
50
+ ): FeedbackModePredicate {
47
51
  const mode = cfg?.feedbackMode;
48
52
  if (mode === 'immediate') return (s) => s.revealed || s.reviewing;
49
53
  if (mode === 'never') return () => false;
@@ -56,7 +60,9 @@ export function resolveFeedbackMode(cfg: QuizConfig | undefined | null): Feedbac
56
60
  * - `'incorrect-only'` — keep questions the learner got right.
57
61
  * - `'full'` (default) — reset everything.
58
62
  */
59
- export function resolveRetryStrategy(cfg: QuizConfig | undefined | null): RetryStrategyPredicate {
63
+ export function resolveRetryStrategy(
64
+ cfg: QuizConfig | undefined | null,
65
+ ): RetryStrategyPredicate {
60
66
  if (cfg?.retryMode === 'incorrect-only') {
61
67
  return (results) => {
62
68
  const locked = new Set<number>();
@@ -1,6 +1,14 @@
1
1
  import type { AccessFn } from './access.js';
2
2
  import type { XAPIAgent } from './xapi/types.js';
3
3
 
4
+ /**
5
+ * Quiz enum domains as runtime tuples. The unions below derive from these, and
6
+ * the build-time validator imports them too — so the accepted value set has a
7
+ * single source and can't drift between the types and the validator.
8
+ */
9
+ export const FEEDBACK_MODES = ['review', 'immediate', 'never'] as const;
10
+ export const RETRY_MODES = ['full', 'incorrect-only'] as const;
11
+
4
12
  /**
5
13
  * Per-page quiz configuration. Single source of truth — the build plugin
6
14
  * extracts this from `pageConfig.quiz` and embeds it in the manifest;
@@ -10,8 +18,8 @@ export interface QuizConfig {
10
18
  graded?: boolean;
11
19
  gatesProgress?: boolean;
12
20
  maxAttempts?: number;
13
- feedbackMode?: 'review' | 'immediate' | 'never';
14
- retryMode?: 'full' | 'incorrect-only';
21
+ feedbackMode?: (typeof FEEDBACK_MODES)[number];
22
+ retryMode?: (typeof RETRY_MODES)[number];
15
23
  }
16
24
 
17
25
  export interface CourseConfig {
@@ -19,6 +27,10 @@ export interface CourseConfig {
19
27
  description?: string;
20
28
  author?: string;
21
29
  version?: string;
30
+ /** BCP-47 language tag for <html lang>. Defaults to 'en'. WCAG 3.1.1. */
31
+ language?: string;
32
+ /** Accessibility checker configuration. */
33
+ a11y?: A11yConfig;
22
34
  branding?: {
23
35
  logo?: string;
24
36
  primaryColor?: string;
@@ -45,6 +57,16 @@ export interface CourseConfig {
45
57
  xapi?: XAPIConfig | XAPIConfig[];
46
58
  }
47
59
 
60
+ /** Accessibility checker configuration. */
61
+ export interface A11yConfig {
62
+ /** Build-gate severity for promotable Tier-1 rules + Tier-1a warnings. */
63
+ level?: 'warn' | 'error';
64
+ /** axe ruleset tags for the Tier-2 runtime auditor. */
65
+ standard?: 'wcag2a' | 'wcag2aa' | 'wcag21aa';
66
+ /** Per-rule escape hatch matched literally against each diagnostic's ID. */
67
+ ignore?: string[];
68
+ }
69
+
48
70
  export interface ManualCompletion {
49
71
  mode: 'manual';
50
72
  /**
@@ -45,7 +45,10 @@ export function validateAgent(actor: unknown): string | null {
45
45
  }
46
46
  }
47
47
  if (a.mbox_sha1sum !== undefined) {
48
- if (typeof a.mbox_sha1sum !== 'string' || !/^[0-9a-f]{40}$/i.test(a.mbox_sha1sum)) {
48
+ if (
49
+ typeof a.mbox_sha1sum !== 'string' ||
50
+ !/^[0-9a-f]{40}$/i.test(a.mbox_sha1sum)
51
+ ) {
49
52
  return '.mbox_sha1sum must be a 40-character hex string';
50
53
  }
51
54
  }
@@ -43,7 +43,7 @@ export class XAPIClient {
43
43
  */
44
44
  sendStatement(
45
45
  partial: PartialStatement,
46
- options?: SendStatementOptions
46
+ options?: SendStatementOptions,
47
47
  ): Promise<SendStatementResult> {
48
48
  try {
49
49
  validatePartialStatement(partial);
@@ -59,8 +59,8 @@ export class XAPIClient {
59
59
  if (this.#publishers.every(blocked)) {
60
60
  return Promise.reject(
61
61
  new XAPIConfigError(
62
- 'XAPIClient.sendStatement: page is unloading; author statements queued during unload are dropped to keep Terminated last (cmi5 §9.3.6).'
63
- )
62
+ 'XAPIClient.sendStatement: page is unloading; author statements queued during unload are dropped to keep Terminated last (cmi5 §9.3.6).',
63
+ ),
64
64
  );
65
65
  }
66
66
  const id = uuidv4();
@@ -80,9 +80,9 @@ export class XAPIClient {
80
80
  endpoint: pub.getEndpoint(),
81
81
  ok: false,
82
82
  error: new XAPIConfigError(
83
- 'destination skipped: cmi5 publisher is unloading; statement dropped to keep Terminated last (cmi5 §9.3.6).'
83
+ 'destination skipped: cmi5 publisher is unloading; statement dropped to keep Terminated last (cmi5 §9.3.6).',
84
84
  ),
85
- })
85
+ }),
86
86
  );
87
87
  continue;
88
88
  }
@@ -39,7 +39,7 @@ export function defaultAccountHomePage(activityId: string): string | null {
39
39
  export function synthesizeSCORM12Actor(
40
40
  api: SCORM12API,
41
41
  activityId: string,
42
- actorAccountHomePage?: string
42
+ actorAccountHomePage?: string,
43
43
  ): XAPIAgent | null {
44
44
  let id = '';
45
45
  let name = '';
@@ -68,7 +68,7 @@ export function synthesizeSCORM12Actor(
68
68
  export function synthesizeSCORM2004Actor(
69
69
  api: SCORM2004API,
70
70
  activityId: string,
71
- actorAccountHomePage?: string
71
+ actorAccountHomePage?: string,
72
72
  ): XAPIAgent | null {
73
73
  let id = '';
74
74
  let name = '';