Files
Florian Wilde 075b42584a fix: Kalender zeigt auch abgeschlossene, weiterlaufende Serien
Wer bei einer laufenden Serie auf dem Stand ist, gilt als „abgeschlossen" —
und verlor damit die kommenden Folgen aus dem Kalender, obwohl man genau die
dort sehen will. Jetzt faellt nur Abgebrochenes heraus.

Ausserdem: Der SVG-Titel des Diagramms brach die Hydration, weil React
<title> als Dokument-Metadatum behandelt und aus dem SVG heraushebt.
2026-09-01 19:54:16 +02:00

178 lines
6.1 KiB
TypeScript

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" } });
});