mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-27 18:12:05 -07:00
Add backlog tasks and launcher time helper tests
- Track follow-up cleanup work in Backlog.md - Replace Date.now usage with shared nowMs helper - Add launcher args/parser and core regression tests
This commit is contained in:
201
src/anki-integration/field-grouping-merge.test.ts
Normal file
201
src/anki-integration/field-grouping-merge.test.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
FieldGroupingMergeCollaborator,
|
||||
type FieldGroupingMergeNoteInfo,
|
||||
} from './field-grouping-merge';
|
||||
import type { AnkiConnectConfig } from '../types/anki';
|
||||
|
||||
function resolveFieldName(availableFieldNames: string[], preferredName: string): string | null {
|
||||
return (
|
||||
availableFieldNames.find(
|
||||
(name) => name === preferredName || name.toLowerCase() === preferredName.toLowerCase(),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function createCollaborator(
|
||||
options: {
|
||||
config?: Partial<AnkiConnectConfig>;
|
||||
currentSubtitleText?: string;
|
||||
generatedMedia?: {
|
||||
audioField?: string;
|
||||
audioValue?: string;
|
||||
imageField?: string;
|
||||
imageValue?: string;
|
||||
miscInfoValue?: string;
|
||||
};
|
||||
warnings?: Array<{ fieldName: string; reason: string; detail?: string }>;
|
||||
} = {},
|
||||
) {
|
||||
const warnings = options.warnings ?? [];
|
||||
const config = {
|
||||
fields: {
|
||||
sentence: 'Sentence',
|
||||
audio: 'ExpressionAudio',
|
||||
image: 'Picture',
|
||||
miscInfo: 'MiscInfo',
|
||||
...(options.config?.fields ?? {}),
|
||||
},
|
||||
...(options.config ?? {}),
|
||||
} as AnkiConnectConfig;
|
||||
|
||||
return {
|
||||
collaborator: new FieldGroupingMergeCollaborator({
|
||||
getConfig: () => config,
|
||||
getEffectiveSentenceCardConfig: () => ({
|
||||
sentenceField: 'Sentence',
|
||||
audioField: 'SentenceAudio',
|
||||
}),
|
||||
getCurrentSubtitleText: () => options.currentSubtitleText,
|
||||
resolveFieldName,
|
||||
resolveNoteFieldName: (noteInfo, preferredName) => {
|
||||
if (!preferredName) return null;
|
||||
return resolveFieldName(Object.keys(noteInfo.fields), preferredName);
|
||||
},
|
||||
extractFields: (fields) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(fields).map(([key, value]) => [key.toLowerCase(), value.value || '']),
|
||||
),
|
||||
processSentence: (mpvSentence) => `${mpvSentence}::processed`,
|
||||
generateMediaForMerge: async () => options.generatedMedia ?? {},
|
||||
warnFieldParseOnce: (fieldName, reason, detail) => {
|
||||
warnings.push({ fieldName, reason, detail });
|
||||
},
|
||||
}),
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
function makeNote(noteId: number, fields: Record<string, string>): FieldGroupingMergeNoteInfo {
|
||||
return {
|
||||
noteId,
|
||||
fields: Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, { value }])),
|
||||
};
|
||||
}
|
||||
|
||||
test('getGroupableFieldNames includes configured fields without duplicating ExpressionAudio', () => {
|
||||
const { collaborator } = createCollaborator({
|
||||
config: {
|
||||
fields: {
|
||||
image: 'Illustration',
|
||||
sentence: 'SentenceText',
|
||||
audio: 'ExpressionAudio',
|
||||
miscInfo: 'ExtraInfo',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(collaborator.getGroupableFieldNames(), [
|
||||
'Sentence',
|
||||
'SentenceAudio',
|
||||
'Picture',
|
||||
'Illustration',
|
||||
'SentenceText',
|
||||
'ExtraInfo',
|
||||
'SentenceFurigana',
|
||||
]);
|
||||
});
|
||||
|
||||
test('computeFieldGroupingMergedFields syncs a custom audio field from merged SentenceAudio', async () => {
|
||||
const { collaborator } = createCollaborator({
|
||||
config: {
|
||||
fields: {
|
||||
audio: 'CustomAudio',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const merged = await collaborator.computeFieldGroupingMergedFields(
|
||||
1,
|
||||
2,
|
||||
makeNote(1, {
|
||||
SentenceAudio: '[sound:keep.mp3]',
|
||||
CustomAudio: '[sound:stale.mp3]',
|
||||
}),
|
||||
makeNote(2, {
|
||||
SentenceAudio: '[sound:new.mp3]',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
merged.SentenceAudio,
|
||||
'<span data-group-id="1">[sound:keep.mp3]</span><span data-group-id="2">[sound:new.mp3]</span>',
|
||||
);
|
||||
assert.equal(merged.CustomAudio, merged.SentenceAudio);
|
||||
});
|
||||
|
||||
test('computeFieldGroupingMergedFields keeps strict fields when source is empty and warns on malformed spans', async () => {
|
||||
const { collaborator, warnings } = createCollaborator({
|
||||
currentSubtitleText: 'subtitle line',
|
||||
});
|
||||
|
||||
const merged = await collaborator.computeFieldGroupingMergedFields(
|
||||
3,
|
||||
4,
|
||||
makeNote(3, {
|
||||
Sentence: '<span data-group-id="abc">keep sentence</span>',
|
||||
SentenceAudio: '',
|
||||
}),
|
||||
makeNote(4, {
|
||||
Sentence: 'source sentence',
|
||||
SentenceAudio: '[sound:source.mp3]',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
merged.Sentence,
|
||||
'<span data-group-id="3"><span data-group-id="abc">keep sentence</span></span><span data-group-id="4">source sentence</span>',
|
||||
);
|
||||
assert.equal(merged.SentenceAudio, '<span data-group-id="4">[sound:source.mp3]</span>');
|
||||
assert.equal(warnings.length, 4);
|
||||
assert.deepEqual(
|
||||
warnings.map((entry) => entry.reason),
|
||||
['invalid-group-id', 'no-usable-span-entries', 'invalid-group-id', 'no-usable-span-entries'],
|
||||
);
|
||||
});
|
||||
|
||||
test('computeFieldGroupingMergedFields uses generated media only when includeGeneratedMedia is true', async () => {
|
||||
const generatedMedia = {
|
||||
audioField: 'SentenceAudio',
|
||||
audioValue: '[sound:generated.mp3]',
|
||||
imageField: 'Picture',
|
||||
imageValue: '<img src="generated.png">',
|
||||
miscInfoValue: 'generated misc',
|
||||
};
|
||||
const { collaborator: withoutGenerated } = createCollaborator({ generatedMedia });
|
||||
const { collaborator: withGenerated } = createCollaborator({ generatedMedia });
|
||||
|
||||
const keep = makeNote(10, {
|
||||
SentenceAudio: '',
|
||||
Picture: '',
|
||||
MiscInfo: '',
|
||||
});
|
||||
const source = makeNote(11, {
|
||||
SentenceAudio: '',
|
||||
Picture: '',
|
||||
MiscInfo: '',
|
||||
});
|
||||
|
||||
const without = await withoutGenerated.computeFieldGroupingMergedFields(
|
||||
10,
|
||||
11,
|
||||
keep,
|
||||
source,
|
||||
false,
|
||||
);
|
||||
const withMedia = await withGenerated.computeFieldGroupingMergedFields(
|
||||
10,
|
||||
11,
|
||||
keep,
|
||||
source,
|
||||
true,
|
||||
);
|
||||
|
||||
assert.deepEqual(without, {});
|
||||
assert.equal(withMedia.SentenceAudio, '<span data-group-id="11">[sound:generated.mp3]</span>');
|
||||
assert.equal(withMedia.Picture, '<img data-group-id="11" src="generated.png">');
|
||||
assert.equal(withMedia.MiscInfo, '<span data-group-id="11">generated misc</span>');
|
||||
});
|
||||
411
src/anki-integration/field-grouping.test.ts
Normal file
411
src/anki-integration/field-grouping.test.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { FieldGroupingService } from './field-grouping';
|
||||
import type { KikuMergePreviewResponse } from '../types/anki';
|
||||
|
||||
type NoteInfo = {
|
||||
noteId: number;
|
||||
fields: Record<string, { value: string }>;
|
||||
};
|
||||
|
||||
function createHarness(
|
||||
options: {
|
||||
kikuEnabled?: boolean;
|
||||
kikuFieldGrouping?: 'auto' | 'manual' | 'disabled';
|
||||
deck?: string;
|
||||
noteIds?: number[];
|
||||
notesInfo?: NoteInfo[][];
|
||||
duplicateNoteId?: number | null;
|
||||
hasAllConfiguredFields?: boolean;
|
||||
manualHandled?: boolean;
|
||||
expression?: string | null;
|
||||
currentSentenceImageField?: string | undefined;
|
||||
onProcessNewCard?: (noteId: number, options?: { skipKikuFieldGrouping?: boolean }) => void;
|
||||
} = {},
|
||||
) {
|
||||
const calls: string[] = [];
|
||||
const findNotesQueries: Array<{ query: string; maxRetries?: number }> = [];
|
||||
const noteInfoRequests: number[][] = [];
|
||||
const duplicateRequests: Array<{ expression: string; excludeNoteId: number }> = [];
|
||||
const processCalls: Array<{ noteId: number; options?: { skipKikuFieldGrouping?: boolean } }> = [];
|
||||
const autoCalls: Array<{ originalNoteId: number; newNoteId: number; expression: string }> = [];
|
||||
const manualCalls: Array<{ originalNoteId: number; newNoteId: number; expression: string }> = [];
|
||||
|
||||
const noteInfoQueue = [...(options.notesInfo ?? [])];
|
||||
const notes = options.noteIds ?? [2];
|
||||
|
||||
const service = new FieldGroupingService({
|
||||
getConfig: () => ({
|
||||
fields: {
|
||||
word: 'Expression',
|
||||
},
|
||||
}),
|
||||
getEffectiveSentenceCardConfig: () => ({
|
||||
model: 'Sentence',
|
||||
sentenceField: 'Sentence',
|
||||
audioField: 'SentenceAudio',
|
||||
lapisEnabled: false,
|
||||
kikuEnabled: options.kikuEnabled ?? true,
|
||||
kikuFieldGrouping: options.kikuFieldGrouping ?? 'auto',
|
||||
kikuDeleteDuplicateInAuto: true,
|
||||
}),
|
||||
isUpdateInProgress: () => false,
|
||||
getDeck: options.deck ? () => options.deck : undefined,
|
||||
withUpdateProgress: async (_message, action) => {
|
||||
calls.push('withUpdateProgress');
|
||||
return action();
|
||||
},
|
||||
showOsdNotification: (text) => {
|
||||
calls.push(`osd:${text}`);
|
||||
},
|
||||
findNotes: async (query, findNotesOptions) => {
|
||||
findNotesQueries.push({ query, maxRetries: findNotesOptions?.maxRetries });
|
||||
return notes;
|
||||
},
|
||||
notesInfo: async (noteIds) => {
|
||||
noteInfoRequests.push([...noteIds]);
|
||||
return noteInfoQueue.shift() ?? [];
|
||||
},
|
||||
extractFields: (fields) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(fields).map(([key, value]) => [key.toLowerCase(), value.value || '']),
|
||||
),
|
||||
findDuplicateNote: async (expression, excludeNoteId) => {
|
||||
duplicateRequests.push({ expression, excludeNoteId });
|
||||
return options.duplicateNoteId ?? 99;
|
||||
},
|
||||
hasAllConfiguredFields: () => options.hasAllConfiguredFields ?? true,
|
||||
processNewCard: async (noteId, processOptions) => {
|
||||
processCalls.push({ noteId, options: processOptions });
|
||||
options.onProcessNewCard?.(noteId, processOptions);
|
||||
},
|
||||
getSentenceCardImageFieldName: () => options.currentSentenceImageField,
|
||||
resolveFieldName: (availableFieldNames, preferredName) =>
|
||||
availableFieldNames.find(
|
||||
(name) => name === preferredName || name.toLowerCase() === preferredName.toLowerCase(),
|
||||
) ?? null,
|
||||
computeFieldGroupingMergedFields: async () => ({}),
|
||||
getNoteFieldMap: (noteInfo) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(noteInfo.fields).map(([key, value]) => [key, value.value || '']),
|
||||
),
|
||||
handleFieldGroupingAuto: async (originalNoteId, newNoteId, _newNoteInfo, expression) => {
|
||||
autoCalls.push({ originalNoteId, newNoteId, expression });
|
||||
},
|
||||
handleFieldGroupingManual: async (originalNoteId, newNoteId, _newNoteInfo, expression) => {
|
||||
manualCalls.push({ originalNoteId, newNoteId, expression });
|
||||
return options.manualHandled ?? true;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
service,
|
||||
calls,
|
||||
findNotesQueries,
|
||||
noteInfoRequests,
|
||||
duplicateRequests,
|
||||
processCalls,
|
||||
autoCalls,
|
||||
manualCalls,
|
||||
};
|
||||
}
|
||||
|
||||
type SuccessfulPreview = KikuMergePreviewResponse & {
|
||||
ok: true;
|
||||
compact: {
|
||||
action: {
|
||||
keepNoteId: number;
|
||||
deleteNoteId: number;
|
||||
deleteDuplicate: boolean;
|
||||
};
|
||||
mergedFields: Record<string, string>;
|
||||
};
|
||||
full: {
|
||||
result: {
|
||||
wouldDeleteNoteId: number | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
test('triggerFieldGroupingForLastAddedCard stops when kiku mode is disabled', async () => {
|
||||
const harness = createHarness({ kikuEnabled: false });
|
||||
|
||||
await harness.service.triggerFieldGroupingForLastAddedCard();
|
||||
|
||||
assert.deepEqual(harness.calls, ['osd:Kiku mode is not enabled']);
|
||||
assert.equal(harness.findNotesQueries.length, 0);
|
||||
});
|
||||
|
||||
test('triggerFieldGroupingForLastAddedCard stops when field grouping is disabled', async () => {
|
||||
const harness = createHarness({ kikuFieldGrouping: 'disabled' });
|
||||
|
||||
await harness.service.triggerFieldGroupingForLastAddedCard();
|
||||
|
||||
assert.deepEqual(harness.calls, ['osd:Kiku field grouping is disabled']);
|
||||
assert.equal(harness.findNotesQueries.length, 0);
|
||||
});
|
||||
|
||||
test('triggerFieldGroupingForLastAddedCard stops when an update is already in progress', async () => {
|
||||
const service = new FieldGroupingService({
|
||||
getConfig: () => ({ fields: { word: 'Expression' } }),
|
||||
getEffectiveSentenceCardConfig: () => ({
|
||||
model: 'Sentence',
|
||||
sentenceField: 'Sentence',
|
||||
audioField: 'SentenceAudio',
|
||||
lapisEnabled: false,
|
||||
kikuEnabled: true,
|
||||
kikuFieldGrouping: 'auto',
|
||||
kikuDeleteDuplicateInAuto: true,
|
||||
}),
|
||||
isUpdateInProgress: () => true,
|
||||
withUpdateProgress: async () => {
|
||||
throw new Error('should not be called');
|
||||
},
|
||||
showOsdNotification: () => {},
|
||||
findNotes: async () => [],
|
||||
notesInfo: async () => [],
|
||||
extractFields: () => ({}),
|
||||
findDuplicateNote: async () => null,
|
||||
hasAllConfiguredFields: () => true,
|
||||
processNewCard: async () => {},
|
||||
getSentenceCardImageFieldName: () => undefined,
|
||||
resolveFieldName: () => null,
|
||||
computeFieldGroupingMergedFields: async () => ({}),
|
||||
getNoteFieldMap: () => ({}),
|
||||
handleFieldGroupingAuto: async () => {},
|
||||
handleFieldGroupingManual: async () => true,
|
||||
});
|
||||
|
||||
await service.triggerFieldGroupingForLastAddedCard();
|
||||
});
|
||||
|
||||
test('triggerFieldGroupingForLastAddedCard finds the newest note and hands off to auto grouping', async () => {
|
||||
const harness = createHarness({
|
||||
deck: 'Anime Deck',
|
||||
noteIds: [3, 7, 5],
|
||||
notesInfo: [
|
||||
[
|
||||
{
|
||||
noteId: 7,
|
||||
fields: {
|
||||
Expression: { value: 'word-7' },
|
||||
Sentence: { value: 'line-7' },
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
noteId: 7,
|
||||
fields: {
|
||||
Expression: { value: 'word-7' },
|
||||
Sentence: { value: 'line-7' },
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
duplicateNoteId: 42,
|
||||
hasAllConfiguredFields: true,
|
||||
});
|
||||
|
||||
await harness.service.triggerFieldGroupingForLastAddedCard();
|
||||
|
||||
assert.deepEqual(harness.findNotesQueries, [
|
||||
{ query: '"deck:Anime Deck" added:1', maxRetries: undefined },
|
||||
]);
|
||||
assert.deepEqual(harness.noteInfoRequests, [[7], [7]]);
|
||||
assert.deepEqual(harness.duplicateRequests, [{ expression: 'word-7', excludeNoteId: 7 }]);
|
||||
assert.deepEqual(harness.autoCalls, [
|
||||
{
|
||||
originalNoteId: 42,
|
||||
newNoteId: 7,
|
||||
expression: 'word-7',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('triggerFieldGroupingForLastAddedCard refreshes the card when configured fields are missing', async () => {
|
||||
const processCalls: Array<{ noteId: number; options?: { skipKikuFieldGrouping?: boolean } }> = [];
|
||||
const harness = createHarness({
|
||||
noteIds: [11],
|
||||
notesInfo: [
|
||||
[
|
||||
{
|
||||
noteId: 11,
|
||||
fields: {
|
||||
Expression: { value: 'word-11' },
|
||||
Sentence: { value: 'line-11' },
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
noteId: 11,
|
||||
fields: {
|
||||
Expression: { value: 'word-11' },
|
||||
Sentence: { value: 'line-11' },
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
duplicateNoteId: 13,
|
||||
hasAllConfiguredFields: false,
|
||||
onProcessNewCard: (noteId, options) => {
|
||||
processCalls.push({ noteId, options });
|
||||
},
|
||||
});
|
||||
|
||||
await harness.service.triggerFieldGroupingForLastAddedCard();
|
||||
|
||||
assert.deepEqual(processCalls, [{ noteId: 11, options: { skipKikuFieldGrouping: true } }]);
|
||||
assert.deepEqual(harness.manualCalls, []);
|
||||
});
|
||||
|
||||
test('triggerFieldGroupingForLastAddedCard shows a cancellation message when manual grouping is declined', async () => {
|
||||
const harness = createHarness({
|
||||
kikuFieldGrouping: 'manual',
|
||||
noteIds: [9],
|
||||
notesInfo: [
|
||||
[
|
||||
{
|
||||
noteId: 9,
|
||||
fields: {
|
||||
Expression: { value: 'word-9' },
|
||||
Sentence: { value: 'line-9' },
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
noteId: 9,
|
||||
fields: {
|
||||
Expression: { value: 'word-9' },
|
||||
Sentence: { value: 'line-9' },
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
duplicateNoteId: 77,
|
||||
manualHandled: false,
|
||||
});
|
||||
|
||||
await harness.service.triggerFieldGroupingForLastAddedCard();
|
||||
|
||||
assert.deepEqual(harness.manualCalls, [
|
||||
{
|
||||
originalNoteId: 77,
|
||||
newNoteId: 9,
|
||||
expression: 'word-9',
|
||||
},
|
||||
]);
|
||||
assert.equal(harness.calls.at(-1), 'osd:Field grouping cancelled');
|
||||
});
|
||||
|
||||
test('buildFieldGroupingPreview returns merged compact and full previews', async () => {
|
||||
const service = new FieldGroupingService({
|
||||
getConfig: () => ({ fields: { word: 'Expression' } }),
|
||||
getEffectiveSentenceCardConfig: () => ({
|
||||
model: 'Sentence',
|
||||
sentenceField: 'Sentence',
|
||||
audioField: 'SentenceAudio',
|
||||
lapisEnabled: false,
|
||||
kikuEnabled: true,
|
||||
kikuFieldGrouping: 'auto',
|
||||
kikuDeleteDuplicateInAuto: true,
|
||||
}),
|
||||
isUpdateInProgress: () => false,
|
||||
withUpdateProgress: async (_message, action) => action(),
|
||||
showOsdNotification: () => {},
|
||||
findNotes: async () => [],
|
||||
notesInfo: async (noteIds) =>
|
||||
noteIds.map((noteId) => ({
|
||||
noteId,
|
||||
fields: {
|
||||
Sentence: { value: `sentence-${noteId}` },
|
||||
SentenceAudio: { value: `[sound:${noteId}.mp3]` },
|
||||
Picture: { value: `<img src="${noteId}.png">` },
|
||||
MiscInfo: { value: `misc-${noteId}` },
|
||||
},
|
||||
})),
|
||||
extractFields: () => ({}),
|
||||
findDuplicateNote: async () => null,
|
||||
hasAllConfiguredFields: () => true,
|
||||
processNewCard: async () => {},
|
||||
getSentenceCardImageFieldName: () => undefined,
|
||||
resolveFieldName: (availableFieldNames, preferredName) =>
|
||||
availableFieldNames.find(
|
||||
(name) => name === preferredName || name.toLowerCase() === preferredName.toLowerCase(),
|
||||
) ?? null,
|
||||
computeFieldGroupingMergedFields: async () => ({
|
||||
Sentence: 'merged sentence',
|
||||
SentenceAudio: 'merged audio',
|
||||
Picture: 'merged picture',
|
||||
MiscInfo: 'merged misc',
|
||||
}),
|
||||
getNoteFieldMap: (noteInfo) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(noteInfo.fields).map(([key, value]) => [key, value.value || '']),
|
||||
),
|
||||
handleFieldGroupingAuto: async () => {},
|
||||
handleFieldGroupingManual: async () => true,
|
||||
});
|
||||
|
||||
const preview = await service.buildFieldGroupingPreview(1, 2, true);
|
||||
|
||||
assert.equal(preview.ok, true);
|
||||
if (!preview.ok) {
|
||||
throw new Error(preview.error);
|
||||
}
|
||||
const successPreview = preview as SuccessfulPreview;
|
||||
assert.deepEqual(successPreview.compact.action, {
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
deleteDuplicate: true,
|
||||
});
|
||||
assert.equal(successPreview.compact.mergedFields.Sentence, 'merged sentence');
|
||||
assert.equal(successPreview.full.result.wouldDeleteNoteId, 2);
|
||||
});
|
||||
|
||||
test('buildFieldGroupingPreview reports missing notes cleanly', async () => {
|
||||
const service = new FieldGroupingService({
|
||||
getConfig: () => ({ fields: { word: 'Expression' } }),
|
||||
getEffectiveSentenceCardConfig: () => ({
|
||||
model: 'Sentence',
|
||||
sentenceField: 'Sentence',
|
||||
audioField: 'SentenceAudio',
|
||||
lapisEnabled: false,
|
||||
kikuEnabled: true,
|
||||
kikuFieldGrouping: 'auto',
|
||||
kikuDeleteDuplicateInAuto: true,
|
||||
}),
|
||||
isUpdateInProgress: () => false,
|
||||
withUpdateProgress: async (_message, action) => action(),
|
||||
showOsdNotification: () => {},
|
||||
findNotes: async () => [],
|
||||
notesInfo: async () => [
|
||||
{
|
||||
noteId: 1,
|
||||
fields: {
|
||||
Sentence: { value: 'sentence-1' },
|
||||
},
|
||||
},
|
||||
],
|
||||
extractFields: () => ({}),
|
||||
findDuplicateNote: async () => null,
|
||||
hasAllConfiguredFields: () => true,
|
||||
processNewCard: async () => {},
|
||||
getSentenceCardImageFieldName: () => undefined,
|
||||
resolveFieldName: () => null,
|
||||
computeFieldGroupingMergedFields: async () => ({}),
|
||||
getNoteFieldMap: () => ({}),
|
||||
handleFieldGroupingAuto: async () => {},
|
||||
handleFieldGroupingManual: async () => true,
|
||||
});
|
||||
|
||||
const preview = await service.buildFieldGroupingPreview(1, 2, false);
|
||||
|
||||
assert.equal(preview.ok, false);
|
||||
if (preview.ok) {
|
||||
throw new Error('expected preview to fail');
|
||||
}
|
||||
assert.equal(preview.error, 'Could not load selected notes');
|
||||
});
|
||||
Reference in New Issue
Block a user