Flutter Web foreground 알림을 준비하기 위해 초기화 옵션과 token prefix 경계를 public API로 고정한다. 포트/환경 기준 문서와 로드맵 정리, agent-task 리뷰 산출물도 함께 반영한다.
341 lines
10 KiB
Dart
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');
|
|
});
|
|
});
|
|
}
|