test: add JS test framework using Node built-in test runner
Extracted pure utility functions (esc, sanitize, formatBytes, formatTime) into web/ui/util.js with CommonJS export for Node testing compatibility. Added 19 tests across 4 suites using node:test + node:assert (zero deps). - util.js: browser-compatible utility functions - util.test.js: 19 tests (5 esc, 3 sanitize, 6 formatBytes, 5 formatTime) - package.json: 'test' script → node --test util.test.js - mise.toml: js-test task - app.js: removed duplicate helper functions (now in util.js) - index.html: load util.js before app.js
This commit was merged in pull request #14.
This commit is contained in:
@@ -13,6 +13,7 @@ all = { run = "go build -buildvcs=false -o fetch . && make -C third_party/akaiut
|
||||
serve = { run = "go build -buildvcs=false -o fetch . && ./fetch serve" }
|
||||
lint = "golangci-lint run"
|
||||
js-lint = "cd web/ui && npm run lint"
|
||||
js-test = "cd web/ui && npm test"
|
||||
electron-deps = "cd electron && npm install"
|
||||
electron-dev = { run = "go build -buildvcs=false -o fetch . && cd electron && npx electron ." }
|
||||
electron-build = { run = "go build -buildvcs=false -o fetch . && make -C third_party/akaiutil && cd electron && npx electron-builder --dir" }
|
||||
|
||||
@@ -307,12 +307,6 @@ function updateProgress() {
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(secs) {
|
||||
const m = Math.floor(secs / 60);
|
||||
const s = Math.floor(secs % 60);
|
||||
return m + ':' + s.toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
function setActiveItem(filename) {
|
||||
document.querySelectorAll('.wav-item').forEach(el => {
|
||||
el.classList.toggle('playing', el.dataset.file === filename);
|
||||
@@ -325,26 +319,6 @@ function clearActiveItem() {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
function esc(s) {
|
||||
if (!s) return '';
|
||||
return s.replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function sanitize(s) {
|
||||
return s.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Initial load
|
||||
doSearch();
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
<div id="search-results"></div>
|
||||
</section>
|
||||
</div>
|
||||
<script src="util.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Web UI for AKAI Utils",
|
||||
"scripts": {
|
||||
"lint": "eslint *.js"
|
||||
"lint": "eslint *.js",
|
||||
"test": "node --test util.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.0.0"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// AKAI Utils — shared utility functions
|
||||
|
||||
function esc(s) {
|
||||
if (!s) return ''
|
||||
return s.replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function sanitize(s) {
|
||||
return s.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
function formatTime(secs) {
|
||||
const m = Math.floor(secs / 60)
|
||||
const s = Math.floor(secs % 60)
|
||||
return m + ':' + s.toString().padStart(2, '0')
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { esc, sanitize, formatBytes, formatTime }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
const { describe, it } = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const { esc, sanitize, formatBytes, formatTime } = require('./util.js')
|
||||
|
||||
describe('esc', () => {
|
||||
it('escapes HTML entities', () => {
|
||||
assert.strictEqual(esc('<script>alert("xss")</script>'),
|
||||
'<script>alert("xss")</script>')
|
||||
})
|
||||
|
||||
it('handles ampersands', () => {
|
||||
assert.strictEqual(esc('a & b'), 'a & b')
|
||||
})
|
||||
|
||||
it('handles single quotes', () => {
|
||||
assert.strictEqual(esc("it's"), 'it's')
|
||||
})
|
||||
|
||||
it('returns empty string for falsy input', () => {
|
||||
assert.strictEqual(esc(''), '')
|
||||
assert.strictEqual(esc(null), '')
|
||||
assert.strictEqual(esc(undefined), '')
|
||||
})
|
||||
|
||||
it('passes through safe strings', () => {
|
||||
assert.strictEqual(esc('Hello World'), 'Hello World')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sanitize', () => {
|
||||
it('replaces special characters with underscores', () => {
|
||||
assert.strictEqual(sanitize('hello world!'), 'hello_world_')
|
||||
})
|
||||
|
||||
it('preserves alphanumeric and dashes', () => {
|
||||
assert.strictEqual(sanitize('test-123_abc'), 'test-123_abc')
|
||||
})
|
||||
|
||||
it('sanitizes dots and slashes', () => {
|
||||
assert.strictEqual(sanitize('path/to/file.name'),
|
||||
'path_to_file_name')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('formats zero', () => {
|
||||
assert.strictEqual(formatBytes(0), '0 B')
|
||||
})
|
||||
|
||||
it('formats bytes', () => {
|
||||
assert.strictEqual(formatBytes(500), '500 B')
|
||||
})
|
||||
|
||||
it('formats KB', () => {
|
||||
assert.strictEqual(formatBytes(1024), '1 KB')
|
||||
})
|
||||
|
||||
it('formats MB', () => {
|
||||
assert.strictEqual(formatBytes(1048576), '1 MB')
|
||||
})
|
||||
|
||||
it('formats GB', () => {
|
||||
assert.strictEqual(formatBytes(1073741824), '1 GB')
|
||||
})
|
||||
|
||||
it('formats fractional', () => {
|
||||
assert.strictEqual(formatBytes(1536), '1.5 KB')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatTime', () => {
|
||||
it('formats zero', () => {
|
||||
assert.strictEqual(formatTime(0), '0:00')
|
||||
})
|
||||
|
||||
it('formats seconds', () => {
|
||||
assert.strictEqual(formatTime(5), '0:05')
|
||||
})
|
||||
|
||||
it('formats minutes and seconds', () => {
|
||||
assert.strictEqual(formatTime(65), '1:05')
|
||||
})
|
||||
|
||||
it('formats double-digit seconds', () => {
|
||||
assert.strictEqual(formatTime(75), '1:15')
|
||||
})
|
||||
|
||||
it('formats large times', () => {
|
||||
assert.strictEqual(formatTime(3661), '61:01')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user