@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,149 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { mount, unmount, flushSync } from 'svelte';
3
- import AudioPlayer from './AudioPlayer.svelte';
4
-
5
- const BASE_PROPS = {
6
- hasAudio: true,
7
- audioUrl: 'https://example.com/audio.mp3',
8
- useFeatureButtonAudio: false,
9
- autoplayEnabled: false,
10
- audioTranscript: 'The word is look.',
11
- showVisibleTranscript: false,
12
- transcriptId: 'test-transcript',
13
- featureAudioSkin: { silentUrl: '', playingUrl: '' },
14
- uiText: {
15
- clickToEnableAutoplay: 'Click to enable',
16
- transcriptLabel: 'Transcript',
17
- audioResourceUnavailable: 'Unavailable',
18
- listenSilentAlt: 'Repeat instructions',
19
- listenPlayingAlt: 'Instructions are playing',
20
- listenSilentAltEs: 'Escuchar. Repetir las instrucciones.',
21
- listenPlayingAltEs: 'Escuchar. Estas son las instrucciones.',
22
- },
23
- };
24
-
25
- function mountPlayer(props: Partial<typeof BASE_PROPS>) {
26
- const target = document.createElement('div');
27
- document.body.appendChild(target);
28
- const component = mount(AudioPlayer, { target, props: { ...BASE_PROPS, ...props } });
29
- return { target, component };
30
- }
31
-
32
- const mounts: Array<{ target: HTMLElement; component: ReturnType<typeof mount> }> = [];
33
-
34
- afterEach(() => {
35
- for (const { target, component } of mounts.splice(0)) {
36
- unmount(component);
37
- target.remove();
38
- }
39
- });
40
-
41
- describe('AudioPlayer — audio element', () => {
42
- it('renders native controls when hasAudio=true and useFeatureButtonAudio=false', () => {
43
- const { target, component } = mountPlayer({ useFeatureButtonAudio: false });
44
- mounts.push({ target, component });
45
- flushSync();
46
- expect(target.querySelector('.pie-audio-player')).not.toBeNull();
47
- });
48
-
49
- it('renders nothing when hasAudio is false', () => {
50
- const { target, component } = mountPlayer({ hasAudio: false });
51
- mounts.push({ target, component });
52
- flushSync();
53
- expect(target.querySelector('.pie-audio-container')).toBeNull();
54
- });
55
-
56
- it('renders an error message when hasAudio=true but audioUrl is missing', () => {
57
- const { target, component } = mountPlayer({ audioUrl: undefined });
58
- mounts.push({ target, component });
59
- flushSync();
60
- expect(target.querySelector('.pie-audio-error')).not.toBeNull();
61
- });
62
-
63
- it('renders the feature button when useFeatureButtonAudio=true', () => {
64
- const { target, component } = mountPlayer({ useFeatureButtonAudio: true });
65
- mounts.push({ target, component });
66
- flushSync();
67
- expect(target.querySelector('.pie-listen-button')).not.toBeNull();
68
- });
69
-
70
- it('silent image alt is "Repeat instructions" in feature-button mode', () => {
71
- const { target, component } = mountPlayer({ useFeatureButtonAudio: true });
72
- mounts.push({ target, component });
73
- flushSync();
74
- const imgs = target.querySelectorAll('.pie-listen-icon');
75
- const alts = Array.from(imgs).map((el) => (el as HTMLImageElement).alt);
76
- expect(alts).toContain('Repeat instructions');
77
- });
78
-
79
- it('playing image alt is "Instructions are playing" in feature-button mode', () => {
80
- const { target, component } = mountPlayer({ useFeatureButtonAudio: true });
81
- mounts.push({ target, component });
82
- flushSync();
83
- const imgs = target.querySelectorAll('.pie-listen-icon');
84
- const alts = Array.from(imgs).map((el) => (el as HTMLImageElement).alt);
85
- expect(alts).toContain('Instructions are playing');
86
- });
87
- });
88
-
89
- describe('AudioPlayer — autoplay-blocked prompt', () => {
90
- it('shows pie-audio-autoplay-enable when autoplay is blocked by the browser', async () => {
91
- // Override HTMLAudioElement.play on the global so all audio elements reject
92
- const originalPlay = HTMLMediaElement.prototype.play;
93
- HTMLMediaElement.prototype.play = () =>
94
- Promise.reject(new DOMException('autoplay blocked', 'NotAllowedError'));
95
-
96
- const { target, component } = mountPlayer({
97
- useFeatureButtonAudio: false,
98
- autoplayEnabled: true,
99
- });
100
- mounts.push({ target, component });
101
- flushSync();
102
-
103
- // Allow the rejected promise microtask to settle
104
- await Promise.resolve();
105
- flushSync();
106
-
107
- expect(target.querySelector('.pie-audio-autoplay-enable')).not.toBeNull();
108
-
109
- HTMLMediaElement.prototype.play = originalPlay;
110
- });
111
-
112
- it('hides pie-audio-autoplay-enable when autoplay is not blocked', async () => {
113
- const originalPlay = HTMLMediaElement.prototype.play;
114
- HTMLMediaElement.prototype.play = () => Promise.resolve();
115
-
116
- const { target, component } = mountPlayer({
117
- useFeatureButtonAudio: false,
118
- autoplayEnabled: true,
119
- });
120
- mounts.push({ target, component });
121
- flushSync();
122
- await Promise.resolve();
123
- flushSync();
124
-
125
- expect(target.querySelector('.pie-audio-autoplay-enable')).toBeNull();
126
-
127
- HTMLMediaElement.prototype.play = originalPlay;
128
- });
129
-
130
- it('pie-audio-autoplay-enable is not rendered in feature-button mode even when autoplay is blocked', async () => {
131
- const originalPlay = HTMLMediaElement.prototype.play;
132
- HTMLMediaElement.prototype.play = () =>
133
- Promise.reject(new DOMException('autoplay blocked', 'NotAllowedError'));
134
-
135
- const { target, component } = mountPlayer({
136
- useFeatureButtonAudio: true,
137
- autoplayEnabled: true,
138
- });
139
- mounts.push({ target, component });
140
- flushSync();
141
- await Promise.resolve();
142
- flushSync();
143
-
144
- // Feature-button mode never shows the enable-autoplay prompt
145
- expect(target.querySelector('.pie-audio-autoplay-enable')).toBeNull();
146
-
147
- HTMLMediaElement.prototype.play = originalPlay;
148
- });
149
- });
@@ -1,222 +0,0 @@
1
- <script lang="ts">
2
- interface Choice {
3
- id: string;
4
- labelHtml?: string;
5
- imageUrl?: string;
6
- imageAlt?: string;
7
- }
8
-
9
- let {
10
- choice,
11
- choiceMode = 'text',
12
- isHorizontal = false,
13
- isSelected = false,
14
- isDisabled = false,
15
- correctness = undefined,
16
- isEvaluateMode = false,
17
- instanceId,
18
- radioGroupName,
19
- }: {
20
- choice: Choice;
21
- choiceMode?: 'text' | 'image';
22
- isHorizontal?: boolean;
23
- isSelected?: boolean;
24
- isDisabled?: boolean;
25
- correctness?: 'correct' | 'incorrect' | undefined;
26
- isEvaluateMode?: boolean;
27
- instanceId: string;
28
- radioGroupName: string;
29
- } = $props();
30
- </script>
31
-
32
- <div
33
- class={`flex items-start choice-row pie-choice ${isHorizontal ? 'choice-row-horizontal pie-choice-horizontal' : ''} ${isSelected ? 'is-selected pie-choice-selected' : ''} ${correctness ? `choice-${correctness} pie-choice-${correctness}` : ''}`}
34
- style="gap:var(--mpb-choice-row-gap, 0.5rem);"
35
- >
36
- {#if isHorizontal}
37
- <label
38
- for={`${instanceId}-opt-${choice.id}`}
39
- class="cursor-pointer choice-tile text-center pie-choice-tile"
40
- >
41
- <span class="choice-tile-content pie-choice-tile-content">
42
- {#if choiceMode === 'image' && choice.imageUrl}
43
- <img
44
- src={choice.imageUrl}
45
- alt={choice.imageAlt || `Choice ${choice.id}`}
46
- class="object-contain pie-choice-image"
47
- style="max-height:var(--mpb-choice-image-max-height, 5rem);"
48
- />
49
- {:else}
50
- <span class="choice-html pie-choice-label">{@html choice.labelHtml || ''}</span>
51
- {/if}
52
- </span>
53
- <input
54
- type="radio"
55
- name={radioGroupName}
56
- id={`${instanceId}-opt-${choice.id}`}
57
- value={choice.id}
58
- checked={isSelected}
59
- disabled={isDisabled}
60
- class="choice-radio-bottom pie-choice-radio pie-choice-radio-bottom"
61
- />
62
- </label>
63
- {:else}
64
- <input
65
- type="radio"
66
- name={radioGroupName}
67
- id={`${instanceId}-opt-${choice.id}`}
68
- value={choice.id}
69
- checked={isSelected}
70
- disabled={isDisabled}
71
- class="choice-radio-inline pie-choice-radio pie-choice-radio-inline"
72
- />
73
- <label for={`${instanceId}-opt-${choice.id}`} class="cursor-pointer flex-1 pie-choice-label-wrap">
74
- {#if choiceMode === 'image' && choice.imageUrl}
75
- <img
76
- src={choice.imageUrl}
77
- alt={choice.imageAlt || `Choice ${choice.id}`}
78
- class="object-contain pie-choice-image"
79
- style="max-height:var(--mpb-choice-image-max-height, 5rem);"
80
- />
81
- {:else}
82
- <span class="choice-html pie-choice-label">{@html choice.labelHtml || ''}</span>
83
- {/if}
84
- </label>
85
- {/if}
86
- {#if isEvaluateMode && correctness}
87
- <span
88
- class={`pie-choice-feedback-badge ${correctness === 'correct' ? 'pie-choice-feedback-correct' : 'pie-choice-feedback-incorrect'}`}
89
- aria-hidden="true"
90
- >
91
- {correctness === 'correct' ? '✓' : '✕'}
92
- </span>
93
- {/if}
94
- </div>
95
-
96
- <style>
97
- .choice-row-horizontal {
98
- flex-direction: column;
99
- align-items: center;
100
- width: min(var(--mpb-choice-width-px, 170px), var(--mpb-choice-width-vw, 30vw));
101
- min-height: var(--mpb-choice-tile-min-height, 11rem);
102
- gap: 0;
103
- }
104
-
105
- .choice-tile {
106
- display: flex;
107
- flex-direction: column;
108
- justify-content: space-between;
109
- align-items: center;
110
- width: 100%;
111
- min-height: var(--mpb-choice-tile-min-height, 11rem);
112
- padding: 0.8rem 0.65rem 0.5rem;
113
- border-radius: 8px;
114
- background: transparent;
115
- transition: background-color 120ms ease-in-out;
116
- }
117
-
118
- .choice-tile-content {
119
- width: 100%;
120
- display: flex;
121
- align-items: center;
122
- justify-content: center;
123
- min-height: var(--mpb-choice-content-min-height, 7.5rem);
124
- }
125
-
126
- .choice-row-horizontal:hover .choice-tile {
127
- background: var(--mpb-choice-hover-bg, var(--pie-correct-answer-choice-hover-bg, #f2f2f2));
128
- }
129
-
130
- .pie-choice-horizontal:not(.is-selected):not(:hover) .pie-choice-tile {
131
- background: transparent;
132
- }
133
-
134
- .choice-row-horizontal.is-selected .choice-tile {
135
- background: var(--mpb-choice-selected-bg, var(--pie-correct-answer-choice-selected-bg, #fcfcd3));
136
- }
137
-
138
- .choice-row-horizontal.is-selected:hover .choice-tile {
139
- background: var(--mpb-choice-selected-bg, var(--pie-correct-answer-choice-selected-bg, #fcfcd3));
140
- }
141
-
142
- .pie-choice:not(.pie-choice-horizontal):hover .pie-choice-label-wrap {
143
- background: var(--pie-correct-answer-choice-hover-bg, #ececec);
144
- }
145
-
146
- .pie-choice:not(.pie-choice-horizontal):not(.is-selected):not(:hover) .pie-choice-label-wrap {
147
- background: transparent;
148
- }
149
-
150
- .pie-choice:not(.pie-choice-horizontal).is-selected .pie-choice-label-wrap {
151
- background: var(--pie-correct-answer-choice-selected-bg, #f1f1f1);
152
- border-radius: 6px;
153
- }
154
-
155
- .pie-choice:not(.pie-choice-horizontal).is-selected:hover .pie-choice-label-wrap {
156
- background: var(--pie-correct-answer-choice-selected-bg, #f1f1f1);
157
- }
158
-
159
- .pie-choice.choice-correct {
160
- border-left: 3px solid var(--pie-correct-answer-choice-correct-border, #0ea449);
161
- }
162
-
163
- .pie-choice.choice-incorrect {
164
- border-left: 3px solid var(--pie-correct-answer-choice-incorrect-border, #bf0d00);
165
- }
166
-
167
- .pie-choice-horizontal.choice-correct .choice-tile {
168
- background: var(--pie-correct-answer-choice-correct-bg, #e8f5e9);
169
- }
170
-
171
- .pie-choice-horizontal.choice-incorrect .choice-tile {
172
- background: var(--pie-correct-answer-choice-incorrect-bg, #ffebee);
173
- }
174
-
175
- .pie-choice-feedback-badge {
176
- margin-left: auto;
177
- display: inline-flex;
178
- align-items: center;
179
- justify-content: center;
180
- width: 1.1rem;
181
- height: 1.1rem;
182
- border-radius: 9999px;
183
- font-size: 0.72rem;
184
- line-height: 1;
185
- font-weight: 700;
186
- color: var(--pie-correct-answer-feedback-glyph-color, #fff);
187
- }
188
-
189
- .pie-choice-feedback-correct {
190
- background: var(--pie-correct-answer-feedback-correct-bg, #087d38);
191
- }
192
-
193
- .pie-choice-feedback-incorrect {
194
- background: var(--pie-correct-answer-feedback-incorrect-bg, #bf0d00);
195
- }
196
-
197
- .choice-row-horizontal :global(p) {
198
- margin: 0;
199
- text-align: center;
200
- }
201
-
202
- .choice-html :global(p) {
203
- margin: 0;
204
- }
205
-
206
- .choice-html {
207
- width: 100%;
208
- display: inline-flex;
209
- align-items: center;
210
- justify-content: center;
211
- text-align: center;
212
- }
213
-
214
- .choice-radio-bottom {
215
- margin-top: var(--mpb-horizontal-choice-radio-top-margin, 0.5rem);
216
- padding: var(--mpb-choice-radio-padding, 0px);
217
- }
218
-
219
- .choice-radio-inline {
220
- margin-top: var(--mpb-horizontal-choice-radio-top-margin, 0.5rem);
221
- }
222
- </style>
@@ -1,179 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { mount, unmount, flushSync } from 'svelte';
3
- import ChoiceRow from './ChoiceRow.svelte';
4
-
5
- const CHOICE = { id: 'choice-a', labelHtml: '<p>Look</p>' };
6
- const CHOICE_IMG = { id: 'choice-b', imageUrl: 'https://example.com/img.png', imageAlt: 'A cat' };
7
-
8
- const BASE = {
9
- choice: CHOICE,
10
- instanceId: 'test',
11
- radioGroupName: 'test-group',
12
- };
13
-
14
- const mounts: Array<{ target: HTMLElement; component: ReturnType<typeof mount> }> = [];
15
-
16
- function mountRow(props: Record<string, unknown>) {
17
- const target = document.createElement('div');
18
- document.body.appendChild(target);
19
- const component = mount(ChoiceRow, { target, props });
20
- mounts.push({ target, component });
21
- flushSync();
22
- return target;
23
- }
24
-
25
- afterEach(() => {
26
- for (const { target, component } of mounts.splice(0)) {
27
- unmount(component);
28
- target.remove();
29
- }
30
- });
31
-
32
- describe('ChoiceRow — inline layout (isHorizontal=false)', () => {
33
- it('renders a radio input and label for the choice', () => {
34
- const target = mountRow({ ...BASE });
35
- expect(target.querySelector('input[type="radio"]')).not.toBeNull();
36
- expect(target.querySelector('label')).not.toBeNull();
37
- });
38
-
39
- it('radio id and label for are paired', () => {
40
- const target = mountRow({ ...BASE });
41
- const input = target.querySelector('input[type="radio"]') as HTMLInputElement;
42
- const label = target.querySelector('label') as HTMLLabelElement;
43
- expect(input.id).toBe('test-opt-choice-a');
44
- expect(label.htmlFor).toBe('test-opt-choice-a');
45
- });
46
-
47
- it('radio has correct name and value', () => {
48
- const target = mountRow({ ...BASE });
49
- const input = target.querySelector('input[type="radio"]') as HTMLInputElement;
50
- expect(input.name).toBe('test-group');
51
- expect(input.value).toBe('choice-a');
52
- });
53
-
54
- it('radio is not checked when isSelected=false', () => {
55
- const target = mountRow({ ...BASE, isSelected: false });
56
- const input = target.querySelector('input[type="radio"]') as HTMLInputElement;
57
- expect(input.checked).toBe(false);
58
- });
59
-
60
- it('radio is checked when isSelected=true', () => {
61
- const target = mountRow({ ...BASE, isSelected: true });
62
- const input = target.querySelector('input[type="radio"]') as HTMLInputElement;
63
- expect(input.checked).toBe(true);
64
- });
65
-
66
- it('radio is disabled when isDisabled=true', () => {
67
- const target = mountRow({ ...BASE, isDisabled: true });
68
- const input = target.querySelector('input[type="radio"]') as HTMLInputElement;
69
- expect(input.disabled).toBe(true);
70
- });
71
-
72
- it('inline radio has pie-choice-radio-inline class', () => {
73
- const target = mountRow({ ...BASE });
74
- const input = target.querySelector('input[type="radio"]') as HTMLInputElement;
75
- expect(input.classList.contains('pie-choice-radio-inline')).toBe(true);
76
- });
77
-
78
- it('renders label html for text choice', () => {
79
- const target = mountRow({ ...BASE });
80
- expect(target.querySelector('.pie-choice-label')).not.toBeNull();
81
- });
82
-
83
- it('renders image for image choice', () => {
84
- const target = mountRow({ ...BASE, choice: CHOICE_IMG, choiceMode: 'image' });
85
- const img = target.querySelector('img.pie-choice-image') as HTMLImageElement;
86
- expect(img).not.toBeNull();
87
- expect(img.alt).toBe('A cat');
88
- });
89
- });
90
-
91
- describe('ChoiceRow — horizontal layout (isHorizontal=true)', () => {
92
- it('wraps radio inside label (tile layout)', () => {
93
- const target = mountRow({ ...BASE, isHorizontal: true });
94
- const label = target.querySelector('label.pie-choice-tile') as HTMLLabelElement;
95
- expect(label).not.toBeNull();
96
- expect(label.querySelector('input[type="radio"]')).not.toBeNull();
97
- });
98
-
99
- it('radio has pie-choice-radio-bottom class in horizontal mode', () => {
100
- const target = mountRow({ ...BASE, isHorizontal: true });
101
- const input = target.querySelector('input[type="radio"]') as HTMLInputElement;
102
- expect(input.classList.contains('pie-choice-radio-bottom')).toBe(true);
103
- });
104
-
105
- it('choice content is inside pie-choice-tile-content', () => {
106
- const target = mountRow({ ...BASE, isHorizontal: true });
107
- expect(target.querySelector('.pie-choice-tile-content')).not.toBeNull();
108
- });
109
- });
110
-
111
- describe('ChoiceRow — CSS state classes', () => {
112
- it('applies is-selected and pie-choice-selected when isSelected=true', () => {
113
- const target = mountRow({ ...BASE, isSelected: true });
114
- const row = target.querySelector('.pie-choice') as HTMLElement;
115
- expect(row.classList.contains('is-selected')).toBe(true);
116
- expect(row.classList.contains('pie-choice-selected')).toBe(true);
117
- });
118
-
119
- it('does not apply selected classes when isSelected=false', () => {
120
- const target = mountRow({ ...BASE, isSelected: false });
121
- const row = target.querySelector('.pie-choice') as HTMLElement;
122
- expect(row.classList.contains('is-selected')).toBe(false);
123
- });
124
-
125
- it('applies choice-correct and pie-choice-correct when correctness=correct', () => {
126
- const target = mountRow({ ...BASE, correctness: 'correct' });
127
- const row = target.querySelector('.pie-choice') as HTMLElement;
128
- expect(row.classList.contains('choice-correct')).toBe(true);
129
- expect(row.classList.contains('pie-choice-correct')).toBe(true);
130
- });
131
-
132
- it('applies choice-incorrect and pie-choice-incorrect when correctness=incorrect', () => {
133
- const target = mountRow({ ...BASE, correctness: 'incorrect' });
134
- const row = target.querySelector('.pie-choice') as HTMLElement;
135
- expect(row.classList.contains('choice-incorrect')).toBe(true);
136
- expect(row.classList.contains('pie-choice-incorrect')).toBe(true);
137
- });
138
-
139
- it('applies no correctness class when correctness=undefined', () => {
140
- const target = mountRow({ ...BASE });
141
- const row = target.querySelector('.pie-choice') as HTMLElement;
142
- expect(row.classList.contains('choice-correct')).toBe(false);
143
- expect(row.classList.contains('choice-incorrect')).toBe(false);
144
- });
145
- });
146
-
147
- describe('ChoiceRow — feedback badge', () => {
148
- it('renders no badge outside evaluate mode', () => {
149
- const target = mountRow({ ...BASE, isEvaluateMode: false, correctness: 'correct' });
150
- expect(target.querySelector('.pie-choice-feedback-badge')).toBeNull();
151
- });
152
-
153
- it('renders no badge in evaluate mode when correctness is undefined', () => {
154
- const target = mountRow({ ...BASE, isEvaluateMode: true, correctness: undefined });
155
- expect(target.querySelector('.pie-choice-feedback-badge')).toBeNull();
156
- });
157
-
158
- it('renders ✓ badge with pie-choice-feedback-correct in evaluate mode when correct', () => {
159
- const target = mountRow({ ...BASE, isEvaluateMode: true, correctness: 'correct' });
160
- const badge = target.querySelector('.pie-choice-feedback-badge') as HTMLElement;
161
- expect(badge).not.toBeNull();
162
- expect(badge.classList.contains('pie-choice-feedback-correct')).toBe(true);
163
- expect(badge.textContent?.trim()).toBe('✓');
164
- });
165
-
166
- it('renders ✕ badge with pie-choice-feedback-incorrect in evaluate mode when incorrect', () => {
167
- const target = mountRow({ ...BASE, isEvaluateMode: true, correctness: 'incorrect' });
168
- const badge = target.querySelector('.pie-choice-feedback-badge') as HTMLElement;
169
- expect(badge).not.toBeNull();
170
- expect(badge.classList.contains('pie-choice-feedback-incorrect')).toBe(true);
171
- expect(badge.textContent?.trim()).toBe('✕');
172
- });
173
-
174
- it('badge has aria-hidden=true', () => {
175
- const target = mountRow({ ...BASE, isEvaluateMode: true, correctness: 'correct' });
176
- const badge = target.querySelector('.pie-choice-feedback-badge') as HTMLElement;
177
- expect(badge.getAttribute('aria-hidden')).toBe('true');
178
- });
179
- });
@@ -1,83 +0,0 @@
1
- <script lang="ts">
2
- interface DisplayChoice {
3
- imageUrl?: string;
4
- imageAlt?: string;
5
- labelHtml?: string;
6
- }
7
-
8
- let {
9
- choiceMode = 'text',
10
- displayChoice = undefined,
11
- displayChoiceLabelHtml = '',
12
- isStandalone = false,
13
- blankWidth,
14
- blankBorderWidth,
15
- ariaLabel,
16
- }: {
17
- choiceMode?: 'text' | 'image';
18
- displayChoice?: DisplayChoice;
19
- displayChoiceLabelHtml?: string;
20
- isStandalone?: boolean;
21
- blankWidth: string;
22
- blankBorderWidth: string;
23
- ariaLabel: string;
24
- } = $props();
25
- </script>
26
-
27
- <span
28
- class={`inline-flex items-center min-h-[1.5em] px-2 mx-1 border-b-2 border-gray-500 align-baseline cloze-marker pie-blank-slot ${isStandalone ? 'cloze-marker-standalone pie-blank-slot-standalone' : ''}`}
29
- style={`width:${blankWidth};border-bottom-width:${blankBorderWidth};`}
30
- role="status"
31
- aria-live="polite"
32
- aria-atomic="true"
33
- aria-label={ariaLabel}
34
- >
35
- {#if choiceMode === 'image' && displayChoice?.imageUrl}
36
- <img
37
- src={displayChoice.imageUrl}
38
- alt={displayChoice.imageAlt || 'Selected answer image'}
39
- class="w-auto object-contain pie-blank-image"
40
- style="max-height:var(--mpb-selected-image-max-height, 4rem);"
41
- />
42
- {:else if displayChoiceLabelHtml}
43
- <span class="cloze-marker-value pie-blank-value">{@html displayChoiceLabelHtml}</span>
44
- {:else}
45
- <span class="cloze-marker-empty" aria-hidden="true">&nbsp;</span>
46
- {/if}
47
- </span>
48
-
49
- <style>
50
- .cloze-marker {
51
- display: inline-flex;
52
- align-items: center;
53
- justify-content: center;
54
- text-align: center;
55
- vertical-align: baseline;
56
- }
57
-
58
- .cloze-marker:focus-within {
59
- outline: 2px solid var(--pie-focus, #2563eb);
60
- outline-offset: 2px;
61
- }
62
-
63
- .cloze-marker-standalone {
64
- width: var(--mpb-blank-standalone-width, 7rem);
65
- }
66
-
67
- .cloze-marker-empty {
68
- display: inline-block;
69
- min-width: 4ch;
70
- }
71
-
72
- .cloze-marker-value {
73
- width: 100%;
74
- display: inline-flex;
75
- align-items: center;
76
- justify-content: center;
77
- text-align: center;
78
- }
79
-
80
- .cloze-marker-value :global(p) {
81
- margin: 0;
82
- }
83
- </style>