v2.53 — Sprint E: Recap-Zeiträume (Woche/Monat/Jahr)

## Recap-Zeiträume
- recap_service.dart: RecapZeitraum-Enum (woche/monat/jahr), berechne(zeitraum:, jetzt:) mit injizierbarem Historie-Loader (Tests ohne DB), Fallback-Kette Woche → Monat → Jahr → Gesamt-Historie, Zeitraum-Getter (kopfzeile, introTitel, introUntertitel, zeitraumLabel, shareTitel, Monatsname)
- recap_screen.dart: Zeitraum-Umschalter (7 Tage / Monat / Jahr) oben im Recap, dynamische Texte („Deine Woche in Musik“, „Abspielungen in dieser Woche“, „Melo Wochen-Recap“), Seiten-Reset bei Wechsel
- home_screen.dart: Recap-Karte „Recap 2026“ + „Woche · Monat · Jahr“-Untertitel
- settings_screen.dart: Kachel „Recap (Woche/Monat/Jahr)“
- test/recap_service_test.dart: 12 neue Tests (Filter, Fallback-Kette, Aggregation, Label-Getter)
- flutter analyze 0 Issues, Tests 123/123
This commit is contained in:
Dustin
2026-08-05 11:10:49 +02:00
parent bc3357bf72
commit 22ce611537
5 changed files with 454 additions and 27 deletions
+220
View File
@@ -0,0 +1,220 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:melo_app/services/recap_service.dart';
/// RecapService-Tests mit injiziertem Historie-Loader (kein DB-Zugriff).
/// Abgedeckt: Zeitraum-Filter (Woche/Monat/Jahr), Fallback-Kette
/// (Woche → Monat → Jahr → Gesamt), Aggregation + Label-Getter.
void main() {
// Fester Anker-Zeitpunkt: 5. August 2026, 15:00 Uhr
final jetzt = DateTime(2026, 8, 5, 15, 0, 0);
Map<String, dynamic> zeile(
int id,
String playedAt, {
String titel = 'Song',
String kuenstler = 'Künstler',
int dauer = 200,
}) =>
{
'song_id': id,
'played_at': playedAt,
'titel': titel,
'kuenstler': kuenstler,
'dauer_sekunden': dauer,
};
RecapService service(List<Map<String, dynamic>> rows) =>
RecapService(historie: () async => rows);
test('leere History → null (alle Zeiträume)', () async {
expect(await service([]).berechne(jetzt: jetzt), isNull);
expect(await service([]).berechne(zeitraum: RecapZeitraum.woche, jetzt: jetzt),
isNull);
expect(
await service([])
.berechne(zeitraum: RecapZeitraum.monat, jetzt: jetzt),
isNull);
});
group('Jahr', () {
test('nur Einträge des aktuellen Jahres zählen', () async {
final rows = [
zeile(1, '2026-03-01T10:00:00'),
zeile(2, '2026-08-01T10:00:00'),
zeile(3, '2025-12-31T23:59:00'), // anderes Jahr → raus
];
final d = await service(rows).berechne(jetzt: jetzt);
expect(d, isNotNull);
expect(d!.gesamtAnzahl, 2);
expect(d.zeitraum, RecapZeitraum.jahr);
});
test('Jahr ohne Daten → Gesamt-Historie (alle Einträge)', () async {
final rows = [
zeile(1, '2024-01-01T10:00:00'),
zeile(2, '2023-06-15T10:00:00'),
];
final d = await service(rows).berechne(jetzt: jetzt);
expect(d, isNotNull);
expect(d!.gesamtAnzahl, 2); // Fallback auf Gesamt
});
});
group('Monat', () {
test('nur Einträge des aktuellen Kalendermonats zählen', () async {
final rows = [
zeile(1, '2026-08-01T10:00:00'),
zeile(2, '2026-08-05T08:00:00'),
zeile(3, '2026-07-31T23:59:00'), // Juli → raus
zeile(4, '2026-01-15T10:00:00'), // Januar → raus
];
final d =
await service(rows).berechne(zeitraum: RecapZeitraum.monat, jetzt: jetzt);
expect(d, isNotNull);
expect(d!.gesamtAnzahl, 2);
expect(d.zeitraum, RecapZeitraum.monat);
expect(d.monatsName, 'August');
});
test('Monat ohne Daten → nächstgrößerer Zeitraum (Jahr)', () async {
final rows = [
zeile(1, '2026-03-01T10:00:00'),
zeile(2, '2026-05-15T10:00:00'),
];
final d =
await service(rows).berechne(zeitraum: RecapZeitraum.monat, jetzt: jetzt);
expect(d, isNotNull);
expect(d!.zeitraum, RecapZeitraum.jahr); // Fallback Monat → Jahr
expect(d.gesamtAnzahl, 2);
});
});
group('Woche', () {
test('nur Einträge der letzten 7 Tage zählen', () async {
final rows = [
zeile(1, '2026-08-04T10:00:00'), // innerhalb 7 Tage
zeile(2, '2026-07-30T12:00:00'), // genau 6 Tage vorher → drin
zeile(3, '2026-07-29T12:00:00'), // > 7 Tage → raus
zeile(4, '2026-01-01T10:00:00'), // alt → raus
];
final d =
await service(rows).berechne(zeitraum: RecapZeitraum.woche, jetzt: jetzt);
expect(d, isNotNull);
expect(d!.gesamtAnzahl, 2);
expect(d.zeitraum, RecapZeitraum.woche);
});
test('Woche ohne Daten → Monat (nächstgrößerer Zeitraum)', () async {
// Anker 20.8.: 7-Tage-Grenze = 13.8. → 10.8. liegt im Monat, aber
// außerhalb der letzten 7 Tage.
final spaeter = DateTime(2026, 8, 20, 15, 0, 0);
final rows = [
zeile(1, '2026-08-10T10:00:00'),
];
final d = await service(rows)
.berechne(zeitraum: RecapZeitraum.woche, jetzt: spaeter);
expect(d, isNotNull);
expect(d!.zeitraum, RecapZeitraum.monat);
expect(d.gesamtAnzahl, 1);
});
test('Woche UND Monat ohne Daten → Jahr', () async {
final rows = [
zeile(1, '2026-03-01T10:00:00'),
];
final d =
await service(rows).berechne(zeitraum: RecapZeitraum.woche, jetzt: jetzt);
expect(d, isNotNull);
expect(d!.zeitraum, RecapZeitraum.jahr);
});
});
group('Aggregation', () {
test('Top 5 Songs + Top 3 Künstler + Hörzeit + Nacht-%', () async {
final rows = [
// Song A 3×, Song B 2×, Song C 1×
zeile(1, '2026-08-01T10:00:00', titel: 'A', kuenstler: 'K1', dauer: 300),
zeile(1, '2026-08-02T10:00:00', titel: 'A', kuenstler: 'K1', dauer: 300),
zeile(1, '2026-08-03T23:00:00', titel: 'A', kuenstler: 'K1', dauer: 300), // nach 22 Uhr
zeile(2, '2026-08-01T11:00:00', titel: 'B', kuenstler: 'K1', dauer: 240),
zeile(2, '2026-08-02T11:00:00', titel: 'B', kuenstler: 'K1', dauer: 240),
zeile(3, '2026-08-01T12:00:00', titel: 'C', kuenstler: 'K2', dauer: 180),
];
final d =
await service(rows).berechne(zeitraum: RecapZeitraum.monat, jetzt: jetzt);
expect(d, isNotNull);
expect(d!.gesamtAnzahl, 6);
expect(d.topSongs.length, 3);
expect(d.topSongs.first.song.titel, 'A');
expect(d.topSongs.first.anzahl, 3);
expect(d.topKuenstler.length, 2);
expect(d.topKuenstler.first.name, 'K1');
expect(d.topKuenstler.first.anzahl, 5);
// Hörzeit: 3×300 + 2×240 + 1×180 = 1560s = 26 Min
expect(d.gesamtSekunden, 1560);
expect(d.hoerzeitFormatiert, '26 Min');
// 1 von 6 Abspielungen nach 22 Uhr → 17%
expect(d.nachtProzent, 17);
expect(d.haeufigsterTag, isNotEmpty);
});
});
group('Label-Getter', () {
test('Woche: Kopfzeile, Intro, Teilen', () {
final d = RecapDaten(
zeitraum: RecapZeitraum.woche,
jahr: 2026,
monat: 8,
gesamtAnzahl: 12,
gesamtSekunden: 3600,
haeufigsterTag: 'Freitag',
nachtProzent: 30,
topSongs: [],
topKuenstler: [],
);
expect(d.kopfzeile, 'MELO RECAP · 7 TAGE');
expect(d.introTitel, '7 Tage');
expect(d.introUntertitel, 'Deine Woche in Musik');
expect(d.zeitraumLabel, 'Diese Woche');
expect(d.shareTitel, 'Melo Wochen-Recap');
});
test('Monat: Kopfzeile, Intro, Teilen', () {
final d = RecapDaten(
zeitraum: RecapZeitraum.monat,
jahr: 2026,
monat: 8,
gesamtAnzahl: 12,
gesamtSekunden: 3600,
haeufigsterTag: 'Freitag',
nachtProzent: 30,
topSongs: [],
topKuenstler: [],
);
expect(d.kopfzeile, 'MELO RECAP · AUGUST');
expect(d.introTitel, 'August');
expect(d.introUntertitel, 'Dein Monat in Musik');
expect(d.zeitraumLabel, 'Dein Monat');
expect(d.shareTitel, 'Melo Monats-Recap August');
});
test('Jahr: Kopfzeile, Intro, Teilen', () {
final d = RecapDaten(
zeitraum: RecapZeitraum.jahr,
jahr: 2026,
monat: 8,
gesamtAnzahl: 12,
gesamtSekunden: 3600,
haeufigsterTag: 'Freitag',
nachtProzent: 30,
topSongs: [],
topKuenstler: [],
);
expect(d.kopfzeile, 'MELO RECAP 2026');
expect(d.introTitel, '2026');
expect(d.introUntertitel, 'Dein Jahr in Musik');
expect(d.zeitraumLabel, 'Dein Jahr');
expect(d.shareTitel, 'Melo Jahres-Recap 2026');
});
});
}