adapt-authoring-core 1.9.2 → 2.0.0
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/docs/writing-tests.md +283 -69
- package/index.js +1 -1
- package/lib/App.js +4 -4
- package/lib/DependencyLoader.js +5 -5
- package/lib/Utils.js +9 -76
- package/lib/utils/constants.js +11 -0
- package/lib/utils/ensureDir.js +14 -0
- package/lib/utils/escapeRegExp.js +8 -0
- package/lib/utils/getArgs.js +12 -0
- package/lib/utils/isObject.js +8 -0
- package/lib/utils/readJson.js +10 -0
- package/lib/utils/spawn.js +33 -0
- package/lib/utils/toBoolean.js +9 -0
- package/lib/utils/writeJson.js +11 -0
- package/package.json +1 -1
- package/tests/utils-constants.spec.js +16 -0
- package/tests/utils-ensureDir.spec.js +41 -0
- package/tests/utils-escapeRegExp.spec.js +65 -0
- package/tests/utils-getArgs.spec.js +22 -0
- package/tests/utils-isObject.spec.js +49 -0
- package/tests/utils-readJson.spec.js +53 -0
- package/tests/utils-spawn.spec.js +76 -0
- package/tests/utils-toBoolean.spec.js +42 -0
- package/tests/utils-writeJson.spec.js +34 -0
- package/tests/Utils.spec.js +0 -80
package/docs/writing-tests.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Writing tests
|
|
2
2
|
|
|
3
|
-
Instructions for writing tests in this module.
|
|
3
|
+
Instructions for writing tests in this project. Both unit tests (per-module) and integration tests (full application) use the same stack and assertion style.
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
@@ -9,57 +9,7 @@ Instructions for writing tests in this module.
|
|
|
9
9
|
|
|
10
10
|
No external test dependencies are needed. Do not introduce Mocha, Jest, or other test frameworks.
|
|
11
11
|
|
|
12
|
-
> **Note:** Some older modules in the project use Mocha + Should.js. New tests
|
|
13
|
-
|
|
14
|
-
## File placement and naming
|
|
15
|
-
|
|
16
|
-
- Tests live in a `tests/` directory at the module root
|
|
17
|
-
- One test file per source module, named `<moduleName>.spec.js`
|
|
18
|
-
- Test data/fixtures go in `tests/data/`
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
my-module/
|
|
22
|
-
├── lib/
|
|
23
|
-
│ ├── myModule.js
|
|
24
|
-
│ └── myUtils.js
|
|
25
|
-
└── tests/
|
|
26
|
-
├── data/
|
|
27
|
-
│ └── fixtures.json
|
|
28
|
-
├── myModule.spec.js
|
|
29
|
-
└── myUtils.spec.js
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## File structure
|
|
33
|
-
|
|
34
|
-
Every test file follows this structure:
|
|
35
|
-
|
|
36
|
-
```js
|
|
37
|
-
import { describe, it, before } from 'node:test'
|
|
38
|
-
import assert from 'node:assert/strict'
|
|
39
|
-
import MyModule from '../lib/myModule.js'
|
|
40
|
-
|
|
41
|
-
describe('My Module', () => {
|
|
42
|
-
let instance
|
|
43
|
-
|
|
44
|
-
before(() => {
|
|
45
|
-
instance = new MyModule()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
describe('#methodName()', () => {
|
|
49
|
-
it('should describe expected behaviour', () => {
|
|
50
|
-
assert.equal(instance.methodName(), 'expected')
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
})
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Key rules:
|
|
57
|
-
|
|
58
|
-
- Import `describe`, `it`, `before`, `after` etc. from `node:test`
|
|
59
|
-
- Import `assert` from `node:assert/strict` (strict mode uses `deepStrictEqual` by default)
|
|
60
|
-
- Use ES module `import` syntax — this project is `"type": "module"`
|
|
61
|
-
- Group tests by method using nested `describe` blocks, prefixed with `#` for instance methods
|
|
62
|
-
- Store shared state in `let` variables scoped to the `describe` block
|
|
12
|
+
> **Note:** Some older modules in the project use Mocha + Should.js. New tests use the built-in Node.js test library instead.
|
|
63
13
|
|
|
64
14
|
## Assertions
|
|
65
15
|
|
|
@@ -96,6 +46,17 @@ assert.notEqual(a, b)
|
|
|
96
46
|
assert.notDeepEqual(obj1, obj2)
|
|
97
47
|
```
|
|
98
48
|
|
|
49
|
+
## Async tests
|
|
50
|
+
|
|
51
|
+
`node:test` supports `async/await` directly:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
it('should connect successfully', async () => {
|
|
55
|
+
const result = await instance.connect()
|
|
56
|
+
assert.equal(typeof result, 'object')
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
99
60
|
## Dynamic test generation
|
|
100
61
|
|
|
101
62
|
When testing a function against multiple inputs, generate tests in a loop:
|
|
@@ -117,18 +78,64 @@ invalidInputs.forEach((input) => {
|
|
|
117
78
|
})
|
|
118
79
|
```
|
|
119
80
|
|
|
120
|
-
##
|
|
81
|
+
## General rules
|
|
121
82
|
|
|
122
|
-
`
|
|
83
|
+
- Import `describe`, `it`, `before`, `after` etc. from `node:test`
|
|
84
|
+
- Import `assert` from `node:assert/strict` (strict mode uses `deepStrictEqual` by default)
|
|
85
|
+
- Use ES module `import` syntax — this project is `"type": "module"`
|
|
86
|
+
- Store shared state in `let` variables scoped to the `describe` block
|
|
87
|
+
- Don't add external test dependencies — `node:test` and `node:assert` are sufficient
|
|
88
|
+
- Don't mock more than necessary — prefer testing real behaviour
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Unit tests
|
|
93
|
+
|
|
94
|
+
Unit tests live inside individual modules and test classes and utilities in isolation.
|
|
95
|
+
|
|
96
|
+
### File placement and naming
|
|
97
|
+
|
|
98
|
+
- Tests live in a `tests/` directory at the module root
|
|
99
|
+
- One test file per source module, named `<moduleName>.spec.js`
|
|
100
|
+
- Test data/fixtures go in `tests/data/`
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
my-module/
|
|
104
|
+
├── lib/
|
|
105
|
+
│ ├── myModule.js
|
|
106
|
+
│ └── myUtils.js
|
|
107
|
+
└── tests/
|
|
108
|
+
├── data/
|
|
109
|
+
│ └── fixtures.json
|
|
110
|
+
├── myModule.spec.js
|
|
111
|
+
└── myUtils.spec.js
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### File structure
|
|
123
115
|
|
|
124
116
|
```js
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
import { describe, it, before } from 'node:test'
|
|
118
|
+
import assert from 'node:assert/strict'
|
|
119
|
+
import MyModule from '../lib/myModule.js'
|
|
120
|
+
|
|
121
|
+
describe('My Module', () => {
|
|
122
|
+
let instance
|
|
123
|
+
|
|
124
|
+
before(() => {
|
|
125
|
+
instance = new MyModule()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('#methodName()', () => {
|
|
129
|
+
it('should describe expected behaviour', () => {
|
|
130
|
+
assert.equal(instance.methodName(), 'expected')
|
|
131
|
+
})
|
|
132
|
+
})
|
|
128
133
|
})
|
|
129
134
|
```
|
|
130
135
|
|
|
131
|
-
|
|
136
|
+
- Group tests by method using nested `describe` blocks, prefixed with `#` for instance methods
|
|
137
|
+
|
|
138
|
+
### Test data
|
|
132
139
|
|
|
133
140
|
Place fixture files in `tests/data/`. Use `import` or `fs` to load them:
|
|
134
141
|
|
|
@@ -141,33 +148,31 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
141
148
|
const fixtures = JSON.parse(readFileSync(join(__dirname, 'data', 'fixtures.json')))
|
|
142
149
|
```
|
|
143
150
|
|
|
144
|
-
|
|
151
|
+
### What to test
|
|
145
152
|
|
|
146
153
|
- All public methods on exported classes and utilities
|
|
147
154
|
- Both success and error paths
|
|
148
155
|
- Edge cases (empty input, missing arguments, invalid types)
|
|
149
156
|
- That errors are thrown or returned where expected (use `assert.throws` / `assert.rejects`)
|
|
150
157
|
|
|
151
|
-
|
|
158
|
+
### What NOT to do
|
|
152
159
|
|
|
153
160
|
- Don't test private/internal methods (prefixed with `_`)
|
|
154
|
-
- Don't add external test dependencies — `node:test` and `node:assert` are sufficient
|
|
155
161
|
- Don't write tests that depend on execution order between `describe` blocks
|
|
156
|
-
- Don't mock more than necessary — prefer testing real behaviour
|
|
157
162
|
|
|
158
|
-
|
|
163
|
+
### Add script to package.json
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
```
|
|
165
|
+
```json
|
|
162
166
|
"scripts": {
|
|
163
167
|
"test": "node --test tests/"
|
|
164
168
|
}
|
|
165
169
|
```
|
|
166
170
|
|
|
167
|
-
|
|
171
|
+
### Add GitHub workflow
|
|
168
172
|
|
|
169
|
-
The following workflow should be added to
|
|
170
|
-
|
|
173
|
+
The following workflow should be added to `/.github`:
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
171
176
|
name: Tests
|
|
172
177
|
on: push
|
|
173
178
|
jobs:
|
|
@@ -183,7 +188,7 @@ jobs:
|
|
|
183
188
|
- run: npm test
|
|
184
189
|
```
|
|
185
190
|
|
|
186
|
-
|
|
191
|
+
### Running unit tests
|
|
187
192
|
|
|
188
193
|
```bash
|
|
189
194
|
# run the test suite
|
|
@@ -194,3 +199,212 @@ npx standard
|
|
|
194
199
|
```
|
|
195
200
|
|
|
196
201
|
Both must pass before submitting a pull request.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Integration tests
|
|
206
|
+
|
|
207
|
+
Integration tests live in the `integration-tests/` package and test the full application with a real database. They boot the app once and exercise cross-module workflows such as authentication, content CRUD, course import, and course build/export.
|
|
208
|
+
|
|
209
|
+
See the [integration-tests README](../../integration-tests/README.md) for setup and run instructions.
|
|
210
|
+
|
|
211
|
+
### File placement and naming
|
|
212
|
+
|
|
213
|
+
All integration test files live in `integration-tests/tests/`, named `<area>.spec.js`:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
integration-tests/
|
|
217
|
+
├── bin/
|
|
218
|
+
│ └── run.js # test runner entry point
|
|
219
|
+
├── lib/
|
|
220
|
+
│ ├── app.js # app bootstrap helpers
|
|
221
|
+
│ ├── db.js # database utilities
|
|
222
|
+
│ └── fixtures.js # fixture management
|
|
223
|
+
├── tests/
|
|
224
|
+
│ ├── auth.spec.js
|
|
225
|
+
│ ├── content.spec.js
|
|
226
|
+
│ ├── mongodb.spec.js
|
|
227
|
+
│ ├── adaptframework-import.spec.js
|
|
228
|
+
│ └── ...
|
|
229
|
+
└── fixtures/
|
|
230
|
+
├── manifest.json # maps fixture keys to files
|
|
231
|
+
└── course-export.zip
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### App bootstrap helpers
|
|
235
|
+
|
|
236
|
+
Integration tests share a single app instance. Use the helpers from `lib/app.js`:
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
import { getApp, getModule, cleanDb } from '../lib/app.js'
|
|
240
|
+
|
|
241
|
+
// getApp() — boots the app (cached, only boots once per run)
|
|
242
|
+
// getModule() — waits for a named module to be ready (e.g. 'content', 'auth-local')
|
|
243
|
+
// cleanDb() — deletes documents from test collections
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### File structure
|
|
247
|
+
|
|
248
|
+
Every integration test follows this pattern:
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
import { describe, it, before, after } from 'node:test'
|
|
252
|
+
import assert from 'node:assert/strict'
|
|
253
|
+
import { getApp, getModule, cleanDb } from '../lib/app.js'
|
|
254
|
+
|
|
255
|
+
let content
|
|
256
|
+
|
|
257
|
+
describe('Content CRUD operations', () => {
|
|
258
|
+
before(async () => {
|
|
259
|
+
await getApp()
|
|
260
|
+
content = await getModule('content')
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
after(async () => {
|
|
264
|
+
await cleanDb(['content', 'users', 'authtokens'])
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('Course creation', () => {
|
|
268
|
+
it('should insert a course', async () => {
|
|
269
|
+
const course = await content.insert(
|
|
270
|
+
{ _type: 'course', title: 'Test Course', createdBy },
|
|
271
|
+
{ validate: false, schemaName: 'course' }
|
|
272
|
+
)
|
|
273
|
+
assert.ok(course._id, 'course should have an _id')
|
|
274
|
+
assert.equal(course._type, 'course')
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Key differences from unit tests:
|
|
281
|
+
|
|
282
|
+
- Always call `await getApp()` in `before()` to ensure the app is booted
|
|
283
|
+
- Use `getModule(name)` to get module instances — never import modules directly
|
|
284
|
+
- Always clean up in `after()` using `cleanDb(collections)` to avoid cross-test pollution
|
|
285
|
+
|
|
286
|
+
### Database cleanup
|
|
287
|
+
|
|
288
|
+
`cleanDb()` accepts an array of collection names to clear. Pass only the collections your tests touch:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
after(async () => {
|
|
292
|
+
await cleanDb(['content', 'users', 'authtokens'])
|
|
293
|
+
})
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The default collections (used when called with no arguments) are: `content`, `assets`, `courseassets`, `tags`, `adaptbuilds`, `contentplugins`.
|
|
297
|
+
|
|
298
|
+
> **Important:** Always include `contentplugins` when cleaning content-related data. Stale plugin records cause `MISSING_SCHEMA` errors on subsequent runs.
|
|
299
|
+
|
|
300
|
+
### Available modules
|
|
301
|
+
|
|
302
|
+
Modules are accessed by name via `getModule()`. Common modules used in tests:
|
|
303
|
+
|
|
304
|
+
| Name | Description |
|
|
305
|
+
|------|-------------|
|
|
306
|
+
| `content` | Content CRUD (`insert`, `find`, `update`, `delete`) |
|
|
307
|
+
| `auth` | Core authentication (token management, `disavowUser`) |
|
|
308
|
+
| `auth-local` | Local auth (`register`, `registerSuper`, `setUserEnabled`) |
|
|
309
|
+
| `users` | User management (`find`, `update`, `delete`) |
|
|
310
|
+
| `roles` | Role management (`find`, `getScopesForRole`) |
|
|
311
|
+
| `mongodb` | Raw database access (`find`, `insert`, `getCollection`) |
|
|
312
|
+
| `adaptframework` | Course import/build/export (`importCourse`, `buildCourse`) |
|
|
313
|
+
|
|
314
|
+
### Fixtures
|
|
315
|
+
|
|
316
|
+
Integration tests use a fixture system for test data such as course export zips.
|
|
317
|
+
|
|
318
|
+
Fixtures are declared in `fixtures/manifest.json`:
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
{
|
|
322
|
+
"course-export": "course-export.zip"
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Load a fixture in your test with `getFixture()`, which copies the file to a temp directory to preserve the original:
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
import { getFixture } from '../lib/fixtures.js'
|
|
330
|
+
|
|
331
|
+
it('should import a course', async () => {
|
|
332
|
+
const framework = await getModule('adaptframework')
|
|
333
|
+
const importer = await framework.importCourse({
|
|
334
|
+
importPath: await getFixture('course-export'),
|
|
335
|
+
userId: '000000000000000000000000',
|
|
336
|
+
tags: [],
|
|
337
|
+
importContent: true,
|
|
338
|
+
importPlugins: true,
|
|
339
|
+
migrateContent: true,
|
|
340
|
+
updatePlugins: false,
|
|
341
|
+
removeSource: false
|
|
342
|
+
})
|
|
343
|
+
assert.ok(importer.summary.courseId)
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Custom fixtures can be provided via the `CUSTOM_DIR` environment variable. Custom fixtures override built-in fixtures when keys collide.
|
|
348
|
+
|
|
349
|
+
### Writing assertions
|
|
350
|
+
|
|
351
|
+
Be specific with assertions. Check exact counts, validate required fields, and verify relationships — don't just check that something is truthy or non-empty:
|
|
352
|
+
|
|
353
|
+
```js
|
|
354
|
+
// Bad — only checks existence
|
|
355
|
+
const items = await content.find({ _courseId, _type: 'block' })
|
|
356
|
+
assert.ok(items.length > 0, 'should have blocks')
|
|
357
|
+
|
|
358
|
+
// Good — checks exact count and validates structure
|
|
359
|
+
const items = await content.find({ _courseId, _type: 'block' })
|
|
360
|
+
assert.equal(items.length, 23, 'should have 23 blocks')
|
|
361
|
+
for (const item of items) {
|
|
362
|
+
assert.ok(item.title, `block "${item._id}" should have a title`)
|
|
363
|
+
assert.ok(articleIds.has(item._parentId?.toString()),
|
|
364
|
+
`block "${item._id}" should have an article as parent`)
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
When validating content hierarchy, verify that parent references point to items of the correct type:
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
const blockIds = new Set(
|
|
372
|
+
(await content.find({ _courseId, _type: 'block' })).map(b => b._id.toString())
|
|
373
|
+
)
|
|
374
|
+
for (const component of components) {
|
|
375
|
+
assert.ok(blockIds.has(component._parentId?.toString()),
|
|
376
|
+
`component "${component._id}" should have a block as parent`)
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Error assertions
|
|
381
|
+
|
|
382
|
+
Use a predicate function with `assert.rejects` when errors may have different structures across modules:
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
await assert.rejects(
|
|
386
|
+
() => authLocal.registerSuper({ email: 'second@example.com', password }),
|
|
387
|
+
(err) => {
|
|
388
|
+
assert.ok(
|
|
389
|
+
err.code === 'SUPER_USER_EXISTS' || err.id === 'SUPER_USER_EXISTS',
|
|
390
|
+
`expected SUPER_USER_EXISTS, got: ${err.code || err.id || err.message}`
|
|
391
|
+
)
|
|
392
|
+
return true
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Running integration tests
|
|
398
|
+
|
|
399
|
+
From the adapt-authoring app directory:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
# run all integration tests
|
|
403
|
+
npx at-integration-test
|
|
404
|
+
|
|
405
|
+
# run specific test files
|
|
406
|
+
npx at-integration-test auth content
|
|
407
|
+
|
|
408
|
+
# include custom tests
|
|
409
|
+
CUSTOM_DIR=/path/to/custom npx at-integration-test
|
|
410
|
+
```
|
package/index.js
CHANGED
|
@@ -2,4 +2,4 @@ export { default as AbstractModule } from './lib/AbstractModule.js'
|
|
|
2
2
|
export { default as App } from './lib/App.js'
|
|
3
3
|
export { default as DependencyLoader } from './lib/DependencyLoader.js'
|
|
4
4
|
export { default as Hook } from './lib/Hook.js'
|
|
5
|
-
export {
|
|
5
|
+
export { metadataFileName, packageFileName, isObject, getArgs, spawn, readJson, writeJson, toBoolean, ensureDir, escapeRegExp } from './lib/Utils.js'
|
package/lib/App.js
CHANGED
|
@@ -2,7 +2,7 @@ import AbstractModule from './AbstractModule.js'
|
|
|
2
2
|
import DependencyLoader from './DependencyLoader.js'
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
|
-
import
|
|
5
|
+
import { metadataFileName, packageFileName, getArgs } from './Utils.js'
|
|
6
6
|
|
|
7
7
|
let instance
|
|
8
8
|
/**
|
|
@@ -27,8 +27,8 @@ class App extends AbstractModule {
|
|
|
27
27
|
/** @override */
|
|
28
28
|
constructor () {
|
|
29
29
|
const rootDir = process.env.ROOT_DIR ?? process.cwd()
|
|
30
|
-
const adaptJson = JSON.parse(fs.readFileSync(path.join(rootDir,
|
|
31
|
-
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir,
|
|
30
|
+
const adaptJson = JSON.parse(fs.readFileSync(path.join(rootDir, metadataFileName)))
|
|
31
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, packageFileName)))
|
|
32
32
|
super(null, { ...packageJson, ...adaptJson, name: 'adapt-authoring-core', rootDir })
|
|
33
33
|
this.git = this.getGitInfo()
|
|
34
34
|
}
|
|
@@ -39,7 +39,7 @@ class App extends AbstractModule {
|
|
|
39
39
|
* Reference to the passed arguments (parsed for easy reference)
|
|
40
40
|
* @type {Object}
|
|
41
41
|
*/
|
|
42
|
-
this.args =
|
|
42
|
+
this.args = getArgs()
|
|
43
43
|
/**
|
|
44
44
|
* Instance of App instance (required by all AbstractModules)
|
|
45
45
|
* @type {App}
|
package/lib/DependencyLoader.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'fs-extra'
|
|
|
4
4
|
import { glob } from 'glob'
|
|
5
5
|
import path from 'path'
|
|
6
6
|
import Hook from './Hook.js'
|
|
7
|
-
import
|
|
7
|
+
import { metadataFileName, packageFileName } from './Utils.js'
|
|
8
8
|
/**
|
|
9
9
|
* Handles the loading of Adapt authoring tool module dependencies.
|
|
10
10
|
* @memberof core
|
|
@@ -88,9 +88,9 @@ class DependencyLoader {
|
|
|
88
88
|
*/
|
|
89
89
|
async loadConfigs () {
|
|
90
90
|
/** @ignore */ this._configsLoaded = false
|
|
91
|
-
const files = await glob(`${this.app.rootDir}/node_modules/**/${
|
|
91
|
+
const files = await glob(`${this.app.rootDir}/node_modules/**/${metadataFileName}`)
|
|
92
92
|
const deps = files
|
|
93
|
-
.map(d => d.replace(`${
|
|
93
|
+
.map(d => d.replace(`${metadataFileName}`, ''))
|
|
94
94
|
.sort((a, b) => a.length < b.length ? -1 : 1)
|
|
95
95
|
|
|
96
96
|
const configCache = {}
|
|
@@ -125,8 +125,8 @@ class DependencyLoader {
|
|
|
125
125
|
*/
|
|
126
126
|
async loadModuleConfig (modDir) {
|
|
127
127
|
return {
|
|
128
|
-
...await fs.readJson(path.join(modDir,
|
|
129
|
-
...await fs.readJson(path.join(modDir,
|
|
128
|
+
...await fs.readJson(path.join(modDir, packageFileName)),
|
|
129
|
+
...await fs.readJson(path.join(modDir, metadataFileName)),
|
|
130
130
|
rootDir: modDir
|
|
131
131
|
}
|
|
132
132
|
}
|
package/lib/Utils.js
CHANGED
|
@@ -1,76 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* The name of the file used for defining Adapt authoring tool metadata
|
|
11
|
-
* @return {String}
|
|
12
|
-
*/
|
|
13
|
-
static get metadataFileName () {
|
|
14
|
-
return 'adapt-authoring.json'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The name of the Node.js package file
|
|
19
|
-
* @return {String}
|
|
20
|
-
*/
|
|
21
|
-
static get packageFileName () {
|
|
22
|
-
return 'package.json'
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Returns the passed arguments, parsed by minimist for easy access
|
|
27
|
-
* @return {Object} The parsed arguments
|
|
28
|
-
* @see {@link https://github.com/substack/minimist#readme}
|
|
29
|
-
*/
|
|
30
|
-
static getArgs () {
|
|
31
|
-
const args = minimist(process.argv)
|
|
32
|
-
args.params = args._.slice(2)
|
|
33
|
-
return args
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Determines if param is a Javascript object (note: returns false for arrays, functions and null)
|
|
38
|
-
* @return {Boolean}
|
|
39
|
-
*/
|
|
40
|
-
static isObject (o) {
|
|
41
|
-
return typeof o === 'object' && o !== null && !Array.isArray(o)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Reusable Promise-based spawn wrapper, which handles output/error handling
|
|
46
|
-
* @param {Object} options
|
|
47
|
-
* @param {String} options.cmd Command to run
|
|
48
|
-
* @param {String} options.cwd Current working directory
|
|
49
|
-
* @param {Array<String>} options.args
|
|
50
|
-
* @returns Promise
|
|
51
|
-
*/
|
|
52
|
-
static async spawn (options) {
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
if (!options.cwd) options.cwd = ''
|
|
55
|
-
App.instance.log('verbose', 'SPAWN', options)
|
|
56
|
-
const task = spawn(options.cmd, options.args ?? [], { cwd: options.cwd })
|
|
57
|
-
let stdout = ''
|
|
58
|
-
let stderr = ''
|
|
59
|
-
let error
|
|
60
|
-
task.stdout.on('data', data => {
|
|
61
|
-
stdout += data
|
|
62
|
-
})
|
|
63
|
-
task.stderr.on('data', data => {
|
|
64
|
-
stderr += data
|
|
65
|
-
})
|
|
66
|
-
task.on('error', e => {
|
|
67
|
-
error = e
|
|
68
|
-
})
|
|
69
|
-
task.on('close', exitCode => {
|
|
70
|
-
exitCode !== 0 ? reject(App.instance.errors.SPAWN.setData({ error: error ?? stderr ?? stdout })) : resolve(stdout)
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export default Utils
|
|
1
|
+
export { metadataFileName, packageFileName } from './utils/constants.js'
|
|
2
|
+
export { isObject } from './utils/isObject.js'
|
|
3
|
+
export { getArgs } from './utils/getArgs.js'
|
|
4
|
+
export { spawn } from './utils/spawn.js'
|
|
5
|
+
export { readJson } from './utils/readJson.js'
|
|
6
|
+
export { writeJson } from './utils/writeJson.js'
|
|
7
|
+
export { toBoolean } from './utils/toBoolean.js'
|
|
8
|
+
export { ensureDir } from './utils/ensureDir.js'
|
|
9
|
+
export { escapeRegExp } from './utils/escapeRegExp.js'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The name of the file used for defining Adapt authoring tool metadata
|
|
3
|
+
* @type {string}
|
|
4
|
+
*/
|
|
5
|
+
export const metadataFileName = 'adapt-authoring.json'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The name of the Node.js package file
|
|
9
|
+
* @type {string}
|
|
10
|
+
*/
|
|
11
|
+
export const packageFileName = 'package.json'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures a directory exists, creating it recursively if needed.
|
|
5
|
+
* @param {string} dir - Absolute path to the directory
|
|
6
|
+
* @returns {Promise<void>}
|
|
7
|
+
*/
|
|
8
|
+
export async function ensureDir (dir) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.mkdir(dir, { recursive: true })
|
|
11
|
+
} catch (e) {
|
|
12
|
+
if (e.code !== 'EEXIST') throw e
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import minimist from 'minimist'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the passed arguments, parsed by minimist for easy access
|
|
5
|
+
* @return {Object} The parsed arguments
|
|
6
|
+
* @see {@link https://github.com/substack/minimist#readme}
|
|
7
|
+
*/
|
|
8
|
+
export function getArgs () {
|
|
9
|
+
const args = minimist(process.argv)
|
|
10
|
+
args.params = args._.slice(2)
|
|
11
|
+
return args
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reads and parses a JSON file.
|
|
5
|
+
* @param {string} filepath - Absolute path to the JSON file
|
|
6
|
+
* @returns {Promise<Object>}
|
|
7
|
+
*/
|
|
8
|
+
export async function readJson (filepath) {
|
|
9
|
+
return JSON.parse(await fs.readFile(filepath))
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import App from '../App.js'
|
|
2
|
+
import { spawn as nodeSpawn } from 'child_process'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reusable Promise-based spawn wrapper, which handles output/error handling
|
|
6
|
+
* @param {Object} options
|
|
7
|
+
* @param {String} options.cmd Command to run
|
|
8
|
+
* @param {String} options.cwd Current working directory
|
|
9
|
+
* @param {Array<String>} options.args
|
|
10
|
+
* @returns Promise
|
|
11
|
+
*/
|
|
12
|
+
export async function spawn (options) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
if (!options.cwd) options.cwd = ''
|
|
15
|
+
App.instance.log('verbose', 'SPAWN', options)
|
|
16
|
+
const task = nodeSpawn(options.cmd, options.args ?? [], { cwd: options.cwd })
|
|
17
|
+
let stdout = ''
|
|
18
|
+
let stderr = ''
|
|
19
|
+
let error
|
|
20
|
+
task.stdout.on('data', data => {
|
|
21
|
+
stdout += data
|
|
22
|
+
})
|
|
23
|
+
task.stderr.on('data', data => {
|
|
24
|
+
stderr += data
|
|
25
|
+
})
|
|
26
|
+
task.on('error', e => {
|
|
27
|
+
error = e
|
|
28
|
+
})
|
|
29
|
+
task.on('close', exitCode => {
|
|
30
|
+
exitCode !== 0 ? reject(App.instance.errors.SPAWN.setData({ error: error ?? stderr ?? stdout })) : resolve(stdout)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a value to a boolean. Returns `true` only for `true` or `"true"`.
|
|
3
|
+
* Returns `undefined` if the value is `undefined`.
|
|
4
|
+
* @param {*} val
|
|
5
|
+
* @returns {boolean|undefined}
|
|
6
|
+
*/
|
|
7
|
+
export function toBoolean (val) {
|
|
8
|
+
if (val !== undefined) return val === true || val === 'true'
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Writes data as formatted JSON to a file.
|
|
5
|
+
* @param {string} filepath - Absolute path to the JSON file
|
|
6
|
+
* @param {*} data - Data to serialise
|
|
7
|
+
* @returns {Promise<void>}
|
|
8
|
+
*/
|
|
9
|
+
export function writeJson (filepath, data) {
|
|
10
|
+
return fs.writeFile(filepath, JSON.stringify(data, null, 2))
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { metadataFileName, packageFileName } from '../lib/utils/constants.js'
|
|
5
|
+
|
|
6
|
+
describe('metadataFileName', () => {
|
|
7
|
+
it('should be adapt-authoring.json', () => {
|
|
8
|
+
assert.equal(metadataFileName, 'adapt-authoring.json')
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('packageFileName', () => {
|
|
13
|
+
it('should be package.json', () => {
|
|
14
|
+
assert.equal(packageFileName, 'package.json')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
|
|
7
|
+
import { ensureDir } from '../lib/utils/ensureDir.js'
|
|
8
|
+
|
|
9
|
+
describe('ensureDir()', () => {
|
|
10
|
+
it('should create a directory that does not exist', async () => {
|
|
11
|
+
const tmpDir = path.join(os.tmpdir(), `ensureDir-test-${Date.now()}`)
|
|
12
|
+
try {
|
|
13
|
+
await ensureDir(tmpDir)
|
|
14
|
+
const stat = await fs.stat(tmpDir)
|
|
15
|
+
assert.ok(stat.isDirectory())
|
|
16
|
+
} finally {
|
|
17
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should create nested directories recursively', async () => {
|
|
22
|
+
const tmpDir = path.join(os.tmpdir(), `ensureDir-nested-${Date.now()}`, 'a', 'b', 'c')
|
|
23
|
+
try {
|
|
24
|
+
await ensureDir(tmpDir)
|
|
25
|
+
const stat = await fs.stat(tmpDir)
|
|
26
|
+
assert.ok(stat.isDirectory())
|
|
27
|
+
} finally {
|
|
28
|
+
await fs.rm(path.join(os.tmpdir(), `ensureDir-nested-${Date.now()}`), { recursive: true, force: true }).catch(() => {})
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should not throw if the directory already exists', async () => {
|
|
33
|
+
const tmpDir = path.join(os.tmpdir(), `ensureDir-exists-${Date.now()}`)
|
|
34
|
+
try {
|
|
35
|
+
await fs.mkdir(tmpDir, { recursive: true })
|
|
36
|
+
await assert.doesNotReject(() => ensureDir(tmpDir))
|
|
37
|
+
} finally {
|
|
38
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { escapeRegExp } from '../lib/utils/escapeRegExp.js'
|
|
5
|
+
|
|
6
|
+
describe('escapeRegExp()', () => {
|
|
7
|
+
it('should escape dots', () => {
|
|
8
|
+
assert.equal(escapeRegExp('file.js'), 'file\\.js')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should escape asterisks', () => {
|
|
12
|
+
assert.equal(escapeRegExp('a*b'), 'a\\*b')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should escape plus signs', () => {
|
|
16
|
+
assert.equal(escapeRegExp('a+b'), 'a\\+b')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should escape question marks', () => {
|
|
20
|
+
assert.equal(escapeRegExp('a?b'), 'a\\?b')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should escape parentheses and pipe', () => {
|
|
24
|
+
assert.equal(escapeRegExp('(a|b)'), '\\(a\\|b\\)')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should escape square brackets', () => {
|
|
28
|
+
assert.equal(escapeRegExp('[abc]'), '\\[abc\\]')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should escape curly braces', () => {
|
|
32
|
+
assert.equal(escapeRegExp('{1,2}'), '\\{1,2\\}')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should escape caret and dollar', () => {
|
|
36
|
+
assert.equal(escapeRegExp('^start$'), '\\^start\\$')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should escape backslashes', () => {
|
|
40
|
+
assert.equal(escapeRegExp('a\\b'), 'a\\\\b')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should escape hyphens', () => {
|
|
44
|
+
assert.equal(escapeRegExp('a-b'), 'a\\-b')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should return plain strings unchanged', () => {
|
|
48
|
+
assert.equal(escapeRegExp('hello'), 'hello')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should escape all special characters', () => {
|
|
52
|
+
const special = '.*+\\-?^${}()|[]'
|
|
53
|
+
const escaped = escapeRegExp(special)
|
|
54
|
+
const re = new RegExp(escaped)
|
|
55
|
+
assert.ok(re.test(special))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should produce a string usable in RegExp', () => {
|
|
59
|
+
const input = 'file.name+(v2)[1].js'
|
|
60
|
+
const escaped = escapeRegExp(input)
|
|
61
|
+
const regex = new RegExp(escaped)
|
|
62
|
+
assert.ok(regex.test(input))
|
|
63
|
+
assert.ok(!regex.test('filexname'))
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { getArgs } from '../lib/utils/getArgs.js'
|
|
5
|
+
|
|
6
|
+
describe('getArgs()', () => {
|
|
7
|
+
it('should return an object with parsed arguments', () => {
|
|
8
|
+
const args = getArgs()
|
|
9
|
+
assert.equal(typeof args, 'object')
|
|
10
|
+
assert.ok(Array.isArray(args.params))
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should include the underscore array from minimist', () => {
|
|
14
|
+
const args = getArgs()
|
|
15
|
+
assert.ok(Array.isArray(args._))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should derive params by slicing first two entries from _', () => {
|
|
19
|
+
const args = getArgs()
|
|
20
|
+
assert.deepEqual(args.params, args._.slice(2))
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { isObject } from '../lib/utils/isObject.js'
|
|
5
|
+
|
|
6
|
+
describe('isObject()', () => {
|
|
7
|
+
const validObjects = [
|
|
8
|
+
{ value: {}, label: 'empty object' },
|
|
9
|
+
{ value: { key: 'value' }, label: 'object with properties' },
|
|
10
|
+
{ value: { nested: { key: 'value' } }, label: 'nested object' }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
validObjects.forEach(({ value, label }) => {
|
|
14
|
+
it(`should return true for ${label}`, () => {
|
|
15
|
+
assert.equal(isObject(value), true)
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const objectLikeButValid = [
|
|
20
|
+
{ value: new Date(), label: 'Date instance' },
|
|
21
|
+
{ value: /regex/, label: 'RegExp instance' }
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
objectLikeButValid.forEach(({ value, label }) => {
|
|
25
|
+
it(`should return true for ${label}`, () => {
|
|
26
|
+
assert.equal(isObject(value), true)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const invalidObjects = [
|
|
31
|
+
{ value: null, label: 'null' },
|
|
32
|
+
{ value: [], label: 'empty array' },
|
|
33
|
+
{ value: [1, 2, 3], label: 'array with values' },
|
|
34
|
+
{ value: 'string', label: 'string' },
|
|
35
|
+
{ value: 123, label: 'number' },
|
|
36
|
+
{ value: true, label: 'boolean' },
|
|
37
|
+
{ value: undefined, label: 'undefined' },
|
|
38
|
+
{ value: () => {}, label: 'function' },
|
|
39
|
+
{ value: 0, label: 'zero' },
|
|
40
|
+
{ value: '', label: 'empty string' },
|
|
41
|
+
{ value: NaN, label: 'NaN' }
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
invalidObjects.forEach(({ value, label }) => {
|
|
45
|
+
it(`should return false for ${label}`, () => {
|
|
46
|
+
assert.equal(isObject(value), false)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
|
|
7
|
+
import { readJson } from '../lib/utils/readJson.js'
|
|
8
|
+
|
|
9
|
+
describe('readJson()', () => {
|
|
10
|
+
it('should read and parse a valid JSON file', async () => {
|
|
11
|
+
const tmpFile = path.join(os.tmpdir(), `readJson-test-${Date.now()}.json`)
|
|
12
|
+
await fs.writeFile(tmpFile, JSON.stringify({ name: 'test', version: '1.0.0' }))
|
|
13
|
+
try {
|
|
14
|
+
const result = await readJson(tmpFile)
|
|
15
|
+
assert.equal(result.name, 'test')
|
|
16
|
+
assert.equal(result.version, '1.0.0')
|
|
17
|
+
} finally {
|
|
18
|
+
await fs.unlink(tmpFile)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should handle nested JSON structures', async () => {
|
|
23
|
+
const tmpFile = path.join(os.tmpdir(), `readJson-nested-${Date.now()}.json`)
|
|
24
|
+
const data = { a: { b: [1, 2, 3] } }
|
|
25
|
+
await fs.writeFile(tmpFile, JSON.stringify(data))
|
|
26
|
+
try {
|
|
27
|
+
const result = await readJson(tmpFile)
|
|
28
|
+
assert.deepEqual(result, data)
|
|
29
|
+
} finally {
|
|
30
|
+
await fs.unlink(tmpFile)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should throw on non-existent file', async () => {
|
|
35
|
+
await assert.rejects(
|
|
36
|
+
() => readJson('/tmp/does-not-exist-readjson.json'),
|
|
37
|
+
{ code: 'ENOENT' }
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should throw on invalid JSON', async () => {
|
|
42
|
+
const tmpFile = path.join(os.tmpdir(), `readJson-invalid-${Date.now()}.json`)
|
|
43
|
+
await fs.writeFile(tmpFile, '{ not valid json }')
|
|
44
|
+
try {
|
|
45
|
+
await assert.rejects(
|
|
46
|
+
() => readJson(tmpFile),
|
|
47
|
+
SyntaxError
|
|
48
|
+
)
|
|
49
|
+
} finally {
|
|
50
|
+
await fs.unlink(tmpFile)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import App from '../lib/App.js'
|
|
4
|
+
|
|
5
|
+
mock.getter(App, 'instance', () => ({
|
|
6
|
+
log () {},
|
|
7
|
+
errors: {
|
|
8
|
+
SPAWN: {
|
|
9
|
+
setData (data) {
|
|
10
|
+
const e = new Error('SPAWN')
|
|
11
|
+
e.data = data
|
|
12
|
+
return e
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
const { spawn } = await import('../lib/utils/spawn.js')
|
|
19
|
+
|
|
20
|
+
describe('spawn()', () => {
|
|
21
|
+
it('should resolve with stdout on success', async () => {
|
|
22
|
+
const result = await spawn({ cmd: 'echo', args: ['hello'] })
|
|
23
|
+
assert.equal(result.trim(), 'hello')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should resolve with empty string when command produces no output', async () => {
|
|
27
|
+
const result = await spawn({ cmd: 'true' })
|
|
28
|
+
assert.equal(result, '')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should reject with SPAWN error on non-zero exit code', async () => {
|
|
32
|
+
await assert.rejects(
|
|
33
|
+
() => spawn({ cmd: 'false' }),
|
|
34
|
+
(err) => {
|
|
35
|
+
assert.equal(err.message, 'SPAWN')
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should reject with stderr data on failure', async () => {
|
|
42
|
+
await assert.rejects(
|
|
43
|
+
() => spawn({ cmd: 'node', args: ['-e', 'process.stderr.write("oops"); process.exit(1)'] }),
|
|
44
|
+
(err) => {
|
|
45
|
+
assert.equal(err.message, 'SPAWN')
|
|
46
|
+
assert.equal(err.data.error, 'oops')
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should default cwd to empty string when not provided', async () => {
|
|
53
|
+
const result = await spawn({ cmd: 'echo', args: ['test'] })
|
|
54
|
+
assert.equal(result.trim(), 'test')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should use provided cwd', async () => {
|
|
58
|
+
const result = await spawn({ cmd: 'pwd', cwd: '/tmp' })
|
|
59
|
+
assert.match(result.trim(), /tmp/)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should pass args to the command', async () => {
|
|
63
|
+
const result = await spawn({ cmd: 'echo', args: ['-n', 'no newline'] })
|
|
64
|
+
assert.equal(result, 'no newline')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should reject with error event data for invalid commands', async () => {
|
|
68
|
+
await assert.rejects(
|
|
69
|
+
() => spawn({ cmd: 'nonexistent-command-that-does-not-exist' }),
|
|
70
|
+
(err) => {
|
|
71
|
+
assert.equal(err.message, 'SPAWN')
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { toBoolean } from '../lib/utils/toBoolean.js'
|
|
5
|
+
|
|
6
|
+
describe('toBoolean()', () => {
|
|
7
|
+
it('should return true for true', () => {
|
|
8
|
+
assert.equal(toBoolean(true), true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should return true for "true"', () => {
|
|
12
|
+
assert.equal(toBoolean('true'), true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should return false for false', () => {
|
|
16
|
+
assert.equal(toBoolean(false), false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should return false for "false"', () => {
|
|
20
|
+
assert.equal(toBoolean('false'), false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should return false for 0', () => {
|
|
24
|
+
assert.equal(toBoolean(0), false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should return false for an empty string', () => {
|
|
28
|
+
assert.equal(toBoolean(''), false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should return false for null', () => {
|
|
32
|
+
assert.equal(toBoolean(null), false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should return undefined for undefined', () => {
|
|
36
|
+
assert.equal(toBoolean(undefined), undefined)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should return false for a non-"true" string', () => {
|
|
40
|
+
assert.equal(toBoolean('yes'), false)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
|
|
7
|
+
import { writeJson } from '../lib/utils/writeJson.js'
|
|
8
|
+
|
|
9
|
+
describe('writeJson()', () => {
|
|
10
|
+
it('should write formatted JSON to a file', async () => {
|
|
11
|
+
const tmpFile = path.join(os.tmpdir(), `writeJson-test-${Date.now()}.json`)
|
|
12
|
+
const data = { name: 'test', items: [1, 2, 3] }
|
|
13
|
+
try {
|
|
14
|
+
await writeJson(tmpFile, data)
|
|
15
|
+
const content = await fs.readFile(tmpFile, 'utf-8')
|
|
16
|
+
assert.equal(content, JSON.stringify(data, null, 2))
|
|
17
|
+
} finally {
|
|
18
|
+
await fs.unlink(tmpFile).catch(() => {})
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should overwrite an existing file', async () => {
|
|
23
|
+
const tmpFile = path.join(os.tmpdir(), `writeJson-overwrite-${Date.now()}.json`)
|
|
24
|
+
try {
|
|
25
|
+
await writeJson(tmpFile, { old: true })
|
|
26
|
+
await writeJson(tmpFile, { new: true })
|
|
27
|
+
const content = JSON.parse(await fs.readFile(tmpFile, 'utf-8'))
|
|
28
|
+
assert.equal(content.new, true)
|
|
29
|
+
assert.equal(content.old, undefined)
|
|
30
|
+
} finally {
|
|
31
|
+
await fs.unlink(tmpFile).catch(() => {})
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
})
|
package/tests/Utils.spec.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import Utils from '../lib/Utils.js'
|
|
4
|
-
|
|
5
|
-
describe('Utils', () => {
|
|
6
|
-
describe('.metadataFileName', () => {
|
|
7
|
-
it('should return the metadata file name', () => {
|
|
8
|
-
assert.equal(Utils.metadataFileName, 'adapt-authoring.json')
|
|
9
|
-
})
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
describe('.packageFileName', () => {
|
|
13
|
-
it('should return the package file name', () => {
|
|
14
|
-
assert.equal(Utils.packageFileName, 'package.json')
|
|
15
|
-
})
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('.getArgs()', () => {
|
|
19
|
-
it('should return an object with parsed arguments', () => {
|
|
20
|
-
const args = Utils.getArgs()
|
|
21
|
-
assert.equal(typeof args, 'object')
|
|
22
|
-
assert.ok(Array.isArray(args.params))
|
|
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
|
-
})
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
describe('.isObject()', () => {
|
|
37
|
-
const validObjects = [
|
|
38
|
-
{ value: {}, label: 'empty object' },
|
|
39
|
-
{ value: { key: 'value' }, label: 'object with properties' },
|
|
40
|
-
{ value: { nested: { key: 'value' } }, label: 'nested object' }
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
validObjects.forEach(({ value, label }) => {
|
|
44
|
-
it(`should return true for ${label}`, () => {
|
|
45
|
-
assert.equal(Utils.isObject(value), true)
|
|
46
|
-
})
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
const objectLikeButValid = [
|
|
50
|
-
{ value: new Date(), label: 'Date instance' },
|
|
51
|
-
{ value: /regex/, label: 'RegExp instance' }
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
objectLikeButValid.forEach(({ value, label }) => {
|
|
55
|
-
it(`should return true for ${label}`, () => {
|
|
56
|
-
assert.equal(Utils.isObject(value), true)
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
const invalidObjects = [
|
|
61
|
-
{ value: null, label: 'null' },
|
|
62
|
-
{ value: [], label: 'empty array' },
|
|
63
|
-
{ value: [1, 2, 3], label: 'array with values' },
|
|
64
|
-
{ value: 'string', label: 'string' },
|
|
65
|
-
{ value: 123, label: 'number' },
|
|
66
|
-
{ value: true, label: 'boolean' },
|
|
67
|
-
{ value: undefined, label: 'undefined' },
|
|
68
|
-
{ value: () => {}, label: 'function' },
|
|
69
|
-
{ value: 0, label: 'zero' },
|
|
70
|
-
{ value: '', label: 'empty string' },
|
|
71
|
-
{ value: NaN, label: 'NaN' }
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
invalidObjects.forEach(({ value, label }) => {
|
|
75
|
-
it(`should return false for ${label}`, () => {
|
|
76
|
-
assert.equal(Utils.isObject(value), false)
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
})
|