diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-09 20:16:41 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-09 20:16:41 +0300 |
| commit | 8aa495df3b3da9213bdf29d7a15c1473fefcc63b (patch) | |
| tree | fff04a324527e08b75a9667dd4e81b5a07a0ea0d /web/js/tests/admin-rescan.test.js | |
| parent | c68965ac297f6b4f0da4ad541e921646c320c489 (diff) | |
s1 add media rescan hotkey and progress indicator
Diffstat (limited to 'web/js/tests/admin-rescan.test.js')
| -rw-r--r-- | web/js/tests/admin-rescan.test.js | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/web/js/tests/admin-rescan.test.js b/web/js/tests/admin-rescan.test.js new file mode 100644 index 0000000..f1661db --- /dev/null +++ b/web/js/tests/admin-rescan.test.js @@ -0,0 +1,156 @@ +import { initKeyboard } from '../keyboard.js'; +import { renderScanProgress, triggerRescan } from '../views/admin-status.js'; + +const failures = []; +const requests = []; +let keydownHandler = null; +let rescanStatus = 200; + +function assert(cond, msg) { + if (!cond) failures.push(msg || 'assertion failed'); +} + +const indicator = mockElement(); +const indicatorText = mockElement(); +const toastEl = { + className: '', + textContent: '', + classList: { remove() {} }, +}; + +globalThis.document = { + addEventListener(type, handler) { + if (type === 'keydown') keydownHandler = handler; + }, + getElementById(id) { + if (id === 'scan-indicator') return indicator; + if (id === 'scan-indicator-text') return indicatorText; + if (id === 'toast') return toastEl; + return null; + }, +}; +globalThis.location = { pathname: '/', href: '' }; +globalThis.setTimeout = () => 0; + +globalThis.fetch = async (url, options = {}) => { + requests.push({ url: String(url), method: options.method || 'GET' }); + if (String(url) === '/api/admin/rescan') { + if (rescanStatus !== 200) { + return jsonResponse({ error: 'admin only' }, rescanStatus); + } + return jsonResponse({ status: 'ok' }); + } + if (String(url) === '/api/admin/scan-progress') { + return jsonResponse({ + running: true, + current_set: 'movies', + sets_total: 2, + sets_done: 1, + files_total: 9, + files_done: 4, + }); + } + return jsonResponse({}); +}; + +function jsonResponse(body, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + }); +} + +function mockElement() { + return { + textContent: '', + hidden: true, + classList: { + add(name) { + if (name === 'hidden') this.hidden = true; + }, + remove(name) { + if (name === 'hidden') this.hidden = false; + }, + hidden: true, + }, + }; +} + +function pressKey(key) { + let prevented = false; + keydownHandler?.({ + key, + code: `Key${key.toUpperCase()}`, + target: { tagName: 'BODY', isContentEditable: false }, + preventDefault() { prevented = true; }, + }); + return prevented; +} + +function testKeyboardRescanHandler() { + let rescans = 0; + initKeyboard({ rescanMedia: () => { rescans += 1; } }); + + const prevented = pressKey('M'); + + assert(prevented, 'M should prevent default browser handling'); + assert(rescans === 1, 'M should call the rescan handler'); +} + +function testRenderRunningProgress() { + renderScanProgress({ + running: true, + current_set: 'music', + sets_total: 3, + sets_done: 2, + files_total: 20, + files_done: 7, + }); + + assert(indicator.classList.hidden === false, 'running progress should show the indicator'); + assert(indicatorText.textContent === 'Scanning music 2/3 sets, 7/20 files', 'running progress text should include set and file counts'); +} + +function testRenderIdleProgressHidesIndicator() { + renderScanProgress({ running: false }); + + assert(indicator.classList.hidden === true, 'idle progress should hide the indicator'); +} + +async function testTriggerRescanRefreshesProgress() { + requests.length = 0; + rescanStatus = 200; + await triggerRescan(); + + assert(requests[0]?.url === '/api/admin/rescan', 'trigger should call rescan endpoint first'); + assert(requests[0]?.method === 'POST', 'trigger should post to rescan endpoint'); + assert(requests[1]?.url === '/api/admin/scan-progress', 'trigger should refresh progress after starting scan'); + assert(indicator.classList.hidden === false, 'trigger should show refreshed running progress'); +} + +async function testTriggerRescanErrorDoesNotPollProgress() { + requests.length = 0; + toastEl.textContent = ''; + rescanStatus = 403; + + await triggerRescan(); + + assert(requests.length === 1, 'failed trigger should not poll scan progress'); + assert(toastEl.textContent === 'admin only', 'failed trigger should show the API error'); +} + +console.log('Running admin rescan frontend tests...'); +testKeyboardRescanHandler(); +testRenderRunningProgress(); +testRenderIdleProgressHidesIndicator(); +await testTriggerRescanRefreshesProgress(); +await testTriggerRescanErrorDoesNotPollProgress(); + +if (failures.length) { + console.error('FAILURES:'); + failures.forEach((m) => console.error(' - ' + m)); + process.exit(1); +} else { + console.log('All admin rescan frontend tests passed.'); + process.exit(0); +} |
