fix(character-dictionary): normalize fallback titles and wire close button

- Trim blank fallback titles in toAniListMediaCandidate; fall back to
  \"AniList <id>\" 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
This commit is contained in:
2026-04-25 20:03:51 -07:00
parent 992856ac5e
commit ba48db6255
5 changed files with 49 additions and 4 deletions

View File

@@ -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 });
}
});

View File

@@ -136,13 +136,14 @@ function toAniListMediaCandidate(
}, },
fallbackTitle: string, fallbackTitle: string,
): AniListMediaCandidate { ): AniListMediaCandidate {
const normalizedFallback = fallbackTitle.trim() || `AniList ${entry.id}`;
return { return {
id: entry.id, id: entry.id,
title: title:
entry.title?.english?.trim() || entry.title?.english?.trim() ||
entry.title?.romaji?.trim() || entry.title?.romaji?.trim() ||
entry.title?.native?.trim() || entry.title?.native?.trim() ||
fallbackTitle, normalizedFallback,
episodes: typeof entry.episodes === 'number' && entry.episodes > 0 ? entry.episodes : null, episodes: typeof entry.episodes === 'number' && entry.episodes > 0 ? entry.episodes : null,
}; };
} }

View File

@@ -38,12 +38,18 @@ function createElementStub() {
} }
function createNodeStub(hidden = false) { function createNodeStub(hidden = false) {
const listeners = new Map<string, Array<() => void>>();
return { return {
textContent: '', textContent: '',
children: [] as unknown[], children: [] as unknown[],
classList: createClassList(hidden ? ['hidden'] : []), classList: createClassList(hidden ? ['hidden'] : []),
setAttribute: () => {}, 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[]) { replaceChildren(...children: unknown[]) {
this.children = [...children]; this.children = [...children];
}, },
@@ -69,6 +75,7 @@ test('character dictionary modal loads candidates and applies selected override'
const calls: number[] = []; const calls: number[] = [];
const overlay = createNodeStub(); const overlay = createNodeStub();
const modalNode = createNodeStub(true); const modalNode = createNodeStub(true);
const closeButton = createNodeStub();
const candidates = createNodeStub(); const candidates = createNodeStub();
const status = createNodeStub(); const status = createNodeStub();
const state = createRendererState(); const state = createRendererState();
@@ -110,7 +117,7 @@ test('character dictionary modal loads candidates and applies selected override'
dom: { dom: {
overlay, overlay,
characterDictionaryModal: modalNode, characterDictionaryModal: modalNode,
characterDictionaryClose: createNodeStub(), characterDictionaryClose: closeButton,
characterDictionarySummary: createNodeStub(), characterDictionarySummary: createNodeStub(),
characterDictionaryCurrent: createNodeStub(), characterDictionaryCurrent: createNodeStub(),
characterDictionaryCandidates: candidates, characterDictionaryCandidates: candidates,
@@ -122,6 +129,7 @@ test('character dictionary modal loads candidates and applies selected override'
syncSettingsModalSubtitleSuppression: () => {}, syncSettingsModalSubtitleSuppression: () => {},
}, },
); );
modal.wireDomEvents();
await modal.openCharacterDictionaryModal(); await modal.openCharacterDictionaryModal();
assert.equal(state.characterDictionaryModalOpen, true); assert.equal(state.characterDictionaryModalOpen, true);
@@ -137,6 +145,9 @@ test('character dictionary modal loads candidates and applies selected override'
assert.deepEqual(calls, [21355]); assert.deepEqual(calls, [21355]);
assert.match(status.textContent, /Override saved/); assert.match(status.textContent, /Override saved/);
closeButton.dispatchEvent('click');
assert.equal(state.characterDictionaryModalOpen, false);
} finally { } finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument }); Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });

View File

@@ -214,11 +214,14 @@ export function createCharacterDictionaryModal(
return false; return false;
} }
function wireDomEvents(): void {
ctx.dom.characterDictionaryClose.addEventListener('click', closeCharacterDictionaryModal); ctx.dom.characterDictionaryClose.addEventListener('click', closeCharacterDictionaryModal);
}
return { return {
openCharacterDictionaryModal, openCharacterDictionaryModal,
closeCharacterDictionaryModal, closeCharacterDictionaryModal,
handleCharacterDictionaryKeydown, handleCharacterDictionaryKeydown,
wireDomEvents,
}; };
} }

View File

@@ -657,6 +657,7 @@ async function init(): Promise<void> {
controllerDebugModal.wireDomEvents(); controllerDebugModal.wireDomEvents();
sessionHelpModal.wireDomEvents(); sessionHelpModal.wireDomEvents();
subtitleSidebarModal.wireDomEvents(); subtitleSidebarModal.wireDomEvents();
characterDictionaryModal.wireDomEvents();
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
subtitleSidebarModal.disposeDomEvents(); subtitleSidebarModal.disposeDomEvents();
}); });