diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-17 15:25:52 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-17 15:25:52 +0300 |
| commit | 914bd7cd6aa14e839332a98d91c30b19865b0cf2 (patch) | |
| tree | 02b11537237a204048589e14ed24938b805e42f7 /web/js/tests | |
| parent | 3b24f0e1be832584d6550e8cc3e5d24329f66f90 (diff) | |
Restructure repo: move Go server into player-server/
Diffstat (limited to 'web/js/tests')
| -rw-r--r-- | web/js/tests/admin-rescan.test.js | 178 | ||||
| -rw-r--r-- | web/js/tests/keyboard-enter.test.js | 75 | ||||
| -rw-r--r-- | web/js/tests/media-pagination.test.js | 56 | ||||
| -rw-r--r-- | web/js/tests/playback-resume.test.js | 51 | ||||
| -rw-r--r-- | web/js/tests/podcast-episodes-api.test.js | 74 | ||||
| -rw-r--r-- | web/js/tests/progress-actions.test.js | 120 | ||||
| -rw-r--r-- | web/js/tests/search-parser.test.js | 33 | ||||
| -rw-r--r-- | web/js/tests/shuffle.test.js | 165 |
8 files changed, 0 insertions, 752 deletions
diff --git a/web/js/tests/admin-rescan.test.js b/web/js/tests/admin-rescan.test.js deleted file mode 100644 index 2ce5f1b..0000000 --- a/web/js/tests/admin-rescan.test.js +++ /dev/null @@ -1,178 +0,0 @@ -import { initKeyboard } from '../keyboard.js'; -import { renderScanProgress, triggerRescan } from '../views/admin-status.js'; -import { readFileSync } from 'node:fs'; - -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'); -} - -function testScanIndicatorOutsideHeader() { - const html = readFileSync(new URL('../../index.html', import.meta.url), 'utf8'); - const header = html.slice(html.indexOf('<header'), html.indexOf('</header>')); - const afterHeader = html.slice(html.indexOf('</header>')); - - assert(!header.includes('id="scan-indicator"'), 'scan indicator should not be inside the collapsible header'); - assert(afterHeader.includes('id="scan-indicator"'), 'scan indicator should remain in the document after the header'); -} - -function testScanIndicatorAboveModalOverlay() { - const css = readFileSync(new URL('../../css/layout.css', import.meta.url), 'utf8'); - const scanRule = css.match(/\.scan-indicator\s*\{[^}]*z-index:\s*(\d+)/); - const modalRule = css.match(/\.modal-overlay\s*\{[^}]*z-index:\s*(\d+)/); - - assert(scanRule, 'scan indicator should define a z-index'); - assert(modalRule, 'modal overlay should define a z-index'); - assert(Number(scanRule?.[1]) > Number(modalRule?.[1]), 'scan indicator should render above open admin modal overlay'); -} - -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(); -testScanIndicatorOutsideHeader(); -testScanIndicatorAboveModalOverlay(); -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); -} diff --git a/web/js/tests/keyboard-enter.test.js b/web/js/tests/keyboard-enter.test.js deleted file mode 100644 index 434f50c..0000000 --- a/web/js/tests/keyboard-enter.test.js +++ /dev/null @@ -1,75 +0,0 @@ -import { initKeyboard } from '../keyboard.js'; - -const failures = []; -let keydownHandler = null; - -globalThis.document = { - addEventListener(type, handler) { - if (type === 'keydown') keydownHandler = handler; - }, -}; - -function assert(cond, msg) { - if (!cond) failures.push(msg || 'assertion failed'); -} - -function pressKey(key, target = { tagName: 'BODY', isContentEditable: false }) { - let prevented = false; - keydownHandler?.({ - key, - code: key.length === 1 ? `Key${key.toUpperCase()}` : key, - target, - preventDefault() { prevented = true; }, - }); - return prevented; -} - -function testEnterActivatesGridHandler() { - let entered = 0; - initKeyboard({ enter: () => { entered += 1; } }); - - const prevented = pressKey('Enter', { tagName: 'DIV', isContentEditable: false }); - - assert(entered === 1, 'Enter on a grid/card target should call the enter handler'); - assert(prevented, 'Enter on a grid/card target should prevent native default handling'); -} - -function testEnterLeavesFocusedButtonNative() { - let entered = 0; - initKeyboard({ enter: () => { entered += 1; } }); - - const prevented = pressKey('Enter', { tagName: 'BUTTON', isContentEditable: false }); - - assert(entered === 0, 'Enter on a focused button should not also call the global enter handler'); - assert(!prevented, 'Enter on a focused button should leave native button activation alone'); -} - -function testEscapeStillWorksOnNativeControl() { - let escaped = 0; - let blurred = 0; - initKeyboard({ escape: () => { escaped += 1; } }); - - const prevented = pressKey('Escape', { - tagName: 'BUTTON', - isContentEditable: false, - blur() { blurred += 1; }, - }); - - assert(escaped === 1, 'Escape on a focused button should call escape handler'); - assert(blurred === 1, 'Escape on a focused button should blur the button'); - assert(!prevented, 'Escape on a focused button should preserve existing keyboard behavior'); -} - -console.log('Running keyboard Enter tests...'); -testEnterActivatesGridHandler(); -testEnterLeavesFocusedButtonNative(); -testEscapeStillWorksOnNativeControl(); - -if (failures.length) { - console.error('FAILURES:'); - failures.forEach((m) => console.error(' - ' + m)); - process.exit(1); -} else { - console.log('All keyboard Enter tests passed.'); - process.exit(0); -} diff --git a/web/js/tests/media-pagination.test.js b/web/js/tests/media-pagination.test.js deleted file mode 100644 index 0d599fc..0000000 --- a/web/js/tests/media-pagination.test.js +++ /dev/null @@ -1,56 +0,0 @@ -import { paginateItems, setMediaPageSize } from '../views/media-grid.js'; -import { state } from '../state.js'; - -const failures = []; - -function assert(cond, msg) { - if (!cond) failures.push(msg || 'assertion failed'); -} - -function items(count) { - return Array.from({ length: count }, (_, i) => i + 1); -} - -function testDefaultPageSize() { - setMediaPageSize(undefined); - const first = paginateItems(items(101), 0); - assert(first.items.length === 100, 'default first page should contain 100 items'); - assert(first.hasNext, 'default first page should have next'); - assert(!first.hasPrev, 'default first page should not have previous'); - - const second = paginateItems(items(101), 1); - assert(second.items.length === 1, 'default second page should contain remaining item'); - assert(second.hasPrev, 'default second page should have previous'); - assert(!second.hasNext, 'default second page should not have next'); -} - -function testConfiguredPageSize() { - state.mediaPage = 2; - setMediaPageSize(25); - const page = paginateItems(items(60), 1); - assert(state.mediaPage === 0, 'changing page size should reset current media page'); - assert(page.items.length === 25, 'configured page should contain configured number of items'); - assert(page.start === 25, 'configured second page should start at item offset 25'); - assert(page.end === 50, 'configured second page should end at item offset 50'); -} - -function testClampsOutOfRangePage() { - setMediaPageSize(25); - const page = paginateItems(items(60), 99); - assert(page.page === 2, 'page should clamp to the last page'); - assert(page.items.length === 10, 'last page should contain remaining items'); -} - -console.log('Running media pagination tests...'); -testDefaultPageSize(); -testConfiguredPageSize(); -testClampsOutOfRangePage(); - -if (failures.length) { - console.error('FAILURES:'); - failures.forEach((m) => console.error(' - ' + m)); - process.exit(1); -} else { - console.log('All media pagination tests passed.'); - process.exit(0); -} diff --git a/web/js/tests/playback-resume.test.js b/web/js/tests/playback-resume.test.js deleted file mode 100644 index 2c9d199..0000000 --- a/web/js/tests/playback-resume.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import { API } from '../api.js'; -import { state } from '../state.js'; - -// --- Minimal test harness for browser module validation --- -const failures = []; -function assert(cond, msg) { - if (!cond) failures.push(msg || 'assertion failed'); -} - -// Mock fetch and DOM for headless validation -const mockDetail = { - media: { id: 7, file_name: 'song.mp3', type: 'audio', duration: 180 }, - progress: { user_id: 1, media_id: 7, position_seconds: 42.5, updated_at: new Date().toISOString() } -}; - -// We can't run the real module in Node without DOM, so we test the JSON shape contract instead. -function testDetailShape() { - assert(mockDetail.media.id === 7, 'media.id should exist'); - assert(mockDetail.progress.position_seconds === 42.5, 'progress.position_seconds should be 42.5'); -} - -function testResumeFromComputation() { - const detailWithProgress = { progress: { position_seconds: 99 } }; - const detailWithout = { progress: null }; - const resumeFrom = detailWithProgress.progress ? detailWithProgress.progress.position_seconds : 0; - assert(resumeFrom === 99, 'resumeFrom should be 99 when progress exists'); - const resumeFromNone = detailWithout.progress ? detailWithout.progress.position_seconds : 0; - assert(resumeFromNone === 0, 'resumeFrom should be 0 when no progress'); -} - -function testListItemShape() { - const item = { id: 1, file_name: 'a.mp4', type: 'video', duration: 120 }; - assert(item.id === 1, 'list item id'); - assert(item.file_name === 'a.mp4', 'list item file_name'); - assert(!('resume_from' in item), 'list item should not have resume_from'); -} - -// Run tests -console.log('Running playback resume frontend contract tests...'); -testDetailShape(); -testResumeFromComputation(); -testListItemShape(); - -if (failures.length) { - console.error('FAILURES:'); - failures.forEach((m) => console.error(' - ' + m)); - process.exit(1); -} else { - console.log('All frontend contract tests passed.'); - process.exit(0); -} diff --git a/web/js/tests/podcast-episodes-api.test.js b/web/js/tests/podcast-episodes-api.test.js deleted file mode 100644 index d46d782..0000000 --- a/web/js/tests/podcast-episodes-api.test.js +++ /dev/null @@ -1,74 +0,0 @@ -import { API } from '../api.js'; - -const failures = []; -const requested = []; - -function assert(cond, msg) { - if (!cond) failures.push(msg || 'assertion failed'); -} - -globalThis.fetch = async (url) => { - requested.push(url); - return new Response('[]', { - status: 200, - headers: { 'content-type': 'application/json' }, - }); -}; - -globalThis.location = { pathname: '/', href: '' }; - -async function capturesUrl(fn) { - requested.length = 0; - await fn(); - return requested[0]; -} - -function paramsFrom(url) { - return new URL(url, 'http://player.test').searchParams; -} - -async function testDefaults() { - const params = paramsFrom(await capturesUrl(() => API.podcastEpisodes(5))); - assert(params.get('limit') === '50', 'default limit should be 50'); - assert(params.get('offset') === '0', 'default offset should be 0'); -} - -async function testSecondArgumentIsOffset() { - const params = paramsFrom(await capturesUrl(() => API.podcastEpisodes(5, 0))); - assert(params.get('limit') === '50', 'single numeric argument should keep default limit'); - assert(params.get('offset') === '0', 'single numeric argument should set explicit offset'); -} - -async function testOptionsObject() { - const params = paramsFrom(await capturesUrl(() => API.podcastEpisodes(5, { limit: 25, offset: 75 }))); - assert(params.get('limit') === '25', 'options object should set limit'); - assert(params.get('offset') === '75', 'options object should set offset'); -} - -async function testOptionsObjectKeepsZeroLimit() { - const params = paramsFrom(await capturesUrl(() => API.podcastEpisodes(5, { limit: 0 }))); - assert(params.get('limit') === '0', 'options object should preserve explicit zero limit'); - assert(params.get('offset') === '0', 'options object should default offset to 0'); -} - -async function testLegacyLimitOffset() { - const params = paramsFrom(await capturesUrl(() => API.podcastEpisodes(5, 10, 20))); - assert(params.get('limit') === '10', 'legacy third-argument form should preserve limit'); - assert(params.get('offset') === '20', 'legacy third-argument form should preserve offset'); -} - -console.log('Running podcast episodes API pagination tests...'); -await testDefaults(); -await testSecondArgumentIsOffset(); -await testOptionsObject(); -await testOptionsObjectKeepsZeroLimit(); -await testLegacyLimitOffset(); - -if (failures.length) { - console.error('FAILURES:'); - failures.forEach((m) => console.error(' - ' + m)); - process.exit(1); -} else { - console.log('All podcast episodes API pagination tests passed.'); - process.exit(0); -} diff --git a/web/js/tests/progress-actions.test.js b/web/js/tests/progress-actions.test.js deleted file mode 100644 index 1694ea3..0000000 --- a/web/js/tests/progress-actions.test.js +++ /dev/null @@ -1,120 +0,0 @@ -import { API } from '../api.js'; -import { state } from '../state.js'; -import { initMediaGrid, loadMedia } from '../views/media-grid.js'; - -const failures = []; -const requests = []; - -const grid = mockElement(); -const emptyHint = mockElement(); -const breadcrumb = mockElement(); -const resultCount = mockElement(); - -globalThis.location = { pathname: '/index.html', href: '' }; -globalThis.document = { - getElementById(id) { - if (id === 'media-grid') return grid; - if (id === 'empty-hint') return emptyHint; - if (id === 'breadcrumb-bar') return breadcrumb; - if (id === 'result-count') return resultCount; - return null; - }, - querySelectorAll() { return []; }, -}; - -globalThis.fetch = async (url, options = {}) => { - requests.push({ url: String(url), options }); - if (String(url) === '/api/in-progress') { - return jsonResponse([ - { - id: 7, - file_name: 'resume.mp4', - type: 'video', - duration: 125, - file_size_bytes: 2048, - thumbnail_path: 'thumbs/resume.jpg', - }, - ]); - } - return jsonResponse({ status: 'ok' }); -}; - -function assert(cond, msg) { - if (!cond) failures.push(msg || 'assertion failed'); -} - -function mockElement() { - return { - classList: { - add() {}, - remove() {}, - toggle() {}, - contains() { return false; }, - }, - innerHTML: '', - textContent: '', - addEventListener() {}, - querySelectorAll() { return []; }, - querySelector() { return null; }, - }; -} - -function jsonResponse(body) { - return { - ok: true, - status: 200, - headers: { get: () => 'application/json' }, - json: async () => body, - }; -} - -async function testProgressStatusAPIWrapper() { - requests.length = 0; - await API.progressStatus(7, 'finished'); - const req = requests[0]; - assert(req?.url === '/api/progress/status', 'progressStatus should call the progress status endpoint'); - assert(req?.options?.method === 'POST', 'progressStatus should POST'); - assert( - req?.options?.body === JSON.stringify({ media_id: 7, status: 'finished' }), - 'progressStatus should send media_id and status', - ); -} - -async function testInProgressAPIWrapper() { - requests.length = 0; - const list = await API.inProgress(); - assert(requests[0]?.url === '/api/in-progress', 'inProgress should call the in-progress endpoint'); - assert(Array.isArray(list), 'inProgress should return the decoded list'); -} - -async function testInProgressVirtualGrid() { - requests.length = 0; - state.virtualSet = 'in-progress'; - state.selectedSetId = null; - state.selectedSetIds = []; - state.folderPath = ''; - state.mediaPage = 0; - initMediaGrid(); - - await loadMedia(); - - assert(requests[0]?.url === '/api/in-progress', 'virtual set should load via API.inProgress'); - assert(state.media.length === 1 && state.media[0].id === 7, 'virtual set should store flat media results'); - assert(grid.innerHTML.includes('data-action="mark-finished"'), 'cards should render a finished action'); - assert(grid.innerHTML.includes('data-action="mark-not-started"'), 'cards should render a not-started action'); - assert(resultCount.textContent === '1 items', 'result count should reflect in-progress media'); -} - -console.log('Running progress action tests...'); -await testProgressStatusAPIWrapper(); -await testInProgressAPIWrapper(); -await testInProgressVirtualGrid(); - -if (failures.length) { - console.error('FAILURES:'); - failures.forEach((m) => console.error(' - ' + m)); - process.exit(1); -} else { - console.log('All progress action tests passed.'); - process.exit(0); -} diff --git a/web/js/tests/search-parser.test.js b/web/js/tests/search-parser.test.js deleted file mode 100644 index 8b8e287..0000000 --- a/web/js/tests/search-parser.test.js +++ /dev/null @@ -1,33 +0,0 @@ -import { parseQuery } from '../search.js'; - -const failures = []; - -function assert(cond, msg) { - if (!cond) failures.push(msg || 'assertion failed'); -} - -function testSetFilter() { - const parsed = parseQuery('set:yoga'); - assert(parsed.set === 'yoga', 'set token should parse set name'); - assert(parsed.search === '', 'set token should not become text search'); -} - -function testQuotedSetFilter() { - const parsed = parseQuery('set:"Yoga Flow" type:video morning'); - assert(parsed.set === 'Yoga Flow', 'quoted set token should preserve spaces'); - assert(parsed.type === 'video', 'other filters should still parse'); - assert(parsed.search === 'morning', 'free text should still parse'); -} - -console.log('Running search parser tests...'); -testSetFilter(); -testQuotedSetFilter(); - -if (failures.length) { - console.error('FAILURES:'); - failures.forEach((m) => console.error(' - ' + m)); - process.exit(1); -} else { - console.log('All search parser tests passed.'); - process.exit(0); -} diff --git a/web/js/tests/shuffle.test.js b/web/js/tests/shuffle.test.js deleted file mode 100644 index 2472543..0000000 --- a/web/js/tests/shuffle.test.js +++ /dev/null @@ -1,165 +0,0 @@ -import { initKeyboard } from '../keyboard.js'; -import { enable, isOn, revision, toggle } from '../shuffle.js'; -import { state } from '../state.js'; -import { initMediaGrid, loadMedia } from '../views/media-grid.js'; - -const failures = []; -const requests = []; -let keydownHandler = null; - -const button = { - active: false, - classList: { - toggle(name, enabled) { - if (name === 'active') button.active = enabled; - }, - }, -}; -const grid = mockElement(); -const emptyHint = mockElement(); -const breadcrumb = mockElement(); -const resultCount = mockElement(); - -globalThis.document = { - activeElement: null, - fullscreenElement: null, - addEventListener(type, handler) { - if (type === 'keydown') keydownHandler = handler; - }, - getElementById(id) { - if (id === 'shuffle-toggle') return button; - if (id === 'media-grid') return grid; - if (id === 'empty-hint') return emptyHint; - if (id === 'breadcrumb-bar') return breadcrumb; - if (id === 'result-count') return resultCount; - return null; - }, -}; -globalThis.location = { pathname: '/index.html', href: '' }; -globalThis.fetch = async (url) => { - requests.push(String(url)); - return { - ok: true, - status: 200, - headers: { get: () => 'application/json' }, - json: async () => [], - }; -}; - -function assert(cond, msg) { - if (!cond) failures.push(msg || 'assertion failed'); -} - -function mockElement() { - return { - classList: { - add() {}, - remove() {}, - toggle() {}, - contains() { return false; }, - }, - innerHTML: '', - textContent: '', - addEventListener() {}, - querySelectorAll() { return []; }, - querySelector() { return null; }, - }; -} - -function testEnableTurnsShuffleOn() { - enable(); - assert(isOn(), 'enable should turn shuffle on'); - assert(button.active, 'enable should update the shuffle button active state'); -} - -function testRepeatedEnableKeepsShuffleOnAndAdvancesRevision() { - const before = revision(); - enable(); - assert(isOn(), 'repeated enable should keep shuffle on'); - assert(revision() > before, 'repeated enable should advance revision for a fresh random load'); -} - -function testToggleCanStillTurnShuffleOff() { - toggle(); - assert(!isOn(), 'toggle should still turn shuffle off for the toolbar button'); - assert(!button.active, 'toggle should update the shuffle button inactive state'); -} - -async function testKeyboardReshuffleRequestsRandomMediaWithNewRevision() { - state.selectedSetId = null; - state.selectedSetIds = []; - state.folderPath = ''; - state.mediaPage = 0; - Object.assign(state.filters, { - type: '', - search: '', - favorites: false, - tags: '', - sort: '', - minDuration: '', - maxDuration: '', - minFileSize: '', - maxFileSize: '', - }); - requests.length = 0; - - initMediaGrid({ - isShuffle: isOn, - shuffleRevision: revision, - }); - initKeyboard({ - shuffle: () => { - enable(); - loadMedia(); - }, - }); - - pressKey('r'); - await waitForRequests(1); - pressKey('r'); - await waitForRequests(2); - - const first = new URL(requests[0], 'http://player.test'); - const second = new URL(requests[1], 'http://player.test'); - assert(first.pathname === '/api/media', 'keyboard shuffle should request media'); - assert(first.searchParams.get('sort') === 'random', 'keyboard shuffle should request random sort'); - assert(second.searchParams.get('sort') === 'random', 'keyboard reshuffle should keep random sort'); - assert(first.searchParams.get('shuffle_revision'), 'keyboard shuffle should include shuffle revision'); - assert(second.searchParams.get('shuffle_revision'), 'keyboard reshuffle should include shuffle revision'); - assert( - second.searchParams.get('shuffle_revision') !== first.searchParams.get('shuffle_revision'), - 'keyboard reshuffle should change shuffle revision in the media request', - ); -} - -function pressKey(key) { - keydownHandler?.({ - key, - code: `Key${key.toUpperCase()}`, - target: { tagName: 'BODY', isContentEditable: false }, - preventDefault() {}, - }); -} - -async function waitForRequests(count) { - for (let i = 0; i < 10; i += 1) { - if (requests.length >= count) return; - await Promise.resolve(); - } - assert(false, `expected ${count} request(s), got ${requests.length}`); -} - -console.log('Running shuffle tests...'); -testEnableTurnsShuffleOn(); -testRepeatedEnableKeepsShuffleOnAndAdvancesRevision(); -testToggleCanStillTurnShuffleOff(); -await testKeyboardReshuffleRequestsRandomMediaWithNewRevision(); - -if (failures.length) { - console.error('FAILURES:'); - failures.forEach((m) => console.error(' - ' + m)); - process.exit(1); -} else { - console.log('All shuffle tests passed.'); - process.exit(0); -} |
