mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-12 05:16:19 -07:00
feat(character-dictionary): add manager modal and scope name matching to current media (#86)
This commit is contained in:
@@ -24,6 +24,7 @@ function createClassList(initialTokens: string[] = []) {
|
||||
}
|
||||
|
||||
function createElementStub() {
|
||||
const listeners = new Map<string, Array<(event?: { stopPropagation?: () => void }) => void>>();
|
||||
return {
|
||||
className: '',
|
||||
textContent: '',
|
||||
@@ -35,7 +36,15 @@ function createElementStub() {
|
||||
append(...children: unknown[]) {
|
||||
this.children.push(...children);
|
||||
},
|
||||
addEventListener: () => {},
|
||||
addEventListener: (
|
||||
event: string,
|
||||
listener: (event?: { stopPropagation?: () => void }) => void,
|
||||
) => {
|
||||
listeners.set(event, [...(listeners.get(event) ?? []), listener]);
|
||||
},
|
||||
dispatchEvent: (event: string, payload?: { stopPropagation?: () => void }) => {
|
||||
for (const listener of listeners.get(event) ?? []) listener(payload);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,6 +166,299 @@ test('character dictionary modal announces open before AniList refresh resolves'
|
||||
}
|
||||
});
|
||||
|
||||
test('character dictionary modal opens manager view with active entries', async () => {
|
||||
const previousWindow = globalThis.window;
|
||||
const previousDocument = globalThis.document;
|
||||
const calls: string[] = [];
|
||||
const overlay = createNodeStub();
|
||||
const modalNode = createNodeStub(true);
|
||||
const managedEntries = createNodeStub();
|
||||
const summary = createNodeStub();
|
||||
const state = createRendererState();
|
||||
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
electronAPI: {
|
||||
getCharacterDictionaryManagerSnapshot: async () => {
|
||||
calls.push('snapshot');
|
||||
return {
|
||||
entries: [
|
||||
{ mediaId: 21202, label: '21202 - KonoSuba', title: 'KonoSuba', current: true },
|
||||
{
|
||||
mediaId: 115230,
|
||||
label: '115230 - Tower of God',
|
||||
title: 'Tower of God',
|
||||
current: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
removeCharacterDictionaryManagedEntry: async () => ({ ok: true, entries: [] }),
|
||||
moveCharacterDictionaryManagedEntry: async () => ({ ok: true, entries: [] }),
|
||||
getCharacterDictionarySelection: async () => ({
|
||||
seriesKey: '',
|
||||
guessTitle: null,
|
||||
current: null,
|
||||
override: null,
|
||||
candidates: [],
|
||||
}),
|
||||
setCharacterDictionarySelection: async () => ({
|
||||
ok: false,
|
||||
seriesKey: '',
|
||||
selected: { id: 0, title: '', episodes: null },
|
||||
staleMediaIds: [],
|
||||
}),
|
||||
notifyOverlayModalClosed: () => {},
|
||||
notifyOverlayModalOpened: () => {},
|
||||
} as never,
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement: () => createElementStub(),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const modal = createCharacterDictionaryModal(
|
||||
{
|
||||
state,
|
||||
dom: {
|
||||
overlay,
|
||||
characterDictionaryModal: modalNode,
|
||||
characterDictionaryClose: createNodeStub(),
|
||||
characterDictionarySummary: summary,
|
||||
characterDictionaryCurrent: createNodeStub(),
|
||||
characterDictionarySearchInput: createNodeStub(),
|
||||
characterDictionarySearchButton: createNodeStub(),
|
||||
characterDictionaryCandidates: createNodeStub(),
|
||||
characterDictionaryStatus: createNodeStub(),
|
||||
characterDictionarySearchPanel: createNodeStub(),
|
||||
characterDictionaryManagerPanel: createNodeStub(true),
|
||||
characterDictionaryOverrideTab: createNodeStub(),
|
||||
characterDictionaryManageTab: createNodeStub(),
|
||||
characterDictionaryManagedEntries: managedEntries,
|
||||
},
|
||||
} as never,
|
||||
{
|
||||
modalStateReader: { isAnyModalOpen: () => false },
|
||||
syncSettingsModalSubtitleSuppression: () => {},
|
||||
},
|
||||
) as ReturnType<typeof createCharacterDictionaryModal> & {
|
||||
openCharacterDictionaryManagerModal: () => Promise<void>;
|
||||
};
|
||||
|
||||
await modal.openCharacterDictionaryManagerModal();
|
||||
|
||||
assert.equal(state.characterDictionaryModalOpen, true);
|
||||
assert.deepEqual(calls, ['snapshot']);
|
||||
assert.equal(managedEntries.children.length, 2);
|
||||
assert.equal(
|
||||
summary.textContent,
|
||||
'2 loaded character dictionaries. Order controls eviction priority; current dictionary stays loaded.',
|
||||
);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||
}
|
||||
});
|
||||
|
||||
test('character dictionary manager reports failed reorder IPC calls', async () => {
|
||||
const previousWindow = globalThis.window;
|
||||
const previousDocument = globalThis.document;
|
||||
const overlay = createNodeStub();
|
||||
const modalNode = createNodeStub(true);
|
||||
const managedEntries = createNodeStub();
|
||||
const status = createNodeStub();
|
||||
const state = createRendererState();
|
||||
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
electronAPI: {
|
||||
getCharacterDictionaryManagerSnapshot: async () => ({
|
||||
entries: [
|
||||
{ mediaId: 21202, label: '21202 - KonoSuba', title: 'KonoSuba', current: true },
|
||||
{
|
||||
mediaId: 115230,
|
||||
label: '115230 - Tower of God',
|
||||
title: 'Tower of God',
|
||||
current: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
moveCharacterDictionaryManagedEntry: async () => {
|
||||
throw new Error('move failed');
|
||||
},
|
||||
removeCharacterDictionaryManagedEntry: async () => ({ ok: true, entries: [] }),
|
||||
getCharacterDictionarySelection: async () => ({
|
||||
seriesKey: '',
|
||||
guessTitle: null,
|
||||
current: null,
|
||||
override: null,
|
||||
candidates: [],
|
||||
}),
|
||||
setCharacterDictionarySelection: async () => ({
|
||||
ok: false,
|
||||
seriesKey: '',
|
||||
selected: { id: 0, title: '', episodes: null },
|
||||
staleMediaIds: [],
|
||||
}),
|
||||
notifyOverlayModalClosed: () => {},
|
||||
notifyOverlayModalOpened: () => {},
|
||||
} as never,
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement: () => createElementStub(),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const modal = createCharacterDictionaryModal(
|
||||
{
|
||||
state,
|
||||
dom: {
|
||||
overlay,
|
||||
characterDictionaryModal: modalNode,
|
||||
characterDictionaryClose: createNodeStub(),
|
||||
characterDictionarySummary: createNodeStub(),
|
||||
characterDictionaryCurrent: createNodeStub(),
|
||||
characterDictionarySearchInput: createNodeStub(),
|
||||
characterDictionarySearchButton: createNodeStub(),
|
||||
characterDictionaryCandidates: createNodeStub(),
|
||||
characterDictionaryStatus: status,
|
||||
characterDictionarySearchPanel: createNodeStub(),
|
||||
characterDictionaryManagerPanel: createNodeStub(true),
|
||||
characterDictionaryOverrideTab: createNodeStub(),
|
||||
characterDictionaryManageTab: createNodeStub(),
|
||||
characterDictionaryManagedEntries: managedEntries,
|
||||
},
|
||||
} as never,
|
||||
{
|
||||
modalStateReader: { isAnyModalOpen: () => false },
|
||||
syncSettingsModalSubtitleSuppression: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
await modal.openCharacterDictionaryManagerModal();
|
||||
const secondEntry = managedEntries.children[1] as { children: unknown[] };
|
||||
const controls = secondEntry.children[1] as {
|
||||
children: Array<{ dispatchEvent: (event: string, payload?: unknown) => void }>;
|
||||
};
|
||||
controls.children[0]?.dispatchEvent('click', { stopPropagation: () => {} });
|
||||
await flushAsyncWork();
|
||||
|
||||
assert.equal(status.textContent, 'move failed');
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||
}
|
||||
});
|
||||
|
||||
test('character dictionary manager reports pending refresh after removal', async () => {
|
||||
const previousWindow = globalThis.window;
|
||||
const previousDocument = globalThis.document;
|
||||
const overlay = createNodeStub();
|
||||
const modalNode = createNodeStub(true);
|
||||
const managedEntries = createNodeStub();
|
||||
const status = createNodeStub();
|
||||
const state = createRendererState();
|
||||
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
electronAPI: {
|
||||
getCharacterDictionaryManagerSnapshot: async () => ({
|
||||
entries: [
|
||||
{ mediaId: 21202, label: '21202 - KonoSuba', title: 'KonoSuba', current: true },
|
||||
{
|
||||
mediaId: 115230,
|
||||
label: '115230 - Tower of God',
|
||||
title: 'Tower of God',
|
||||
current: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
moveCharacterDictionaryManagedEntry: async () => ({ ok: true, entries: [] }),
|
||||
removeCharacterDictionaryManagedEntry: async () => ({
|
||||
ok: true,
|
||||
entries: [
|
||||
{ mediaId: 21202, label: '21202 - KonoSuba', title: 'KonoSuba', current: true },
|
||||
],
|
||||
rebuildRequired: true,
|
||||
}),
|
||||
getCharacterDictionarySelection: async () => ({
|
||||
seriesKey: '',
|
||||
guessTitle: null,
|
||||
current: null,
|
||||
override: null,
|
||||
candidates: [],
|
||||
}),
|
||||
setCharacterDictionarySelection: async () => ({
|
||||
ok: false,
|
||||
seriesKey: '',
|
||||
selected: { id: 0, title: '', episodes: null },
|
||||
staleMediaIds: [],
|
||||
}),
|
||||
notifyOverlayModalClosed: () => {},
|
||||
notifyOverlayModalOpened: () => {},
|
||||
} as never,
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement: () => createElementStub(),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const modal = createCharacterDictionaryModal(
|
||||
{
|
||||
state,
|
||||
dom: {
|
||||
overlay,
|
||||
characterDictionaryModal: modalNode,
|
||||
characterDictionaryClose: createNodeStub(),
|
||||
characterDictionarySummary: createNodeStub(),
|
||||
characterDictionaryCurrent: createNodeStub(),
|
||||
characterDictionarySearchInput: createNodeStub(),
|
||||
characterDictionarySearchButton: createNodeStub(),
|
||||
characterDictionaryCandidates: createNodeStub(),
|
||||
characterDictionaryStatus: status,
|
||||
characterDictionarySearchPanel: createNodeStub(),
|
||||
characterDictionaryManagerPanel: createNodeStub(true),
|
||||
characterDictionaryOverrideTab: createNodeStub(),
|
||||
characterDictionaryManageTab: createNodeStub(),
|
||||
characterDictionaryManagedEntries: managedEntries,
|
||||
},
|
||||
} as never,
|
||||
{
|
||||
modalStateReader: { isAnyModalOpen: () => false },
|
||||
syncSettingsModalSubtitleSuppression: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
await modal.openCharacterDictionaryManagerModal();
|
||||
const secondEntry = managedEntries.children[1] as { children: unknown[] };
|
||||
const controls = secondEntry.children[1] as {
|
||||
children: Array<{ dispatchEvent: (event: string, payload?: unknown) => void }>;
|
||||
};
|
||||
controls.children[3]?.dispatchEvent('click', { stopPropagation: () => {} });
|
||||
await flushAsyncWork();
|
||||
|
||||
assert.equal(status.textContent, 'Entry removed. Merged dictionary will refresh shortly.');
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||
}
|
||||
});
|
||||
|
||||
test('character dictionary modal loads candidates and applies selected override', async () => {
|
||||
const previousWindow = globalThis.window;
|
||||
const previousDocument = globalThis.document;
|
||||
|
||||
Reference in New Issue
Block a user