@pie-element/mc-populated-blank 0.2.11 → 0.2.12

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 (62) hide show
  1. package/package.json +3 -14
  2. package/dist/controller/index.test.d.ts +0 -63
  3. package/dist/controller/index.test.d.ts.map +0 -1
  4. package/dist/delivery/AudioPlayer.test.d.ts +0 -2
  5. package/dist/delivery/AudioPlayer.test.d.ts.map +0 -1
  6. package/dist/delivery/ChoiceRow.test.d.ts +0 -2
  7. package/dist/delivery/ChoiceRow.test.d.ts.map +0 -1
  8. package/dist/delivery/ClozeMarker.test.d.ts +0 -2
  9. package/dist/delivery/ClozeMarker.test.d.ts.map +0 -1
  10. package/dist/delivery/McPopulatedBlank.transcript.test.d.ts +0 -2
  11. package/dist/delivery/McPopulatedBlank.transcript.test.d.ts.map +0 -1
  12. package/dist/delivery/computeAudioMode.test.d.ts +0 -2
  13. package/dist/delivery/computeAudioMode.test.d.ts.map +0 -1
  14. package/dist/delivery/computeChoiceCorrectness.test.d.ts +0 -19
  15. package/dist/delivery/computeChoiceCorrectness.test.d.ts.map +0 -1
  16. package/dist/delivery/computeDisplayState.test.d.ts +0 -38
  17. package/dist/delivery/computeDisplayState.test.d.ts.map +0 -1
  18. package/dist/delivery/computeLayoutProfile.test.d.ts +0 -2
  19. package/dist/delivery/computeLayoutProfile.test.d.ts.map +0 -1
  20. package/dist/delivery/computeLayoutStyle.test.d.ts +0 -2
  21. package/dist/delivery/computeLayoutStyle.test.d.ts.map +0 -1
  22. package/src/author/Author.svelte +0 -41
  23. package/src/author/index.ts +0 -28
  24. package/src/controller/defaults.ts +0 -61
  25. package/src/controller/index.test.ts +0 -428
  26. package/src/controller/index.ts +0 -372
  27. package/src/delivery/AudioPlayer.svelte +0 -246
  28. package/src/delivery/AudioPlayer.test.ts +0 -149
  29. package/src/delivery/ChoiceRow.svelte +0 -222
  30. package/src/delivery/ChoiceRow.test.ts +0 -179
  31. package/src/delivery/ClozeMarker.svelte +0 -83
  32. package/src/delivery/ClozeMarker.test.ts +0 -128
  33. package/src/delivery/McPopulatedBlank.svelte +0 -739
  34. package/src/delivery/McPopulatedBlank.transcript.test.ts +0 -154
  35. package/src/delivery/computeAudioMode.test.ts +0 -40
  36. package/src/delivery/computeAudioMode.ts +0 -11
  37. package/src/delivery/computeChoiceCorrectness.test.ts +0 -120
  38. package/src/delivery/computeChoiceCorrectness.ts +0 -42
  39. package/src/delivery/computeDisplayState.test.ts +0 -274
  40. package/src/delivery/computeDisplayState.ts +0 -101
  41. package/src/delivery/computeLayoutProfile.test.ts +0 -163
  42. package/src/delivery/computeLayoutProfile.ts +0 -72
  43. package/src/delivery/computeLayoutStyle.test.ts +0 -168
  44. package/src/delivery/computeLayoutStyle.ts +0 -128
  45. package/src/delivery/cqt-css/sel-r1-base.css +0 -59
  46. package/src/delivery/cqt-css/sel-r1-g-stem.css +0 -14
  47. package/src/delivery/cqt-css/sel-r1-gg-plus.css +0 -12
  48. package/src/delivery/cqt-css/sel-r1-ggplus.css +0 -17
  49. package/src/delivery/cqt-css/sel-r1-gplusggg.css +0 -25
  50. package/src/delivery/cqt-css/sel-r1-plusggg.css +0 -17
  51. package/src/delivery/cqt-css/sel-r1-s3.css +0 -85
  52. package/src/delivery/cqt-css/sel-vic.css +0 -99
  53. package/src/delivery/cqt-css/sr-vic.css +0 -63
  54. package/src/delivery/index.ts +0 -94
  55. package/src/delivery/variant-css-map.ts +0 -124
  56. package/src/index.iife.ts +0 -13
  57. package/src/index.ts +0 -1
  58. package/src/print/Print.svelte +0 -95
  59. package/src/print/index.ts +0 -18
  60. package/src/shared/layoutLimits.ts +0 -76
  61. package/src/shared/types.ts +0 -84
  62. package/src/types/css-raw.d.ts +0 -4
