@longsightgroup/qti3-core 0.9.2 → 0.9.3

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 (219) hide show
  1. package/README.md +20 -3
  2. package/dist/assert-never.d.ts +3 -0
  3. package/dist/assert-never.d.ts.map +1 -0
  4. package/dist/assert-never.js +5 -0
  5. package/dist/assert-never.js.map +1 -0
  6. package/dist/attempt-state-constants.d.ts +6 -0
  7. package/dist/attempt-state-constants.d.ts.map +1 -0
  8. package/dist/attempt-state-constants.js +6 -0
  9. package/dist/attempt-state-constants.js.map +1 -0
  10. package/dist/attempt-state.d.ts +5 -0
  11. package/dist/attempt-state.d.ts.map +1 -0
  12. package/dist/attempt-state.js +190 -0
  13. package/dist/attempt-state.js.map +1 -0
  14. package/dist/catalog.js +1 -1
  15. package/dist/catalog.js.map +1 -1
  16. package/dist/content-text.d.ts.map +1 -1
  17. package/dist/content-text.js +3 -0
  18. package/dist/content-text.js.map +1 -1
  19. package/dist/custom-operators.d.ts +13 -0
  20. package/dist/custom-operators.d.ts.map +1 -0
  21. package/dist/custom-operators.js +2 -0
  22. package/dist/custom-operators.js.map +1 -0
  23. package/dist/delivery-redaction.js +1 -1
  24. package/dist/delivery-redaction.js.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/parser-custom-interactions.js +1 -1
  30. package/dist/parser-custom-interactions.js.map +1 -1
  31. package/dist/parser-declarations.d.ts.map +1 -1
  32. package/dist/parser-declarations.js +4 -4
  33. package/dist/parser-declarations.js.map +1 -1
  34. package/dist/parser-item-metadata.d.ts.map +1 -1
  35. package/dist/parser-item-metadata.js +8 -7
  36. package/dist/parser-item-metadata.js.map +1 -1
  37. package/dist/parser-processing.js +2 -2
  38. package/dist/parser-processing.js.map +1 -1
  39. package/dist/parser-values.d.ts +5 -1
  40. package/dist/parser-values.d.ts.map +1 -1
  41. package/dist/parser-values.js +33 -0
  42. package/dist/parser-values.js.map +1 -1
  43. package/dist/processing-evaluator-boolean.d.ts +8 -0
  44. package/dist/processing-evaluator-boolean.d.ts.map +1 -0
  45. package/dist/processing-evaluator-boolean.js +47 -0
  46. package/dist/processing-evaluator-boolean.js.map +1 -0
  47. package/dist/processing-evaluator-collection.d.ts +12 -0
  48. package/dist/processing-evaluator-collection.d.ts.map +1 -0
  49. package/dist/processing-evaluator-collection.js +35 -0
  50. package/dist/processing-evaluator-collection.js.map +1 -0
  51. package/dist/processing-evaluator-comparison.d.ts +8 -0
  52. package/dist/processing-evaluator-comparison.d.ts.map +1 -0
  53. package/dist/processing-evaluator-comparison.js +88 -0
  54. package/dist/processing-evaluator-comparison.js.map +1 -0
  55. package/dist/processing-evaluator-custom.d.ts +8 -0
  56. package/dist/processing-evaluator-custom.d.ts.map +1 -0
  57. package/dist/processing-evaluator-custom.js +14 -0
  58. package/dist/processing-evaluator-custom.js.map +1 -0
  59. package/dist/processing-evaluator-geometry.d.ts +8 -0
  60. package/dist/processing-evaluator-geometry.d.ts.map +1 -0
  61. package/dist/processing-evaluator-geometry.js +21 -0
  62. package/dist/processing-evaluator-geometry.js.map +1 -0
  63. package/dist/processing-evaluator-numeric.d.ts +8 -0
  64. package/dist/processing-evaluator-numeric.d.ts.map +1 -0
  65. package/dist/processing-evaluator-numeric.js +112 -0
  66. package/dist/processing-evaluator-numeric.js.map +1 -0
  67. package/dist/processing-evaluator-string.d.ts +8 -0
  68. package/dist/processing-evaluator-string.d.ts.map +1 -0
  69. package/dist/processing-evaluator-string.js +33 -0
  70. package/dist/processing-evaluator-string.js.map +1 -0
  71. package/dist/processing-evaluator-variable.d.ts +8 -0
  72. package/dist/processing-evaluator-variable.d.ts.map +1 -0
  73. package/dist/processing-evaluator-variable.js +39 -0
  74. package/dist/processing-evaluator-variable.js.map +1 -0
  75. package/dist/processing-evaluator.d.ts +22 -0
  76. package/dist/processing-evaluator.d.ts.map +1 -0
  77. package/dist/processing-evaluator.js +137 -0
  78. package/dist/processing-evaluator.js.map +1 -0
  79. package/dist/processing-expression-children.d.ts +3 -0
  80. package/dist/processing-expression-children.d.ts.map +1 -0
  81. package/dist/processing-expression-children.js +68 -0
  82. package/dist/processing-expression-children.js.map +1 -0
  83. package/dist/processing-mapping.d.ts +13 -0
  84. package/dist/processing-mapping.d.ts.map +1 -0
  85. package/dist/processing-mapping.js +120 -0
  86. package/dist/processing-mapping.js.map +1 -0
  87. package/dist/processing-operators.d.ts +11 -0
  88. package/dist/processing-operators.d.ts.map +1 -0
  89. package/dist/processing-operators.js +178 -0
  90. package/dist/processing-operators.js.map +1 -0
  91. package/dist/processing-random.d.ts +2 -0
  92. package/dist/processing-random.d.ts.map +1 -0
  93. package/dist/processing-random.js +16 -0
  94. package/dist/processing-random.js.map +1 -0
  95. package/dist/processing-state.d.ts +8 -0
  96. package/dist/processing-state.d.ts.map +1 -0
  97. package/dist/processing-state.js +44 -0
  98. package/dist/processing-state.js.map +1 -0
  99. package/dist/processing-templates.d.ts +3 -0
  100. package/dist/processing-templates.d.ts.map +1 -0
  101. package/dist/processing-templates.js +12 -0
  102. package/dist/processing-templates.js.map +1 -0
  103. package/dist/processing-values.d.ts +15 -0
  104. package/dist/processing-values.d.ts.map +1 -0
  105. package/dist/processing-values.js +150 -0
  106. package/dist/processing-values.js.map +1 -0
  107. package/dist/processing-variables.d.ts +7 -0
  108. package/dist/processing-variables.d.ts.map +1 -0
  109. package/dist/processing-variables.js +40 -0
  110. package/dist/processing-variables.js.map +1 -0
  111. package/dist/serializer-processing-expressions.d.ts.map +1 -1
  112. package/dist/serializer-processing-expressions.js +2 -8
  113. package/dist/serializer-processing-expressions.js.map +1 -1
  114. package/dist/serializer-processing-xml.js +2 -2
  115. package/dist/serializer-processing-xml.js.map +1 -1
  116. package/dist/serializer-processing.d.ts.map +1 -1
  117. package/dist/serializer-processing.js +3 -0
  118. package/dist/serializer-processing.js.map +1 -1
  119. package/dist/session.d.ts +5 -14
  120. package/dist/session.d.ts.map +1 -1
  121. package/dist/session.js +76 -1158
  122. package/dist/session.js.map +1 -1
  123. package/dist/shared-vocabulary-coverage-policy.js +1 -1
  124. package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
  125. package/dist/shared-vocabulary-generated-families.js +1 -1
  126. package/dist/shared-vocabulary-generated-families.js.map +1 -1
  127. package/dist/shared-vocabulary-support.js +3 -3
  128. package/dist/shared-vocabulary-support.js.map +1 -1
  129. package/dist/shared-vocabulary.d.ts.map +1 -1
  130. package/dist/shared-vocabulary.js +4 -3
  131. package/dist/shared-vocabulary.js.map +1 -1
  132. package/dist/support-evidence.d.ts +22 -0
  133. package/dist/support-evidence.d.ts.map +1 -0
  134. package/dist/support-evidence.js +93 -0
  135. package/dist/support-evidence.js.map +1 -0
  136. package/dist/support.d.ts.map +1 -1
  137. package/dist/support.js +30 -43
  138. package/dist/support.js.map +1 -1
  139. package/dist/trusted-item-session.d.ts.map +1 -1
  140. package/dist/trusted-item-session.js +6 -5
  141. package/dist/trusted-item-session.js.map +1 -1
  142. package/dist/tts.d.ts.map +1 -1
  143. package/dist/tts.js +16 -2
  144. package/dist/tts.js.map +1 -1
  145. package/dist/validation-geometry.d.ts +8 -0
  146. package/dist/validation-geometry.d.ts.map +1 -0
  147. package/dist/validation-geometry.js +187 -0
  148. package/dist/validation-geometry.js.map +1 -0
  149. package/dist/validation-interactions.d.ts +3 -0
  150. package/dist/validation-interactions.d.ts.map +1 -0
  151. package/dist/validation-interactions.js +644 -0
  152. package/dist/validation-interactions.js.map +1 -0
  153. package/dist/validation-primitives.d.ts +11 -0
  154. package/dist/validation-primitives.d.ts.map +1 -0
  155. package/dist/validation-primitives.js +48 -0
  156. package/dist/validation-primitives.js.map +1 -0
  157. package/dist/validation-processing.d.ts +4 -0
  158. package/dist/validation-processing.d.ts.map +1 -0
  159. package/dist/validation-processing.js +569 -0
  160. package/dist/validation-processing.js.map +1 -0
  161. package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
  162. package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
  163. package/dist/validation-shared-vocabulary-layout.js +102 -0
  164. package/dist/validation-shared-vocabulary-layout.js.map +1 -0
  165. package/dist/validation.d.ts.map +1 -1
  166. package/dist/validation.js +27 -1641
  167. package/dist/validation.js.map +1 -1
  168. package/dist/xml.js +1 -1
  169. package/dist/xml.js.map +1 -1
  170. package/package.json +1 -1
  171. package/src/assert-never.ts +4 -0
  172. package/src/attempt-state-constants.ts +5 -0
  173. package/src/attempt-state.ts +245 -0
  174. package/src/catalog.ts +1 -1
  175. package/src/content-text.ts +3 -0
  176. package/src/custom-operators.ts +12 -0
  177. package/src/delivery-redaction.ts +1 -1
  178. package/src/index.ts +2 -0
  179. package/src/parser-custom-interactions.ts +1 -1
  180. package/src/parser-declarations.ts +4 -3
  181. package/src/parser-item-metadata.ts +8 -7
  182. package/src/parser-processing.ts +2 -2
  183. package/src/parser-values.ts +36 -0
  184. package/src/processing-evaluator-boolean.ts +48 -0
  185. package/src/processing-evaluator-collection.ts +50 -0
  186. package/src/processing-evaluator-comparison.ts +106 -0
  187. package/src/processing-evaluator-custom.ts +20 -0
  188. package/src/processing-evaluator-geometry.ts +26 -0
  189. package/src/processing-evaluator-numeric.ts +155 -0
  190. package/src/processing-evaluator-string.ts +60 -0
  191. package/src/processing-evaluator-variable.ts +81 -0
  192. package/src/processing-evaluator.ts +173 -0
  193. package/src/processing-expression-children.ts +69 -0
  194. package/src/processing-mapping.ts +153 -0
  195. package/src/processing-operators.ts +184 -0
  196. package/src/processing-random.ts +16 -0
  197. package/src/processing-state.ts +69 -0
  198. package/src/processing-templates.ts +12 -0
  199. package/src/processing-values.ts +162 -0
  200. package/src/processing-variables.ts +83 -0
  201. package/src/serializer-processing-expressions.ts +2 -12
  202. package/src/serializer-processing-xml.ts +2 -2
  203. package/src/serializer-processing.ts +3 -0
  204. package/src/session.ts +128 -2006
  205. package/src/shared-vocabulary-coverage-policy.ts +1 -1
  206. package/src/shared-vocabulary-generated-families.ts +1 -1
  207. package/src/shared-vocabulary-support.ts +3 -3
  208. package/src/shared-vocabulary.ts +3 -2
  209. package/src/support-evidence.ts +107 -0
  210. package/src/support.ts +59 -50
  211. package/src/trusted-item-session.ts +6 -4
  212. package/src/tts.ts +29 -15
  213. package/src/validation-geometry.ts +204 -0
  214. package/src/validation-interactions.ts +831 -0
  215. package/src/validation-primitives.ts +63 -0
  216. package/src/validation-processing.ts +785 -0
  217. package/src/validation-shared-vocabulary-layout.ts +117 -0
  218. package/src/validation.ts +44 -2077
  219. package/src/xml.ts +1 -1
