@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,154 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { mount, unmount, flushSync } from 'svelte';
3
- import McPopulatedBlank from './McPopulatedBlank.svelte';
4
-
5
- const BASE_MODEL = {
6
- id: '1',
7
- element: 'mc-populated-blank',
8
- template: '<p>{{blank}}</p>',
9
- choiceMode: 'text',
10
- choices: [
11
- { id: 'a', labelHtml: 'louk' },
12
- { id: 'b', labelHtml: 'lok' },
13
- { id: 'c', labelHtml: 'look' },
14
- ],
15
- correctChoiceId: 'c',
16
- hasAudio: true,
17
- audioUrl: 'https://example.com/audio.mp3',
18
- audioTranscript: 'The word is look. Pick the correct spelling of the word look.',
19
- interactionMode: 'populate_blank',
20
- sentenceHtml: '',
21
- layoutProfile: 'audio_blank_only',
22
- customType: 'sel_r1-_plusggg',
23
- useFeatureButtonAudio: true,
24
- choiceLayout: 'horizontal',
25
- autoplayAudioEnabled: false,
26
- completeAudioEnabled: false,
27
- showVisibleTranscript: false,
28
- mode: 'gather',
29
- };
30
-
31
- function mountComponent(modelOverrides: Record<string, unknown> = {}) {
32
- const target = document.createElement('div');
33
- document.body.appendChild(target);
34
- const component = mount(McPopulatedBlank as any, {
35
- target,
36
- props: { model: { ...BASE_MODEL, ...modelOverrides }, session: {} },
37
- });
38
- return { target, component };
39
- }
40
-
41
- const mounts: Array<{ target: HTMLElement; component: ReturnType<typeof mount> }> = [];
42
-
43
- afterEach(() => {
44
- for (const { target, component } of mounts.splice(0)) {
45
- unmount(component);
46
- target.remove();
47
- }
48
- });
49
-
50
- describe('McPopulatedBlank — transcript rendering', () => {
51
- it('renders transcript in the DOM when audioTranscript is set', () => {
52
- const { target, component } = mountComponent();
53
- mounts.push({ target, component });
54
- flushSync();
55
- expect(target.querySelector('.pie-audio-transcript')).not.toBeNull();
56
- });
57
-
58
- it('hides transcript with sr-only when showVisibleTranscript is false', () => {
59
- const { target, component } = mountComponent({ showVisibleTranscript: false });
60
- mounts.push({ target, component });
61
- flushSync();
62
- const transcript = target.querySelector('.pie-audio-transcript');
63
- expect(transcript?.classList.contains('sr-only')).toBe(true);
64
- });
65
-
66
- it('shows transcript without sr-only when ancestor has rli-with-audio-transcript class', async () => {
67
- const wrapper = document.createElement('div');
68
- document.body.appendChild(wrapper);
69
- const target = document.createElement('div');
70
- wrapper.appendChild(target);
71
- const component = mount(McPopulatedBlank as any, {
72
- target,
73
- props: { model: { ...BASE_MODEL }, session: {} },
74
- });
75
- mounts.push({ target, component });
76
- flushSync();
77
-
78
- wrapper.classList.add('rli-with-audio-transcript');
79
- await new Promise((r) => setTimeout(r, 0));
80
- flushSync();
81
-
82
- const transcript = target.querySelector('.pie-audio-transcript');
83
- expect(transcript?.classList.contains('sr-only')).toBe(false);
84
- wrapper.remove();
85
- });
86
-
87
- it('transcript text has no label prefix', async () => {
88
- const wrapper = document.createElement('div');
89
- document.body.appendChild(wrapper);
90
- const target = document.createElement('div');
91
- wrapper.appendChild(target);
92
- const component = mount(McPopulatedBlank as any, {
93
- target,
94
- props: { model: { ...BASE_MODEL }, session: {} },
95
- });
96
- mounts.push({ target, component });
97
- flushSync();
98
-
99
- wrapper.classList.add('rli-with-audio-transcript');
100
- await new Promise((r) => setTimeout(r, 0));
101
- flushSync();
102
-
103
- const transcript = target.querySelector('.pie-audio-transcript');
104
- expect(transcript?.textContent?.trim()).not.toMatch(/^Transcript:/i);
105
- wrapper.remove();
106
- });
107
-
108
- it('transcript contains the audioTranscript text', () => {
109
- const { target, component } = mountComponent();
110
- mounts.push({ target, component });
111
- flushSync();
112
- const transcript = target.querySelector('.pie-audio-transcript');
113
- expect(transcript?.textContent).toContain(BASE_MODEL.audioTranscript);
114
- });
115
-
116
- it('omits transcript element when audioTranscript is empty', () => {
117
- const { target, component } = mountComponent({ audioTranscript: '' });
118
- mounts.push({ target, component });
119
- flushSync();
120
- expect(target.querySelector('.pie-audio-transcript')).toBeNull();
121
- });
122
-
123
- it('transcript renders before the first choice tile', () => {
124
- const { target, component } = mountComponent();
125
- mounts.push({ target, component });
126
- flushSync();
127
- const all = Array.from(target.querySelectorAll('.pie-audio-transcript, .pie-choice'));
128
- expect(all[0].classList.contains('pie-audio-transcript')).toBe(true);
129
- });
130
-
131
- it('shows transcript when ancestor gains rli-with-audio-transcript class', async () => {
132
- const wrapper = document.createElement('div');
133
- document.body.appendChild(wrapper);
134
- const target = document.createElement('div');
135
- wrapper.appendChild(target);
136
- const component = mount(McPopulatedBlank as any, {
137
- target,
138
- props: { model: { ...BASE_MODEL, showVisibleTranscript: false }, session: {} },
139
- });
140
- mounts.push({ target, component });
141
- flushSync();
142
-
143
- const transcript = target.querySelector('.pie-audio-transcript');
144
- expect(transcript?.classList.contains('sr-only')).toBe(true);
145
-
146
- wrapper.classList.add('rli-with-audio-transcript');
147
- // Allow MutationObserver microtask to fire
148
- await new Promise((r) => setTimeout(r, 0));
149
- flushSync();
150
-
151
- expect(transcript?.classList.contains('sr-only')).toBe(false);
152
- wrapper.remove();
153
- });
154
- });
@@ -1,40 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { computeAudioMode } from './computeAudioMode';
3
-
4
- describe('computeAudioMode', () => {
5
- it('C1: no audio → none regardless of audioUrl', () => {
6
- expect(
7
- computeAudioMode({
8
- hasAudio: false,
9
- audioUrl: 'http://example.com/a.mp3',
10
- useFeatureButtonAudio: true,
11
- })
12
- ).toBe('none');
13
- });
14
-
15
- it('C2: audio enabled but no audioUrl → error', () => {
16
- expect(
17
- computeAudioMode({ hasAudio: true, audioUrl: undefined, useFeatureButtonAudio: false })
18
- ).toBe('error');
19
- });
20
-
21
- it('C3: audio enabled, url present, feature button → feature-button', () => {
22
- expect(
23
- computeAudioMode({
24
- hasAudio: true,
25
- audioUrl: 'http://example.com/a.mp3',
26
- useFeatureButtonAudio: true,
27
- })
28
- ).toBe('feature-button');
29
- });
30
-
31
- it('C4: audio enabled, url present, no feature button → controls', () => {
32
- expect(
33
- computeAudioMode({
34
- hasAudio: true,
35
- audioUrl: 'http://example.com/a.mp3',
36
- useFeatureButtonAudio: false,
37
- })
38
- ).toBe('controls');
39
- });
40
- });
@@ -1,11 +0,0 @@
1
- export type AudioMode = 'feature-button' | 'controls' | 'error' | 'none';
2
-
3
- export function computeAudioMode(params: {
4
- hasAudio: boolean;
5
- audioUrl: string | undefined;
6
- useFeatureButtonAudio: boolean;
7
- }): AudioMode {
8
- if (!params.hasAudio) return 'none';
9
- if (!params.audioUrl) return 'error';
10
- return params.useFeatureButtonAudio ? 'feature-button' : 'controls';
11
- }
@@ -1,120 +0,0 @@
1
- /**
2
- * Unit tests for computeChoiceCorrectness.
3
- *
4
- * This is the harness for extracting the choiceCorrectnessById state machine
5
- * from McPopulatedBlank.svelte. The function takes five pure inputs and returns
6
- * a Map — no component rendering required.
7
- *
8
- * Cases:
9
- * C1. Not evaluate mode → empty map
10
- * C2. Evaluate mode, no correctChoiceId → empty map
11
- * C3. Reveal active, student chose correctly → { correctId: 'correct' } only
12
- * C4. Reveal active, student chose wrong → { correctId: 'correct' } only (student badge suppressed)
13
- * C5. Evaluate, unanswered → { correctId: 'incorrect' }
14
- * C6. Evaluate, student chose correctly → { correctId: 'correct' }
15
- * C7. Evaluate, student chose wrong → { studentId: 'incorrect', correctId: 'incorrect' }
16
- * C8. Empty-string ids treated as "no selection" → C5 path, not C7
17
- */
18
-
19
- import { describe, expect, it } from 'vitest';
20
- import { computeChoiceCorrectness } from './computeChoiceCorrectness';
21
-
22
- describe('computeChoiceCorrectness', () => {
23
- // C1 — not in evaluate mode: no badges regardless of selection
24
- it('C1: returns empty map when not in evaluate mode', () => {
25
- const result = computeChoiceCorrectness({
26
- isEvaluateMode: false,
27
- correctChoiceId: 'choice-a',
28
- selectedId: 'choice-b',
29
- showCorrectAnswer: false,
30
- });
31
- expect(result.size).toBe(0);
32
- });
33
-
34
- // C2 — evaluate mode but no correct answer defined: nothing to badge
35
- it('C2: returns empty map when correctChoiceId is absent', () => {
36
- const result = computeChoiceCorrectness({
37
- isEvaluateMode: true,
38
- correctChoiceId: '',
39
- selectedId: 'choice-b',
40
- showCorrectAnswer: false,
41
- });
42
- expect(result.size).toBe(0);
43
- });
44
-
45
- // C3 — reveal active, student answered correctly: only the correct badge
46
- it('C3: reveal active + correct selection → correct badge on correct choice only', () => {
47
- const result = computeChoiceCorrectness({
48
- isEvaluateMode: true,
49
- correctChoiceId: 'choice-a',
50
- selectedId: 'choice-a',
51
- showCorrectAnswer: true,
52
- });
53
- expect(result.size).toBe(1);
54
- expect(result.get('choice-a')).toBe('correct');
55
- });
56
-
57
- // C4 — reveal active, student chose wrong: correct badge only, student's badge suppressed
58
- it('C4: reveal active + wrong selection → correct badge only, student choice suppressed', () => {
59
- const result = computeChoiceCorrectness({
60
- isEvaluateMode: true,
61
- correctChoiceId: 'choice-a',
62
- selectedId: 'choice-b',
63
- showCorrectAnswer: true,
64
- });
65
- expect(result.size).toBe(1);
66
- expect(result.get('choice-a')).toBe('correct');
67
- expect(result.has('choice-b')).toBe(false);
68
- });
69
-
70
- // C5 — evaluate mode, student did not answer: flag the correct choice as missed
71
- it('C5: unanswered in evaluate mode → correct choice marked incorrect (missed)', () => {
72
- const result = computeChoiceCorrectness({
73
- isEvaluateMode: true,
74
- correctChoiceId: 'choice-a',
75
- selectedId: '',
76
- showCorrectAnswer: false,
77
- });
78
- expect(result.size).toBe(1);
79
- expect(result.get('choice-a')).toBe('incorrect');
80
- });
81
-
82
- // C6 — evaluate mode, student chose the right answer
83
- it('C6: correct selection in evaluate mode → correct badge on correct choice', () => {
84
- const result = computeChoiceCorrectness({
85
- isEvaluateMode: true,
86
- correctChoiceId: 'choice-a',
87
- selectedId: 'choice-a',
88
- showCorrectAnswer: false,
89
- });
90
- expect(result.size).toBe(1);
91
- expect(result.get('choice-a')).toBe('correct');
92
- });
93
-
94
- // C7 — evaluate mode, student chose the wrong answer: both choices badged incorrect
95
- it('C7: wrong selection in evaluate mode → student choice and correct choice both marked incorrect', () => {
96
- const result = computeChoiceCorrectness({
97
- isEvaluateMode: true,
98
- correctChoiceId: 'choice-a',
99
- selectedId: 'choice-b',
100
- showCorrectAnswer: false,
101
- });
102
- expect(result.size).toBe(2);
103
- expect(result.get('choice-b')).toBe('incorrect');
104
- expect(result.get('choice-a')).toBe('incorrect');
105
- });
106
-
107
- // C8 — both ids are empty strings: empty selectedId is "no selection", not a wrong answer
108
- it('C8: empty-string selectedId is treated as unanswered (C5 path), not wrong answer (C7 path)', () => {
109
- const result = computeChoiceCorrectness({
110
- isEvaluateMode: true,
111
- correctChoiceId: 'choice-a',
112
- selectedId: '',
113
- showCorrectAnswer: false,
114
- });
115
- // Must be C5 (size 1, correct choice marked incorrect) not C7 (size 2)
116
- expect(result.size).toBe(1);
117
- expect(result.get('choice-a')).toBe('incorrect');
118
- expect(result.has('')).toBe(false);
119
- });
120
- });
@@ -1,42 +0,0 @@
1
- export interface ChoiceCorrectnessParams {
2
- isEvaluateMode: boolean;
3
- correctChoiceId: string;
4
- selectedId: string;
5
- showCorrectAnswer: boolean;
6
- }
7
-
8
- /**
9
- * Computes which choice ids should display a correctness badge and what kind.
10
- *
11
- * Reveal mode (showCorrectAnswer=true) shows only the canonical correct answer —
12
- * the student's wrong selection is deliberately suppressed so the UI only
13
- * highlights what to remember, not what to regret.
14
- *
15
- * Unanswered-in-evaluate (selectedId='') treats the correct choice as missed
16
- * (incorrect badge) rather than triggering the two-badge wrong-answer path.
17
- */
18
- export function computeChoiceCorrectness(
19
- params: ChoiceCorrectnessParams
20
- ): Map<string, 'correct' | 'incorrect'> {
21
- const map = new Map<string, 'correct' | 'incorrect'>();
22
- const { isEvaluateMode, correctChoiceId, selectedId, showCorrectAnswer } = params;
23
-
24
- if (!isEvaluateMode || !correctChoiceId) {
25
- return map;
26
- }
27
- if (showCorrectAnswer) {
28
- map.set(correctChoiceId, 'correct');
29
- return map;
30
- }
31
- if (!selectedId) {
32
- map.set(correctChoiceId, 'incorrect');
33
- return map;
34
- }
35
- if (selectedId === correctChoiceId) {
36
- map.set(correctChoiceId, 'correct');
37
- return map;
38
- }
39
- map.set(selectedId, 'incorrect');
40
- map.set(correctChoiceId, 'incorrect');
41
- return map;
42
- }
@@ -1,274 +0,0 @@
1
- /**
2
- * Unit tests for computeDisplayState.
3
- *
4
- * computeFeatureAudioSkin
5
- * A1. No locale, no overrides → default English skin
6
- * A2. Spanish locale (es-MX) → built-in es skin
7
- * A3. Exact locale match in byLocale → that skin
8
- * A4. No exact match, lang prefix match (es) → lang skin
9
- * A5. byLocale.default when no locale/lang match → byLocale.default skin
10
- * A6. customSingle fallback when byLocale empty → customSingle
11
- * A7. byLocale.default wins over customSingle
12
- *
13
- * computeDisplayChoiceId
14
- * D1. Normal gather: returns selectedId
15
- * D2. alwaysShowCorrect=true: returns correctChoiceId regardless
16
- * D3. alwaysShowCorrect=true, no correctChoiceId: returns selectedId
17
- * D4. evaluate + showCorrectAnswer=true: returns correctChoiceId
18
- * D5. evaluate + showCorrectAnswer=false: returns selectedId
19
- * D6. Not evaluate + showCorrectAnswer=true: returns selectedId (not overridden outside evaluate)
20
- *
21
- * computeResultText
22
- * R1. Not evaluate mode → empty string
23
- * R2. Evaluate + showCorrectAnswer=true → empty string (suppressed while reveal active)
24
- * R3. Evaluate + correct → 'Correct answer selected'
25
- * R4. Evaluate + incorrect + has selection → 'Incorrect answer selected'
26
- * R5. Evaluate + incorrect + no selection → empty string (unanswered, no message)
27
- * R6. Evaluate + unanswered (not correct, not incorrect) → empty string
28
- *
29
- * computeLegendText
30
- * L1. Plain text prompt under limit → prompt text as-is
31
- * L2. HTML prompt → tags stripped
32
- * L3. Prompt over legendMaxChars → truncated with ellipsis
33
- * L4. Empty prompt → answerChoicesLabel fallback
34
- * L5. legendMaxChars < 8 → clamped to 8 before truncation
35
- * L6. Whitespace-only prompt (after strip) → fallback
36
- */
37
-
38
- import { describe, expect, it } from 'vitest';
39
- import {
40
- computeFeatureAudioSkin,
41
- computeDisplayChoiceId,
42
- computeResultText,
43
- computeLegendText,
44
- DEFAULT_AUDIO_BUTTON_SKINS,
45
- } from './computeDisplayState';
46
-
47
- const CUSTOM_SKIN = {
48
- silentUrl: 'https://example.com/s.svg',
49
- playingUrl: 'https://example.com/p.svg',
50
- };
51
- const BY_LOCALE_SKIN = {
52
- silentUrl: 'https://example.com/l.svg',
53
- playingUrl: 'https://example.com/lp.svg',
54
- };
55
-
56
- // ---------------------------------------------------------------------------
57
- // computeFeatureAudioSkin
58
- // ---------------------------------------------------------------------------
59
-
60
- describe('computeFeatureAudioSkin', () => {
61
- it('A1: no locale, no overrides → default English skin', () => {
62
- const result = computeFeatureAudioSkin({
63
- locale: '',
64
- audioButtonSkin: null,
65
- audioButtonSkinsByLocale: {},
66
- });
67
- expect(result).toEqual(DEFAULT_AUDIO_BUTTON_SKINS.default);
68
- });
69
-
70
- it('A2: Spanish locale (es-MX) → built-in es skin', () => {
71
- const result = computeFeatureAudioSkin({
72
- locale: 'es-MX',
73
- audioButtonSkin: null,
74
- audioButtonSkinsByLocale: {},
75
- });
76
- expect(result).toEqual(DEFAULT_AUDIO_BUTTON_SKINS.es);
77
- });
78
-
79
- it('A3: exact locale match in byLocale → that skin', () => {
80
- const result = computeFeatureAudioSkin({
81
- locale: 'fr-CA',
82
- audioButtonSkin: null,
83
- audioButtonSkinsByLocale: { 'fr-ca': BY_LOCALE_SKIN },
84
- });
85
- expect(result).toEqual(BY_LOCALE_SKIN);
86
- });
87
-
88
- it('A4: no exact match, lang prefix match → lang skin', () => {
89
- const frSkin = { silentUrl: 'fr-s', playingUrl: 'fr-p' };
90
- const result = computeFeatureAudioSkin({
91
- locale: 'fr-CA',
92
- audioButtonSkin: null,
93
- audioButtonSkinsByLocale: { fr: frSkin },
94
- });
95
- expect(result).toEqual(frSkin);
96
- });
97
-
98
- it('A5: byLocale.default when no locale/lang match → byLocale.default', () => {
99
- const defaultOverride = { silentUrl: 'def-s', playingUrl: 'def-p' };
100
- const result = computeFeatureAudioSkin({
101
- locale: 'de',
102
- audioButtonSkin: null,
103
- audioButtonSkinsByLocale: { default: defaultOverride },
104
- });
105
- expect(result).toEqual(defaultOverride);
106
- });
107
-
108
- it('A6: customSingle fallback when byLocale empty', () => {
109
- const result = computeFeatureAudioSkin({
110
- locale: '',
111
- audioButtonSkin: CUSTOM_SKIN,
112
- audioButtonSkinsByLocale: {},
113
- });
114
- expect(result).toEqual(CUSTOM_SKIN);
115
- });
116
-
117
- it('A7: byLocale.default wins over customSingle', () => {
118
- const defaultOverride = { silentUrl: 'def-s', playingUrl: 'def-p' };
119
- const result = computeFeatureAudioSkin({
120
- locale: '',
121
- audioButtonSkin: CUSTOM_SKIN,
122
- audioButtonSkinsByLocale: { default: defaultOverride },
123
- });
124
- expect(result).toEqual(defaultOverride);
125
- });
126
- });
127
-
128
- // ---------------------------------------------------------------------------
129
- // computeDisplayChoiceId
130
- // ---------------------------------------------------------------------------
131
-
132
- describe('computeDisplayChoiceId', () => {
133
- const base = {
134
- selectedId: 'a',
135
- isEvaluateMode: false,
136
- showCorrectAnswer: false,
137
- alwaysShowCorrect: false,
138
- correctChoiceId: 'b',
139
- };
140
-
141
- it('D1: gather mode → selectedId', () => {
142
- expect(computeDisplayChoiceId({ ...base })).toBe('a');
143
- });
144
-
145
- it('D2: alwaysShowCorrect=true → correctChoiceId', () => {
146
- expect(computeDisplayChoiceId({ ...base, alwaysShowCorrect: true })).toBe('b');
147
- });
148
-
149
- it('D3: alwaysShowCorrect=true, no correctChoiceId → selectedId', () => {
150
- expect(computeDisplayChoiceId({ ...base, alwaysShowCorrect: true, correctChoiceId: '' })).toBe(
151
- 'a'
152
- );
153
- });
154
-
155
- it('D4: evaluate + showCorrectAnswer=true → correctChoiceId', () => {
156
- expect(computeDisplayChoiceId({ ...base, isEvaluateMode: true, showCorrectAnswer: true })).toBe(
157
- 'b'
158
- );
159
- });
160
-
161
- it('D5: evaluate + showCorrectAnswer=false → selectedId', () => {
162
- expect(
163
- computeDisplayChoiceId({ ...base, isEvaluateMode: true, showCorrectAnswer: false })
164
- ).toBe('a');
165
- });
166
-
167
- it('D6: not evaluate + showCorrectAnswer=true → selectedId (not overridden)', () => {
168
- expect(
169
- computeDisplayChoiceId({ ...base, isEvaluateMode: false, showCorrectAnswer: true })
170
- ).toBe('a');
171
- });
172
- });
173
-
174
- // ---------------------------------------------------------------------------
175
- // computeResultText
176
- // ---------------------------------------------------------------------------
177
-
178
- describe('computeResultText', () => {
179
- const base = {
180
- isEvaluateMode: true,
181
- showCorrectAnswer: false,
182
- isCorrect: false,
183
- isIncorrect: false,
184
- selectedId: '',
185
- };
186
-
187
- it('R1: not evaluate mode → empty string', () => {
188
- expect(computeResultText({ ...base, isEvaluateMode: false })).toBe('');
189
- });
190
-
191
- it('R2: evaluate + showCorrectAnswer=true → empty string', () => {
192
- expect(computeResultText({ ...base, isCorrect: true, showCorrectAnswer: true })).toBe('');
193
- });
194
-
195
- it('R3: evaluate + correct → Correct answer selected', () => {
196
- expect(computeResultText({ ...base, isCorrect: true })).toBe('Correct answer selected');
197
- });
198
-
199
- it('R4: evaluate + incorrect + has selection → Incorrect answer selected', () => {
200
- expect(computeResultText({ ...base, isIncorrect: true, selectedId: 'a' })).toBe(
201
- 'Incorrect answer selected'
202
- );
203
- });
204
-
205
- it('R5: evaluate + incorrect + no selection → empty string', () => {
206
- expect(computeResultText({ ...base, isIncorrect: true, selectedId: '' })).toBe('');
207
- });
208
-
209
- it('R6: evaluate + unanswered (neither correct nor incorrect) → empty string', () => {
210
- expect(computeResultText({ ...base })).toBe('');
211
- });
212
- });
213
-
214
- // ---------------------------------------------------------------------------
215
- // computeLegendText
216
- // ---------------------------------------------------------------------------
217
-
218
- describe('computeLegendText', () => {
219
- it('L1: plain text prompt under limit → prompt text', () => {
220
- expect(
221
- computeLegendText({
222
- prompt: 'Choose the word',
223
- legendMaxChars: 40,
224
- answerChoicesLabel: 'Choices',
225
- })
226
- ).toBe('Choose the word');
227
- });
228
-
229
- it('L2: HTML prompt → tags stripped', () => {
230
- expect(
231
- computeLegendText({
232
- prompt: '<p><strong>Choose</strong> the word</p>',
233
- legendMaxChars: 40,
234
- answerChoicesLabel: 'Choices',
235
- })
236
- ).toBe('Choose the word');
237
- });
238
-
239
- it('L3: prompt over legendMaxChars → truncated with ellipsis', () => {
240
- const result = computeLegendText({
241
- prompt: 'Choose the correct spelling',
242
- legendMaxChars: 10,
243
- answerChoicesLabel: 'Choices',
244
- });
245
- expect(result.length).toBeLessThanOrEqual(10);
246
- expect(result.endsWith('…')).toBe(true);
247
- });
248
-
249
- it('L4: empty prompt → answerChoicesLabel fallback', () => {
250
- expect(
251
- computeLegendText({ prompt: '', legendMaxChars: 40, answerChoicesLabel: 'Answer choices' })
252
- ).toBe('Answer choices');
253
- });
254
-
255
- it('L5: legendMaxChars below 8 → clamped, no crash', () => {
256
- const result = computeLegendText({
257
- prompt: 'Hi there friend',
258
- legendMaxChars: 2,
259
- answerChoicesLabel: 'Choices',
260
- });
261
- expect(result.endsWith('…')).toBe(true);
262
- expect(result.length).toBeGreaterThan(0);
263
- });
264
-
265
- it('L6: whitespace-only HTML prompt → fallback', () => {
266
- expect(
267
- computeLegendText({
268
- prompt: '<p> </p>',
269
- legendMaxChars: 40,
270
- answerChoicesLabel: 'Answer choices',
271
- })
272
- ).toBe('Answer choices');
273
- });
274
- });