mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-26 16:19:26 -07:00
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:
29
src/main/character-dictionary-runtime/fetch.test.ts
Normal file
29
src/main/character-dictionary-runtime/fetch.test.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -214,11 +214,14 @@ export function createCharacterDictionaryModal(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.dom.characterDictionaryClose.addEventListener('click', closeCharacterDictionaryModal);
|
function wireDomEvents(): void {
|
||||||
|
ctx.dom.characterDictionaryClose.addEventListener('click', closeCharacterDictionaryModal);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openCharacterDictionaryModal,
|
openCharacterDictionaryModal,
|
||||||
closeCharacterDictionaryModal,
|
closeCharacterDictionaryModal,
|
||||||
handleCharacterDictionaryKeydown,
|
handleCharacterDictionaryKeydown,
|
||||||
|
wireDomEvents,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user