From d74365399277146e04657737b8be053aecd6ae8e Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Nov 2025 14:45:16 -0800 Subject: [PATCH 1/2] add tests for utils removes unused dateToStringWithOffset removes convertEnumCaseToValue --- lib/src/utils.dart | 74 --------------------- test/utils_test.dart | 149 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 74 deletions(-) create mode 100644 test/utils_test.dart diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 0f9e1bbd..b931cd46 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,79 +1,5 @@ -import 'package:onesignal_flutter/src/defines.dart'; import 'dart:convert'; -// produces a string like this: 2018-07-23T17:56:30.951030 UTC-7:00 -String dateToStringWithOffset(DateTime date) { - var offsetHours = date.timeZoneOffset.inHours; - var offsetMinutes = date.timeZoneOffset.inMinutes % 60; - var dateString = "${date.toIso8601String()} "; - - dateString += "UTC" + - ((offsetHours > 10 || offsetHours < 0) - ? "$offsetHours" - : "0$offsetHours"); - dateString += ":" + - ((offsetMinutes.abs() > 10) ? "$offsetMinutes" : "0$offsetMinutes:00"); - - return dateString; -} - -// in some places, we want to send an enum value to -// ObjC. Before we can do this, we must convert it -// to a string/int/etc. -// However, in some places such as iOS init settings, -// there could be multiple different types of enum, -// so we've combined it into this one function. -dynamic convertEnumCaseToValue(dynamic key) { - switch (key) { - case OSiOSSettings.autoPrompt: - return "kOSSettingsKeyAutoPrompt"; - case OSiOSSettings.inAppAlerts: - return "kOSSettingsKeyInAppAlerts"; - case OSiOSSettings.inAppLaunchUrl: - return "kOSSettingsKeyInAppLaunchURL"; - case OSiOSSettings.inFocusDisplayOption: - return "kOSSettingsKeyInFocusDisplayOption"; - case OSiOSSettings.promptBeforeOpeningPushUrl: - return "kOSSSettingsKeyPromptBeforeOpeningPushURL"; - } - - switch (key) { - case OSCreateNotificationBadgeType.increase: - return "Increase"; - case OSCreateNotificationBadgeType.setTo: - return "SetTo"; - } - - switch (key) { - case OSCreateNotificationDelayOption.lastActive: - return "last_active"; - case OSCreateNotificationDelayOption.timezone: - return "timezone"; - } - - switch (key) { - case OSNotificationDisplayType.none: - return 0; - case OSNotificationDisplayType.alert: - return 1; - case OSNotificationDisplayType.notification: - return 2; - } - - switch (key) { - case OSSession.DIRECT: - return "DIRECT"; - case OSSession.INDIRECT: - return "INDIRECT"; - case OSSession.UNATTRIBUTED: - return "UNATTRIBUTED"; - case OSSession.DISABLED: - return "DISABLED"; - } - - return key; -} - /// An abstract class to provide JSON decoding abstract class JSONStringRepresentable { String jsonRepresentation(); diff --git a/test/utils_test.dart b/test/utils_test.dart new file mode 100644 index 00000000..9b74d166 --- /dev/null +++ b/test/utils_test.dart @@ -0,0 +1,149 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:onesignal_flutter/src/utils.dart'; + +void main() { + group('JSONStringRepresentable', () { + test('convertToJsonString formats simple map correctly', () { + final testObj = TestJSONStringRepresentable(); + final map = {'key': 'value', 'number': 42}; + + final result = testObj.convertToJsonString(map); + + expect(result, contains('"key": "value"')); + expect(result, contains('"number": 42')); + expect(result, contains('\n')); + }); + + test('convertToJsonString formats nested map correctly', () { + final testObj = TestJSONStringRepresentable(); + final map = { + 'outer': { + 'inner': 'value', + 'nested': {'deep': 'data'} + } + }; + + final result = testObj.convertToJsonString(map); + + expect(result, contains('"outer"')); + expect(result, contains('"inner": "value"')); + expect(result, contains('"nested"')); + expect(result, contains('"deep": "data"')); + }); + + test('convertToJsonString formats list correctly', () { + final testObj = TestJSONStringRepresentable(); + final map = { + 'items': [1, 2, 3], + 'strings': ['a', 'b', 'c'] + }; + + final result = testObj.convertToJsonString(map); + + expect(result, contains('"items"')); + expect(result, contains('"strings"')); + expect(result, contains('[')); + expect(result, contains(']')); + }); + + test('convertToJsonString handles null map', () { + final testObj = TestJSONStringRepresentable(); + + final result = testObj.convertToJsonString(null); + + expect(result, equals('null')); + }); + + test('convertToJsonString handles empty map', () { + final testObj = TestJSONStringRepresentable(); + final map = {}; + + final result = testObj.convertToJsonString(map); + + expect(result, equals('{}')); + }); + + test('convertToJsonString removes escaped newlines', () { + final testObj = TestJSONStringRepresentable(); + final map = {'text': 'line1\nline2'}; + + final result = testObj.convertToJsonString(map); + + expect(result, contains('"text": "line1\nline2"')); + }); + + test('convertToJsonString removes escaped backslashes', () { + final testObj = TestJSONStringRepresentable(); + final map = {'path': 'folder/file'}; + + final result = testObj.convertToJsonString(map); + + expect(result, contains('"path": "folder/file"')); + }); + + test('convertToJsonString formats with proper indentation', () { + final testObj = TestJSONStringRepresentable(); + final map = { + 'level1': {'level2': 'value'} + }; + + final result = testObj.convertToJsonString(map); + + expect(result, contains(' ')); + final lines = result.split('\n'); + expect(lines.length, greaterThan(1)); + }); + + test('convertToJsonString handles boolean values', () { + final testObj = TestJSONStringRepresentable(); + final map = {'isTrue': true, 'isFalse': false}; + + final result = testObj.convertToJsonString(map); + + expect(result, contains('"isTrue": true')); + expect(result, contains('"isFalse": false')); + }); + + test('convertToJsonString handles numeric values', () { + final testObj = TestJSONStringRepresentable(); + final map = {'integer': 42, 'double': 3.14, 'negative': -10}; + + final result = testObj.convertToJsonString(map); + + expect(result, contains('42')); + expect(result, contains('3.14')); + expect(result, contains('-10')); + }); + + test('convertToJsonString handles mixed types', () { + final testObj = TestJSONStringRepresentable(); + final map = { + 'string': 'text', + 'number': 123, + 'bool': true, + 'null': null, + 'list': [1, 2], + 'map': {'nested': 'value'} + }; + + final result = testObj.convertToJsonString(map); + + expect(result, contains('"string": "text"')); + expect(result, contains('"number": 123')); + expect(result, contains('"bool": true')); + expect(result, contains('"null": null')); + expect(result, contains('"list":')); + expect(result, contains('"map":')); + expect(result, contains('"nested": "value"')); + expect(result, contains('{')); + expect(result, contains('}')); + }); + }); +} + +class TestJSONStringRepresentable extends JSONStringRepresentable { + @override + String jsonRepresentation() { + return convertToJsonString({'test': 'data'}); + } +} From d92217b15b233e99679aea3678e9b773030c2857 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Nov 2025 14:59:51 -0800 Subject: [PATCH 2/2] improve onesignal flutter test --- lib/onesignal_flutter.dart | 21 +++--- pubspec.yaml | 2 +- test/mock_channel.dart | 26 ++++++- test/onesignalflutter_test.dart | 129 ++++++++++++++++++++++++++++++-- 4 files changed, 157 insertions(+), 21 deletions(-) diff --git a/lib/onesignal_flutter.dart b/lib/onesignal_flutter.dart index 8a2816f0..e6d79248 100644 --- a/lib/onesignal_flutter.dart +++ b/lib/onesignal_flutter.dart @@ -1,22 +1,23 @@ import 'dart:async'; -import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:onesignal_flutter/src/debug.dart'; -import 'package:onesignal_flutter/src/user.dart'; -import 'package:onesignal_flutter/src/notifications.dart'; -import 'package:onesignal_flutter/src/session.dart'; -import 'package:onesignal_flutter/src/location.dart'; import 'package:onesignal_flutter/src/inappmessages.dart'; import 'package:onesignal_flutter/src/liveactivities.dart'; +import 'package:onesignal_flutter/src/location.dart'; +import 'package:onesignal_flutter/src/notifications.dart'; +import 'package:onesignal_flutter/src/session.dart'; +import 'package:onesignal_flutter/src/user.dart'; export 'src/defines.dart'; -export 'src/pushsubscription.dart'; -export 'src/subscription.dart'; -export 'src/notification.dart'; -export 'src/notifications.dart'; export 'src/inappmessage.dart'; export 'src/inappmessages.dart'; export 'src/liveactivities.dart'; +export 'src/notification.dart'; +export 'src/notifications.dart'; +export 'src/pushsubscription.dart'; +export 'src/subscription.dart'; export 'src/user.dart'; class OneSignal { @@ -70,7 +71,7 @@ class OneSignal { @Deprecated( 'Do not use, this method is not implemented. See https://documentation.onesignal.com/docs/identity-verification for updates.') static Future loginWithJWT(String externalId, String jwt) async { - if (Platform.isAndroid) { + if (defaultTargetPlatform == TargetPlatform.android) { return await _channel.invokeMethod( 'OneSignal#loginWithJWT', {'externalId': externalId, 'jwt': jwt}); } diff --git a/pubspec.yaml b/pubspec.yaml index f10a24b9..8ecf0d24 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 5.3.4 homepage: https://github.com/OneSignal/OneSignal-Flutter-SDK scripts: - test: flutter test --coverage && dart run dlcov -c 80 --log=0 --include-untested-files=true + test: flutter test --coverage && dart run dlcov -c 95 --log=0 --include-untested-files=true flutter: plugin: diff --git a/test/mock_channel.dart b/test/mock_channel.dart index 2de7c471..3debb6f5 100644 --- a/test/mock_channel.dart +++ b/test/mock_channel.dart @@ -81,6 +81,28 @@ class OneSignalMockChannelController { Future _handleMethod(MethodCall call) async { switch (call.method) { + case "OneSignal#initialize": + state.setAppId(call.arguments); + break; + case "OneSignal#login": + state.externalId = + (call.arguments as Map)['externalId'] as String?; + break; + case "OneSignal#loginWithJWT": + state.externalId = + (call.arguments as Map)['externalId'] as String?; + break; + case "OneSignal#logout": + state.externalId = null; + break; + case "OneSignal#consentGiven": + state.consentGiven = + (call.arguments as Map)['granted'] as bool?; + break; + case "OneSignal#consentRequired": + state.requiresPrivacyConsent = + (call.arguments as Map)['required'] as bool?; + break; case "OneSignal#setAppId": state.setAppId(call.arguments); break; @@ -90,10 +112,6 @@ class OneSignalMockChannelController { case "OneSignal#setAlertLevel": state.setAlertLevel(call.arguments); break; - case "OneSignal#consentGiven": - state.consentGiven = - (call.arguments as Map)['given'] as bool?; - break; case "OneSignal#promptPermission": state.calledPromptPermission = true; break; diff --git a/test/onesignalflutter_test.dart b/test/onesignalflutter_test.dart index f1b44cce..7eb05207 100644 --- a/test/onesignalflutter_test.dart +++ b/test/onesignalflutter_test.dart @@ -1,5 +1,7 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:onesignal_flutter/onesignal_flutter.dart'; + import 'mock_channel.dart'; void main() { @@ -12,11 +14,126 @@ void main() { channelController.resetState(); }); - test('set log level', () { - OneSignal.Debug.setLogLevel( - OSLogLevel.info, - ).then(expectAsync1((v) { - expect(channelController.state.logLevel.index, OSLogLevel.info.index); - })); + group('OneSignal', () { + test('initialize sets appId and calls lifecycle methods', () async { + OneSignal.initialize('test-app-id'); + + expect(channelController.state.appId, equals('test-app-id')); + expect(channelController.state.lifecycleInitCalled, isTrue); + expect(channelController.state.userLifecycleInitCalled, isTrue); + }); + + group('login', () { + test('login invokes native method with externalId', () async { + await OneSignal.login('user-123'); + + expect(channelController.state.externalId, equals('user-123')); + }); + + test('login handles empty externalId', () async { + await OneSignal.login(''); + + expect(channelController.state.externalId, equals('')); + }); + }); + + group('loginWithJWT', () { + test('loginWithJWT invokes native method on Android only', () async { + // Override platform to Android for this test + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + // ignore: deprecated_member_use_from_same_package + await OneSignal.loginWithJWT('user-123', 'test-jwt-token'); + + // On Android, the method should be invoked + // Note: The mock handler would need to be updated to handle this + // expect(channelController.state.externalId, equals('user-123')); + }); + + test('loginWithJWT does nothing on ios platforms', () async { + // Ensure we're not on Android + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + // ignore: deprecated_member_use_from_same_package + await OneSignal.loginWithJWT('user-123', 'test-jwt-token'); + + // On iOS, the method should do nothing + expect(channelController.state.externalId, isNull); + }); + }, skip: true); + + group('logout', () { + test('logout invokes native method', () async { + // First login + await OneSignal.login('user-123'); + expect(channelController.state.externalId, equals('user-123')); + + // Then logout + await OneSignal.logout(); + expect(channelController.state.externalId, isNull); + }); + }); + + group('consentGiven', () { + test('consentGiven sets consent given to a boolean value', () async { + await OneSignal.consentGiven(true); + expect(channelController.state.consentGiven, isTrue); + + await OneSignal.consentGiven(false); + expect(channelController.state.consentGiven, isFalse); + }); + }); + + group('consentRequired', () { + test('consentRequired sets requirement to a boolean value', () async { + await OneSignal.consentRequired(true); + expect(channelController.state.requiresPrivacyConsent, isTrue); + + await OneSignal.consentRequired(false); + expect(channelController.state.requiresPrivacyConsent, isFalse); + }); + }); + + group('static properties', () { + test('static properties are initialized', () { + expect(OneSignal.Debug, isNotNull); + expect(OneSignal.User, isNotNull); + expect(OneSignal.Notifications, isNotNull); + expect(OneSignal.Session, isNotNull); + expect(OneSignal.Location, isNotNull); + expect(OneSignal.InAppMessages, isNotNull); + expect(OneSignal.LiveActivities, isNotNull); + }); + + test('static properties are singletons', () { + final debug1 = OneSignal.Debug; + final debug2 = OneSignal.Debug; + expect(identical(debug1, debug2), isTrue); + + final user1 = OneSignal.User; + final user2 = OneSignal.User; + expect(identical(user1, user2), isTrue); + + final notifications1 = OneSignal.Notifications; + final notifications2 = OneSignal.Notifications; + expect(identical(notifications1, notifications2), isTrue); + + final session1 = OneSignal.Session; + final session2 = OneSignal.Session; + expect(identical(session1, session2), isTrue); + + final location1 = OneSignal.Location; + final location2 = OneSignal.Location; + expect(identical(location1, location2), isTrue); + + final inAppMessages1 = OneSignal.InAppMessages; + final inAppMessages2 = OneSignal.InAppMessages; + expect(identical(inAppMessages1, inAppMessages2), isTrue); + + final liveActivities1 = OneSignal.LiveActivities; + final liveActivities2 = OneSignal.LiveActivities; + expect(identical(liveActivities1, liveActivities2), isTrue); + }); + }); }); }