@pie-element/mc-populated-blank 0.2.10 → 0.2.11
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/README.md +0 -2
- package/dist/author/index.js +186 -152
- package/dist/author/index.js.map +1 -1
- package/dist/controller/defaults.d.ts +41 -73
- package/dist/controller/defaults.d.ts.map +1 -1
- package/dist/controller/index.d.ts +154 -10
- package/dist/controller/index.d.ts.map +1 -1
- package/dist/controller/index.js +29 -30
- package/dist/controller/index.js.map +1 -1
- package/dist/controller/index.test.d.ts +63 -0
- package/dist/controller/index.test.d.ts.map +1 -0
- package/dist/delivery/AudioPlayer.test.d.ts +2 -0
- package/dist/delivery/AudioPlayer.test.d.ts.map +1 -0
- package/dist/delivery/ChoiceRow.test.d.ts +2 -0
- package/dist/delivery/ChoiceRow.test.d.ts.map +1 -0
- package/dist/delivery/ClozeMarker.test.d.ts +2 -0
- package/dist/delivery/ClozeMarker.test.d.ts.map +1 -0
- package/dist/delivery/McPopulatedBlank.transcript.test.d.ts +2 -0
- package/dist/delivery/McPopulatedBlank.transcript.test.d.ts.map +1 -0
- package/dist/delivery/computeAudioMode.d.ts +7 -0
- package/dist/delivery/computeAudioMode.d.ts.map +1 -0
- package/dist/delivery/computeAudioMode.test.d.ts +2 -0
- package/dist/delivery/computeAudioMode.test.d.ts.map +1 -0
- package/dist/delivery/computeChoiceCorrectness.d.ts +18 -0
- package/dist/delivery/computeChoiceCorrectness.d.ts.map +1 -0
- package/dist/delivery/computeChoiceCorrectness.test.d.ts +19 -0
- package/dist/delivery/computeChoiceCorrectness.test.d.ts.map +1 -0
- package/dist/delivery/computeDisplayState.d.ts +47 -0
- package/dist/delivery/computeDisplayState.d.ts.map +1 -0
- package/dist/delivery/computeDisplayState.test.d.ts +38 -0
- package/dist/delivery/computeDisplayState.test.d.ts.map +1 -0
- package/dist/delivery/computeLayoutProfile.d.ts +18 -0
- package/dist/delivery/computeLayoutProfile.d.ts.map +1 -0
- package/dist/delivery/computeLayoutProfile.test.d.ts +2 -0
- package/dist/delivery/computeLayoutProfile.test.d.ts.map +1 -0
- package/dist/delivery/computeLayoutStyle.d.ts +27 -0
- package/dist/delivery/computeLayoutStyle.d.ts.map +1 -0
- package/dist/delivery/computeLayoutStyle.test.d.ts +2 -0
- package/dist/delivery/computeLayoutStyle.test.d.ts.map +1 -0
- package/dist/delivery/index.d.ts.map +1 -1
- package/dist/delivery/index.js +1591 -927
- package/dist/delivery/index.js.map +1 -1
- package/dist/delivery/variant-css-map.d.ts.map +1 -1
- package/dist/index.iife.d.ts +151 -10
- package/dist/index.iife.d.ts.map +1 -1
- package/dist/index.iife.js +233 -114
- package/dist/index.js +1591 -927
- package/dist/index.js.map +1 -1
- package/dist/print/index.js +194 -166
- package/dist/print/index.js.map +1 -1
- package/dist/shared/layoutLimits.d.ts +40 -0
- package/dist/shared/layoutLimits.d.ts.map +1 -0
- package/dist/shared/types.d.ts +82 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/package.json +4 -2
- package/src/controller/defaults.ts +9 -39
- package/src/controller/index.test.ts +428 -0
- package/src/controller/index.ts +63 -50
- package/src/delivery/AudioPlayer.svelte +246 -0
- package/src/delivery/AudioPlayer.test.ts +149 -0
- package/src/delivery/ChoiceRow.svelte +222 -0
- package/src/delivery/ChoiceRow.test.ts +179 -0
- package/src/delivery/ClozeMarker.svelte +83 -0
- package/src/delivery/ClozeMarker.test.ts +128 -0
- package/src/delivery/McPopulatedBlank.svelte +225 -732
- package/src/delivery/McPopulatedBlank.transcript.test.ts +154 -0
- package/src/delivery/computeAudioMode.test.ts +40 -0
- package/src/delivery/computeAudioMode.ts +11 -0
- package/src/delivery/computeChoiceCorrectness.test.ts +120 -0
- package/src/delivery/computeChoiceCorrectness.ts +42 -0
- package/src/delivery/computeDisplayState.test.ts +274 -0
- package/src/delivery/computeDisplayState.ts +101 -0
- package/src/delivery/computeLayoutProfile.test.ts +163 -0
- package/src/delivery/computeLayoutProfile.ts +72 -0
- package/src/delivery/computeLayoutStyle.test.ts +168 -0
- package/src/delivery/computeLayoutStyle.ts +128 -0
- package/src/delivery/cqt-css/sel-r1-base.css +59 -0
- package/src/delivery/cqt-css/sel-r1-g-stem.css +5 -20
- package/src/delivery/cqt-css/sel-r1-gg-plus.css +3 -20
- package/src/delivery/cqt-css/sel-r1-ggplus.css +4 -21
- package/src/delivery/cqt-css/sel-r1-gplusggg.css +16 -11
- package/src/delivery/cqt-css/sel-r1-plusggg.css +8 -11
- package/src/delivery/cqt-css/sel-r1-s3.css +3 -9
- package/src/delivery/cqt-css/sel-vic.css +61 -0
- package/src/delivery/cqt-css/sr-vic.css +49 -0
- package/src/delivery/index.ts +14 -23
- package/src/delivery/variant-css-map.ts +26 -12
- package/src/shared/layoutLimits.ts +76 -0
- package/src/shared/types.ts +84 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type LayoutLimits = {
|
|
2
|
+
blankStandaloneWidthRem: number;
|
|
3
|
+
blankWideWidthRem: number;
|
|
4
|
+
blankUnderlineWidthPx: number;
|
|
5
|
+
blankUnderlineWideWidthPx: number;
|
|
6
|
+
horizontalChoiceWidthPx: number;
|
|
7
|
+
horizontalChoiceWidthVw: number;
|
|
8
|
+
horizontalChoiceTileMinHeightRem: number;
|
|
9
|
+
horizontalChoiceContentMinHeightRem: number;
|
|
10
|
+
selectedImageMaxHeightRem: number;
|
|
11
|
+
choiceImageMaxHeightRem: number;
|
|
12
|
+
listenButtonSizePx: number;
|
|
13
|
+
stimulusMinColumnPx: number;
|
|
14
|
+
textMinColumnPx: number;
|
|
15
|
+
legendMaxChars: number;
|
|
16
|
+
choiceGroupGapRem: number;
|
|
17
|
+
choiceRowGapRem: number;
|
|
18
|
+
toggleButtonGapRem: number;
|
|
19
|
+
horizontalChoiceRadioTopMarginRem: number;
|
|
20
|
+
audioBlankTemplateMarginTopRem: number;
|
|
21
|
+
audioBlankTemplateMarginBottomRem: number;
|
|
22
|
+
audioInstructionsMaxWidthPx: number;
|
|
23
|
+
narrowHorizontalChoiceMaxWidthPx: number;
|
|
24
|
+
stimulusGridColumnGapRem: number;
|
|
25
|
+
stimulusGridRowGapRem: number;
|
|
26
|
+
stimulusSentenceMarginTopRem: number;
|
|
27
|
+
stimulusChoicesMarginTopRem: number;
|
|
28
|
+
tokenGridColumnGapRem: number;
|
|
29
|
+
tokenGridRowGapRem: number;
|
|
30
|
+
tokenTemplateMarginTopRem: number;
|
|
31
|
+
tokenInlineTokenGapRem: number;
|
|
32
|
+
tokenChoicesMarginTopRem: number;
|
|
33
|
+
inlineGridColumnGapRem: number;
|
|
34
|
+
inlineGridRowGapRem: number;
|
|
35
|
+
inlineTemplateMarginTopRem: number;
|
|
36
|
+
inlineChoicesMarginTopRem: number;
|
|
37
|
+
};
|
|
38
|
+
/** Baseline values tuned to match historical CQT visuals; override via model.layoutLimits. */
|
|
39
|
+
export declare const DEFAULT_LAYOUT_LIMITS: LayoutLimits;
|
|
40
|
+
//# sourceMappingURL=layoutLimits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layoutLimits.d.ts","sourceRoot":"","sources":["../../src/shared/layoutLimits.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,uBAAuB,EAAE,MAAM,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,yBAAyB,EAAE,MAAM,CAAC;IAClC,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;IAChC,gCAAgC,EAAE,MAAM,CAAC;IACzC,mCAAmC,EAAE,MAAM,CAAC;IAC5C,yBAAyB,EAAE,MAAM,CAAC;IAClC,uBAAuB,EAAE,MAAM,CAAC;IAChC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iCAAiC,EAAE,MAAM,CAAC;IAC1C,8BAA8B,EAAE,MAAM,CAAC;IACvC,iCAAiC,EAAE,MAAM,CAAC;IAC1C,2BAA2B,EAAE,MAAM,CAAC;IACpC,gCAAgC,EAAE,MAAM,CAAC;IACzC,wBAAwB,EAAE,MAAM,CAAC;IACjC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,4BAA4B,EAAE,MAAM,CAAC;IACrC,2BAA2B,EAAE,MAAM,CAAC;IACpC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yBAAyB,EAAE,MAAM,CAAC;IAClC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,wBAAwB,EAAE,MAAM,CAAC;IACjC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,0BAA0B,EAAE,MAAM,CAAC;IACnC,yBAAyB,EAAE,MAAM,CAAC;CACnC,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,qBAAqB,EAAE,YAoCnC,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { LayoutLimits } from './layoutLimits';
|
|
2
|
+
export type McpbChoiceMode = 'text' | 'image';
|
|
3
|
+
export type McpbInteractionMode = 'populate_blank' | 'audio_mc_only';
|
|
4
|
+
export type McpbDeliveryMode = 'gather' | 'view' | 'evaluate' | 'authoring' | 'print';
|
|
5
|
+
export type McpbRole = 'student' | 'instructor';
|
|
6
|
+
export type McpbCorrectness = 'correct' | 'incorrect' | 'unanswered';
|
|
7
|
+
export interface McpbChoice {
|
|
8
|
+
id: string;
|
|
9
|
+
labelHtml?: string;
|
|
10
|
+
imageUrl?: string;
|
|
11
|
+
imageAlt?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface McpbAudioButtonSkin {
|
|
14
|
+
silentUrl: string;
|
|
15
|
+
playingUrl: string;
|
|
16
|
+
}
|
|
17
|
+
export interface McpbUiText {
|
|
18
|
+
answerChoices?: string;
|
|
19
|
+
selectedAnswerInSentence?: string;
|
|
20
|
+
showCorrectAnswer?: string;
|
|
21
|
+
hideCorrectAnswer?: string;
|
|
22
|
+
clickToEnableAutoplay?: string;
|
|
23
|
+
audioResourceUnavailable?: string;
|
|
24
|
+
transcriptLabel?: string;
|
|
25
|
+
listenLabelEn?: string;
|
|
26
|
+
listenLabelEs?: string;
|
|
27
|
+
listenSilentAlt?: string;
|
|
28
|
+
listenPlayingAlt?: string;
|
|
29
|
+
listenSilentAltEs?: string;
|
|
30
|
+
listenPlayingAltEs?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface McpbQuestion {
|
|
33
|
+
id?: string;
|
|
34
|
+
element?: string;
|
|
35
|
+
prompt?: string;
|
|
36
|
+
promptEnabled?: boolean;
|
|
37
|
+
interactionMode?: McpbInteractionMode;
|
|
38
|
+
layoutProfile?: string;
|
|
39
|
+
choiceLayout?: string;
|
|
40
|
+
layoutProfilePresets?: Record<string, Partial<LayoutLimits>>;
|
|
41
|
+
layoutLimits?: Partial<LayoutLimits>;
|
|
42
|
+
audioButtonSkin?: McpbAudioButtonSkin | null;
|
|
43
|
+
audioButtonSkinsByLocale?: Record<string, McpbAudioButtonSkin>;
|
|
44
|
+
uiText?: McpbUiText;
|
|
45
|
+
sentenceHtml?: string;
|
|
46
|
+
template?: string;
|
|
47
|
+
choiceMode?: McpbChoiceMode;
|
|
48
|
+
choices?: McpbChoice[];
|
|
49
|
+
correctChoiceId?: string;
|
|
50
|
+
hasAudio?: boolean;
|
|
51
|
+
autoplayAudioEnabled?: boolean;
|
|
52
|
+
completeAudioEnabled?: boolean;
|
|
53
|
+
audioUrl?: string;
|
|
54
|
+
audioTranscript?: string;
|
|
55
|
+
showVisibleTranscript?: boolean;
|
|
56
|
+
useFeatureButtonAudio?: boolean;
|
|
57
|
+
locale?: string;
|
|
58
|
+
shuffle?: boolean;
|
|
59
|
+
lockChoiceOrder?: boolean;
|
|
60
|
+
teacherInstructions?: string;
|
|
61
|
+
teacherInstructionsEnabled?: boolean;
|
|
62
|
+
customType?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface McpbSession {
|
|
65
|
+
id?: string;
|
|
66
|
+
element?: string;
|
|
67
|
+
choiceId?: string;
|
|
68
|
+
audioStartTime?: number;
|
|
69
|
+
audioEndTime?: number;
|
|
70
|
+
shuffledValues?: string[];
|
|
71
|
+
data?: {
|
|
72
|
+
shuffledValues?: string[];
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface McpbEnv {
|
|
76
|
+
mode?: McpbDeliveryMode;
|
|
77
|
+
role?: McpbRole;
|
|
78
|
+
'@pie-element'?: {
|
|
79
|
+
lockChoiceOrder?: boolean;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-element/mc-populated-blank",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "Multiple-choice option populates an inline blank in a sentence template (Svelte 5 PIE element)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"clean": "rm -rf dist tsconfig.tsbuildinfo"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
+
"@pie-lib/delivery-events-svelte": "0.1.0",
|
|
50
51
|
"@pie-lib/styling-svelte": "0.1.2"
|
|
51
52
|
},
|
|
52
53
|
"peerDependencies": {
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
57
58
|
"svelte": "^5.54.0",
|
|
58
59
|
"typescript": "^5.9.3",
|
|
59
|
-
"vite": "^8.0.1"
|
|
60
|
+
"vite": "^8.0.1",
|
|
61
|
+
"vitest": "^4.1.0"
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -1,43 +1,9 @@
|
|
|
1
|
+
import { DEFAULT_LAYOUT_LIMITS } from '../shared/layoutLimits';
|
|
2
|
+
|
|
3
|
+
export { DEFAULT_LAYOUT_LIMITS };
|
|
4
|
+
|
|
1
5
|
/** Token authors must include exactly once in `template` (HTML). */
|
|
2
6
|
export const BLANK_TOKEN = '{{blank}}';
|
|
3
|
-
/** Baseline values tuned to match historical CQT visuals; override via model.layoutLimits. */
|
|
4
|
-
export const DEFAULT_LAYOUT_LIMITS = {
|
|
5
|
-
blankStandaloneWidthRem: 7,
|
|
6
|
-
blankWideWidthRem: 10,
|
|
7
|
-
blankUnderlineWidthPx: 2,
|
|
8
|
-
blankUnderlineWideWidthPx: 4,
|
|
9
|
-
horizontalChoiceWidthPx: 170,
|
|
10
|
-
horizontalChoiceWidthVw: 30,
|
|
11
|
-
horizontalChoiceTileMinHeightRem: 11,
|
|
12
|
-
horizontalChoiceContentMinHeightRem: 7.5,
|
|
13
|
-
selectedImageMaxHeightRem: 4,
|
|
14
|
-
choiceImageMaxHeightRem: 5,
|
|
15
|
-
listenButtonSizePx: 128,
|
|
16
|
-
stimulusMinColumnPx: 210,
|
|
17
|
-
textMinColumnPx: 260,
|
|
18
|
-
legendMaxChars: 120,
|
|
19
|
-
choiceGroupGapRem: 0.5,
|
|
20
|
-
choiceRowGapRem: 0.5,
|
|
21
|
-
toggleButtonGapRem: 0.5,
|
|
22
|
-
horizontalChoiceRadioTopMarginRem: 0.5,
|
|
23
|
-
audioBlankTemplateMarginTopRem: 0.8,
|
|
24
|
-
audioBlankTemplateMarginBottomRem: 1.8,
|
|
25
|
-
audioInstructionsMaxWidthPx: 875,
|
|
26
|
-
narrowHorizontalChoiceMaxWidthPx: 230,
|
|
27
|
-
stimulusGridColumnGapRem: 2,
|
|
28
|
-
stimulusGridRowGapRem: 0.7,
|
|
29
|
-
stimulusSentenceMarginTopRem: 0.2,
|
|
30
|
-
stimulusChoicesMarginTopRem: 0.9,
|
|
31
|
-
tokenGridColumnGapRem: 1.5,
|
|
32
|
-
tokenGridRowGapRem: 0.8,
|
|
33
|
-
tokenTemplateMarginTopRem: 0.6,
|
|
34
|
-
tokenInlineTokenGapRem: 0.35,
|
|
35
|
-
tokenChoicesMarginTopRem: 0.2,
|
|
36
|
-
inlineGridColumnGapRem: 1.5,
|
|
37
|
-
inlineGridRowGapRem: 0.65,
|
|
38
|
-
inlineTemplateMarginTopRem: 0.45,
|
|
39
|
-
inlineChoicesMarginTopRem: 0.25,
|
|
40
|
-
} as const;
|
|
41
7
|
|
|
42
8
|
export default {
|
|
43
9
|
model: {
|
|
@@ -54,7 +20,7 @@ export default {
|
|
|
54
20
|
audioButtonSkinsByLocale: {},
|
|
55
21
|
uiText: {
|
|
56
22
|
answerChoices: 'Answer choices',
|
|
57
|
-
selectedAnswerInSentence: '
|
|
23
|
+
selectedAnswerInSentence: 'blank',
|
|
58
24
|
showCorrectAnswer: 'Show correct answer',
|
|
59
25
|
hideCorrectAnswer: 'Hide correct answer',
|
|
60
26
|
clickToEnableAutoplay: 'Click to enable audio autoplay',
|
|
@@ -62,6 +28,10 @@ export default {
|
|
|
62
28
|
transcriptLabel: 'Transcript',
|
|
63
29
|
listenLabelEn: 'Listen',
|
|
64
30
|
listenLabelEs: 'Escuchar',
|
|
31
|
+
listenSilentAlt: 'Repeat instructions',
|
|
32
|
+
listenPlayingAlt: 'Instructions are playing',
|
|
33
|
+
listenSilentAltEs: 'Escuchar. Repetir las instrucciones.',
|
|
34
|
+
listenPlayingAltEs: 'Escuchar. Estas son las instrucciones.',
|
|
65
35
|
},
|
|
66
36
|
sentenceHtml: '',
|
|
67
37
|
template: `<p>The answer is ${BLANK_TOKEN}.</p>`,
|
|
@@ -0,0 +1,428 @@
|
|
|
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
|
+
});
|