widget refresh enhancements

This commit is contained in:
2026-08-06 20:22:32 +02:00
parent 646e2c0451
commit ab23422a86
22 changed files with 1027 additions and 99 deletions
@@ -0,0 +1,67 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/background/widget_background_task.dart';
void main() {
final now = DateTime(2026, 8, 6, 12, 0);
group('shouldSkipRefresh', () {
test('skips when the snapshot is younger than the debounce window', () {
expect(
shouldSkipRefresh(
fetchedAt: now.subtract(const Duration(minutes: 5)),
now: now,
force: false,
),
isTrue,
);
});
test('refreshes when the snapshot is older than the debounce window', () {
expect(
shouldSkipRefresh(
fetchedAt: now.subtract(const Duration(minutes: 15)),
now: now,
force: false,
),
isFalse,
);
});
test('refreshes exactly at the debounce boundary', () {
expect(
shouldSkipRefresh(
fetchedAt: now.subtract(WidgetBackgroundTask.refreshDebounce),
now: now,
force: false,
),
isFalse,
);
});
test('force bypasses a fresh snapshot', () {
expect(
shouldSkipRefresh(
fetchedAt: now.subtract(const Duration(minutes: 1)),
now: now,
force: true,
),
isFalse,
);
});
test('refreshes when no snapshot exists yet', () {
expect(shouldSkipRefresh(fetchedAt: null, now: now, force: false), isFalse);
});
test('refreshes when fetchedAt lies in the future (clock change)', () {
expect(
shouldSkipRefresh(
fetchedAt: now.add(const Duration(minutes: 5)),
now: now,
force: false,
),
isFalse,
);
});
});
}
+41
View File
@@ -17,6 +17,47 @@ void main() {
);
});
test('widget refresh push identified by source + type', () {
expect(
classifyPush({'source': 'connect', 'type': 'widget-refresh'}),
PushKind.widgetRefresh,
);
});
test('widget refresh push with reason still classifies', () {
expect(
classifyPush({
'source': 'connect',
'type': 'widget-refresh',
'reason': 'morning',
}),
PushKind.widgetRefresh,
);
});
test('connect push with unrelated type stays connect', () {
expect(
classifyPush({'source': 'connect', 'type': 'newsletter', 'title': 'Hi'}),
PushKind.connect,
);
});
test('nextcloud fields take precedence over widget-refresh type', () {
expect(
classifyPush({
'subject': 'enc',
'signature': 'sig',
'source': 'connect',
'type': 'widget-refresh',
}),
PushKind.nextcloud,
);
});
test('widget-refresh type without connect source is unknown', () {
expect(classifyPush({'type': 'widget-refresh'}), PushKind.unknown);
});
test('subject without signature is not a nextcloud push', () {
expect(classifyPush({'subject': 'enc'}), PushKind.unknown);
});
+93 -2
View File
@@ -345,15 +345,32 @@ void main() {
});
});
group('startOfCalendarWeek', () {
test('Tuesday maps to its own Monday', () {
expect(
WidgetDataMapper.startOfCalendarWeek(DateTime(2026, 5, 5, 10)),
DateTime(2026, 5, 4),
);
});
test('Sunday stays in the current week (no roll-forward)', () {
expect(
WidgetDataMapper.startOfCalendarWeek(DateTime(2026, 5, 10, 22)),
DateTime(2026, 5, 4),
);
});
});
group('buildWeekData', () {
final now = DateTime(2026, 5, 5, 10); // Tuesday
test('contains lessons across the school week', () {
test('covers the 14-day window including next week', () {
final lessons = [
_lesson(date: DateTime(2026, 5, 4), startHhmm: 800, endHhmm: 845, subjectName: 'MO'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 800, endHhmm: 845, subjectName: 'WE'),
_lesson(date: DateTime(2026, 5, 8), startHhmm: 800, endHhmm: 845, subjectName: 'FR'),
_lesson(date: DateTime(2026, 5, 11), startHhmm: 800, endHhmm: 845, subjectName: 'NEXT'),
_lesson(date: DateTime(2026, 5, 18), startHhmm: 800, endHhmm: 845, subjectName: 'FAR'),
];
final data = WidgetDataMapper.buildWeekData(
now: now,
@@ -365,8 +382,82 @@ void main() {
expect(data.anchorDate, DateTime(2026, 5, 4));
expect(
data.lessons.map((l) => l.subjectShort).toList(),
['MO', 'WE', 'FR'],
['MO', 'WE', 'FR', 'NEXT'],
);
});
test('window stays on the current week when the anchor rolls forward', () {
// Friday evening: day/week anchors jump to next Monday, but the window
// still starts at the current week's Monday so today stays available.
final fridayEvening = DateTime(2026, 5, 8, 18);
final lessons = [
_lesson(date: DateTime(2026, 5, 8), startHhmm: 800, endHhmm: 845, subjectName: 'FR'),
_lesson(date: DateTime(2026, 5, 11), startHhmm: 800, endHhmm: 845, subjectName: 'NEXT'),
];
final data = WidgetDataMapper.buildWeekData(
now: fridayEvening,
lessons: lessons,
subjects: null,
rooms: null,
holidays: null,
);
expect(data.anchorDate, DateTime(2026, 5, 11));
expect(
data.lessons.map((l) => l.subjectShort).toList(),
['FR', 'NEXT'],
);
});
test('carries per-day holiday info in days', () {
final holidays = TimetableGetHolidaysResponse(
result: [
McHoliday(
shortName: 'Pfingsten',
longName: 'Pfingstferien',
startDate: DateTime(2026, 5, 14),
endDate: DateTime(2026, 5, 15),
),
],
);
final data = WidgetDataMapper.buildWeekData(
now: now,
lessons: const [],
subjects: null,
rooms: null,
holidays: holidays,
);
expect(data.days, hasLength(WidgetDataMapper.weekWindowDays));
expect(data.days.first.date, DateTime(2026, 5, 4));
expect(data.days.last.date, DateTime(2026, 5, 17));
final holidayDays =
data.days.where((d) => d.isHoliday).map((d) => d.date).toList();
expect(holidayDays, [DateTime(2026, 5, 14), DateTime(2026, 5, 15)]);
expect(
data.days.firstWhere((d) => d.isHoliday).holidayName,
'Pfingstferien',
);
});
test('sets top-level holiday flags for the anchor day', () {
final holidays = TimetableGetHolidaysResponse(
result: [
McHoliday(
shortName: 'Oster',
longName: 'Osterferien',
startDate: DateTime(2026, 5, 4),
endDate: DateTime(2026, 5, 8),
),
],
);
final data = WidgetDataMapper.buildWeekData(
now: now,
lessons: const [],
subjects: null,
rooms: null,
holidays: holidays,
);
expect(data.isHoliday, isTrue);
expect(data.holidayName, 'Osterferien');
});
});
}