improved performance and battery usage on older devices

This commit is contained in:
2026-09-25 23:58:08 +02:00
parent ed8e52f475
commit 56a497985b
70 changed files with 1631 additions and 621 deletions
+21
View File
@@ -0,0 +1,21 @@
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);
});
});
}
@@ -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,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));
});
}
+21 -30
View File
@@ -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));
});
});
}