@@ -1,428 +0,0 @@
1
- /**
2
- * Unit tests for the McPopulatedBlank controller.
3
- *
4
- * Cases are grouped by exported function.
5
- *
6
- * getCorrectness
7
- * G1. No session → unanswered
8
- * G2. No choiceId → unanswered
9
- * G3. Correct choice selected → correct
10
- * G4. Wrong choice selected → incorrect
11
- *
12
- * outcome
13
- * O1. Empty session → score 0, empty: true
14
- * O2. No choiceId → score 0, empty: true
15
- * O3. Correct choice → score 1, empty: false
16
- * O4. Wrong choice → score 0, empty: false
17
- * O5. traceLog includes mode, selected id, correct id, final score
18
- *
19
- * model — choice ordering
20
- * M1. shuffle=false → original order preserved
21
- * M2. shuffle=true, instructor role → original order locked
22
- * M3. shuffle=true, lockChoiceOrder=true → original order locked
23
- * M4. shuffle=true, stored shuffle in session.data.shuffledValues → restored
24
- * M5. shuffle=true, stored shuffle in session.shuffledValues (legacy) → restored
25
- * M6. shuffle=true, stored shuffle has unknown ids → extras appended at end
26
- * M7. shuffle=true, no stored shuffle → shuffled; updateSession called with new order
27
- * M8. shuffle=true, no stored shuffle, no updateSession → shuffled silently (no throw)
28
- *
29
- * model — output fields
30
- * M9. evaluate mode → correctness, responseCorrect, correctChoiceId present
31
- * M10. gather mode → correctness fields absent
32
- * M11. instructor + view mode → teacherInstructions included when enabled
33
- * M12. student role → teacherInstructions always null
34
- * M13. prompt included when promptEnabled=true
35
- * M14. prompt null when promptEnabled=false
36
- * M15. audioUrl null when hasAudio=false
37
- * M16. audioUrl passed through when hasAudio=true
38
- * M17. disabled=true in view/evaluate mode, false in gather
39
- *
40
- * validate
41
- * V1. Valid question → empty errors object
42
- * V2. promptEnabled + empty prompt → prompt error
43
- * V3. populate_blank with no blank token → template error
44
- * V4. populate_blank with two blank tokens → template error
45
- * V5. audio_mc_only with a blank token → template error
46
- * V6. Unknown interactionMode → interactionMode error
47
- * V7. Fewer than two choices → choices error
48
- * V8. Choice missing id → choices error
49
- * V9. Text mode choice missing labelHtml → choices error
50
- * V10. Image mode choice missing imageUrl → choices error
51
- * V11. Image mode choice missing imageAlt → choices error
52
- * V12. correctChoiceId not in choices → correctChoiceId error
53
- * V13. hasAudio=true with no audioUrl → audioUrl error
54
- * V14. layoutLimits with a non-positive value → layoutLimits error
55
- * V15. layoutLimits with a valid value → no error
56
- *
57
- * createCorrectResponseSession
58
- * CR1. Instructor + mode≠evaluate → session with correct choiceId
59
- * CR2. Student role → null
60
- * CR3. Evaluate mode → null
61
- */
62
-
63
- import { describe, expect, it, vi } from 'vitest';
64
- import { getCorrectness, outcome, model, validate, createCorrectResponseSession } from './index';
65
-
66
- // ---------------------------------------------------------------------------
67
- // Shared fixtures
68
- // ---------------------------------------------------------------------------
69
-
70
- const CHOICES = [
71
- { id: 'a', labelHtml: '<p>Alpha</p>' },
72
- { id: 'b', labelHtml: '<p>Beta</p>' },
73
- { id: 'c', labelHtml: '<p>Gamma</p>' },
74
- ];
75
-
76
- const BASE_QUESTION = {
77
- id: '1',
78
- element: 'mc-populated-blank',
79
- template: '<p>The answer is {{blank}}.</p>',
80
- interactionMode: 'populate_blank' as const,
81
- choiceMode: 'text' as const,
82
- choices: CHOICES,
83
- correctChoiceId: 'b',
84
- hasAudio: false,
85
- shuffle: false,
86
- };
87
-
88
- const GATHER_ENV = { mode: 'gather' as const, role: 'student' as const };
89
- const EVALUATE_ENV = { mode: 'evaluate' as const, role: 'student' as const };
90
- const INSTRUCTOR_VIEW_ENV = { mode: 'view' as const, role: 'instructor' as const };
91
-
92
- // ---------------------------------------------------------------------------
93
- // getCorrectness
94
- // ---------------------------------------------------------------------------
95
-
96
- describe('getCorrectness', () => {
97
- it('G1: no session → unanswered', () => {
98
- expect(getCorrectness(BASE_QUESTION, null as any)).toBe('unanswered');
99
- });
100
-
101
- it('G2: session with no choiceId → unanswered', () => {
102
- expect(getCorrectness(BASE_QUESTION, {})).toBe('unanswered');
103
- });
104
-
105
- it('G3: correct choice selected → correct', () => {
106
- expect(getCorrectness(BASE_QUESTION, { choiceId: 'b' })).toBe('correct');
107
- });
108
-
109
- it('G4: wrong choice selected → incorrect', () => {
110
- expect(getCorrectness(BASE_QUESTION, { choiceId: 'a' })).toBe('incorrect');
111
- });
112
- });
113
-
114
- // ---------------------------------------------------------------------------
115
- // outcome
116
- // ---------------------------------------------------------------------------
117
-
118
- describe('outcome', () => {
119
- it('O1: empty session → score 0, empty: true', async () => {
120
- const result = await outcome(BASE_QUESTION, {}, GATHER_ENV);
121
- expect(result).toMatchObject({ score: 0, empty: true });
122
- });
123
-
124
- it('O2: session with no choiceId → score 0, empty: true', async () => {
125
- const result = await outcome(BASE_QUESTION, { choiceId: '' }, GATHER_ENV);
126
- expect(result).toMatchObject({ score: 0, empty: true });
127
- });
128
-
129
- it('O3: correct choice → score 1, empty: false', async () => {
130
- const result = await outcome(BASE_QUESTION, { choiceId: 'b' }, EVALUATE_ENV);
131
- expect(result).toMatchObject({ score: 1, empty: false });
132
- });
133
-
134
- it('O4: wrong choice → score 0, empty: false', async () => {
135
- const result = await outcome(BASE_QUESTION, { choiceId: 'a' }, EVALUATE_ENV);
136
- expect(result).toMatchObject({ score: 0, empty: false });
137
- });
138
-
139
- it('O5: traceLog includes mode, selected id, correct id, final score', async () => {
140
- const result = (await outcome(BASE_QUESTION, { choiceId: 'a' }, EVALUATE_ENV)) as any;
141
- const log = result.traceLog.join('\n');
142
- expect(log).toContain('evaluate');
143
- expect(log).toContain('a');
144
- expect(log).toContain('b');
145
- expect(log).toContain('0');
146
- });
147
- });
148
-
149
- // ---------------------------------------------------------------------------
150
- // model — choice ordering
151
- // ---------------------------------------------------------------------------
152
-
153
- describe('model — choice ordering', () => {
154
- it('M1: shuffle=false → original order preserved', async () => {
155
- const result = (await model({ ...BASE_QUESTION, shuffle: false }, {}, GATHER_ENV)) as any;
156
- expect(result.choices.map((c: any) => c.id)).toEqual(['a', 'b', 'c']);
157
- });
158
-
159
- it('M2: shuffle=true, instructor role → original order locked', async () => {
160
- const result = (await model(
161
- { ...BASE_QUESTION, shuffle: true },
162
- {},
163
- { mode: 'view', role: 'instructor' }
164
- )) as any;
165
- expect(result.choices.map((c: any) => c.id)).toEqual(['a', 'b', 'c']);
166
- });
167
-
168
- it('M3: shuffle=true, lockChoiceOrder=true → original order locked', async () => {
169
- const result = (await model(
170
- { ...BASE_QUESTION, shuffle: true, lockChoiceOrder: true },
171
- {},
172
- GATHER_ENV
173
- )) as any;
174
- expect(result.choices.map((c: any) => c.id)).toEqual(['a', 'b', 'c']);
175
- });
176
-
177
- it('M4: shuffle=true, stored shuffle in session.data.shuffledValues → order restored', async () => {
178
- const session = { data: { shuffledValues: ['c', 'a', 'b'] } };
179
- const result = (await model({ ...BASE_QUESTION, shuffle: true }, session, GATHER_ENV)) as any;
180
- expect(result.choices.map((c: any) => c.id)).toEqual(['c', 'a', 'b']);
181
- });
182
-
183
- it('M5: shuffle=true, stored shuffle in session.shuffledValues (legacy) → order restored', async () => {
184
- const session = { shuffledValues: ['b', 'c', 'a'] };
185
- const result = (await model({ ...BASE_QUESTION, shuffle: true }, session, GATHER_ENV)) as any;
186
- expect(result.choices.map((c: any) => c.id)).toEqual(['b', 'c', 'a']);
187
- });
188
-
189
- it('M6: shuffle=true, stored shuffle has unknown id → known ids first, extras appended', async () => {
190
- const session = { data: { shuffledValues: ['c', 'a', 'unknown'] } };
191
- const result = (await model({ ...BASE_QUESTION, shuffle: true }, session, GATHER_ENV)) as any;
192
- const ids = result.choices.map((c: any) => c.id);
193
- expect(ids[0]).toBe('c');
194
- expect(ids[1]).toBe('a');
195
- expect(ids[2]).toBe('b'); // leftover appended
196
- expect(ids).not.toContain('unknown');
197
- });
198
-
199
- it('M7: shuffle=true, no stored shuffle → shuffled; updateSession called with new order', async () => {
200
- const session = { id: 'sess-1', element: 'mc-populated-blank' };
201
- const updateSession = vi.fn().mockResolvedValue(undefined);
202
- await model({ ...BASE_QUESTION, shuffle: true }, session, GATHER_ENV, updateSession);
203
- expect(updateSession).toHaveBeenCalledOnce();
204
- const [id, element, data] = updateSession.mock.calls[0];
205
- expect(id).toBe('sess-1');
206
- expect(element).toBe('mc-populated-blank');
207
- expect(data.shuffledValues).toHaveLength(3);
208
- expect(new Set(data.shuffledValues)).toEqual(new Set(['a', 'b', 'c']));
209
- });
210
-
211
- it('M8: shuffle=true, no stored shuffle, no updateSession → shuffled silently without throw', async () => {
212
- const session = { id: 'sess-1', element: 'mc-populated-blank' };
213
- const result = (await model({ ...BASE_QUESTION, shuffle: true }, session, GATHER_ENV)) as any;
214
- expect(result.choices).toHaveLength(3);
215
- });
216
- });
217
-
218
- // ---------------------------------------------------------------------------
219
- // model — output fields
220
- // ---------------------------------------------------------------------------
221
-
222
- describe('model — output fields', () => {
223
- it('M9: evaluate mode → correctness, responseCorrect, correctChoiceId in output', async () => {
224
- const result = await model(BASE_QUESTION, { choiceId: 'b' }, EVALUATE_ENV);
225
- expect(result.correctness).toBe('correct');
226
- expect(result.responseCorrect).toBe(true);
227
- expect(result.correctChoiceId).toBe('b');
228
- });
229
-
230
- it('M10: gather mode → correctness fields absent', async () => {
231
- const result = await model(BASE_QUESTION, { choiceId: 'b' }, GATHER_ENV);
232
- expect(result.correctness).toBeUndefined();
233
- expect(result.responseCorrect).toBeUndefined();
234
- expect(result.correctChoiceId).toBeUndefined();
235
- });
236
-
237
- it('M11: instructor + view mode → teacherInstructions included when enabled', async () => {
238
- const q = {
239
- ...BASE_QUESTION,
240
- teacherInstructions: 'Do this.',
241
- teacherInstructionsEnabled: true,
242
- };
243
- const result = await model(q, {}, INSTRUCTOR_VIEW_ENV);
244
- expect(result.teacherInstructions).toBe('Do this.');
245
- });
246
-
247
- it('M12: student role → teacherInstructions always null', async () => {
248
- const q = {
249
- ...BASE_QUESTION,
250
- teacherInstructions: 'Do this.',
251
- teacherInstructionsEnabled: true,
252
- };
253
- const result = await model(q, {}, GATHER_ENV);
254
- expect(result.teacherInstructions).toBeNull();
255
- });
256
-
257
- it('M13: promptEnabled=true → prompt included', async () => {
258
- const q = { ...BASE_QUESTION, promptEnabled: true, prompt: '<p>Hello</p>' };
259
- const result = await model(q, {}, GATHER_ENV);
260
- expect(result.prompt).toBe('<p>Hello</p>');
261
- });
262
-
263
- it('M14: promptEnabled=false → prompt null', async () => {
264
- const q = { ...BASE_QUESTION, promptEnabled: false, prompt: '<p>Hello</p>' };
265
- const result = await model(q, {}, GATHER_ENV);
266
- expect(result.prompt).toBeNull();
267
- });
268
-
269
- it('M15: hasAudio=false → audioUrl null', async () => {
270
- const q = { ...BASE_QUESTION, hasAudio: false, audioUrl: 'https://example.com/a.mp3' };
271
- const result = await model(q, {}, GATHER_ENV);
272
- expect(result.audioUrl).toBeNull();
273
- });
274
-
275
- it('M16: hasAudio=true → audioUrl passed through', async () => {
276
- const q = { ...BASE_QUESTION, hasAudio: true, audioUrl: 'https://example.com/a.mp3' };
277
- const result = await model(q, {}, GATHER_ENV);
278
- expect(result.audioUrl).toBe('https://example.com/a.mp3');
279
- });
280
-
281
- it('M17: disabled=false in gather mode, disabled=true in view mode', async () => {
282
- const gather = await model(BASE_QUESTION, {}, GATHER_ENV);
283
- const view = await model(BASE_QUESTION, {}, { mode: 'view', role: 'student' });
284
- expect(gather.disabled).toBe(false);
285
- expect(view.disabled).toBe(true);
286
- });
287
- });
288
-
289
- // ---------------------------------------------------------------------------
290
- // validate
291
- // ---------------------------------------------------------------------------
292
-
293
- describe('validate', () => {
294
- it('V1: valid question → empty errors', () => {
295
- expect(validate(BASE_QUESTION)).toEqual({});
296
- });
297
-
298
- it('V2: promptEnabled + empty prompt → prompt error', () => {
299
- const errors = validate({ ...BASE_QUESTION, promptEnabled: true, prompt: '' });
300
- expect(errors.prompt).toBeTruthy();
301
- });
302
-
303
- it('V3: populate_blank with no blank token → template error', () => {
304
- const errors = validate({ ...BASE_QUESTION, template: '<p>No token here.</p>' });
305
- expect(errors.template).toBeTruthy();
306
- });
307
-
308
- it('V4: populate_blank with two blank tokens → template error', () => {
309
- const errors = validate({ ...BASE_QUESTION, template: '<p>{{blank}} and {{blank}}</p>' });
310
- expect(errors.template).toBeTruthy();
311
- });
312
-
313
- it('V5: audio_mc_only with a blank token in template → template error', () => {
314
- const errors = validate({
315
- ...BASE_QUESTION,
316
- interactionMode: 'audio_mc_only',
317
- template: '<p>{{blank}}</p>',
318
- });
319
- expect(errors.template).toBeTruthy();
320
- });
321
-
322
- it('V6: unknown interactionMode → interactionMode error', () => {
323
- const errors = validate({ ...BASE_QUESTION, interactionMode: 'unknown' as any });
324
- expect(errors.interactionMode).toBeTruthy();
325
- });
326
-
327
- it('V7: fewer than two choices → choices error', () => {
328
- const errors = validate({ ...BASE_QUESTION, choices: [CHOICES[0]] });
329
- expect(errors.choices).toBeTruthy();
330
- });
331
-
332
- it('V8: choice missing id → choices error', () => {
333
- const errors = validate({
334
- ...BASE_QUESTION,
335
- choices: [{ id: '', labelHtml: '<p>A</p>' }, CHOICES[1]],
336
- });
337
- expect(errors.choices).toBeTruthy();
338
- });
339
-
340
- it('V9: text mode choice missing labelHtml → choices error', () => {
341
- const errors = validate({
342
- ...BASE_QUESTION,
343
- choiceMode: 'text',
344
- choices: [{ id: 'a', labelHtml: '' }, CHOICES[1]],
345
- });
346
- expect(errors.choices).toBeTruthy();
347
- });
348
-
349
- it('V10: image mode choice missing imageUrl → choices error', () => {
350
- const errors = validate({
351
- ...BASE_QUESTION,
352
- choiceMode: 'image',
353
- choices: [
354
- { id: 'a', imageUrl: '', imageAlt: 'Alt' },
355
- { id: 'b', imageUrl: 'https://example.com/b.png', imageAlt: 'B' },
356
- ],
357
- });
358
- expect(errors.choices).toBeTruthy();
359
- });
360
-
361
- it('V11: image mode choice missing imageAlt → choices error', () => {
362
- const errors = validate({
363
- ...BASE_QUESTION,
364
- choiceMode: 'image',
365
- choices: [
366
- { id: 'a', imageUrl: 'https://example.com/a.png', imageAlt: '' },
367
- { id: 'b', imageUrl: 'https://example.com/b.png', imageAlt: 'B' },
368
- ],
369
- });
370
- expect(errors.choices).toBeTruthy();
371
- });
372
-
373
- it('V12: correctChoiceId not in choices → correctChoiceId error', () => {
374
- const errors = validate({ ...BASE_QUESTION, correctChoiceId: 'z' });
375
- expect(errors.correctChoiceId).toBeTruthy();
376
- });
377
-
378
- it('V13: hasAudio=true with no audioUrl → audioUrl error', () => {
379
- const errors = validate({ ...BASE_QUESTION, hasAudio: true, audioUrl: '' });
380
- expect(errors.audioUrl).toBeTruthy();
381
- });
382
-
383
- it('V14: layoutLimits with non-positive value → layoutLimits error', () => {
384
- const errors = validate({
385
- ...BASE_QUESTION,
386
- layoutLimits: { blankStandaloneWidthRem: -1 } as any,
387
- });
388
- expect(errors.layoutLimits).toBeTruthy();
389
- });
390
-
391
- it('V15: layoutLimits with valid positive value → no error', () => {
392
- const errors = validate({
393
- ...BASE_QUESTION,
394
- layoutLimits: { blankStandaloneWidthRem: 8 } as any,
395
- });
396
- expect(errors.layoutLimits).toBeUndefined();
397
- });
398
- });
399
-
400
- // ---------------------------------------------------------------------------
401
- // createCorrectResponseSession
402
- // ---------------------------------------------------------------------------
403
-
404
- describe('createCorrectResponseSession', () => {
405
- it('CR1: instructor + mode≠evaluate → session with correct choiceId', async () => {
406
- const result = await createCorrectResponseSession(BASE_QUESTION, {
407
- mode: 'view',
408
- role: 'instructor',
409
- });
410
- expect(result).toMatchObject({ choiceId: 'b' });
411
- });
412
-
413
- it('CR2: student role → null', async () => {
414
- const result = await createCorrectResponseSession(BASE_QUESTION, {
415
- mode: 'view',
416
- role: 'student',
417
- });
418
- expect(result).toBeNull();
419
- });
420
-
421
- it('CR3: evaluate mode (instructor) → null', async () => {
422
- const result = await createCorrectResponseSession(BASE_QUESTION, {
423
- mode: 'evaluate',
424
- role: 'instructor',
425
- });
426
- expect(result).toBeNull();
427
- });
428
- });