nexo/packages/messaging_flutter/test/nexo_messaging_test.dart
toki caafd4f320 feat(messaging): Web 알림 API 기준을 추가한다
Flutter Web foreground 알림을 준비하기 위해 초기화 옵션과 token prefix 경계를 public API로 고정한다.

포트/환경 기준 문서와 로드맵 정리, agent-task 리뷰 산출물도 함께 반영한다.
2026-06-07 10:59:12 +09:00

341 lines
10 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',
);
});
});
group('NexoMessagingInitializeOptions', () {
test('has all default values set to true', () {
const options = NexoMessagingInitializeOptions();
expect(options.listenForTokenRefresh, isTrue);
expect(options.requestNotificationPermission, isTrue);
expect(options.registerDeviceToken, isTrue);
});
test('can override all fields to false', () {
const options = NexoMessagingInitializeOptions(
listenForTokenRefresh: false,
requestNotificationPermission: false,
registerDeviceToken: false,
);
expect(options.listenForTokenRefresh, isFalse);
expect(options.requestNotificationPermission, isFalse);
expect(options.registerDeviceToken, isFalse);
});
test('can override individual fields', () {
const options1 = NexoMessagingInitializeOptions(
requestNotificationPermission: false,
);
expect(options1.listenForTokenRefresh, isTrue);
expect(options1.requestNotificationPermission, isFalse);
expect(options1.registerDeviceToken, isTrue);
const options2 = NexoMessagingInitializeOptions(
listenForTokenRefresh: false,
registerDeviceToken: false,
);
expect(options2.listenForTokenRefresh, isFalse);
expect(options2.requestNotificationPermission, isTrue);
expect(options2.registerDeviceToken, isFalse);
});
});
group('NexoMessagingTokenPrefix', () {
test('androidV2 has the expected value', () {
expect(NexoMessagingTokenPrefix.androidV2, 'android_rn-v2');
});
test('appleV2 has the expected value', () {
expect(NexoMessagingTokenPrefix.appleV2, 'apple_rn-v2');
});
test('webV2 has the expected value', () {
expect(NexoMessagingTokenPrefix.webV2, 'web_rn-v2');
});
});
}