@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.
- package/package.json +3 -14
- package/dist/controller/index.test.d.ts +0 -63
- package/dist/controller/index.test.d.ts.map +0 -1
- package/dist/delivery/AudioPlayer.test.d.ts +0 -2
- package/dist/delivery/AudioPlayer.test.d.ts.map +0 -1
- package/dist/delivery/ChoiceRow.test.d.ts +0 -2
- package/dist/delivery/ChoiceRow.test.d.ts.map +0 -1
- package/dist/delivery/ClozeMarker.test.d.ts +0 -2
- package/dist/delivery/ClozeMarker.test.d.ts.map +0 -1
- package/dist/delivery/McPopulatedBlank.transcript.test.d.ts +0 -2
- package/dist/delivery/McPopulatedBlank.transcript.test.d.ts.map +0 -1
- package/dist/delivery/computeAudioMode.test.d.ts +0 -2
- package/dist/delivery/computeAudioMode.test.d.ts.map +0 -1
- package/dist/delivery/computeChoiceCorrectness.test.d.ts +0 -19
- package/dist/delivery/computeChoiceCorrectness.test.d.ts.map +0 -1
- package/dist/delivery/computeDisplayState.test.d.ts +0 -38
- package/dist/delivery/computeDisplayState.test.d.ts.map +0 -1
- package/dist/delivery/computeLayoutProfile.test.d.ts +0 -2
- package/dist/delivery/computeLayoutProfile.test.d.ts.map +0 -1
- package/dist/delivery/computeLayoutStyle.test.d.ts +0 -2
- package/dist/delivery/computeLayoutStyle.test.d.ts.map +0 -1
- package/src/author/Author.svelte +0 -41
- package/src/author/index.ts +0 -28
- package/src/controller/defaults.ts +0 -61
- package/src/controller/index.test.ts +0 -428
- package/src/controller/index.ts +0 -372
- package/src/delivery/AudioPlayer.svelte +0 -246
- package/src/delivery/AudioPlayer.test.ts +0 -149
- package/src/delivery/ChoiceRow.svelte +0 -222
- package/src/delivery/ChoiceRow.test.ts +0 -179
- package/src/delivery/ClozeMarker.svelte +0 -83
- package/src/delivery/ClozeMarker.test.ts +0 -128
- package/src/delivery/McPopulatedBlank.svelte +0 -739
- package/src/delivery/McPopulatedBlank.transcript.test.ts +0 -154
- package/src/delivery/computeAudioMode.test.ts +0 -40
- package/src/delivery/computeAudioMode.ts +0 -11
- package/src/delivery/computeChoiceCorrectness.test.ts +0 -120
- package/src/delivery/computeChoiceCorrectness.ts +0 -42
- package/src/delivery/computeDisplayState.test.ts +0 -274
- package/src/delivery/computeDisplayState.ts +0 -101
- package/src/delivery/computeLayoutProfile.test.ts +0 -163
- package/src/delivery/computeLayoutProfile.ts +0 -72
- package/src/delivery/computeLayoutStyle.test.ts +0 -168
- package/src/delivery/computeLayoutStyle.ts +0 -128
- package/src/delivery/cqt-css/sel-r1-base.css +0 -59
- package/src/delivery/cqt-css/sel-r1-g-stem.css +0 -14
- package/src/delivery/cqt-css/sel-r1-gg-plus.css +0 -12
- package/src/delivery/cqt-css/sel-r1-ggplus.css +0 -17
- package/src/delivery/cqt-css/sel-r1-gplusggg.css +0 -25
- package/src/delivery/cqt-css/sel-r1-plusggg.css +0 -17
- package/src/delivery/cqt-css/sel-r1-s3.css +0 -85
- package/src/delivery/cqt-css/sel-vic.css +0 -99
- package/src/delivery/cqt-css/sr-vic.css +0 -63
- package/src/delivery/index.ts +0 -94
- package/src/delivery/variant-css-map.ts +0 -124
- package/src/index.iife.ts +0 -13
- package/src/index.ts +0 -1
- package/src/print/Print.svelte +0 -95
- package/src/print/index.ts +0 -18
- package/src/shared/layoutLimits.ts +0 -76
- package/src/shared/types.ts +0 -84
- 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
|
-
});
|