nexo/packages/messaging_flutter/test/nexo_messaging_test.dart
toki 5e4089c629 feat: notification pipeline implementation
- Update messaging Flutter plugin with Firebase messaging service improvements
- Add notification helper and custom push notification helper enhancements
- Implement notification opened event handling
- Add Android unit tests for payload gate and notification helpers
- Update flutter-test app with integration and widget tests
- Update notification pipeline roadmap and task documentation
2026-05-31 06:34:44 +09:00

290 lines
8.8 KiB
Dart

import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nexo_messaging/nexo_messaging.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('NotificationOpenedEvent.fromMap', () {
test('parses server_url, channel_id, root_id, is_crt_enabled', () {
final event = NotificationOpenedEvent.fromMap({
'server_url': 'https://nexo.example.com',
'channel_id': 'channel-123',
'root_id': 'root-456',
'is_crt_enabled': 'true',
});
expect(event.serverUrl, 'https://nexo.example.com');
expect(event.channelId, 'channel-123');
expect(event.rootId, 'root-456');
expect(event.isCRTEnabled, isTrue);
});
test('defaults isCRTEnabled to false when flag is absent or non-true', () {
final missing = NotificationOpenedEvent.fromMap({
'server_url': 'https://nexo.example.com',
'channel_id': 'channel-123',
});
expect(missing.isCRTEnabled, isFalse);
final falsy = NotificationOpenedEvent.fromMap({
'is_crt_enabled': 'false',
});
expect(falsy.isCRTEnabled, isFalse);
});
test('accepts boolean and numeric-string CRT truthy flags', () {
final boolean = NotificationOpenedEvent.fromMap({'is_crt_enabled': true});
final numericString = NotificationOpenedEvent.fromMap({
'is_crt_enabled': '1',
});
expect(boolean.isCRTEnabled, isTrue);
expect(numericString.isCRTEnabled, isTrue);
});
});
group('NexoMessagingPlugin.handleNativeEvent', () {
late NexoMessagingPlugin plugin;
final actionChannel = MethodChannel(NexoMessagingPlugin.actionChannelName);
setUp(() {
plugin = NexoMessagingPlugin.instance;
plugin.onNavigateToChannel = null;
plugin.onNavigateToThread = null;
plugin.onDeviceTokenReady = null;
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(actionChannel, null);
});
test('opened event emits NotificationOpenedEvent on stream', () async {
final future = plugin.onNotificationOpened.first;
plugin.handleNativeEvent({
'type': PushNotificationType.opened,
'server_url': 'https://nexo.example.com',
'channel_id': 'channel-123',
});
final event = await future;
expect(event.serverUrl, 'https://nexo.example.com');
expect(event.channelId, 'channel-123');
});
test(
'message event with userInteraction routes through opened handler',
() async {
String? navServer;
String? navChannel;
plugin.onNavigateToChannel = (s, c) {
navServer = s;
navChannel = c;
};
plugin.handleNativeEvent({
'type': PushNotificationType.message,
'userInteraction': true,
'server_url': 'https://nexo.example.com',
'channel_id': 'channel-xyz',
});
expect(navServer, 'https://nexo.example.com');
expect(navChannel, 'channel-xyz');
},
);
test(
'message event without userInteraction does not trigger navigation',
() async {
var channelCalled = false;
var threadCalled = false;
plugin.onNavigateToChannel = (_, _) => channelCalled = true;
plugin.onNavigateToThread = (_, _) => threadCalled = true;
final openedEvents = <NotificationOpenedEvent>[];
final sub = plugin.onNotificationOpened.listen(openedEvents.add);
plugin.handleNativeEvent({
'type': PushNotificationType.message,
'server_url': 'https://nexo.example.com',
'channel_id': 'channel-xyz',
});
// Allow any pending microtasks to settle.
await Future<void>.delayed(Duration.zero);
await sub.cancel();
expect(channelCalled, isFalse);
expect(threadCalled, isFalse);
expect(openedEvents, isEmpty);
},
);
test('opened event with CRT and rootId routes to thread callback', () {
String? navServer;
String? navRoot;
plugin.onNavigateToThread = (s, r) {
navServer = s;
navRoot = r;
};
plugin.handleNativeEvent({
'type': PushNotificationType.opened,
'server_url': 'https://nexo.example.com',
'channel_id': 'channel-123',
'root_id': 'root-789',
'is_crt_enabled': 'true',
});
expect(navServer, 'https://nexo.example.com');
expect(navRoot, 'root-789');
});
test(
'opened event with boolean CRT and rootId routes to thread callback',
() {
String? navServer;
String? navRoot;
plugin.onNavigateToThread = (s, r) {
navServer = s;
navRoot = r;
};
plugin.handleNativeEvent({
'type': PushNotificationType.opened,
'server_url': 'https://nexo.example.com',
'channel_id': 'channel-123',
'root_id': 'root-789',
'is_crt_enabled': true,
});
expect(navServer, 'https://nexo.example.com');
expect(navRoot, 'root-789');
},
);
test(
'opened event with CRT but empty rootId falls back to channel callback',
() {
String? navServer;
String? navChannel;
var threadCalled = false;
plugin.onNavigateToChannel = (s, c) {
navServer = s;
navChannel = c;
};
plugin.onNavigateToThread = (_, _) => threadCalled = true;
plugin.handleNativeEvent({
'type': PushNotificationType.opened,
'server_url': 'https://nexo.example.com',
'channel_id': 'channel-123',
'root_id': '',
'is_crt_enabled': true,
});
expect(threadCalled, isFalse);
expect(navServer, 'https://nexo.example.com');
expect(navChannel, 'channel-123');
},
);
test('opened event without server_url skips navigation callbacks', () {
var channelCalled = false;
var threadCalled = false;
plugin.onNavigateToChannel = (_, _) => channelCalled = true;
plugin.onNavigateToThread = (_, _) => threadCalled = true;
plugin.handleNativeEvent({
'type': PushNotificationType.opened,
'channel_id': 'channel-123',
});
expect(channelCalled, isFalse);
expect(threadCalled, isFalse);
});
test('every event is forwarded to onNotification stream', () async {
final future = plugin.onNotification.first;
plugin.handleNativeEvent({
'type': PushNotificationType.clear,
'channel_id': 'channel-123',
});
final raw = await future;
expect(raw['type'], PushNotificationType.clear);
expect(raw['channel_id'], 'channel-123');
});
test(
'token_refresh event saves formatted device token and invokes callback',
() async {
String? channelToken;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(actionChannel, (call) async {
if (call.method == 'saveDeviceToken') {
final args = call.arguments as Map;
channelToken = args['token'] as String?;
}
return null;
});
final completer = Completer<String>();
plugin.onDeviceTokenReady = completer.complete;
plugin.handleNativeEvent({
'type': PushNotificationType.tokenRefresh,
'token': 'raw-token',
});
final callbackToken = await completer.future;
expect(channelToken, 'android_rn-v2:raw-token');
expect(callbackToken, 'android_rn-v2:raw-token');
},
);
test(
'debugSendNativeEventForTesting invokes native debug method',
() async {
MethodCall? capturedCall;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(actionChannel, (call) async {
capturedCall = call;
return null;
});
await plugin.debugSendNativeEventForTesting({
'type': PushNotificationType.opened,
'server_url': 'https://nexo.example.com',
'channel_id': 'channel-debug',
});
expect(capturedCall?.method, 'debugSendNativeEvent');
expect(capturedCall?.arguments, {
'type': PushNotificationType.opened,
'server_url': 'https://nexo.example.com',
'channel_id': 'channel-debug',
});
},
);
});
group('Channel constants', () {
test('notification and action channel names are preserved', () {
expect(
NexoMessagingPlugin.notificationChannelName,
'com.tokilabs.nexo.messaging/notifications',
);
expect(
NexoMessagingPlugin.actionChannelName,
'com.tokilabs.nexo.messaging/notification_actions',
);
});
});
}