mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-01 18:12:06 -07:00
fix: address CodeRabbit PR feedback
This commit is contained in:
@@ -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 -->
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user