fix: address CodeRabbit PR feedback

This commit is contained in:
2026-03-30 22:47:27 -07:00
parent 13680af3f6
commit ff760eaa32
4 changed files with 262 additions and 507 deletions

View File

@@ -3,9 +3,9 @@ id: TASK-255
title: Add overlay playlist browser modal for sibling video files and mpv queue title: Add overlay playlist browser modal for sibling video files and mpv queue
status: In Progress status: In Progress
assignee: assignee:
- codex - '@codex'
created_date: '2026-03-30 05:46' created_date: '2026-03-30 05:46'
updated_date: '2026-03-31 04:57' updated_date: '2026-03-31 05:40'
labels: labels:
- feature - feature
- overlay - overlay
@@ -44,6 +44,8 @@ Add an in-session overlay modal that opens from a keybinding during active playb
2026-03-30 CodeRabbit follow-up: 1) add failing runtime coverage for unreadable playlist-browser file stat failures, 2) add failing renderer coverage for stale snapshot UI reset on refresh failure/close, 3) add failing renderer coverage to block playlist-browser open when another modal already owns the overlay, 4) implement minimal fixes, 5) rerun targeted tests plus typecheck for touched surfaces. 2026-03-30 CodeRabbit follow-up: 1) add failing runtime coverage for unreadable playlist-browser file stat failures, 2) add failing renderer coverage for stale snapshot UI reset on refresh failure/close, 3) add failing renderer coverage to block playlist-browser open when another modal already owns the overlay, 4) implement minimal fixes, 5) rerun targeted tests plus typecheck for touched surfaces.
2026-03-30 current CodeRabbit round: verify 4 unresolved threads, ignore already-fixed outdated dblclick thread if current code matches, add failing-first coverage for selection preservation / timestamp fixture consistency / string test-clock alignment, implement minimal fixes, rerun targeted tests plus typecheck. 2026-03-30 current CodeRabbit round: verify 4 unresolved threads, ignore already-fixed outdated dblclick thread if current code matches, add failing-first coverage for selection preservation / timestamp fixture consistency / string test-clock alignment, implement minimal fixes, rerun targeted tests plus typecheck.
2026-03-30 latest CodeRabbit round on PR #37: 1) add failing coverage for negative fractional numeric __subminerTestNowMs input so nowMs() matches the string-backed path, 2) add failing coverage that playlist-browser modal tests restore absent window/document globals without leaving undefined-valued properties behind, 3) refactor repeated playlist-browser modal test harness into a shared setup/teardown fixture while preserving assertions, 4) implement minimal fixes, 5) rerun touched tests plus typecheck.
<!-- SECTION:PLAN:END --> <!-- SECTION:PLAN:END -->
## Implementation Notes ## Implementation Notes
@@ -76,4 +78,6 @@ Split playlist-browser UI row rendering into `src/renderer/modals/playlist-brows
2026-03-30 PR #37 unresolved CodeRabbit threads currently reduce to three likely-actionable items plus one outdated renderer dblclick thread to verify against HEAD before touching code. 2026-03-30 PR #37 unresolved CodeRabbit threads currently reduce to three likely-actionable items plus one outdated renderer dblclick thread to verify against HEAD before touching code.
2026-03-30 Addressed latest unresolved CodeRabbit items on PR #37: preserved playlist-browser selection across mutation snapshots, taught nowMs() to honor string-backed test clocks so it stays aligned with currentDbTimestamp(), and normalized maintenance test timestamp fixtures to toDbTimestamp(). The older playlist-browser dblclick thread remains unresolved in GitHub state but current HEAD already contains that fix in playlist-browser-renderer.ts. 2026-03-30 Addressed latest unresolved CodeRabbit items on PR #37: preserved playlist-browser selection across mutation snapshots, taught nowMs() to honor string-backed test clocks so it stays aligned with currentDbTimestamp(), and normalized maintenance test timestamp fixtures to toDbTimestamp(). The older playlist-browser dblclick thread remains unresolved in GitHub state but current HEAD already contains that fix in playlist-browser-renderer.ts.
2026-03-30 latest CodeRabbit remediation on PR #37: switched nowMs() numeric test-clock branch from Math.floor() to Math.trunc() so numeric and string-backed mock clocks agree for negative fractional values. Refactored playlist-browser modal tests onto a shared setup/teardown fixture that restores global window/document descriptors correctly, and added regression coverage that injected globals are deleted when originally absent. Verification: `bun test src/core/services/immersion-tracker/time.test.ts src/renderer/modals/playlist-browser.test.ts`, `bun run typecheck`.
<!-- SECTION:NOTES:END --> <!-- SECTION:NOTES:END -->

View File

@@ -8,10 +8,21 @@ test('nowMs returns wall-clock epoch milliseconds', () => {
test('nowMs honors string-backed test clock values', () => { test('nowMs honors string-backed test clock values', () => {
const previousNowMs = globalThis.__subminerTestNowMs; const previousNowMs = globalThis.__subminerTestNowMs;
globalThis.__subminerTestNowMs = '1700000000123.9'; globalThis.__subminerTestNowMs = '123.9';
try { try {
assert.equal(nowMs(), 1_700_000_000_123); assert.equal(nowMs(), 123);
} finally {
globalThis.__subminerTestNowMs = previousNowMs;
}
});
test('nowMs truncates negative numeric test clock values', () => {
const previousNowMs = globalThis.__subminerTestNowMs;
globalThis.__subminerTestNowMs = -1.9;
try {
assert.equal(nowMs(), -1);
} finally { } finally {
globalThis.__subminerTestNowMs = previousNowMs; globalThis.__subminerTestNowMs = previousNowMs;
} }

View File

@@ -4,7 +4,7 @@ declare global {
function getMockNowMs(testNowMs: number | string | undefined): number | null { function getMockNowMs(testNowMs: number | string | undefined): number | null {
if (typeof testNowMs === 'number' && Number.isFinite(testNowMs)) { if (typeof testNowMs === 'number' && Number.isFinite(testNowMs)) {
return Math.floor(testNowMs); return Math.trunc(testNowMs);
} }
if (typeof testNowMs === 'string') { if (typeof testNowMs === 'string') {
const parsed = Number(testNowMs.trim()); const parsed = Number(testNowMs.trim());

View File

@@ -203,141 +203,153 @@ function createMutationSnapshot(): PlaylistBrowserSnapshot {
}; };
} }
test('playlist browser modal opens with playlist-focused current item selection', async () => { function restoreGlobalDescriptor<K extends keyof typeof globalThis>(
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown }; key: K,
const previousWindow = globals.window; descriptor: PropertyDescriptor | undefined,
const previousDocument = globals.document; ) {
const notifications: string[] = []; if (descriptor) {
Object.defineProperty(globalThis, key, descriptor);
return;
}
Reflect.deleteProperty(globalThis, key);
}
function createPlaylistBrowserDomFixture() {
return {
overlay: {
classList: createClassList(),
focus: () => {},
},
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle: createFakeElement(),
playlistBrowserStatus: createFakeElement(),
playlistBrowserDirectoryList: createListStub(),
playlistBrowserPlaylistList: createListStub(),
playlistBrowserClose: createFakeElement(),
};
}
function createPlaylistBrowserElectronApi(overrides?: Partial<ElectronAPI>): ElectronAPI {
return {
getPlaylistBrowserSnapshot: async () => createSnapshot(),
notifyOverlayModalOpened: () => {},
notifyOverlayModalClosed: () => {},
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
...overrides,
} as ElectronAPI;
}
function setupPlaylistBrowserModalTest(options?: {
electronApi?: Partial<ElectronAPI>;
shouldToggleMouseIgnore?: boolean;
}) {
const previousWindowDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'window');
const previousDocumentDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'document');
const state = createRendererState();
const dom = createPlaylistBrowserDomFixture();
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: options?.shouldToggleMouseIgnore ?? false,
},
dom,
};
Object.defineProperty(globalThis, 'window', { Object.defineProperty(globalThis, 'window', {
configurable: true, configurable: true,
value: { value: {
electronAPI: { electronAPI: createPlaylistBrowserElectronApi(options?.electronApi),
getPlaylistBrowserSnapshot: async () => createSnapshot(),
notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
} as unknown as ElectronAPI,
focus: () => {}, focus: () => {},
}, } satisfies { electronAPI: ElectronAPI; focus: () => void },
writable: true,
}); });
Object.defineProperty(globalThis, 'document', { Object.defineProperty(globalThis, 'document', {
configurable: true, configurable: true,
value: { value: {
createElement: () => createPlaylistRow(), createElement: () => createPlaylistRow(),
}, },
writable: true,
});
return {
state,
dom,
createModal(overrides: Partial<Parameters<typeof createPlaylistBrowserModal>[1]> = {}) {
return createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {},
...overrides,
});
},
restore() {
restoreGlobalDescriptor('window', previousWindowDescriptor);
restoreGlobalDescriptor('document', previousDocumentDescriptor);
},
};
}
test('playlist browser test cleanup must delete injected globals that were originally absent', () => {
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'window'), false);
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'document'), false);
const env = setupPlaylistBrowserModalTest();
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'window'), true);
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'document'), true);
env.restore();
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'window'), false);
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'document'), false);
assert.equal(typeof globalThis.window, 'undefined');
assert.equal(typeof globalThis.document, 'undefined');
});
test('playlist browser modal opens with playlist-focused current item selection', async () => {
const notifications: string[] = [];
const env = setupPlaylistBrowserModalTest({
electronApi: {
notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
},
}); });
try { try {
const state = createRendererState(); const modal = env.createModal();
const directoryList = createListStub();
const playlistList = createListStub();
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay: {
classList: createClassList(),
focus: () => {},
},
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle: createFakeElement(),
playlistBrowserStatus: createFakeElement(),
playlistBrowserDirectoryList: directoryList,
playlistBrowserPlaylistList: playlistList,
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {},
});
await modal.openPlaylistBrowserModal(); await modal.openPlaylistBrowserModal();
assert.equal(state.playlistBrowserModalOpen, true); assert.equal(env.state.playlistBrowserModalOpen, true);
assert.equal(state.playlistBrowserActivePane, 'playlist'); assert.equal(env.state.playlistBrowserActivePane, 'playlist');
assert.equal(state.playlistBrowserSelectedPlaylistIndex, 1); assert.equal(env.state.playlistBrowserSelectedPlaylistIndex, 1);
assert.equal(state.playlistBrowserSelectedDirectoryIndex, 1); assert.equal(env.state.playlistBrowserSelectedDirectoryIndex, 1);
assert.equal(directoryList.children.length, 2); assert.equal(env.dom.playlistBrowserDirectoryList.children.length, 2);
assert.equal(playlistList.children.length, 2); assert.equal(env.dom.playlistBrowserPlaylistList.children.length, 2);
assert.equal(directoryList.children[0]?.children.length, 2); assert.equal(env.dom.playlistBrowserDirectoryList.children[0]?.children.length, 2);
assert.equal(playlistList.children[0]?.children.length, 2); assert.equal(env.dom.playlistBrowserPlaylistList.children[0]?.children.length, 2);
assert.deepEqual(notifications, ['open:playlist-browser']); assert.deepEqual(notifications, ['open:playlist-browser']);
} finally { } finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); env.restore();
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
} }
}); });
test('playlist browser modal action buttons stop double-click propagation', async () => { test('playlist browser modal action buttons stop double-click propagation', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown }; const env = setupPlaylistBrowserModalTest();
const previousWindow = globals.window;
const previousDocument = globals.document;
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
electronAPI: {
getPlaylistBrowserSnapshot: async () => createSnapshot(),
notifyOverlayModalOpened: () => {},
notifyOverlayModalClosed: () => {},
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
} as unknown as ElectronAPI,
focus: () => {},
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createPlaylistRow(),
},
});
try { try {
const state = createRendererState(); const modal = env.createModal();
const directoryList = createListStub();
const playlistList = createListStub();
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay: {
classList: createClassList(),
focus: () => {},
},
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle: createFakeElement(),
playlistBrowserStatus: createFakeElement(),
playlistBrowserDirectoryList: directoryList,
playlistBrowserPlaylistList: playlistList,
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {},
});
await modal.openPlaylistBrowserModal(); await modal.openPlaylistBrowserModal();
const row = directoryList.children[0] as ReturnType<typeof createPlaylistRow> | undefined; const row =
env.dom.playlistBrowserDirectoryList.children[0] as
| ReturnType<typeof createPlaylistRow>
| undefined;
const trailing = row?.children?.[1] as ReturnType<typeof createPlaylistRow> | undefined; const trailing = row?.children?.[1] as ReturnType<typeof createPlaylistRow> | undefined;
const button = const button =
trailing?.children?.at(-1) as trailing?.children?.at(-1) as
@@ -355,98 +367,53 @@ test('playlist browser modal action buttons stop double-click propagation', asyn
assert.equal(stopped, true); assert.equal(stopped, true);
} finally { } finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); env.restore();
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
} }
}); });
test('playlist browser preserves prior selection across mutation snapshots', async () => { test('playlist browser preserves prior selection across mutation snapshots', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown }; const env = setupPlaylistBrowserModalTest({
const previousWindow = globals.window; electronApi: {
const previousDocument = globals.document; getPlaylistBrowserSnapshot: async () => ({
...createSnapshot(),
Object.defineProperty(globalThis, 'window', { directoryItems: [
configurable: true, ...createSnapshot().directoryItems,
value: { {
electronAPI: { path: '/tmp/show/Show - S01E03.mkv',
getPlaylistBrowserSnapshot: async () => ({ basename: 'Show - S01E03.mkv',
...createSnapshot(), episodeLabel: 'S1E3',
directoryItems: [ isCurrentFile: false,
...createSnapshot().directoryItems, },
{ ],
path: '/tmp/show/Show - S01E03.mkv', playlistItems: [
basename: 'Show - S01E03.mkv', ...createSnapshot().playlistItems,
episodeLabel: 'S1E3', {
isCurrentFile: false, index: 2,
}, id: 3,
], filename: '/tmp/show/Show - S01E03.mkv',
playlistItems: [ title: 'Episode 3',
...createSnapshot().playlistItems, displayLabel: 'Episode 3',
{ current: false,
index: 2, playing: false,
id: 3, path: '/tmp/show/Show - S01E03.mkv',
filename: '/tmp/show/Show - S01E03.mkv', },
title: 'Episode 3', ],
displayLabel: 'Episode 3', }),
current: false, appendPlaylistBrowserFile: async () => ({
playing: false, ok: true,
path: '/tmp/show/Show - S01E03.mkv', message: 'Queued file',
}, snapshot: createMutationSnapshot(),
], }),
}),
notifyOverlayModalOpened: () => {},
notifyOverlayModalClosed: () => {},
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({
ok: true,
message: 'Queued file',
snapshot: createMutationSnapshot(),
}),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
} as unknown as ElectronAPI,
focus: () => {},
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createPlaylistRow(),
}, },
}); });
try { try {
const state = createRendererState(); const modal = env.createModal();
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay: {
classList: createClassList(),
focus: () => {},
},
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle: createFakeElement(),
playlistBrowserStatus: createFakeElement(),
playlistBrowserDirectoryList: createListStub(),
playlistBrowserPlaylistList: createListStub(),
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {},
});
await modal.openPlaylistBrowserModal(); await modal.openPlaylistBrowserModal();
state.playlistBrowserActivePane = 'directory'; env.state.playlistBrowserActivePane = 'directory';
state.playlistBrowserSelectedDirectoryIndex = 2; env.state.playlistBrowserSelectedDirectoryIndex = 2;
state.playlistBrowserSelectedPlaylistIndex = 0; env.state.playlistBrowserSelectedPlaylistIndex = 0;
await modal.handlePlaylistBrowserKeydown({ await modal.handlePlaylistBrowserKeydown({
key: 'Enter', key: 'Enter',
@@ -457,88 +424,47 @@ test('playlist browser preserves prior selection across mutation snapshots', asy
shiftKey: false, shiftKey: false,
} as never); } as never);
assert.equal(state.playlistBrowserSelectedDirectoryIndex, 2); assert.equal(env.state.playlistBrowserSelectedDirectoryIndex, 2);
assert.equal(state.playlistBrowserSelectedPlaylistIndex, 2); assert.equal(env.state.playlistBrowserSelectedPlaylistIndex, 2);
} finally { } finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); env.restore();
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
} }
}); });
test('playlist browser modal keydown routes append, remove, reorder, tab switch, and play', async () => { test('playlist browser modal keydown routes append, remove, reorder, tab switch, and play', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
const previousWindow = globals.window;
const previousDocument = globals.document;
const calls: Array<[string, unknown[]]> = []; const calls: Array<[string, unknown[]]> = [];
const notifications: string[] = []; const notifications: string[] = [];
const env = setupPlaylistBrowserModalTest({
Object.defineProperty(globalThis, 'window', { electronApi: {
configurable: true, notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
value: { notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
electronAPI: { appendPlaylistBrowserFile: async (filePath: string) => {
getPlaylistBrowserSnapshot: async () => createSnapshot(), calls.push(['append', [filePath]]);
notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`), return { ok: true, message: 'append-ok', snapshot: createSnapshot() };
notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`), },
focusMainWindow: async () => {}, playPlaylistBrowserIndex: async (index: number) => {
setIgnoreMouseEvents: () => {}, calls.push(['play', [index]]);
appendPlaylistBrowserFile: async (filePath: string) => { return { ok: true, message: 'play-ok', snapshot: createSnapshot() };
calls.push(['append', [filePath]]); },
return { ok: true, message: 'append-ok', snapshot: createSnapshot() }; removePlaylistBrowserIndex: async (index: number) => {
}, calls.push(['remove', [index]]);
playPlaylistBrowserIndex: async (index: number) => { return { ok: true, message: 'remove-ok', snapshot: createSnapshot() };
calls.push(['play', [index]]); },
return { ok: true, message: 'play-ok', snapshot: createSnapshot() }; movePlaylistBrowserIndex: async (index: number, direction: -1 | 1) => {
}, calls.push(['move', [index, direction]]);
removePlaylistBrowserIndex: async (index: number) => { return { ok: true, message: 'move-ok', snapshot: createSnapshot() };
calls.push(['remove', [index]]); },
return { ok: true, message: 'remove-ok', snapshot: createSnapshot() };
},
movePlaylistBrowserIndex: async (index: number, direction: -1 | 1) => {
calls.push(['move', [index, direction]]);
return { ok: true, message: 'move-ok', snapshot: createSnapshot() };
},
} as unknown as ElectronAPI,
focus: () => {},
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createPlaylistRow(),
}, },
}); });
try { try {
const state = createRendererState(); const modal = env.createModal();
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay: {
classList: createClassList(),
focus: () => {},
},
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle: createFakeElement(),
playlistBrowserStatus: createFakeElement(),
playlistBrowserDirectoryList: createListStub(),
playlistBrowserPlaylistList: createListStub(),
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {},
});
await modal.openPlaylistBrowserModal(); await modal.openPlaylistBrowserModal();
const preventDefault = () => {}; const preventDefault = () => {};
state.playlistBrowserActivePane = 'directory'; env.state.playlistBrowserActivePane = 'directory';
state.playlistBrowserSelectedDirectoryIndex = 0; env.state.playlistBrowserSelectedDirectoryIndex = 0;
await modal.handlePlaylistBrowserKeydown({ await modal.handlePlaylistBrowserKeydown({
key: 'Enter', key: 'Enter',
code: 'Enter', code: 'Enter',
@@ -556,7 +482,7 @@ test('playlist browser modal keydown routes append, remove, reorder, tab switch,
metaKey: false, metaKey: false,
shiftKey: false, shiftKey: false,
} as never); } as never);
assert.equal(state.playlistBrowserActivePane, 'playlist'); assert.equal(env.state.playlistBrowserActivePane, 'playlist');
await modal.handlePlaylistBrowserKeydown({ await modal.handlePlaylistBrowserKeydown({
key: 'ArrowDown', key: 'ArrowDown',
@@ -591,73 +517,28 @@ test('playlist browser modal keydown routes append, remove, reorder, tab switch,
['remove', [1]], ['remove', [1]],
['play', [1]], ['play', [1]],
]); ]);
assert.equal(state.playlistBrowserModalOpen, false); assert.equal(env.state.playlistBrowserModalOpen, false);
assert.deepEqual(notifications, ['open:playlist-browser', 'close:playlist-browser']); assert.deepEqual(notifications, ['open:playlist-browser', 'close:playlist-browser']);
} finally { } finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); env.restore();
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
} }
}); });
test('playlist browser keeps modal open when playing selected queue item fails', async () => { test('playlist browser keeps modal open when playing selected queue item fails', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
const previousWindow = globals.window;
const previousDocument = globals.document;
const notifications: string[] = []; const notifications: string[] = [];
const env = setupPlaylistBrowserModalTest({
Object.defineProperty(globalThis, 'window', { electronApi: {
configurable: true, notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
value: { notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
electronAPI: { playPlaylistBrowserIndex: async () => ({ ok: false, message: 'play failed' }),
getPlaylistBrowserSnapshot: async () => createSnapshot(),
notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
playPlaylistBrowserIndex: async () => ({ ok: false, message: 'play failed' }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
} as unknown as ElectronAPI,
focus: () => {},
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createPlaylistRow(),
}, },
}); });
try { try {
const state = createRendererState(); const modal = env.createModal();
const playlistBrowserStatus = createFakeElement();
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay: {
classList: createClassList(),
focus: () => {},
},
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle: createFakeElement(),
playlistBrowserStatus,
playlistBrowserDirectoryList: createListStub(),
playlistBrowserPlaylistList: createListStub(),
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {},
});
await modal.openPlaylistBrowserModal(); await modal.openPlaylistBrowserModal();
assert.equal(state.playlistBrowserModalOpen, true); assert.equal(env.state.playlistBrowserModalOpen, true);
await modal.handlePlaylistBrowserKeydown({ await modal.handlePlaylistBrowserKeydown({
key: 'Enter', key: 'Enter',
@@ -668,250 +549,109 @@ test('playlist browser keeps modal open when playing selected queue item fails',
shiftKey: false, shiftKey: false,
} as never); } as never);
assert.equal(state.playlistBrowserModalOpen, true); assert.equal(env.state.playlistBrowserModalOpen, true);
assert.equal(playlistBrowserStatus.textContent, 'play failed'); assert.equal(env.dom.playlistBrowserStatus.textContent, 'play failed');
assert.equal(playlistBrowserStatus.classList.contains('error'), true); assert.equal(env.dom.playlistBrowserStatus.classList.contains('error'), true);
assert.deepEqual(notifications, ['open:playlist-browser']); assert.deepEqual(notifications, ['open:playlist-browser']);
} finally { } finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); env.restore();
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
} }
}); });
test('playlist browser refresh failure clears stale rendered rows and reports the error', async () => { test('playlist browser refresh failure clears stale rendered rows and reports the error', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
const previousWindow = globals.window;
const previousDocument = globals.document;
const notifications: string[] = []; const notifications: string[] = [];
let refreshShouldFail = false; let refreshShouldFail = false;
const env = setupPlaylistBrowserModalTest({
Object.defineProperty(globalThis, 'window', { electronApi: {
configurable: true, getPlaylistBrowserSnapshot: async () => {
value: { if (refreshShouldFail) {
electronAPI: { throw new Error('snapshot failed');
getPlaylistBrowserSnapshot: async () => { }
if (refreshShouldFail) { return createSnapshot();
throw new Error('snapshot failed'); },
} notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
return createSnapshot(); notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
},
notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
} as unknown as ElectronAPI,
focus: () => {},
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createPlaylistRow(),
}, },
}); });
try { try {
const state = createRendererState(); const modal = env.createModal();
const playlistBrowserTitle = createFakeElement();
const playlistBrowserStatus = createFakeElement();
const directoryList = createListStub();
const playlistList = createListStub();
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay: {
classList: createClassList(),
focus: () => {},
},
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle,
playlistBrowserStatus,
playlistBrowserDirectoryList: directoryList,
playlistBrowserPlaylistList: playlistList,
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {},
});
await modal.openPlaylistBrowserModal(); await modal.openPlaylistBrowserModal();
assert.equal(directoryList.children.length, 2); assert.equal(env.dom.playlistBrowserDirectoryList.children.length, 2);
assert.equal(playlistList.children.length, 2); assert.equal(env.dom.playlistBrowserPlaylistList.children.length, 2);
refreshShouldFail = true; refreshShouldFail = true;
await modal.refreshSnapshot(); await modal.refreshSnapshot();
assert.equal(state.playlistBrowserSnapshot, null); assert.equal(env.state.playlistBrowserSnapshot, null);
assert.equal(directoryList.children.length, 0); assert.equal(env.dom.playlistBrowserDirectoryList.children.length, 0);
assert.equal(playlistList.children.length, 0); assert.equal(env.dom.playlistBrowserPlaylistList.children.length, 0);
assert.equal(playlistBrowserTitle.textContent, 'Playlist Browser'); assert.equal(env.dom.playlistBrowserTitle.textContent, 'Playlist Browser');
assert.equal(playlistBrowserStatus.textContent, 'snapshot failed'); assert.equal(env.dom.playlistBrowserStatus.textContent, 'snapshot failed');
assert.equal(playlistBrowserStatus.classList.contains('error'), true); assert.equal(env.dom.playlistBrowserStatus.classList.contains('error'), true);
assert.deepEqual(notifications, ['open:playlist-browser']); assert.deepEqual(notifications, ['open:playlist-browser']);
} finally { } finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); env.restore();
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
} }
}); });
test('playlist browser close clears rendered snapshot ui', async () => { test('playlist browser close clears rendered snapshot ui', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
const previousWindow = globals.window;
const previousDocument = globals.document;
const notifications: string[] = []; const notifications: string[] = [];
const env = setupPlaylistBrowserModalTest({
Object.defineProperty(globalThis, 'window', { electronApi: {
configurable: true, notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
value: { notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
electronAPI: {
getPlaylistBrowserSnapshot: async () => createSnapshot(),
notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
} as unknown as ElectronAPI,
focus: () => {},
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createPlaylistRow(),
}, },
}); });
try { try {
const state = createRendererState(); const modal = env.createModal();
const playlistBrowserTitle = createFakeElement();
const playlistBrowserStatus = createFakeElement();
const directoryList = createListStub();
const playlistList = createListStub();
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay: {
classList: createClassList(),
focus: () => {},
},
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle,
playlistBrowserStatus,
playlistBrowserDirectoryList: directoryList,
playlistBrowserPlaylistList: playlistList,
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {},
});
await modal.openPlaylistBrowserModal(); await modal.openPlaylistBrowserModal();
assert.equal(directoryList.children.length, 2); assert.equal(env.dom.playlistBrowserDirectoryList.children.length, 2);
assert.equal(playlistList.children.length, 2); assert.equal(env.dom.playlistBrowserPlaylistList.children.length, 2);
modal.closePlaylistBrowserModal(); modal.closePlaylistBrowserModal();
assert.equal(state.playlistBrowserSnapshot, null); assert.equal(env.state.playlistBrowserSnapshot, null);
assert.equal(state.playlistBrowserStatus, ''); assert.equal(env.state.playlistBrowserStatus, '');
assert.equal(directoryList.children.length, 0); assert.equal(env.dom.playlistBrowserDirectoryList.children.length, 0);
assert.equal(playlistList.children.length, 0); assert.equal(env.dom.playlistBrowserPlaylistList.children.length, 0);
assert.equal(playlistBrowserTitle.textContent, 'Playlist Browser'); assert.equal(env.dom.playlistBrowserTitle.textContent, 'Playlist Browser');
assert.equal(playlistBrowserStatus.textContent, ''); assert.equal(env.dom.playlistBrowserStatus.textContent, '');
assert.deepEqual(notifications, ['open:playlist-browser', 'close:playlist-browser']); assert.deepEqual(notifications, ['open:playlist-browser', 'close:playlist-browser']);
} finally { } finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); env.restore();
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
} }
}); });
test('playlist browser open is ignored while another modal is already open', async () => { test('playlist browser open is ignored while another modal is already open', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
const previousWindow = globals.window;
const previousDocument = globals.document;
const notifications: string[] = []; const notifications: string[] = [];
let snapshotCalls = 0; let snapshotCalls = 0;
const env = setupPlaylistBrowserModalTest({
Object.defineProperty(globalThis, 'window', { electronApi: {
configurable: true, getPlaylistBrowserSnapshot: async () => {
value: { snapshotCalls += 1;
electronAPI: { return createSnapshot();
getPlaylistBrowserSnapshot: async () => { },
snapshotCalls += 1; notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
return createSnapshot(); notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
},
notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
} as unknown as ElectronAPI,
focus: () => {},
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createPlaylistRow(),
}, },
}); });
try { try {
const state = createRendererState(); const modal = env.createModal({
const overlay = {
classList: createClassList(),
focus: () => {},
};
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay,
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle: createFakeElement(),
playlistBrowserStatus: createFakeElement(),
playlistBrowserDirectoryList: createListStub(),
playlistBrowserPlaylistList: createListStub(),
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => true }, modalStateReader: { isAnyModalOpen: () => true },
syncSettingsModalSubtitleSuppression: () => {},
}); });
await modal.openPlaylistBrowserModal(); await modal.openPlaylistBrowserModal();
assert.equal(state.playlistBrowserModalOpen, false); assert.equal(env.state.playlistBrowserModalOpen, false);
assert.equal(snapshotCalls, 0); assert.equal(snapshotCalls, 0);
assert.equal(overlay.classList.contains('interactive'), false); assert.equal(env.dom.overlay.classList.contains('interactive'), false);
assert.deepEqual(notifications, []); assert.deepEqual(notifications, []);
} finally { } finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); env.restore();
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
} }
}); });