From ba48db6255dd2f29db662a2b425a702bb13273f5 Mon Sep 17 00:00:00 2001 From: sudacode Date: Sat, 25 Apr 2026 20:03:51 -0700 Subject: [PATCH] fix(character-dictionary): normalize fallback titles and wire close button - Trim blank fallback titles in toAniListMediaCandidate; fall back to \"AniList \" when all title fields and the fallback string are empty - Add fetch unit test covering the trimmed-fallback path - Extract wireDomEvents from createCharacterDictionaryModal so event listeners are bound explicitly after construction - Call wireDomEvents in renderer init alongside other modal wiring - Extend modal test to cover close-button click dismissing the modal --- .../fetch.test.ts | 29 +++++++++++++++++++ .../character-dictionary-runtime/fetch.ts | 3 +- .../modals/character-dictionary.test.ts | 15 ++++++++-- src/renderer/modals/character-dictionary.ts | 5 +++- src/renderer/renderer.ts | 1 + 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 src/main/character-dictionary-runtime/fetch.test.ts diff --git a/src/main/character-dictionary-runtime/fetch.test.ts b/src/main/character-dictionary-runtime/fetch.test.ts new file mode 100644 index 00000000..3bb1fab4 --- /dev/null +++ b/src/main/character-dictionary-runtime/fetch.test.ts @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { searchAniListMediaCandidates } from './fetch'; + +test('searchAniListMediaCandidates trims fallback candidate titles', async () => { + const previousFetch = globalThis.fetch; + Object.defineProperty(globalThis, 'fetch', { + configurable: true, + value: async () => + new Response( + JSON.stringify({ + data: { + Page: { + media: [{ id: 21355, episodes: 25, title: {} }], + }, + }, + }), + ), + }); + + try { + const candidates = await searchAniListMediaCandidates(' Re:ZERO '); + + assert.equal(candidates[0]?.title, 'Re:ZERO'); + } finally { + Object.defineProperty(globalThis, 'fetch', { configurable: true, value: previousFetch }); + } +}); diff --git a/src/main/character-dictionary-runtime/fetch.ts b/src/main/character-dictionary-runtime/fetch.ts index 0c84b05b..17e02565 100644 --- a/src/main/character-dictionary-runtime/fetch.ts +++ b/src/main/character-dictionary-runtime/fetch.ts @@ -136,13 +136,14 @@ function toAniListMediaCandidate( }, fallbackTitle: string, ): AniListMediaCandidate { + const normalizedFallback = fallbackTitle.trim() || `AniList ${entry.id}`; return { id: entry.id, title: entry.title?.english?.trim() || entry.title?.romaji?.trim() || entry.title?.native?.trim() || - fallbackTitle, + normalizedFallback, episodes: typeof entry.episodes === 'number' && entry.episodes > 0 ? entry.episodes : null, }; } diff --git a/src/renderer/modals/character-dictionary.test.ts b/src/renderer/modals/character-dictionary.test.ts index 7ef9aa20..ac8afbd4 100644 --- a/src/renderer/modals/character-dictionary.test.ts +++ b/src/renderer/modals/character-dictionary.test.ts @@ -38,12 +38,18 @@ function createElementStub() { } function createNodeStub(hidden = false) { + const listeners = new Map void>>(); return { textContent: '', children: [] as unknown[], classList: createClassList(hidden ? ['hidden'] : []), setAttribute: () => {}, - addEventListener: () => {}, + addEventListener: (event: string, listener: () => void) => { + listeners.set(event, [...(listeners.get(event) ?? []), listener]); + }, + dispatchEvent: (event: string) => { + for (const listener of listeners.get(event) ?? []) listener(); + }, replaceChildren(...children: unknown[]) { this.children = [...children]; }, @@ -69,6 +75,7 @@ test('character dictionary modal loads candidates and applies selected override' const calls: number[] = []; const overlay = createNodeStub(); const modalNode = createNodeStub(true); + const closeButton = createNodeStub(); const candidates = createNodeStub(); const status = createNodeStub(); const state = createRendererState(); @@ -110,7 +117,7 @@ test('character dictionary modal loads candidates and applies selected override' dom: { overlay, characterDictionaryModal: modalNode, - characterDictionaryClose: createNodeStub(), + characterDictionaryClose: closeButton, characterDictionarySummary: createNodeStub(), characterDictionaryCurrent: createNodeStub(), characterDictionaryCandidates: candidates, @@ -122,6 +129,7 @@ test('character dictionary modal loads candidates and applies selected override' syncSettingsModalSubtitleSuppression: () => {}, }, ); + modal.wireDomEvents(); await modal.openCharacterDictionaryModal(); assert.equal(state.characterDictionaryModalOpen, true); @@ -137,6 +145,9 @@ test('character dictionary modal loads candidates and applies selected override' assert.deepEqual(calls, [21355]); assert.match(status.textContent, /Override saved/); + + closeButton.dispatchEvent('click'); + assert.equal(state.characterDictionaryModalOpen, false); } finally { Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument }); diff --git a/src/renderer/modals/character-dictionary.ts b/src/renderer/modals/character-dictionary.ts index a8964cf7..0837491f 100644 --- a/src/renderer/modals/character-dictionary.ts +++ b/src/renderer/modals/character-dictionary.ts @@ -214,11 +214,14 @@ export function createCharacterDictionaryModal( return false; } - ctx.dom.characterDictionaryClose.addEventListener('click', closeCharacterDictionaryModal); + function wireDomEvents(): void { + ctx.dom.characterDictionaryClose.addEventListener('click', closeCharacterDictionaryModal); + } return { openCharacterDictionaryModal, closeCharacterDictionaryModal, handleCharacterDictionaryKeydown, + wireDomEvents, }; } diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 5c50f3b5..51bcc146 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -657,6 +657,7 @@ async function init(): Promise { controllerDebugModal.wireDomEvents(); sessionHelpModal.wireDomEvents(); subtitleSidebarModal.wireDomEvents(); + characterDictionaryModal.wireDomEvents(); window.addEventListener('beforeunload', () => { subtitleSidebarModal.disposeDomEvents(); });