added guardian login with views for their assigned childs
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/session/session.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/app_modules.dart';
|
||||
import 'package:marianum_mobile/storage/modules_settings.dart';
|
||||
|
||||
@@ -11,9 +12,7 @@ void main() {
|
||||
'default position', () {
|
||||
// Regression: persisted settings from before the ticker module existed
|
||||
// repeatedly made new modules vanish from bar, "Mehr" and settings list.
|
||||
final stale = Modules.values
|
||||
.where((m) => m != Modules.ticker)
|
||||
.toList();
|
||||
final stale = Modules.values.where((m) => m != Modules.ticker).toList();
|
||||
|
||||
final effective = AppModule.effectiveModuleOrder(settingsWith(stale));
|
||||
|
||||
@@ -72,9 +71,7 @@ void main() {
|
||||
// visible modules must not move or drop it (previously the raw persisted
|
||||
// indices were used, moving the wrong module).
|
||||
final effective = AppModule.effectiveModuleOrder(settingsWith([]));
|
||||
final displayed = effective
|
||||
.where((m) => m != Modules.ticker)
|
||||
.toList();
|
||||
final displayed = effective.where((m) => m != Modules.ticker).toList();
|
||||
final tickerSlot = effective.indexOf(Modules.ticker);
|
||||
|
||||
final result = AppModule.reorderModuleOrder(
|
||||
@@ -92,4 +89,22 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('isAvailableFor', () {
|
||||
final student = CredentialSession(username: 'max', password: 'pw');
|
||||
const guardian = GuardianSession(email: 'e@x.de');
|
||||
|
||||
test('password accounts see every module', () {
|
||||
for (final m in Modules.values) {
|
||||
expect(AppModule.isAvailableFor(m, student), isTrue, reason: m.name);
|
||||
}
|
||||
});
|
||||
|
||||
test('guardians lose exactly the Nextcloud modules', () {
|
||||
final hidden = Modules.values
|
||||
.where((m) => !AppModule.isAvailableFor(m, guardian))
|
||||
.toSet();
|
||||
expect(hidden, {Modules.talk, Modules.files});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/marianumconnect/queries/get_capabilities/guardian_child.dart';
|
||||
import 'package:marianum_mobile/background/widget_background_task.dart';
|
||||
import 'package:marianum_mobile/session/session.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/capabilities/bloc/capabilities_state.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/children/child_selection_cubit.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/timetable/policy/timetable_policy.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/timetable/primary/primary_subject_resolver.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/timetable/subject/timetable_subject.dart';
|
||||
import 'package:marianum_mobile/view/pages/absence_report/absence_form_policy.dart';
|
||||
import 'package:marianum_mobile/widget_data/widget_sync.dart';
|
||||
|
||||
const _anna = GuardianChild(id: 'a', firstName: 'Anna', lastName: 'X');
|
||||
const _ben = GuardianChild(id: 'b', firstName: 'Ben', lastName: 'X');
|
||||
const _guardian = GuardianSession(email: 'e@x.de');
|
||||
final _student = CredentialSession(username: 'max', password: 'pw');
|
||||
|
||||
void main() {
|
||||
group('effectiveChild', () {
|
||||
test('keeps a still-linked selection', () {
|
||||
expect(effectiveChild([_anna, _ben], 'b'), _ben);
|
||||
});
|
||||
|
||||
test('falls back to the first child', () {
|
||||
expect(effectiveChild([_anna, _ben], null), _anna);
|
||||
expect(effectiveChild([_anna, _ben], 'gone'), _anna);
|
||||
});
|
||||
|
||||
test('is null without children', () {
|
||||
expect(effectiveChild(const [], 'a'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('resolvePrimarySubject', () {
|
||||
test('password accounts see their own plan', () {
|
||||
expect(
|
||||
resolvePrimarySubject(
|
||||
session: _student,
|
||||
children: const [],
|
||||
selectedChildId: null,
|
||||
),
|
||||
const OwnTimetable(),
|
||||
);
|
||||
});
|
||||
|
||||
test('guardians see the selected child', () {
|
||||
expect(
|
||||
resolvePrimarySubject(
|
||||
session: _guardian,
|
||||
children: const [_anna, _ben],
|
||||
selectedChildId: 'b',
|
||||
),
|
||||
const ChildTimetable('b'),
|
||||
);
|
||||
});
|
||||
|
||||
test('no plan when signed out or without children', () {
|
||||
expect(
|
||||
resolvePrimarySubject(
|
||||
session: null,
|
||||
children: const [],
|
||||
selectedChildId: null,
|
||||
),
|
||||
const NoTimetable(),
|
||||
);
|
||||
expect(
|
||||
resolvePrimarySubject(
|
||||
session: _guardian,
|
||||
children: const [],
|
||||
selectedChildId: null,
|
||||
),
|
||||
const NoTimetable(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('child subject', () {
|
||||
test('siblings get separate persistent slots', () {
|
||||
expect(
|
||||
const ChildTimetable('a').storageId,
|
||||
isNot(const ChildTimetable('b').storageId),
|
||||
);
|
||||
expect(const ChildTimetable('a').persistent, isTrue);
|
||||
expect(const ChildTimetable('a').supportsCustomEvents, isFalse);
|
||||
});
|
||||
|
||||
test('child plans offer no custom events', () {
|
||||
final p = TimetablePolicy.resolve(
|
||||
subject: const ChildTimetable('a'),
|
||||
capabilities: const CapabilitiesState(userType: 'PARENT'),
|
||||
);
|
||||
expect(p.canManageCustomEvents, isFalse);
|
||||
expect(p.canEditSubjectColors, isTrue);
|
||||
expect(p.canOpenForeign, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('AbsenceFormPolicy', () {
|
||||
test('users report for themselves with an editable identity', () {
|
||||
final p = AbsenceFormPolicy.resolve(
|
||||
session: _student,
|
||||
children: const [],
|
||||
selectedChildId: null,
|
||||
)!;
|
||||
expect(p.child, isNull);
|
||||
expect(p.identityEditable, isTrue);
|
||||
});
|
||||
|
||||
test('guardians report for the selected child', () {
|
||||
final p = AbsenceFormPolicy.resolve(
|
||||
session: _guardian,
|
||||
children: const [_anna, _ben],
|
||||
selectedChildId: 'b',
|
||||
)!;
|
||||
expect(p.child, _ben);
|
||||
expect(p.identityEditable, isFalse);
|
||||
});
|
||||
|
||||
test('guardians without children cannot report', () {
|
||||
expect(
|
||||
AbsenceFormPolicy.resolve(
|
||||
session: _guardian,
|
||||
children: const [],
|
||||
selectedChildId: null,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('widget subject', () {
|
||||
test('round-trips primary subjects', () {
|
||||
for (final subject in const [OwnTimetable(), ChildTimetable('a:b')]) {
|
||||
expect(
|
||||
WidgetSync.decodeSubject(WidgetSync.encodeSubject(subject)),
|
||||
subject,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('a missing value is the own plan of older installs', () {
|
||||
expect(WidgetSync.decodeSubject(null), const OwnTimetable());
|
||||
});
|
||||
|
||||
test('foreign and empty plans never back the widget', () {
|
||||
expect(WidgetSync.encodeSubject(const NoTimetable()), isNull);
|
||||
expect(WidgetSync.decodeSubject('child:'), isNull);
|
||||
});
|
||||
|
||||
test('background refresh follows the session', () {
|
||||
expect(
|
||||
widgetRefreshSubject(session: _student, stored: null),
|
||||
const OwnTimetable(),
|
||||
);
|
||||
expect(
|
||||
widgetRefreshSubject(
|
||||
session: _guardian,
|
||||
stored: const ChildTimetable('a'),
|
||||
),
|
||||
const ChildTimetable('a'),
|
||||
);
|
||||
// Before the app published a child, a guardian has nothing to fetch.
|
||||
expect(
|
||||
widgetRefreshSubject(session: _guardian, stored: const OwnTimetable()),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.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_rooms/timetable_get_rooms_response.dart';
|
||||
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
|
||||
import 'package:marianum_mobile/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/capabilities/bloc/capabilities_state.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/timetable/policy/timetable_policy.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/timetable/subject/timetable_subject.dart';
|
||||
|
||||
ElementTimetable _element(TimetableElementType type, [int id = 7]) =>
|
||||
ElementTimetable((type: type, id: id, label: 'X'));
|
||||
|
||||
void main() {
|
||||
group('TimetableSubject', () {
|
||||
test('own plan keeps the legacy storage slot and persists', () {
|
||||
const own = OwnTimetable();
|
||||
expect(own.storageId, '');
|
||||
expect(own.persistent, isTrue);
|
||||
expect(own.supportsCustomEvents, isTrue);
|
||||
});
|
||||
|
||||
test('element plans get distinct slots and do not persist', () {
|
||||
final room = _element(TimetableElementType.room, 1);
|
||||
final teacher = _element(TimetableElementType.teacher, 1);
|
||||
expect(room.storageId, isNot(teacher.storageId));
|
||||
expect(room.storageId, isNot(const OwnTimetable().storageId));
|
||||
expect(room.persistent, isFalse);
|
||||
expect(room.supportsCustomEvents, isFalse);
|
||||
});
|
||||
|
||||
test('element equality ignores the display label', () {
|
||||
final a = ElementTimetable((
|
||||
type: TimetableElementType.room,
|
||||
id: 3,
|
||||
label: 'A',
|
||||
));
|
||||
final b = ElementTimetable((
|
||||
type: TimetableElementType.room,
|
||||
id: 3,
|
||||
label: 'B',
|
||||
));
|
||||
expect(a, b);
|
||||
expect(a.hashCode, b.hashCode);
|
||||
expect(a, isNot(_element(TimetableElementType.schoolClass, 3)));
|
||||
});
|
||||
});
|
||||
|
||||
group('TimetablePolicy', () {
|
||||
const student = CapabilitiesState(userType: 'STUDENT');
|
||||
const teacher = CapabilitiesState(
|
||||
userType: 'TEACHER',
|
||||
viewForeignTimetables: true,
|
||||
);
|
||||
|
||||
test('own plan offers custom events and subject colours', () {
|
||||
final p = TimetablePolicy.resolve(
|
||||
subject: const OwnTimetable(),
|
||||
capabilities: student,
|
||||
);
|
||||
expect(p.canManageCustomEvents, isTrue);
|
||||
expect(p.canEditSubjectColors, isTrue);
|
||||
expect(p.canOpenForeign, isFalse);
|
||||
expect(p.showClassInsteadOfTeacher, isFalse);
|
||||
});
|
||||
|
||||
test('own teacher plan shows classes and may open foreign plans', () {
|
||||
final p = TimetablePolicy.resolve(
|
||||
subject: const OwnTimetable(),
|
||||
capabilities: teacher,
|
||||
);
|
||||
expect(p.showClassInsteadOfTeacher, isTrue);
|
||||
expect(p.canOpenForeign, isTrue);
|
||||
});
|
||||
|
||||
test('foreign plans are read-only', () {
|
||||
final p = TimetablePolicy.resolve(
|
||||
subject: _element(TimetableElementType.room),
|
||||
capabilities: teacher,
|
||||
);
|
||||
expect(p.canManageCustomEvents, isFalse);
|
||||
expect(p.canEditSubjectColors, isFalse);
|
||||
expect(p.canOpenForeign, isTrue);
|
||||
expect(p.showClassInsteadOfTeacher, isFalse);
|
||||
});
|
||||
|
||||
test('class labels follow the viewed element, not the viewer', () {
|
||||
expect(
|
||||
TimetablePolicy.resolve(
|
||||
subject: _element(TimetableElementType.teacher),
|
||||
capabilities: student,
|
||||
).showClassInsteadOfTeacher,
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
TimetablePolicy.resolve(
|
||||
subject: _element(TimetableElementType.student),
|
||||
capabilities: teacher,
|
||||
).showClassInsteadOfTeacher,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('TimetableState.isReady', () {
|
||||
final empty = TimetableState(
|
||||
startDate: DateTime(2026, 9, 14),
|
||||
endDate: DateTime(2026, 9, 18),
|
||||
);
|
||||
|
||||
final withReference = empty.copyWith(
|
||||
rooms: TimetableGetRoomsResponse(result: const []),
|
||||
subjects: TimetableGetSubjectsResponse(result: const []),
|
||||
schoolHolidays: TimetableGetHolidaysResponse(result: const []),
|
||||
);
|
||||
|
||||
test('is false without reference data', () {
|
||||
expect(empty.isReady(needsCustomEvents: false), isFalse);
|
||||
});
|
||||
|
||||
test('only waits for custom events when the subject needs them', () {
|
||||
expect(withReference.isReady(needsCustomEvents: false), isTrue);
|
||||
expect(withReference.isReady(needsCustomEvents: true), isFalse);
|
||||
expect(
|
||||
withReference
|
||||
.copyWith(customEvents: GetCustomTimetableEventResponse(const []))
|
||||
.isReady(needsCustomEvents: true),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user