test: cut low-value tests, rewrite the rest around behavior, parallelize lanes (#285)

This commit is contained in:
sudacode authored and GitHub committed 2026-10-05 00:07:44 -07:00
1 parent 4b6842d034
commit b48083ddd8
368 files changed
+18045 -60331

No files matched your search

@@ -1,52 +1,10 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { Window } from 'happy-dom';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { apiClient } from '../../lib/api-client';
import { AnilistSelector } from './AnilistSelector';
interface TestWindow extends Window {
IS_REACT_ACT_ENVIRONMENT?: boolean;
}
function installDom(): () => void {
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
const previousHTMLElement = globalThis.HTMLElement;
const previousISReactActEnvironment = (
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT;
const window = new Window() as TestWindow;
Object.defineProperty(globalThis, 'window', { value: window, configurable: true });
Object.defineProperty(globalThis, 'document', { value: window.document, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: window.HTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
return () => {
Object.defineProperty(globalThis, 'window', {
value: previousWindow,
configurable: true,
});
Object.defineProperty(globalThis, 'document', {
value: previousDocument,
configurable: true,
});
Object.defineProperty(globalThis, 'HTMLElement', {
value: previousHTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = previousISReactActEnvironment;
};
}
import { installDom } from '../../test-utils/dom';
function renderSelector(root: Root, props: { animeId: number; initialQuery: string }): void {
root.render(
@@ -1,29 +0,0 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { AnimeCard } from './AnimeCard';
test('AnimeCard includes linked AniList id in cover URLs to avoid stale library covers', () => {
const markup = renderToStaticMarkup(
<AnimeCard
anime={{
mediaKind: 'anime',
animeId: 42,
canonicalTitle: 'Test Anime',
anilistId: 21699,
tmdbId: null,
tmdbType: null,
totalSessions: 1,
totalActiveMs: 600_000,
totalCards: 0,
totalTokensSeen: 100,
episodeCount: 1,
episodesTotal: 10,
lastWatchedMs: 1_000,
}}
onClick={() => {}}
/>,
);
assert.match(markup, /\/api\/stats\/anime\/42\/cover\?coverRetry=21699/);
});
@@ -1,17 +1,8 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { AnimeCoverImage } from './AnimeCoverImage';
import { AnimeHeader } from './AnimeHeader';
test('AnimeCoverImage includes manual relink cover retry tokens', () => {
const markup = renderToStaticMarkup(
<AnimeCoverImage animeId={42} title="Test Anime" coverRetryToken={7} />,
);
assert.match(markup, /\/api\/stats\/anime\/42\/cover\?coverRetry=7/);
});
test('AnimeHeader uses the linked AniList id to avoid stale cached cover art', () => {
const markup = renderToStaticMarkup(
<AnimeHeader
@@ -1,48 +1,12 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { Window } from 'happy-dom';
import { act, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { apiClient } from '../../lib/api-client';
import type { AnimeLibraryItem } from '../../types/stats';
import { AnimeMergeDialog } from './AnimeMergeDialog';
import { LibraryEntryPicker } from './LibraryEntryPicker';
interface TestWindow extends Window {
IS_REACT_ACT_ENVIRONMENT?: boolean;
}
function installDom(): () => void {
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
const previousHTMLElement = globalThis.HTMLElement;
const previousIsReactActEnvironment = (
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT;
const window = new Window() as TestWindow;
Object.defineProperty(globalThis, 'window', { value: window, configurable: true });
Object.defineProperty(globalThis, 'document', { value: window.document, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: window.HTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
return () => {
Object.defineProperty(globalThis, 'window', { value: previousWindow, configurable: true });
Object.defineProperty(globalThis, 'document', { value: previousDocument, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: previousHTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = previousIsReactActEnvironment;
};
}
import { installDom } from '../../test-utils/dom';
function libraryItem(animeId: number, title: string): AnimeLibraryItem {
return {
@@ -1,48 +1,12 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { Window } from 'happy-dom';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { apiClient } from '../../lib/api-client';
import type { AnimeLibraryItem, StatsMergeAnimeResponse } from '../../types/stats';
import { AnimeMergeDialog } from './AnimeMergeDialog';
import { AnimeTab } from './AnimeTab';
interface TestWindow extends Window {
IS_REACT_ACT_ENVIRONMENT?: boolean;
}
function installDom(): () => void {
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
const previousHTMLElement = globalThis.HTMLElement;
const previousIsReactActEnvironment = (
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT;
const window = new Window() as TestWindow;
Object.defineProperty(globalThis, 'window', { value: window, configurable: true });
Object.defineProperty(globalThis, 'document', { value: window.document, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: window.HTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
return () => {
Object.defineProperty(globalThis, 'window', { value: previousWindow, configurable: true });
Object.defineProperty(globalThis, 'document', { value: previousDocument, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: previousHTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = previousIsReactActEnvironment;
};
}
import { installDom } from '../../test-utils/dom';
function libraryItem(animeId: number, title: string, episodeCount: number): AnimeLibraryItem {
return {
+1 -37
View File
@@ -1,47 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { Window } from 'happy-dom';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { apiClient } from '../../lib/api-client';
import type { AnimeDetailData, AnimeLibraryItem } from '../../types/stats';
import { AnimeTab } from './AnimeTab';
interface TestWindow extends Window {
IS_REACT_ACT_ENVIRONMENT?: boolean;
}
function installDom(): () => void {
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
const previousHTMLElement = globalThis.HTMLElement;
const previousIsReactActEnvironment = (
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT;
const window = new Window() as TestWindow;
Object.defineProperty(globalThis, 'window', { value: window, configurable: true });
Object.defineProperty(globalThis, 'document', { value: window.document, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: window.HTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
return () => {
Object.defineProperty(globalThis, 'window', { value: previousWindow, configurable: true });
Object.defineProperty(globalThis, 'document', { value: previousDocument, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: previousHTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = previousIsReactActEnvironment;
};
}
import { installDom } from '../../test-utils/dom';
function libraryItem(anilistId: number | null): AnimeLibraryItem {
return {
@@ -1,47 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { Window } from 'happy-dom';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { apiClient } from '../../lib/api-client';
import type { AnimeEpisode, AnimeLibraryItem, StatsMoveVideoResponse } from '../../types/stats';
import { EpisodeList } from './EpisodeList';
interface TestWindow extends Window {
IS_REACT_ACT_ENVIRONMENT?: boolean;
}
function installDom(): () => void {
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
const previousHTMLElement = globalThis.HTMLElement;
const previousIsReactActEnvironment = (
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT;
const window = new Window() as TestWindow;
Object.defineProperty(globalThis, 'window', { value: window, configurable: true });
Object.defineProperty(globalThis, 'document', { value: window.document, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: window.HTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
return () => {
Object.defineProperty(globalThis, 'window', { value: previousWindow, configurable: true });
Object.defineProperty(globalThis, 'document', { value: previousDocument, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: previousHTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = previousIsReactActEnvironment;
};
}
import { installDom } from '../../test-utils/dom';
function episode(videoId: number, title: string): AnimeEpisode {
return {
@@ -1,50 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { Window } from 'happy-dom';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { apiClient } from '../../lib/api-client';
import type { StatsTmdbSearchResult } from '../../types/stats';
import { TmdbSelector } from './TmdbSelector';
interface TestWindow extends Window {
IS_REACT_ACT_ENVIRONMENT?: boolean;
}
function installDom(): () => void {
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
const previousHTMLElement = globalThis.HTMLElement;
const previousISReactActEnvironment = (
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT;
const window = new Window() as TestWindow;
Object.defineProperty(globalThis, 'window', { value: window, configurable: true });
Object.defineProperty(globalThis, 'document', { value: window.document, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: window.HTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
return () => {
Object.defineProperty(globalThis, 'window', { value: previousWindow, configurable: true });
Object.defineProperty(globalThis, 'document', {
value: previousDocument,
configurable: true,
});
Object.defineProperty(globalThis, 'HTMLElement', {
value: previousHTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = previousISReactActEnvironment;
};
}
import { installDom } from '../../test-utils/dom';
const HANZAWA: StatsTmdbSearchResult = {
tmdbId: 61222,
@@ -1,22 +0,0 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
test('delete confirmation dialog swallows Escape before closing', () => {
const source = fs.readFileSync(
path.join(process.cwd(), 'stats/src/components/layout/DeleteConfirmDialog.tsx'),
'utf8',
);
const handlerBlock = source.match(
/const onKeyDown = \(event: KeyboardEvent\) => \{(?<body>[\s\S]*?)\n \};/,
)?.groups?.body;
assert.ok(handlerBlock);
assert.match(handlerBlock, /event\.preventDefault\(\);/);
assert.match(handlerBlock, /event\.stopPropagation\(\);/);
assert.match(handlerBlock, /event\.stopImmediatePropagation\(\);/);
assert.ok(
handlerBlock.indexOf('event.stopPropagation();') < handlerBlock.indexOf('finish(false);'),
);
});
@@ -1,6 +1,5 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { Window } from 'happy-dom';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { apiClient } from '../../lib/api-client';
@@ -8,30 +7,7 @@ import { setDeleteConfirmPresenter } from '../../lib/delete-confirm';
import { localDayFromMs } from '../../lib/formatters';
import type { OverviewData, SessionSummary } from '../../types/stats';
import { OverviewTab } from './OverviewTab';
function installDom() {
const window = new Window();
const properties = {
window,
document: window.document,
HTMLElement: window.HTMLElement,
ResizeObserver: window.ResizeObserver,
IS_REACT_ACT_ENVIRONMENT: true,
};
const previous = Object.keys(properties).map(
(key) => [key, Object.getOwnPropertyDescriptor(globalThis, key)] as const,
);
for (const [key, value] of Object.entries(properties)) {
Object.defineProperty(globalThis, key, { value, configurable: true, writable: true });
}
return () => {
for (const [key, descriptor] of previous) {
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
else Reflect.deleteProperty(globalThis, key);
}
window.happyDOM.abort();
};
}
import { installDom } from '../../test-utils/dom';
function session(sessionId: number): SessionSummary {
return {
@@ -1,47 +0,0 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { TrackingSnapshot } from './TrackingSnapshot';
import type { OverviewSummary } from '../../lib/dashboard-data';
const summary: OverviewSummary = {
todayActiveMs: 0,
todayCards: 0,
streakDays: 0,
allTimeMinutes: 120,
totalTrackedCards: 9,
episodesToday: 0,
activeAnimeCount: 0,
totalEpisodesWatched: 5,
totalAnimeCompleted: 1,
averageSessionMinutes: 40,
activeDays: 12,
totalSessions: 15,
lookupRate: {
shortValue: '2.3 / 100 words',
longValue: '2.3 lookups per 100 words',
},
todayTokens: 0,
newWordsToday: 0,
newWordsThisWeek: 0,
recentWatchTime: [],
};
test('TrackingSnapshot renders Yomitan lookup rate copy on the homepage card', () => {
const markup = renderToStaticMarkup(
<TrackingSnapshot summary={summary} knownWordsSummary={null} />,
);
assert.match(markup, /Lookup Rate/);
assert.match(markup, /2\.3 \/ 100 words/);
assert.match(markup, /Lifetime Yomitan lookups normalized by total words seen/);
});
test('TrackingSnapshot labels new words as unique headwords', () => {
const markup = renderToStaticMarkup(
<TrackingSnapshot summary={summary} knownWordsSummary={null} />,
);
assert.match(markup, /Unique headwords seen for the first time today/);
assert.match(markup, /Unique headwords seen for the first time this week/);
});
@@ -1,32 +0,0 @@
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';
import { formatSentenceSearchMatchCountLabel } from './SearchTab';
const SEARCH_TAB_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'SearchTab.tsx');
test('formatSentenceSearchMatchCountLabel uses singular label for one result', () => {
assert.equal(formatSentenceSearchMatchCountLabel(1), 'Match');
assert.equal(formatSentenceSearchMatchCountLabel(0), 'Matches');
assert.equal(formatSentenceSearchMatchCountLabel(2), 'Matches');
});
test('SearchTab forwards stored secondary subtitle text when mining from search results', () => {
const source = fs.readFileSync(SEARCH_TAB_PATH, 'utf8');
assert.match(source, /buildStatsMineCardParams\(result,\s*searchedWord,\s*mode\)/);
});
test('SearchTab enables headword sentence search by default and forwards the toggle', () => {
const source = fs.readFileSync(SEARCH_TAB_PATH, 'utf8');
assert.match(source, /const \[searchByHeadword,\s*setSearchByHeadword\] = useState\(true\);/);
assert.match(
source,
/apiClient\s*\.\s*searchSentences\(trimmed,\s*SEARCH_LIMIT,\s*searchByHeadword\)/,
);
assert.match(source, /checked=\{searchByHeadword\}/);
assert.match(source, /setSearchByHeadword\(event\.target\.checked\)/);
});
@@ -93,29 +93,3 @@ test('SessionEventPopover hides preview-unavailable fallback while note info is
assert.match(markup, /Loading Anki note info/);
assert.doesNotMatch(markup, /Preview unavailable/);
});
test('SessionEventPopover keeps the loading state clean until note preview data arrives', () => {
const marker: SessionChartMarker = {
key: 'card-9001',
kind: 'card',
anchorTsMs: 9_001,
eventTsMs: 9_001,
noteIds: [1773808840964],
cardsDelta: 1,
};
const markup = renderToStaticMarkup(
<SessionEventPopover
marker={marker}
noteInfos={new Map()}
loading={true}
pinned={true}
onTogglePinned={() => {}}
onClose={() => {}}
onOpenNote={() => {}}
/>,
);
assert.match(markup, /Loading Anki note info/);
assert.doesNotMatch(markup, /Preview unavailable/);
});
@@ -132,29 +132,9 @@ test('deleting session counts keep rows disabled during overlapping delete flows
assert.equal(deleting.size, 0);
});
test('buildBucketDeleteHandler does not call onStart when confirm returns false', async () => {
let startCalled = false;
const bucket = makeBucket([makeSession({ sessionId: 1 }), makeSession({ sessionId: 2 })]);
const handler = buildBucketDeleteHandler({
bucket,
apiClient: { deleteSessions: async () => {} },
confirm: () => false,
onStart: () => {
startCalled = true;
},
onSuccess: () => {},
onError: () => {},
});
await handler();
assert.equal(startCalled, false);
});
test('buildBucketDeleteHandler is a no-op when confirm returns false', async () => {
let deleteCalled = false;
let startCalled = false;
let successCalled = false;
const bucket = makeBucket([makeSession({ sessionId: 1 }), makeSession({ sessionId: 2 })]);
@@ -167,6 +147,9 @@ test('buildBucketDeleteHandler is a no-op when confirm returns false', async ()
},
},
confirm: () => false,
onStart: () => {
startCalled = true;
},
onSuccess: () => {
successCalled = true;
},
@@ -176,6 +159,7 @@ test('buildBucketDeleteHandler is a no-op when confirm returns false', async ()
await handler();
assert.equal(deleteCalled, false);
assert.equal(startCalled, false);
assert.equal(successCalled, false);
});
@@ -1,28 +1,8 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { AnimeVisibilityFilter } from './TrendsTab';
test('AnimeVisibilityFilter uses title visibility wording', () => {
const markup = renderToStaticMarkup(
<AnimeVisibilityFilter
animeTitles={['KonoSuba']}
hiddenAnime={new Set()}
maxTitles={null}
maxTitlesMode="total"
onShowAll={() => {}}
onHideAll={() => {}}
onToggleAnime={() => {}}
onMaxTitlesChange={() => {}}
onMaxTitlesModeChange={() => {}}
/>,
);
assert.match(markup, /Title Visibility/);
assert.doesNotMatch(markup, /Anime Visibility/);
});
test('AnimeVisibilityFilter offers a per-chart title limit selector', () => {
const markup = renderToStaticMarkup(
<AnimeVisibilityFilter
@@ -43,25 +23,6 @@ test('AnimeVisibilityFilter offers a per-chart title limit selector', () => {
assert.match(markup, /<option value="7" selected="">/);
});
test('AnimeVisibilityFilter offers top vs most-recent ranking modes', () => {
const markup = renderToStaticMarkup(
<AnimeVisibilityFilter
animeTitles={['KonoSuba']}
hiddenAnime={new Set()}
maxTitles={7}
maxTitlesMode="recent"
onShowAll={() => {}}
onHideAll={() => {}}
onToggleAnime={() => {}}
onMaxTitlesChange={() => {}}
onMaxTitlesModeChange={() => {}}
/>,
);
assert.match(markup, /<option value="total">top<\/option>/);
assert.match(markup, /<option value="recent" selected="">most recent<\/option>/);
});
test('AnimeVisibilityFilter keeps the ranking mode selectable even when showing all titles', () => {
const markup = renderToStaticMarkup(
<AnimeVisibilityFilter
@@ -80,10 +41,3 @@ test('AnimeVisibilityFilter keeps the ranking mode selectable even when showing
assert.match(markup, /aria-label="Title ranking mode"/);
assert.doesNotMatch(markup, /aria-label="Title ranking mode"[^>]*disabled/);
});
test('TrendsTab source labels words per minute without reading speed wording', async () => {
const source = await readFile(new URL('./TrendsTab.tsx', import.meta.url), 'utf8');
assert.match(source, /title="Words \/ Min"/);
assert.doesNotMatch(source, /Reading Speed/);
});
@@ -3,6 +3,7 @@ import test from 'node:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { buildCrossAnimeWordRows, CrossAnimeWordsTable } from './CrossAnimeWordsTable';
import type { VocabularyEntry } from '../../types/stats';
import { withLocalStorage } from '../../test-utils/dom';
function makeEntry(over: Partial<VocabularyEntry>): VocabularyEntry {
return {
@@ -20,46 +21,6 @@ function makeEntry(over: Partial<VocabularyEntry>): VocabularyEntry {
} as VocabularyEntry;
}
function withLocalStorage<T>(initial: Record<string, string>, run: () => T): T {
const previous = Object.getOwnPropertyDescriptor(globalThis, 'localStorage');
const values = new Map(Object.entries(initial));
const storage = {
get length() {
return values.size;
},
clear() {
values.clear();
},
getItem(key: string) {
return values.get(key) ?? null;
},
key(index: number) {
return Array.from(values.keys())[index] ?? null;
},
removeItem(key: string) {
values.delete(key);
},
setItem(key: string, value: string) {
values.set(key, value);
},
} as Storage;
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
value: storage,
});
try {
return run();
} finally {
if (previous) {
Object.defineProperty(globalThis, 'localStorage', previous);
} else {
delete (globalThis as { localStorage?: unknown }).localStorage;
}
}
}
test('cross-title rows can hide kana-only headwords', () => {
const rows = buildCrossAnimeWordRows(
[
@@ -77,17 +38,6 @@ test('cross-title rows can hide kana-only headwords', () => {
);
});
test('cross-title table renders a Hide Kana filter button', () => {
const markup = renderToStaticMarkup(
<CrossAnimeWordsTable
words={[makeEntry({ headword: 'さらに', word: 'さらに', reading: 'さらに' })]}
knownWords={new Set()}
/>,
);
assert.match(markup, /Hide Kana/);
});
test('cross-title table uses saved Hide Kana preference on first render', () => {
const markup = withLocalStorage({ 'subminer.stats.crossAnimeWords.hideKanaOnly': 'true' }, () =>
renderToStaticMarkup(
@@ -1,47 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { Window } from 'happy-dom';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { apiClient } from '../../lib/api-client';
import type { StatsDuplicateLineCleanupResult } from '../../types/stats';
import { DuplicateLineCleanup } from './DuplicateLineCleanup';
interface TestWindow extends Window {
IS_REACT_ACT_ENVIRONMENT?: boolean;
}
function installDom(): () => void {
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
const previousHTMLElement = globalThis.HTMLElement;
const previousISReactActEnvironment = (
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT;
const window = new Window() as TestWindow;
Object.defineProperty(globalThis, 'window', { value: window, configurable: true });
Object.defineProperty(globalThis, 'document', { value: window.document, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: window.HTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
return () => {
Object.defineProperty(globalThis, 'window', { value: previousWindow, configurable: true });
Object.defineProperty(globalThis, 'document', { value: previousDocument, configurable: true });
Object.defineProperty(globalThis, 'HTMLElement', {
value: previousHTMLElement,
configurable: true,
});
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = previousISReactActEnvironment;
};
}
import { installDom } from '../../test-utils/dom';
function findButton(container: Element, label: string): HTMLButtonElement {
const match = [...container.querySelectorAll('button')].find(
@@ -7,6 +7,7 @@ import {
isKanaOnlyTokenText,
} from './FrequencyRankTable';
import type { VocabularyEntry } from '../../types/stats';
import { withLocalStorage } from '../../test-utils/dom';
function makeEntry(over: Partial<VocabularyEntry>): VocabularyEntry {
return {
@@ -24,46 +25,6 @@ function makeEntry(over: Partial<VocabularyEntry>): VocabularyEntry {
} as VocabularyEntry;
}
function withLocalStorage<T>(initial: Record<string, string>, run: () => T): T {
const previous = Object.getOwnPropertyDescriptor(globalThis, 'localStorage');
const values = new Map(Object.entries(initial));
const storage = {
get length() {
return values.size;
},
clear() {
values.clear();
},
getItem(key: string) {
return values.get(key) ?? null;
},
key(index: number) {
return Array.from(values.keys())[index] ?? null;
},
removeItem(key: string) {
values.delete(key);
},
setItem(key: string, value: string) {
values.set(key, value);
},
} as Storage;
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
value: storage,
});
try {
return run();
} finally {
if (previous) {
Object.defineProperty(globalThis, 'localStorage', previous);
} else {
delete (globalThis as { localStorage?: unknown }).localStorage;
}
}
}
test('renders headword and reading inline in a single column (no separate Reading header)', () => {
const entry = makeEntry({});
const markup = renderToStaticMarkup(
@@ -117,14 +78,6 @@ test('frequency rows can hide kana-only headwords', () => {
);
});
test('renders a Hide Kana filter button', () => {
const entry = makeEntry({ headword: 'さらに', word: 'さらに', reading: 'さらに' });
const markup = renderToStaticMarkup(
<FrequencyRankTable words={[entry]} knownWords={new Set()} />,
);
assert.match(markup, /Hide Kana/);
});
test('uses saved Hide Kana preference on first render', () => {
const markup = withLocalStorage({ 'subminer.stats.frequencyRank.hideKanaOnly': 'true' }, () =>
renderToStaticMarkup(
@@ -1,21 +0,0 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { KanjiDetailPanel } from './KanjiDetailPanel';
test('KanjiDetailPanel uses the centered detail modal layout', () => {
const markup = renderToStaticMarkup(
createElement(KanjiDetailPanel, { kanjiId: 1, onClose: () => {} }),
);
assert.match(
markup,
/class="[^"]*fixed[^"]*inset-0[^"]*z-40[^"]*flex[^"]*items-center[^"]*justify-center[^"]*p-4/,
);
assert.match(
markup,
/class="[^"]*relative[^"]*flex[^"]*max-h-\[85vh\][^"]*w-full[^"]*max-w-2xl[^"]*flex-col/,
);
assert.doesNotMatch(markup, /class="[^"]*absolute[^"]*right-0[^"]*top-0[^"]*h-full/);
});
@@ -1,32 +0,0 @@
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 WORD_DETAIL_PANEL_PATH = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'WordDetailPanel.tsx',
);
test('WordDetailPanel uses the shared stats mining payload builder', () => {
const source = fs.readFileSync(WORD_DETAIL_PANEL_PATH, 'utf8');
assert.match(source, /buildStatsMineCardParams/);
assert.match(source, /getStatsMineCardUnavailableReason/);
assert.match(source, /buildStatsMineCardParams\(\s*occ,\s*data!\.detail\.headword,\s*mode\s*\)/);
});
test('WordDetailPanel shows partial media mining errors instead of silent success', () => {
const source = fs.readFileSync(WORD_DETAIL_PANEL_PATH, 'utf8');
assert.match(source, /getStatsMineCardError/);
assert.match(source, /const responseError = getStatsMineCardError\(result\);/);
});
test('WordDetailPanel uses the wider centered detail modal layout', () => {
const source = fs.readFileSync(WORD_DETAIL_PANEL_PATH, 'utf8');
assert.match(source, /fixed inset-0 z-40 flex items-center justify-center p-4/);
assert.match(source, /relative flex max-h-\[85vh\] w-full max-w-2xl flex-col/);
});