release / govulncheck (push) Successful in 23s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 4m49s
release / android (push) Successful in 5m32s
release / Build signed APK (releases and dev) (push) Successful in 5m39s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 28s
release / Verify release artifacts (tag releases only) (push) Skipped
CI run 8789 warned on both: - kit 3 deprecates `config.alias`. The $test-utils alias becomes a `#test-utils/*` subpath import, matching #lib, and its 43 import sites move with it. - Vite's coming native config loader needs the extension on vitest.config.ts's import of vite.config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
3.9 KiB
TypeScript
129 lines
3.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
import { flushSync } from 'svelte';
|
|
import type { TrackRef } from '#lib/api/types.js';
|
|
import { makeTrack } from '#test-utils/fixtures/track.js';
|
|
|
|
function track(id: string): TrackRef {
|
|
return makeTrack({
|
|
id,
|
|
title: `T ${id}`,
|
|
duration_sec: 200,
|
|
stream_url: `/api/tracks/${id}/stream`
|
|
});
|
|
}
|
|
|
|
function press(key: string, target?: EventTarget): void {
|
|
const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true });
|
|
(target ?? window).dispatchEvent(event);
|
|
}
|
|
|
|
describe('useGlobalShortcuts', () => {
|
|
let cleanup: () => void;
|
|
|
|
beforeEach(async () => {
|
|
const store = await import('./store.svelte');
|
|
store.playQueue([track('1'), track('2'), track('3')]);
|
|
store.reportDuration(200);
|
|
flushSync();
|
|
const { useGlobalShortcuts } = await import('./shortcuts.svelte');
|
|
cleanup = $effect.root(() => useGlobalShortcuts());
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup?.();
|
|
});
|
|
|
|
test('Space toggles play/pause', async () => {
|
|
const store = await import('./store.svelte');
|
|
// playQueue leaves the store at 'loading' (intent-to-play; jsdom
|
|
// never moves it to 'playing' because there's no real audio). The
|
|
// togglePlay contract is: loading|playing -> paused, anything else
|
|
// -> loading. So Space round-trips loading <-> paused.
|
|
expect(store.player.state).toBe('loading');
|
|
press(' ');
|
|
flushSync();
|
|
expect(store.player.state).toBe('paused');
|
|
press(' ');
|
|
flushSync();
|
|
expect(store.player.state).toBe('loading');
|
|
});
|
|
|
|
test('K is an alias for Space (YouTube convention)', async () => {
|
|
const store = await import('./store.svelte');
|
|
expect(store.player.state).toBe('loading');
|
|
press('k');
|
|
flushSync();
|
|
expect(store.player.state).toBe('paused');
|
|
});
|
|
|
|
test('ArrowRight advances to the next track', async () => {
|
|
const store = await import('./store.svelte');
|
|
expect(store.player.index).toBe(0);
|
|
press('ArrowRight');
|
|
flushSync();
|
|
expect(store.player.index).toBe(1);
|
|
});
|
|
|
|
test('ArrowLeft goes to the previous track', async () => {
|
|
const store = await import('./store.svelte');
|
|
store.skipNext();
|
|
flushSync();
|
|
expect(store.player.index).toBe(1);
|
|
press('ArrowLeft');
|
|
flushSync();
|
|
expect(store.player.index).toBe(0);
|
|
});
|
|
|
|
test('M toggles mute, restoring the previous volume', async () => {
|
|
const store = await import('./store.svelte');
|
|
store.setVolume(0.42);
|
|
flushSync();
|
|
expect(store.player.volume).toBe(0.42);
|
|
press('m');
|
|
flushSync();
|
|
expect(store.player.volume).toBe(0);
|
|
press('m');
|
|
flushSync();
|
|
expect(store.player.volume).toBeCloseTo(0.42, 5);
|
|
});
|
|
|
|
test('ArrowUp / ArrowDown adjust volume by 5%', async () => {
|
|
const store = await import('./store.svelte');
|
|
store.setVolume(0.5);
|
|
flushSync();
|
|
press('ArrowUp');
|
|
flushSync();
|
|
expect(store.player.volume).toBeCloseTo(0.55, 5);
|
|
press('ArrowDown');
|
|
flushSync();
|
|
expect(store.player.volume).toBeCloseTo(0.5, 5);
|
|
});
|
|
|
|
test('shortcuts are ignored when focus is in an input', async () => {
|
|
const store = await import('./store.svelte');
|
|
const before = store.player.state;
|
|
const input = document.createElement('input');
|
|
document.body.appendChild(input);
|
|
input.focus();
|
|
press(' ', input);
|
|
flushSync();
|
|
// No state change — Space should pass through to the input.
|
|
expect(store.player.state).toBe(before);
|
|
input.remove();
|
|
});
|
|
|
|
test('modifier keys disable the shortcut (browser hotkey passthrough)', async () => {
|
|
const store = await import('./store.svelte');
|
|
const before = store.player.state;
|
|
const event = new KeyboardEvent('keydown', {
|
|
key: ' ',
|
|
ctrlKey: true,
|
|
bubbles: true,
|
|
cancelable: true
|
|
});
|
|
window.dispatchEvent(event);
|
|
flushSync();
|
|
expect(store.player.state).toBe(before);
|
|
});
|
|
});
|