import "./setup"; import assert from "node:assert/strict"; import test, { after, before } from "node:test"; import { installTmdbStub, SERIES_ID, MOVIE_ID } from "./fixtures/tmdb"; import { prisma } from "../src/lib/db"; import { importMovie, importSeries } from "../src/lib/catalog"; import { addToLibrary, markEpisodesWatched, markMovieWatched } from "../src/lib/library"; import { getContinueWatching, getMonthlyActivity, getTotals, getUpcomingEpisodes, getYearReview, } from "../src/lib/stats"; /** * Prüft die Auswertungen. Besonderes Augenmerk auf die Monatsgruppierung: * dort führte ein Zeitzonenversatz zwischen Datenbank und JavaScript einmal * dazu, dass das Diagramm leer blieb, obwohl Sichtungen vorlagen. */ const stub = installTmdbStub(); const TEST_EMAIL = "test-stats@flimmer.invalid"; let userId: string; let entryId: string; let seriesTitleId: string; async function cleanup() { await prisma.user.deleteMany({ where: { email: TEST_EMAIL } }); await prisma.title.deleteMany({ where: { tmdbId: { gte: 999000, lt: 1000000 } } }); } before(async () => { await cleanup(); userId = ( await prisma.user.create({ data: { email: TEST_EMAIL, name: "Statistik" }, select: { id: true } }) ).id; seriesTitleId = (await importSeries(SERIES_ID, { region: "DE" })).id; const movieTitleId = (await importMovie(MOVIE_ID, "DE")).id; const entry = await addToLibrary(userId, seriesTitleId, "WATCHING"); entryId = entry.id; const episodes = await prisma.episode.findMany({ where: { titleId: seriesTitleId, seasonNumber: { gt: 0 } }, orderBy: [{ seasonNumber: "asc" }, { episodeNumber: "asc" }], select: { id: true }, }); // Drei Episoden: heute, vor einem Monat, vor zwei Monaten. for (const [index, episode] of episodes.slice(0, 3).entries()) { const when = new Date(); when.setMonth(when.getMonth() - index); when.setHours(20, 30, 0, 0); await markEpisodesWatched(userId, entry.id, [episode.id], when); } const movieEntry = await addToLibrary(userId, movieTitleId, "WATCHING"); await markMovieWatched(userId, movieEntry.id, new Date()); }); after(async () => { await cleanup(); stub.restore(); await prisma.$disconnect(); }); test("Gesamtzahlen zählen Episoden und Filme getrennt", async () => { const totals = await getTotals(userId); assert.equal(totals.episodes, 3); assert.equal(totals.movies, 1); // 52 + 47 + 48 aus den Episoden, 118 aus dem Film assert.equal(totals.minutes, 52 + 47 + 48 + 118); assert.equal(totals.seriesCount, 1); assert.equal(totals.movieCount, 1); }); test("Monatsverlauf ordnet Sichtungen dem richtigen Monat zu", async () => { const points = await getMonthlyActivity(userId, 12); assert.equal(points.length, 12); const total = points.reduce((sum, point) => sum + point.episodes + point.movies, 0); assert.equal(total, 4, "keine Sichtung fällt beim Gruppieren durchs Raster"); const current = points[points.length - 1]; assert.ok(current); assert.equal(current.episodes, 1, "die Sichtung von heute liegt im laufenden Monat"); assert.equal(current.movies, 1); const previous = points[points.length - 2]; assert.equal(previous?.episodes, 1, "die Sichtung von vor einem Monat liegt im Vormonat"); // Lückenlos: auch Monate ohne Sichtung sind vertreten. assert.ok(points.some((point) => point.episodes === 0 && point.movies === 0)); }); test("Jahresrückblick fasst das laufende Jahr zusammen", async () => { const review = await getYearReview(userId, new Date().getFullYear()); assert.ok(review.minutes > 0); assert.ok(review.episodes >= 1); assert.equal(review.movies, 1); assert.ok(review.busiestMonth, "der stärkste Monat wird benannt"); assert.match( review.busiestMonth!.label, /\d{4}$/, "die Beschriftung endet auf die Jahreszahl, nicht auf einen rohen Zeitstempel", ); assert.ok(review.topTitle); }); test("Weiterschauen nennt die nächste offene Episode", async () => { const items = await getContinueWatching(userId, 10); const series = items.find((item) => item.titleId === seriesTitleId); assert.ok(series, "die laufende Serie steht in der Liste"); assert.equal(series.watched, 3); assert.equal(series.total, 5); assert.equal(series.next?.episodeNumber, 1); assert.equal(series.next?.seasonNumber, 2, "nach Staffel 1 kommt Staffel 2"); assert.ok(series.lastWatchedAt); }); test("Weiterschauen lässt durchgeschaute Serien weg", async () => { const episodes = await prisma.episode.findMany({ where: { titleId: seriesTitleId, seasonNumber: { gt: 0 } }, select: { id: true }, }); await markEpisodesWatched(userId, entryId, episodes.map((episode) => episode.id)); const items = await getContinueWatching(userId, 10); assert.equal( items.find((item) => item.titleId === seriesTitleId), undefined, "wo nichts mehr offen ist, gibt es auch nichts weiterzuschauen", ); }); test("Kalender liefert nur Episoden im gefragten Zeitraum", async () => { // Die Fixture hat eine Episode am 08.01.2099. Der vorige Test hat die Serie // auf „Abgeschlossen" gebracht — sie muss trotzdem im Kalender stehen, denn // bei einer laufenden Serie heißt „abgeschlossen" nur: auf dem Stand. const episodes = await getUpcomingEpisodes( userId, new Date("2098-12-01T00:00:00.000Z"), new Date("2099-03-01T00:00:00.000Z"), ); assert.equal(episodes.length, 1); assert.equal(episodes[0]?.seasonNumber, 2); assert.equal(episodes[0]?.episodeNumber, 2); const empty = await getUpcomingEpisodes( userId, new Date("2050-01-01T00:00:00.000Z"), new Date("2050-12-31T00:00:00.000Z"), ); assert.equal(empty.length, 0, "außerhalb des Zeitraums kommt nichts"); }); test("Abgebrochenes taucht im Kalender nicht mehr auf", async () => { await prisma.libraryEntry.update({ where: { id: entryId }, data: { status: "DROPPED" } }); const episodes = await getUpcomingEpisodes( userId, new Date("2098-12-01T00:00:00.000Z"), new Date("2099-03-01T00:00:00.000Z"), ); assert.equal(episodes.length, 0, "wer abgebrochen hat, braucht keine Erinnerung"); await prisma.libraryEntry.update({ where: { id: entryId }, data: { status: "COMPLETED" } }); });