adapt-authoring-ui 1.8.6 → 1.9.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/.github/workflows/tests.yml +15 -0
- package/lib/UiBuild.js +1 -1
- package/package.json +3 -2
- package/tests/CacheManager.spec.js +170 -0
- package/tests/JavaScriptTask.spec.js +247 -0
- package/tests/UiBuild.spec.js +51 -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/UiBuild.js
CHANGED
|
@@ -60,7 +60,7 @@ class UiBuild {
|
|
|
60
60
|
this.preBuildHook = new Hook()
|
|
61
61
|
this.postBuildHook = new Hook()
|
|
62
62
|
|
|
63
|
-
this.jsTask = new JavaScriptTask(this.Paths.Output, this.log, this.uiPlugins, this.app.getConfig('tempDir'))
|
|
63
|
+
this.jsTask = new JavaScriptTask(this.Paths.Output, this.log, this.uiPlugins, path.join(this.app.getConfig('tempDir'), 'ui-build-cache'))
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
collate (collateAtFolderName, destFolder, srcFileName) {
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Front-end application for the Adapt authoring tool",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-ui",
|
|
6
6
|
"license": "GPL-3.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"postinstall": "node npm_hooks/postinstall.js"
|
|
10
|
+
"postinstall": "node npm_hooks/postinstall.js",
|
|
11
|
+
"test": "node --test 'tests/**/*.spec.js'"
|
|
11
12
|
},
|
|
12
13
|
"repository": "github:adapt-security/adapt-authoring-ui",
|
|
13
14
|
"dependencies": {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import crypto from 'crypto'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import fs from 'fs-extra'
|
|
7
|
+
import CacheManager from '../lib/CacheManager.js'
|
|
8
|
+
|
|
9
|
+
describe('CacheManager', () => {
|
|
10
|
+
describe('constructor', () => {
|
|
11
|
+
it('should set maxAge to the provided value', () => {
|
|
12
|
+
const cm = new CacheManager({ maxAge: 1000, logger: { log () {} } })
|
|
13
|
+
assert.equal(cm.maxAge, 1000)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should use ONE_WEEK as default maxAge', () => {
|
|
17
|
+
const ONE_WEEK = 7 * 24 * 60 * 60 * 1000
|
|
18
|
+
const cm = new CacheManager({ logger: { log () {} } })
|
|
19
|
+
assert.equal(cm.maxAge, ONE_WEEK)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should set the provided logger', () => {
|
|
23
|
+
const logger = { log () {} }
|
|
24
|
+
const cm = new CacheManager({ logger })
|
|
25
|
+
assert.equal(cm.logger, logger)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should use a default logger when none is provided', () => {
|
|
29
|
+
const cm = new CacheManager()
|
|
30
|
+
assert.equal(typeof cm.logger.log, 'function')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should use provided tempDir', () => {
|
|
34
|
+
const tempDir = path.join(os.tmpdir(), 'adapt-authoring-test-custom')
|
|
35
|
+
const cm = new CacheManager({ tempDir, logger: { log () {} } })
|
|
36
|
+
assert.equal(cm.tempDir, tempDir)
|
|
37
|
+
// Cleanup
|
|
38
|
+
try { fs.rmdirSync(tempDir) } catch (e) {}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should use default tempDir when none is provided', () => {
|
|
42
|
+
const cm = new CacheManager({ logger: { log () {} } })
|
|
43
|
+
assert.equal(cm.tempDir, path.join(os.tmpdir(), 'adapt-authoring'))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should ensure tempDir directory exists', () => {
|
|
47
|
+
const cm = new CacheManager({ logger: { log () {} } })
|
|
48
|
+
assert.ok(fs.existsSync(cm.tempDir))
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('static hash', () => {
|
|
53
|
+
it('should return a SHA1 hex digest of the input', () => {
|
|
54
|
+
const input = '/some/path/to/file'
|
|
55
|
+
const expected = crypto
|
|
56
|
+
.createHash('sha1')
|
|
57
|
+
.update(input, 'utf8')
|
|
58
|
+
.digest('hex')
|
|
59
|
+
assert.equal(CacheManager.hash(input), expected)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should return different hashes for different inputs', () => {
|
|
63
|
+
const hash1 = CacheManager.hash('/path/one')
|
|
64
|
+
const hash2 = CacheManager.hash('/path/two')
|
|
65
|
+
assert.notEqual(hash1, hash2)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should return the same hash for the same input', () => {
|
|
69
|
+
const hash1 = CacheManager.hash('/same/path')
|
|
70
|
+
const hash2 = CacheManager.hash('/same/path')
|
|
71
|
+
assert.equal(hash1, hash2)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should return a 40-character hex string', () => {
|
|
75
|
+
const hash = CacheManager.hash('test')
|
|
76
|
+
assert.match(hash, /^[0-9a-f]{40}$/)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('cachePath', () => {
|
|
81
|
+
it('should return a .cache file path under tempDir', () => {
|
|
82
|
+
const cm = new CacheManager({ logger: { log () {} } })
|
|
83
|
+
const result = cm.cachePath('/base', '/output')
|
|
84
|
+
assert.ok(result.startsWith(cm.tempDir))
|
|
85
|
+
assert.ok(result.endsWith('.cache'))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should incorporate both basePath and outputFilePath into the hash', () => {
|
|
89
|
+
const cm = new CacheManager({ logger: { log () {} } })
|
|
90
|
+
const result1 = cm.cachePath('/base1', '/output')
|
|
91
|
+
const result2 = cm.cachePath('/base2', '/output')
|
|
92
|
+
assert.notEqual(result1, result2)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should use process.cwd() as default outputFilePath', () => {
|
|
96
|
+
const cm = new CacheManager({ logger: { log () {} } })
|
|
97
|
+
const expectedHash = CacheManager.hash(path.join('/base', process.cwd()))
|
|
98
|
+
const expected = path.join(cm.tempDir, `${expectedHash}.cache`)
|
|
99
|
+
assert.equal(cm.cachePath('/base'), expected)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should produce a deterministic result for same inputs', () => {
|
|
103
|
+
const cm = new CacheManager({ logger: { log () {} } })
|
|
104
|
+
const result1 = cm.cachePath('/base', '/output')
|
|
105
|
+
const result2 = cm.cachePath('/base', '/output')
|
|
106
|
+
assert.equal(result1, result2)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('checkFilePath', () => {
|
|
111
|
+
it('should return a last.touch file path under tempDir', () => {
|
|
112
|
+
const cm = new CacheManager({ logger: { log () {} } })
|
|
113
|
+
assert.equal(cm.checkFilePath, path.join(cm.tempDir, 'last.touch'))
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('isCleaningTime', () => {
|
|
118
|
+
let cm
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
cm = new CacheManager({ maxAge: 1000, logger: { log () {} } })
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should return true when checkFile does not exist', async () => {
|
|
125
|
+
const checkPath = cm.checkFilePath
|
|
126
|
+
try { fs.unlinkSync(checkPath) } catch (e) {}
|
|
127
|
+
const result = await cm.isCleaningTime()
|
|
128
|
+
assert.equal(result, true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should return false when checkFile was just created', async () => {
|
|
132
|
+
const checkPath = cm.checkFilePath
|
|
133
|
+
fs.writeFileSync(checkPath, String(Date.now()))
|
|
134
|
+
const result = await cm.isCleaningTime()
|
|
135
|
+
assert.equal(result, false)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('clean', () => {
|
|
140
|
+
let cm
|
|
141
|
+
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
// Use a large maxAge so the checkFile is not treated as expired
|
|
144
|
+
cm = new CacheManager({ maxAge: 7 * 24 * 60 * 60 * 1000, logger: { log () {} } })
|
|
145
|
+
fs.ensureDirSync(cm.tempDir)
|
|
146
|
+
// Remove checkFile to ensure cleaning happens
|
|
147
|
+
try { fs.unlinkSync(cm.checkFilePath) } catch (e) {}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should create checkFile after cleaning', async () => {
|
|
151
|
+
await cm.clean()
|
|
152
|
+
assert.ok(fs.existsSync(cm.checkFilePath))
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should not clean when it is not cleaning time', async () => {
|
|
156
|
+
// First write the checkFile so it appears fresh
|
|
157
|
+
fs.writeFileSync(cm.checkFilePath, String(Date.now()))
|
|
158
|
+
// Set a long maxAge so checkInterval is large
|
|
159
|
+
cm.maxAge = 7 * 24 * 60 * 60 * 1000
|
|
160
|
+
// Create a test file
|
|
161
|
+
const testFile = path.join(cm.tempDir, 'test-no-clean.cache')
|
|
162
|
+
fs.writeFileSync(testFile, 'data')
|
|
163
|
+
await cm.clean()
|
|
164
|
+
// File should still exist since it is not cleaning time
|
|
165
|
+
assert.ok(fs.existsSync(testFile))
|
|
166
|
+
// Cleanup
|
|
167
|
+
try { fs.unlinkSync(testFile) } catch (e) {}
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
})
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* JavaScriptTask depends on rollup, babel, glob, fs-extra and other heavy
|
|
7
|
+
* build dependencies. We test the pure utility methods in isolation by
|
|
8
|
+
* reimplementing them with the same logic.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reimplementation of JavaScriptTask.prototype.escapeRegExp for isolated testing.
|
|
13
|
+
*/
|
|
14
|
+
function escapeRegExp (string) {
|
|
15
|
+
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('JavaScriptTask', () => {
|
|
19
|
+
describe('escapeRegExp', () => {
|
|
20
|
+
it('should escape dots', () => {
|
|
21
|
+
assert.equal(escapeRegExp('file.js'), 'file\\.js')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should escape asterisks', () => {
|
|
25
|
+
assert.equal(escapeRegExp('**/*.js'), '\\*\\*/\\*\\.js')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should escape plus signs', () => {
|
|
29
|
+
assert.equal(escapeRegExp('a+b'), 'a\\+b')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should escape question marks', () => {
|
|
33
|
+
assert.equal(escapeRegExp('file?.js'), 'file\\?\\.js')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should escape caret', () => {
|
|
37
|
+
assert.equal(escapeRegExp('^start'), '\\^start')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should escape dollar sign', () => {
|
|
41
|
+
assert.equal(escapeRegExp('end$'), 'end\\$')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should escape curly braces', () => {
|
|
45
|
+
assert.equal(escapeRegExp('{a,b}'), '\\{a,b\\}')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should escape parentheses', () => {
|
|
49
|
+
assert.equal(escapeRegExp('(group)'), '\\(group\\)')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should escape pipe', () => {
|
|
53
|
+
assert.equal(escapeRegExp('a|b'), 'a\\|b')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should escape square brackets', () => {
|
|
57
|
+
assert.equal(escapeRegExp('[abc]'), '\\[abc\\]')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should escape backslashes', () => {
|
|
61
|
+
assert.equal(escapeRegExp('path\\to'), 'path\\\\to')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should escape hyphen/minus', () => {
|
|
65
|
+
assert.equal(escapeRegExp('a-b'), 'a\\-b')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should return the same string if no special chars', () => {
|
|
69
|
+
assert.equal(escapeRegExp('hello world'), 'hello world')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should handle empty string', () => {
|
|
73
|
+
assert.equal(escapeRegExp(''), '')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should handle multiple special characters together', () => {
|
|
77
|
+
const input = 'C:\\Users\\test\\file.js'
|
|
78
|
+
const expected = 'C:\\\\Users\\\\test\\\\file\\.js'
|
|
79
|
+
assert.equal(escapeRegExp(input), expected)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should produce a string usable in new RegExp without error', () => {
|
|
83
|
+
const special = 'path/to/file.*(test)+[0]'
|
|
84
|
+
const escaped = escapeRegExp(special)
|
|
85
|
+
assert.doesNotThrow(() => new RegExp(escaped))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should produce a regex that matches the original string literally', () => {
|
|
89
|
+
const special = 'hello.world+foo*bar'
|
|
90
|
+
const escaped = escapeRegExp(special)
|
|
91
|
+
const regex = new RegExp(escaped)
|
|
92
|
+
assert.ok(regex.test(special))
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('checkCache', () => {
|
|
97
|
+
/**
|
|
98
|
+
* Reimplementation of JavaScriptTask.prototype.checkCache for isolated testing.
|
|
99
|
+
* Uses a context object instead of `this`.
|
|
100
|
+
*/
|
|
101
|
+
function checkCache (context, invalidate) {
|
|
102
|
+
if (!context.cache) return
|
|
103
|
+
const idHash = {}
|
|
104
|
+
const missing = {}
|
|
105
|
+
context.cache.modules.forEach(mod => {
|
|
106
|
+
const moduleId = mod.id
|
|
107
|
+
const isRollupHelper = (moduleId[0] === '\u0000')
|
|
108
|
+
if (isRollupHelper) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
if (!fs.existsSync(moduleId)) {
|
|
112
|
+
context.log('error', `Cache missing file: ${moduleId.replace(context.cwd, '')}`)
|
|
113
|
+
missing[moduleId] = true
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
if (invalidate && invalidate.includes(moduleId)) {
|
|
117
|
+
context.log('debug', `Cache skipping file: ${moduleId.replace(context.cwd, '')}`)
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
idHash[moduleId] = mod
|
|
121
|
+
return true
|
|
122
|
+
})
|
|
123
|
+
if (Object.keys(missing).length) {
|
|
124
|
+
context.cache = null
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
context.cache.modules = Object.values(idHash)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
it('should return early when cache is null', () => {
|
|
131
|
+
const context = { cache: null, log () {} }
|
|
132
|
+
checkCache(context, [])
|
|
133
|
+
assert.equal(context.cache, null)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should clear cache when a module file is missing', () => {
|
|
137
|
+
const context = {
|
|
138
|
+
cache: {
|
|
139
|
+
modules: [
|
|
140
|
+
{ id: '/nonexistent/path/to/module.js' }
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
cwd: process.cwd() + '/',
|
|
144
|
+
log () {}
|
|
145
|
+
}
|
|
146
|
+
checkCache(context, [])
|
|
147
|
+
assert.equal(context.cache, null)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should skip rollup helper modules (prefixed with null char)', () => {
|
|
151
|
+
// Create a real file so the non-helper module passes the existsSync check
|
|
152
|
+
const existingFile = import.meta.url.replace('file://', '').replace(/\/[^/]+$/, '/JavaScriptTask.spec.js')
|
|
153
|
+
const context = {
|
|
154
|
+
cache: {
|
|
155
|
+
modules: [
|
|
156
|
+
{ id: '\u0000rollupHelper' },
|
|
157
|
+
{ id: existingFile }
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
cwd: '',
|
|
161
|
+
log () {}
|
|
162
|
+
}
|
|
163
|
+
checkCache(context, [])
|
|
164
|
+
// Cache should not be null since the real file exists
|
|
165
|
+
assert.notEqual(context.cache, null)
|
|
166
|
+
// Only the real file should remain (rollup helper is skipped, not added to idHash)
|
|
167
|
+
assert.equal(context.cache.modules.length, 1)
|
|
168
|
+
assert.equal(context.cache.modules[0].id, existingFile)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should remove invalidated modules from cache', () => {
|
|
172
|
+
const existingFile = import.meta.url.replace('file://', '').replace(/\/[^/]+$/, '/JavaScriptTask.spec.js')
|
|
173
|
+
const context = {
|
|
174
|
+
cache: {
|
|
175
|
+
modules: [
|
|
176
|
+
{ id: existingFile }
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
cwd: '',
|
|
180
|
+
log () {}
|
|
181
|
+
}
|
|
182
|
+
checkCache(context, [existingFile])
|
|
183
|
+
assert.notEqual(context.cache, null)
|
|
184
|
+
assert.equal(context.cache.modules.length, 0)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should keep valid non-invalidated modules', () => {
|
|
188
|
+
const existingFile = import.meta.url.replace('file://', '').replace(/\/[^/]+$/, '/JavaScriptTask.spec.js')
|
|
189
|
+
const context = {
|
|
190
|
+
cache: {
|
|
191
|
+
modules: [
|
|
192
|
+
{ id: existingFile }
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
cwd: '',
|
|
196
|
+
log () {}
|
|
197
|
+
}
|
|
198
|
+
checkCache(context, [])
|
|
199
|
+
assert.notEqual(context.cache, null)
|
|
200
|
+
assert.equal(context.cache.modules.length, 1)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('logPrettyError', () => {
|
|
205
|
+
it('should handle errors with loc and babel plugin', () => {
|
|
206
|
+
const err = new Error('babel: Unexpected token\n 1 | code here\n | ^')
|
|
207
|
+
err.loc = { line: 1, column: 5 }
|
|
208
|
+
err.plugin = 'babel'
|
|
209
|
+
err.id = '/Users/test/project/src/file.js'
|
|
210
|
+
|
|
211
|
+
// Extract babel error handling logic from logPrettyError
|
|
212
|
+
err.frame = err.message.substr(err.message.indexOf('\n') + 1)
|
|
213
|
+
err.message = err.message.substr(0, err.message.indexOf('\n')).slice(2).replace(/^([^:]*): /, '')
|
|
214
|
+
|
|
215
|
+
assert.equal(err.message, 'Unexpected token')
|
|
216
|
+
assert.ok(err.frame.includes('code here'))
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should handle errors without loc property', () => {
|
|
220
|
+
const err = new Error('Generic build error')
|
|
221
|
+
const hasLoc = Boolean(err.loc)
|
|
222
|
+
assert.equal(hasLoc, false)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('should handle errors with loc but non-babel plugin', () => {
|
|
226
|
+
const logged = []
|
|
227
|
+
const log = (level, ...args) => logged.push({ level, args })
|
|
228
|
+
const err = new Error('Some plugin error')
|
|
229
|
+
err.loc = { line: 5, column: 10 }
|
|
230
|
+
err.plugin = 'other-plugin'
|
|
231
|
+
err.id = '/test/src/file.js'
|
|
232
|
+
|
|
233
|
+
// Non-babel plugin path logs err.toString() in the default case
|
|
234
|
+
let hasOutput = false
|
|
235
|
+
switch (err.plugin) {
|
|
236
|
+
case 'babel':
|
|
237
|
+
break
|
|
238
|
+
default:
|
|
239
|
+
hasOutput = true
|
|
240
|
+
log('error', err.toString())
|
|
241
|
+
}
|
|
242
|
+
assert.equal(hasOutput, true)
|
|
243
|
+
assert.equal(logged.length, 1)
|
|
244
|
+
assert.equal(logged[0].level, 'error')
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import path from 'upath'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* UiBuild depends on the full app context (adapt-authoring-core Hook, app object,
|
|
7
|
+
* gaze, rollup, etc.). We extract and test the pure utility methods in isolation
|
|
8
|
+
* by reimplementing them with the same logic as UiBuild.prototype methods.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reimplementation of UiBuild.prototype.collate for isolated testing.
|
|
13
|
+
* Computes an output path from a collateAtFolderName, destFolder, and srcFileName.
|
|
14
|
+
*/
|
|
15
|
+
function collate (collateAtFolderName, destFolder, srcFileName) {
|
|
16
|
+
const nameParts = srcFileName.split('/')
|
|
17
|
+
if (nameParts[nameParts.length - 1] === collateAtFolderName) {
|
|
18
|
+
return destFolder
|
|
19
|
+
}
|
|
20
|
+
const startOfCollatePath = srcFileName.indexOf(collateAtFolderName) + collateAtFolderName.length + 1
|
|
21
|
+
return path.join(destFolder, srcFileName.substr(startOfCollatePath))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('UiBuild', () => {
|
|
25
|
+
describe('collate', () => {
|
|
26
|
+
it('should return destFolder when srcFileName ends with collateAtFolderName', () => {
|
|
27
|
+
const result = collate('assets', '/output/assets', 'some/path/assets')
|
|
28
|
+
assert.equal(result, '/output/assets')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should extract the path after collateAtFolderName and join with destFolder', () => {
|
|
32
|
+
const result = collate('assets', '/output/assets', 'some/path/assets/images/logo.png')
|
|
33
|
+
assert.equal(result, '/output/assets/images/logo.png')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should handle nested folder structures correctly', () => {
|
|
37
|
+
const result = collate('required', '/build', 'app/core/required/config.json')
|
|
38
|
+
assert.equal(result, '/build/config.json')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should handle deeply nested paths after collateAtFolderName', () => {
|
|
42
|
+
const result = collate('libraries', '/out/libs', 'app/libraries/vendor/jquery/jquery.min.js')
|
|
43
|
+
assert.equal(result, '/out/libs/vendor/jquery/jquery.min.js')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should handle single file after collateAtFolderName', () => {
|
|
47
|
+
const result = collate('assets', '/dest', 'module/assets/file.txt')
|
|
48
|
+
assert.equal(result, '/dest/file.txt')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|