mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-28 04:49:49 -07:00
977 lines
24 KiB
TypeScript
977 lines
24 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import type { AnkiConnectConfig } from '../types/anki';
|
|
import { KnownWordCacheManager } from './known-word-cache';
|
|
|
|
async function waitForCondition(
|
|
condition: () => boolean,
|
|
timeoutMs = 500,
|
|
intervalMs = 10,
|
|
): Promise<void> {
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
if (condition()) {
|
|
return;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
}
|
|
throw new Error('Timed out waiting for condition');
|
|
}
|
|
|
|
function createKnownWordCacheHarness(config: AnkiConnectConfig): {
|
|
manager: KnownWordCacheManager;
|
|
calls: {
|
|
findNotes: number;
|
|
notesInfo: number;
|
|
};
|
|
statePath: string;
|
|
clientState: {
|
|
findNotesResult: number[];
|
|
notesInfoResult: Array<{ noteId: number; fields: Record<string, { value: string }> }>;
|
|
findNotesByQuery: Map<string, number[]>;
|
|
};
|
|
cleanup: () => void;
|
|
} {
|
|
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-known-word-cache-'));
|
|
const statePath = path.join(stateDir, 'known-words-cache.json');
|
|
const calls = {
|
|
findNotes: 0,
|
|
notesInfo: 0,
|
|
};
|
|
const clientState = {
|
|
findNotesResult: [] as number[],
|
|
notesInfoResult: [] as Array<{ noteId: number; fields: Record<string, { value: string }> }>,
|
|
findNotesByQuery: new Map<string, number[]>(),
|
|
};
|
|
const manager = new KnownWordCacheManager({
|
|
client: {
|
|
findNotes: async (query) => {
|
|
calls.findNotes += 1;
|
|
if (clientState.findNotesByQuery.has(query)) {
|
|
return clientState.findNotesByQuery.get(query) ?? [];
|
|
}
|
|
return clientState.findNotesResult;
|
|
},
|
|
notesInfo: async (noteIds) => {
|
|
calls.notesInfo += 1;
|
|
return clientState.notesInfoResult.filter((note) => noteIds.includes(note.noteId));
|
|
},
|
|
},
|
|
getConfig: () => config,
|
|
knownWordCacheStatePath: statePath,
|
|
showStatusNotification: () => undefined,
|
|
});
|
|
|
|
return {
|
|
manager,
|
|
calls,
|
|
statePath,
|
|
clientState,
|
|
cleanup: () => {
|
|
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
},
|
|
};
|
|
}
|
|
|
|
test('KnownWordCacheManager startLifecycle keeps fresh persisted cache without immediate refresh', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
refreshMinutes: 60,
|
|
},
|
|
};
|
|
const { manager, calls, statePath, cleanup } = createKnownWordCacheHarness(config);
|
|
const originalDateNow = Date.now;
|
|
|
|
try {
|
|
Date.now = () => 120_000;
|
|
fs.writeFileSync(
|
|
statePath,
|
|
JSON.stringify({
|
|
version: 2,
|
|
refreshedAtMs: 120_000,
|
|
scope: '{"refreshMinutes":60,"scope":"all","fieldsWord":""}',
|
|
words: ['猫'],
|
|
notes: {
|
|
'1': ['猫'],
|
|
},
|
|
}),
|
|
'utf-8',
|
|
);
|
|
|
|
manager.startLifecycle();
|
|
|
|
assert.equal(manager.isKnownWord('猫'), true);
|
|
assert.equal(calls.findNotes, 0);
|
|
assert.equal(calls.notesInfo, 0);
|
|
// v2 states carry no readings, so they load usable but stale to trigger a
|
|
// prompt upgrade refresh.
|
|
assert.equal(
|
|
(
|
|
manager as unknown as {
|
|
getMsUntilNextRefresh: () => number;
|
|
}
|
|
).getMsUntilNextRefresh(),
|
|
0,
|
|
);
|
|
} finally {
|
|
Date.now = originalDateNow;
|
|
manager.stopLifecycle();
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager startLifecycle keeps fresh v3 persisted cache without immediate refresh', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
refreshMinutes: 60,
|
|
},
|
|
};
|
|
const { manager, calls, statePath, cleanup } = createKnownWordCacheHarness(config);
|
|
const originalDateNow = Date.now;
|
|
|
|
try {
|
|
Date.now = () => 120_000;
|
|
fs.writeFileSync(
|
|
statePath,
|
|
JSON.stringify({
|
|
version: 3,
|
|
refreshedAtMs: 120_000,
|
|
scope: '{"refreshMinutes":60,"scope":"all","fieldsWord":""}',
|
|
notes: {
|
|
'1': [{ word: '猫', reading: 'ねこ' }],
|
|
},
|
|
}),
|
|
'utf-8',
|
|
);
|
|
|
|
manager.startLifecycle();
|
|
|
|
assert.equal(manager.isKnownWord('猫'), true);
|
|
assert.equal(manager.isKnownWord('猫', 'ねこ'), true);
|
|
assert.equal(manager.isKnownWord('猫', 'びょう'), false);
|
|
assert.equal(calls.findNotes, 0);
|
|
assert.equal(calls.notesInfo, 0);
|
|
assert.equal(
|
|
(
|
|
manager as unknown as {
|
|
getMsUntilNextRefresh: () => number;
|
|
}
|
|
).getMsUntilNextRefresh() > 0,
|
|
true,
|
|
);
|
|
} finally {
|
|
Date.now = originalDateNow;
|
|
manager.stopLifecycle();
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager startLifecycle immediately refreshes stale persisted cache', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
refreshMinutes: 1,
|
|
},
|
|
};
|
|
const { manager, calls, statePath, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
const originalDateNow = Date.now;
|
|
|
|
try {
|
|
Date.now = () => 120_000;
|
|
fs.writeFileSync(
|
|
statePath,
|
|
JSON.stringify({
|
|
version: 2,
|
|
refreshedAtMs: 59_000,
|
|
scope: '{"refreshMinutes":1,"scope":"all","fieldsWord":"Word"}',
|
|
words: ['猫'],
|
|
notes: {
|
|
'1': ['猫'],
|
|
},
|
|
}),
|
|
'utf-8',
|
|
);
|
|
|
|
clientState.findNotesResult = [1];
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '犬' },
|
|
},
|
|
},
|
|
];
|
|
|
|
manager.startLifecycle();
|
|
await waitForCondition(() => calls.findNotes === 1 && calls.notesInfo === 1);
|
|
|
|
assert.equal(manager.isKnownWord('猫'), false);
|
|
assert.equal(manager.isKnownWord('犬'), true);
|
|
} finally {
|
|
Date.now = originalDateNow;
|
|
manager.stopLifecycle();
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager invalidates persisted cache when fields.word changes', () => {
|
|
const config: AnkiConnectConfig = {
|
|
deck: 'Mining',
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
manager.appendFromNoteInfo({
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '猫' },
|
|
},
|
|
});
|
|
assert.equal(manager.isKnownWord('猫'), true);
|
|
|
|
config.fields = {
|
|
...config.fields,
|
|
word: 'Expression',
|
|
};
|
|
|
|
(
|
|
manager as unknown as {
|
|
loadKnownWordCacheState: () => void;
|
|
}
|
|
).loadKnownWordCacheState();
|
|
|
|
assert.equal(manager.isKnownWord('猫'), false);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager refresh incrementally reconciles deleted and edited note words', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, statePath, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
fs.writeFileSync(
|
|
statePath,
|
|
JSON.stringify({
|
|
version: 2,
|
|
refreshedAtMs: 1,
|
|
scope: '{"refreshMinutes":1440,"scope":"all","fieldsWord":"Word"}',
|
|
words: ['猫', '犬'],
|
|
notes: {
|
|
'1': ['猫'],
|
|
'2': ['犬'],
|
|
},
|
|
}),
|
|
'utf-8',
|
|
);
|
|
|
|
(
|
|
manager as unknown as {
|
|
loadKnownWordCacheState: () => void;
|
|
}
|
|
).loadKnownWordCacheState();
|
|
|
|
clientState.findNotesResult = [1];
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '鳥' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('猫'), false);
|
|
assert.equal(manager.isKnownWord('犬'), false);
|
|
assert.equal(manager.isKnownWord('鳥'), true);
|
|
|
|
const persisted = JSON.parse(fs.readFileSync(statePath, 'utf-8')) as {
|
|
version: number;
|
|
notes?: Record<string, Array<{ word: string; reading: string | null }>>;
|
|
};
|
|
assert.equal(persisted.version, 3);
|
|
assert.deepEqual(persisted.notes, {
|
|
'1': [{ word: '鳥', reading: null }],
|
|
});
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager uses empty query when no known-word deck is configured', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesByQuery.set('', [1]);
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '猫' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('猫'), true);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager skips malformed note info without fields', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesResult = [1, 2];
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: undefined as unknown as Record<string, { value: string }>,
|
|
},
|
|
{
|
|
noteId: 2,
|
|
fields: {
|
|
Word: { value: '猫' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('猫'), true);
|
|
assert.equal(manager.isKnownWord('犬'), false);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager preserves cache state key captured before refresh work', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
refreshMinutes: 1,
|
|
},
|
|
};
|
|
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-known-word-cache-key-'));
|
|
const statePath = path.join(stateDir, 'known-words-cache.json');
|
|
let notesInfoStarted = false;
|
|
let releaseNotesInfo!: () => void;
|
|
const notesInfoGate = new Promise<void>((resolve) => {
|
|
releaseNotesInfo = resolve;
|
|
});
|
|
const manager = new KnownWordCacheManager({
|
|
client: {
|
|
findNotes: async () => [1],
|
|
notesInfo: async () => {
|
|
notesInfoStarted = true;
|
|
await notesInfoGate;
|
|
return [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '猫' },
|
|
},
|
|
},
|
|
];
|
|
},
|
|
},
|
|
getConfig: () => config,
|
|
knownWordCacheStatePath: statePath,
|
|
showStatusNotification: () => undefined,
|
|
});
|
|
|
|
try {
|
|
const refreshPromise = manager.refresh(true);
|
|
await waitForCondition(() => notesInfoStarted);
|
|
|
|
config.fields = {
|
|
...config.fields,
|
|
word: 'Expression',
|
|
};
|
|
releaseNotesInfo();
|
|
await refreshPromise;
|
|
|
|
const persisted = JSON.parse(fs.readFileSync(statePath, 'utf-8')) as {
|
|
scope: string;
|
|
notes: Record<string, Array<{ word: string; reading: string | null }>>;
|
|
};
|
|
assert.equal(persisted.scope, '{"refreshMinutes":1,"scope":"all","fieldsWord":"Word"}');
|
|
assert.deepEqual(persisted.notes, { '1': [{ word: '猫', reading: null }] });
|
|
} finally {
|
|
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager does not borrow fields from other decks during refresh', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
decks: {
|
|
Mining: [],
|
|
Reading: ['AltWord'],
|
|
},
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesByQuery.set('deck:"Mining"', [1]);
|
|
clientState.findNotesByQuery.set('deck:"Reading"', []);
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
AltWord: { value: '猫' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('猫'), false);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager invalidates persisted cache when per-deck fields change', () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
decks: {
|
|
Mining: ['Word'],
|
|
},
|
|
},
|
|
};
|
|
const { manager, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
manager.appendFromNoteInfo({
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '猫' },
|
|
},
|
|
});
|
|
assert.equal(manager.isKnownWord('猫'), true);
|
|
|
|
config.knownWords = {
|
|
...config.knownWords,
|
|
decks: {
|
|
Mining: ['Expression'],
|
|
},
|
|
};
|
|
|
|
(
|
|
manager as unknown as {
|
|
loadKnownWordCacheState: () => void;
|
|
}
|
|
).loadKnownWordCacheState();
|
|
|
|
assert.equal(manager.isKnownWord('猫'), false);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager preserves deck-specific field mappings during refresh', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
decks: {
|
|
Mining: ['Expression'],
|
|
Reading: ['Word'],
|
|
},
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesByQuery.set('deck:"Mining"', [1]);
|
|
clientState.findNotesByQuery.set('deck:"Reading"', [2]);
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Expression: { value: '猫' },
|
|
Word: { value: 'should-not-count' },
|
|
},
|
|
},
|
|
{
|
|
noteId: 2,
|
|
fields: {
|
|
Word: { value: '犬' },
|
|
Expression: { value: 'also-ignored' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('猫'), true);
|
|
assert.equal(manager.isKnownWord('犬'), true);
|
|
assert.equal(manager.isKnownWord('should-not-count'), false);
|
|
assert.equal(manager.isKnownWord('also-ignored'), false);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager uses the current deck fields for immediate append', () => {
|
|
const config: AnkiConnectConfig = {
|
|
deck: 'Mining',
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
decks: {
|
|
Mining: ['Expression'],
|
|
Reading: ['Word'],
|
|
},
|
|
},
|
|
};
|
|
const { manager, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
manager.appendFromNoteInfo({
|
|
noteId: 1,
|
|
fields: {
|
|
Expression: { value: '猫' },
|
|
Word: { value: 'should-not-count' },
|
|
},
|
|
});
|
|
|
|
assert.equal(manager.isKnownWord('猫'), true);
|
|
assert.equal(manager.isKnownWord('should-not-count'), false);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager uses all configured deck fields for immediate append without a current deck', () => {
|
|
const config: AnkiConnectConfig = {
|
|
deck: '',
|
|
fields: {
|
|
word: 'Expression',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
decks: {
|
|
'Kaishi 1.5k': ['Word'],
|
|
Minecraft: ['Expression', 'Word'],
|
|
},
|
|
},
|
|
};
|
|
const { manager, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
manager.appendFromNoteInfo({
|
|
noteId: 1,
|
|
fields: {
|
|
Expression: { value: '別人' },
|
|
},
|
|
});
|
|
|
|
assert.equal(manager.isKnownWord('別人'), true);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager reports immediate append cache clears as mutations', () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Expression',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
refreshMinutes: 60,
|
|
},
|
|
};
|
|
const { manager, statePath, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
fs.writeFileSync(
|
|
statePath,
|
|
JSON.stringify({
|
|
version: 2,
|
|
refreshedAtMs: Date.now(),
|
|
scope: '{"refreshMinutes":60,"scope":"all","fieldsWord":"Expression"}',
|
|
words: ['猫'],
|
|
notes: {
|
|
'1': ['猫'],
|
|
},
|
|
}),
|
|
'utf-8',
|
|
);
|
|
manager.startLifecycle();
|
|
assert.equal(manager.isKnownWord('猫'), true);
|
|
|
|
config.fields = { word: 'Word' };
|
|
const changed = manager.appendFromNoteInfo({
|
|
noteId: 2,
|
|
fields: {
|
|
Word: { value: '' },
|
|
},
|
|
});
|
|
|
|
assert.equal(changed, true);
|
|
assert.equal(manager.isKnownWord('猫'), false);
|
|
} finally {
|
|
manager.stopLifecycle();
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager skips immediate append when addMinedWordsImmediately is disabled', () => {
|
|
const config: AnkiConnectConfig = {
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
addMinedWordsImmediately: false,
|
|
},
|
|
};
|
|
const { manager, statePath, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
manager.appendFromNoteInfo({
|
|
noteId: 1,
|
|
fields: {
|
|
Expression: { value: '猫' },
|
|
},
|
|
});
|
|
|
|
assert.equal(manager.isKnownWord('猫'), false);
|
|
assert.equal(fs.existsSync(statePath), false);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager disambiguates known words by note reading', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesResult = [1];
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '床' },
|
|
'Word Reading': { value: 'ゆか' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('床'), true);
|
|
assert.equal(manager.isKnownWord('床', 'ゆか'), true);
|
|
assert.equal(manager.isKnownWord('床', 'ユカ'), true);
|
|
// Same spelling, different word (床/とこ "bed") must not match.
|
|
assert.equal(manager.isKnownWord('床', 'とこ'), false);
|
|
// Note readings stay matchable as kana words.
|
|
assert.equal(manager.isKnownWord('ゆか'), true);
|
|
assert.equal(manager.isKnownWord('とこ'), false);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager suppresses reading-only matches when disallowed', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesResult = [1];
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '警告' },
|
|
'Word Reading': { value: 'けいこく' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
// Reading-only match stays available for kana subtitle text…
|
|
assert.equal(manager.isKnownWord('けいこく'), true);
|
|
// …but a kanji token's reading (渓谷/けいこく) must not borrow 警告's.
|
|
assert.equal(
|
|
manager.isKnownWord('けいこく', undefined, { allowReadingOnlyMatch: false }),
|
|
false,
|
|
);
|
|
// Mined word texts still match regardless of the flag.
|
|
assert.equal(manager.isKnownWord('警告', undefined, { allowReadingOnlyMatch: false }), true);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager does not match single-kana text by reading alone', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesResult = [1, 2];
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '夜' },
|
|
'Word Reading': { value: 'よ' },
|
|
},
|
|
},
|
|
{
|
|
noteId: 2,
|
|
fields: {
|
|
Word: { value: 'え' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
// よ must not count as known just because 夜 is read よ.
|
|
assert.equal(manager.isKnownWord('よ'), false);
|
|
assert.equal(manager.isKnownWord('ヨ'), false);
|
|
assert.equal(manager.isKnownWord('夜'), true);
|
|
assert.equal(manager.isKnownWord('夜', 'よ'), true);
|
|
// A literal single-kana word entry still matches via the word map.
|
|
assert.equal(manager.isKnownWord('え'), true);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager probes reading fields even with per-deck word fields configured', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Expression',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
decks: {
|
|
'Kaishi 1.5k': ['Word'],
|
|
},
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesByQuery.set('deck:"Kaishi 1.5k"', [1]);
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '床' },
|
|
'Word Reading': { value: 'ゆか' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('床', 'ゆか'), true);
|
|
assert.equal(manager.isKnownWord('床', 'とこ'), false);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager matches words without readings in any reading', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesResult = [1];
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: '床' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('床'), true);
|
|
assert.equal(manager.isKnownWord('床', 'とこ'), true);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager extracts word and reading from furigana word fields', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesResult = [1];
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Word: { value: 'お 決[き]まり' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('お決まり'), true);
|
|
assert.equal(manager.isKnownWord('お決まり', 'おきまり'), true);
|
|
assert.equal(manager.isKnownWord('お決まり', 'おさだまり'), false);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager treats non-kana reading fields as words', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesResult = [1];
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Reading: { value: '漢字' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('漢字'), true);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
|
|
test('KnownWordCacheManager keeps kana-only reading notes matchable', async () => {
|
|
const config: AnkiConnectConfig = {
|
|
fields: {
|
|
word: 'Word',
|
|
},
|
|
knownWords: {
|
|
highlightEnabled: true,
|
|
},
|
|
};
|
|
const { manager, clientState, cleanup } = createKnownWordCacheHarness(config);
|
|
|
|
try {
|
|
clientState.findNotesResult = [1];
|
|
clientState.notesInfoResult = [
|
|
{
|
|
noteId: 1,
|
|
fields: {
|
|
Reading: { value: 'たべる' },
|
|
},
|
|
},
|
|
];
|
|
|
|
await manager.refresh(true);
|
|
|
|
assert.equal(manager.isKnownWord('たべる'), true);
|
|
assert.equal(manager.isKnownWord('タベル'), true);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|