merged develop
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/cache_store.dart';
|
||||
|
||||
void main() {
|
||||
group('CacheStore.parse', () {
|
||||
test('splits the timestamp header from the payload', () {
|
||||
final entry = CacheStore.parse('1700000000000\n{"a":"b\\nc"}');
|
||||
expect(entry!.lastUpdate, 1700000000000);
|
||||
expect(entry.json, '{"a":"b\\nc"}');
|
||||
});
|
||||
|
||||
test('keeps newlines inside the payload', () {
|
||||
expect(CacheStore.parse('1\nline1\nline2')!.json, 'line1\nline2');
|
||||
});
|
||||
|
||||
test('rejects files without a valid header', () {
|
||||
expect(CacheStore.parse('{"a":1}'), isNull);
|
||||
expect(CacheStore.parse('abc\n{}'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('CacheStore.directoryName', () {
|
||||
test('keeps the original directory for the pre-multi-account account', () {
|
||||
expect(CacheStore.directoryName(''), 'request_cache');
|
||||
});
|
||||
|
||||
test('separates accounts and the signed-out state', () {
|
||||
final names = {
|
||||
CacheStore.directoryName(''),
|
||||
CacheStore.directoryName('a1b2'),
|
||||
CacheStore.directoryName('c3d4'),
|
||||
CacheStore.directoryName(null),
|
||||
};
|
||||
expect(names, hasLength(4));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -86,6 +86,15 @@ void main() {
|
||||
final d = DateTime(2026, 5, 18, 13, 45);
|
||||
expect(d.subtractDays(5), d.addDays(-5));
|
||||
});
|
||||
|
||||
test('dateOnly drops the time of day', () {
|
||||
expect(DateTime(2026, 9, 24, 13, 45).dateOnly, DateTime(2026, 9, 24));
|
||||
});
|
||||
|
||||
test('mondayOfWeek returns Monday midnight for any weekday', () {
|
||||
expect(DateTime(2026, 9, 21).mondayOfWeek, DateTime(2026, 9, 21));
|
||||
expect(DateTime(2026, 9, 27, 23, 59).mondayOfWeek, DateTime(2026, 9, 21));
|
||||
});
|
||||
});
|
||||
|
||||
group('DateTimeFormatting', () {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart';
|
||||
import 'package:marianum_mobile/push/push_registration.dart';
|
||||
|
||||
void main() {
|
||||
@@ -37,4 +38,36 @@ void main() {
|
||||
expect(AuthorizationStatus.values, hasLength(5));
|
||||
});
|
||||
});
|
||||
|
||||
group('PushRegistration.canDisplay', () {
|
||||
test('only authorized and provisional show notifications', () {
|
||||
expect(
|
||||
AuthorizationStatus.values.where(PushRegistration.canDisplay),
|
||||
unorderedEquals([
|
||||
AuthorizationStatus.authorized,
|
||||
AuthorizationStatus.provisional,
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('osPermissionWireValue', () {
|
||||
test('maps every status', () {
|
||||
expect(osPermissionWireValue(AuthorizationStatus.authorized), 'granted');
|
||||
expect(
|
||||
osPermissionWireValue(AuthorizationStatus.provisional),
|
||||
'provisional',
|
||||
);
|
||||
expect(osPermissionWireValue(AuthorizationStatus.denied), 'denied');
|
||||
expect(
|
||||
osPermissionWireValue(AuthorizationStatus.deniedPermanently),
|
||||
'denied',
|
||||
);
|
||||
expect(
|
||||
osPermissionWireValue(AuthorizationStatus.notDetermined),
|
||||
'notDetermined',
|
||||
);
|
||||
expect(osPermissionWireValue(null), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,5 +62,18 @@ void main() {
|
||||
test('empty shares compare equal', () {
|
||||
expect(_share().contentEquals(_share()), isTrue);
|
||||
});
|
||||
|
||||
test('relocated share still equals its raw delivery', () {
|
||||
final raw = _share(filePaths: ['/cache/a.jpg']);
|
||||
final claimed = PendingShare(
|
||||
filePaths: ['/cache/share_intent/1/a.jpg'],
|
||||
text: null,
|
||||
receivedAt: DateTime(2026, 1, 1),
|
||||
sourcePaths: raw.filePaths,
|
||||
);
|
||||
|
||||
expect(claimed.contentEquals(raw), isTrue);
|
||||
expect(raw.contentEquals(claimed), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,4 +114,47 @@ void main() {
|
||||
expect(hidden, {Modules.talk, Modules.files});
|
||||
});
|
||||
});
|
||||
|
||||
group('applyTeacherDefaults', () {
|
||||
test('hides the absence report once for teachers', () {
|
||||
final settings = settingsWith(Modules.values);
|
||||
|
||||
expect(AppModule.applyTeacherDefaults(settings, isTeacher: true), isTrue);
|
||||
expect(settings.hiddenModules, [Modules.absenceReport]);
|
||||
expect(settings.teacherDefaultsApplied, isTrue);
|
||||
});
|
||||
|
||||
test('keeps a module the teacher re-enabled afterwards', () {
|
||||
final settings = settingsWith(Modules.values);
|
||||
AppModule.applyTeacherDefaults(settings, isTeacher: true);
|
||||
settings.hiddenModules.remove(Modules.absenceReport);
|
||||
|
||||
expect(
|
||||
AppModule.applyTeacherDefaults(settings, isTeacher: true),
|
||||
isFalse,
|
||||
);
|
||||
expect(settings.hiddenModules, isEmpty);
|
||||
});
|
||||
|
||||
test('leaves non-teachers untouched', () {
|
||||
final settings = settingsWith(Modules.values);
|
||||
|
||||
expect(
|
||||
AppModule.applyTeacherDefaults(settings, isTeacher: false),
|
||||
isFalse,
|
||||
);
|
||||
expect(settings.hiddenModules, isEmpty);
|
||||
expect(settings.teacherDefaultsApplied, isFalse);
|
||||
});
|
||||
|
||||
test('does not duplicate an already hidden module', () {
|
||||
final settings = ModulesSettings(
|
||||
moduleOrder: Modules.values,
|
||||
hiddenModules: [Modules.absenceReport, Modules.files],
|
||||
);
|
||||
AppModule.applyTeacherDefaults(settings, isTeacher: true);
|
||||
|
||||
expect(settings.hiddenModules, [Modules.absenceReport, Modules.files]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms_response.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
|
||||
TimetableGetRoomsResponse _rooms(String name) => TimetableGetRoomsResponse(
|
||||
result: [McRoom(id: 1, shortName: name, longName: name)],
|
||||
);
|
||||
|
||||
void main() {
|
||||
final base = TimetableState(
|
||||
startDate: DateTime(2026, 9, 21),
|
||||
endDate: DateTime(2026, 9, 25),
|
||||
rooms: _rooms('A101'),
|
||||
dataVersion: 3,
|
||||
);
|
||||
|
||||
test('returns the same state when the content is unchanged', () {
|
||||
expect(base.withReferenceData(rooms: _rooms('A101')), same(base));
|
||||
});
|
||||
|
||||
test('bumps dataVersion and takes the new value when content changed', () {
|
||||
final next = _rooms('B202');
|
||||
final result = base.withReferenceData(rooms: next);
|
||||
expect(result.rooms, same(next));
|
||||
expect(result.dataVersion, 4);
|
||||
});
|
||||
|
||||
test('keeps unchanged instances when only another field changed', () {
|
||||
final result = base.withReferenceData(rooms: _rooms('A101'));
|
||||
expect(result.rooms, same(base.rooms));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import 'package:marianum_mobile/extensions/date_time.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/timetable/bloc/week_cache.dart';
|
||||
|
||||
TimetableGetWeekResponse _week(DateTime monday, {String room = 'A101'}) =>
|
||||
TimetableGetWeekResponse(
|
||||
from: monday,
|
||||
until: monday.addDays(4),
|
||||
entries: [
|
||||
McTimetableEntry(
|
||||
id: 1,
|
||||
date: monday,
|
||||
startTime: DateTime(1970, 1, 1, 8),
|
||||
endTime: DateTime(1970, 1, 1, 9),
|
||||
subjects: const ['ma'],
|
||||
teachers: const [],
|
||||
rooms: [room],
|
||||
classNames: const [],
|
||||
lessonType: 'LESSON',
|
||||
status: 'REGULAR',
|
||||
substitutionText: null,
|
||||
lessonText: null,
|
||||
infoText: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
void main() {
|
||||
final monday = DateTime(2026, 9, 21);
|
||||
|
||||
test('returns null when the stored week has identical content', () {
|
||||
final cache = {monday.weekKey(): _week(monday)};
|
||||
final result = mergeWeekIntoCache(
|
||||
cache,
|
||||
monday,
|
||||
_week(monday),
|
||||
viewedWeekStart: monday,
|
||||
now: monday,
|
||||
);
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('stores changed content', () {
|
||||
final cache = {monday.weekKey(): _week(monday)};
|
||||
final changed = _week(monday, room: 'B202');
|
||||
final result = mergeWeekIntoCache(
|
||||
cache,
|
||||
monday,
|
||||
changed,
|
||||
viewedWeekStart: monday,
|
||||
now: monday,
|
||||
);
|
||||
expect(result![monday.weekKey()], same(changed));
|
||||
});
|
||||
|
||||
test('drops weeks far from both the viewed week and today', () {
|
||||
final far = monday.addDays(7 * (kWeekCacheRadius + 3));
|
||||
final nearToday = monday.addDays(-7 * kWeekCacheRadius);
|
||||
final viewed = monday.addDays(7 * (kWeekCacheRadius * 3));
|
||||
final cache = {
|
||||
far.weekKey(): _week(far),
|
||||
nearToday.weekKey(): _week(nearToday),
|
||||
};
|
||||
final result = mergeWeekIntoCache(
|
||||
cache,
|
||||
viewed,
|
||||
_week(viewed),
|
||||
viewedWeekStart: viewed,
|
||||
now: monday.addDays(2),
|
||||
);
|
||||
expect(result!.keys, {nearToday.weekKey(), viewed.weekKey()});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import 'package:marianum_mobile/api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import 'package:marianum_mobile/api/marianumcloud/talk/room/notification_level.dart';
|
||||
|
||||
typedef Level = GetRoomResponseObjectParticipantNotificationLevel;
|
||||
typedef RoomType = GetRoomResponseObjectConversationType;
|
||||
|
||||
GetRoomResponseObject _room(RoomType type, Level level) =>
|
||||
GetRoomResponseObject(
|
||||
1,
|
||||
't',
|
||||
type,
|
||||
'n',
|
||||
'd',
|
||||
'',
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
's',
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
level,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
GetChatResponseObject(
|
||||
1,
|
||||
't',
|
||||
GetRoomResponseObjectMessageActorType.user,
|
||||
'a',
|
||||
'A',
|
||||
0,
|
||||
'',
|
||||
GetRoomResponseObjectMessageType.comment,
|
||||
true,
|
||||
'r',
|
||||
'm',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
|
||||
void main() {
|
||||
test('Standard wird bei 1:1-Chats zu „Alle Nachrichten"', () {
|
||||
final room = _room(RoomType.oneToOne, Level.defaultLevel);
|
||||
expect(room.effectiveNotificationLevel, Level.alwaysNotify);
|
||||
expect(room.isMuted, isFalse);
|
||||
});
|
||||
|
||||
test('Standard wird bei Gruppen zu „Nur Erwähnungen"', () {
|
||||
expect(
|
||||
_room(RoomType.group, Level.defaultLevel).effectiveNotificationLevel,
|
||||
Level.notifyOnMention,
|
||||
);
|
||||
});
|
||||
|
||||
test('explizit gesetzter Level bleibt erhalten', () {
|
||||
final room = _room(RoomType.oneToOne, Level.neverNotify);
|
||||
expect(room.effectiveNotificationLevel, Level.neverNotify);
|
||||
expect(room.isMuted, isTrue);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/utils/directory_size.dart';
|
||||
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
|
||||
setUp(() => tmp = Directory.systemTemp.createTempSync('dir_size_test'));
|
||||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
|
||||
test('sums files recursively', () async {
|
||||
File('${tmp.path}/a').writeAsBytesSync(List.filled(10, 0));
|
||||
Directory('${tmp.path}/sub').createSync();
|
||||
File('${tmp.path}/sub/b').writeAsBytesSync(List.filled(5, 0));
|
||||
expect(await directorySize(tmp), 15);
|
||||
});
|
||||
|
||||
test('returns 0 for a missing directory', () async {
|
||||
expect(await directorySize(Directory('${tmp.path}/missing')), 0);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/utils/recurrence_occurrences.dart';
|
||||
|
||||
void main() {
|
||||
setUp(RecurrenceOccurrences.clear);
|
||||
|
||||
final anchor = DateTime.utc(2024, 9, 2, 8);
|
||||
|
||||
test('returns only occurrences inside the half-open range', () {
|
||||
final hits = RecurrenceOccurrences.between(
|
||||
'RRULE:FREQ=DAILY',
|
||||
anchor,
|
||||
DateTime.utc(2026, 9, 21),
|
||||
DateTime.utc(2026, 9, 24),
|
||||
);
|
||||
expect(hits, [
|
||||
DateTime.utc(2026, 9, 21, 8),
|
||||
DateTime.utc(2026, 9, 22, 8),
|
||||
DateTime.utc(2026, 9, 23, 8),
|
||||
]);
|
||||
});
|
||||
|
||||
test('answers earlier ranges after a later one was expanded', () {
|
||||
RecurrenceOccurrences.between(
|
||||
'RRULE:FREQ=WEEKLY',
|
||||
anchor,
|
||||
DateTime.utc(2026, 1, 1),
|
||||
DateTime.utc(2026, 1, 31),
|
||||
);
|
||||
final hits = RecurrenceOccurrences.between(
|
||||
'RRULE:FREQ=WEEKLY',
|
||||
anchor,
|
||||
DateTime.utc(2024, 9, 1),
|
||||
DateTime.utc(2024, 9, 10),
|
||||
);
|
||||
expect(hits, [DateTime.utc(2024, 9, 2, 8), DateTime.utc(2024, 9, 9, 8)]);
|
||||
});
|
||||
|
||||
test('stops at the end of a finite series', () {
|
||||
final hits = RecurrenceOccurrences.between(
|
||||
'RRULE:FREQ=DAILY;COUNT=2',
|
||||
anchor,
|
||||
DateTime.utc(2024, 1, 1),
|
||||
DateTime.utc(2030, 1, 1),
|
||||
);
|
||||
expect(hits, hasLength(2));
|
||||
});
|
||||
}
|
||||
@@ -11,13 +11,10 @@ CacheableFile _file({
|
||||
bool isDirectory = false,
|
||||
}) => CacheableFile(path: path, isDirectory: isDirectory, name: name);
|
||||
|
||||
Map<String, dynamic> _doc(ListFilesResponse listing) => {
|
||||
'json': jsonEncode(listing.toJson()),
|
||||
'lastupdate': 0,
|
||||
};
|
||||
String _payload(ListFilesResponse listing) => jsonEncode(listing.toJson());
|
||||
|
||||
void main() {
|
||||
group('searchLocalCaches', () {
|
||||
group('local cache index', () {
|
||||
final root = ListFilesResponse({
|
||||
_file(path: 'Documents/', name: 'Documents', isDirectory: true),
|
||||
_file(path: 'Photos/', name: 'Photos', isDirectory: true),
|
||||
@@ -27,51 +24,45 @@ void main() {
|
||||
_file(path: 'Documents/Tax-Report.pdf', name: 'Tax-Report.pdf'),
|
||||
_file(path: 'Documents/Notes.txt', name: 'Notes.txt'),
|
||||
});
|
||||
final docs = {
|
||||
'/MarianumMobile/wd-folder-aaa': _doc(root),
|
||||
'/MarianumMobile/wd-folder-bbb': _doc(documents),
|
||||
'/MarianumMobile/get-room-ccc': {'json': '{}', 'lastupdate': 0},
|
||||
};
|
||||
final index = buildLocalCacheIndex([
|
||||
_payload(root),
|
||||
_payload(documents),
|
||||
]);
|
||||
|
||||
test('matches by name case-insensitively across all caches', () async {
|
||||
final hits = await searchLocalCaches('report', docs: docs);
|
||||
test('matches by name case-insensitively across all caches', () {
|
||||
final hits = searchLocalCacheIndex(index, 'report');
|
||||
final paths = hits.map((f) => f.path).toSet();
|
||||
expect(paths, {'Reports.pdf', 'Documents/Tax-Report.pdf'});
|
||||
});
|
||||
|
||||
test('returns empty list for empty query', () async {
|
||||
expect(await searchLocalCaches(' ', docs: docs), isEmpty);
|
||||
test('returns empty list for empty query', () {
|
||||
expect(searchLocalCacheIndex(index, ' '), isEmpty);
|
||||
});
|
||||
|
||||
test('respects pathScope prefix', () async {
|
||||
final hits = await searchLocalCaches(
|
||||
test('respects pathScope prefix', () {
|
||||
final hits = searchLocalCacheIndex(
|
||||
index,
|
||||
'report',
|
||||
pathScope: ['Documents'],
|
||||
docs: docs,
|
||||
);
|
||||
expect(hits.map((f) => f.path), ['Documents/Tax-Report.pdf']);
|
||||
});
|
||||
|
||||
test('ignores non-folder cache documents', () async {
|
||||
final hits = await searchLocalCaches('anything', docs: docs);
|
||||
// Only documents starting with `wd-folder-` are scanned. The unrelated
|
||||
// `get-room-ccc` doc must not crash the helper.
|
||||
expect(hits, isEmpty);
|
||||
test('skips payloads that are not folder listings', () {
|
||||
expect(buildLocalCacheIndex(['not json', '{"foo": 1}']), isEmpty);
|
||||
});
|
||||
|
||||
test('deduplicates entries that appear in multiple cached folders',
|
||||
() async {
|
||||
test('deduplicates entries that appear in multiple cached folders', () {
|
||||
final shared = _file(
|
||||
path: 'Documents/Tax-Report.pdf',
|
||||
name: 'Tax-Report.pdf',
|
||||
);
|
||||
final dedupRoot = ListFilesResponse({shared});
|
||||
final dedupDocs = {
|
||||
'/MarianumMobile/wd-folder-aaa': _doc(dedupRoot),
|
||||
'/MarianumMobile/wd-folder-bbb': _doc(dedupRoot),
|
||||
};
|
||||
final hits = await searchLocalCaches('tax', docs: dedupDocs);
|
||||
expect(hits, hasLength(1));
|
||||
final dedupIndex = buildLocalCacheIndex([
|
||||
_payload(dedupRoot),
|
||||
_payload(dedupRoot),
|
||||
]);
|
||||
expect(searchLocalCacheIndex(dedupIndex, 'tax'), hasLength(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import 'package:marianum_mobile/view/pages/timetable/data/lesson_type_label.dart';
|
||||
|
||||
McTimetableEntry _entry({
|
||||
String lessonType = 'LESSON',
|
||||
String? substitutionText,
|
||||
String? lessonText,
|
||||
}) => McTimetableEntry(
|
||||
id: 1,
|
||||
date: DateTime(2026, 5, 4),
|
||||
startTime: DateTime(1970, 1, 1, 8),
|
||||
endTime: DateTime(1970, 1, 1, 15),
|
||||
subjects: const [],
|
||||
teachers: const [],
|
||||
rooms: const ['A101'],
|
||||
classNames: const ['8a'],
|
||||
lessonType: lessonType,
|
||||
status: 'REGULAR',
|
||||
substitutionText: substitutionText,
|
||||
lessonText: lessonText,
|
||||
infoText: null,
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('LessonTypeLabel.forEntry', () {
|
||||
test('prefers the substitution text', () {
|
||||
expect(
|
||||
LessonTypeLabel.forEntry(
|
||||
_entry(substitutionText: 'Klassenfahrt 8a', lessonText: 'Anderes'),
|
||||
),
|
||||
'Klassenfahrt 8a',
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to the lesson text', () {
|
||||
expect(
|
||||
LessonTypeLabel.forEntry(_entry(lessonText: ' Wandertag\n8a ')),
|
||||
'Wandertag 8a',
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to "Event" with room when no text is set', () {
|
||||
expect(
|
||||
LessonTypeLabel.forEntry(_entry(substitutionText: ' ')),
|
||||
'A101 Event',
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps known lesson types over free text', () {
|
||||
expect(
|
||||
LessonTypeLabel.forEntry(
|
||||
_entry(lessonType: 'BREAK_SUPERVISION', lessonText: 'Hof'),
|
||||
),
|
||||
'A101 Aufsicht',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -65,10 +65,10 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to the teacher when the entry has no class', () {
|
||||
test('shows only the room when the entry has no class', () {
|
||||
expect(
|
||||
_location(showClassInsteadOfTeacher: true, classNames: const []),
|
||||
'A101\nMüller',
|
||||
'A101',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -33,6 +34,8 @@ McTimetableEntry _lesson({
|
||||
String? teacherName,
|
||||
String? teacherOrgname,
|
||||
String? substText,
|
||||
String lessonType = 'LESSON',
|
||||
List<String> classNames = const [],
|
||||
}) => McTimetableEntry(
|
||||
id: 1,
|
||||
date: date,
|
||||
@@ -50,8 +53,8 @@ McTimetableEntry _lesson({
|
||||
]
|
||||
: const [],
|
||||
rooms: room != null ? [room] : const [],
|
||||
classNames: const [],
|
||||
lessonType: 'LESSON',
|
||||
classNames: classNames,
|
||||
lessonType: lessonType,
|
||||
status: status,
|
||||
substitutionText: substText,
|
||||
lessonText: null,
|
||||
@@ -117,8 +120,18 @@ void main() {
|
||||
|
||||
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'),
|
||||
_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,
|
||||
@@ -164,14 +177,11 @@ void main() {
|
||||
rooms: null,
|
||||
holidays: null,
|
||||
);
|
||||
expect(
|
||||
data.lessons.map((l) => l.status).toList(),
|
||||
[
|
||||
WidgetLessonStatus.cancelled,
|
||||
WidgetLessonStatus.irregular,
|
||||
WidgetLessonStatus.teacherChanged,
|
||||
],
|
||||
);
|
||||
expect(data.lessons.map((l) => l.status).toList(), [
|
||||
WidgetLessonStatus.cancelled,
|
||||
WidgetLessonStatus.irregular,
|
||||
WidgetLessonStatus.teacherChanged,
|
||||
]);
|
||||
});
|
||||
|
||||
test('marks day as holiday when in holiday range', () {
|
||||
@@ -224,10 +234,11 @@ void main() {
|
||||
rooms: null,
|
||||
holidays: null,
|
||||
);
|
||||
expect(
|
||||
data.lessons.map((l) => l.subjectShort).toList(),
|
||||
['MA', 'EN', 'BIO'],
|
||||
);
|
||||
expect(data.lessons.map((l) => l.subjectShort).toList(), [
|
||||
'MA',
|
||||
'EN',
|
||||
'BIO',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -236,9 +247,24 @@ void main() {
|
||||
|
||||
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'),
|
||||
_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(
|
||||
@@ -264,8 +290,18 @@ void main() {
|
||||
|
||||
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'),
|
||||
_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(
|
||||
@@ -315,7 +351,12 @@ void main() {
|
||||
|
||||
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'),
|
||||
_lesson(
|
||||
date: DateTime(2026, 5, 6),
|
||||
startHhmm: 900,
|
||||
endHhmm: 945,
|
||||
subjectName: 'EN',
|
||||
),
|
||||
];
|
||||
final events = GetCustomTimetableEventResponse([
|
||||
_event(
|
||||
@@ -361,16 +402,198 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
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'),
|
||||
_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,
|
||||
@@ -380,10 +603,12 @@ void main() {
|
||||
holidays: null,
|
||||
);
|
||||
expect(data.anchorDate, DateTime(2026, 5, 4));
|
||||
expect(
|
||||
data.lessons.map((l) => l.subjectShort).toList(),
|
||||
['MO', 'WE', 'FR', 'NEXT'],
|
||||
);
|
||||
expect(data.lessons.map((l) => l.subjectShort).toList(), [
|
||||
'MO',
|
||||
'WE',
|
||||
'FR',
|
||||
'NEXT',
|
||||
]);
|
||||
});
|
||||
|
||||
test('window stays on the current week when the anchor rolls forward', () {
|
||||
@@ -391,8 +616,18 @@ void main() {
|
||||
// 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'),
|
||||
_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,
|
||||
@@ -402,10 +637,7 @@ void main() {
|
||||
holidays: null,
|
||||
);
|
||||
expect(data.anchorDate, DateTime(2026, 5, 11));
|
||||
expect(
|
||||
data.lessons.map((l) => l.subjectShort).toList(),
|
||||
['FR', 'NEXT'],
|
||||
);
|
||||
expect(data.lessons.map((l) => l.subjectShort).toList(), ['FR', 'NEXT']);
|
||||
});
|
||||
|
||||
test('carries per-day holiday info in days', () {
|
||||
@@ -429,8 +661,10 @@ void main() {
|
||||
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();
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user