summaryrefslogtreecommitdiff
path: root/web/js/tests
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-17 15:25:52 +0300
committerPaul Buetow <paul@buetow.org>2026-05-17 15:25:52 +0300
commit914bd7cd6aa14e839332a98d91c30b19865b0cf2 (patch)
tree02b11537237a204048589e14ed24938b805e42f7 /web/js/tests
parent3b24f0e1be832584d6550e8cc3e5d24329f66f90 (diff)
Restructure repo: move Go server into player-server/
Diffstat (limited to 'web/js/tests')
-rw-r--r--web/js/tests/admin-rescan.test.js178
-rw-r--r--web/js/tests/keyboard-enter.test.js75
-rw-r--r--web/js/tests/media-pagination.test.js56
-rw-r--r--web/js/tests/playback-resume.test.js51
-rw-r--r--web/js/tests/podcast-episodes-api.test.js74
-rw-r--r--web/js/tests/progress-actions.test.js120
-rw-r--r--web/js/tests/search-parser.test.js33
-rw-r--r--web/js/tests/shuffle.test.js165
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);
-}