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.
This commit is contained in:
@@ -0,0 +1,177 @@
|
|||||||
|
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" } });
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user