adapt-authoring-core 2.5.0 → 3.0.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.
@@ -1,7 +1,20 @@
1
- import { describe, it } from 'node:test'
1
+ import { afterEach, beforeEach, describe, it } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
+ import App from '../lib/App.js'
3
4
  import DataCache from '../lib/DataCache.js'
4
5
 
6
+ function stubApp (mongodbStub, logCalls) {
7
+ const mockApp = {
8
+ waitForModule: async () => mongodbStub,
9
+ logger: {
10
+ log: (...args) => logCalls.push(args)
11
+ }
12
+ }
13
+ const original = Object.getOwnPropertyDescriptor(App, 'instance')
14
+ Object.defineProperty(App, 'instance', { get: () => mockApp, configurable: true })
15
+ return () => Object.defineProperty(App, 'instance', original)
16
+ }
17
+
5
18
  describe('DataCache', () => {
6
19
  describe('#prune()', () => {
7
20
  it('should remove expired entries from the cache', () => {
@@ -36,4 +49,74 @@ describe('DataCache', () => {
36
49
  assert.deepEqual(instance.cache, {})
37
50
  })
38
51
  })
52
+
53
+ describe('#get()', () => {
54
+ let findCalls
55
+ let logCalls
56
+ let restore
57
+ const mongoStub = {
58
+ find: async (...args) => {
59
+ findCalls.push(args)
60
+ return [{ _id: 'stub' }]
61
+ }
62
+ }
63
+
64
+ beforeEach(() => {
65
+ findCalls = []
66
+ logCalls = []
67
+ restore = stubApp(mongoStub, logCalls)
68
+ })
69
+
70
+ afterEach(() => restore())
71
+
72
+ it('should query the DB and log a miss on first call', async () => {
73
+ const cache = new DataCache({ enable: true, lifespan: 10000 })
74
+ const result = await cache.get({ _id: '1' }, { collectionName: 'users' }, {})
75
+ assert.deepEqual(result, [{ _id: 'stub' }])
76
+ assert.equal(findCalls.length, 1)
77
+ assert.equal(cache.misses, 1)
78
+ assert.equal(cache.hits, 0)
79
+ assert.equal(logCalls[0][0], 'verbose')
80
+ assert.equal(logCalls[0][1], 'datacache')
81
+ assert.equal(logCalls[0][2], 'miss')
82
+ })
83
+
84
+ it('should return cached data and log a hit on a repeat call', async () => {
85
+ const cache = new DataCache({ enable: true, lifespan: 10000 })
86
+ await cache.get({ _id: '1' }, { collectionName: 'users' }, {})
87
+ await cache.get({ _id: '1' }, { collectionName: 'users' }, {})
88
+ assert.equal(findCalls.length, 1)
89
+ assert.equal(cache.hits, 1)
90
+ assert.equal(cache.misses, 1)
91
+ assert.equal(logCalls[1][2], 'hit')
92
+ })
93
+
94
+ it('should re-query the DB after an entry expires', async () => {
95
+ const cache = new DataCache({ enable: true, lifespan: 10 })
96
+ await cache.get({ _id: '1' }, { collectionName: 'users' }, {})
97
+ await new Promise(resolve => setTimeout(resolve, 20))
98
+ await cache.get({ _id: '1' }, { collectionName: 'users' }, {})
99
+ assert.equal(findCalls.length, 2)
100
+ assert.equal(cache.hits, 0)
101
+ assert.equal(cache.misses, 2)
102
+ })
103
+
104
+ it('should always query the DB when the cache is disabled', async () => {
105
+ const cache = new DataCache({ enable: false, lifespan: 10000 })
106
+ await cache.get({ _id: '1' }, { collectionName: 'users' }, {})
107
+ await cache.get({ _id: '1' }, { collectionName: 'users' }, {})
108
+ assert.equal(findCalls.length, 2)
109
+ assert.equal(cache.hits, 0)
110
+ assert.equal(cache.misses, 2)
111
+ })
112
+
113
+ it('should treat different queries as distinct cache entries', async () => {
114
+ const cache = new DataCache({ enable: true, lifespan: 10000 })
115
+ await cache.get({ _id: '1' }, { collectionName: 'users' }, {})
116
+ await cache.get({ _id: '2' }, { collectionName: 'users' }, {})
117
+ assert.equal(findCalls.length, 2)
118
+ assert.equal(cache.misses, 2)
119
+ assert.equal(cache.hits, 0)
120
+ })
121
+ })
39
122
  })
@@ -1,5 +1,6 @@
1
1
  import { describe, it, before, after } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
+ import AdaptError from '../lib/AdaptError.js'
3
4
  import DependencyLoader from '../lib/DependencyLoader.js'
4
5
  import fs from 'fs-extra'
5
6
  import path from 'path'
@@ -62,12 +63,11 @@ describe('DependencyLoader', () => {
62
63
  })
63
64
  })
