diff --git a/lib/flutter_skin.dart b/lib/flutter_skin.dart index 3e73c7c..23845c7 100644 --- a/lib/flutter_skin.dart +++ b/lib/flutter_skin.dart @@ -18,7 +18,10 @@ class FlutterSkin with WidgetsBindingObserver { // Private constructor FlutterSkin._(); - static Future init({required String apiKey}) async { + static Future init({ + required String apiKey, + @visibleForTesting FskinRemoteConfig? remoteConfig, + }) async { if (apiKey.trim().isEmpty) { _logger.logError('apiKey must not be empty'); throw ArgumentError.value(apiKey, 'apiKey', 'apiKey must not be empty'); @@ -33,7 +36,11 @@ class FlutterSkin with WidgetsBindingObserver { } _instance!.apiKey = apiKey; - await FskinRemoteConfig.init(apiKey: apiKey); + if (remoteConfig != null) { + FlutterSkin.remoteConfig = remoteConfig; + } else { + FlutterSkin.remoteConfig = await FskinRemoteConfig.init(apiKey: apiKey); + } return _instance!; } @@ -108,11 +115,5 @@ class FlutterSkin with WidgetsBindingObserver { @visibleForTesting static void resetInstance() { _instance = null; - FlutterSkin.remoteConfig = FskinRemoteConfig.singleton; - } - - @visibleForTesting - void setRemoteConfig(FskinRemoteConfig remoteConfig) { - FlutterSkin.remoteConfig = remoteConfig; } } diff --git a/lib/remote/fskin_remote_config.dart b/lib/remote/fskin_remote_config.dart index a5de829..21f279d 100644 --- a/lib/remote/fskin_remote_config.dart +++ b/lib/remote/fskin_remote_config.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_skin/constants/fskin_constants.dart'; import 'package:flutter_skin/models/project_config.dart'; import 'package:flutter_skin/services/skin_service.dart'; @@ -15,6 +16,8 @@ class FskinRemoteConfig { late String apiKey; ProjectConfig? _cachedConfig; + SkinService? skinService; + Stream get onSkinChanged => _skinController.stream; ProjectConfig? get projectConfig { @@ -39,9 +42,27 @@ class FskinRemoteConfig { } // Factory method to initialize and get the singleton instance - static Future init({required String apiKey}) async { + static Future init({ + required String apiKey, + @visibleForTesting SkinService? skinService, + }) async { _instance ??= FskinRemoteConfig._(); + + if (apiKey.trim().isEmpty || + !RegExp(FskinConstants.keyRegex).hasMatch(apiKey)) { + throw ArgumentError.value( + apiKey, + 'apiKey', + 'apiKey must be a valid non-empty string', + ); + } + _instance!.apiKey = apiKey; + if (skinService != null) { + _instance!.skinService = skinService; + } else { + _instance!.skinService = SkinService(); + } await _instance!.fetchConfig(); return _instance!; } @@ -49,7 +70,12 @@ class FskinRemoteConfig { Future fetchConfig() async { // Call the skin service to fetch skin for developer and project //final skin = await SkinService().getSkin(apiKey); - _cachedConfig = await SkinService().fetchData(apiKey); + _cachedConfig = await skinService?.fetchData(apiKey); _skinController.add(ThemeData(colorScheme: _cachedConfig?.skin?.colors)); } + + @visibleForTesting + static void resetInstance() { + _instance = null; + } } diff --git a/pubspec.yaml b/pubspec.yaml index bc55693..c68c1e2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 - mockito: ^5.5.0 + mocktail: ^1.0.5 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/flutter_skin/flutter_skin_init_test.dart b/test/flutter_skin/flutter_skin_init_test.dart index 88670fd..90fe727 100644 --- a/test/flutter_skin/flutter_skin_init_test.dart +++ b/test/flutter_skin/flutter_skin_init_test.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_skin/flutter_skin.dart'; import 'package:flutter_skin/models/project_config.dart'; import 'package:flutter_skin/remote/fskin_remote_config.dart'; +import 'package:flutter_skin/services/skin_service.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; +import 'package:mocktail/mocktail.dart'; import '../mocks/skin_model_mocks.dart'; @@ -11,6 +12,8 @@ class MockFskinRemoteConfig extends Mock implements FskinRemoteConfig {} class MockProjectConfig extends Mock implements ProjectConfig {} +class MockSkinService extends Mock implements SkinService {} + void main() async { group('FlutterSkin Initialization Tests', () { final apiKey = @@ -18,13 +21,14 @@ void main() async { setUp(() async { TestWidgetsFlutterBinding.ensureInitialized(); - await FlutterSkin.init(apiKey: apiKey); + FlutterSkin.resetInstance(); + await FlutterSkin.init( + apiKey: apiKey, + remoteConfig: MockFskinRemoteConfig(), + ); }); - test('fetches and applies skin on successful response', () async { - // Testing with fake api key will result in null theme. - await FlutterSkin.init(apiKey: apiKey); - + test('Checking FlutterSkin initialization with fake API key', () async { final skin = FlutterSkin.theme; final instance = FlutterSkin.singleton; @@ -32,13 +36,12 @@ void main() async { expect(skin, null); }); - test('applies skin from remote config', () async { + test('Veryfing skin model initalization with remote config', () async { final instance = FlutterSkin.singleton; - instance.setRemoteConfig(MockFskinRemoteConfig()); when( - FlutterSkin.remoteConfig.projectConfig, - ).thenReturn(ProjectConfig(skin: skinModelMock)); + () => FlutterSkin.remoteConfig.projectConfig, + ).thenAnswer((_) => ProjectConfig(skin: skinModelMock)); final skin = FlutterSkin.theme; expect(instance.apiKey, apiKey); @@ -48,11 +51,9 @@ void main() async { test('project config returns a nullable skin', () async { final instance = FlutterSkin.singleton; - - instance.setRemoteConfig(MockFskinRemoteConfig()); when( - FlutterSkin.remoteConfig.projectConfig, - ).thenReturn(MockProjectConfig()); + () => FlutterSkin.remoteConfig.projectConfig, + ).thenAnswer((_) => MockProjectConfig()); var skin = FlutterSkin.theme; expect(instance.apiKey, apiKey); diff --git a/test/flutter_skin/flutter_skin_integration_with_service.dart b/test/flutter_skin/flutter_skin_integration_with_service.dart new file mode 100644 index 0000000..eb86ed0 --- /dev/null +++ b/test/flutter_skin/flutter_skin_integration_with_service.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_skin/flutter_skin.dart'; +import 'package:flutter_skin/models/project_config.dart'; +import 'package:flutter_skin/remote/fskin_remote_config.dart'; +import 'package:flutter_skin/services/skin_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../mocks/skin_model_mocks.dart'; + +class MockSkinService extends Mock implements SkinService {} + +void main() { + group('FlutterSkin: Verify FlutterService Integration', () { + final apiKey = + 'fsk_1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + + MockSkinService? mockSkinService; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + FlutterSkin.resetInstance(); + FskinRemoteConfig.resetInstance(); + mockSkinService = MockSkinService(); + }); + + test('Verifying Service returning valid skin theme data', () async { + when( + () => mockSkinService?.fetchData(apiKey), + ).thenAnswer((_) async => ProjectConfig(skin: skinModelMock)); + + var remoteConfig = await FskinRemoteConfig.init( + apiKey: apiKey, + skinService: mockSkinService, + ); + + await FlutterSkin.init(apiKey: apiKey, remoteConfig: remoteConfig); + final fskinInstance = FlutterSkin.singleton; + + final skin = FlutterSkin.theme; + expect(fskinInstance.apiKey, apiKey); + expect(skin, isA()); + expect(skin?.colorScheme.primary, skinModelMock.colors?.primary); + }); + + test('Verifying Service returning null skin theme data', () async { + when( + () => mockSkinService?.fetchData(apiKey), + ).thenAnswer((_) async => ProjectConfig(skin: null)); + + var remoteConfig = await FskinRemoteConfig.init( + apiKey: apiKey, + skinService: mockSkinService, + ); + + await FlutterSkin.init(apiKey: apiKey, remoteConfig: remoteConfig); + final fskinInstance = FlutterSkin.singleton; + + final skin = FlutterSkin.theme; + expect(fskinInstance.apiKey, apiKey); + expect(skin, isA()); + expect(skin?.colorScheme.primary, isNull); + }); + + test( + 'Verifying Service returning null skin value with fallback theme data', + () async { + when( + () => mockSkinService?.fetchData(apiKey), + ).thenAnswer((_) async => ProjectConfig(skin: null)); + + var remoteConfig = await FskinRemoteConfig.init( + apiKey: apiKey, + skinService: mockSkinService, + ); + + await FlutterSkin.init(apiKey: apiKey, remoteConfig: remoteConfig); + final fskinInstance = FlutterSkin.singleton; + + final skin = FlutterSkin.toThemeData( + fallbackTheme: ThemeData( + colorScheme: ColorScheme.light(primary: Colors.red), + ), + ); + expect(fskinInstance.apiKey, apiKey); + expect(skin, isA()); + expect(skin?.colorScheme.primary, Colors.red); + }, + ); + + test( + 'Verifying Service returning valid skin data with fallback theme data', + () async { + when( + () => mockSkinService?.fetchData(apiKey), + ).thenAnswer((_) async => ProjectConfig(skin: skinModelMock)); + + var remoteConfig = await FskinRemoteConfig.init( + apiKey: apiKey, + skinService: mockSkinService, + ); + + await FlutterSkin.init(apiKey: apiKey, remoteConfig: remoteConfig); + final fskinInstance = FlutterSkin.singleton; + + final skin = FlutterSkin.toThemeData( + fallbackTheme: ThemeData( + colorScheme: ColorScheme.light(primary: Colors.red), + ), + ); + expect(fskinInstance.apiKey, apiKey); + expect(skin, isA()); + expect(skin?.colorScheme.primary, skinModelMock.colors?.primary); + }, + ); + }); +}