diff --git a/.gitignore b/.gitignore index 3266facd..7868544b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,13 @@ android/.idea flutter_export_environment.sh example/ios/Flutter/ephemeral/ +# Coverage +coverage/ +test/dlcov_references_test.dart +dlcov.log + # IntelliJ related *.iml *.ipr *.iws -.idea/ \ No newline at end of file +.idea/ diff --git a/lib/src/inappmessage.dart b/lib/src/inappmessage.dart index fae8aaf9..5fc1c094 100644 --- a/lib/src/inappmessage.dart +++ b/lib/src/inappmessage.dart @@ -16,8 +16,8 @@ class OSInAppMessageClickEvent extends JSONStringRepresentable { String jsonRepresentation() { return convertToJsonString({ - 'message': this.message, - 'result': this.result, + 'message': this.message.jsonRepresentation(), + 'result': this.result.jsonRepresentation(), }); } } diff --git a/lib/src/inappmessages.dart b/lib/src/inappmessages.dart index b74340cd..4c0d8ebd 100644 --- a/lib/src/inappmessages.dart +++ b/lib/src/inappmessages.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:flutter/services.dart'; import 'package:onesignal_flutter/onesignal_flutter.dart'; import 'package:onesignal_flutter/src/inappmessage.dart'; @@ -20,7 +21,7 @@ class OneSignalInAppMessages { // constructor method OneSignalInAppMessages() { - this._channel.setMethodCallHandler(_handleMethod); + this._channel.setMethodCallHandler(handleMethod); } List _clickListeners = @@ -78,7 +79,8 @@ class OneSignalInAppMessages { } // Private function that gets called by ObjC/Java - Future _handleMethod(MethodCall call) async { + // Exposed as public for testing purposes + Future handleMethod(MethodCall call) async { if (call.method == 'OneSignal#onClickInAppMessage') { for (var listener in _clickListeners) { listener( diff --git a/pubspec.yaml b/pubspec.yaml index 8ccebfba..5ac83182 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,6 +3,9 @@ description: OneSignal is a free push notification service for mobile apps. This version: 5.3.4 homepage: https://github.com/OneSignal/OneSignal-Flutter-SDK +scripts: + test: dlcov -c 80 --lcov-gen="flutter test --coverage" --include-untested-files=true + flutter: plugin: platforms: @@ -20,7 +23,9 @@ dev_dependencies: test: ^1.5.1 flutter_test: sdk: flutter + dlcov: ^4.2.1 + rps: ^0.9.1 environment: - sdk: '>=2.12.0 <3.0.0' - flutter: '>=1.10.0' + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.10.0" diff --git a/test/debug_test.dart b/test/debug_test.dart new file mode 100644 index 00000000..b56d07d8 --- /dev/null +++ b/test/debug_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:onesignal_flutter/onesignal_flutter.dart'; + +import 'mock_channel.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + OneSignalMockChannelController channelController = + OneSignalMockChannelController(); + + setUp(() { + channelController.resetState(); + }); + + group('OneSignalDebug', () { + group('setLogLevel', () { + for (final logLevel in OSLogLevel.values) { + test('sets log level to ${logLevel.toString().split('.').last}', + () async { + await OneSignal.Debug.setLogLevel(logLevel); + expect(channelController.state.logLevel, logLevel); + }); + } + }); + + group('setAlertLevel', () { + for (final logLevel in OSLogLevel.values) { + test('sets alert level to ${logLevel.toString().split('.').last}', + () async { + await OneSignal.Debug.setAlertLevel(logLevel); + expect(channelController.state.visualLevel, logLevel); + }); + } + }); + }); +} diff --git a/test/inappmessage_test.dart b/test/inappmessage_test.dart new file mode 100644 index 00000000..be5bb927 --- /dev/null +++ b/test/inappmessage_test.dart @@ -0,0 +1,178 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:onesignal_flutter/src/inappmessage.dart'; +import 'package:onesignal_flutter/src/utils.dart'; + +const validMessageJson = { + 'message_id': 'test-message-id-123', +}; + +const validClickResultJson = { + 'action_id': 'action-123', + 'url': 'https://example.com', + 'closing_message': true, +}; + +void _testMessageEvent( + String eventName, + T Function(Map) constructor, +) { + group(eventName, () { + test('creates from valid JSON with message', () { + final json = {'message': validMessageJson}; + final event = constructor(json); + expect(event, isNotNull); + }); + + test('creates from JSON with null message_id', () { + final json = {'message': {}}; + final event = constructor(json); + expect(event, isNotNull); + }); + + test('jsonRepresentation returns correct JSON string', () { + final json = {'message': validMessageJson}; + final event = constructor(json); + final jsonString = event.jsonRepresentation(); + expect(jsonString, contains('"message"')); + }); + }); +} + +void main() { + group('OSInAppMessage', () { + test('creates from valid JSON with message_id', () { + final message = OSInAppMessage(validMessageJson); + + expect(message.messageId, validMessageJson['message_id']); + }); + + test('creates from JSON with null message_id', () { + final json = {}; + final message = OSInAppMessage(json); + + expect(message.messageId, isNull); + }); + + test('jsonRepresentation returns correct JSON string', () { + // Test with valid message_id + final message1 = OSInAppMessage(validMessageJson); + final jsonString1 = message1.jsonRepresentation(); + expect(jsonString1, contains('"message_id": "test-message-id-123"')); + + // Test with null message_id + final json = {}; + final message2 = OSInAppMessage(json); + final jsonString2 = message2.jsonRepresentation(); + expect(jsonString2, contains('"message_id"')); + expect(jsonString2, contains('null')); + }); + }); + + group('OSInAppMessageClickResult', () { + test('creates from valid JSON with all fields', () { + final result = OSInAppMessageClickResult(validClickResultJson); + + expect(result.actionId, 'action-123'); + expect(result.url, 'https://example.com'); + expect(result.closingMessage, true); + }); + + test('creates from minimal JSON', () { + final json = { + 'closing_message': false, + }; + final result = OSInAppMessageClickResult(json); + + expect(result.actionId, isNull); + expect(result.url, isNull); + expect(result.closingMessage, false); + }); + + test('jsonRepresentation returns correct JSON string', () { + final result = OSInAppMessageClickResult(validClickResultJson); + final jsonString = result.jsonRepresentation(); + + expect(jsonString, contains('"action_id": "action-123"')); + expect(jsonString, contains('"url": "https://example.com"')); + expect(jsonString, contains('"closing_message": true')); + }); + + test('jsonRepresentation handles null optional fields', () { + final json = { + 'closing_message': false, + }; + final result = OSInAppMessageClickResult(json); + final jsonString = result.jsonRepresentation(); + + expect(jsonString, contains('"action_id": null')); + expect(jsonString, contains('"url": null')); + expect(jsonString, contains('"closing_message": false')); + }); + }); + + group('OSInAppMessageClickEvent', () { + test('creates from valid JSON with message and result', () { + final json = { + 'message': validMessageJson, + 'result': validClickResultJson, + 'closing_message': false, + }; + final event = OSInAppMessageClickEvent(json); + + expect(event.message.messageId, 'test-message-id-123'); + expect(event.result.actionId, 'action-123'); + expect(event.result.url, 'https://example.com'); + expect(event.result.closingMessage, true); + }); + + test('creates from JSON with minimal fields', () { + final json = { + 'message': validMessageJson, + 'result': { + 'closing_message': false, + }, + }; + final event = OSInAppMessageClickEvent(json); + + expect(event.message.messageId, 'test-message-id-123'); + expect(event.result.actionId, isNull); + expect(event.result.url, isNull); + expect(event.result.closingMessage, false); + }); + + test('jsonRepresentation returns correct JSON string', () { + final json = { + 'message': { + 'message_id': 'test-message-id-123', + }, + 'result': { + 'action_id': 'action-123', + 'url': 'https://example.com', + 'closing_message': true, + }, + }; + final event = OSInAppMessageClickEvent(json); + final jsonString = event.jsonRepresentation(); + + expect(jsonString, contains('"message"')); + expect(jsonString, contains('"result"')); + }); + }); + + _testMessageEvent( + 'OSInAppMessageWillDisplayEvent', + (json) => OSInAppMessageWillDisplayEvent(json), + ); + _testMessageEvent( + 'OSInAppMessageDidDisplayEvent', + (json) => OSInAppMessageDidDisplayEvent(json), + ); + _testMessageEvent( + 'OSInAppMessageWillDismissEvent', + (json) => OSInAppMessageWillDismissEvent(json), + ); + _testMessageEvent( + 'OSInAppMessageDidDismissEvent', + (json) => OSInAppMessageDidDismissEvent(json), + ); +} diff --git a/test/inappmessages_test.dart b/test/inappmessages_test.dart new file mode 100644 index 00000000..9921fcfc --- /dev/null +++ b/test/inappmessages_test.dart @@ -0,0 +1,451 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:onesignal_flutter/src/inappmessage.dart'; +import 'package:onesignal_flutter/src/inappmessages.dart'; + +const validMessageJson = { + 'message_id': 'test-message-id-123', +}; + +const validClickResultJson = { + 'action_id': 'action-123', + 'url': 'https://example.com', + 'closing_message': true, +}; + +const triggerName = 'purchase_made'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('OneSignalInAppMessages', () { + late OneSignalInAppMessages inAppMessages; + late List methodCalls; + + setUp(() { + methodCalls = []; + 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'}); + }); + + test('handles multiple triggers sequentially', () async { + const triggerName2 = 'trigger2'; + await inAppMessages.addTrigger(triggerName, 'value1'); + await inAppMessages.addTrigger(triggerName2, 'value2'); + + expect(methodCalls.length, 2); + expect(methodCalls[0].arguments, {triggerName: 'value1'}); + expect(methodCalls[1].arguments, {triggerName2: 'value2'}); + }); + }); + + group('addTriggers', () { + test('invokes OneSignal#addTriggers method with map of triggers', + () async { + final triggers = { + 'purchase_made': 'true', + 'user_level': '5', + }; + + await inAppMessages.addTriggers(triggers); + + expect(methodCalls.length, 1); + expect(methodCalls[0].method, 'OneSignal#addTriggers'); + expect(methodCalls[0].arguments, triggers); + }); + + test('handles empty triggers map', () async { + await inAppMessages.addTriggers({}); + + expect(methodCalls.length, 1); + expect(methodCalls[0].arguments, {}); + }); + }); + + group('removeTrigger', () { + 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); + }); + }); + + group('removeTriggers', () { + test('invokes OneSignal#removeTriggers method with list of keys', + () async { + final keys = ['trigger1', 'trigger2']; + + await inAppMessages.removeTriggers(keys); + + expect(methodCalls.length, 1); + expect(methodCalls[0].method, 'OneSignal#removeTriggers'); + expect(methodCalls[0].arguments, keys); + }); + + test('handles empty keys list', () async { + await inAppMessages.removeTriggers([]); + + expect(methodCalls.length, 1); + expect(methodCalls[0].arguments, []); + }); + }); + + group('clearTriggers', () { + test('invokes OneSignal#clearTriggers method', () async { + await inAppMessages.clearTriggers(); + + expect(methodCalls.length, 1); + expect(methodCalls[0].method, 'OneSignal#clearTriggers'); + }); + }); + + group('paused', () { + test('invokes OneSignal#paused', () async { + await inAppMessages.paused(true); + + expect(methodCalls.length, 1); + expect(methodCalls[0].method, 'OneSignal#paused'); + expect(methodCalls[0].arguments, true); + + await inAppMessages.paused(false); + + expect(methodCalls.length, 2); + expect(methodCalls[1].method, 'OneSignal#paused'); + expect(methodCalls[1].arguments, 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; + }, + ); + + final result = await inAppMessages.arePaused(); + + expect(result, true); + }); + }); + + group('lifecycleInit', () { + test('invokes OneSignal#lifecycleInit method', () async { + await inAppMessages.lifecycleInit(); + + expect(methodCalls.length, 1); + expect(methodCalls[0].method, 'OneSignal#lifecycleInit'); + }); + }); + + group('Click listeners', () { + test('addClickListener adds listener to list', () async { + bool listenerCalled = false; + + void listener(OSInAppMessageClickEvent event) { + listenerCalled = true; + } + + inAppMessages.addClickListener(listener); + + // Simulate native call to verify listener was added + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onClickInAppMessage', + { + 'message': validMessageJson, + 'result': validClickResultJson, + }, + ), + ); + + expect(listenerCalled, true); + }); + + test('removeClickListener removes listener from list', () async { + bool listenerCalled = false; + + void listener(OSInAppMessageClickEvent event) { + listenerCalled = true; + } + + inAppMessages.addClickListener(listener); + inAppMessages.removeClickListener(listener); + + // Simulate native call to verify listener was removed + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onClickInAppMessage', + { + 'message': validMessageJson, + 'result': validClickResultJson, + }, + ), + ); + + expect(listenerCalled, false); + }); + }); + + group('WillDisplay listeners', () { + test('addWillDisplayListener adds listener', () async { + bool listenerCalled = false; + late OSInAppMessageWillDisplayEvent receivedEvent; + + void listener(OSInAppMessageWillDisplayEvent event) { + listenerCalled = true; + receivedEvent = event; + } + + inAppMessages.addWillDisplayListener(listener); + + // Simulate native call to verify listener was added + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onWillDisplayInAppMessage', + {'message': validMessageJson}, + ), + ); + + expect(listenerCalled, true); + expect(receivedEvent.message.messageId, 'test-message-id-123'); + }); + + test('removeWillDisplayListener removes listener', () async { + bool listenerCalled = false; + + void listener(OSInAppMessageWillDisplayEvent event) { + listenerCalled = true; + } + + inAppMessages.addWillDisplayListener(listener); + inAppMessages.removeWillDisplayListener(listener); + + // Simulate native call to verify listener was removed + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onWillDisplayInAppMessage', + {'message': validMessageJson}, + ), + ); + + expect(listenerCalled, false); + }); + }); + + group('DidDisplay listeners', () { + test('addDidDisplayListener adds listener', () async { + bool listenerCalled = false; + + void listener(OSInAppMessageDidDisplayEvent event) { + listenerCalled = true; + } + + inAppMessages.addDidDisplayListener(listener); + + // Simulate native call to verify listener was added + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onDidDisplayInAppMessage', + {'message': validMessageJson}, + ), + ); + + expect(listenerCalled, true); + }); + + test('removeDidDisplayListener removes listener', () async { + bool listenerCalled = false; + + void listener(OSInAppMessageDidDisplayEvent event) { + listenerCalled = true; + } + + inAppMessages.addDidDisplayListener(listener); + inAppMessages.removeDidDisplayListener(listener); + + // Simulate native call to verify listener was removed + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onDidDisplayInAppMessage', + {'message': validMessageJson}, + ), + ); + + expect(listenerCalled, false); + }); + + test('did display listener is invoked', () async { + bool listenerCalled = false; + + void listener(OSInAppMessageDidDisplayEvent event) { + listenerCalled = true; + } + + inAppMessages.addDidDisplayListener(listener); + + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onDidDisplayInAppMessage', + {'message': validMessageJson}, + ), + ); + + expect(listenerCalled, true); + }); + }); + + group('WillDismiss listeners', () { + test('addWillDismissListener adds listener', () async { + bool listenerCalled = false; + late OSInAppMessageWillDismissEvent receivedEvent; + + void listener(OSInAppMessageWillDismissEvent event) { + listenerCalled = true; + receivedEvent = event; + } + + inAppMessages.addWillDismissListener(listener); + + // Simulate native call to verify listener was added + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onWillDismissInAppMessage', + {'message': validMessageJson}, + ), + ); + + expect(listenerCalled, true); + expect(receivedEvent.message.messageId, 'test-message-id-123'); + }); + + test('removeWillDismissListener removes listener', () async { + bool listenerCalled = false; + + void listener(OSInAppMessageWillDismissEvent event) { + listenerCalled = true; + } + + inAppMessages.addWillDismissListener(listener); + inAppMessages.removeWillDismissListener(listener); + + // Simulate native call to verify listener was removed + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onWillDismissInAppMessage', + {'message': validMessageJson}, + ), + ); + + expect(listenerCalled, false); + }); + }); + + group('DidDismiss listeners', () { + test('addDidDismissListener adds listener', () async { + bool listenerCalled = false; + late OSInAppMessageDidDismissEvent receivedEvent; + + void listener(OSInAppMessageDidDismissEvent event) { + listenerCalled = true; + receivedEvent = event; + } + + inAppMessages.addDidDismissListener(listener); + + // Simulate native call to verify listener was added + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onDidDismissInAppMessage', + {'message': validMessageJson}, + ), + ); + + expect(listenerCalled, true); + expect(receivedEvent.message.messageId, 'test-message-id-123'); + }); + + test('removeDidDismissListener removes listener', () async { + bool listenerCalled = false; + + void listener(OSInAppMessageDidDismissEvent event) { + listenerCalled = true; + } + + inAppMessages.addDidDismissListener(listener); + inAppMessages.removeDidDismissListener(listener); + + // Simulate native call to verify listener was removed + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onDidDismissInAppMessage', + {'message': validMessageJson}, + ), + ); + + expect(listenerCalled, false); + }); + }); + + group('Multiple listeners', () { + test('multiple click listeners are all invoked', () async { + int listenerCount = 0; + + void listener1(OSInAppMessageClickEvent event) { + listenerCount++; + } + + void listener2(OSInAppMessageClickEvent event) { + listenerCount++; + } + + inAppMessages.addClickListener(listener1); + inAppMessages.addClickListener(listener2); + + await inAppMessages.handleMethod( + MethodCall( + 'OneSignal#onClickInAppMessage', + { + 'message': validMessageJson, + 'result': validClickResultJson, + }, + ), + ); + + expect(listenerCount, 2); + }); + }); + }); +} diff --git a/test/mock_channel.dart b/test/mock_channel.dart index 6cb35b43..9691fc42 100644 --- a/test/mock_channel.dart +++ b/test/mock_channel.dart @@ -29,7 +29,6 @@ class OneSignalMockChannelController { } Future _handleMethod(MethodCall call) async { - print("Mock method called: ${call.method}"); switch (call.method) { case "OneSignal#setAppId": state.setAppId(call.arguments); @@ -37,6 +36,9 @@ class OneSignalMockChannelController { case "OneSignal#setLogLevel": state.setLogLevel(call.arguments); break; + case "OneSignal#setAlertLevel": + state.setAlertLevel(call.arguments); + break; case "OneSignal#consentGiven": state.consentGiven = (call.arguments as Map)['given'] as bool?; @@ -128,6 +130,12 @@ class OneSignalState { if (visual != null) visualLevel = OSLogLevel.values[visual]; } + void setAlertLevel(Map params) { + int? visual = params['visualLevel'] as int?; + + if (visual != null) visualLevel = OSLogLevel.values[visual]; + } + void consentRequired(Map params) { requiresPrivacyConsent = params['required'] as bool?; }