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