Files
TPSchedule/mobile/test/services/notification_gateway_test.dart

297 lines
11 KiB
Dart

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';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hepan_everyday/services/local_notification_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'system handle audit only accepts IDs from the requested cache',
() async {
const system = MethodChannel('com.tpschedule.app/reminders');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
messenger.setMockMethodCallHandler(system, (call) async {
expect(call.method, 'findMissingAlarmHandles');
expect((call.arguments as List).toSet(), {11, 22});
return [22, 999];
});
addTearDown(() => messenger.setMockMethodCallHandler(system, null));
expect(await PluginReminderGateway().missingSystemIds({11, 22}), {22});
},
);
test(
'optional handle audit failure does not prevent reconciliation',
() async {
const system = MethodChannel('com.tpschedule.app/reminders');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
var calls = 0;
messenger.setMockMethodCallHandler(system, (_) async {
calls++;
throw PlatformException(code: 'reminder_system_unavailable');
});
addTearDown(() => messenger.setMockMethodCallHandler(system, null));
final gateway = PluginReminderGateway();
expect(await gateway.missingSystemIds({11}), isEmpty);
expect(await gateway.missingSystemIds({}), isEmpty);
expect(calls, 1);
},
);
test('fresh notification initialization creates the course category before checks', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
AndroidFlutterLocalNotificationsPlugin.registerWith();
const channel = MethodChannel('dexterous.com/flutter/local_notifications');
const system = MethodChannel('com.tpschedule.app/reminders');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
final categories = <Map<Object?, Object?>>[];
messenger.setMockMethodCallHandler(channel, (call) async {
switch (call.method) {
case 'initialize':
return true;
case 'createNotificationChannel':
final data = call.arguments as Map;
categories.add({
...Map<Object?, Object?>.from(data),
// Android returns a numeric color even when no LED was set.
'ledColor': data['ledColor'] ?? 0,
});
return null;
case 'getNotificationAppLaunchDetails':
return {'notificationLaunchedApp': false};
case 'areNotificationsEnabled':
case 'canScheduleExactNotifications':
return true;
case 'getNotificationChannels':
return categories;
case 'pendingNotificationRequests':
return [];
}
return null;
});
messenger.setMockMethodCallHandler(
system,
(_) async => <Object?, Object?>{},
);
addTearDown(() {
debugDefaultTargetPlatformOverride = null;
messenger.setMockMethodCallHandler(channel, null);
messenger.setMockMethodCallHandler(system, null);
});
final gateway = PluginReminderGateway();
final diagnosis = await gateway.diagnose();
expect(diagnosis['channelImportance'], Importance.high.value);
expect(diagnosis['appNotificationsEnabled'], isTrue);
expect(diagnosis['channelExists'], isTrue);
expect(categories.single['id'], 'hepan_lessons');
});
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();
const channel = MethodChannel('dexterous.com/flutter/local_notifications');
var attempts = 0;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (call) async {
if (call.method == 'initialize') {
attempts++;
if (attempts == 1) {
throw PlatformException(code: 'temporarily-unavailable');
}
return true;
}
if (call.method == 'getNotificationAppLaunchDetails') {
return {'notificationLaunchedApp': false};
}
return null;
});
addTearDown(() {
debugDefaultTargetPlatformOverride = null;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
});
final gateway = PluginReminderGateway();
await expectLater(gateway.initialize(), throwsA(isA<PlatformException>()));
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);
},
);
}