Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions lib/flutter_skin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ class FlutterSkin with WidgetsBindingObserver {
// Private constructor
FlutterSkin._();

static Future<FlutterSkin> init({required String apiKey}) async {
static Future<FlutterSkin> 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');
Expand All @@ -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!;
}
Expand Down Expand Up @@ -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;
}
}
30 changes: 28 additions & 2 deletions lib/remote/fskin_remote_config.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -15,6 +16,8 @@ class FskinRemoteConfig {
late String apiKey;
ProjectConfig? _cachedConfig;

SkinService? skinService;

Stream<ThemeData> get onSkinChanged => _skinController.stream;

ProjectConfig? get projectConfig {
Expand All @@ -39,17 +42,40 @@ class FskinRemoteConfig {
}

// Factory method to initialize and get the singleton instance
static Future<FskinRemoteConfig> init({required String apiKey}) async {
static Future<FskinRemoteConfig> 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!;
}

Future<void> 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;
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 15 additions & 14 deletions test/flutter_skin/flutter_skin_init_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,46 @@ 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';

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 =
'fsk_1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';

setUp(() async {
TestWidgetsFlutterBinding.ensureInitialized();
await FlutterSkin.init(apiKey: apiKey);
FlutterSkin.resetInstance();
await FlutterSkin.init(
apiKey: apiKey,
remoteConfig: MockFskinRemoteConfig(),
);
});
Comment thread
koukibadr marked this conversation as resolved.

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;

expect(instance.apiKey, apiKey);
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);
Expand All @@ -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);
Expand Down
117 changes: 117 additions & 0 deletions test/flutter_skin/flutter_skin_integration_with_service.dart
Original file line number Diff line number Diff line change
@@ -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<ThemeData>());
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<Null>());
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<ThemeData>());
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<ThemeData>());
expect(skin?.colorScheme.primary, skinModelMock.colors?.primary);
},
);
});
}
Loading