adapt-authoring-adaptframework 1.11.0 → 1.12.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-adaptframework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"description": "Adapt framework integration for the Adapt authoring tool",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-adaptframework",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, before, after } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
+
import _ from 'lodash'
|
|
3
4
|
import fs from 'fs/promises'
|
|
4
5
|
import path from 'path'
|
|
5
6
|
import { fileURLToPath } from 'url'
|
|
@@ -7,6 +8,38 @@ import AdaptFrameworkBuild from '../lib/AdaptFrameworkBuild.js'
|
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* transformContentItems is async and calls App.instance.waitForModule('tags')
|
|
13
|
+
* at the end. We extract the synchronous logic here to test it in isolation.
|
|
14
|
+
*/
|
|
15
|
+
function transformContentItemsSync (build, items) {
|
|
16
|
+
items.forEach(i => {
|
|
17
|
+
['_courseId', '_parentId'].forEach(k => {
|
|
18
|
+
i[k] = build.idMap[i[k]] || i[k]
|
|
19
|
+
})
|
|
20
|
+
if (i._friendlyId) {
|
|
21
|
+
i._id = i._friendlyId
|
|
22
|
+
}
|
|
23
|
+
const idMapEntries = Object.entries(build.assetData.idMap)
|
|
24
|
+
const itemString = idMapEntries.reduce((s, [_id, assetPath]) => {
|
|
25
|
+
const relPath = assetPath.replace(build.courseDir, 'course')
|
|
26
|
+
return s.replace(new RegExp(_id, 'g'), relPath)
|
|
27
|
+
}, JSON.stringify(i))
|
|
28
|
+
Object.assign(i, JSON.parse(itemString))
|
|
29
|
+
if (i._component) {
|
|
30
|
+
i._component = build.enabledPlugins.find(p => p.name === i._component)?.targetAttribute.slice(1) ?? i._component
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
build.enabledPlugins.forEach(({ targetAttribute, type }) => {
|
|
34
|
+
let key = `_${type}`
|
|
35
|
+
if (type === 'component' || type === 'extension') key += 's'
|
|
36
|
+
const globals = build.courseData.course.data._globals
|
|
37
|
+
if (!globals?.[targetAttribute]) return
|
|
38
|
+
_.merge(globals, { [key]: { [targetAttribute]: globals[targetAttribute] } })
|
|
39
|
+
delete globals[targetAttribute]
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
10
43
|
describe('AdaptFrameworkBuild', () => {
|
|
11
44
|
describe('constructor', () => {
|
|
12
45
|
it('should set action and related boolean flags for preview', () => {
|
|
@@ -198,5 +231,220 @@ describe('AdaptFrameworkBuild', () => {
|
|
|
198
231
|
assert.equal(build.courseData.contentObject.data.length, 1)
|
|
199
232
|
assert.equal(build.courseData.contentObject.data[0]._id, 'menu1')
|
|
200
233
|
})
|
|
234
|
+
|
|
235
|
+
it('should sort deeply nested content in global order', () => {
|
|
236
|
+
const build = new AdaptFrameworkBuild({ action: 'preview', courseId: 'c1', userId: 'u1' })
|
|
237
|
+
build.courseData = {
|
|
238
|
+
course: { dir: '/tmp', fileName: 'course.json', data: undefined },
|
|
239
|
+
config: { dir: '/tmp', fileName: 'config.json', data: undefined },
|
|
240
|
+
contentObject: { dir: '/tmp', fileName: 'contentObjects.json', data: [] },
|
|
241
|
+
article: { dir: '/tmp', fileName: 'articles.json', data: [] },
|
|
242
|
+
block: { dir: '/tmp', fileName: 'blocks.json', data: [] },
|
|
243
|
+
component: { dir: '/tmp', fileName: 'components.json', data: [] }
|
|
244
|
+
}
|
|
245
|
+
const items = [
|
|
246
|
+
{ _id: 'course1', _type: 'course' },
|
|
247
|
+
{ _id: 'page1', _type: 'page', _parentId: 'course1', _sortOrder: 1 },
|
|
248
|
+
{ _id: 'page2', _type: 'page', _parentId: 'course1', _sortOrder: 2 },
|
|
249
|
+
{ _id: 'art1', _type: 'article', _parentId: 'page1', _sortOrder: 1 },
|
|
250
|
+
{ _id: 'art2', _type: 'article', _parentId: 'page2', _sortOrder: 1 },
|
|
251
|
+
{ _id: 'art3', _type: 'article', _parentId: 'page1', _sortOrder: 2 }
|
|
252
|
+
]
|
|
253
|
+
build.sortContentItems(items)
|
|
254
|
+
|
|
255
|
+
const articleIds = build.courseData.article.data.map(a => a._id)
|
|
256
|
+
assert.equal(articleIds[0], 'art1')
|
|
257
|
+
assert.equal(articleIds[1], 'art3')
|
|
258
|
+
assert.equal(articleIds[2], 'art2')
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('#transformContentItems()', () => {
|
|
263
|
+
it('should replace _courseId and _parentId with friendlyIds from idMap', () => {
|
|
264
|
+
const build = new AdaptFrameworkBuild({ action: 'preview', courseId: 'c1', userId: 'u1' })
|
|
265
|
+
build.idMap = { course1: 'co-friendly', page1: 'page-friendly' }
|
|
266
|
+
build.assetData = { idMap: {} }
|
|
267
|
+
build.enabledPlugins = []
|
|
268
|
+
build.courseData = { course: { dir: '/tmp', data: {} } }
|
|
269
|
+
|
|
270
|
+
const items = [
|
|
271
|
+
{ _courseId: 'course1', _parentId: 'page1' }
|
|
272
|
+
]
|
|
273
|
+
transformContentItemsSync(build, items)
|
|
274
|
+
assert.equal(items[0]._courseId, 'co-friendly')
|
|
275
|
+
assert.equal(items[0]._parentId, 'page-friendly')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should replace _id with _friendlyId when present', () => {
|
|
279
|
+
const build = new AdaptFrameworkBuild({ action: 'preview', courseId: 'c1', userId: 'u1' })
|
|
280
|
+
build.idMap = {}
|
|
281
|
+
build.assetData = { idMap: {} }
|
|
282
|
+
build.enabledPlugins = []
|
|
283
|
+
build.courseData = { course: { dir: '/tmp', data: {} } }
|
|
284
|
+
|
|
285
|
+
const items = [{ _id: 'abc', _friendlyId: 'my-friendly' }]
|
|
286
|
+
transformContentItemsSync(build, items)
|
|
287
|
+
assert.equal(items[0]._id, 'my-friendly')
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should not replace _id when _friendlyId is absent', () => {
|
|
291
|
+
const build = new AdaptFrameworkBuild({ action: 'preview', courseId: 'c1', userId: 'u1' })
|
|
292
|
+
build.idMap = {}
|
|
293
|
+
build.assetData = { idMap: {} }
|
|
294
|
+
build.enabledPlugins = []
|
|
295
|
+
build.courseData = { course: { dir: '/tmp', data: {} } }
|
|
296
|
+
|
|
297
|
+
const items = [{ _id: 'abc' }]
|
|
298
|
+
transformContentItemsSync(build, items)
|
|
299
|
+
assert.equal(items[0]._id, 'abc')
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('should replace asset _ids with relative paths', () => {
|
|
303
|
+
const build = new AdaptFrameworkBuild({ action: 'preview', courseId: 'c1', userId: 'u1' })
|
|
304
|
+
build.idMap = {}
|
|
305
|
+
build.courseDir = '/build/course'
|
|
306
|
+
build.assetData = { idMap: { asset123: '/build/course/assets/image.png' } }
|
|
307
|
+
build.enabledPlugins = []
|
|
308
|
+
build.courseData = { course: { dir: '/tmp', data: {} } }
|
|
309
|
+
|
|
310
|
+
const items = [{ graphic: 'asset123' }]
|
|
311
|
+
transformContentItemsSync(build, items)
|
|
312
|
+
assert.equal(items[0].graphic, 'course/assets/image.png')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('should resolve _component to targetAttribute', () => {
|
|
316
|
+
const build = new AdaptFrameworkBuild({ action: 'preview', courseId: 'c1', userId: 'u1' })
|
|
317
|
+
build.idMap = {}
|
|
318
|
+
build.assetData = { idMap: {} }
|
|
319
|
+
build.enabledPlugins = [
|
|
320
|
+
{ name: 'adapt-contrib-text', targetAttribute: '_text', type: 'component' }
|
|
321
|
+
]
|
|
322
|
+
build.courseData = { course: { dir: '/tmp', data: {} } }
|
|
323
|
+
|
|
324
|
+
const items = [{ _component: 'adapt-contrib-text' }]
|
|
325
|
+
transformContentItemsSync(build, items)
|
|
326
|
+
assert.equal(items[0]._component, 'text')
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('should keep _component as-is when plugin not found', () => {
|
|
330
|
+
const build = new AdaptFrameworkBuild({ action: 'preview', courseId: 'c1', userId: 'u1' })
|
|
331
|
+
build.idMap = {}
|
|
332
|
+
build.assetData = { idMap: {} }
|
|
333
|
+
build.enabledPlugins = []
|
|
334
|
+
build.courseData = { course: { dir: '/tmp', data: {} } }
|
|
335
|
+
|
|
336
|
+
const items = [{ _component: 'unknown-plugin' }]
|
|
337
|
+
transformContentItemsSync(build, items)
|
|
338
|
+
assert.equal(items[0]._component, 'unknown-plugin')
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('should move globals into nested _extensions object', () => {
|
|
342
|
+
const build = new AdaptFrameworkBuild({ action: 'preview', courseId: 'c1', userId: 'u1' })
|
|
343
|
+
build.idMap = {}
|
|
344
|
+
build.assetData = { idMap: {} }
|
|
345
|
+
build.enabledPlugins = [
|
|
346
|
+
{ name: 'adapt-contrib-trickle', targetAttribute: '_trickle', type: 'extension' }
|
|
347
|
+
]
|
|
348
|
+
build.courseData = {
|
|
349
|
+
course: {
|
|
350
|
+
dir: '/tmp',
|
|
351
|
+
data: {
|
|
352
|
+
_globals: {
|
|
353
|
+
_trickle: { label: 'Trickle' }
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
transformContentItemsSync(build, [])
|
|
360
|
+
const globals = build.courseData.course.data._globals
|
|
361
|
+
assert.equal(globals._extensions._trickle.label, 'Trickle')
|
|
362
|
+
assert.equal(globals._trickle, undefined)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('should use _components key for component type globals', () => {
|
|
366
|
+
const build = new AdaptFrameworkBuild({ action: 'preview', courseId: 'c1', userId: 'u1' })
|
|
367
|
+
build.idMap = {}
|
|
368
|
+
build.assetData = { idMap: {} }
|
|
369
|
+
build.enabledPlugins = [
|
|
370
|
+
{ name: 'adapt-contrib-text', targetAttribute: '_text', type: 'component' }
|
|
371
|
+
]
|
|
372
|
+
build.courseData = {
|
|
373
|
+
course: {
|
|
374
|
+
dir: '/tmp',
|
|
375
|
+
data: {
|
|
376
|
+
_globals: {
|
|
377
|
+
_text: { ariaRegion: 'Text' }
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
transformContentItemsSync(build, [])
|
|
384
|
+
const globals = build.courseData.course.data._globals
|
|
385
|
+
assert.equal(globals._components._text.ariaRegion, 'Text')
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('should keep _courseId as-is when not in idMap', () => {
|
|
389
|
+
const build = new AdaptFrameworkBuild({ action: 'preview', courseId: 'c1', userId: 'u1' })
|
|
390
|
+
build.idMap = {}
|
|
391
|
+
build.assetData = { idMap: {} }
|
|
392
|
+
build.enabledPlugins = []
|
|
393
|
+
build.courseData = { course: { dir: '/tmp', data: {} } }
|
|
394
|
+
|
|
395
|
+
const items = [{ _courseId: 'unmapped' }]
|
|
396
|
+
transformContentItemsSync(build, items)
|
|
397
|
+
assert.equal(items[0]._courseId, 'unmapped')
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
describe('#writeContentJson() asset mapping', () => {
|
|
402
|
+
it('should map asset data to export format for export builds', () => {
|
|
403
|
+
const build = new AdaptFrameworkBuild({ action: 'export', courseId: 'c1', userId: 'u1' })
|
|
404
|
+
build.assetData = {
|
|
405
|
+
data: [
|
|
406
|
+
{ title: 'Logo', description: 'A logo', path: 'assets/logo.png', tags: ['branding'], _id: '123', url: '' },
|
|
407
|
+
{ title: 'Icon', description: 'An icon', path: 'assets/icon.svg', tags: [], _id: '456', url: '' }
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
/* simulate the mapping logic from writeContentJson */
|
|
411
|
+
const mapped = build.assetData.data.map(d => ({
|
|
412
|
+
title: d.title,
|
|
413
|
+
description: d.description,
|
|
414
|
+
filename: d.path,
|
|
415
|
+
tags: d.tags
|
|
416
|
+
}))
|
|
417
|
+
assert.equal(mapped.length, 2)
|
|
418
|
+
assert.equal(mapped[0].filename, 'assets/logo.png')
|
|
419
|
+
assert.equal(mapped[0].title, 'Logo')
|
|
420
|
+
assert.equal(mapped[0]._id, undefined)
|
|
421
|
+
assert.deepEqual(mapped[1].tags, [])
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('should not include assets for export when assetData is empty', () => {
|
|
425
|
+
const build = new AdaptFrameworkBuild({ action: 'export', courseId: 'c1', userId: 'u1' })
|
|
426
|
+
build.assetData = { data: [] }
|
|
427
|
+
build.courseData = {
|
|
428
|
+
course: { dir: '/tmp', fileName: 'course.json', data: {} }
|
|
429
|
+
}
|
|
430
|
+
const data = Object.values(build.courseData)
|
|
431
|
+
if (build.isExport && build.assetData.data.length) {
|
|
432
|
+
data.push(build.assetData)
|
|
433
|
+
}
|
|
434
|
+
assert.equal(data.length, 1)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('should include asset data entry for non-empty export', () => {
|
|
438
|
+
const build = new AdaptFrameworkBuild({ action: 'export', courseId: 'c1', userId: 'u1' })
|
|
439
|
+
build.assetData = { data: [{ title: 'img', path: 'a.png' }] }
|
|
440
|
+
build.courseData = {
|
|
441
|
+
course: { dir: '/tmp', fileName: 'course.json', data: {} }
|
|
442
|
+
}
|
|
443
|
+
const data = Object.values(build.courseData)
|
|
444
|
+
if (build.isExport && build.assetData.data.length) {
|
|
445
|
+
data.push(build.assetData)
|
|
446
|
+
}
|
|
447
|
+
assert.equal(data.length, 2)
|
|
448
|
+
})
|
|
201
449
|
})
|
|
202
450
|
})
|
|
@@ -26,4 +26,302 @@ describe('AdaptFrameworkImport', () => {
|
|
|
26
26
|
assert.equal(AdaptFrameworkImport.typeToSchema({ _type: 'block' }), 'block')
|
|
27
27
|
})
|
|
28
28
|
})
|
|
29
|
+
|
|
30
|
+
describe('#getSortedData()', () => {
|
|
31
|
+
const getSortedData = AdaptFrameworkImport.prototype.getSortedData
|
|
32
|
+
|
|
33
|
+
it('should sort a simple course hierarchy into levels', () => {
|
|
34
|
+
const ctx = {
|
|
35
|
+
contentJson: {
|
|
36
|
+
course: { _id: 'course1' },
|
|
37
|
+
contentObjects: {
|
|
38
|
+
page1: { _id: 'page1', _parentId: 'course1' },
|
|
39
|
+
art1: { _id: 'art1', _parentId: 'page1' },
|
|
40
|
+
block1: { _id: 'block1', _parentId: 'art1' }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const { sorted, hierarchy } = getSortedData.call(ctx)
|
|
45
|
+
assert.equal(sorted.length, 3)
|
|
46
|
+
assert.deepEqual(sorted[0], ['page1'])
|
|
47
|
+
assert.deepEqual(sorted[1], ['art1'])
|
|
48
|
+
assert.deepEqual(sorted[2], ['block1'])
|
|
49
|
+
assert.ok(hierarchy)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should group siblings at the same level', () => {
|
|
53
|
+
const ctx = {
|
|
54
|
+
contentJson: {
|
|
55
|
+
course: { _id: 'course1' },
|
|
56
|
+
contentObjects: {
|
|
57
|
+
page1: { _id: 'page1', _parentId: 'course1' },
|
|
58
|
+
page2: { _id: 'page2', _parentId: 'course1' },
|
|
59
|
+
art1: { _id: 'art1', _parentId: 'page1' },
|
|
60
|
+
art2: { _id: 'art2', _parentId: 'page2' }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const { sorted } = getSortedData.call(ctx)
|
|
65
|
+
assert.equal(sorted.length, 2)
|
|
66
|
+
assert.equal(sorted[0].length, 2)
|
|
67
|
+
assert.ok(sorted[0].includes('page1'))
|
|
68
|
+
assert.ok(sorted[0].includes('page2'))
|
|
69
|
+
assert.equal(sorted[1].length, 2)
|
|
70
|
+
assert.ok(sorted[1].includes('art1'))
|
|
71
|
+
assert.ok(sorted[1].includes('art2'))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should not include course in the sorted output', () => {
|
|
75
|
+
const ctx = {
|
|
76
|
+
contentJson: {
|
|
77
|
+
course: { _id: 'course1' },
|
|
78
|
+
contentObjects: {
|
|
79
|
+
page1: { _id: 'page1', _parentId: 'course1' }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const { sorted } = getSortedData.call(ctx)
|
|
84
|
+
const allIds = sorted.flat()
|
|
85
|
+
assert.ok(!allIds.includes('course1'))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should build correct hierarchy map', () => {
|
|
89
|
+
const ctx = {
|
|
90
|
+
contentJson: {
|
|
91
|
+
course: { _id: 'course1' },
|
|
92
|
+
contentObjects: {
|
|
93
|
+
page1: { _id: 'page1', _parentId: 'course1' },
|
|
94
|
+
page2: { _id: 'page2', _parentId: 'course1' },
|
|
95
|
+
art1: { _id: 'art1', _parentId: 'page1' }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const { hierarchy } = getSortedData.call(ctx)
|
|
100
|
+
assert.deepEqual(hierarchy.course1, ['page1', 'page2'])
|
|
101
|
+
assert.deepEqual(hierarchy.page1, ['art1'])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should handle deep nesting (5 levels)', () => {
|
|
105
|
+
const ctx = {
|
|
106
|
+
contentJson: {
|
|
107
|
+
course: { _id: 'c' },
|
|
108
|
+
contentObjects: {
|
|
109
|
+
p: { _id: 'p', _parentId: 'c' },
|
|
110
|
+
a: { _id: 'a', _parentId: 'p' },
|
|
111
|
+
b: { _id: 'b', _parentId: 'a' },
|
|
112
|
+
x: { _id: 'x', _parentId: 'b' },
|
|
113
|
+
y: { _id: 'y', _parentId: 'x' }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const { sorted } = getSortedData.call(ctx)
|
|
118
|
+
assert.equal(sorted.length, 5)
|
|
119
|
+
assert.deepEqual(sorted[0], ['p'])
|
|
120
|
+
assert.deepEqual(sorted[1], ['a'])
|
|
121
|
+
assert.deepEqual(sorted[2], ['b'])
|
|
122
|
+
assert.deepEqual(sorted[3], ['x'])
|
|
123
|
+
assert.deepEqual(sorted[4], ['y'])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should handle a single page', () => {
|
|
127
|
+
const ctx = {
|
|
128
|
+
contentJson: {
|
|
129
|
+
course: { _id: 'c' },
|
|
130
|
+
contentObjects: {
|
|
131
|
+
p: { _id: 'p', _parentId: 'c' }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const { sorted } = getSortedData.call(ctx)
|
|
136
|
+
assert.equal(sorted.length, 1)
|
|
137
|
+
assert.deepEqual(sorted[0], ['p'])
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('#extractAssets()', () => {
|
|
142
|
+
function makeCtx (assetMap) {
|
|
143
|
+
const ctx = { assetMap }
|
|
144
|
+
ctx.extractAssets = AdaptFrameworkImport.prototype.extractAssets.bind(ctx)
|
|
145
|
+
return ctx
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
it('should replace asset paths with mapped IDs', () => {
|
|
149
|
+
const ctx = makeCtx({ 'course/en/assets/logo.png': 'asset123' })
|
|
150
|
+
const schema = {
|
|
151
|
+
_graphic: {
|
|
152
|
+
properties: {
|
|
153
|
+
src: { _backboneForms: 'Asset' }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const data = { _graphic: { src: 'course/en/assets/logo.png' } }
|
|
158
|
+
ctx.extractAssets(schema, data)
|
|
159
|
+
assert.equal(data._graphic.src, 'asset123')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should delete empty string asset values', () => {
|
|
163
|
+
const ctx = makeCtx({})
|
|
164
|
+
const schema = {
|
|
165
|
+
_graphic: {
|
|
166
|
+
properties: {
|
|
167
|
+
src: { _backboneForms: 'Asset' }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const data = { _graphic: { src: '' } }
|
|
172
|
+
ctx.extractAssets(schema, data)
|
|
173
|
+
assert.equal('src' in data._graphic, false)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should keep value when not in assetMap', () => {
|
|
177
|
+
const ctx = makeCtx({})
|
|
178
|
+
const schema = {
|
|
179
|
+
img: { _backboneForms: { type: 'Asset' } }
|
|
180
|
+
}
|
|
181
|
+
const data = { img: 'unknown/path.png' }
|
|
182
|
+
ctx.extractAssets(schema, data)
|
|
183
|
+
assert.equal(data.img, 'unknown/path.png')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should recurse into nested properties', () => {
|
|
187
|
+
const ctx = makeCtx({ 'assets/bg.jpg': 'asset456' })
|
|
188
|
+
const schema = {
|
|
189
|
+
_settings: {
|
|
190
|
+
properties: {
|
|
191
|
+
_background: {
|
|
192
|
+
properties: {
|
|
193
|
+
src: { _backboneForms: 'Asset' }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const data = { _settings: { _background: { src: 'assets/bg.jpg' } } }
|
|
200
|
+
ctx.extractAssets(schema, data)
|
|
201
|
+
assert.equal(data._settings._background.src, 'asset456')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should recurse into array items', () => {
|
|
205
|
+
const ctx = makeCtx({ 'assets/a.png': 'id1', 'assets/b.png': 'id2' })
|
|
206
|
+
const schema = {
|
|
207
|
+
_items: {
|
|
208
|
+
items: {
|
|
209
|
+
properties: {
|
|
210
|
+
src: { _backboneForms: 'Asset' }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const data = {
|
|
216
|
+
_items: [
|
|
217
|
+
{ src: 'assets/a.png' },
|
|
218
|
+
{ src: 'assets/b.png' }
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
ctx.extractAssets(schema, data)
|
|
222
|
+
assert.equal(data._items[0].src, 'id1')
|
|
223
|
+
assert.equal(data._items[1].src, 'id2')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should skip undefined data keys', () => {
|
|
227
|
+
const ctx = makeCtx({})
|
|
228
|
+
const schema = {
|
|
229
|
+
_graphic: { _backboneForms: 'Asset' }
|
|
230
|
+
}
|
|
231
|
+
const data = {}
|
|
232
|
+
ctx.extractAssets(schema, data)
|
|
233
|
+
assert.equal('_graphic' in data, false)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should handle null schema gracefully', () => {
|
|
237
|
+
const ctx = makeCtx({})
|
|
238
|
+
ctx.extractAssets(null, { a: 1 })
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should handle _backboneForms as object with type', () => {
|
|
242
|
+
const ctx = makeCtx({ 'path/img.png': 'mapped' })
|
|
243
|
+
const schema = {
|
|
244
|
+
hero: { _backboneForms: { type: 'Asset' } }
|
|
245
|
+
}
|
|
246
|
+
const data = { hero: 'path/img.png' }
|
|
247
|
+
ctx.extractAssets(schema, data)
|
|
248
|
+
assert.equal(data.hero, 'mapped')
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('#loadContentFile()', () => {
|
|
253
|
+
/**
|
|
254
|
+
* loadContentFile depends on FWUtils.readJson (file I/O) and App.instance,
|
|
255
|
+
* but we can test the classification logic by providing a mock.
|
|
256
|
+
* We extract and test the classification behaviour inline.
|
|
257
|
+
*/
|
|
258
|
+
it('should classify course type into contentJson.course', () => {
|
|
259
|
+
const classify = (contents, filePath, ctx) => {
|
|
260
|
+
if (contents._type === 'course') {
|
|
261
|
+
ctx.contentJson.course = contents
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
if (filePath.endsWith('config.json')) {
|
|
265
|
+
ctx.contentJson.config = { _id: 'config', _type: 'config', ...contents }
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
if (Array.isArray(contents)) {
|
|
269
|
+
contents.forEach(c => {
|
|
270
|
+
ctx.contentJson.contentObjects[c._id] = c
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const ctx = { contentJson: { course: {}, config: {}, contentObjects: {} } }
|
|
275
|
+
classify({ _type: 'course', title: 'My Course' }, 'en/course.json', ctx)
|
|
276
|
+
assert.equal(ctx.contentJson.course.title, 'My Course')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('should classify config.json as config with defaults', () => {
|
|
280
|
+
const classify = (contents, filePath, ctx) => {
|
|
281
|
+
if (contents._type === 'course') {
|
|
282
|
+
ctx.contentJson.course = contents
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
if (filePath.endsWith('config.json')) {
|
|
286
|
+
ctx.contentJson.config = { _id: 'config', _type: 'config', ...contents }
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
if (Array.isArray(contents)) {
|
|
290
|
+
contents.forEach(c => {
|
|
291
|
+
ctx.contentJson.contentObjects[c._id] = c
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const ctx = { contentJson: { course: {}, config: {}, contentObjects: {} } }
|
|
296
|
+
classify({ _defaultLanguage: 'en' }, 'course/config.json', ctx)
|
|
297
|
+
assert.equal(ctx.contentJson.config._id, 'config')
|
|
298
|
+
assert.equal(ctx.contentJson.config._type, 'config')
|
|
299
|
+
assert.equal(ctx.contentJson.config._defaultLanguage, 'en')
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('should store array contents into contentObjects by _id', () => {
|
|
303
|
+
const classify = (contents, filePath, ctx) => {
|
|
304
|
+
if (contents._type === 'course') {
|
|
305
|
+
ctx.contentJson.course = contents
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
if (filePath.endsWith('config.json')) {
|
|
309
|
+
ctx.contentJson.config = { _id: 'config', _type: 'config', ...contents }
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
if (Array.isArray(contents)) {
|
|
313
|
+
contents.forEach(c => {
|
|
314
|
+
ctx.contentJson.contentObjects[c._id] = c
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const ctx = { contentJson: { course: {}, config: {}, contentObjects: {} } }
|
|
319
|
+
classify([
|
|
320
|
+
{ _id: 'page1', _type: 'page' },
|
|
321
|
+
{ _id: 'art1', _type: 'article' }
|
|
322
|
+
], 'en/contentObjects.json', ctx)
|
|
323
|
+
assert.equal(ctx.contentJson.contentObjects.page1._type, 'page')
|
|
324
|
+
assert.equal(ctx.contentJson.contentObjects.art1._type, 'article')
|
|
325
|
+
})
|
|
326
|
+
})
|
|
29
327
|
})
|
|
@@ -20,11 +20,8 @@ describe('AdaptFrameworkUtils', () => {
|
|
|
20
20
|
})
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// causing slice(1, -1) to strip the last character
|
|
26
|
-
it('should truncate action for URLs without trailing slash/path (known bug)', () => {
|
|
27
|
-
assert.equal(AdaptFrameworkUtils.inferBuildAction({ url: '/import' }), 'impor')
|
|
23
|
+
it('should return full action for URLs without trailing slash', () => {
|
|
24
|
+
assert.equal(AdaptFrameworkUtils.inferBuildAction({ url: '/import' }), 'import')
|
|
28
25
|
})
|
|
29
26
|
})
|
|
30
27
|
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { describe, it, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import ConfigTransform from '../lib/migrations/config.js'
|
|
5
|
+
import GraphicSrcTransform from '../lib/migrations/graphic-src.js'
|
|
6
|
+
import NavOrderTransform from '../lib/migrations/nav-order.js'
|
|
7
|
+
import ParentIdTransform from '../lib/migrations/parent-id.js'
|
|
8
|
+
import RemoveUndef from '../lib/migrations/remove-undef.js'
|
|
9
|
+
import StartPage from '../lib/migrations/start-page.js'
|
|
10
|
+
import ThemeUndef from '../lib/migrations/theme-undef.js'
|
|
11
|
+
|
|
12
|
+
describe('Migrations', () => {
|
|
13
|
+
describe('ConfigTransform', () => {
|
|
14
|
+
it('should convert numeric ARIA levels to strings', async () => {
|
|
15
|
+
const data = {
|
|
16
|
+
_type: 'config',
|
|
17
|
+
_accessibility: {
|
|
18
|
+
_ariaLevels: {
|
|
19
|
+
_menu: 1,
|
|
20
|
+
_menuItem: 2,
|
|
21
|
+
_page: 3,
|
|
22
|
+
_article: 4,
|
|
23
|
+
_block: 5,
|
|
24
|
+
_component: 6,
|
|
25
|
+
_componentItem: 7,
|
|
26
|
+
_notify: 8
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
await ConfigTransform(data)
|
|
31
|
+
assert.equal(data._accessibility._ariaLevels._menu, '1')
|
|
32
|
+
assert.equal(data._accessibility._ariaLevels._menuItem, '2')
|
|
33
|
+
assert.equal(data._accessibility._ariaLevels._page, '3')
|
|
34
|
+
assert.equal(data._accessibility._ariaLevels._article, '4')
|
|
35
|
+
assert.equal(data._accessibility._ariaLevels._block, '5')
|
|
36
|
+
assert.equal(data._accessibility._ariaLevels._component, '6')
|
|
37
|
+
assert.equal(data._accessibility._ariaLevels._componentItem, '7')
|
|
38
|
+
assert.equal(data._accessibility._ariaLevels._notify, '8')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should not modify already-string values', async () => {
|
|
42
|
+
const data = {
|
|
43
|
+
_type: 'config',
|
|
44
|
+
_accessibility: { _ariaLevels: { _menu: '1' } }
|
|
45
|
+
}
|
|
46
|
+
await ConfigTransform(data)
|
|
47
|
+
assert.equal(data._accessibility._ariaLevels._menu, '1')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should skip non-config types', async () => {
|
|
51
|
+
const data = {
|
|
52
|
+
_type: 'course',
|
|
53
|
+
_accessibility: { _ariaLevels: { _menu: 1 } }
|
|
54
|
+
}
|
|
55
|
+
await ConfigTransform(data)
|
|
56
|
+
assert.equal(data._accessibility._ariaLevels._menu, 1)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should skip config without _ariaLevels', async () => {
|
|
60
|
+
const data = { _type: 'config', _accessibility: {} }
|
|
61
|
+
await ConfigTransform(data)
|
|
62
|
+
assert.deepEqual(data._accessibility, {})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should not convert falsy values (0)', async () => {
|
|
66
|
+
const data = {
|
|
67
|
+
_type: 'config',
|
|
68
|
+
_accessibility: { _ariaLevels: { _menu: 0 } }
|
|
69
|
+
}
|
|
70
|
+
await ConfigTransform(data)
|
|
71
|
+
assert.equal(data._accessibility._ariaLevels._menu, 0)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('GraphicSrcTransform', () => {
|
|
76
|
+
it('should copy src to large and small for graphic component', async () => {
|
|
77
|
+
const data = {
|
|
78
|
+
_component: 'adapt-contrib-graphic',
|
|
79
|
+
_graphic: { src: 'image.png' }
|
|
80
|
+
}
|
|
81
|
+
await GraphicSrcTransform(data)
|
|
82
|
+
assert.equal(data._graphic.large, 'image.png')
|
|
83
|
+
assert.equal(data._graphic.small, 'image.png')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should copy src to large and small for hotgraphic component', async () => {
|
|
87
|
+
const data = {
|
|
88
|
+
_component: 'adapt-contrib-hotgraphic',
|
|
89
|
+
_graphic: { src: 'hot.png' },
|
|
90
|
+
_items: [
|
|
91
|
+
{ _graphic: { src: 'item1.png' } },
|
|
92
|
+
{ _graphic: { src: 'item2.png' } }
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
await GraphicSrcTransform(data)
|
|
96
|
+
assert.equal(data._graphic.large, 'hot.png')
|
|
97
|
+
assert.equal(data._graphic.small, 'hot.png')
|
|
98
|
+
assert.equal(data._items[0]._graphic.large, 'item1.png')
|
|
99
|
+
assert.equal(data._items[0]._graphic.small, 'item1.png')
|
|
100
|
+
assert.equal(data._items[1]._graphic.large, 'item2.png')
|
|
101
|
+
assert.equal(data._items[1]._graphic.small, 'item2.png')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should skip non-graphic components', async () => {
|
|
105
|
+
const data = {
|
|
106
|
+
_component: 'adapt-contrib-text',
|
|
107
|
+
_graphic: { src: 'image.png' }
|
|
108
|
+
}
|
|
109
|
+
await GraphicSrcTransform(data)
|
|
110
|
+
assert.equal(data._graphic.large, undefined)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should not overwrite if src is absent', async () => {
|
|
114
|
+
const data = {
|
|
115
|
+
_component: 'adapt-contrib-graphic',
|
|
116
|
+
_graphic: { large: 'existing.png' }
|
|
117
|
+
}
|
|
118
|
+
await GraphicSrcTransform(data)
|
|
119
|
+
assert.equal(data._graphic.large, 'existing.png')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should handle graphic without _items', async () => {
|
|
123
|
+
const data = {
|
|
124
|
+
_component: 'adapt-contrib-graphic',
|
|
125
|
+
_graphic: { src: 'only.png' }
|
|
126
|
+
}
|
|
127
|
+
await GraphicSrcTransform(data)
|
|
128
|
+
assert.equal(data._graphic.large, 'only.png')
|
|
129
|
+
assert.equal(data._graphic.small, 'only.png')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('NavOrderTransform', () => {
|
|
134
|
+
it('should convert string _navOrder to number', async () => {
|
|
135
|
+
const data = {
|
|
136
|
+
_type: 'course',
|
|
137
|
+
_globals: {
|
|
138
|
+
_extensions: {
|
|
139
|
+
_trickle: { _navOrder: '100' },
|
|
140
|
+
_resources: { _navOrder: '200' }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
await NavOrderTransform(data)
|
|
145
|
+
assert.equal(data._globals._extensions._trickle._navOrder, 100)
|
|
146
|
+
assert.equal(data._globals._extensions._resources._navOrder, 200)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should keep numeric _navOrder as-is', async () => {
|
|
150
|
+
const data = {
|
|
151
|
+
_type: 'course',
|
|
152
|
+
_globals: { _extensions: { _trickle: { _navOrder: 50 } } }
|
|
153
|
+
}
|
|
154
|
+
await NavOrderTransform(data)
|
|
155
|
+
assert.equal(data._globals._extensions._trickle._navOrder, 50)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should skip non-course types', async () => {
|
|
159
|
+
const data = {
|
|
160
|
+
_type: 'config',
|
|
161
|
+
_globals: { _extensions: { _trickle: { _navOrder: '100' } } }
|
|
162
|
+
}
|
|
163
|
+
await NavOrderTransform(data)
|
|
164
|
+
assert.equal(data._globals._extensions._trickle._navOrder, '100')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should skip extensions without _navOrder', async () => {
|
|
168
|
+
const data = {
|
|
169
|
+
_type: 'course',
|
|
170
|
+
_globals: { _extensions: { _trickle: { other: 'value' } } }
|
|
171
|
+
}
|
|
172
|
+
await NavOrderTransform(data)
|
|
173
|
+
assert.equal(data._globals._extensions._trickle._navOrder, undefined)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('ParentIdTransform', () => {
|
|
178
|
+
it('should remap _parentId using idMap', async () => {
|
|
179
|
+
const data = { _parentId: 'old-id' }
|
|
180
|
+
const importer = { idMap: { 'old-id': 'new-id' } }
|
|
181
|
+
await ParentIdTransform(data, importer)
|
|
182
|
+
assert.equal(data._parentId, 'new-id')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should set _parentId to undefined when not in idMap', async () => {
|
|
186
|
+
const data = { _parentId: 'unknown' }
|
|
187
|
+
const importer = { idMap: {} }
|
|
188
|
+
await ParentIdTransform(data, importer)
|
|
189
|
+
assert.equal(data._parentId, undefined)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should skip when _parentId is falsy', async () => {
|
|
193
|
+
const data = { _parentId: undefined }
|
|
194
|
+
const importer = { idMap: { undefined: 'should-not-set' } }
|
|
195
|
+
await ParentIdTransform(data, importer)
|
|
196
|
+
assert.equal(data._parentId, undefined)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should skip when _parentId is not present', async () => {
|
|
200
|
+
const data = { _type: 'course' }
|
|
201
|
+
const importer = { idMap: {} }
|
|
202
|
+
await ParentIdTransform(data, importer)
|
|
203
|
+
assert.equal(data._parentId, undefined)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('RemoveUndef', () => {
|
|
208
|
+
it('should remove null properties', async () => {
|
|
209
|
+
const data = { a: 1, b: null, c: 'test' }
|
|
210
|
+
await RemoveUndef(data)
|
|
211
|
+
assert.equal(data.a, 1)
|
|
212
|
+
assert.equal(data.c, 'test')
|
|
213
|
+
assert.equal('b' in data, false)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should recursively remove nulls in nested objects', async () => {
|
|
217
|
+
const data = { a: { b: null, c: { d: null, e: 1 } } }
|
|
218
|
+
await RemoveUndef(data)
|
|
219
|
+
assert.equal('b' in data.a, false)
|
|
220
|
+
assert.equal('d' in data.a.c, false)
|
|
221
|
+
assert.equal(data.a.c.e, 1)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('should not recurse into arrays', async () => {
|
|
225
|
+
const data = { arr: [null, 1, null] }
|
|
226
|
+
await RemoveUndef(data)
|
|
227
|
+
assert.deepEqual(data.arr, [null, 1, null])
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('should preserve non-null falsy values', async () => {
|
|
231
|
+
const data = { a: 0, b: false, c: '', d: undefined }
|
|
232
|
+
await RemoveUndef(data)
|
|
233
|
+
assert.equal(data.a, 0)
|
|
234
|
+
assert.equal(data.b, false)
|
|
235
|
+
assert.equal(data.c, '')
|
|
236
|
+
assert.equal(data.d, undefined)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should handle empty object', async () => {
|
|
240
|
+
const data = {}
|
|
241
|
+
await RemoveUndef(data)
|
|
242
|
+
assert.deepEqual(data, {})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should handle deeply nested nulls', async () => {
|
|
246
|
+
const data = { a: { b: { c: { d: null } } } }
|
|
247
|
+
await RemoveUndef(data)
|
|
248
|
+
assert.equal('d' in data.a.b.c, false)
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('StartPage', () => {
|
|
253
|
+
it('should assign _friendlyId to content objects from _startIds', async () => {
|
|
254
|
+
const data = {
|
|
255
|
+
_type: 'course',
|
|
256
|
+
_start: {
|
|
257
|
+
_startIds: [
|
|
258
|
+
{ _id: 'page1' },
|
|
259
|
+
{ _id: 'page2' }
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const importer = {
|
|
264
|
+
contentJson: {
|
|
265
|
+
contentObjects: {
|
|
266
|
+
page1: { _id: 'page1' },
|
|
267
|
+
page2: { _id: 'page2' }
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
framework: { log: mock.fn() }
|
|
271
|
+
}
|
|
272
|
+
await StartPage(data, importer)
|
|
273
|
+
assert.equal(importer.contentJson.contentObjects.page1._friendlyId, 'start_page_1')
|
|
274
|
+
assert.equal(importer.contentJson.contentObjects.page2._friendlyId, 'start_page_2')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should preserve existing _friendlyId', async () => {
|
|
278
|
+
const data = {
|
|
279
|
+
_type: 'course',
|
|
280
|
+
_start: {
|
|
281
|
+
_startIds: [{ _id: 'page1' }]
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const importer = {
|
|
285
|
+
contentJson: {
|
|
286
|
+
contentObjects: {
|
|
287
|
+
page1: { _id: 'page1', _friendlyId: 'my-custom-id' }
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
framework: { log: mock.fn() }
|
|
291
|
+
}
|
|
292
|
+
await StartPage(data, importer)
|
|
293
|
+
assert.equal(importer.contentJson.contentObjects.page1._friendlyId, 'my-custom-id')
|
|
294
|
+
assert.equal(data._start._startIds[0]._id, 'my-custom-id')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('should log warning for missing content object', async () => {
|
|
298
|
+
const logFn = mock.fn()
|
|
299
|
+
const data = {
|
|
300
|
+
_type: 'course',
|
|
301
|
+
_start: {
|
|
302
|
+
_startIds: [{ _id: 'missing' }]
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const importer = {
|
|
306
|
+
contentJson: { contentObjects: {} },
|
|
307
|
+
framework: { log: logFn }
|
|
308
|
+
}
|
|
309
|
+
await StartPage(data, importer)
|
|
310
|
+
assert.equal(logFn.mock.calls.length, 1)
|
|
311
|
+
assert.equal(logFn.mock.calls[0].arguments[0], 'warn')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should skip non-course types', async () => {
|
|
315
|
+
const data = { _type: 'config', _start: { _startIds: [{ _id: 'p1' }] } }
|
|
316
|
+
const importer = { contentJson: { contentObjects: {} } }
|
|
317
|
+
await StartPage(data, importer)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('should skip when _start is undefined', async () => {
|
|
321
|
+
const data = { _type: 'course' }
|
|
322
|
+
await StartPage(data, {})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('should update _startIds[i]._id to match _friendlyId', async () => {
|
|
326
|
+
const data = {
|
|
327
|
+
_type: 'course',
|
|
328
|
+
_start: { _startIds: [{ _id: 'page1' }] }
|
|
329
|
+
}
|
|
330
|
+
const importer = {
|
|
331
|
+
contentJson: {
|
|
332
|
+
contentObjects: { page1: { _id: 'page1' } }
|
|
333
|
+
},
|
|
334
|
+
framework: { log: mock.fn() }
|
|
335
|
+
}
|
|
336
|
+
await StartPage(data, importer)
|
|
337
|
+
assert.equal(data._start._startIds[0]._id, 'start_page_1')
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
describe('ThemeUndef', () => {
|
|
342
|
+
it('should set _theme from used plugins when undefined', async () => {
|
|
343
|
+
const data = { _type: 'config' }
|
|
344
|
+
const importer = {
|
|
345
|
+
usedContentPlugins: {
|
|
346
|
+
'adapt-contrib-vanilla': { name: 'adapt-contrib-vanilla', type: 'theme' },
|
|
347
|
+
'adapt-contrib-text': { name: 'adapt-contrib-text', type: 'component' }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
await ThemeUndef(data, importer)
|
|
351
|
+
assert.equal(data._theme, 'adapt-contrib-vanilla')
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('should not override existing _theme', async () => {
|
|
355
|
+
const data = { _type: 'config', _theme: 'my-theme' }
|
|
356
|
+
const importer = {
|
|
357
|
+
usedContentPlugins: {
|
|
358
|
+
'adapt-contrib-vanilla': { name: 'adapt-contrib-vanilla', type: 'theme' }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
await ThemeUndef(data, importer)
|
|
362
|
+
assert.equal(data._theme, 'my-theme')
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('should skip non-config types', async () => {
|
|
366
|
+
const data = { _type: 'course' }
|
|
367
|
+
const importer = {
|
|
368
|
+
usedContentPlugins: {
|
|
369
|
+
'adapt-contrib-vanilla': { name: 'adapt-contrib-vanilla', type: 'theme' }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
await ThemeUndef(data, importer)
|
|
373
|
+
assert.equal(data._theme, undefined)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should set _theme to undefined when no theme plugin found', async () => {
|
|
377
|
+
const data = { _type: 'config' }
|
|
378
|
+
const importer = {
|
|
379
|
+
usedContentPlugins: {
|
|
380
|
+
'adapt-contrib-text': { name: 'adapt-contrib-text', type: 'component' }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
await ThemeUndef(data, importer)
|
|
384
|
+
assert.equal(data._theme, undefined)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('should handle empty usedContentPlugins', async () => {
|
|
388
|
+
const data = { _type: 'config' }
|
|
389
|
+
const importer = { usedContentPlugins: {} }
|
|
390
|
+
await ThemeUndef(data, importer)
|
|
391
|
+
assert.equal(data._theme, undefined)
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
})
|