mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 12:55:20 -07:00
Persist stats exclusions in DB and fix word metrics filtering (#60)
This commit is contained in:
@@ -172,6 +172,50 @@ test('getSessionEvents can request only specific event types', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('getExcludedWords requests database-backed exclusions', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
seenUrl = String(input);
|
||||
return new Response(JSON.stringify([{ headword: '猫', word: '猫', reading: 'ねこ' }]), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const words = await apiClient.getExcludedWords();
|
||||
assert.equal(seenUrl, `${BASE_URL}/api/stats/excluded-words`);
|
||||
assert.deepEqual(words, [{ headword: '猫', word: '猫', reading: 'ねこ' }]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('setExcludedWords replaces database-backed exclusions', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
let seenMethod = '';
|
||||
let seenBody = '';
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
seenUrl = String(input);
|
||||
seenMethod = init?.method ?? 'GET';
|
||||
seenBody = String(init?.body ?? '');
|
||||
return new Response(null, { status: 200 });
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
await apiClient.setExcludedWords([{ headword: '猫', word: '猫', reading: 'ねこ' }]);
|
||||
assert.equal(seenUrl, `${BASE_URL}/api/stats/excluded-words`);
|
||||
assert.equal(seenMethod, 'PUT');
|
||||
assert.deepEqual(JSON.parse(seenBody), {
|
||||
words: [{ headword: '猫', word: '猫', reading: 'ねこ' }],
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('getSessionTimeline requests full session history when limit is omitted', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let seenUrl = '';
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
KanjiDetailData,
|
||||
EpisodeDetailData,
|
||||
StatsAnkiNoteInfo,
|
||||
StatsExcludedWord,
|
||||
} from '../types/stats';
|
||||
|
||||
type StatsLocationLike = Pick<Location, 'protocol' | 'origin' | 'search'>;
|
||||
@@ -85,11 +86,19 @@ export const apiClient = {
|
||||
return fetchJson<SessionEvent[]>(`/api/stats/sessions/${id}/events?${params.toString()}`);
|
||||
},
|
||||
getSessionKnownWordsTimeline: (id: number) =>
|
||||
fetchJson<Array<{ linesSeen: number; knownWordsSeen: number }>>(
|
||||
fetchJson<Array<{ linesSeen: number; knownWordsSeen: number; totalWordsSeen: number }>>(
|
||||
`/api/stats/sessions/${id}/known-words-timeline`,
|
||||
),
|
||||
getVocabulary: (limit = 100) =>
|
||||
fetchJson<VocabularyEntry[]>(`/api/stats/vocabulary?limit=${limit}`),
|
||||
getExcludedWords: () => fetchJson<StatsExcludedWord[]>('/api/stats/excluded-words'),
|
||||
setExcludedWords: async (words: StatsExcludedWord[]): Promise<void> => {
|
||||
await fetchResponse('/api/stats/excluded-words', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ words }),
|
||||
});
|
||||
},
|
||||
getWordOccurrences: (headword: string, word: string, reading: string, limit = 50, offset = 0) =>
|
||||
fetchJson<VocabularyOccurrenceEntry[]>(
|
||||
`/api/stats/vocabulary/occurrences?headword=${encodeURIComponent(headword)}&word=${encodeURIComponent(word)}&reading=${encodeURIComponent(reading)}&limit=${limit}&offset=${offset}`,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { SessionDetail, getKnownPctAxisMax } from '../components/sessions/SessionDetail';
|
||||
import {
|
||||
SessionDetail,
|
||||
buildKnownWordsRatioChartData,
|
||||
getKnownPctAxisMax,
|
||||
} from '../components/sessions/SessionDetail';
|
||||
import { buildSessionChartEvents } from './session-events';
|
||||
import { EventType } from '../types/stats';
|
||||
|
||||
@@ -69,3 +73,21 @@ test('getKnownPctAxisMax adds headroom above the highest known percentage', () =
|
||||
test('getKnownPctAxisMax caps the chart top at 100%', () => {
|
||||
assert.equal(getKnownPctAxisMax([97.1, 98.6]), 100);
|
||||
});
|
||||
|
||||
test('buildKnownWordsRatioChartData uses filtered known-word timeline totals', () => {
|
||||
const chartData = buildKnownWordsRatioChartData(
|
||||
[
|
||||
{ sampleMs: 1_000, linesSeen: 1, tokensSeen: 10 },
|
||||
{ sampleMs: 2_000, linesSeen: 2, tokensSeen: 20 },
|
||||
],
|
||||
new Map([
|
||||
[1, { knownWordsSeen: 2, totalWordsSeen: 3 }],
|
||||
[2, { knownWordsSeen: 3, totalWordsSeen: 4 }],
|
||||
]),
|
||||
);
|
||||
|
||||
assert.deepEqual(chartData, [
|
||||
{ tsMs: 1_000, knownWords: 2, unknownWords: 1, totalWords: 3 },
|
||||
{ tsMs: 2_000, knownWords: 3, unknownWords: 1, totalWords: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user