mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 16:46:06 -08:00
test: cut low-value tests, rewrite the rest around behavior, parallelize lanes (#285)
This commit is contained in:
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,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/);
|
||||
});
|
||||
Reference in new issue
Block a user