adapt-authoring-core 1.8.0 → 1.9.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/.github/workflows/tests.yml +15 -0
- package/lib/AbstractModule.js +5 -1
- package/lib/Hook.js +1 -1
- package/package.json +1 -1
- package/tests/AbstractModule.spec.js +215 -0
- package/tests/App.spec.js +57 -1
- package/tests/DependencyLoader.spec.js +256 -0
- package/tests/Hook.spec.js +113 -3
- package/tests/Utils.spec.js +10 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
on: push
|
|
3
|
+
jobs:
|
|
4
|
+
default:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- uses: actions/setup-node@v4
|
|
11
|
+
with:
|
|
12
|
+
node-version: 'lts/*'
|
|
13
|
+
cache: 'npm'
|
|
14
|
+
- run: npm ci
|
|
15
|
+
- run: npm test
|
package/lib/AbstractModule.js
CHANGED
|
@@ -74,6 +74,7 @@ class AbstractModule {
|
|
|
74
74
|
if (!error) {
|
|
75
75
|
this._isReady = true
|
|
76
76
|
}
|
|
77
|
+
/** @ignore */ this._initError = error
|
|
77
78
|
this.initTime = Math.round((Date.now() - this._startTime))
|
|
78
79
|
this.log('verbose', AbstractModule.MODULE_READY, this.initTime)
|
|
79
80
|
await this.readyHook.invoke(error)
|
|
@@ -86,7 +87,10 @@ class AbstractModule {
|
|
|
86
87
|
async onReady () {
|
|
87
88
|
return new Promise((resolve, reject) => {
|
|
88
89
|
if (this._isReady) {
|
|
89
|
-
return
|
|
90
|
+
return resolve(this)
|
|
91
|
+
}
|
|
92
|
+
if (this._initError) {
|
|
93
|
+
return reject(this._initError)
|
|
90
94
|
}
|
|
91
95
|
this.readyHook.tap(error => {
|
|
92
96
|
/** @ignore */this._initError = error
|
package/lib/Hook.js
CHANGED
package/package.json
CHANGED
|
@@ -66,6 +66,69 @@ describe('AbstractModule', () => {
|
|
|
66
66
|
await module.readyHook.invoke()
|
|
67
67
|
assert.equal(hookCalled, true)
|
|
68
68
|
})
|
|
69
|
+
|
|
70
|
+
it('should use constructor name when pkg is undefined', async () => {
|
|
71
|
+
const mockApp = {
|
|
72
|
+
dependencyloader: {
|
|
73
|
+
moduleLoadedHook: {
|
|
74
|
+
tap: () => {},
|
|
75
|
+
untap: () => {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const module = new AbstractModule(mockApp, undefined)
|
|
80
|
+
await module.onReady()
|
|
81
|
+
assert.equal(module.name, 'AbstractModule')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should use subclass constructor name when pkg.name is empty', async () => {
|
|
85
|
+
const mockApp = {
|
|
86
|
+
dependencyloader: {
|
|
87
|
+
moduleLoadedHook: {
|
|
88
|
+
tap: () => {},
|
|
89
|
+
untap: () => {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class MyCustomModule extends AbstractModule {
|
|
95
|
+
async init () {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const module = new MyCustomModule(mockApp, { name: '' })
|
|
99
|
+
await module.onReady()
|
|
100
|
+
assert.equal(module.name, 'MyCustomModule')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should set _startTime during construction', async () => {
|
|
104
|
+
const before = Date.now()
|
|
105
|
+
const mockApp = {
|
|
106
|
+
dependencyloader: {
|
|
107
|
+
moduleLoadedHook: {
|
|
108
|
+
tap: () => {},
|
|
109
|
+
untap: () => {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const module = new AbstractModule(mockApp, { name: 'test' })
|
|
114
|
+
const after = Date.now()
|
|
115
|
+
await module.onReady()
|
|
116
|
+
assert.ok(module._startTime >= before)
|
|
117
|
+
assert.ok(module._startTime <= after)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should initialise initTime as undefined', () => {
|
|
121
|
+
const mockApp = {
|
|
122
|
+
dependencyloader: {
|
|
123
|
+
moduleLoadedHook: {
|
|
124
|
+
tap: () => {},
|
|
125
|
+
untap: () => {}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const module = new AbstractModule(mockApp, { name: 'test' })
|
|
130
|
+
assert.equal(module.initTime, undefined)
|
|
131
|
+
})
|
|
69
132
|
})
|
|
70
133
|
|
|
71
134
|
describe('#init()', () => {
|
|
@@ -111,6 +174,43 @@ describe('AbstractModule', () => {
|
|
|
111
174
|
const module = new TestModule(mockApp, { name: 'test' })
|
|
112
175
|
await assert.rejects(module.onReady(), { message: 'init error' })
|
|
113
176
|
})
|
|
177
|
+
|
|
178
|
+
it('should handle async init that resolves', async () => {
|
|
179
|
+
const mockApp = {
|
|
180
|
+
dependencyloader: {
|
|
181
|
+
moduleLoadedHook: {
|
|
182
|
+
tap: () => {},
|
|
183
|
+
untap: () => {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let completed = false
|
|
188
|
+
|
|
189
|
+
class TestModule extends AbstractModule {
|
|
190
|
+
async init () {
|
|
191
|
+
await new Promise(resolve => setTimeout(resolve, 5))
|
|
192
|
+
completed = true
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const module = new TestModule(mockApp, { name: 'test' })
|
|
197
|
+
await module.onReady()
|
|
198
|
+
assert.equal(completed, true)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should default init to a no-op that resolves', async () => {
|
|
202
|
+
const mockApp = {
|
|
203
|
+
dependencyloader: {
|
|
204
|
+
moduleLoadedHook: {
|
|
205
|
+
tap: () => {},
|
|
206
|
+
untap: () => {}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const module = new AbstractModule(mockApp, { name: 'test' })
|
|
211
|
+
await module.onReady()
|
|
212
|
+
assert.equal(module._isReady, true)
|
|
213
|
+
})
|
|
114
214
|
})
|
|
115
215
|
|
|
116
216
|
describe('#setReady()', () => {
|
|
@@ -192,6 +292,28 @@ describe('AbstractModule', () => {
|
|
|
192
292
|
|
|
193
293
|
assert.equal(module.initTime, firstInitTime)
|
|
194
294
|
})
|
|
295
|
+
|
|
296
|
+
it('should calculate initTime even on error', async () => {
|
|
297
|
+
const mockApp = {
|
|
298
|
+
dependencyloader: {
|
|
299
|
+
moduleLoadedHook: {
|
|
300
|
+
tap: () => {},
|
|
301
|
+
untap: () => {}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
class TestModule extends AbstractModule {
|
|
307
|
+
async init () {
|
|
308
|
+
throw new Error('fail')
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const module = new TestModule(mockApp, { name: 'test' })
|
|
313
|
+
try { await module.onReady() } catch (e) { /* expected */ }
|
|
314
|
+
assert.equal(typeof module.initTime, 'number')
|
|
315
|
+
assert.ok(module.initTime >= 0)
|
|
316
|
+
})
|
|
195
317
|
})
|
|
196
318
|
|
|
197
319
|
describe('#onReady()', () => {
|
|
@@ -264,6 +386,24 @@ describe('AbstractModule', () => {
|
|
|
264
386
|
|
|
265
387
|
assert.equal(resolvedModule, module)
|
|
266
388
|
})
|
|
389
|
+
|
|
390
|
+
// TODO: Bug - onReady() hangs forever after a failed init, because _isReady
|
|
391
|
+
// remains false and readyHook has already been invoked, so the tap never fires.
|
|
392
|
+
// Calling onReady() a second time after a failure will never resolve or reject.
|
|
393
|
+
it('should support multiple concurrent onReady calls', async () => {
|
|
394
|
+
const mockApp = {
|
|
395
|
+
dependencyloader: {
|
|
396
|
+
moduleLoadedHook: {
|
|
397
|
+
tap: () => {},
|
|
398
|
+
untap: () => {}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const module = new AbstractModule(mockApp, { name: 'test' })
|
|
403
|
+
const [r1, r2] = await Promise.all([module.onReady(), module.onReady()])
|
|
404
|
+
assert.equal(r1, module)
|
|
405
|
+
assert.equal(r2, module)
|
|
406
|
+
})
|
|
267
407
|
})
|
|
268
408
|
|
|
269
409
|
describe('#getConfig()', () => {
|
|
@@ -330,6 +470,28 @@ describe('AbstractModule', () => {
|
|
|
330
470
|
|
|
331
471
|
assert.equal(result, undefined)
|
|
332
472
|
})
|
|
473
|
+
|
|
474
|
+
it('should construct key from module name and config key', async () => {
|
|
475
|
+
let requestedKey
|
|
476
|
+
const mockApp = {
|
|
477
|
+
config: {
|
|
478
|
+
get: (key) => { requestedKey = key; return 'value' }
|
|
479
|
+
},
|
|
480
|
+
dependencyloader: {
|
|
481
|
+
moduleLoadedHook: {
|
|
482
|
+
tap: () => {},
|
|
483
|
+
untap: () => {}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const module = new AbstractModule(mockApp, { name: 'my-module' })
|
|
488
|
+
|
|
489
|
+
await module.onReady()
|
|
490
|
+
|
|
491
|
+
module.getConfig('myKey')
|
|
492
|
+
|
|
493
|
+
assert.equal(requestedKey, 'my-module.myKey')
|
|
494
|
+
})
|
|
333
495
|
})
|
|
334
496
|
|
|
335
497
|
describe('#log()', () => {
|
|
@@ -452,6 +614,59 @@ describe('AbstractModule', () => {
|
|
|
452
614
|
|
|
453
615
|
assert.deepEqual(loggedArgs, ['arg1', 'arg2', 'arg3'])
|
|
454
616
|
})
|
|
617
|
+
|
|
618
|
+
it('should queue log and deliver when logger module loads', async () => {
|
|
619
|
+
let loggedLevel
|
|
620
|
+
let tapCallback
|
|
621
|
+
const mockApp = {
|
|
622
|
+
dependencyloader: {
|
|
623
|
+
moduleLoadedHook: {
|
|
624
|
+
tap: (fn) => { tapCallback = fn },
|
|
625
|
+
untap: () => {}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const module = new AbstractModule(mockApp, { name: 'test-mod' })
|
|
630
|
+
await module.onReady()
|
|
631
|
+
|
|
632
|
+
module.log('warn', 'deferred message')
|
|
633
|
+
assert.ok(tapCallback)
|
|
634
|
+
|
|
635
|
+
mockApp.logger = {
|
|
636
|
+
name: 'adapt-authoring-logger',
|
|
637
|
+
log: (level) => {
|
|
638
|
+
loggedLevel = level
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
tapCallback(null, { name: 'adapt-authoring-logger' })
|
|
643
|
+
assert.equal(loggedLevel, 'warn')
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
it('should not log when loaded module is not the logger', async () => {
|
|
647
|
+
const logCalled = false
|
|
648
|
+
let tapCallback
|
|
649
|
+
const mockApp = {
|
|
650
|
+
dependencyloader: {
|
|
651
|
+
moduleLoadedHook: {
|
|
652
|
+
tap: (fn) => { tapCallback = fn },
|
|
653
|
+
untap: () => {}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const module = new AbstractModule(mockApp, { name: 'test-mod' })
|
|
658
|
+
await module.onReady()
|
|
659
|
+
|
|
660
|
+
// No logger set yet, so log queues the callback
|
|
661
|
+
module.log('info', 'some message')
|
|
662
|
+
assert.ok(tapCallback)
|
|
663
|
+
|
|
664
|
+
// Now simulate a non-logger module loading - _log checks !this.app.logger
|
|
665
|
+
// which is true (no logger), so it returns false
|
|
666
|
+
const result = tapCallback(null, { name: 'adapt-authoring-other' })
|
|
667
|
+
assert.equal(result, false)
|
|
668
|
+
assert.equal(logCalled, false)
|
|
669
|
+
})
|
|
455
670
|
})
|
|
456
671
|
|
|
457
672
|
describe('constructor with null pkg', () => {
|
package/tests/App.spec.js
CHANGED
|
@@ -40,13 +40,30 @@ describe('App', () => {
|
|
|
40
40
|
assert.ok(app instanceof App)
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
it('should return the same instance on subsequent calls', () => {
|
|
43
|
+
it('should return the same instance on subsequent calls (singleton)', () => {
|
|
44
44
|
const app1 = App.instance
|
|
45
45
|
const app2 = App.instance
|
|
46
46
|
assert.equal(app1, app2)
|
|
47
47
|
})
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
describe('constructor', () => {
|
|
51
|
+
it('should set name to adapt-authoring-core', () => {
|
|
52
|
+
const app = App.instance
|
|
53
|
+
assert.equal(app.name, 'adapt-authoring-core')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should set rootDir from ROOT_DIR env var', () => {
|
|
57
|
+
const app = App.instance
|
|
58
|
+
assert.equal(app.rootDir, testRootDir)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should initialize git info', () => {
|
|
62
|
+
const app = App.instance
|
|
63
|
+
assert.equal(typeof app.git, 'object')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
50
67
|
describe('#dependencies', () => {
|
|
51
68
|
it('should return the dependency configs from dependencyloader', () => {
|
|
52
69
|
const app = App.instance
|
|
@@ -70,6 +87,25 @@ describe('App', () => {
|
|
|
70
87
|
app.rootDir = origRootDir
|
|
71
88
|
assert.deepEqual(info, {})
|
|
72
89
|
})
|
|
90
|
+
|
|
91
|
+
it('should return object with branch and commit when .git exists', async () => {
|
|
92
|
+
const gitDir = path.join(testRootDir, '.git')
|
|
93
|
+
const refsDir = path.join(gitDir, 'refs', 'heads')
|
|
94
|
+
await fs.ensureDir(refsDir)
|
|
95
|
+
await fs.writeFile(path.join(gitDir, 'HEAD'), 'ref: refs/heads/main\n')
|
|
96
|
+
await fs.writeFile(path.join(refsDir, 'main'), 'abc123def456\n')
|
|
97
|
+
|
|
98
|
+
const app = App.instance
|
|
99
|
+
const origRootDir = app.rootDir
|
|
100
|
+
app.rootDir = testRootDir
|
|
101
|
+
const info = app.getGitInfo()
|
|
102
|
+
app.rootDir = origRootDir
|
|
103
|
+
|
|
104
|
+
assert.equal(info.branch, 'main')
|
|
105
|
+
assert.equal(info.commit, 'abc123def456')
|
|
106
|
+
|
|
107
|
+
await fs.remove(gitDir)
|
|
108
|
+
})
|
|
73
109
|
})
|
|
74
110
|
|
|
75
111
|
describe('#waitForModule()', () => {
|
|
@@ -100,5 +136,25 @@ describe('App', () => {
|
|
|
100
136
|
assert.deepEqual(result[0], { name: 'mod-a' })
|
|
101
137
|
assert.deepEqual(result[1], { name: 'mod-b' })
|
|
102
138
|
})
|
|
139
|
+
|
|
140
|
+
it('should return single result (not array) for single module', async () => {
|
|
141
|
+
const app = App.instance
|
|
142
|
+
const origWaitForModule = app.dependencyloader.waitForModule.bind(app.dependencyloader)
|
|
143
|
+
app.dependencyloader.waitForModule = async (name) => ({ name })
|
|
144
|
+
const result = await app.waitForModule('single-mod')
|
|
145
|
+
app.dependencyloader.waitForModule = origWaitForModule
|
|
146
|
+
|
|
147
|
+
assert.ok(!Array.isArray(result))
|
|
148
|
+
assert.deepEqual(result, { name: 'single-mod' })
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('#setReady()', () => {
|
|
153
|
+
it('should set _isStarting to false', async () => {
|
|
154
|
+
const app = App.instance
|
|
155
|
+
app._isStarting = true
|
|
156
|
+
await app.setReady()
|
|
157
|
+
assert.equal(app._isStarting, false)
|
|
158
|
+
})
|
|
103
159
|
})
|
|
104
160
|
})
|
|
@@ -38,6 +38,18 @@ describe('DependencyLoader', () => {
|
|
|
38
38
|
assert.equal(typeof loader.configsLoadedHook.invoke, 'function')
|
|
39
39
|
assert.equal(typeof loader.moduleLoadedHook.invoke, 'function')
|
|
40
40
|
})
|
|
41
|
+
|
|
42
|
+
it('should tap logProgress into moduleLoadedHook', () => {
|
|
43
|
+
const mockApp = { rootDir: '/test' }
|
|
44
|
+
const loader = new DependencyLoader(mockApp)
|
|
45
|
+
assert.ok(loader.moduleLoadedHook.hasObservers)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should derive name from constructor name in lowercase', () => {
|
|
49
|
+
const mockApp = { rootDir: '/test' }
|
|
50
|
+
const loader = new DependencyLoader(mockApp)
|
|
51
|
+
assert.equal(loader.name, 'dependencyloader')
|
|
52
|
+
})
|
|
41
53
|
})
|
|
42
54
|
|
|
43
55
|
describe('#log()', () => {
|
|
@@ -80,6 +92,27 @@ describe('DependencyLoader', () => {
|
|
|
80
92
|
loader.log('info', 'test message')
|
|
81
93
|
})
|
|
82
94
|
})
|
|
95
|
+
|
|
96
|
+
it('should pass level and args to app.logger.log', () => {
|
|
97
|
+
let loggedLevel, loggedName, loggedArgs
|
|
98
|
+
const mockApp = {
|
|
99
|
+
rootDir: '/test',
|
|
100
|
+
logger: {
|
|
101
|
+
_isReady: true,
|
|
102
|
+
log: (level, name, ...args) => {
|
|
103
|
+
loggedLevel = level
|
|
104
|
+
loggedName = name
|
|
105
|
+
loggedArgs = args
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const loader = new DependencyLoader(mockApp)
|
|
110
|
+
loader.log('warn', 'message1', 'message2')
|
|
111
|
+
|
|
112
|
+
assert.equal(loggedLevel, 'warn')
|
|
113
|
+
assert.equal(loggedName, 'dependencyloader')
|
|
114
|
+
assert.deepEqual(loggedArgs, ['message1', 'message2'])
|
|
115
|
+
})
|
|
83
116
|
})
|
|
84
117
|
|
|
85
118
|
describe('#logError()', () => {
|
|
@@ -98,6 +131,20 @@ describe('DependencyLoader', () => {
|
|
|
98
131
|
|
|
99
132
|
assert.equal(loggedLevel, 'error')
|
|
100
133
|
})
|
|
134
|
+
|
|
135
|
+
it('should pass all arguments through to log', () => {
|
|
136
|
+
let loggedArgs
|
|
137
|
+
const mockApp = {
|
|
138
|
+
rootDir: '/test',
|
|
139
|
+
logger: {
|
|
140
|
+
_isReady: true,
|
|
141
|
+
log: (level, name, ...args) => { loggedArgs = args }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const loader = new DependencyLoader(mockApp)
|
|
145
|
+
loader.logError('msg1', 'msg2')
|
|
146
|
+
assert.deepEqual(loggedArgs, ['msg1', 'msg2'])
|
|
147
|
+
})
|
|
101
148
|
})
|
|
102
149
|
|
|
103
150
|
describe('#getConfig()', () => {
|
|
@@ -128,6 +175,35 @@ describe('DependencyLoader', () => {
|
|
|
128
175
|
|
|
129
176
|
assert.equal(result, 'testValue')
|
|
130
177
|
})
|
|
178
|
+
|
|
179
|
+
it('should return undefined when config exists but is not ready', () => {
|
|
180
|
+
const mockApp = {
|
|
181
|
+
rootDir: '/test',
|
|
182
|
+
config: {
|
|
183
|
+
_isReady: false,
|
|
184
|
+
get: () => 'should not be called'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const loader = new DependencyLoader(mockApp)
|
|
188
|
+
|
|
189
|
+
const result = loader.getConfig('someKey')
|
|
190
|
+
|
|
191
|
+
assert.equal(result, undefined)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should always use adapt-authoring-core prefix for config keys', () => {
|
|
195
|
+
let requestedKey
|
|
196
|
+
const mockApp = {
|
|
197
|
+
rootDir: '/test',
|
|
198
|
+
config: {
|
|
199
|
+
_isReady: true,
|
|
200
|
+
get: (key) => { requestedKey = key }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const loader = new DependencyLoader(mockApp)
|
|
204
|
+
loader.getConfig('myKey')
|
|
205
|
+
assert.equal(requestedKey, 'adapt-authoring-core.myKey')
|
|
206
|
+
})
|
|
131
207
|
})
|
|
132
208
|
|
|
133
209
|
describe('#loadModuleConfig()', () => {
|
|
@@ -166,6 +242,37 @@ describe('DependencyLoader', () => {
|
|
|
166
242
|
assert.equal(config.essentialType, 'api')
|
|
167
243
|
assert.equal(config.rootDir, testModuleDir)
|
|
168
244
|
})
|
|
245
|
+
|
|
246
|
+
it('should override package.json values with adapt-authoring.json values', async () => {
|
|
247
|
+
const overrideDir = path.join(__dirname, 'data', 'override-module')
|
|
248
|
+
await fs.ensureDir(overrideDir)
|
|
249
|
+
await fs.writeJson(path.join(overrideDir, 'package.json'), {
|
|
250
|
+
name: 'pkg-name',
|
|
251
|
+
description: 'from package'
|
|
252
|
+
})
|
|
253
|
+
await fs.writeJson(path.join(overrideDir, 'adapt-authoring.json'), {
|
|
254
|
+
name: 'adapt-name',
|
|
255
|
+
description: 'from adapt'
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
const mockApp = { rootDir: '/test' }
|
|
259
|
+
const loader = new DependencyLoader(mockApp)
|
|
260
|
+
const config = await loader.loadModuleConfig(overrideDir)
|
|
261
|
+
|
|
262
|
+
assert.equal(config.name, 'adapt-name')
|
|
263
|
+
assert.equal(config.description, 'from adapt')
|
|
264
|
+
|
|
265
|
+
await fs.remove(overrideDir)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('should set rootDir to the module directory', async () => {
|
|
269
|
+
const mockApp = { rootDir: '/test' }
|
|
270
|
+
const loader = new DependencyLoader(mockApp)
|
|
271
|
+
|
|
272
|
+
const config = await loader.loadModuleConfig(testModuleDir)
|
|
273
|
+
|
|
274
|
+
assert.equal(config.rootDir, testModuleDir)
|
|
275
|
+
})
|
|
169
276
|
})
|
|
170
277
|
|
|
171
278
|
describe('#loadModules()', () => {
|
|
@@ -200,6 +307,42 @@ describe('DependencyLoader', () => {
|
|
|
200
307
|
|
|
201
308
|
assert.ok(loader.failedModules.includes('nonexistent-module'))
|
|
202
309
|
})
|
|
310
|
+
|
|
311
|
+
it('should include module name in DependencyError message', async () => {
|
|
312
|
+
const mockApp = { rootDir: '/test' }
|
|
313
|
+
const loader = new DependencyLoader(mockApp)
|
|
314
|
+
loader.configs = { 'nonexistent-module': { module: true, name: 'nonexistent-module' } }
|
|
315
|
+
|
|
316
|
+
await assert.rejects(
|
|
317
|
+
loader.loadModules(['nonexistent-module']),
|
|
318
|
+
(err) => {
|
|
319
|
+
assert.ok(err.message.includes('nonexistent-module'))
|
|
320
|
+
return true
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('should set cause on DependencyError', async () => {
|
|
326
|
+
const mockApp = { rootDir: '/test' }
|
|
327
|
+
const loader = new DependencyLoader(mockApp)
|
|
328
|
+
loader.configs = { 'nonexistent-module': { module: true, name: 'nonexistent-module' } }
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
await loader.loadModules(['nonexistent-module'])
|
|
332
|
+
assert.fail('should have thrown')
|
|
333
|
+
} catch (err) {
|
|
334
|
+
assert.ok(err.cause)
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('should handle empty module list', async () => {
|
|
339
|
+
const mockApp = { rootDir: '/test' }
|
|
340
|
+
const loader = new DependencyLoader(mockApp)
|
|
341
|
+
|
|
342
|
+
await loader.loadModules([])
|
|
343
|
+
|
|
344
|
+
assert.deepEqual(loader.failedModules, [])
|
|
345
|
+
})
|
|
203
346
|
})
|
|
204
347
|
|
|
205
348
|
describe('#loadModule()', () => {
|
|
@@ -223,6 +366,16 @@ describe('DependencyLoader', () => {
|
|
|
223
366
|
|
|
224
367
|
assert.equal(result, undefined)
|
|
225
368
|
})
|
|
369
|
+
|
|
370
|
+
it('should not add to instances when config.module is false', async () => {
|
|
371
|
+
const mockApp = { rootDir: '/test' }
|
|
372
|
+
const loader = new DependencyLoader(mockApp)
|
|
373
|
+
loader.configs = { 'non-module': { module: false } }
|
|
374
|
+
|
|
375
|
+
await loader.loadModule('non-module')
|
|
376
|
+
|
|
377
|
+
assert.equal(loader.instances['non-module'], undefined)
|
|
378
|
+
})
|
|
226
379
|
})
|
|
227
380
|
|
|
228
381
|
describe('#waitForModule()', () => {
|
|
@@ -284,6 +437,39 @@ describe('DependencyLoader', () => {
|
|
|
284
437
|
|
|
285
438
|
assert.equal(result, mockInstance)
|
|
286
439
|
})
|
|
440
|
+
|
|
441
|
+
it('should not double-prefix names that already have the prefix', async () => {
|
|
442
|
+
const mockApp = { rootDir: '/test' }
|
|
443
|
+
const loader = new DependencyLoader(mockApp)
|
|
444
|
+
loader._configsLoaded = true
|
|
445
|
+
const mockInstance = {
|
|
446
|
+
name: 'adapt-authoring-server',
|
|
447
|
+
_isReady: true,
|
|
448
|
+
onReady: async () => mockInstance
|
|
449
|
+
}
|
|
450
|
+
loader.configs = { 'adapt-authoring-server': { name: 'adapt-authoring-server' } }
|
|
451
|
+
loader.instances = { 'adapt-authoring-server': mockInstance }
|
|
452
|
+
|
|
453
|
+
const result = await loader.waitForModule('adapt-authoring-server')
|
|
454
|
+
|
|
455
|
+
assert.equal(result, mockInstance)
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('should resolve via moduleLoadedHook when module not yet loaded', async () => {
|
|
459
|
+
const mockApp = { rootDir: '/test' }
|
|
460
|
+
const loader = new DependencyLoader(mockApp)
|
|
461
|
+
loader._configsLoaded = true
|
|
462
|
+
loader.configs = { 'adapt-authoring-pending': { name: 'adapt-authoring-pending' } }
|
|
463
|
+
|
|
464
|
+
const mockInstance = { name: 'adapt-authoring-pending' }
|
|
465
|
+
|
|
466
|
+
const waitPromise = loader.waitForModule('adapt-authoring-pending')
|
|
467
|
+
|
|
468
|
+
await loader.moduleLoadedHook.invoke(null, mockInstance)
|
|
469
|
+
|
|
470
|
+
const result = await waitPromise
|
|
471
|
+
assert.equal(result, mockInstance)
|
|
472
|
+
})
|
|
287
473
|
})
|
|
288
474
|
|
|
289
475
|
describe('#logProgress()', () => {
|
|
@@ -332,5 +518,75 @@ describe('DependencyLoader', () => {
|
|
|
332
518
|
loader.logProgress(null, { name: 'test-module', initTime: 50 })
|
|
333
519
|
})
|
|
334
520
|
})
|
|
521
|
+
|
|
522
|
+
it('should count module being loaded as loaded even before _isReady', () => {
|
|
523
|
+
const mockApp = { rootDir: '/test' }
|
|
524
|
+
const loader = new DependencyLoader(mockApp)
|
|
525
|
+
loader.configs = {
|
|
526
|
+
'mod-a': { name: 'mod-a', module: true },
|
|
527
|
+
'mod-b': { name: 'mod-b', module: true }
|
|
528
|
+
}
|
|
529
|
+
loader.instances = {}
|
|
530
|
+
|
|
531
|
+
assert.doesNotThrow(() => {
|
|
532
|
+
loader.logProgress(null, { name: 'mod-a', initTime: 10 })
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('should log init times at 100% progress', () => {
|
|
537
|
+
let loggedTimes
|
|
538
|
+
const mockApp = {
|
|
539
|
+
rootDir: '/test',
|
|
540
|
+
logger: {
|
|
541
|
+
_isReady: true,
|
|
542
|
+
log: (level, name, ...args) => {
|
|
543
|
+
if (typeof args[0] === 'object' && !Array.isArray(args[0]) && typeof args[0] !== 'string') {
|
|
544
|
+
loggedTimes = args[0]
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const loader = new DependencyLoader(mockApp)
|
|
550
|
+
loader.configs = {
|
|
551
|
+
'mod-a': { name: 'mod-a', module: true }
|
|
552
|
+
}
|
|
553
|
+
loader.instances = {
|
|
554
|
+
'mod-a': { name: 'mod-a', _isReady: true, initTime: 42 }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
loader.logProgress(null, { name: 'mod-a', initTime: 42 })
|
|
558
|
+
|
|
559
|
+
assert.ok(loggedTimes)
|
|
560
|
+
assert.equal(loggedTimes['mod-a'], 42)
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
it('should include failed module names in log output', () => {
|
|
564
|
+
let loggedMessage
|
|
565
|
+
const mockApp = {
|
|
566
|
+
rootDir: '/test',
|
|
567
|
+
logger: {
|
|
568
|
+
_isReady: true,
|
|
569
|
+
log: (level, name, ...args) => {
|
|
570
|
+
if (typeof args[0] === 'string' && args[0] === 'LOAD') {
|
|
571
|
+
loggedMessage = args[1]
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const loader = new DependencyLoader(mockApp)
|
|
577
|
+
loader.configs = {
|
|
578
|
+
'adapt-authoring-ok': { name: 'adapt-authoring-ok', module: true },
|
|
579
|
+
'adapt-authoring-bad': { name: 'adapt-authoring-bad', module: true }
|
|
580
|
+
}
|
|
581
|
+
loader.instances = {
|
|
582
|
+
'adapt-authoring-ok': { name: 'adapt-authoring-ok', _isReady: true, initTime: 10 }
|
|
583
|
+
}
|
|
584
|
+
loader.failedModules = ['adapt-authoring-bad']
|
|
585
|
+
|
|
586
|
+
loader.logProgress(null, { name: 'adapt-authoring-ok', initTime: 10 })
|
|
587
|
+
|
|
588
|
+
assert.ok(loggedMessage)
|
|
589
|
+
assert.ok(loggedMessage.includes('failed'))
|
|
590
|
+
})
|
|
335
591
|
})
|
|
336
592
|
})
|
package/tests/Hook.spec.js
CHANGED
|
@@ -13,6 +13,12 @@ describe('Hook', () => {
|
|
|
13
13
|
})
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
+
describe('.Types', () => {
|
|
17
|
+
it('should have exactly two type keys', () => {
|
|
18
|
+
assert.equal(Object.keys(Hook.Types).length, 2)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
16
22
|
describe('constructor', () => {
|
|
17
23
|
it('should create a hook with default options', () => {
|
|
18
24
|
const hook = new Hook()
|
|
@@ -20,6 +26,34 @@ describe('Hook', () => {
|
|
|
20
26
|
assert.equal(hook.hasObservers, false)
|
|
21
27
|
})
|
|
22
28
|
|
|
29
|
+
it('should default to parallel type', () => {
|
|
30
|
+
const hook = new Hook()
|
|
31
|
+
assert.equal(hook._options.type, Hook.Types.Parallel)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should default mutable to false', () => {
|
|
35
|
+
const hook = new Hook()
|
|
36
|
+
assert.equal(hook._options.mutable, false)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should allow explicit type override even when mutable is true', () => {
|
|
40
|
+
const hook = new Hook({ type: Hook.Types.Parallel, mutable: true })
|
|
41
|
+
// When type is explicitly provided, it overrides the mutable-forced series default
|
|
42
|
+
assert.equal(hook._options.type, Hook.Types.Parallel)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should handle undefined options', () => {
|
|
46
|
+
const hook = new Hook(undefined)
|
|
47
|
+
assert.equal(hook._options.type, Hook.Types.Parallel)
|
|
48
|
+
assert.equal(hook._options.mutable, false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should initialise with empty observer arrays', () => {
|
|
52
|
+
const hook = new Hook()
|
|
53
|
+
assert.deepEqual(hook._hookObservers, [])
|
|
54
|
+
assert.deepEqual(hook._promiseObservers, [])
|
|
55
|
+
})
|
|
56
|
+
|
|
23
57
|
it('should create a hook that supports parallel execution', async () => {
|
|
24
58
|
const hook = new Hook()
|
|
25
59
|
const results = []
|
|
@@ -100,8 +134,19 @@ describe('Hook', () => {
|
|
|
100
134
|
hook.tap('not a function')
|
|
101
135
|
hook.tap(null)
|
|
102
136
|
hook.tap(42)
|
|
137
|
+
hook.tap(undefined)
|
|
138
|
+
hook.tap({})
|
|
139
|
+
hook.tap([])
|
|
103
140
|
assert.equal(hook.hasObservers, false)
|
|
104
141
|
})
|
|
142
|
+
|
|
143
|
+
it('should work without scope argument', async () => {
|
|
144
|
+
const hook = new Hook()
|
|
145
|
+
let called = false
|
|
146
|
+
hook.tap(() => { called = true })
|
|
147
|
+
await hook.invoke()
|
|
148
|
+
assert.equal(called, true)
|
|
149
|
+
})
|
|
105
150
|
})
|
|
106
151
|
|
|
107
152
|
describe('#untap()', () => {
|
|
@@ -122,6 +167,23 @@ describe('Hook', () => {
|
|
|
122
167
|
// observer still present because tap binds the function
|
|
123
168
|
assert.equal(hook.hasObservers, true)
|
|
124
169
|
})
|
|
170
|
+
|
|
171
|
+
it('should remove a directly-pushed observer by reference', () => {
|
|
172
|
+
const hook = new Hook()
|
|
173
|
+
const fn = () => {}
|
|
174
|
+
hook._hookObservers.push(fn)
|
|
175
|
+
assert.equal(hook.hasObservers, true)
|
|
176
|
+
hook.untap(fn)
|
|
177
|
+
assert.equal(hook.hasObservers, false)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should only remove the first matching reference', () => {
|
|
181
|
+
const hook = new Hook()
|
|
182
|
+
const fn = () => {}
|
|
183
|
+
hook._hookObservers.push(fn, fn)
|
|
184
|
+
hook.untap(fn)
|
|
185
|
+
assert.equal(hook._hookObservers.length, 1)
|
|
186
|
+
})
|
|
125
187
|
})
|
|
126
188
|
|
|
127
189
|
describe('#invoke()', () => {
|
|
@@ -187,6 +249,14 @@ describe('Hook', () => {
|
|
|
187
249
|
const result = await hook.invoke()
|
|
188
250
|
assert.deepEqual(result, [])
|
|
189
251
|
})
|
|
252
|
+
|
|
253
|
+
it('should handle mixed sync and async observers', async () => {
|
|
254
|
+
const hook = new Hook()
|
|
255
|
+
hook.tap(() => 'sync')
|
|
256
|
+
hook.tap(async () => 'async')
|
|
257
|
+
const result = await hook.invoke()
|
|
258
|
+
assert.deepEqual(result, ['sync', 'async'])
|
|
259
|
+
})
|
|
190
260
|
})
|
|
191
261
|
|
|
192
262
|
describe('series hooks', () => {
|
|
@@ -228,6 +298,28 @@ describe('Hook', () => {
|
|
|
228
298
|
hook.tap(() => { throw new Error('series error') })
|
|
229
299
|
await assert.rejects(hook.invoke(), { message: 'series error' })
|
|
230
300
|
})
|
|
301
|
+
|
|
302
|
+
it('should stop on first error and not call subsequent observers', async () => {
|
|
303
|
+
const hook = new Hook({ type: Hook.Types.Series })
|
|
304
|
+
const calls = []
|
|
305
|
+
hook.tap(() => { calls.push(1); throw new Error('stop') })
|
|
306
|
+
hook.tap(() => calls.push(2))
|
|
307
|
+
await assert.rejects(hook.invoke())
|
|
308
|
+
assert.deepEqual(calls, [1])
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should return undefined with no observers', async () => {
|
|
312
|
+
const hook = new Hook({ type: Hook.Types.Series })
|
|
313
|
+
const result = await hook.invoke()
|
|
314
|
+
assert.equal(result, undefined)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('should return single observer result', async () => {
|
|
318
|
+
const hook = new Hook({ type: Hook.Types.Series })
|
|
319
|
+
hook.tap(() => 'only')
|
|
320
|
+
const result = await hook.invoke()
|
|
321
|
+
assert.equal(result, 'only')
|
|
322
|
+
})
|
|
231
323
|
})
|
|
232
324
|
|
|
233
325
|
describe('mutable hooks', () => {
|
|
@@ -239,6 +331,16 @@ describe('Hook', () => {
|
|
|
239
331
|
await hook.invoke(obj)
|
|
240
332
|
assert.equal(obj.value, 3)
|
|
241
333
|
})
|
|
334
|
+
|
|
335
|
+
it('should allow multiple mutations in sequence', async () => {
|
|
336
|
+
const hook = new Hook({ mutable: true })
|
|
337
|
+
const arr = []
|
|
338
|
+
hook.tap((a) => { a.push(1) })
|
|
339
|
+
hook.tap((a) => { a.push(2) })
|
|
340
|
+
hook.tap((a) => { a.push(3) })
|
|
341
|
+
await hook.invoke(arr)
|
|
342
|
+
assert.deepEqual(arr, [1, 2, 3])
|
|
343
|
+
})
|
|
242
344
|
})
|
|
243
345
|
})
|
|
244
346
|
|
|
@@ -249,11 +351,19 @@ describe('Hook', () => {
|
|
|
249
351
|
assert.ok(result instanceof Promise)
|
|
250
352
|
})
|
|
251
353
|
|
|
252
|
-
it('should add an entry to
|
|
354
|
+
it('should add an entry to promise observers', () => {
|
|
253
355
|
const hook = new Hook()
|
|
254
|
-
assert.equal(hook.
|
|
356
|
+
assert.equal(hook._promiseObservers.length, 0)
|
|
255
357
|
hook.onInvoke()
|
|
256
|
-
assert.equal(hook.
|
|
358
|
+
assert.equal(hook._promiseObservers.length, 1)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('should add a resolve/reject pair as promise observer', () => {
|
|
362
|
+
const hook = new Hook()
|
|
363
|
+
hook.onInvoke()
|
|
364
|
+
const observer = hook._promiseObservers[0]
|
|
365
|
+
assert.ok(Array.isArray(observer))
|
|
366
|
+
assert.equal(observer.length, 2)
|
|
257
367
|
})
|
|
258
368
|
})
|
|
259
369
|
})
|
package/tests/Utils.spec.js
CHANGED
|
@@ -21,6 +21,16 @@ describe('Utils', () => {
|
|
|
21
21
|
assert.equal(typeof args, 'object')
|
|
22
22
|
assert.ok(Array.isArray(args.params))
|
|
23
23
|
})
|
|
24
|
+
|
|
25
|
+
it('should include the underscore array from minimist', () => {
|
|
26
|
+
const args = Utils.getArgs()
|
|
27
|
+
assert.ok(Array.isArray(args._))
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should derive params by slicing first two entries from _', () => {
|
|
31
|
+
const args = Utils.getArgs()
|
|
32
|
+
assert.deepEqual(args.params, args._.slice(2))
|
|
33
|
+
})
|
|
24
34
|
})
|
|
25
35
|
|
|
26
36
|
describe('.isObject()', () => {
|