Add feedback service, reminder diagnostics and smooth week transitions
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:timezone/data/latest.dart' as tzdata;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -6,6 +7,129 @@ import 'package:hepan_everyday/services/local_notification_service.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
test(
|
||||
'querying future reminders does not depend on active notification access',
|
||||
() async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
AndroidFlutterLocalNotificationsPlugin.registerWith();
|
||||
const channel = MethodChannel(
|
||||
'dexterous.com/flutter/local_notifications',
|
||||
);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
if (call.method == 'initialize') return true;
|
||||
if (call.method == 'getNotificationAppLaunchDetails') {
|
||||
return {'notificationLaunchedApp': false};
|
||||
}
|
||||
if (call.method == 'pendingNotificationRequests') {
|
||||
return [
|
||||
{
|
||||
'id': 123,
|
||||
'title': '课程',
|
||||
'body': '教室',
|
||||
'payload': 'hepan:course/2026-10-14',
|
||||
},
|
||||
];
|
||||
}
|
||||
if (call.method == 'getActiveNotifications') {
|
||||
throw PlatformException(code: 'active_query_unavailable');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
addTearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
expect(await PluginReminderGateway().pendingIds(), {123});
|
||||
},
|
||||
);
|
||||
test('active lookup failure preserves owned future alarms', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
AndroidFlutterLocalNotificationsPlugin.registerWith();
|
||||
const channel = MethodChannel('dexterous.com/flutter/local_notifications');
|
||||
final messenger =
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
if (call.method == 'initialize') return true;
|
||||
if (call.method == 'getNotificationAppLaunchDetails') {
|
||||
return {'notificationLaunchedApp': false};
|
||||
}
|
||||
if (call.method == 'pendingNotificationRequests') {
|
||||
return [
|
||||
{'id': 123, 'title': '', 'body': '', 'payload': 'hepan:course'},
|
||||
{
|
||||
'id': -1002,
|
||||
'title': '',
|
||||
'body': '',
|
||||
'payload': 'hepan-test:scheduled',
|
||||
},
|
||||
];
|
||||
}
|
||||
if (call.method == 'getActiveNotifications') {
|
||||
throw PlatformException(code: 'unavailable');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
addTearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
messenger.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
expect(await PluginReminderGateway().ownedIds(), {123});
|
||||
});
|
||||
test(
|
||||
'background test uses an exact alarm isolated from course ownership',
|
||||
() async {
|
||||
tzdata.initializeTimeZones();
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
AndroidFlutterLocalNotificationsPlugin.registerWith();
|
||||
const channel = MethodChannel(
|
||||
'dexterous.com/flutter/local_notifications',
|
||||
);
|
||||
final messenger =
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
||||
Map<Object?, Object?>? scheduled;
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
if (call.method == 'initialize') return true;
|
||||
if (call.method == 'getNotificationAppLaunchDetails') {
|
||||
return {'notificationLaunchedApp': false};
|
||||
}
|
||||
if (call.method == 'areNotificationsEnabled' ||
|
||||
call.method == 'canScheduleExactNotifications') {
|
||||
return true;
|
||||
}
|
||||
if (call.method == 'zonedSchedule') {
|
||||
scheduled = call.arguments as Map<Object?, Object?>;
|
||||
return null;
|
||||
}
|
||||
if (call.method == 'pendingNotificationRequests') {
|
||||
return [
|
||||
if (scheduled != null)
|
||||
{
|
||||
'id': -1002,
|
||||
'title': '',
|
||||
'body': '',
|
||||
'payload': 'hepan-test:scheduled',
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
addTearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
messenger.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
final gateway = PluginReminderGateway();
|
||||
expect(await gateway.sendTestReminder(scheduled: true), contains('10 秒'));
|
||||
expect(scheduled?['id'], -1002);
|
||||
expect(scheduled?['payload'], 'hepan-test:scheduled');
|
||||
expect(
|
||||
(scheduled?['platformSpecifics'] as Map?)?['scheduleMode'],
|
||||
'exactAllowWhileIdle',
|
||||
);
|
||||
expect(await gateway.pendingIds(), isEmpty);
|
||||
},
|
||||
);
|
||||
test('transient native initialization failure can be retried', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
AndroidFlutterLocalNotificationsPlugin.registerWith();
|
||||
@@ -66,9 +190,9 @@ void main() {
|
||||
'playSound': true,
|
||||
'enableVibration': true,
|
||||
'enableLights': false,
|
||||
'bypassDnd': false,
|
||||
'ledColor': 0,
|
||||
'audioAttributesUsage': 5,
|
||||
'bypassDnd': false,
|
||||
'ledColor': 0,
|
||||
'audioAttributesUsage': 5,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class AlarmGateway extends ReminderGateway {
|
||||
ReminderPermissions permission = const ReminderPermissions(true, true);
|
||||
final alarms = <int, ReminderRequest>{};
|
||||
int permissionRequests = 0;
|
||||
bool dropWrites = false;
|
||||
@override
|
||||
Future<ReminderPermissions> permissions({required bool request}) async {
|
||||
if (request) permissionRequests++;
|
||||
@@ -22,7 +23,7 @@ class AlarmGateway extends ReminderGateway {
|
||||
Future<Set<int>> pendingIds() async => alarms.keys.toSet();
|
||||
@override
|
||||
Future<void> schedule(ReminderRequest value) async {
|
||||
alarms[value.id] = value;
|
||||
if (!dropWrites) alarms[value.id] = value;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -47,6 +48,23 @@ void main() {
|
||||
return AppSnapshot.fromJson(json).settings;
|
||||
}
|
||||
|
||||
test(
|
||||
'does not report success when native scheduling did not persist',
|
||||
() async {
|
||||
gateway.dropWrites = true;
|
||||
final events = ScheduleEngine.occurrences([course()], term);
|
||||
await expectLater(
|
||||
service.reconcile(events, settings, DateTime.utc(2026, 9, 9)),
|
||||
throwsA(
|
||||
isA<ReminderSetupException>().having(
|
||||
(e) => e.code,
|
||||
'code',
|
||||
'schedule_not_saved',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'selected advance times produce independent alarms and survive refresh',
|
||||
() async {
|
||||
|
||||
Reference in New Issue
Block a user