From 3b0b49cf544a3ca0c2d8e0fb73cc78b36858528f Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 18 Sep 2026 13:09:52 -0700 Subject: [PATCH] fix(jimaku): ignore stale media info responses - Prevent closed or superseded modals from applying late media info - Add regression coverage --- src/renderer/modals/jimaku.test.ts | 85 ++++++++++++++++++++++++++++++ src/renderer/modals/jimaku.ts | 5 ++ 2 files changed, 90 insertions(+) diff --git a/src/renderer/modals/jimaku.test.ts b/src/renderer/modals/jimaku.test.ts index 5e59757e..e8e59ed7 100644 --- a/src/renderer/modals/jimaku.test.ts +++ b/src/renderer/modals/jimaku.test.ts @@ -488,3 +488,88 @@ test('a slow files reply for a previously selected entry is ignored', async () = Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument }); } }); + +test('media info arriving after the modal closed does not fill inputs or search', async () => { + const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown }; + const previousWindow = globals.window; + const previousDocument = globals.document; + + let resolveMediaInfo!: (info: unknown) => void; + let searchCalls = 0; + const electronAPI = { + getJimakuMediaInfo: () => + new Promise((resolve) => { + resolveMediaInfo = resolve; + }), + jimakuSearchEntries: async () => { + searchCalls += 1; + return { ok: true, data: [] }; + }, + notifyOverlayModalClosed: () => {}, + } as unknown as ElectronAPI; + + Object.defineProperty(globalThis, 'window', { + configurable: true, + value: { electronAPI }, + }); + Object.defineProperty(globalThis, 'document', { + configurable: true, + value: { + activeElement: null, + createElement: () => createElementStub(), + }, + }); + + try { + const state = createRendererState(); + const titleInput = { value: '' }; + const status = { textContent: '', style: { color: '' } }; + + const ctx = { + dom: { + overlay: { classList: createClassList() }, + jimakuModal: { classList: createClassList(['hidden']), setAttribute: () => {} }, + jimakuTitleInput: titleInput, + jimakuSeasonInput: { value: '' }, + jimakuEpisodeInput: { value: '' }, + jimakuSearchButton: { addEventListener: () => {} }, + jimakuCloseButton: { addEventListener: () => {} }, + jimakuStatus: status, + jimakuEntriesSection: { classList: createClassList(['hidden']) }, + jimakuEntriesList: createListStub(), + jimakuFilesSection: { classList: createClassList(['hidden']) }, + jimakuFilesList: createListStub(), + jimakuBroadenButton: { classList: createClassList(['hidden']), addEventListener: () => {} }, + jimakuTabAnimeButton: { classList: createClassList(['active']), setAttribute: () => {} }, + jimakuTabLiveActionButton: { classList: createClassList(), setAttribute: () => {} }, + }, + state, + }; + + const jimakuModal = createJimakuModal(ctx as never, { + modalStateReader: { isAnyModalOpen: () => false }, + syncSettingsModalSubtitleSuppression: () => {}, + }); + + jimakuModal.openJimakuModal(); + await flushAsyncWork(); + jimakuModal.closeJimakuModal(); + + resolveMediaInfo({ + title: 'Shinzanmono', + season: 1, + episode: 3, + confidence: 'high', + filename: 'Shinzanmono S01E03.mkv', + rawTitle: 'Shinzanmono S01E03', + }); + await flushAsyncWork(); + + assert.equal(titleInput.value, ''); + assert.equal(searchCalls, 0); + assert.equal(status.textContent, 'Loading media info...'); + } finally { + Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); + Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument }); + } +}); diff --git a/src/renderer/modals/jimaku.ts b/src/renderer/modals/jimaku.ts index 009a7097..086baf92 100644 --- a/src/renderer/modals/jimaku.ts +++ b/src/renderer/modals/jimaku.ts @@ -307,9 +307,13 @@ export function createJimakuModal( resetJimakuLists(); renderTabs(); + // Media info can resolve after the user already closed the modal or + // started their own search; a stale reply must not touch the inputs. + const generation = searchGeneration; window.electronAPI .getJimakuMediaInfo() .then((info: JimakuMediaInfo) => { + if (generation !== searchGeneration) return; ctx.dom.jimakuTitleInput.value = info.title || ''; ctx.dom.jimakuSeasonInput.value = info.season ? String(info.season) : ''; ctx.dom.jimakuEpisodeInput.value = info.episode ? String(info.episode) : ''; @@ -324,6 +328,7 @@ export function createJimakuModal( } }) .catch(() => { + if (generation !== searchGeneration) return; setJimakuStatus('Failed to load media info.', true); }); }