64
65
 
65
- it('should call app.logger when available and ready', () => {
66
+ it('should call app.logger when available', () => {
66
67
  let logged = false
67
68
  const mockApp = {
68
69
  rootDir: '/test',
69
70
  logger: {
70
- _isReady: true, // Note: Mock uses private property to simulate ready state
71
71
  log: () => { logged = true }
72
72
  }
73
73
  }
@@ -78,27 +78,11 @@ describe('DependencyLoader', () => {
78
78
  assert.equal(logged, true)
79
79
  })
80
80
 
81
- it('should fall back to console.log when logger not ready', () => {
82
- const mockApp = {
83
- rootDir: '/test',
84
- logger: {
85
- _isReady: false, // Note: Mock uses private property to check ready state
86
- log: () => {}
87
- }
88
- }
89
- const loader = new DependencyLoader(mockApp)
90
-
91
- assert.doesNotThrow(() => {
92
- loader.log('info', 'test message')
93
- })
94
- })
95
-
96
81
  it('should pass level and args to app.logger.log', () => {
97
82
  let loggedLevel, loggedName, loggedArgs
98
83
  const mockApp = {
99
84
  rootDir: '/test',
100
85
  logger: {
101
- _isReady: true,
102
86
  log: (level, name, ...args) => {
103
87
  loggedLevel = level
104
88
  loggedName = name
@@ -115,97 +99,6 @@ describe('DependencyLoader', () => {
115
99
  })
116
100
  })
117
101
 
118
- describe('#logError()', () => {
119
- it('should call log with error level', () => {
120
- let loggedLevel
121
- const mockApp = {
122
- rootDir: '/test',
123
- logger: {
124
- _isReady: true,
125
- log: (level) => { loggedLevel = level }
126
- }
127
- }
128
- const loader = new DependencyLoader(mockApp)
129
-
130
- loader.logError('error message')
131
-
132
- assert.equal(loggedLevel, 'error')
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
- })
148
- })
149
-
150
- describe('#getConfig()', () => {
151
- it('should return undefined when config is not ready', () => {
152
- const mockApp = {
153
- rootDir: '/test'
154
- }
155
- const loader = new DependencyLoader(mockApp)
156
-
157
- const result = loader.getConfig('someKey')
158
-
159
- assert.equal(result, undefined)
160
- })
161
-
162
- it('should return config value when config is ready', () => {
163
- const mockApp = {
164
- rootDir: '/test',
165
- config: {
166
- _isReady: true, // Note: Mock uses private property to simulate ready state
167
- get: (key) => {
168
- if (key === 'adapt-authoring-core.testKey') return 'testValue'
169
- }
170
- }
171
- }
172
- const loader = new DependencyLoader(mockApp)
173
-
174
- const result = loader.getConfig('testKey')
175
-
176
- assert.equal(result, 'testValue')
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
- })
207
- })
208
-
209
102
  describe('#loadConfigs()', () => {
210
103
  let testRootDir
211
104
 
@@ -339,63 +232,72 @@ describe('DependencyLoader', () => {
339
232
  })
340
233
 
341
234
  describe('#loadModules()', () => {
342
- it('should throw DependencyError when module fails without force', async () => {
235
+ it('should add non-fatal failures to failedModules', async () => {
343
236
  const mockApp = { rootDir: '/test' }
344
237
  const loader = new DependencyLoader(mockApp)
345
238
  loader.configs = { 'nonexistent-module': { module: true, name: 'nonexistent-module' } }
346
239
 
347
- await assert.rejects(
348
- loader.loadModules(['nonexistent-module']),
349
- { name: 'DependencyError' }
350
- )
351
- })
352
-
353
- it('should not throw when module fails with force option', async () => {
354
- const mockApp = { rootDir: '/test' }
355
- const loader = new DependencyLoader(mockApp)
356
- loader.configs = { 'nonexistent-module': { module: true, name: 'nonexistent-module' } }
357
-
358
- await loader.loadModules(['nonexistent-module'], { force: true })
240
+ await loader.loadModules(['nonexistent-module'])
359
241
 
360
242
  assert.ok(loader.failedModules.includes('nonexistent-module'))
361
243
  })
362
244
 
363
- it('should log peer dependency warnings on failure with force', async () => {
245
+ it('should throw when module throws a fatal error', async () => {
364
246
  const mockApp = { rootDir: '/test' }
365
247
  const loader = new DependencyLoader(mockApp)
366
- loader.configs = { 'nonexistent-module': { module: true, name: 'nonexistent-module' } }
367
- loader.peerDependencies = { 'nonexistent-module': ['dependent-mod'] }
248
+ loader.configs = { 'fatal-module': { module: true, name: 'fatal-module' } }
368
249
 
369
- await loader.loadModules(['nonexistent-module'], { force: true })
250
+ // monkey-patch loadModule to throw a fatal error
251
+ const originalLoadModule = loader.loadModule.bind(loader)
252
+ loader.loadModule = async (name) => {
253
+ if (name === 'fatal-module') {
254
+ const error = new Error('Fatal')
255
+ error.isFatal = true
256
+ throw error
257
+ }
258
+ return originalLoadModule(name)
259
+ }
370
260
 
371
- assert.ok(loader.failedModules.includes('nonexistent-module'))
261
+ await assert.rejects(
262
+ loader.loadModules(['fatal-module']),
263
+ (err) => {
264
+ assert.equal(err.isFatal, true)
265
+ return true
266
+ }
267
+ )
372
268
  })
373
269
 
374
- it('should include module name in DependencyError message', async () => {
270
+ it('should throw when error.cause is fatal', async () => {
375
271
  const mockApp = { rootDir: '/test' }
376
272
  const loader = new DependencyLoader(mockApp)
377
- loader.configs = { 'nonexistent-module': { module: true, name: 'nonexistent-module' } }
273
+ loader.configs = { 'fatal-module': { module: true, name: 'fatal-module' } }
274
+
275
+ loader.loadModule = async () => {
276
+ const cause = new Error('Root cause')
277
+ cause.isFatal = true
278
+ const error = new Error('Wrapper')
279
+ error.cause = cause
280
+ throw error
281
+ }
378
282
 
379
283
  await assert.rejects(
380
- loader.loadModules(['nonexistent-module']),
284
+ loader.loadModules(['fatal-module']),
381
285
  (err) => {
382
- assert.ok(err.message.includes('nonexistent-module'))
286
+ assert.equal(err.cause.isFatal, true)
383
287
  return true
384
288
  }
385
289
  )
386
290
  })
387
291
 
388
- it('should set cause on DependencyError', async () => {
292
+ it('should log peer dependency warnings on non-fatal failure', async () => {
389
293
  const mockApp = { rootDir: '/test' }
390
294
  const loader = new DependencyLoader(mockApp)
391
295
  loader.configs = { 'nonexistent-module': { module: true, name: 'nonexistent-module' } }
296
+ loader.peerDependencies = { 'nonexistent-module': ['dependent-mod'] }
392
297
 
393
- try {
394
- await loader.loadModules(['nonexistent-module'])
395
- assert.fail('should have thrown')
396
- } catch (err) {
397
- assert.ok(err.cause)
398
- }
298
+ await loader.loadModules(['nonexistent-module'])
299
+
300
+ assert.ok(loader.failedModules.includes('nonexistent-module'))
399
301
  })
400
302
 
401
303
  it('should handle empty module list', async () => {
@@ -410,13 +312,18 @@ describe('DependencyLoader', () => {
410
312
 
411
313
  describe('#loadModule()', () => {
412
314
  it('should throw when module already exists', async () => {
413
- const mockApp = { rootDir: '/test' }
315
+ const mockApp = {
316
+ rootDir: '/test',
317
+ errors: {
318
+ DEP_ALREADY_LOADED: new AdaptError('DEP_ALREADY_LOADED')
319
+ }
320
+ }
414
321
  const loader = new DependencyLoader(mockApp)
415
322
  loader.instances = { 'existing-module': {} }
416
323
 
417
324
  await assert.rejects(
418
325
  loader.loadModule('existing-module'),
419
- { message: 'Module already exists' }
326
+ { code: 'DEP_ALREADY_LOADED' }
420
327
  )
421
328
  })
422
329
 
@@ -443,19 +350,29 @@ describe('DependencyLoader', () => {
443
350
 
444
351
  describe('#waitForModule()', () => {
445
352
  it('should throw for missing module', async () => {
446
- const mockApp = { rootDir: '/test' }
353
+ const mockApp = {
354
+ rootDir: '/test',
355
+ errors: {
356
+ DEP_MISSING: new AdaptError('DEP_MISSING')
357
+ }
358
+ }
447
359
  const loader = new DependencyLoader(mockApp)
448
360
  loader._configsLoaded = true
449
361
  loader.configs = {}
450
362
 
451
363
  await assert.rejects(
452
364
  loader.waitForModule('adapt-authoring-missing'),
453
- { message: "Missing required module 'adapt-authoring-missing'" }
365
+ { code: 'DEP_MISSING' }
454
366
  )
455
367
  })
456
368
 
457
369
  it('should throw for failed module', async () => {
458
- const mockApp = { rootDir: '/test' }
370
+ const mockApp = {
371
+ rootDir: '/test',
372
+ errors: {
373
+ DEP_FAILED: new AdaptError('DEP_FAILED')
374
+ }
375
+ }
459
376
  const loader = new DependencyLoader(mockApp)
460
377
  loader._configsLoaded = true
461
378
  loader.configs = { 'adapt-authoring-failed': { name: 'adapt-authoring-failed' } }
@@ -463,7 +380,7 @@ describe('DependencyLoader', () => {
463
380
 
464
381
  await assert.rejects(
465
382
  loader.waitForModule('adapt-authoring-failed'),
466
- { message: "Dependency 'adapt-authoring-failed' failed to load" }
383
+ { code: 'DEP_FAILED' }
467
384
  )
468
385
  })
469
386
 
@@ -601,7 +518,6 @@ describe('DependencyLoader', () => {
601
518
  const mockApp = {
602
519
  rootDir: '/test',
603
520
  logger: {
604
- _isReady: true,
605
521
  log: (level, name, ...args) => {
606
522
  if (typeof args[0] === 'object' && !Array.isArray(args[0]) && typeof args[0] !== 'string') {
607
523
  loggedTimes = args[0]
@@ -628,7 +544,6 @@ describe('DependencyLoader', () => {
628
544
  const mockApp = {
629
545
  rootDir: '/test',
630
546
  logger: {
631
- _isReady: true,
632
547
  log: (level, name, ...args) => {
633
548
  if (typeof args[0] === 'string' && args[0] === 'LOAD') {
634
549
  loggedMessage = args[1]
@@ -0,0 +1,91 @@
1
+ import { describe, it, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import Errors from '../lib/Errors.js'
4
+ import AdaptError from '../lib/AdaptError.js'
5
+ import fs from 'fs-extra'
6
+ import path from 'path'
7
+ import { fileURLToPath } from 'url'
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
+
11
+ describe('Errors', () => {
12
+ let testDir
13
+
14
+ before(async () => {
15
+ testDir = path.join(__dirname, 'data', 'errors-test')
16
+ const errorsDir = path.join(testDir, 'errors')
17
+ await fs.ensureDir(errorsDir)
18
+ await fs.writeJson(path.join(errorsDir, 'test.json'), {
19
+ TEST_ERROR: {
20
+ description: 'A test error',
21
+ statusCode: 400,
22
+ data: { id: 'The item ID' }
23
+ },
24
+ FATAL_ERROR: {
25
+ description: 'A fatal error',
26
+ statusCode: 500,
27
+ isFatal: true
28
+ }
29
+ })
30
+ })
31
+
32
+ after(async () => {
33
+ await fs.remove(testDir)
34
+ })
35
+
36
+ describe('constructor', () => {
37
+ it('should load error definitions from dependencies', () => {
38
+ const deps = { test: { name: 'test', rootDir: testDir } }
39
+ const errors = new Errors({ dependencies: deps })
40
+ assert.ok(errors.TEST_ERROR)
41
+ assert.ok(errors.FATAL_ERROR)
42
+ })
43
+
44
+ it('should return AdaptError instances', () => {
45
+ const deps = { test: { name: 'test', rootDir: testDir } }
46
+ const errors = new Errors({ dependencies: deps })
47
+ assert.ok(errors.TEST_ERROR instanceof AdaptError)
48
+ })
49
+
50
+ it('should return fresh instances on each access', () => {
51
+ const deps = { test: { name: 'test', rootDir: testDir } }
52
+ const errors = new Errors({ dependencies: deps })
53
+ assert.notEqual(errors.TEST_ERROR, errors.TEST_ERROR)
54
+ })
55
+
56
+ it('should set statusCode from definition', () => {
57
+ const deps = { test: { name: 'test', rootDir: testDir } }
58
+ const errors = new Errors({ dependencies: deps })
59
+ assert.equal(errors.TEST_ERROR.statusCode, 400)
60
+ })
61
+
62
+ it('should set isFatal from definition', () => {
63
+ const deps = { test: { name: 'test', rootDir: testDir } }
64
+ const errors = new Errors({ dependencies: deps })
65
+ assert.equal(errors.FATAL_ERROR.isFatal, true)
66
+ assert.equal(errors.TEST_ERROR.isFatal, false)
67
+ })
68
+
69
+ it('should warn on duplicate error codes', () => {
70
+ const dupDir = path.join(__dirname, 'data', 'errors-dup')
71
+ const errorsDir = path.join(dupDir, 'errors')
72
+ fs.ensureDirSync(errorsDir)
73
+ fs.writeJsonSync(path.join(errorsDir, 'dup.json'), {
74
+ TEST_ERROR: { description: 'duplicate', statusCode: 500 }
75
+ })
76
+ const deps = {
77
+ test: { name: 'test', rootDir: testDir },
78
+ dup: { name: 'dup', rootDir: dupDir }
79
+ }
80
+ let warned = false
81
+ new Errors({ dependencies: deps, log: () => { warned = true } }) // eslint-disable-line no-new
82
+ assert.ok(warned)
83
+ fs.removeSync(dupDir)
84
+ })
85
+
86
+ it('should handle empty dependencies', () => {
87
+ const errors = new Errors({ dependencies: {} })
88
+ assert.deepEqual(Object.keys(errors), [])
89
+ })
90
+ })
91
+ })
@@ -0,0 +1,116 @@
1
+ import { describe, it, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import Lang from '../lib/Lang.js'
4
+ import fs from 'fs-extra'
5
+ import path from 'path'
6
+ import { fileURLToPath } from 'url'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ function createLang (phrases, defaultLang = 'en') {
11
+ const lang = new Lang({ dependencies: {}, defaultLang, rootDir: __dirname })
12
+ lang.phrases = phrases
13
+ return lang
14
+ }
15
+
16
+ describe('Lang', () => {
17
+ let testDir
18
+
19
+ before(async () => {
20
+ testDir = path.join(__dirname, 'data', 'lang-test')
21
+ const langDir = path.join(testDir, 'lang')
22
+ await fs.ensureDir(langDir)
23
+ await fs.writeJson(path.join(langDir, 'en.json'), {
24
+ 'app.name': 'Test App',
25
+ 'app.greeting': 'Hello ${name}', // eslint-disable-line no-template-curly-in-string
26
+ 'error.TEST_ERROR': 'A test error occurred'
27
+ })
28
+ await fs.writeJson(path.join(langDir, 'fr.json'), {
29
+ 'app.name': 'Application Test'
30
+ })
31
+ })
32
+
33
+ after(async () => {
34
+ await fs.remove(testDir)
35
+ })
36
+
37
+ describe('#loadPhrases()', () => {
38
+ it('should load phrases from dependencies', async () => {
39
+ const lang = new Lang()
40
+ await lang.loadPhrases({ test: { rootDir: testDir } }, testDir)
41
+ assert.ok(lang.phrases.en)
42
+ assert.equal(lang.phrases.en['app.name'], 'Test App')
43
+ })
44
+
45
+ it('should load multiple languages', async () => {
46
+ const lang = new Lang()
47
+ await lang.loadPhrases({ test: { rootDir: testDir } }, testDir)
48
+ assert.ok(lang.phrases.fr)
49
+ assert.equal(lang.phrases.fr['app.name'], 'Application Test')
50
+ })
51
+ })
52
+
53
+ describe('#supportedLanguages', () => {
54
+ it('should return loaded language keys', async () => {
55
+ const lang = new Lang()
56
+ await lang.loadPhrases({ test: { rootDir: testDir } }, testDir)
57
+ const languages = lang.supportedLanguages
58
+ assert.ok(languages.includes('en'))
59
+ assert.ok(languages.includes('fr'))
60
+ })
61
+ })
62
+
63
+ describe('#translate()', () => {
64
+ it('should return translated string', () => {
65
+ const lang = createLang({ en: { hello: 'Hello' } })
66
+ assert.equal(lang.translate('en', 'hello'), 'Hello')
67
+ })
68
+
69
+ it('should substitute data placeholders', () => {
70
+ // eslint-disable-next-line no-template-curly-in-string
71
+ const lang = createLang({ en: { greeting: 'Hello ${name}' } })
72
+ assert.equal(lang.translate('en', 'greeting', { name: 'World' }), 'Hello World')
73
+ })
74
+
75
+ it('should fall back to default lang when lang is not a string', () => {
76
+ const lang = createLang({ en: { hello: 'Hello' } })
77
+ assert.equal(lang.translate(undefined, 'hello'), 'Hello')
78
+ })
79
+
80
+ it('should return key and warn when key is missing', () => {
81
+ let warned = false
82
+ const lang = createLang({ en: {} })
83
+ lang.log = () => { warned = true }
84
+ assert.equal(lang.translate('en', 'missing.key'), 'missing.key')
85
+ assert.ok(warned)
86
+ })
87
+
88
+ it('should return non-error, non-string values unchanged', () => {
89
+ const lang = createLang({})
90
+ assert.equal(lang.translate('en', 42), 42)
91
+ })
92
+
93
+ it('should translate an error using its code', () => {
94
+ const lang = createLang({ en: { 'error.TEST': 'Translated error' } })
95
+ const error = new Error('TEST')
96
+ error.code = 'TEST'
97
+ assert.equal(lang.translate('en', error), 'Translated error')
98
+ })
99
+
100
+ it('should fall back to error message when error has no code', () => {
101
+ const lang = createLang({ en: {} })
102
+ lang.log = () => {}
103
+ assert.equal(lang.translate('en', new Error('boom')), 'boom')
104
+ })
105
+
106
+ it('should substitute a code-less error in data using its message', () => {
107
+ // eslint-disable-next-line no-template-curly-in-string
108
+ const lang = createLang({ en: { 'error.OUTER': 'Failed: ${cause}' } })
109
+ lang.log = () => {}
110
+ const outer = new Error('OUTER')
111
+ outer.code = 'OUTER'
112
+ outer.data = { cause: new Error('disk full') }
113
+ assert.equal(lang.translate('en', outer), 'Failed: disk full')
114
+ })
115
+ })
116
+ })