@conform-ed/contracts 0.0.6 → 0.0.9

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.
@@ -0,0 +1,330 @@
1
+ import { expect, test } from "bun:test";
2
+ import { H5pV1 } from "@conform-ed/contracts";
3
+
4
+ // --- PackageManifest (h5p.json) ---
5
+
6
+ test("H5P PackageManifest accepts a minimal valid h5p.json", () => {
7
+ const result = H5pV1.Schemas.PackageManifest.safeParse({
8
+ title: "My H5P Content",
9
+ language: "en",
10
+ machineName: "H5P.CoursePresentation",
11
+ mainLibrary: "H5P.CoursePresentation",
12
+ preloadedDependencies: [{ machineName: "H5P.CoursePresentation", majorVersion: 1, minorVersion: 27 }],
13
+ embedTypes: ["iframe"],
14
+ });
15
+ expect(result.success).toBe(true);
16
+ });
17
+
18
+ test("H5P PackageManifest accepts all optional fields", () => {
19
+ const result = H5pV1.Schemas.PackageManifest.safeParse({
20
+ title: "Interactive Video",
21
+ language: "en-US",
22
+ machineName: "H5P.InteractiveVideo",
23
+ mainLibrary: "H5P.InteractiveVideo",
24
+ preloadedDependencies: [
25
+ { machineName: "H5P.InteractiveVideo", majorVersion: 1, minorVersion: 28 },
26
+ { machineName: "H5P.Video", majorVersion: 1, minorVersion: 6 },
27
+ ],
28
+ embedTypes: ["iframe", "div"],
29
+ contentType: "Interactive Video",
30
+ description: "An interactive video content type",
31
+ author: "H5P Community",
32
+ authors: [{ name: "Jane Doe", role: "Author" }],
33
+ license: "CC BY",
34
+ licenseVersion: "4.0",
35
+ licenseExtras: "Attribution required",
36
+ yearFrom: 2020,
37
+ yearTo: 2024,
38
+ defaultLanguage: "en",
39
+ a11yTitle: "Interactive Video",
40
+ changes: [{ date: "01-01-24 00:00:00", changes: ["Initial release"] }],
41
+ });
42
+ expect(result.success).toBe(true);
43
+ });
44
+
45
+ test("H5P PackageManifest rejects missing required title", () => {
46
+ const result = H5pV1.Schemas.PackageManifest.safeParse({
47
+ language: "en",
48
+ machineName: "H5P.Image",
49
+ mainLibrary: "H5P.Image",
50
+ preloadedDependencies: [{ machineName: "H5P.Image", majorVersion: 1, minorVersion: 1 }],
51
+ embedTypes: ["div"],
52
+ });
53
+ expect(result.success).toBe(false);
54
+ });
55
+
56
+ test("H5P PackageManifest rejects invalid machine name", () => {
57
+ const result = H5pV1.Schemas.PackageManifest.safeParse({
58
+ title: "Test",
59
+ language: "en",
60
+ machineName: "invalid machine name!",
61
+ mainLibrary: "H5P.Image",
62
+ preloadedDependencies: [{ machineName: "H5P.Image", majorVersion: 1, minorVersion: 1 }],
63
+ embedTypes: ["div"],
64
+ });
65
+ expect(result.success).toBe(false);
66
+ });
67
+
68
+ test("H5P PackageManifest rejects invalid embed type", () => {
69
+ const result = H5pV1.Schemas.PackageManifest.safeParse({
70
+ title: "Test",
71
+ language: "en",
72
+ machineName: "H5P.Image",
73
+ mainLibrary: "H5P.Image",
74
+ preloadedDependencies: [{ machineName: "H5P.Image", majorVersion: 1, minorVersion: 1 }],
75
+ embedTypes: ["flash"],
76
+ });
77
+ expect(result.success).toBe(false);
78
+ });
79
+
80
+ test("H5P PackageManifest rejects yearTo < yearFrom", () => {
81
+ const result = H5pV1.Schemas.PackageManifest.safeParse({
82
+ title: "Test",
83
+ language: "en",
84
+ machineName: "H5P.Image",
85
+ mainLibrary: "H5P.Image",
86
+ preloadedDependencies: [{ machineName: "H5P.Image", majorVersion: 1, minorVersion: 1 }],
87
+ embedTypes: ["div"],
88
+ yearFrom: 2024,
89
+ yearTo: 2020,
90
+ });
91
+ expect(result.success).toBe(false);
92
+ });
93
+
94
+ test("H5P PackageManifest rejects duplicate embed types", () => {
95
+ const result = H5pV1.Schemas.PackageManifest.safeParse({
96
+ title: "Test",
97
+ language: "en",
98
+ machineName: "H5P.Image",
99
+ mainLibrary: "H5P.Image",
100
+ preloadedDependencies: [{ machineName: "H5P.Image", majorVersion: 1, minorVersion: 1 }],
101
+ embedTypes: ["iframe", "iframe"],
102
+ });
103
+ expect(result.success).toBe(false);
104
+ });
105
+
106
+ // --- LibraryManifest (library.json) ---
107
+
108
+ test("H5P LibraryManifest accepts a minimal valid library.json", () => {
109
+ const result = H5pV1.Schemas.LibraryManifest.safeParse({
110
+ title: "Image",
111
+ machineName: "H5P.Image",
112
+ majorVersion: 1,
113
+ minorVersion: 1,
114
+ patchVersion: 33,
115
+ runnable: 0,
116
+ });
117
+ expect(result.success).toBe(true);
118
+ });
119
+
120
+ test("H5P LibraryManifest accepts a runnable library with full fields", () => {
121
+ const result = H5pV1.Schemas.LibraryManifest.safeParse({
122
+ title: "True/False Question",
123
+ machineName: "H5P.TrueFalse",
124
+ majorVersion: 1,
125
+ minorVersion: 8,
126
+ patchVersion: 24,
127
+ runnable: 1,
128
+ description: "True/False question content type",
129
+ author: "H5P Community",
130
+ license: "MIT",
131
+ preloadedJs: [{ path: "h5p-true-false.js" }],
132
+ preloadedCss: [{ path: "h5p-true-false.css" }],
133
+ preloadedDependencies: [
134
+ { machineName: "H5P.Question", majorVersion: 1, minorVersion: 5 },
135
+ { machineName: "H5P.JoubelUI", majorVersion: 1, minorVersion: 3 },
136
+ ],
137
+ embedTypes: ["iframe"],
138
+ w: 640,
139
+ h: 480,
140
+ fullscreen: 0,
141
+ contentType: "Question",
142
+ coreApi: { majorVersion: 1, minorVersion: 28 },
143
+ metadataSettings: { disable: 0, disableExtraTitleField: 0 },
144
+ });
145
+ expect(result.success).toBe(true);
146
+ });
147
+
148
+ test("H5P LibraryManifest rejects invalid machineName", () => {
149
+ const result = H5pV1.Schemas.LibraryManifest.safeParse({
150
+ title: "Bad Library",
151
+ machineName: "bad name with spaces",
152
+ majorVersion: 1,
153
+ minorVersion: 0,
154
+ patchVersion: 0,
155
+ runnable: 0,
156
+ });
157
+ expect(result.success).toBe(false);
158
+ });
159
+
160
+ // --- Shared schemas ---
161
+
162
+ test("H5P VersionRef rejects patch version (not part of the schema)", () => {
163
+ // The schema only allows machineName, majorVersion, minorVersion
164
+ const result = H5pV1.Shared.VersionRef.safeParse({
165
+ machineName: "H5P.Image",
166
+ majorVersion: 1,
167
+ minorVersion: 1,
168
+ patchVersion: 33,
169
+ });
170
+ // strictObject rejects extra fields
171
+ expect(result.success).toBe(false);
172
+ });
173
+
174
+ test("H5P LibraryFolderName validates correct folder name", () => {
175
+ expect(H5pV1.Shared.LibraryFolderName.safeParse("H5P.Image-1.1").success).toBe(true);
176
+ expect(H5pV1.Shared.LibraryFolderName.safeParse("H5P.Column-1.22").success).toBe(true);
177
+ expect(H5pV1.Shared.LibraryFolderName.safeParse("H5P.InteractiveVideo-1.28").success).toBe(true);
178
+ });
179
+
180
+ test("H5P LibraryFolderName rejects folder names with patch version", () => {
181
+ expect(H5pV1.Shared.LibraryFolderName.safeParse("H5P.Image-1.1.33").success).toBe(false);
182
+ });
183
+
184
+ // --- Semantics (semantics.json) ---
185
+
186
+ test("H5P Semantics accepts a simple text field", () => {
187
+ const result = H5pV1.Schemas.SemanticsField.safeParse({
188
+ name: "question",
189
+ type: "text",
190
+ label: "Question",
191
+ importance: "high",
192
+ maxLength: 500,
193
+ });
194
+ expect(result.success).toBe(true);
195
+ });
196
+
197
+ test("H5P Semantics accepts a boolean field", () => {
198
+ const result = H5pV1.Schemas.SemanticsField.safeParse({
199
+ name: "enableRetry",
200
+ type: "boolean",
201
+ label: "Enable Retry",
202
+ default: true,
203
+ optional: true,
204
+ });
205
+ expect(result.success).toBe(true);
206
+ });
207
+
208
+ test("H5P Semantics accepts a select field with options", () => {
209
+ const result = H5pV1.Schemas.SemanticsField.safeParse({
210
+ name: "correct",
211
+ type: "select",
212
+ label: "Correct Answer",
213
+ options: [
214
+ { value: "true", label: "True" },
215
+ { value: "false", label: "False" },
216
+ ],
217
+ });
218
+ expect(result.success).toBe(true);
219
+ });
220
+
221
+ test("H5P Semantics accepts a group with nested fields", () => {
222
+ const result = H5pV1.Schemas.SemanticsField.safeParse({
223
+ name: "behaviour",
224
+ type: "group",
225
+ label: "Behavioural settings",
226
+ expanded: true,
227
+ fields: [
228
+ { name: "enableRetry", type: "boolean", label: "Enable retry button" },
229
+ { name: "enableSolutionsButton", type: "boolean", label: "Enable solution button" },
230
+ { name: "autoCheck", type: "boolean", label: "Auto-check" },
231
+ ],
232
+ });
233
+ expect(result.success).toBe(true);
234
+ });
235
+
236
+ test("H5P Semantics accepts a list field containing a group", () => {
237
+ const result = H5pV1.Schemas.SemanticsField.safeParse({
238
+ name: "alternatives",
239
+ type: "list",
240
+ label: "Answer Alternatives",
241
+ min: 2,
242
+ entity: "alternative",
243
+ field: {
244
+ name: "alternative",
245
+ type: "group",
246
+ fields: [
247
+ { name: "text", type: "html", label: "Text", importance: "high" },
248
+ { name: "correct", type: "boolean", label: "Correct" },
249
+ ],
250
+ },
251
+ });
252
+ expect(result.success).toBe(true);
253
+ });
254
+
255
+ test("H5P Semantics accepts deeply nested groups (3 levels)", () => {
256
+ const result = H5pV1.Schemas.Semantics.safeParse([
257
+ {
258
+ name: "outer",
259
+ type: "group",
260
+ fields: [
261
+ {
262
+ name: "middle",
263
+ type: "group",
264
+ fields: [{ name: "inner", type: "text", label: "Inner text" }],
265
+ },
266
+ ],
267
+ },
268
+ ]);
269
+ expect(result.success).toBe(true);
270
+ });
271
+
272
+ test("H5P Semantics accepts library field with options", () => {
273
+ const result = H5pV1.Schemas.SemanticsField.safeParse({
274
+ name: "content",
275
+ type: "library",
276
+ label: "Content",
277
+ options: ["H5P.Image 1.1", "H5P.Video 1.6", "H5P.Audio 1.5"],
278
+ });
279
+ expect(result.success).toBe(true);
280
+ });
281
+
282
+ test("H5P Semantics accepts an image field", () => {
283
+ const result = H5pV1.Schemas.SemanticsField.safeParse({
284
+ name: "file",
285
+ type: "image",
286
+ label: "Image",
287
+ importance: "high",
288
+ allowedMimeTypes: ["image/jpeg", "image/png", "image/gif", "image/svg+xml"],
289
+ });
290
+ expect(result.success).toBe(true);
291
+ });
292
+
293
+ test("H5P Semantics rejects unknown field type", () => {
294
+ const result = H5pV1.Schemas.SemanticsField.safeParse({
295
+ name: "bad",
296
+ type: "richtext",
297
+ label: "Bad Type",
298
+ });
299
+ expect(result.success).toBe(false);
300
+ });
301
+
302
+ test("H5P Semantics rejects field without name", () => {
303
+ const result = H5pV1.Schemas.SemanticsField.safeParse({
304
+ type: "text",
305
+ label: "No name",
306
+ });
307
+ expect(result.success).toBe(false);
308
+ });
309
+
310
+ // --- MediaFile (content.json sub-structure) ---
311
+
312
+ test("H5P MediaFile accepts a valid image file reference", () => {
313
+ const result = H5pV1.Schemas.MediaFile.safeParse({
314
+ path: "images/my-photo.jpg",
315
+ mime: "image/jpeg",
316
+ copyright: { license: "CC BY", author: "John Doe", year: "2024" },
317
+ width: 1920,
318
+ height: 1080,
319
+ });
320
+ expect(result.success).toBe(true);
321
+ });
322
+
323
+ test("H5P LibraryEmbed accepts a valid embedded library reference", () => {
324
+ const result = H5pV1.Schemas.LibraryEmbed.safeParse({
325
+ library: "H5P.Image 1.1",
326
+ params: { file: { path: "images/img.jpg", mime: "image/jpeg" }, alt: "A photo" },
327
+ subContentId: "abc123",
328
+ });
329
+ expect(result.success).toBe(true);
330
+ });
package/test/xapi.test.ts CHANGED
@@ -243,18 +243,26 @@ test("xAPI Agent schema enforces exactly one Inverse Functional Identifier", ()
243
243
  // exactly one — valid
244
244
  expect(XapiV1_0_3.Schemas.Agent.safeParse({ mbox: "mailto:alice@example.com" }).success).toBe(true);
245
245
  expect(XapiV1_0_3.Schemas.Agent.safeParse({ openid: "https://example.com/alice" }).success).toBe(true);
246
- expect(XapiV1_0_3.Schemas.Agent.safeParse({ mbox_sha1sum: "da39a3ee5e6b4b0d3255bfef95601890afd80709" }).success).toBe(true);
247
- expect(XapiV1_0_3.Schemas.Agent.safeParse({ account: { homePage: "https://lms.example.com", name: "alice" } }).success).toBe(true);
246
+ expect(XapiV1_0_3.Schemas.Agent.safeParse({ mbox_sha1sum: "da39a3ee5e6b4b0d3255bfef95601890afd80709" }).success).toBe(
247
+ true,
248
+ );
249
+ expect(
250
+ XapiV1_0_3.Schemas.Agent.safeParse({ account: { homePage: "https://lms.example.com", name: "alice" } }).success,
251
+ ).toBe(true);
248
252
 
249
253
  // zero IFIs — invalid
250
254
  expect(XapiV1_0_3.Schemas.Agent.safeParse({ objectType: "Agent", name: "Alice" }).success).toBe(false);
251
255
 
252
256
  // two IFIs — invalid per spec: "An Agent MUST NOT include more than one Inverse Functional Identifier"
253
257
  expect(
254
- XapiV1_0_3.Schemas.Agent.safeParse({ mbox: "mailto:alice@example.com", openid: "https://example.com/alice" }).success,
258
+ XapiV1_0_3.Schemas.Agent.safeParse({ mbox: "mailto:alice@example.com", openid: "https://example.com/alice" })
259
+ .success,
255
260
  ).toBe(false);
256
261
  expect(
257
- XapiV2_0.Schemas.Agent.safeParse({ mbox: "mailto:alice@example.com", account: { homePage: "https://lms.example.com", name: "alice" } }).success,
262
+ XapiV2_0.Schemas.Agent.safeParse({
263
+ mbox: "mailto:alice@example.com",
264
+ account: { homePage: "https://lms.example.com", name: "alice" },
265
+ }).success,
258
266
  ).toBe(false);
259
267
  });
260
268