Files
Client/test/widget_data/widget_data_mapper_test.dart
T

698 lines
19 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import 'package:marianum_mobile/api/mhsl/custom_timetable_event/custom_timetable_event.dart';
import 'package:marianum_mobile/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
import 'package:marianum_mobile/widget_data/widget_data.dart';
import 'package:marianum_mobile/widget_data/widget_data_mapper.dart';
CustomTimetableEvent _event({
required String id,
required String title,
required DateTime start,
required DateTime end,
}) => CustomTimetableEvent(
id: id,
title: title,
description: '',
startDate: start,
endDate: end,
color: 'orange',
rrule: '',
createdAt: start,
updatedAt: start,
);
McTimetableEntry _lesson({
required DateTime date,
required int startHhmm,
required int endHhmm,
String status = 'REGULAR',
String? subjectName,
String? room,
String? teacherName,
String? teacherOrgname,
String? substText,
String lessonType = 'LESSON',
List<String> classNames = const [],
}) => McTimetableEntry(
id: 1,
date: date,
startTime: DateTime(1970, 1, 1, startHhmm ~/ 100, startHhmm % 100),
endTime: DateTime(1970, 1, 1, endHhmm ~/ 100, endHhmm % 100),
subjects: subjectName != null ? [subjectName] : const [],
teachers: teacherName != null
? [
McTimetableTeacher(
shortName: teacherName,
displayName: teacherName,
originalShortName: teacherOrgname,
originalDisplayName: teacherOrgname,
),
]
: const [],
rooms: room != null ? [room] : const [],
classNames: classNames,
lessonType: lessonType,
status: status,
substitutionText: substText,
lessonText: null,
infoText: null,
);
void main() {
group('resolveDayAnchor', () {
test('weekday before cutoff stays today', () {
final anchor = WidgetDataMapper.resolveDayAnchor(
DateTime(2026, 5, 6, 10),
);
expect(anchor, DateTime(2026, 5, 6));
});
test('weekday after cutoff jumps to next school day', () {
final anchor = WidgetDataMapper.resolveDayAnchor(
DateTime(2026, 5, 6, 19),
);
expect(anchor, DateTime(2026, 5, 7));
});
test('Friday after cutoff jumps to Monday', () {
final anchor = WidgetDataMapper.resolveDayAnchor(
DateTime(2026, 5, 8, 18),
);
expect(anchor, DateTime(2026, 5, 11));
});
test('Saturday morning jumps to Monday', () {
final anchor = WidgetDataMapper.resolveDayAnchor(
DateTime(2026, 5, 9, 10),
);
expect(anchor, DateTime(2026, 5, 11));
});
test('Sunday evening jumps to Monday', () {
final anchor = WidgetDataMapper.resolveDayAnchor(
DateTime(2026, 5, 10, 22),
);
expect(anchor, DateTime(2026, 5, 11));
});
});
group('resolveWeekAnchor', () {
test('Tuesday returns the Monday of that week', () {
final anchor = WidgetDataMapper.resolveWeekAnchor(
DateTime(2026, 5, 5, 10),
);
expect(anchor, DateTime(2026, 5, 4));
});
test('Sunday returns next Monday', () {
final anchor = WidgetDataMapper.resolveWeekAnchor(
DateTime(2026, 5, 10, 10),
);
expect(anchor, DateTime(2026, 5, 11));
});
});
group('buildDayData', () {
final now = DateTime(2026, 5, 6, 10);
test('only includes lessons on the anchor day', () {
final lessons = [
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 800,
endHhmm: 845,
subjectName: 'MA',
),
_lesson(
date: DateTime(2026, 5, 7),
startHhmm: 800,
endHhmm: 845,
subjectName: 'EN',
),
];
final data = WidgetDataMapper.buildDayData(
now: now,
lessons: lessons,
subjects: null,
rooms: null,
holidays: null,
);
expect(data.lessons, hasLength(1));
expect(data.lessons.first.subjectShort, 'MA');
});
test('classifies cancelled and irregular lessons', () {
final lessons = [
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 800,
endHhmm: 845,
subjectName: 'MA',
status: 'CANCELLED',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'EN',
status: 'IRREGULAR',
teacherName: 'Müller',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 1000,
endHhmm: 1045,
subjectName: 'BIO',
teacherName: 'Müller',
teacherOrgname: 'Schmidt',
),
];
final data = WidgetDataMapper.buildDayData(
now: now,
lessons: lessons,
subjects: null,
rooms: null,
holidays: null,
);
expect(data.lessons.map((l) => l.status).toList(), [
WidgetLessonStatus.cancelled,
WidgetLessonStatus.irregular,
WidgetLessonStatus.teacherChanged,
]);
});
test('marks day as holiday when in holiday range', () {
final holidays = TimetableGetHolidaysResponse(
result: [
McHoliday(
shortName: 'Pfingsten',
longName: 'Pfingstferien',
startDate: DateTime(2026, 5, 6),
endDate: DateTime(2026, 5, 10),
),
],
);
final data = WidgetDataMapper.buildDayData(
now: now,
lessons: const [],
subjects: null,
rooms: null,
holidays: holidays,
);
expect(data.isHoliday, isTrue);
expect(data.holidayName, 'Pfingstferien');
});
test('lessons are sorted by start time', () {
final lessons = [
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 1000,
endHhmm: 1045,
subjectName: 'BIO',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 800,
endHhmm: 845,
subjectName: 'MA',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'EN',
),
];
final data = WidgetDataMapper.buildDayData(
now: now,
lessons: lessons,
subjects: null,
rooms: null,
holidays: null,
);
expect(data.lessons.map((l) => l.subjectShort).toList(), [
'MA',
'EN',
'BIO',
]);
});
});
group('event collision bumping', () {
final now = DateTime(2026, 5, 6, 10);
test('long event bumps every regular lesson it covers', () {
final lessons = [
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 800,
endHhmm: 845,
subjectName: 'MA',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'EN',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 1000,
endHhmm: 1045,
subjectName: 'BIO',
),
];
final events = GetCustomTimetableEventResponse([
_event(
id: 'a',
title: 'Wandertag',
start: DateTime(2026, 5, 6, 8),
end: DateTime(2026, 5, 6, 11),
),
]);
final data = WidgetDataMapper.buildDayData(
now: now,
lessons: lessons,
subjects: null,
rooms: null,
holidays: null,
customEvents: events,
);
expect(data.lessons, hasLength(3));
for (final l in data.lessons) {
expect(l.siblingCount, 1, reason: '${l.subjectShort} should be bumped');
}
});
test('event + same-slot duplicate regular: kept lesson shows +2', () {
final lessons = [
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'EN',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'MA',
),
];
final events = GetCustomTimetableEventResponse([
_event(
id: 'long',
title: 'Wandertag',
start: DateTime(2026, 5, 6, 8),
end: DateTime(2026, 5, 6, 12),
),
]);
final data = WidgetDataMapper.buildDayData(
now: now,
lessons: lessons,
subjects: null,
rooms: null,
holidays: null,
customEvents: events,
);
expect(data.lessons, hasLength(1));
expect(data.lessons.first.siblingCount, 2);
});
test('multi-day event splits into one block per calendar day', () {
final events = GetCustomTimetableEventResponse([
_event(
id: 'multi',
title: 'Klassenfahrt',
start: DateTime(2026, 5, 4, 8),
end: DateTime(2026, 5, 6, 14),
),
]);
final data = WidgetDataMapper.buildWeekData(
now: DateTime(2026, 5, 5, 10),
lessons: const [],
subjects: null,
rooms: null,
holidays: null,
customEvents: events,
);
expect(data.lessons, hasLength(3));
expect(data.lessons[0].start, DateTime(2026, 5, 4, 8));
expect(data.lessons[0].end, DateTime(2026, 5, 4, 23, 59));
expect(data.lessons[1].start, DateTime(2026, 5, 5, 0));
expect(data.lessons[1].end, DateTime(2026, 5, 5, 23, 59));
expect(data.lessons[2].start, DateTime(2026, 5, 6, 0));
expect(data.lessons[2].end, DateTime(2026, 5, 6, 14));
});
test('two events covering the same regular lesson bump it twice', () {
final lessons = [
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'EN',
),
];
final events = GetCustomTimetableEventResponse([
_event(
id: 'long',
title: 'Termin lang',
start: DateTime(2026, 5, 6, 8),
end: DateTime(2026, 5, 6, 12),
),
_event(
id: 'short',
title: 'Termin kurz',
start: DateTime(2026, 5, 6, 9),
end: DateTime(2026, 5, 6, 10),
),
]);
final data = WidgetDataMapper.buildDayData(
now: now,
lessons: lessons,
subjects: null,
rooms: null,
holidays: null,
customEvents: events,
);
expect(data.lessons, hasLength(1));
expect(data.lessons.first.subjectShort, 'EN');
expect(data.lessons.first.siblingCount, 2);
});
});
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('teacher plan', () {
final now = DateTime(2026, 5, 6, 7);
final day = DateTime(2026, 5, 6);
WidgetLesson single(McTimetableEntry lesson) =>
WidgetDataMapper.buildDayData(
now: now,
lessons: [lesson],
subjects: null,
rooms: null,
holidays: null,
showClassInsteadOfTeacher: true,
).lessons.single;
test('shows the classes in place of the teacher', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 755,
endHhmm: 840,
subjectName: 'MA',
teacherName: 'MUE',
teacherOrgname: 'KUN',
classNames: const ['10a', '10b'],
),
);
expect(lesson.teacher, '10a, 10b');
expect(lesson.originalTeacher, isNull);
});
test('leaves the slot empty for entries without a class', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 920,
endHhmm: 935,
room: 'Hof',
teacherName: 'MUE',
lessonType: 'BREAK_SUPERVISION',
),
);
expect(lesson.teacher, isNull);
});
});
group('tile color', () {
final now = DateTime(2026, 5, 6, 7);
final day = DateTime(2026, 5, 6);
WidgetLesson single(
McTimetableEntry lesson, {
TimetableGetSubjectsResponse? subjects,
}) => WidgetDataMapper.buildDayData(
now: now,
lessons: [lesson],
subjects: subjects,
rooms: null,
holidays: null,
).lessons.single;
test('regular lesson without subject color uses the status red', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 755,
endHhmm: 840,
subjectName: 'MA',
teacherName: 'MUE',
),
);
expect(lesson.color, '#993333');
});
test('regular lesson takes the personal subject color', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 755,
endHhmm: 840,
subjectName: 'MA',
teacherName: 'MUE',
),
subjects: TimetableGetSubjectsResponse(
result: [
McSubject(
id: 1,
shortName: 'MA',
longName: 'Mathematik',
color: 'indigo',
),
],
),
);
expect(lesson.color, '#5C6BC0');
});
test('substitution keeps its signal color despite a subject color', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 755,
endHhmm: 840,
status: 'IRREGULAR',
subjectName: 'MA',
teacherName: 'MUE',
),
subjects: TimetableGetSubjectsResponse(
result: [
McSubject(
id: 1,
shortName: 'MA',
longName: 'Mathematik',
color: 'indigo',
),
],
),
);
expect(lesson.status, WidgetLessonStatus.irregular);
expect(lesson.color, '#8F19B3');
});
test('supervision is classified as duty with room in the title', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 920,
endHhmm: 935,
room: 'Hof',
teacherName: 'MUE',
lessonType: 'BREAK_SUPERVISION',
),
);
expect(lesson.status, WidgetLessonStatus.duty);
expect(lesson.subjectShort, 'Hof Aufsicht');
expect(lesson.color, '#00796B');
});
test('custom event resolves legacy color names', () {
final data = WidgetDataMapper.buildDayData(
now: now,
lessons: const [],
subjects: null,
rooms: null,
holidays: null,
customEvents: GetCustomTimetableEventResponse([
_event(
id: 'e',
title: 'Probe',
start: DateTime(2026, 5, 6, 14),
end: DateTime(2026, 5, 6, 15),
),
]),
);
expect(data.lessons.single.color, '#FFA000');
});
});
group('buildWeekData', () {
final now = DateTime(2026, 5, 5, 10); // Tuesday
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,
lessons: lessons,
subjects: null,
rooms: null,
holidays: null,
);
expect(data.anchorDate, DateTime(2026, 5, 4));
expect(data.lessons.map((l) => l.subjectShort).toList(), [
'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');
});
});
}