test: Add first row of tests for the site
This commit is contained in:
parent
f3b9cfd2f0
commit
d7642ff78e
7 changed files with 665 additions and 18 deletions
35
js/db.js
35
js/db.js
|
|
@ -102,8 +102,6 @@ export class MusicDatabase {
|
|||
async addToHistory(track) {
|
||||
const storeName = 'history_tracks';
|
||||
const minified = this._minifyItem(track.type || 'track', track);
|
||||
const timestamp = Date.now();
|
||||
const entry = { ...minified, timestamp };
|
||||
|
||||
const db = await this.open();
|
||||
|
||||
|
|
@ -112,25 +110,34 @@ export class MusicDatabase {
|
|||
const store = transaction.objectStore(storeName);
|
||||
const index = store.index('timestamp');
|
||||
|
||||
const cursorReq = index.openCursor(null, 'prev');
|
||||
const lastReq = index.openCursor(null, 'prev');
|
||||
let lastTimestamp = 0;
|
||||
|
||||
cursorReq.onsuccess = (e) => {
|
||||
lastReq.onsuccess = (e) => {
|
||||
const cursor = e.target.result;
|
||||
if (cursor) {
|
||||
const lastTrack = cursor.value;
|
||||
if (lastTrack.id === track.id) {
|
||||
store.delete(cursor.primaryKey);
|
||||
if (cursor && lastTimestamp === 0) {
|
||||
lastTimestamp = cursor.value.timestamp;
|
||||
}
|
||||
|
||||
const timestamp = Math.max(Date.now(), lastTimestamp + 1);
|
||||
const entry = { ...minified, timestamp };
|
||||
|
||||
const dedupeReq = index.openCursor(null, 'prev');
|
||||
dedupeReq.onsuccess = (e2) => {
|
||||
const dedupeCursor = e2.target.result;
|
||||
if (dedupeCursor) {
|
||||
const trackInHistory = dedupeCursor.value;
|
||||
if (trackInHistory.id === track.id) {
|
||||
store.delete(dedupeCursor.primaryKey);
|
||||
}
|
||||
dedupeCursor.continue();
|
||||
} else {
|
||||
store.put(entry);
|
||||
resolve(entry);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
cursorReq.onerror = (_e) => {
|
||||
// If cursor fails, just try to put (fallback)
|
||||
store.put(entry);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => resolve(entry);
|
||||
transaction.onerror = (e) => reject(e.target.error);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,7 +291,8 @@ export class Player {
|
|||
}
|
||||
|
||||
setPlaybackSpeed(speed) {
|
||||
const validSpeed = Math.max(0.01, Math.min(100, parseFloat(speed) || 1.0));
|
||||
const parsed = parseFloat(speed);
|
||||
const validSpeed = Math.max(0.01, Math.min(100, isNaN(parsed) ? 1.0 : parsed));
|
||||
audioEffectsSettings.setSpeed(validSpeed);
|
||||
this.applyAudioEffects();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1830,7 +1830,8 @@ export const audioEffectsSettings = {
|
|||
},
|
||||
|
||||
setSpeed(speed) {
|
||||
const validSpeed = Math.max(0.01, Math.min(100, parseFloat(speed) || 1.0));
|
||||
const parsed = parseFloat(speed);
|
||||
const validSpeed = Math.max(0.01, Math.min(100, isNaN(parsed) ? 1.0 : parsed));
|
||||
localStorage.setItem(this.SPEED_KEY, validSpeed.toString());
|
||||
},
|
||||
|
||||
|
|
|
|||
107
js/tests/db.test.js
Normal file
107
js/tests/db.test.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { expect, test, describe, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { MusicDatabase } from '../db.js';
|
||||
|
||||
describe('MusicDatabase', () => {
|
||||
let db;
|
||||
const TEST_DB_NAME = 'TestMonochromeDB';
|
||||
|
||||
beforeEach(async () => {
|
||||
db = new MusicDatabase();
|
||||
db.dbName = TEST_DB_NAME;
|
||||
const req = indexedDB.deleteDatabase(TEST_DB_NAME);
|
||||
await new Promise((resolve) => {
|
||||
req.onsuccess = resolve;
|
||||
req.onerror = resolve;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (db.db) {
|
||||
db.db.close();
|
||||
}
|
||||
const req = indexedDB.deleteDatabase(TEST_DB_NAME);
|
||||
await new Promise((resolve) => {
|
||||
req.onsuccess = resolve;
|
||||
req.onerror = resolve;
|
||||
});
|
||||
});
|
||||
|
||||
test('opens database and creates stores', async () => {
|
||||
const openedDb = await db.open();
|
||||
expect(openedDb.name).toBe(TEST_DB_NAME);
|
||||
expect(openedDb.objectStoreNames.contains('favorites_tracks')).toBe(true);
|
||||
expect(openedDb.objectStoreNames.contains('history_tracks')).toBe(true);
|
||||
expect(openedDb.objectStoreNames.contains('user_playlists')).toBe(true);
|
||||
});
|
||||
|
||||
test('toggleFavorite adds and removes items', async () => {
|
||||
const track = { id: 'track1', title: 'Test Track', artist: { name: 'Artist' } };
|
||||
|
||||
const added = await db.toggleFavorite('track', track);
|
||||
expect(added).toBe(true);
|
||||
const favorites = await db.getFavorites('track');
|
||||
expect(favorites.length).toBe(1);
|
||||
expect(favorites[0].id).toBe('track1');
|
||||
|
||||
const removed = await db.toggleFavorite('track', track);
|
||||
expect(removed).toBe(false);
|
||||
const favoritesAfter = await db.getFavorites('track');
|
||||
expect(favoritesAfter.length).toBe(0);
|
||||
});
|
||||
|
||||
test('addToHistory manages recent tracks and avoids duplicates', async () => {
|
||||
const track1 = { id: 't1', title: 'Track 1' };
|
||||
const track2 = { id: 't2', title: 'Track 2' };
|
||||
|
||||
await db.addToHistory(track1);
|
||||
await db.addToHistory(track2);
|
||||
await db.addToHistory(track1);
|
||||
|
||||
const history = await db.getHistory();
|
||||
expect(history.length).toBe(2);
|
||||
expect(history[0].id).toBe('t1');
|
||||
expect(history[1].id).toBe('t2');
|
||||
});
|
||||
|
||||
test('playlist operations: create, add, remove, delete', async () => {
|
||||
const track = { id: 'track1', title: 'Test Track' };
|
||||
|
||||
const playlist = await db.createPlaylist('My Playlist', [track]);
|
||||
expect(playlist.name).toBe('My Playlist');
|
||||
expect(playlist.tracks.length).toBe(1);
|
||||
|
||||
const track2 = { id: 'track2', title: 'Track 2' };
|
||||
await db.addTrackToPlaylist(playlist.id, track2);
|
||||
|
||||
const updated = await db.getPlaylist(playlist.id);
|
||||
expect(updated.tracks.length).toBe(2);
|
||||
expect(updated.tracks[1].id).toBe('track2');
|
||||
|
||||
await db.removeTrackFromPlaylist(playlist.id, 'track1');
|
||||
const afterRemove = await db.getPlaylist(playlist.id);
|
||||
expect(afterRemove.tracks.length).toBe(1);
|
||||
expect(afterRemove.tracks[0].id).toBe('track2');
|
||||
|
||||
await db.deletePlaylist(playlist.id);
|
||||
const deleted = await db.getPlaylist(playlist.id);
|
||||
expect(deleted).toBeUndefined();
|
||||
});
|
||||
|
||||
test('pinned items management', async () => {
|
||||
const album = { id: 'album1', title: 'Album 1', type: 'album' };
|
||||
|
||||
await db.togglePinned(album, 'album');
|
||||
let pinned = await db.getPinned();
|
||||
expect(pinned.length).toBe(1);
|
||||
expect(pinned[0].id).toBe('album1');
|
||||
|
||||
await db.togglePinned({ id: 'a2', title: 'A2' }, 'album');
|
||||
await db.togglePinned({ id: 'a3', title: 'A3' }, 'album');
|
||||
await db.togglePinned({ id: 'a4', title: 'A4' }, 'album');
|
||||
|
||||
pinned = await db.getPinned();
|
||||
expect(pinned.length).toBe(3);
|
||||
expect(pinned.some((p) => p.id === 'a4')).toBe(true);
|
||||
expect(pinned.some((p) => p.id === 'album1')).toBe(false);
|
||||
});
|
||||
});
|
||||
195
js/tests/player.test.js
Normal file
195
js/tests/player.test.js
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import { expect, test, describe, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { Player } from '../player.js';
|
||||
import { REPEAT_MODE } from '../utils.js';
|
||||
import { audioEffectsSettings } from '../storage.js';
|
||||
|
||||
vi.mock('../audio-context.js', () => ({
|
||||
audioContextManager: {
|
||||
init: vi.fn(),
|
||||
resume: vi.fn(() => Promise.resolve()),
|
||||
isReady: vi.fn(() => false),
|
||||
setVolume: vi.fn(),
|
||||
changeSource: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../storage.js', () => ({
|
||||
queueManager: {
|
||||
getQueue: vi.fn(() => null),
|
||||
saveQueue: vi.fn(),
|
||||
},
|
||||
replayGainSettings: { getMode: vi.fn(() => 'off'), getPreamp: vi.fn(() => 0) },
|
||||
trackDateSettings: { useAlbumYear: vi.fn(() => true) },
|
||||
exponentialVolumeSettings: { applyCurve: vi.fn((v) => v) },
|
||||
audioEffectsSettings: {
|
||||
getSpeed: vi.fn(() => 1.0),
|
||||
setSpeed: vi.fn(),
|
||||
isPreservePitchEnabled: vi.fn(() => true),
|
||||
setPreservePitch: vi.fn(),
|
||||
},
|
||||
radioSettings: { isEnabled: vi.fn(() => false) },
|
||||
contentBlockingSettings: {
|
||||
shouldHideTrack: vi.fn(() => false),
|
||||
shouldHideAlbum: vi.fn(() => false),
|
||||
shouldHideArtist: vi.fn(() => false),
|
||||
},
|
||||
qualityBadgeSettings: { isEnabled: vi.fn(() => true) },
|
||||
coverArtSizeSettings: { getSize: vi.fn(() => '1280') },
|
||||
apiSettings: {
|
||||
loadInstancesFromGitHub: vi.fn(() => Promise.resolve([])),
|
||||
getInstances: vi.fn(() => Promise.resolve([])),
|
||||
},
|
||||
recentActivityManager: { addArtist: vi.fn(), addAlbum: vi.fn() },
|
||||
themeManager: { getTheme: vi.fn(() => 'dark'), setTheme: vi.fn() },
|
||||
lastFMStorage: { isEnabled: vi.fn(() => false) },
|
||||
nowPlayingSettings: { getMode: vi.fn(() => 'cover') },
|
||||
gaplessPlaybackSettings: { isEnabled: vi.fn(() => true) },
|
||||
}));
|
||||
|
||||
vi.mock('../db.js', () => ({
|
||||
db: {
|
||||
get: vi.fn(),
|
||||
put: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../ui.js', () => ({
|
||||
UIRenderer: {
|
||||
renderQueue: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('shaka-player', () => ({
|
||||
default: {
|
||||
polyfill: { installAll: vi.fn() },
|
||||
Player: {
|
||||
isBrowserSupported: vi.fn(() => true),
|
||||
prototype: {
|
||||
configure: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
load: vi.fn(),
|
||||
unload: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
polyfill: { installAll: vi.fn() },
|
||||
Player: class {
|
||||
static isBrowserSupported() {
|
||||
return true;
|
||||
}
|
||||
configure() {}
|
||||
addEventListener() {}
|
||||
load() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
unload() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
destroy() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Player', () => {
|
||||
let audioElement;
|
||||
let api;
|
||||
let player;
|
||||
|
||||
beforeEach(async () => {
|
||||
document.body.innerHTML = `
|
||||
<audio id="audio-player"></audio>
|
||||
<video id="video-player"></video>
|
||||
<div class="now-playing-bar">
|
||||
<img class="cover" src="">
|
||||
<div class="title"></div>
|
||||
<div class="artist"></div>
|
||||
<div class="album"></div>
|
||||
</div>
|
||||
<div id="total-duration"></div>
|
||||
`;
|
||||
|
||||
audioElement = document.getElementById('audio-player');
|
||||
api = {
|
||||
getCoverUrl: vi.fn((id) => `url-${id}`),
|
||||
getCoverSrcset: vi.fn(),
|
||||
getStreamUrl: vi.fn(),
|
||||
};
|
||||
|
||||
Player._instance = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initialization sets up initial state', async () => {
|
||||
player = new Player(audioElement, api);
|
||||
expect(player.audio).toBe(audioElement);
|
||||
expect(player.api).toBe(api);
|
||||
expect(player.queue).toEqual([]);
|
||||
expect(player.shuffleActive).toBe(false);
|
||||
});
|
||||
|
||||
test('setVolume updates userVolume and localStorage', () => {
|
||||
player = new Player(audioElement, api);
|
||||
player.setVolume(0.5);
|
||||
expect(player.userVolume).toBe(0.5);
|
||||
expect(localStorage.getItem('volume')).toBe('0.5');
|
||||
});
|
||||
|
||||
test('shuffle toggles correctly', () => {
|
||||
player = new Player(audioElement, api);
|
||||
player.queue = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
||||
|
||||
player.toggleShuffle();
|
||||
expect(player.shuffleActive).toBe(true);
|
||||
expect(player.shuffledQueue.length).toBe(3);
|
||||
|
||||
player.toggleShuffle();
|
||||
expect(player.shuffleActive).toBe(false);
|
||||
});
|
||||
|
||||
test('repeat mode cycles correctly', () => {
|
||||
player = new Player(audioElement, api);
|
||||
expect(player.repeatMode).toBe(REPEAT_MODE.OFF);
|
||||
|
||||
player.toggleRepeat();
|
||||
expect(player.repeatMode).toBe(REPEAT_MODE.ALL);
|
||||
|
||||
player.toggleRepeat();
|
||||
expect(player.repeatMode).toBe(REPEAT_MODE.ONE);
|
||||
|
||||
player.toggleRepeat();
|
||||
expect(player.repeatMode).toBe(REPEAT_MODE.OFF);
|
||||
});
|
||||
|
||||
test('addToQueue adds tracks to the end', async () => {
|
||||
player = new Player(audioElement, api);
|
||||
player.queue = [{ id: 1 }];
|
||||
|
||||
await player.addToQueue([{ id: 2 }, { id: 3 }]);
|
||||
expect(player.queue.length).toBe(3);
|
||||
expect(player.queue[2].id).toBe(3);
|
||||
});
|
||||
|
||||
test('clearQueue resets queue state', async () => {
|
||||
player = new Player(audioElement, api);
|
||||
player.queue = [{ id: 1 }];
|
||||
player.currentQueueIndex = 0;
|
||||
|
||||
await player.clearQueue();
|
||||
expect(player.queue).toEqual([]);
|
||||
expect(player.currentQueueIndex).toBe(-1);
|
||||
});
|
||||
|
||||
test('setPlaybackSpeed clamps values', () => {
|
||||
player = new Player(audioElement, api);
|
||||
|
||||
player.setPlaybackSpeed(2.0);
|
||||
expect(audioEffectsSettings.setSpeed).toHaveBeenCalledWith(2.0);
|
||||
|
||||
player.setPlaybackSpeed(0);
|
||||
expect(audioEffectsSettings.setSpeed).toHaveBeenCalledWith(0.01);
|
||||
});
|
||||
});
|
||||
125
js/tests/storage.test.js
Normal file
125
js/tests/storage.test.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { expect, test, describe, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
recentActivityManager,
|
||||
themeManager,
|
||||
lastFMStorage,
|
||||
nowPlayingSettings,
|
||||
gaplessPlaybackSettings,
|
||||
exponentialVolumeSettings,
|
||||
audioEffectsSettings,
|
||||
} from '../storage.js';
|
||||
|
||||
describe('storage.js', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('recentActivityManager', () => {
|
||||
test('initializes with empty arrays', () => {
|
||||
const recents = recentActivityManager.getRecents();
|
||||
expect(recents.artists).toEqual([]);
|
||||
expect(recents.albums).toEqual([]);
|
||||
});
|
||||
|
||||
test('adds artist and maintains limit', () => {
|
||||
for (let i = 0; i < 15; i++) {
|
||||
recentActivityManager.addArtist({ id: i, name: `Artist ${i}` });
|
||||
}
|
||||
const recents = recentActivityManager.getRecents();
|
||||
expect(recents.artists.length).toBe(10);
|
||||
expect(recents.artists[0].id).toBe(14);
|
||||
});
|
||||
|
||||
test('clears recents', () => {
|
||||
recentActivityManager.addArtist({ id: 1, name: 'Artist' });
|
||||
recentActivityManager.clear();
|
||||
const recents = recentActivityManager.getRecents();
|
||||
expect(recents.artists).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('themeManager', () => {
|
||||
test('gets and sets theme', () => {
|
||||
themeManager.setTheme('dark');
|
||||
expect(themeManager.getTheme()).toBe('dark');
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
||||
});
|
||||
|
||||
test('handles custom theme', () => {
|
||||
const colors = { primary: '#ff0000', background: '#000000' };
|
||||
themeManager.setCustomTheme(colors);
|
||||
expect(themeManager.getTheme()).toBe('custom');
|
||||
expect(themeManager.getCustomTheme()).toEqual(colors);
|
||||
expect(document.documentElement.style.getPropertyValue('--primary')).toBe('#ff0000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lastFMStorage', () => {
|
||||
test('handles enabled state', () => {
|
||||
lastFMStorage.setEnabled(true);
|
||||
expect(lastFMStorage.isEnabled()).toBe(true);
|
||||
lastFMStorage.setEnabled(false);
|
||||
expect(lastFMStorage.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('obfuscates sensitive data', () => {
|
||||
const key = 'test-api-key';
|
||||
lastFMStorage.setCustomApiKey(key);
|
||||
expect(localStorage.getItem(lastFMStorage.CUSTOM_API_KEY)).not.toBe(key);
|
||||
expect(lastFMStorage.getCustomApiKey()).toBe(key);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nowPlayingSettings', () => {
|
||||
test('gets and sets mode', () => {
|
||||
expect(nowPlayingSettings.getMode()).toBe('cover');
|
||||
nowPlayingSettings.setMode('visualizer');
|
||||
expect(nowPlayingSettings.getMode()).toBe('visualizer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gaplessPlaybackSettings', () => {
|
||||
test('defaults to true', () => {
|
||||
expect(gaplessPlaybackSettings.isEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
test('sets enabled state', () => {
|
||||
gaplessPlaybackSettings.setEnabled(false);
|
||||
expect(gaplessPlaybackSettings.isEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exponentialVolumeSettings', () => {
|
||||
test('applies curve when enabled', () => {
|
||||
exponentialVolumeSettings.setEnabled(true);
|
||||
expect(exponentialVolumeSettings.applyCurve(0.5)).toBeCloseTo(0.125);
|
||||
expect(exponentialVolumeSettings.inverseCurve(0.125)).toBeCloseTo(0.5);
|
||||
});
|
||||
|
||||
test('does not apply curve when disabled', () => {
|
||||
exponentialVolumeSettings.setEnabled(false);
|
||||
expect(exponentialVolumeSettings.applyCurve(0.5)).toBe(0.5);
|
||||
expect(exponentialVolumeSettings.inverseCurve(0.5)).toBe(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('audioEffectsSettings', () => {
|
||||
test('gets and sets speed within bounds', () => {
|
||||
audioEffectsSettings.setSpeed(2.0);
|
||||
expect(audioEffectsSettings.getSpeed()).toBe(2.0);
|
||||
|
||||
audioEffectsSettings.setSpeed(200);
|
||||
expect(audioEffectsSettings.getSpeed()).toBe(100);
|
||||
|
||||
audioEffectsSettings.setSpeed(0);
|
||||
expect(audioEffectsSettings.getSpeed()).toBe(0.01);
|
||||
});
|
||||
|
||||
test('resets speed', () => {
|
||||
audioEffectsSettings.setSpeed(2.0);
|
||||
audioEffectsSettings.resetSpeed();
|
||||
expect(audioEffectsSettings.getSpeed()).toBe(1.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
211
js/tests/utils.test.js
Normal file
211
js/tests/utils.test.js
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import { expect, test, describe, vi } from 'vitest';
|
||||
import * as utils from '../utils.js';
|
||||
|
||||
vi.mock('../ModernSettings.js', () => ({
|
||||
modernSettings: {
|
||||
filenameTemplate: '{artist} - {album} - {trackNumber} - {title}',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../icons.js', () => ({
|
||||
SVG_ATMOS: () => '<svg>atmos</svg>',
|
||||
}));
|
||||
|
||||
vi.mock('../storage.js', () => ({
|
||||
qualityBadgeSettings: { isEnabled: vi.fn(() => true) },
|
||||
coverArtSizeSettings: { getSize: vi.fn(() => '1280') },
|
||||
trackDateSettings: { useAlbumYear: vi.fn(() => false) },
|
||||
}));
|
||||
|
||||
describe('utils.js', () => {
|
||||
describe('formatTime', () => {
|
||||
test('formats seconds into M:SS', () => {
|
||||
expect(utils.formatTime(0)).toBe('0:00');
|
||||
expect(utils.formatTime(5)).toBe('0:05');
|
||||
expect(utils.formatTime(60)).toBe('1:00');
|
||||
expect(utils.formatTime(65)).toBe('1:05');
|
||||
});
|
||||
|
||||
test('formats seconds into H:MM:SS', () => {
|
||||
expect(utils.formatTime(3600)).toBe('1:00:00');
|
||||
expect(utils.formatTime(3665)).toBe('1:01:05');
|
||||
});
|
||||
|
||||
test('handles NaN', () => {
|
||||
expect(utils.formatTime(NaN)).toBe('0:00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeForFilename', () => {
|
||||
test('replaces invalid characters with underscores', () => {
|
||||
expect(utils.sanitizeForFilename('a/b:c*d?e"f<g>h|i')).toBe('a_b_c_d_e_f_g_h_i');
|
||||
});
|
||||
|
||||
test('collapses multiple spaces and trims', () => {
|
||||
expect(utils.sanitizeForFilename(' hello world ')).toBe('hello world');
|
||||
});
|
||||
|
||||
test('returns "Unknown" for empty input', () => {
|
||||
expect(utils.sanitizeForFilename('')).toBe('Unknown');
|
||||
expect(utils.sanitizeForFilename(null)).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceTokens', () => {
|
||||
test('replaces tokens in template', () => {
|
||||
const template = '{artist} - {title}';
|
||||
const tokens = { artist: 'Artist', title: 'Title' };
|
||||
expect(utils.replaceTokens(template, tokens)).toBe('Artist - Title');
|
||||
});
|
||||
|
||||
test('leaves unknown tokens as is', () => {
|
||||
const template = '{artist} - {unknown}';
|
||||
const tokens = { artist: 'Artist' };
|
||||
expect(utils.replaceTokens(template, tokens)).toBe('Artist - {unknown}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPathTemplate', () => {
|
||||
test('formats path correctly', () => {
|
||||
const data = {
|
||||
artist: 'Artist',
|
||||
album: 'Album',
|
||||
trackNumber: 1,
|
||||
title: 'Title',
|
||||
discNumber: 1,
|
||||
};
|
||||
const template = '{artist}/{album}/{trackNumber} - {title}';
|
||||
expect(utils.formatPathTemplate(template, data)).toBe('Artist/Album/01 - Title');
|
||||
});
|
||||
|
||||
test('strips . and .. segments', () => {
|
||||
const data = { artist: '..', title: '.' };
|
||||
const template = '{artist}/{title}/song';
|
||||
expect(utils.formatPathTemplate(template, data)).toBe('song');
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectAudioFormat', () => {
|
||||
test('detects flac', () => {
|
||||
const view = new DataView(new Uint8Array([0x66, 0x4c, 0x61, 0x43]).buffer);
|
||||
expect(utils.detectAudioFormat(view)).toBe('flac');
|
||||
});
|
||||
|
||||
test('detects mp4', () => {
|
||||
const view = new DataView(new Uint8Array([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70]).buffer);
|
||||
expect(utils.detectAudioFormat(view)).toBe('mp4');
|
||||
});
|
||||
|
||||
test('detects mp3 (ID3)', () => {
|
||||
const view = new DataView(new Uint8Array([0x49, 0x44, 0x33]).buffer);
|
||||
expect(utils.detectAudioFormat(view)).toBe('mp3');
|
||||
});
|
||||
|
||||
test('detects ogg', () => {
|
||||
const view = new DataView(new Uint8Array([0x4f, 0x67, 0x67, 0x53]).buffer);
|
||||
expect(utils.detectAudioFormat(view)).toBe('ogg');
|
||||
});
|
||||
|
||||
test('returns null for unknown format', () => {
|
||||
const view = new DataView(new Uint8Array([0, 0, 0, 0]).buffer);
|
||||
expect(utils.detectAudioFormat(view)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeQualityToken', () => {
|
||||
test('normalizes various quality strings', () => {
|
||||
expect(utils.normalizeQualityToken('HI_RES_LOSSLESS')).toBe('HI_RES_LOSSLESS');
|
||||
expect(utils.normalizeQualityToken('MASTER')).toBe('HI_RES_LOSSLESS');
|
||||
expect(utils.normalizeQualityToken('HIFI')).toBe('LOSSLESS');
|
||||
expect(utils.normalizeQualityToken('ATMOS')).toBe('DOLBY_ATMOS');
|
||||
});
|
||||
|
||||
test('returns null for unknown quality', () => {
|
||||
expect(utils.normalizeQualityToken('UNKNOWN')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pickBestQuality', () => {
|
||||
test('picks the highest quality from list', () => {
|
||||
expect(utils.pickBestQuality(['LOSSLESS', 'HI_RES_LOSSLESS', 'HIGH'])).toBe('HI_RES_LOSSLESS');
|
||||
expect(utils.pickBestQuality(['LOW', 'HIGH'])).toBe('HIGH');
|
||||
expect(utils.pickBestQuality(['DOLBY_ATMOS', 'HI_RES_LOSSLESS'])).toBe('DOLBY_ATMOS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrackTitle', () => {
|
||||
test('returns title with version if present', () => {
|
||||
expect(utils.getTrackTitle({ title: 'Song', version: 'Remix' })).toBe('Song (Remix)');
|
||||
});
|
||||
|
||||
test('returns just title if no version', () => {
|
||||
expect(utils.getTrackTitle({ title: 'Song' })).toBe('Song');
|
||||
});
|
||||
|
||||
test('returns fallback if no title', () => {
|
||||
expect(utils.getTrackTitle({}, { fallback: 'No Title' })).toBe('No Title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrackArtists', () => {
|
||||
test('joins multiple artists', () => {
|
||||
const track = { artists: [{ name: 'A' }, { name: 'B' }] };
|
||||
expect(utils.getTrackArtists(track)).toBe('A, B');
|
||||
});
|
||||
|
||||
test('returns fallback if no artists', () => {
|
||||
expect(utils.getTrackArtists({})).toBe('Unknown Artist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrackDiscNumber', () => {
|
||||
test('extracts disc number from various properties', () => {
|
||||
expect(utils.getTrackDiscNumber({ discNumber: 2 })).toBe(2);
|
||||
expect(utils.getTrackDiscNumber({ volumeNumber: 3 })).toBe(3);
|
||||
expect(utils.getTrackDiscNumber({ mediaNumber: 4 })).toBe(4);
|
||||
});
|
||||
|
||||
test('returns null for invalid values', () => {
|
||||
expect(utils.getTrackDiscNumber({ discNumber: 0 })).toBeNull();
|
||||
expect(utils.getTrackDiscNumber({ discNumber: 'abc' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tryCatch', () => {
|
||||
test('executes sync function', () => {
|
||||
const fn = vi.fn(() => 'success');
|
||||
const onError = vi.fn();
|
||||
expect(utils.tryCatch(fn, onError)).toBe('success');
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles sync error', () => {
|
||||
const error = new Error('fail');
|
||||
const fn = vi.fn(() => {
|
||||
throw error;
|
||||
});
|
||||
const onError = vi.fn((err) => err.message);
|
||||
expect(utils.tryCatch(fn, onError)).toBe('fail');
|
||||
expect(onError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
test('executes async function', async () => {
|
||||
const fn = vi.fn(async () => 'success');
|
||||
const onError = vi.fn();
|
||||
const result = await utils.tryCatch(fn, onError);
|
||||
expect(result).toBe('success');
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles async error', async () => {
|
||||
const error = new Error('fail');
|
||||
const fn = vi.fn(async () => {
|
||||
throw error;
|
||||
});
|
||||
const onError = vi.fn(async (err) => err.message);
|
||||
const result = await utils.tryCatch(fn, onError);
|
||||
expect(result).toBe('fail');
|
||||
expect(onError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue