import assert from 'node:assert/strict'; import test from 'node:test'; 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'; import { installDom } from '../../test-utils/dom'; function libraryItem(anilistId: number | null): AnimeLibraryItem { return { mediaKind: 'anime', animeId: 7, canonicalTitle: 'Test Anime Season 2', anilistId, tmdbId: null, tmdbType: null, totalSessions: 1, totalActiveMs: 1000, totalCards: 0, totalTokensSeen: 0, episodeCount: 1, episodesTotal: 13, lastWatchedMs: 1, }; } function detailData(anilistId: number | null): AnimeDetailData { return { detail: { mediaKind: 'anime', animeId: 7, canonicalTitle: 'Test Anime Season 2', anilistId, tmdbId: null, tmdbType: null, titleRomaji: null, titleEnglish: null, titleNative: null, description: null, totalSessions: 1, totalActiveMs: 1000, totalCards: 0, totalTokensSeen: 0, totalLinesSeen: 0, totalLookupCount: 0, totalLookupHits: 0, totalYomitanLookupCount: 0, episodeCount: 1, lastWatchedMs: 1, }, episodes: [], anilistEntries: [], }; } function findButton(container: Element, label: string): HTMLElement { const match = [...container.querySelectorAll('button')].find((button) => (button.textContent ?? '').includes(label), ); assert.ok(match, `expected a "${label}" button`); return match as unknown as HTMLElement; } test('AnimeTab refetches the library after the AniList entry is relinked', async () => { const uninstallDom = installDom(); const original = { getAnimeLibrary: apiClient.getAnimeLibrary, getAnimeDetail: apiClient.getAnimeDetail, getAnimeWords: apiClient.getAnimeWords, getAnimeRollups: apiClient.getAnimeRollups, getAnimeKnownWordsSummary: apiClient.getAnimeKnownWordsSummary, searchAnilist: apiClient.searchAnilist, reassignAnimeAnilist: apiClient.reassignAnimeAnilist, }; // The library grid keys its cover URL off anilistId, so a stale list keeps // pointing at the previous entry's art. let anilistId: number | null = 14813; let libraryFetches = 0; apiClient.getAnimeLibrary = (async () => { libraryFetches += 1; return [libraryItem(anilistId)]; }) as typeof apiClient.getAnimeLibrary; apiClient.getAnimeDetail = (async () => detailData(anilistId)) as typeof apiClient.getAnimeDetail; apiClient.getAnimeWords = (async () => []) as typeof apiClient.getAnimeWords; apiClient.getAnimeRollups = (async () => []) as typeof apiClient.getAnimeRollups; apiClient.getAnimeKnownWordsSummary = (async () => ({ totalUniqueWords: 0, knownWordCount: 0, })) as typeof apiClient.getAnimeKnownWordsSummary; apiClient.searchAnilist = (async () => [ { id: 108489, episodes: 12, season: null, seasonYear: null, description: null, coverImage: null, title: { romaji: 'Test Anime Kan', english: null, native: null }, }, ]) as typeof apiClient.searchAnilist; apiClient.reassignAnimeAnilist = (async (_animeId: number, info: { anilistId: number }) => { anilistId = info.anilistId; }) as typeof apiClient.reassignAnimeAnilist; try { const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); await act(async () => { root.render(); }); assert.equal(libraryFetches, 1); assert.match( container.querySelector('img')?.getAttribute('src') ?? '', /\/api\/stats\/anime\/7\/cover\?coverRetry=14813/, ); await act(async () => { findButton(container, 'Test Anime Season 2').click(); }); await act(async () => { findButton(container, 'Change AniList Entry').click(); }); await act(async () => { findButton(container, 'Test Anime Kan').click(); }); await act(async () => { findButton(container, 'Back to Library').click(); }); assert.equal(libraryFetches, 2); assert.match( container.querySelector('img')?.getAttribute('src') ?? '', /\/api\/stats\/anime\/7\/cover\?coverRetry=108489/, ); await act(async () => { root.unmount(); }); } finally { Object.assign(apiClient, original); uninstallDom(); } }); test('AnimeTab watch time follows the displayed kind filter', async () => { const uninstallDom = installDom(); const original = apiClient.getAnimeLibrary; apiClient.getAnimeLibrary = async () => [ { ...libraryItem(42), totalActiveMs: 3600000 }, { ...libraryItem(null), animeId: 8, canonicalTitle: 'Drama', mediaKind: 'live_action', tmdbId: 12, tmdbType: 'tv', totalActiveMs: 7200000, }, ]; const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => { root.render(); }); assert.match(container.textContent ?? '', /2 titles · 3h/); await act(async () => { findButton(container, 'Live Action').click(); }); assert.match(container.textContent ?? '', /1 title · 2h/); assert.equal(findButton(container, 'Live Action').getAttribute('aria-pressed'), 'true'); } finally { await act(async () => { root.unmount(); }); apiClient.getAnimeLibrary = original; uninstallDom(); } }); test('Library kind filter separates YouTube channels from anime and updates totals', async () => { const uninstallDom = installDom(); const original = { getAnimeLibrary: apiClient.getAnimeLibrary, getAnimeMergeRecommendations: apiClient.getAnimeMergeRecommendations, }; apiClient.getAnimeLibrary = async () => [ { ...libraryItem(null), totalActiveMs: 60_000 }, { ...libraryItem(null), animeId: 8, mediaKind: 'youtube', canonicalTitle: 'Language Channel', totalActiveMs: 120_000, }, ]; apiClient.getAnimeMergeRecommendations = async () => ({ recommendations: [] }); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => { root.render(); }); assert.match(container.textContent ?? '', /Test Anime Season 2/); assert.match(container.textContent ?? '', /Language Channel/); assert.match(container.textContent ?? '', /2 titles · 3m/); await act(async () => { findButton(container, 'YouTube').click(); }); assert.doesNotMatch(container.textContent ?? '', /Test Anime Season 2/); assert.match(container.textContent ?? '', /Language Channel/); assert.match(container.textContent ?? '', /1 channel · 2m/); assert.equal(findButton(container, 'YouTube').getAttribute('aria-pressed'), 'true'); await act(async () => { findButton(container, 'Anime').click(); }); assert.match(container.textContent ?? '', /Test Anime Season 2/); assert.doesNotMatch(container.textContent ?? '', /Language Channel/); await act(async () => { findButton(container, 'All Titles').click(); }); assert.match(container.textContent ?? '', /Language Channel/); } finally { await act(async () => { root.unmount(); }); Object.assign(apiClient, original); uninstallDom(); } });