Fix release notification resource and add multiple advance reminders
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hepan_everyday/domain/app_snapshot.dart';
|
||||
import 'package:hepan_everyday/domain/app_settings.dart';
|
||||
|
||||
void main() {
|
||||
test('changing input list cannot change saved reminder choices', () {
|
||||
final choices = [5, 15];
|
||||
final settings = AppSettings(reminderOffsets: choices);
|
||||
choices.clear();
|
||||
expect(settings.reminderOffsets, [5, 15]);
|
||||
expect(() => settings.reminderOffsets.add(30), throwsUnsupportedError);
|
||||
});
|
||||
test('legacy single advance setting migrates to one selected time', () {
|
||||
final json = AppSnapshot(settings: AppSettings(reminderMinutes: 30))
|
||||
.toJson();
|
||||
(json['settings'] as Map).remove('reminderOffsets');
|
||||
final migrated = AppSnapshot.fromJson(json).toJson();
|
||||
expect((migrated['settings'] as Map)['reminderOffsets'], [30]);
|
||||
});
|
||||
test('selected times are normalized and survive snapshot round trip', () {
|
||||
final json = AppSnapshot().toJson();
|
||||
(json['settings'] as Map)['reminderOffsets'] = [15, 5, 15];
|
||||
final saved = AppSnapshot.fromJson(AppSnapshot.fromJson(json).toJson())
|
||||
.toJson();
|
||||
expect((saved['settings'] as Map)['reminderOffsets'], [5, 15]);
|
||||
});
|
||||
for (final invalid in [
|
||||
[],
|
||||
[0],
|
||||
[7],
|
||||
['15'],
|
||||
[15, 90],
|
||||
]) {
|
||||
test('invalid selected reminder times $invalid are rejected', () {
|
||||
final json = AppSnapshot().toJson();
|
||||
(json['settings'] as Map)['reminderOffsets'] = invalid;
|
||||
expect(() => AppSnapshot.fromJson(json), throwsFormatException);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ void main() {
|
||||
'reopening restores courses, settings and calendar identifiers',
|
||||
() async {
|
||||
final original = AppSnapshot(
|
||||
settings: const AppSettings(reminderMinutes: 30),
|
||||
settings: AppSettings(reminderMinutes: 30),
|
||||
courses: [course()],
|
||||
calendarEventIds: {'math/2026-09-09': '17'},
|
||||
);
|
||||
@@ -75,12 +75,8 @@ void main() {
|
||||
);
|
||||
test('valid previous snapshot is available for explicit recovery', () async {
|
||||
final store = FileSnapshotStore(directory);
|
||||
await store.save(
|
||||
AppSnapshot(settings: const AppSettings(reminderMinutes: 15)),
|
||||
);
|
||||
await store.save(
|
||||
AppSnapshot(settings: const AppSettings(reminderMinutes: 30)),
|
||||
);
|
||||
await store.save(AppSnapshot(settings: AppSettings(reminderMinutes: 15)));
|
||||
await store.save(AppSnapshot(settings: AppSettings(reminderMinutes: 30)));
|
||||
await File('${directory.path}/hepan_snapshot.json')
|
||||
.writeAsString('damaged');
|
||||
expect((await store.restoreBackup()).settings.reminderMinutes, 15);
|
||||
|
||||
@@ -35,4 +35,57 @@ void main() {
|
||||
await gateway.initialize();
|
||||
expect(attempts, 2);
|
||||
});
|
||||
test(
|
||||
'disabled course notification channel is reported even with app permission',
|
||||
() async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
AndroidFlutterLocalNotificationsPlugin.registerWith();
|
||||
const channel = MethodChannel(
|
||||
'dexterous.com/flutter/local_notifications',
|
||||
);
|
||||
var importance = 0;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
switch (call.method) {
|
||||
case 'initialize':
|
||||
return true;
|
||||
case 'getNotificationAppLaunchDetails':
|
||||
return {'notificationLaunchedApp': false};
|
||||
case 'areNotificationsEnabled':
|
||||
return true;
|
||||
case 'canScheduleExactNotifications':
|
||||
return true;
|
||||
case 'getNotificationChannels':
|
||||
return [
|
||||
{
|
||||
'id': 'hepan_lessons',
|
||||
'name': '课前提醒',
|
||||
'description': '课程开始前的提醒',
|
||||
'importance': importance,
|
||||
'showBadge': true,
|
||||
'playSound': true,
|
||||
'enableVibration': true,
|
||||
'enableLights': false,
|
||||
'bypassDnd': false,
|
||||
'ledColor': 0,
|
||||
'audioAttributesUsage': 5,
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
addTearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
final gateway = PluginReminderGateway();
|
||||
expect(
|
||||
(await gateway.permissions(request: false)).notifications,
|
||||
isFalse,
|
||||
);
|
||||
importance = 4;
|
||||
expect((await gateway.permissions(request: false)).notifications, isTrue);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:hepan_everyday/services/local_notification_service.dart';
|
||||
import 'package:hepan_everyday/services/notification_service.dart';
|
||||
import 'package:hepan_everyday/domain/schedule_engine.dart';
|
||||
import 'package:hepan_everyday/domain/app_settings.dart';
|
||||
import 'package:hepan_everyday/domain/app_snapshot.dart';
|
||||
|
||||
import '../domain/schedule_engine_test.dart' show course;
|
||||
|
||||
@@ -40,6 +41,78 @@ void main() {
|
||||
gateway = AlarmGateway();
|
||||
service = LocalNotificationService(gateway);
|
||||
});
|
||||
AppSettings multiple(List<int> minutes) {
|
||||
final json = AppSnapshot(settings: settings).toJson();
|
||||
(json['settings'] as Map<String, dynamic>)['reminderOffsets'] = minutes;
|
||||
return AppSnapshot.fromJson(json).settings;
|
||||
}
|
||||
|
||||
test(
|
||||
'selected advance times produce independent alarms and survive refresh',
|
||||
() async {
|
||||
final events = ScheduleEngine.occurrences([course(last: 2)], term);
|
||||
await service.reconcile(
|
||||
events,
|
||||
multiple([5, 15]),
|
||||
DateTime.utc(2026, 9, 9),
|
||||
);
|
||||
expect(gateway.alarms, hasLength(2));
|
||||
expect(gateway.alarms.values.map((a) => a.fireAt.toUtc()).toSet(), {
|
||||
DateTime.utc(2026, 9, 9, 0, 15),
|
||||
DateTime.utc(2026, 9, 9, 0, 25),
|
||||
});
|
||||
final ids = gateway.alarms.keys.toSet();
|
||||
await service.reconcile(
|
||||
events,
|
||||
multiple([15, 5]),
|
||||
DateTime.utc(2026, 9, 9),
|
||||
);
|
||||
expect(gateway.alarms.keys.toSet(), ids);
|
||||
await service.reconcile(events, multiple([5]), DateTime.utc(2026, 9, 9));
|
||||
expect(gateway.alarms, hasLength(1));
|
||||
expect(
|
||||
gateway.alarms.values.single.fireAt.toUtc(),
|
||||
DateTime.utc(2026, 9, 9, 0, 25),
|
||||
);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'elapsed earlier reminder does not remove the later selected reminder',
|
||||
() async {
|
||||
final events = ScheduleEngine.occurrences([course()], term);
|
||||
await service.reconcile(
|
||||
events,
|
||||
multiple([5, 15]),
|
||||
DateTime.utc(2026, 9, 9, 0, 20),
|
||||
);
|
||||
expect(gateway.alarms, hasLength(1));
|
||||
expect(
|
||||
gateway.alarms.values.single.fireAt.toUtc(),
|
||||
DateTime.utc(2026, 9, 9, 0, 25),
|
||||
);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'limit selects earliest reminder instants across all selected times',
|
||||
() async {
|
||||
final events = ScheduleEngine.occurrences(
|
||||
List.generate(501, (i) => course(id: 'c-$i')),
|
||||
term,
|
||||
);
|
||||
final result = await service.reconcile(
|
||||
events,
|
||||
multiple([5, 15]),
|
||||
DateTime.utc(2026, 9, 9),
|
||||
);
|
||||
expect(result.scheduledCount, 400);
|
||||
expect(
|
||||
gateway.alarms.values.every(
|
||||
(a) => a.fireAt.isAtSameMomentAs(DateTime.utc(2026, 9, 9, 0, 15)),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'continuous course schedules one alarm fifteen minutes before school start',
|
||||
() async {
|
||||
|
||||
@@ -41,7 +41,7 @@ void main() {
|
||||
events: events,
|
||||
monday: term.firstMonday,
|
||||
today: term.startDate,
|
||||
settings: const AppSettings(),
|
||||
settings: AppSettings(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -108,7 +108,7 @@ void main() {
|
||||
events: events,
|
||||
monday: term.firstMonday,
|
||||
today: term.startDate,
|
||||
settings: const AppSettings(),
|
||||
settings: AppSettings(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -19,6 +19,43 @@ import 'package:hepan_everyday/domain/semester.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(tzdata.initializeTimeZones);
|
||||
testWidgets(
|
||||
'reminder times can be selected together and last choice stays selected',
|
||||
(tester) async {
|
||||
final controller = uiController();
|
||||
await controller.initialize();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: SettingsPage(controller: controller)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('5 分钟'),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.ensureVisible(find.text('5 分钟'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('5 分钟'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
(controller.snapshot.toJson()['settings'] as Map)['reminderOffsets'],
|
||||
[5, 15],
|
||||
);
|
||||
await tester.tap(find.text('15 分钟'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
(controller.snapshot.toJson()['settings'] as Map)['reminderOffsets'],
|
||||
[5],
|
||||
);
|
||||
await tester.tap(find.text('5 分钟'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
(controller.snapshot.toJson()['settings'] as Map)['reminderOffsets'],
|
||||
[5],
|
||||
);
|
||||
controller.dispose();
|
||||
},
|
||||
);
|
||||
testWidgets(
|
||||
'partial initial calendar sync exposes working cleanup action while switch is off',
|
||||
(tester) async {
|
||||
|
||||
@@ -35,7 +35,7 @@ void main() {
|
||||
events: events,
|
||||
monday: term.firstMonday,
|
||||
today: term.startDate,
|
||||
settings: const AppSettings(),
|
||||
settings: AppSettings(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user