import { describe, expect, it } from 'vitest'; import settings from '$stores/settings'; import type { Media } from '$lib/Data/AniList/media'; import { season } from '$lib/Media/Anime/season'; import { hasDueEpisodes, getAnimeEpisodeState } from '$lib/Media/Anime/Airing/classify'; import { injectAiringTime } from '$lib/Media/Anime/Airing/Subtitled/match'; import type { SubsPlease } from '$lib/Media/Anime/Airing/Subtitled/subsPlease'; const toScheduleTime = (epochSeconds: number) => { const date = new Date(epochSeconds * 1000); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; }; const baseMedia = (id: number): Media => ({ id, idMal: id, status: 'RELEASING', type: 'ANIME', episodes: 12, chapters: 0, volumes: 0, duration: 24, format: 'TV', title: { romaji: `Fixture Show ${id}`, english: `Fixture Show ${id}`, native: `Fixture Show ${id}` }, nextAiringEpisode: { episode: 8, airingAt: Math.floor(Date.now() / 1000) + 24 * 60 * 60 }, synonyms: [], mediaListEntry: { progress: 6, progressVolumes: 0, status: 'CURRENT', score: 0, repeat: 0, startedAt: { year: 2025, month: 1, day: 1 }, completedAt: { year: 0, month: 0, day: 0 }, createdAt: 0, updatedAt: 0, customLists: {} }, startDate: { year: 2025, month: 1 }, endDate: { year: 2025, month: 12 }, coverImage: { extraLarge: 'https://example.com/cover-xl.jpg', medium: 'https://example.com/cover-md.jpg' }, tags: [], genres: [], season: season(), isAdult: false, relations: { edges: [] } }) as Media; const regressionIds = [192507, 189259, 198767]; describe('anime episode classification', () => { it('prefers nativeEpisode for due/upcoming classification', () => { const media = baseMedia(192507); media.nextAiringEpisode = { episode: 7, nativeEpisode: 8, airingAt: Math.floor(Date.now() / 1000) + 6 * 60 * 60, nativeAiringAt: Math.floor(Date.now() / 1000) + 18 * 60 * 60 }; const state = getAnimeEpisodeState(media); expect(state.airedEpisodes).toBe(7); expect(hasDueEpisodes(media)).toBe(true); }); it('treats stale native release data as aired for due detection', () => { const media = baseMedia(189259); media.nextAiringEpisode = { episode: 9, airingAt: Math.floor(Date.now() / 1000) + 72 * 60 * 60, nativeAiringAt: Math.floor(Date.now() / 1000) - 60 * 60 }; const state = getAnimeEpisodeState(media); expect(state.airedEpisodes).toBe(9); expect(hasDueEpisodes(media)).toBe(true); }); }); describe('native countdown toggle parity', () => { for (const id of regressionIds) { it(`keeps media ${id} due with native countdown on/off`, () => { const media = baseMedia(id); const subtitledAiringAt = Math.floor(Date.now() / 1000) + 24 * 60 * 60; const nativeAiringAt = subtitledAiringAt + 12 * 60 * 60; const nativeAiringDate = new Date(nativeAiringAt * 1000); const airingDay = nativeAiringDate.toLocaleString('en-US', { weekday: 'long' }); const subsPlease = { tz: 'America/Los_Angeles', schedule: { [airingDay]: [ { title: media.title.romaji, page: '', image_url: '', time: toScheduleTime(subtitledAiringAt) } ] } } as unknown as SubsPlease; media.nextAiringEpisode = { episode: 8, airingAt: nativeAiringAt }; settings.setKey('displayNativeCountdown', true); const nativeOnly = injectAiringTime(media, subsPlease); settings.setKey('displayNativeCountdown', false); const subtitled = injectAiringTime(media, subsPlease); expect(hasDueEpisodes(nativeOnly)).toBe(true); expect(hasDueEpisodes(subtitled)).toBe(true); }); } });