diff --git a/lib/src/liveactivities.dart b/lib/src/liveactivities.dart index aaf0f65..7021b83 100644 --- a/lib/src/liveactivities.dart +++ b/lib/src/liveactivities.dart @@ -1,5 +1,4 @@ -import 'dart:async'; -import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; class OneSignalLiveActivities { @@ -11,7 +10,7 @@ class OneSignalLiveActivities { /// /// Only applies to iOS. Future enterLiveActivity(String activityId, String token) async { - if (Platform.isIOS) { + if (defaultTargetPlatform == TargetPlatform.iOS) { return await _channel.invokeMethod("OneSignal#enterLiveActivity", {'activityId': activityId, 'token': token}); } @@ -21,7 +20,7 @@ class OneSignalLiveActivities { /// /// Only applies to iOS. Future exitLiveActivity(String activityId) async { - if (Platform.isIOS) { + if (defaultTargetPlatform == TargetPlatform.iOS) { return await _channel.invokeMethod( "OneSignal#exitLiveActivity", {'activityId': activityId}); } @@ -39,7 +38,7 @@ class OneSignalLiveActivities { /// /// Only applies to iOS. Future setupDefault({LiveActivitySetupOptions? options}) async { - if (Platform.isIOS) { + if (defaultTargetPlatform == TargetPlatform.iOS) { dynamic optionsMap; if (options != null) { @@ -62,7 +61,7 @@ class OneSignalLiveActivities { /// Only applies to iOS. Future startDefault( String activityId, dynamic attributes, dynamic content) async { - if (Platform.isIOS) { + if (defaultTargetPlatform == TargetPlatform.iOS) { return await _channel.invokeMethod("OneSignal#startDefault", { 'activityId': activityId, 'attributes': attributes, @@ -78,7 +77,7 @@ class OneSignalLiveActivities { /// /// Only applies to iOS. Future setPushToStartToken(String activityType, String token) async { - if (Platform.isIOS) { + if (defaultTargetPlatform == TargetPlatform.iOS) { return await _channel.invokeMethod("OneSignal#setPushToStartToken", {'activityType': activityType, 'token': token}); } @@ -90,7 +89,7 @@ class OneSignalLiveActivities { /// /// Only applies to iOS. Future removePushToStartToken(String activityType) async { - if (Platform.isIOS) { + if (defaultTargetPlatform == TargetPlatform.iOS) { return await _channel.invokeMethod( "OneSignal#removePushToStartToken", {'activityType': activityType}); } @@ -99,22 +98,12 @@ class OneSignalLiveActivities { /// The setup options for [OneSignal.LiveActivities.setupDefault]. class LiveActivitySetupOptions { - bool _enablePushToStart = true; - bool _enablePushToUpdate = true; - - LiveActivitySetupOptions( - {bool enablePushToStart = true, bool enablePushToUpdate = true}) { - this._enablePushToStart = enablePushToStart; - this._enablePushToUpdate = enablePushToUpdate; - } - /// When true, OneSignal will listen for pushToStart tokens. - bool get enablePushToStart { - return this._enablePushToStart; - } + final bool enablePushToStart; /// When true, OneSignal will listen for pushToUpdate tokens for each started live activity. - bool get enablePushToUpdate { - return this._enablePushToUpdate; - } + final bool enablePushToUpdate; + + LiveActivitySetupOptions( + {this.enablePushToStart = true, this.enablePushToUpdate = true}); } diff --git a/lib/src/notification.dart b/lib/src/notification.dart index 5f6837e..9f403f4 100644 --- a/lib/src/notification.dart +++ b/lib/src/notification.dart @@ -1,7 +1,8 @@ -import 'package:onesignal_flutter/src/utils.dart'; -import 'package:onesignal_flutter/onesignal_flutter.dart'; import 'dart:convert'; +import 'package:onesignal_flutter/onesignal_flutter.dart'; +import 'package:onesignal_flutter/src/utils.dart'; + /// A class representing the notification, including the /// payload of the notification as well as additional /// parameters (such as whether the notification was `shown` diff --git a/test/inappmessages_test.dart b/test/inappmessages_test.dart index 9921fcf..52844cf 100644 --- a/test/inappmessages_test.dart +++ b/test/inappmessages_test.dart @@ -3,6 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:onesignal_flutter/src/inappmessage.dart'; import 'package:onesignal_flutter/src/inappmessages.dart'; +import 'mock_channel.dart'; + const validMessageJson = { 'message_id': 'test-message-id-123', }; @@ -20,47 +22,28 @@ void main() { group('OneSignalInAppMessages', () { late OneSignalInAppMessages inAppMessages; - late List methodCalls; + late OneSignalMockChannelController channelController; setUp(() { - methodCalls = []; + channelController = OneSignalMockChannelController(); + channelController.resetState(); inAppMessages = OneSignalInAppMessages(); - - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel('OneSignal#inappmessages'), - (call) async { - methodCalls.add(call); - return null; - }, - ); - }); - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel('OneSignal#inappmessages'), - null, - ); }); group('addTrigger', () { test('invokes OneSignal#addTrigger method with key-value pair', () async { await inAppMessages.addTrigger(triggerName, 'true'); - expect(methodCalls.length, 1); - expect(methodCalls[0].method, 'OneSignal#addTrigger'); - expect(methodCalls[0].arguments, {triggerName: 'true'}); + expect(channelController.state.triggers, {triggerName: 'true'}); }); test('handles multiple triggers sequentially', () async { const triggerName2 = 'trigger2'; await inAppMessages.addTrigger(triggerName, 'value1'); - await inAppMessages.addTrigger(triggerName2, 'value2'); + expect(channelController.state.triggers, {triggerName: 'value1'}); - expect(methodCalls.length, 2); - expect(methodCalls[0].arguments, {triggerName: 'value1'}); - expect(methodCalls[1].arguments, {triggerName2: 'value2'}); + await inAppMessages.addTrigger(triggerName2, 'value2'); + expect(channelController.state.triggers, {triggerName2: 'value2'}); }); }); @@ -74,16 +57,13 @@ void main() { await inAppMessages.addTriggers(triggers); - expect(methodCalls.length, 1); - expect(methodCalls[0].method, 'OneSignal#addTriggers'); - expect(methodCalls[0].arguments, triggers); + expect(channelController.state.triggers, triggers); }); test('handles empty triggers map', () async { await inAppMessages.addTriggers({}); - expect(methodCalls.length, 1); - expect(methodCalls[0].arguments, {}); + expect(channelController.state.triggers, {}); }); }); @@ -91,9 +71,7 @@ void main() { test('invokes OneSignal#removeTrigger method with key', () async { await inAppMessages.removeTrigger(triggerName); - expect(methodCalls.length, 1); - expect(methodCalls[0].method, 'OneSignal#removeTrigger'); - expect(methodCalls[0].arguments, triggerName); + expect(channelController.state.removedTrigger, triggerName); }); }); @@ -104,16 +82,13 @@ void main() { await inAppMessages.removeTriggers(keys); - expect(methodCalls.length, 1); - expect(methodCalls[0].method, 'OneSignal#removeTriggers'); - expect(methodCalls[0].arguments, keys); + expect(channelController.state.removedTriggers, keys); }); test('handles empty keys list', () async { await inAppMessages.removeTriggers([]); - expect(methodCalls.length, 1); - expect(methodCalls[0].arguments, []); + expect(channelController.state.removedTriggers, []); }); }); @@ -121,8 +96,7 @@ void main() { test('invokes OneSignal#clearTriggers method', () async { await inAppMessages.clearTriggers(); - expect(methodCalls.length, 1); - expect(methodCalls[0].method, 'OneSignal#clearTriggers'); + expect(channelController.state.clearedTriggers, true); }); }); @@ -130,43 +104,35 @@ void main() { test('invokes OneSignal#paused', () async { await inAppMessages.paused(true); - expect(methodCalls.length, 1); - expect(methodCalls[0].method, 'OneSignal#paused'); - expect(methodCalls[0].arguments, true); + expect(channelController.state.inAppMessagesPaused, true); await inAppMessages.paused(false); - expect(methodCalls.length, 2); - expect(methodCalls[1].method, 'OneSignal#paused'); - expect(methodCalls[1].arguments, false); + expect(channelController.state.inAppMessagesPaused, false); }); }); group('arePaused', () { - test('invokes OneSignal#arePaused method', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel('OneSignal#inappmessages'), - (call) async { - if (call.method == 'OneSignal#arePaused') { - return true; - } - return null; - }, - ); - + test('invokes OneSignal#arePaused method and returns correct value', + () async { + await inAppMessages.paused(true); final result = await inAppMessages.arePaused(); expect(result, true); }); + + test('returns false when not paused', () async { + final result = await inAppMessages.arePaused(); + + expect(result, false); + }); }); group('lifecycleInit', () { test('invokes OneSignal#lifecycleInit method', () async { await inAppMessages.lifecycleInit(); - expect(methodCalls.length, 1); - expect(methodCalls[0].method, 'OneSignal#lifecycleInit'); + expect(channelController.state.lifecycleInitCalled, true); }); }); @@ -180,7 +146,6 @@ void main() { inAppMessages.addClickListener(listener); - // Simulate native call to verify listener was added await inAppMessages.handleMethod( MethodCall( 'OneSignal#onClickInAppMessage', @@ -204,7 +169,6 @@ void main() { inAppMessages.addClickListener(listener); inAppMessages.removeClickListener(listener); - // Simulate native call to verify listener was removed await inAppMessages.handleMethod( MethodCall( 'OneSignal#onClickInAppMessage', @@ -231,7 +195,6 @@ void main() { inAppMessages.addWillDisplayListener(listener); - // Simulate native call to verify listener was added await inAppMessages.handleMethod( MethodCall( 'OneSignal#onWillDisplayInAppMessage', @@ -253,7 +216,6 @@ void main() { inAppMessages.addWillDisplayListener(listener); inAppMessages.removeWillDisplayListener(listener); - // Simulate native call to verify listener was removed await inAppMessages.handleMethod( MethodCall( 'OneSignal#onWillDisplayInAppMessage', @@ -275,7 +237,6 @@ void main() { inAppMessages.addDidDisplayListener(listener); - // Simulate native call to verify listener was added await inAppMessages.handleMethod( MethodCall( 'OneSignal#onDidDisplayInAppMessage', @@ -296,7 +257,6 @@ void main() { inAppMessages.addDidDisplayListener(listener); inAppMessages.removeDidDisplayListener(listener); - // Simulate native call to verify listener was removed await inAppMessages.handleMethod( MethodCall( 'OneSignal#onDidDisplayInAppMessage', @@ -339,7 +299,6 @@ void main() { inAppMessages.addWillDismissListener(listener); - // Simulate native call to verify listener was added await inAppMessages.handleMethod( MethodCall( 'OneSignal#onWillDismissInAppMessage', @@ -361,7 +320,6 @@ void main() { inAppMessages.addWillDismissListener(listener); inAppMessages.removeWillDismissListener(listener); - // Simulate native call to verify listener was removed await inAppMessages.handleMethod( MethodCall( 'OneSignal#onWillDismissInAppMessage', @@ -385,7 +343,6 @@ void main() { inAppMessages.addDidDismissListener(listener); - // Simulate native call to verify listener was added await inAppMessages.handleMethod( MethodCall( 'OneSignal#onDidDismissInAppMessage', @@ -407,7 +364,6 @@ void main() { inAppMessages.addDidDismissListener(listener); inAppMessages.removeDidDismissListener(listener); - // Simulate native call to verify listener was removed await inAppMessages.handleMethod( MethodCall( 'OneSignal#onDidDismissInAppMessage', diff --git a/test/liveactivities_test.dart b/test/liveactivities_test.dart new file mode 100644 index 0000000..e2cfce1 --- /dev/null +++ b/test/liveactivities_test.dart @@ -0,0 +1,157 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:onesignal_flutter/src/liveactivities.dart'; + +import 'mock_channel.dart'; + +const activityId = 'test-activity-id'; +const token = 'test-token'; +const activityType = 'TestActivityType'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('OneSignalLiveActivities', () { + late OneSignalLiveActivities liveActivities; + late OneSignalMockChannelController channelController; + + setUp(() { + channelController = OneSignalMockChannelController(); + channelController.resetState(); + liveActivities = OneSignalLiveActivities(); + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + }); + + tearDown(() { + debugDefaultTargetPlatformOverride = null; + }); + + group('enterLiveActivity', () { + test('invokes OneSignal#enterLiveActivity with activityId and token', + () async { + await liveActivities.enterLiveActivity(activityId, token); + + expect(channelController.state.liveActivityEntered, true); + expect(channelController.state.liveActivityId, activityId); + expect(channelController.state.liveActivityToken, token); + }); + }); + + group('exitLiveActivity', () { + test('invokes OneSignal#exitLiveActivity with activityId', () async { + await liveActivities.exitLiveActivity(activityId); + + expect(channelController.state.liveActivityExited, true); + expect(channelController.state.liveActivityId, activityId); + }); + }); + + group('setupDefault', () { + test('invokes OneSignal#setupDefault without options', () async { + await liveActivities.setupDefault(); + + expect(channelController.state.liveActivitySetupCalled, true); + expect(channelController.state.liveActivitySetupOptions, isNull); + }); + + test('invokes OneSignal#setupDefault with custom options', () async { + final options = LiveActivitySetupOptions( + enablePushToStart: false, + enablePushToUpdate: false, + ); + + await liveActivities.setupDefault(options: options); + + expect(channelController.state.liveActivitySetupCalled, true); + expect(channelController.state.liveActivitySetupOptions, { + 'enablePushToStart': false, + 'enablePushToUpdate': false, + }); + }); + + test('setupDefault with default option values', () async { + final options = LiveActivitySetupOptions(); + + await liveActivities.setupDefault(options: options); + + expect(channelController.state.liveActivitySetupOptions, { + 'enablePushToStart': true, + 'enablePushToUpdate': true, + }); + }); + }); + + group('startDefault', () { + test('invokes OneSignal#startDefault with required parameters', () async { + final attributes = {'name': 'John', 'score': 100}; + final content = {'message': 'Hello'}; + + await liveActivities.startDefault(activityId, attributes, content); + + expect(channelController.state.liveActivityStarted, true); + expect(channelController.state.liveActivityId, activityId); + expect(channelController.state.liveActivityAttributes, attributes); + expect(channelController.state.liveActivityContent, content); + }); + + test('handles complex nested attributes and content', () async { + final complexAttributes = { + 'nested': {'key': 'value'}, + 'array': [1, 2, 3], + }; + final complexContent = { + 'state': 'active', + 'data': {'status': 'running'}, + }; + + await liveActivities.startDefault( + activityId, complexAttributes, complexContent); + + expect(channelController.state.liveActivityAttributes['nested']['key'], + 'value'); + expect(channelController.state.liveActivityContent['data']['status'], + 'running'); + }); + }); + + group('setPushToStartToken', () { + test('invokes OneSignal#setPushToStartToken with activityType and token', + () async { + await liveActivities.setPushToStartToken(activityType, token); + + expect(channelController.state.liveActivityPushToStartSet, true); + expect(channelController.state.liveActivityType, activityType); + expect(channelController.state.liveActivityPushToken, token); + }); + }); + + group('removePushToStartToken', () { + test('invokes OneSignal#removePushToStartToken with activityType', + () async { + await liveActivities.removePushToStartToken(activityType); + + expect(channelController.state.liveActivityPushToStartRemoved, true); + expect(channelController.state.liveActivityType, activityType); + }); + }); + }); + + group('LiveActivitySetupOptions', () { + test('creates with default values', () { + final options = LiveActivitySetupOptions(); + + expect(options.enablePushToStart, true); + expect(options.enablePushToUpdate, true); + }); + + test('creates with both custom values', () { + final options = LiveActivitySetupOptions( + enablePushToStart: false, + enablePushToUpdate: false, + ); + + expect(options.enablePushToStart, false); + expect(options.enablePushToUpdate, false); + }); + }); +} diff --git a/test/location_test.dart b/test/location_test.dart new file mode 100644 index 0000000..d48bf25 --- /dev/null +++ b/test/location_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:onesignal_flutter/src/location.dart'; + +import 'mock_channel.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('OneSignalLocation', () { + late OneSignalLocation location; + late OneSignalMockChannelController channelController; + + setUp(() { + channelController = OneSignalMockChannelController(); + channelController.resetState(); + location = OneSignalLocation(); + }); + + group('requestPermission', () { + test('invokes OneSignal#requestPermission method', () async { + await location.requestPermission(); + + expect(channelController.state.locationPermissionRequested, true); + }); + }); + + group('setShared', () { + test('invokes OneSignal#setShared with true', () async { + await location.setShared(true); + + expect(channelController.state.locationShared, true); + }); + + test('handles multiple setShared calls', () async { + await location.setShared(true); + expect(channelController.state.locationShared, true); + + await location.setShared(false); + expect(channelController.state.locationShared, false); + + await location.setShared(true); + expect(channelController.state.locationShared, true); + }); + }); + + group('isShared', () { + test('returns false when location is not shared', () async { + final result = await location.isShared(); + + expect(result, false); + }); + + test('returns correct value after toggling', () async { + await location.setShared(true); + var result = await location.isShared(); + expect(result, true); + + await location.setShared(false); + result = await location.isShared(); + expect(result, false); + + await location.setShared(true); + result = await location.isShared(); + expect(result, true); + }); + }); + }); +} diff --git a/test/mock_channel.dart b/test/mock_channel.dart index 9691fc4..b1be756 100644 --- a/test/mock_channel.dart +++ b/test/mock_channel.dart @@ -12,6 +12,14 @@ class OneSignalMockChannelController { final MethodChannel _channel = const MethodChannel('OneSignal'); final MethodChannel _debugChannel = const MethodChannel('OneSignal#debug'); final MethodChannel _tagsChannel = const MethodChannel('OneSignal#tags'); + final MethodChannel _locationChannel = + const MethodChannel('OneSignal#location'); + final MethodChannel _inAppMessagesChannel = + const MethodChannel('OneSignal#inappmessages'); + final MethodChannel _liveActivitiesChannel = + const MethodChannel('OneSignal#liveactivities'); + final MethodChannel _notificationsChannel = + const MethodChannel('OneSignal#notifications'); late OneSignalState state; @@ -22,6 +30,14 @@ class OneSignalMockChannelController { .setMockMethodCallHandler(_tagsChannel, _handleMethod); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(_debugChannel, _handleMethod); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(_locationChannel, _handleMethod); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(_inAppMessagesChannel, _handleMethod); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(_liveActivitiesChannel, _handleMethod); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(_notificationsChannel, _handleMethod); } void resetState() { @@ -78,6 +94,85 @@ class OneSignalMockChannelController { state.language = (call.arguments as Map)['language'] as String?; return {"success": true}; + case "OneSignal#requestPermission": + state.locationPermissionRequested = true; + break; + case "OneSignal#setShared": + state.locationShared = call.arguments as bool?; + break; + case "OneSignal#isShared": + return state.locationShared ?? false; + case "OneSignal#enterLiveActivity": + state.liveActivityEntered = true; + state.liveActivityId = + (call.arguments as Map)['activityId'] as String?; + state.liveActivityToken = + (call.arguments as Map)['token'] as String?; + break; + case "OneSignal#exitLiveActivity": + state.liveActivityExited = true; + state.liveActivityId = + (call.arguments as Map)['activityId'] as String?; + break; + case "OneSignal#setupDefault": + state.liveActivitySetupCalled = true; + state.liveActivitySetupOptions = (call.arguments + as Map)['options'] as Map?; + break; + case "OneSignal#startDefault": + state.liveActivityStarted = true; + state.liveActivityId = + (call.arguments as Map)['activityId'] as String?; + state.liveActivityAttributes = + (call.arguments as Map)['attributes']; + state.liveActivityContent = + (call.arguments as Map)['content']; + break; + case "OneSignal#setPushToStartToken": + state.liveActivityPushToStartSet = true; + state.liveActivityType = (call.arguments + as Map)['activityType'] as String?; + state.liveActivityPushToken = + (call.arguments as Map)['token'] as String?; + break; + case "OneSignal#removePushToStartToken": + state.liveActivityPushToStartRemoved = true; + state.liveActivityType = (call.arguments + as Map)['activityType'] as String?; + break; + case "OneSignal#addTrigger": + state.addTrigger(call.arguments as Map); + break; + case "OneSignal#addTriggers": + state.triggers = call.arguments as Map?; + return {"success": true}; + case "OneSignal#removeTrigger": + state.removedTrigger = call.arguments as String?; + break; + case "OneSignal#removeTriggers": + state.removedTriggers = call.arguments as List?; + return {"success": true}; + case "OneSignal#clearTriggers": + state.clearedTriggers = true; + break; + case "OneSignal#paused": + state.inAppMessagesPaused = call.arguments as bool?; + break; + case "OneSignal#arePaused": + return state.inAppMessagesPaused ?? false; + case "OneSignal#lifecycleInit": + state.lifecycleInitCalled = true; + break; + case "OneSignal#displayNotification": + // This is called on OneSignal#notifications channel + state.displayedNotificationId = (call.arguments + as Map)['notificationId'] as String?; + break; + case "OneSignal#preventDefault": + // This is called on OneSignal#notifications channel + state.preventedNotificationId = (call.arguments + as Map)['notificationId'] as String?; + break; } } } @@ -101,17 +196,43 @@ class OneSignalState { bool? consentGiven = false; bool? calledPromptPermission; bool? locationShared; + bool? locationPermissionRequested; OSNotificationDisplayType? inFocusDisplayType; bool? disablePush; String? externalId; String? language; + // live activities + bool? liveActivityEntered; + bool? liveActivityExited; + bool? liveActivityStarted; + bool? liveActivitySetupCalled; + bool? liveActivityPushToStartSet; + bool? liveActivityPushToStartRemoved; + String? liveActivityId; + String? liveActivityToken; + String? liveActivityType; + String? liveActivityPushToken; + dynamic liveActivityAttributes; + dynamic liveActivityContent; + Map? liveActivitySetupOptions; + + // in app messages + bool? inAppMessagesPaused; + bool? lifecycleInitCalled; + Map? triggers; + String? removedTrigger; + List? removedTriggers; + bool? clearedTriggers; + // tags Map? tags; List? deleteTags; // notifications Map? postNotificationJson; + String? displayedNotificationId; + String? preventedNotificationId; /* All of the following functions parse the MethodCall @@ -157,4 +278,8 @@ class OneSignalState { email = params['email'] as String?; emailAuthHashToken = params['emailAuthHashToken'] as String?; } + + void addTrigger(Map params) { + triggers = params; + } } diff --git a/test/notification_test.dart b/test/notification_test.dart new file mode 100644 index 0000000..2e41a6e --- /dev/null +++ b/test/notification_test.dart @@ -0,0 +1,584 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:onesignal_flutter/onesignal_flutter.dart'; +import 'package:onesignal_flutter/src/notification.dart'; + +import 'mock_channel.dart'; + +const validNotificationJson = { + 'notificationId': 'notification-123', + 'title': 'Test Title', + 'body': 'Test Body', + 'sound': 'default', + 'launchUrl': 'https://example.com', + 'templateId': 'template-456', + 'templateName': 'Template Name', + 'rawPayload': '{"key":"value"}', + 'additionalData': {'custom_key': 'custom_value'}, + 'buttons': [ + {'id': 'btn1', 'text': 'Button 1'}, + {'id': 'btn2', 'text': 'Button 2', 'icon': 'icon.png'}, + ], +}; + +const iosOnlyJson = { + 'notificationId': 'ios-notification-123', + 'contentAvailable': true, + 'mutableContent': true, + 'category': 'CUSTOM_CATEGORY', + 'badge': 5, + 'badgeIncrement': 1, + 'subtitle': 'iOS Subtitle', + 'relevanceScore': 0.75, + 'interruptionLevel': 'timeSensitive', + 'attachments': { + 'image': 'https://example.com/image.png', + 'video': 'https://example.com/video.mp4', + }, +}; + +const androidOnlyJson = { + 'notificationId': 'android-notification-123', + 'smallIcon': 'icon_small', + 'largeIcon': 'icon_large', + 'bigPicture': 'https://example.com/big_picture.png', + 'smallIconAccentColor': '#FF0000FF', + 'ledColor': '#FFFF0000', + 'lockScreenVisibility': 1, + 'groupKey': 'group_key_1', + 'groupMessage': 'You have 2 messages', + 'fromProjectNumber': '123456789', + 'collapseId': 'collapse_1', + 'priority': 10, + 'androidNotificationId': 1, + 'backgroundImageLayout': { + 'image': 'https://example.com/bg.png', + 'titleTextColor': '#FF000000', + 'bodyTextColor': '#FF666666', + }, +}; + +const clickResultJson = { + 'action_id': 'action-123', + 'url': 'https://example.com/action', +}; + +void main() { + group('OSNotification', () { + test('creates from valid JSON with all shared parameters', () { + final notification = OSNotification(validNotificationJson); + + expect(notification.notificationId, 'notification-123'); + expect(notification.title, 'Test Title'); + expect(notification.body, 'Test Body'); + expect(notification.sound, 'default'); + expect(notification.launchUrl, 'https://example.com'); + expect(notification.templateId, 'template-456'); + expect(notification.templateName, 'Template Name'); + }); + + test('creates from JSON with null optional fields', () { + final json = {'notificationId': 'simple-notification'}; + final notification = OSNotification(json); + + expect(notification.notificationId, 'simple-notification'); + expect(notification.title, isNull); + expect(notification.body, isNull); + expect(notification.sound, isNull); + expect(notification.launchUrl, isNull); + }); + + test('parses additionalData correctly', () { + final notification = OSNotification(validNotificationJson); + + expect(notification.additionalData, isNotNull); + expect(notification.additionalData!['custom_key'], 'custom_value'); + }); + + test('parses buttons correctly', () { + final notification = OSNotification(validNotificationJson); + + expect(notification.buttons, isNotNull); + expect(notification.buttons!.length, 2); + expect(notification.buttons![0].id, 'btn1'); + expect(notification.buttons![0].text, 'Button 1'); + expect(notification.buttons![1].id, 'btn2'); + expect(notification.buttons![1].text, 'Button 2'); + expect(notification.buttons![1].icon, 'icon.png'); + }); + + test('parses iOS-specific parameters', () { + final notification = OSNotification(iosOnlyJson); + + expect(notification.contentAvailable, true); + expect(notification.mutableContent, true); + expect(notification.category, 'CUSTOM_CATEGORY'); + expect(notification.badge, 5); + expect(notification.badgeIncrement, 1); + expect(notification.subtitle, 'iOS Subtitle'); + expect(notification.relevanceScore, 0.75); + expect(notification.interruptionLevel, 'timeSensitive'); + expect(notification.attachments, isNotNull); + }); + + test('parses Android-specific parameters', () { + final notification = OSNotification(androidOnlyJson); + + expect(notification.smallIcon, 'icon_small'); + expect(notification.largeIcon, 'icon_large'); + expect(notification.bigPicture, 'https://example.com/big_picture.png'); + expect(notification.smallIconAccentColor, '#FF0000FF'); + expect(notification.ledColor, '#FFFF0000'); + expect(notification.lockScreenVisibility, 1); + expect(notification.groupKey, 'group_key_1'); + expect(notification.groupMessage, 'You have 2 messages'); + expect(notification.fromProjectNumber, '123456789'); + expect(notification.collapseId, 'collapse_1'); + expect(notification.priority, 10); + expect(notification.androidNotificationId, 1); + }); + + test('parses backgroundImageLayout correctly', () { + final notification = OSNotification(androidOnlyJson); + + expect(notification.backgroundImageLayout, isNotNull); + expect(notification.backgroundImageLayout!.image, + 'https://example.com/bg.png'); + expect(notification.backgroundImageLayout!.titleTextColor, '#FF000000'); + expect(notification.backgroundImageLayout!.bodyTextColor, '#FF666666'); + }); + + test('parses grouped notifications correctly', () { + final groupedNotificationsJson = '''[ + {"notificationId": "grouped-1", "title": "Title 1", "body": "Body 1"}, + {"notificationId": "grouped-2", "title": "Title 2", "body": "Body 2"}, + {"notificationId": "grouped-3", "title": "Title 3", "body": "Body 3"} + ]'''; + + final json = { + 'notificationId': 'parent-notification', + 'title': 'Parent Title', + 'body': 'Parent Body', + 'groupedNotifications': groupedNotificationsJson, + }; + final notification = OSNotification(json); + + expect(notification.groupedNotifications, isNotNull); + expect(notification.groupedNotifications!.length, 3); + expect(notification.groupedNotifications![0].notificationId, 'grouped-1'); + expect(notification.groupedNotifications![0].title, 'Title 1'); + expect(notification.groupedNotifications![1].notificationId, 'grouped-2'); + expect(notification.groupedNotifications![1].body, 'Body 2'); + expect(notification.groupedNotifications![2].notificationId, 'grouped-3'); + expect(notification.groupedNotifications![2].title, 'Title 3'); + }); + + test('creates with empty grouped notifications', () { + final groupedNotificationsJson = '[]'; + + final json = { + 'notificationId': 'notification-with-empty-group', + 'groupedNotifications': groupedNotificationsJson, + }; + final notification = OSNotification(json); + + expect(notification.groupedNotifications, isNotNull); + expect(notification.groupedNotifications!.length, 0); + }); + + test('jsonRepresentation returns correct JSON string', () { + final notification = OSNotification(validNotificationJson); + final jsonRep = notification.jsonRepresentation(); + + expect(jsonRep, isA()); + expect(jsonRep, contains('key')); + expect(jsonRep, contains('value')); + }); + }); + + group('OSNotificationClickResult', () { + test('creates from valid JSON', () { + final result = OSNotificationClickResult(clickResultJson); + + expect(result.actionId, 'action-123'); + expect(result.url, 'https://example.com/action'); + }); + + test('creates from JSON with null fields', () { + final json = {}; + final result = OSNotificationClickResult(json); + + expect(result.actionId, isNull); + expect(result.url, isNull); + }); + + test('jsonRepresentation returns correct JSON string', () { + final result = OSNotificationClickResult(clickResultJson); + final jsonRep = result.jsonRepresentation(); + + expect(jsonRep, isA()); + expect(jsonRep, contains('action-123')); + expect(jsonRep, contains('https://example.com/action')); + }); + + test('jsonRepresentation handles null values', () { + final json = {}; + final result = OSNotificationClickResult(json); + final jsonRep = result.jsonRepresentation(); + + expect(jsonRep, isA()); + expect(jsonRep, contains('action_id')); + expect(jsonRep, contains('url')); + }); + }); + + group('OSActionButton', () { + test('creates with required parameters', () { + final button = OSActionButton(id: 'btn1', text: 'Click Me'); + + expect(button.id, 'btn1'); + expect(button.text, 'Click Me'); + expect(button.icon, isNull); + }); + + test('creates with all parameters', () { + final button = OSActionButton( + id: 'btn2', + text: 'Share', + icon: 'share_icon.png', + ); + + expect(button.id, 'btn2'); + expect(button.text, 'Share'); + expect(button.icon, 'share_icon.png'); + }); + + test('creates from JSON', () { + final json = { + 'id': 'action1', + 'text': 'Open', + 'icon': 'open.png', + }; + final button = OSActionButton.fromJson(json); + + expect(button.id, 'action1'); + expect(button.text, 'Open'); + expect(button.icon, 'open.png'); + }); + + test('creates from JSON without icon', () { + final json = { + 'id': 'action2', + 'text': 'Dismiss', + }; + final button = OSActionButton.fromJson(json); + + expect(button.id, 'action2'); + expect(button.text, 'Dismiss'); + expect(button.icon, isNull); + }); + + test('mapRepresentation returns correct map', () { + final button = OSActionButton( + id: 'btn', + text: 'Text', + icon: 'icon.png', + ); + final map = button.mapRepresentation(); + + expect(map['id'], 'btn'); + expect(map['text'], 'Text'); + expect(map['icon'], 'icon.png'); + }); + + test('jsonRepresentation returns correct JSON string', () { + final button = OSActionButton( + id: 'btn1', + text: 'Action Text', + icon: 'icon.png', + ); + final jsonRep = button.jsonRepresentation(); + + expect(jsonRep, isA()); + expect(jsonRep, contains('btn1')); + expect(jsonRep, contains('Action Text')); + }); + }); + + group('OSAndroidBackgroundImageLayout', () { + test('creates from valid JSON', () { + final layout = OSAndroidBackgroundImageLayout({ + 'image': 'https://example.com/bg.png', + 'titleTextColor': '#FF000000', + 'bodyTextColor': '#FF666666', + }); + + expect(layout.image, 'https://example.com/bg.png'); + expect(layout.titleTextColor, '#FF000000'); + expect(layout.bodyTextColor, '#FF666666'); + }); + + test('creates from empty JSON', () { + final layout = OSAndroidBackgroundImageLayout({}); + + expect(layout.image, isNull); + expect(layout.titleTextColor, isNull); + expect(layout.bodyTextColor, isNull); + }); + + test('jsonRepresentation returns correct JSON string', () { + final layout = OSAndroidBackgroundImageLayout({ + 'image': 'https://example.com/bg.png', + 'titleTextColor': '#FF000000', + 'bodyTextColor': '#FF666666', + }); + final jsonRep = layout.jsonRepresentation(); + + expect(jsonRep, isA()); + expect(jsonRep, contains('image')); + expect(jsonRep, contains('titleTextColor')); + expect(jsonRep, contains('bodyTextColor')); + }); + }); + + group('OSNotificationWillDisplayEvent', () { + test('creates from valid JSON', () { + final json = { + 'notification': validNotificationJson, + }; + final event = OSNotificationWillDisplayEvent(json); + + expect(event.notification, isNotNull); + expect(event.notification.notificationId, 'notification-123'); + expect(event.notification.title, 'Test Title'); + }); + + test('creates with iOS notification data', () { + final json = { + 'notification': iosOnlyJson, + }; + final event = OSNotificationWillDisplayEvent(json); + + expect(event.notification.contentAvailable, true); + expect(event.notification.category, 'CUSTOM_CATEGORY'); + }); + + test('jsonRepresentation returns correct JSON string', () { + final json = { + 'notification': validNotificationJson, + }; + final event = OSNotificationWillDisplayEvent(json); + final jsonRep = event.jsonRepresentation(); + + expect(jsonRep, isA()); + expect(jsonRep, contains('notification')); + }); + }); + + group('OSNotificationClickEvent', () { + test('creates from valid JSON with both notification and result', () { + final json = { + 'notification': validNotificationJson, + 'result': clickResultJson, + }; + final event = OSNotificationClickEvent(json); + + expect(event.notification, isNotNull); + expect(event.notification.notificationId, 'notification-123'); + expect(event.result, isNotNull); + expect(event.result.actionId, 'action-123'); + expect(event.result.url, 'https://example.com/action'); + }); + + test('creates from JSON with minimal data', () { + final json = { + 'notification': {'notificationId': 'click-123'}, + 'result': {}, + }; + final event = OSNotificationClickEvent(json); + + expect(event.notification.notificationId, 'click-123'); + expect(event.result.actionId, isNull); + expect(event.result.url, isNull); + }); + + test('jsonRepresentation returns correct JSON string', () { + final json = { + 'notification': validNotificationJson, + 'result': clickResultJson, + }; + final event = OSNotificationClickEvent(json); + final jsonRep = event.jsonRepresentation(); + + expect(jsonRep, isA()); + expect(jsonRep, contains('notification')); + expect(jsonRep, contains('result')); + }); + + test('jsonRepresentation includes action data from result', () { + final json = { + 'notification': validNotificationJson, + 'result': clickResultJson, + }; + final event = OSNotificationClickEvent(json); + final jsonRep = event.jsonRepresentation(); + + // The notification's jsonRepresentation only includes rawPayload + // The result includes actionId and url + expect(jsonRep, contains('action-123')); + expect(jsonRep, contains('https://example.com/action')); + }); + }); + + group('OSDisplayNotification extension', () { + late OneSignalMockChannelController channelController; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + channelController = OneSignalMockChannelController(); + channelController.resetState(); + }); + + test('display() calls displayNotification with correct notification ID', + () { + final notification = OSNotification(validNotificationJson); + + notification.display(); + + expect( + channelController.state.displayedNotificationId, 'notification-123'); + }); + + test('display() works with different notification IDs', () { + final json = {'notificationId': 'custom-notification-id'}; + final notification = OSNotification(json); + + notification.display(); + + expect(channelController.state.displayedNotificationId, + 'custom-notification-id'); + }); + + test('multiple display() calls update the displayed notification ID', () { + final notification1 = OSNotification({'notificationId': 'first-id'}); + final notification2 = OSNotification({'notificationId': 'second-id'}); + + notification1.display(); + expect(channelController.state.displayedNotificationId, 'first-id'); + + notification2.display(); + expect(channelController.state.displayedNotificationId, 'second-id'); + }); + }); + + group('OSNotificationWillDisplayEvent preventDefault', () { + late OneSignalMockChannelController channelController; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + channelController = OneSignalMockChannelController(); + channelController.resetState(); + }); + + test('preventDefault() calls preventDefault with correct notification ID', + () { + final json = { + 'notification': validNotificationJson, + }; + final event = OSNotificationWillDisplayEvent(json); + + event.preventDefault(); + + expect( + channelController.state.preventedNotificationId, 'notification-123'); + }); + + test('preventDefault() works with different notification IDs', () { + final json = { + 'notification': {'notificationId': 'custom-will-display-id'}, + }; + final event = OSNotificationWillDisplayEvent(json); + + event.preventDefault(); + + expect(channelController.state.preventedNotificationId, + 'custom-will-display-id'); + }); + + test('multiple preventDefault() calls update the prevented notification ID', + () { + final json1 = { + 'notification': {'notificationId': 'prevent-1'}, + }; + final json2 = { + 'notification': {'notificationId': 'prevent-2'}, + }; + final event1 = OSNotificationWillDisplayEvent(json1); + final event2 = OSNotificationWillDisplayEvent(json2); + + event1.preventDefault(); + expect(channelController.state.preventedNotificationId, 'prevent-1'); + + event2.preventDefault(); + expect(channelController.state.preventedNotificationId, 'prevent-2'); + }); + }); + + group('OSNotificationClickEvent preventDefault', () { + late OneSignalMockChannelController channelController; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + channelController = OneSignalMockChannelController(); + channelController.resetState(); + }); + + test('preventDefault() calls preventDefault with correct notification ID', + () { + final json = { + 'notification': validNotificationJson, + 'result': clickResultJson, + }; + final event = OSNotificationClickEvent(json); + + event.preventDefault(); + + expect( + channelController.state.preventedNotificationId, 'notification-123'); + }); + + test('preventDefault() works with different notification IDs', () { + final json = { + 'notification': {'notificationId': 'custom-click-id'}, + 'result': {}, + }; + final event = OSNotificationClickEvent(json); + + event.preventDefault(); + + expect( + channelController.state.preventedNotificationId, 'custom-click-id'); + }); + + test('multiple preventDefault() calls update the prevented notification ID', + () { + final json1 = { + 'notification': {'notificationId': 'click-prevent-1'}, + 'result': {}, + }; + final json2 = { + 'notification': {'notificationId': 'click-prevent-2'}, + 'result': {}, + }; + final event1 = OSNotificationClickEvent(json1); + final event2 = OSNotificationClickEvent(json2); + + event1.preventDefault(); + expect( + channelController.state.preventedNotificationId, 'click-prevent-1'); + + event2.preventDefault(); + expect( + channelController.state.preventedNotificationId, 'click-prevent-2'); + }); + }); +}