@@ -48,7 +48,7 @@ export function findSharedVocabularyCoverageViolations(input: {
48
48
  supportByClass.set(entry.className, existing);
49
49
  }
50
50
 
51
- const gatedSupportClasses = [...supportByClass.keys()].sort();
51
+ const gatedSupportClasses = [...supportByClass.keys()].toSorted();
52
52
  if (gatedSupportClasses.length === 0) {
53
53
  violations.push({ message: "expected at least one enforced shared vocabulary class" });
54
54
  }
@@ -122,7 +122,7 @@ function escapeRegExp(value: string): string {
122
122
 
123
123
  function alternationPattern(values: readonly string[]): string {
124
124
  return [...values]
125
- .sort((left, right) => right.length - left.length)
125
+ .toSorted((left, right) => right.length - left.length)
126
126
  .map(escapeRegExp)
127
127
  .join("|");
128
128
  }
@@ -52,7 +52,7 @@ const graphicBrowserTests = [
52
52
  "tests/browser/player-graphic.spec.ts",
53
53
  "tests/browser/player-graphic-gap-match.spec.ts",
54
54
  ];
55
- const mediaBrowserTests = ["tests/browser/player.spec.ts", ...sharedVocabularyMatrixTests];
55
+ const mediaBrowserTests = ["tests/browser/player-media.spec.ts", ...sharedVocabularyMatrixTests];
56
56
  const mediaPlayerFixture =
57
57
  "packages/fixtures/packages/sv-matrix/items/media-controls-and-pause.xml";
58
58
  const interactionInputWidthFixtures = [
@@ -204,14 +204,14 @@ function registryAttributeSupportProfile(fieldId: string): {
204
204
  fixtures: [mediaPlayerFixture],
205
205
  tests: mediaBrowserTests,
206
206
  notes:
207
- "Reflects authored pause-delay values on rendered media assets. Pause timer behavior is covered in tests/browser/player.spec.ts.",
207
+ "Reflects authored pause-delay values on rendered media assets. Pause timer behavior is covered in tests/browser/player-media.spec.ts.",
208
208
  };
209
209
  case "media-player-pause-duration":
210
210
  return {
211
211
  fixtures: [mediaPlayerFixture],
212
212
  tests: mediaBrowserTests,
213
213
  notes:
214
- "Reflects authored pause-duration values on rendered media assets. Pause timer behavior is covered in tests/browser/player.spec.ts.",
214
+ "Reflects authored pause-duration values on rendered media assets. Pause timer behavior is covered in tests/browser/player-media.spec.ts.",
215
215
  };
216
216
  default:
217
217
  return {
@@ -73,9 +73,10 @@ export function firstMatchingSharedVocabularyClass<T extends string>(
73
73
  classNames: string[],
74
74
  tokens: readonly T[],
75
75
  ): T | undefined {
76
- const tokenSet = new Set<string>(tokens);
77
76
  for (const className of classNames) {
78
- if (tokenSet.has(className)) return className as T;
77
+ for (const token of tokens) {
78
+ if (token === className) return token;
79
+ }
79
80
  }
80
81
  return undefined;
81
82
  }
@@ -0,0 +1,107 @@
1
+ import type { QtiInteractionType } from "./types.js";
2
+
3
+ const coreProcessingTest = "packages/core/src/core.test.ts";
4
+
5
+ export const browserProcessingTests = ["tests/browser/player-processing.spec.ts"];
6
+ export const browserFeedbackTests = ["tests/browser/player-feedback.spec.ts"];
7
+ export const browserAdaptiveTests = ["tests/browser/player-adaptive.spec.ts"];
8
+ export const browserBodyContentTests = ["tests/browser/player-body-content.spec.ts"];
9
+ export const browserHarnessTests = ["tests/browser/player-harness.spec.ts"];
10
+ export const browserMathmlTests = ["tests/browser/player-mathml.spec.ts"];
11
+ export const browserKeyboardA11yTests = ["tests/browser/player-keyboard-a11y.spec.ts"];
12
+ export const browserLifecycleTests = ["tests/browser/player-lifecycle.spec.ts"];
13
+
14
+ const templateProcessingBrowserEvidence = [
15
+ ...browserProcessingTests,
16
+ ...browserBodyContentTests,
17
+ ...browserMathmlTests,
18
+ ];
19
+ const responseProcessingBrowserEvidence = [
20
+ ...browserProcessingTests,
21
+ ...browserFeedbackTests,
22
+ ...browserAdaptiveTests,
23
+ ];
24
+ const randomIntegerBrowserEvidence = browserProcessingTests;
25
+
26
+ /** Browser evidence bundles keyed by parent processing constructs only. */
27
+ export const processingBrowserEvidence = {
28
+ "qti-template-processing": templateProcessingBrowserEvidence,
29
+ "qti-response-processing": responseProcessingBrowserEvidence,
30
+ "qti-random-integer": randomIntegerBrowserEvidence,
31
+ } as const satisfies Record<string, readonly string[]>;
32
+
33
+ export type ProcessingBrowserEvidenceName = keyof typeof processingBrowserEvidence;
34
+
35
+ export const interactionExtraFixtures: Partial<Record<QtiInteractionType, readonly string[]>> = {
36
+ extendedText: [
37
+ "packages/fixtures/packages/sv-matrix/items/extended-text-pattern-mask.xml",
38
+ "packages/fixtures/packages/sv-matrix/items/extended-text-xhtml.xml",
39
+ ],
40
+ textEntry: ["packages/fixtures/packages/sv-matrix/items/text-entry-pattern-mask-inline.xml"],
41
+ };
42
+
43
+ export function browserTestsFor(interactionType: QtiInteractionType): string[] {
44
+ const base = [
45
+ "packages/fixtures/src/fixtures.test.ts",
46
+ "packages/conformance/src/conformance.test.ts",
47
+ "packages/a11y/src/a11y.test.ts",
48
+ "tests/browser/player-interaction-sweep.spec.ts",
49
+ ];
50
+ const extras: Partial<Record<QtiInteractionType, string[]>> = {
51
+ associate: browserKeyboardA11yTests,
52
+ choice: ["tests/browser/player-choice.spec.ts", "tests/browser/player-dom-behavior.spec.ts"],
53
+ drawing: ["tests/browser/player-graphic.spec.ts"],
54
+ endAttempt: ["tests/browser/player-dom-behavior.spec.ts", ...browserKeyboardA11yTests],
55
+ extendedText: [
56
+ "tests/browser/player-dom-behavior.spec.ts",
57
+ "tests/browser/player-extended-text-xhtml.spec.ts",
58
+ ],
59
+ gapMatch: ["tests/browser/player-gap-match.spec.ts"],
60
+ graphicAssociate: ["tests/browser/player-graphic.spec.ts"],
61
+ graphicGapMatch: [
62
+ "tests/browser/player-graphic-gap-match.spec.ts",
63
+ "tests/browser/player-graphic.spec.ts",
64
+ ],
65
+ graphicOrder: ["tests/browser/player-graphic.spec.ts"],
66
+ hotspot: ["tests/browser/player-graphic.spec.ts"],
67
+ hottext: ["tests/browser/player-hottext.spec.ts", "tests/browser/player-dom-behavior.spec.ts"],
68
+ inlineChoice: ["tests/browser/player-inline-choice.spec.ts"],
69
+ match: ["tests/browser/player-match.spec.ts", "tests/browser/player-dom-behavior.spec.ts"],
70
+ media: ["tests/browser/player-media.spec.ts"],
71
+ order: [...browserKeyboardA11yTests, ...browserLifecycleTests],
72
+ portableCustom: ["tests/browser/player-portable-custom.spec.ts"],
73
+ positionObject: ["tests/browser/player-graphic.spec.ts", ...browserKeyboardA11yTests],
74
+ selectPoint: ["tests/browser/player-graphic.spec.ts"],
75
+ slider: browserKeyboardA11yTests,
76
+ textEntry: ["tests/browser/player-dom-behavior.spec.ts"],
77
+ upload: ["tests/browser/player-dom-behavior.spec.ts"],
78
+ };
79
+ return [...base, ...(extras[interactionType] ?? [])];
80
+ }
81
+
82
+ export function interactionSupportFixtures(interactionType: QtiInteractionType): string[] {
83
+ return [
84
+ `packages/fixtures/xml/${interactionType}-reference.xml`,
85
+ ...(interactionExtraFixtures[interactionType] ?? []),
86
+ ];
87
+ }
88
+
89
+ export function interactionSupportTests(interactionType: QtiInteractionType): string[] {
90
+ if (interactionType === "extendedText" || interactionType === "textEntry") {
91
+ return [...browserTestsFor(interactionType), "packages/core/src/pattern-mask.test.ts"];
92
+ }
93
+ return browserTestsFor(interactionType);
94
+ }
95
+
96
+ function isProcessingBrowserEvidenceName(
97
+ qtiName: string,
98
+ ): qtiName is ProcessingBrowserEvidenceName {
99
+ return Object.hasOwn(processingBrowserEvidence, qtiName);
100
+ }
101
+
102
+ export function processingSupportTests(qtiName: string): string[] {
103
+ if (!isProcessingBrowserEvidenceName(qtiName)) {
104
+ return [coreProcessingTest];
105
+ }
106
+ return [coreProcessingTest, ...processingBrowserEvidence[qtiName]];
107
+ }
package/src/support.ts CHANGED
@@ -8,6 +8,13 @@ import type {
8
8
  QtiProcessingElementSupport,
9
9
  QtiSourceLocation,
10
10
  } from "./types.js";
11
+ import {
12
+ browserFeedbackTests,
13
+ browserHarnessTests,
14
+ interactionSupportFixtures,
15
+ interactionSupportTests,
16
+ processingBrowserEvidence,
17
+ } from "./support-evidence.js";
11
18
 
12
19
  export const interactionSupport: QtiInteractionElementSupport[] = [
13
20
  entry("qti-associate-interaction", "associate"),
@@ -51,23 +58,36 @@ export const deprecatedInteractionSupport: QtiInteractionElementSupport[] = [
51
58
  ];
52
59
 
53
60
  export const processingSupport: QtiProcessingElementSupport[] = [
54
- processingEntry("qti-template-processing", "packages/core/src/core.test.ts", [
55
- "packages/fixtures/xml/template-processing-reference.xml",
56
- "packages/fixtures/xml/template-content-reference.xml",
57
- ]),
58
- processingEntry("qti-response-processing", "packages/core/src/core.test.ts", [
59
- "packages/fixtures/xml/mapping-processing-reference.xml",
60
- "packages/fixtures/xml/generic-match-processing-reference.xml",
61
- "packages/fixtures/xml/advanced-processing-reference.xml",
62
- "packages/fixtures/xml/adaptive-feedback-reference.xml",
63
- ]),
61
+ processingEntry(
62
+ "qti-template-processing",
63
+ "packages/core/src/core.test.ts",
64
+ [
65
+ "packages/fixtures/xml/template-processing-reference.xml",
66
+ "packages/fixtures/xml/random-integer-template-reference.xml",
67
+ "packages/fixtures/xml/template-content-reference.xml",
68
+ ],
69
+ [...processingBrowserEvidence["qti-template-processing"]],
70
+ ),
71
+ processingEntry(
72
+ "qti-response-processing",
73
+ "packages/core/src/core.test.ts",
74
+ [
75
+ "packages/fixtures/xml/mapping-processing-reference.xml",
76
+ "packages/fixtures/xml/generic-match-processing-reference.xml",
77
+ "packages/fixtures/xml/advanced-processing-reference.xml",
78
+ "packages/fixtures/xml/adaptive-feedback-reference.xml",
79
+ ],
80
+ [...processingBrowserEvidence["qti-response-processing"]],
81
+ ),
64
82
  processingEntry("qti-set-template-value", "packages/core/src/core.test.ts", [
65
83
  "packages/fixtures/xml/template-processing-reference.xml",
84
+ "packages/fixtures/xml/random-integer-template-reference.xml",
66
85
  "packages/fixtures/xml/template-content-reference.xml",
67
86
  ]),
68
87
  processingEntry("qti-set-default-value", "packages/core/src/core.test.ts"),
69
88
  processingEntry("qti-set-correct-response", "packages/core/src/core.test.ts", [
70
89
  "packages/fixtures/xml/template-processing-reference.xml",
90
+ "packages/fixtures/xml/random-integer-template-reference.xml",
71
91
  ]),
72
92
  processingEntry("qti-template-condition", "packages/core/src/core.test.ts"),
73
93
  processingEntry("qti-template-if", "packages/core/src/core.test.ts"),
@@ -119,11 +139,17 @@ export const processingSupport: QtiProcessingElementSupport[] = [
119
139
  processingEntry("qti-variable", "packages/core/src/core.test.ts", [
120
140
  "packages/fixtures/xml/generic-match-processing-reference.xml",
121
141
  "packages/fixtures/xml/template-processing-reference.xml",
142
+ "packages/fixtures/xml/random-integer-template-reference.xml",
122
143
  "packages/fixtures/xml/template-content-reference.xml",
123
144
  "packages/fixtures/xml/advanced-processing-reference.xml",
124
145
  "packages/fixtures/xml/adaptive-feedback-reference.xml",
125
146
  ]),
126
- processingEntry("qti-random-integer", "packages/core/src/core.test.ts"),
147
+ processingEntry(
148
+ "qti-random-integer",
149
+ "packages/core/src/core.test.ts",
150
+ ["packages/fixtures/xml/random-integer-template-reference.xml"],
151
+ [...processingBrowserEvidence["qti-random-integer"]],
152
+ ),
127
153
  processingEntry("qti-random-float", "packages/core/src/core.test.ts"),
128
154
  processingEntry("qti-random", "packages/core/src/core.test.ts"),
129
155
  processingEntry("qti-multiple", "packages/core/src/core.test.ts", [
@@ -139,8 +165,11 @@ export const processingSupport: QtiProcessingElementSupport[] = [
139
165
  ]),
140
166
  processingEntry("qti-sum", "packages/core/src/core.test.ts", [
141
167
  "packages/fixtures/xml/template-processing-reference.xml",
168
+ "packages/fixtures/xml/random-integer-template-reference.xml",
169
+ ]),
170
+ processingEntry("qti-product", "packages/core/src/core.test.ts", [
171
+ "packages/fixtures/xml/random-integer-template-reference.xml",
142
172
  ]),
143
- processingEntry("qti-product", "packages/core/src/core.test.ts"),
144
173
  processingEntry("qti-min", "packages/core/src/core.test.ts"),
145
174
  processingEntry("qti-max", "packages/core/src/core.test.ts"),
146
175
  processingEntry("qti-subtract", "packages/core/src/core.test.ts"),
@@ -205,9 +234,13 @@ export const itemMetadataSupport: QtiItemMetadataElementSupport[] = [
205
234
  fixtures: [
206
235
  "packages/fixtures/packages/basic-item-player/valid-item-only/items/tolerance-extra-features.xml",
207
236
  ],
208
- tests: ["packages/core/src/core.test.ts", "packages/core/src/parser-item-metadata.test.ts"],
237
+ tests: [
238
+ "packages/core/src/core.test.ts",
239
+ "packages/core/src/parser-item-metadata.test.ts",
240
+ ...browserHarnessTests,
241
+ ],
209
242
  notes:
210
- "Parsed by parser-item-metadata.ts and validated by validateCatalogInfo. Duplicate containers emit item.child.duplicate.",
243
+ "Parsed by parser-item-metadata.ts and validated by validateCatalogInfo. Duplicate containers emit item.child.duplicate. Manual harness debugger coverage is in player-harness.spec.ts.",
211
244
  },
212
245
  {
213
246
  qtiName: "qti-stylesheet",
@@ -225,6 +258,7 @@ export const itemMetadataSupport: QtiItemMetadataElementSupport[] = [
225
258
  "packages/core/src/core.test.ts",
226
259
  "packages/core/src/parser-item-metadata.test.ts",
227
260
  "tests/browser/player-package.spec.ts",
261
+ ...browserHarnessTests,
228
262
  ],
229
263
  notes:
230
264
  "Parsed by parser-item-metadata.ts, validated by validateStylesheets, and delivered by the player only when a host resolveStylesheet hook returns a safe resolved stylesheet URL.",
@@ -239,9 +273,13 @@ export const itemMetadataSupport: QtiItemMetadataElementSupport[] = [
239
273
  render: false,
240
274
  process: false,
241
275
  fixtures: ["packages/fixtures/src/index.ts"],
242
- tests: ["packages/core/src/core.test.ts", "packages/core/src/parser-item-metadata.test.ts"],
276
+ tests: [
277
+ "packages/core/src/core.test.ts",
278
+ "packages/core/src/parser-item-metadata.test.ts",
279
+ ...browserFeedbackTests,
280
+ ],
243
281
  notes:
244
- "Parsed by parser-item-metadata.ts and validated by validateModalFeedback. Player rendering remains out of scope for this metadata slice.",
282
+ "Parsed by parser-item-metadata.ts and validated by validateModalFeedback. Player renders outcome-gated modal feedback in player-feedback.spec.ts.",
245
283
  },
246
284
  {
247
285
  qtiName: "qti-companion-materials-info",
@@ -258,6 +296,7 @@ export const itemMetadataSupport: QtiItemMetadataElementSupport[] = [
258
296
  tests: [
259
297
  "packages/core/src/parser-companion-materials.test.ts",
260
298
  "packages/core/src/parser-item-metadata.test.ts",
299
+ ...browserHarnessTests,
261
300
  ],
262
301
  notes:
263
302
  "Parses qti-physical-material text and qti-digital-material file references. Digital materials require non-empty qti-file-href text and may include label, mime-type, and qti-resource-icon metadata. Hosts read resolved materials through createCompanionMaterialsResolution() or player.getCompanionMaterialsResolution().",
@@ -364,33 +403,14 @@ function entry(qtiName: string, interactionType: QtiInteractionType): QtiInterac
364
403
  validate: true,
365
404
  render: true,
366
405
  process: true,
367
- fixtures: [`packages/fixtures/xml/${interactionType}-reference.xml`],
368
- tests: [
369
- "packages/fixtures/src/fixtures.test.ts",
370
- "packages/conformance/src/conformance.test.ts",
371
- "packages/a11y/src/a11y.test.ts",
372
- "tests/browser/player.spec.ts",
373
- ],
406
+ fixtures: interactionSupportFixtures(interactionType),
407
+ tests: interactionSupportTests(interactionType),
374
408
  };
375
409
  }
376
410
 
377
411
  function extendedTextInteractionEntry(): QtiInteractionElementSupport {
378
412
  return {
379
413
  ...entry("qti-extended-text-interaction", "extendedText"),
380
- fixtures: [
381
- "packages/fixtures/xml/extendedText-reference.xml",
382
- "packages/fixtures/packages/sv-matrix/items/extended-text-pattern-mask.xml",
383
- "packages/fixtures/packages/sv-matrix/items/extended-text-xhtml.xml",
384
- ],
385
- tests: [
386
- "packages/fixtures/src/fixtures.test.ts",
387
- "packages/conformance/src/conformance.test.ts",
388
- "packages/a11y/src/a11y.test.ts",
389
- "packages/core/src/pattern-mask.test.ts",
390
- "tests/browser/player.spec.ts",
391
- "tests/browser/player-dom-behavior.spec.ts",
392
- "tests/browser/player-extended-text-xhtml.spec.ts",
393
- ],
394
414
  notes: "Supports plain and format=xhtml extended text.",
395
415
  };
396
416
  }
@@ -398,18 +418,6 @@ function extendedTextInteractionEntry(): QtiInteractionElementSupport {
398
418
  function textEntryInteractionEntry(): QtiInteractionElementSupport {
399
419
  return {
400
420
  ...entry("qti-text-entry-interaction", "textEntry"),
401
- fixtures: [
402
- "packages/fixtures/xml/textEntry-reference.xml",
403
- "packages/fixtures/packages/sv-matrix/items/text-entry-pattern-mask-inline.xml",
404
- ],
405
- tests: [
406
- "packages/fixtures/src/fixtures.test.ts",
407
- "packages/conformance/src/conformance.test.ts",
408
- "packages/a11y/src/a11y.test.ts",
409
- "packages/core/src/pattern-mask.test.ts",
410
- "tests/browser/player.spec.ts",
411
- "tests/browser/player-dom-behavior.spec.ts",
412
- ],
413
421
  notes: "Supports placeholder-text and pattern-mask on text-entry controls.",
414
422
  };
415
423
  }
@@ -426,6 +434,7 @@ function processingEntry(
426
434
  qtiName: string,
427
435
  test: string,
428
436
  fixtures: string[] = [],
437
+ extraTests: string[] = [],
429
438
  ): QtiProcessingElementSupport {
430
439
  return {
431
440
  qtiName,
@@ -437,6 +446,6 @@ function processingEntry(
437
446
  render: false,
438
447
  process: true,
439
448
  fixtures,
440
- tests: [test],
449
+ tests: [test, ...extraTests],
441
450
  };
442
451
  }
@@ -107,6 +107,7 @@ export function runTrustedItemSession(
107
107
  parsedResult.parsed,
108
108
  input.priorState,
109
109
  input.diagnosticPrefix,
110
+ input.allowedUndeclaredResponseIdentifiers,
110
111
  parsedResult.parsed.diagnostics,
111
112
  );
112
113
  if (!sessionResult.ok) {
@@ -127,9 +128,7 @@ export function runTrustedItemSession(
127
128
  return emptyTrustedItemSessionFailure(diagnostics);
128
129
  }
129
130
 
130
- const shouldScore =
131
- input.scoring === "always" ||
132
- (input.scoring === "onSubmission" && applicationResult.appliedSubmission);
131
+ const shouldScore = input.scoring === "always" || applicationResult.appliedSubmission;
133
132
 
134
133
  let outcomes = sessionResult.session.serialize().outcomes;
135
134
  let state = sessionResult.session.serialize();
@@ -254,12 +253,15 @@ function createTrustedItemSession(
254
253
  parsed: QtiParsedTrustedItem,
255
254
  priorState: QtiAttemptStateV1 | null | undefined,
256
255
  diagnosticPrefix: QtiTrustedInputDiagnosticPrefix,
256
+ allowedUndeclaredResponseIdentifiers: readonly string[] | undefined,
257
257
  existingDiagnostics: QtiDiagnostic[] = [],
258
258
  ): { ok: true; session: QtiItemSession } | { ok: false; diagnostics: QtiDiagnostic[] } {
259
259
  try {
260
260
  return {
261
261
  ok: true,
262
- session: createItemSession(parsed.document, priorState ?? undefined),
262
+ session: createItemSession(parsed.document, priorState ?? undefined, {
263
+ allowedUndeclaredResponseIdentifiers,
264
+ }),
263
265
  };
264
266
  } catch (error) {
265
267
  return {
package/src/tts.ts CHANGED
@@ -103,10 +103,17 @@ interface SegmentInput {
103
103
  }
104
104
 
105
105
  const dataSsmlFunctionNames = new Set(["break", "phoneme", "prosody", "say-as", "sub"]);
106
- const breakStrengths = new Set(["medium", "none", "strong", "weak", "x-strong", "x-weak"]);
107
- const phonemeAlphabets = new Set(["ipa", "x-sampa"]);
108
- const phonemeTypes = new Set(["default", "ruby"]);
109
- const sayAsInterpretations = new Set([
106
+ const breakStrengths = new Set<QtiDataSsmlBreakStrength>([
107
+ "medium",
108
+ "none",
109
+ "strong",
110
+ "weak",
111
+ "x-strong",
112
+ "x-weak",
113
+ ]);
114
+ const phonemeAlphabets = new Set<NonNullable<QtiDataSsmlPhoneme["alphabet"]>>(["ipa", "x-sampa"]);
115
+ const phonemeTypes = new Set<NonNullable<QtiDataSsmlPhoneme["type"]>>(["default", "ruby"]);
116
+ const sayAsInterpretations = new Set<QtiDataSsmlSayAs["interpret-as"]>([
110
117
  "cardinal",
111
118
  "characters",
112
119
  "date",
@@ -200,7 +207,7 @@ function validateBreak(value: Record<string, unknown>, errors: string[]): QtiDat
200
207
  const result: QtiDataSsmlBreak = {};
201
208
  const strength = optionalEnum(value, "strength", "break.strength", breakStrengths, errors);
202
209
  const time = optionalString(value, "time", "break.time", errors);
203
- if (strength) result.strength = strength as QtiDataSsmlBreakStrength;
210
+ if (strength) result.strength = strength;
204
211
  if (time !== undefined) result.time = time;
205
212
  return result;
206
213
  }
@@ -215,8 +222,8 @@ function validatePhoneme(
215
222
  const type = optionalEnum(value, "type", "phoneme.type", phonemeTypes, errors);
216
223
  if (!ph) return undefined;
217
224
  const result: QtiDataSsmlPhoneme = { ph };
218
- if (alphabet) result.alphabet = alphabet as QtiDataSsmlPhoneme["alphabet"];
219
- if (type) result.type = type as QtiDataSsmlPhoneme["type"];
225
+ if (alphabet) result.alphabet = alphabet;
226
+ if (type) result.type = type;
220
227
  return result;
221
228
  }
222
229
 
@@ -241,7 +248,7 @@ function validateSayAs(
241
248
  errors,
242
249
  );
243
250
  if (!interpretation) return undefined;
244
- return { "interpret-as": interpretation as QtiDataSsmlSayAs["interpret-as"] };
251
+ return { "interpret-as": interpretation };
245
252
  }
246
253
 
247
254
  function validateSub(value: Record<string, unknown>, errors: string[]): QtiDataSsmlSub | undefined {
@@ -520,29 +527,36 @@ function requiredString(
520
527
  return optionalString(value, name, path, errors);
521
528
  }
522
529
 
523
- function optionalEnum(
530
+ function isAllowedEnum<T extends string>(value: string, allowed: ReadonlySet<T>): value is T {
531
+ for (const entry of allowed) {
532
+ if (entry === value) return true;
533
+ }
534
+ return false;
535
+ }
536
+
537
+ function optionalEnum<T extends string>(
524
538
  value: Record<string, unknown>,
525
539
  name: string,
526
540
  path: string,
527
- allowed: Set<string>,
541
+ allowed: ReadonlySet<T>,
528
542
  errors: string[],
529
- ): string | undefined {
543
+ ): T | undefined {
530
544
  const property = optionalString(value, name, path, errors);
531
545
  if (property === undefined) return undefined;
532
- if (!allowed.has(property)) {
546
+ if (!isAllowedEnum(property, allowed)) {
533
547
  errors.push(`${path} has unsupported value "${property}".`);
534
548
  return undefined;
535
549
  }
536
550
  return property;
537
551
  }
538
552
 
539
- function requiredEnum(
553
+ function requiredEnum<T extends string>(
540
554
  value: Record<string, unknown>,
541
555
  name: string,
542
556
  path: string,
543
- allowed: Set<string>,
557
+ allowed: ReadonlySet<T>,
544
558
  errors: string[],
545
- ): string | undefined {
559
+ ): T | undefined {
546
560
  if (!Object.hasOwn(value, name)) {
547
561
  errors.push(`${path} is required.`);
548
562
  return undefined;