mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
fix(stats): address Claude review follow-ups
This commit is contained in:
@@ -47,6 +47,19 @@ export function VocabularyTab({
|
||||
if (excluded.length > 0) result = result.filter((w) => !isExcluded(w));
|
||||
return result;
|
||||
}, [words, hideNames, excluded, isExcluded]);
|
||||
const summary = useMemo(
|
||||
() => buildVocabularySummary(filteredWords, kanji),
|
||||
[filteredWords, kanji],
|
||||
);
|
||||
const knownWordCount = useMemo(() => {
|
||||
if (knownWords.size === 0) return 0;
|
||||
|
||||
let count = 0;
|
||||
for (const w of filteredWords) {
|
||||
if (knownWords.has(w.headword)) count += 1;
|
||||
}
|
||||
return count;
|
||||
}, [filteredWords, knownWords]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -63,15 +76,6 @@ export function VocabularyTab({
|
||||
);
|
||||
}
|
||||
|
||||
const summary = buildVocabularySummary(filteredWords, kanji);
|
||||
|
||||
let knownWordCount = 0;
|
||||
if (knownWords.size > 0) {
|
||||
for (const w of filteredWords) {
|
||||
if (knownWords.has(w.headword)) knownWordCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectWord = (entry: VocabularyEntry): void => {
|
||||
onOpenWordDetail?.(entry.wordId);
|
||||
};
|
||||
|
||||
20
stats/src/hooks/useSessions.test.ts
Normal file
20
stats/src/hooks/useSessions.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { toErrorMessage } from './useSessions';
|
||||
|
||||
const USE_SESSIONS_PATH = fileURLToPath(new URL('./useSessions.ts', import.meta.url));
|
||||
|
||||
test('toErrorMessage normalizes Error and non-Error rejections', () => {
|
||||
assert.equal(toErrorMessage(new Error('network down')), 'network down');
|
||||
assert.equal(toErrorMessage('bad gateway'), 'bad gateway');
|
||||
assert.equal(toErrorMessage(503), '503');
|
||||
});
|
||||
|
||||
test('useSessions and useSessionDetail route catch handlers through toErrorMessage', () => {
|
||||
const source = fs.readFileSync(USE_SESSIONS_PATH, 'utf8');
|
||||
const matches = source.match(/setError\(toErrorMessage\(err\)\)/g);
|
||||
|
||||
assert.equal(matches?.length, 2);
|
||||
});
|
||||
@@ -3,6 +3,10 @@ import { getStatsClient } from './useStatsApi';
|
||||
import { SESSION_CHART_EVENT_TYPES } from '../lib/session-events';
|
||||
import type { SessionSummary, SessionTimelinePoint, SessionEvent } from '../types/stats';
|
||||
|
||||
export function toErrorMessage(err: unknown): string {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
export function useSessions(limit = 50) {
|
||||
const [sessions, setSessions] = useState<SessionSummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -21,7 +25,7 @@ export function useSessions(limit = 50) {
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
setError(err.message);
|
||||
setError(toErrorMessage(err));
|
||||
})
|
||||
.finally(() => {
|
||||
if (cancelled) return;
|
||||
@@ -77,7 +81,7 @@ export function useSessionDetail(sessionId: number | null) {
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
setError(err.message);
|
||||
setError(toErrorMessage(err));
|
||||
})
|
||||
.finally(() => {
|
||||
if (cancelled) return;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const VOCABULARY_TAB_PATH = path.resolve(
|
||||
import.meta.dir,
|
||||
'../components/vocabulary/VocabularyTab.tsx',
|
||||
const VOCABULARY_TAB_PATH = fileURLToPath(
|
||||
new URL('../components/vocabulary/VocabularyTab.tsx', import.meta.url),
|
||||
);
|
||||
|
||||
test('VocabularyTab declares all hooks before loading and error early returns', () => {
|
||||
@@ -20,3 +19,16 @@ test('VocabularyTab declares all hooks before loading and error early returns',
|
||||
|
||||
assert.deepEqual(hooksAfterLoadingGuard ?? [], []);
|
||||
});
|
||||
|
||||
test('VocabularyTab memoizes summary and known-word aggregate calculations', () => {
|
||||
const source = fs.readFileSync(VOCABULARY_TAB_PATH, 'utf8');
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/const summary = useMemo\([\s\S]*buildVocabularySummary\(filteredWords, kanji\)[\s\S]*\[filteredWords, kanji\][\s\S]*\);/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const knownWordCount = useMemo\(\(\) => \{[\s\S]*for \(const w of filteredWords\) \{[\s\S]*knownWords\.has\(w\.headword\)[\s\S]*\}\s*return count;\s*\}, \[filteredWords, knownWords\]\);/,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user