@conform-ed/contracts 0.0.8 → 0.0.10

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
+ });