mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-05-14 02:19:08 +02:00
[UNTESTED] Fixes notifiers and adds some additional methods. Adds tests.
Changes: // ArcaneEnvironment breaking: context.read<ArcaneEnvironment>() -> ArcaneEnvironment.of(context) breaking: context.read<ArcaneEnvironment>().state -> ArcaneEnvironment.of(context).environment; // Feature flag service added: reset() // Logging service added: registerInterface() added: unregisterInterfaces() added: unregisterAllInterfaces() // ArcaneReactiveTheme fixed: currentMode, dark, light now actually emit new values when changed added: getters for lightTheme, darkTheme, and systemTheme TODO: test systemTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
+1
-8
@@ -6,14 +6,7 @@ import "package:arcane_framework/arcane_framework.dart";
|
|||||||
/// `Arcane` provides access to important services like logging, feature flags,
|
/// `Arcane` provides access to important services like logging, feature flags,
|
||||||
/// authentication, theming, secure storage, and ID management. It also offers a
|
/// authentication, theming, secure storage, and ID management. It also offers a
|
||||||
/// convenient method for logging messages using the integrated logger.
|
/// convenient method for logging messages using the integrated logger.
|
||||||
class Arcane {
|
abstract class Arcane {
|
||||||
Arcane._internal();
|
|
||||||
|
|
||||||
/// Creates a singleton instance of `Arcane`.
|
|
||||||
///
|
|
||||||
/// This factory constructor always returns the same instance of `Arcane`.
|
|
||||||
factory Arcane() => Arcane._internal();
|
|
||||||
|
|
||||||
/// Provides access to the singleton instance of the logger service.
|
/// Provides access to the singleton instance of the logger service.
|
||||||
///
|
///
|
||||||
/// The `ArcaneLogger` is used for logging messages throughout the app.
|
/// The `ArcaneLogger` is used for logging messages throughout the app.
|
||||||
|
|||||||
@@ -1,54 +1,109 @@
|
|||||||
import "package:arcane_framework/arcane_framework.dart";
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
import "package:flutter/widgets.dart";
|
import "package:flutter/widgets.dart";
|
||||||
import "package:flutter_bloc/flutter_bloc.dart";
|
|
||||||
|
|
||||||
/// A `Cubit` that manages the application environment state.
|
/// An `InheritedWidget` that provides access to the application environment.
|
||||||
///
|
///
|
||||||
/// The `ArcaneEnvironment` cubit holds the current environment (`debug` or `normal`)
|
/// The `ArcaneEnvironment` widget holds the current environment (`debug` or `normal`)
|
||||||
/// and provides a method to enable debug mode.
|
/// and allows descendant widgets to access it.
|
||||||
class ArcaneEnvironment extends Cubit<Environment> {
|
class ArcaneEnvironment extends InheritedWidget {
|
||||||
/// Initializes the cubit with the `normal` environment as the default state.
|
/// The current application environment.
|
||||||
ArcaneEnvironment() : super(Environment.normal);
|
final Environment environment;
|
||||||
|
|
||||||
|
final ValueChanged<Environment> onEnvironmentChanged;
|
||||||
|
|
||||||
|
/// Creates an `ArcaneEnvironment` widget.
|
||||||
|
const ArcaneEnvironment({
|
||||||
|
required this.environment,
|
||||||
|
required Widget child,
|
||||||
|
required this.onEnvironmentChanged,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
|
/// Retrieves the `ArcaneEnvironment` instance from the nearest ancestor.
|
||||||
|
///
|
||||||
|
/// Returns `null` if no `ArcaneEnvironment` ancestor is found.
|
||||||
|
static ArcaneEnvironment? maybeOf(BuildContext context) {
|
||||||
|
return context.dependOnInheritedWidgetOfExactType<ArcaneEnvironment>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the `ArcaneEnvironment` instance from the nearest ancestor.
|
||||||
|
///
|
||||||
|
/// Throws a `StateError` if no `ArcaneEnvironment` ancestor is found.
|
||||||
|
static ArcaneEnvironment of(BuildContext context) {
|
||||||
|
final ArcaneEnvironment? result = maybeOf(context);
|
||||||
|
if (result == null) {
|
||||||
|
throw StateError("No ArcaneEnvironment found in context");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(ArcaneEnvironment oldWidget) {
|
||||||
|
return environment != oldWidget.environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enableDebugMode() => onEnvironmentChanged(Environment.debug);
|
||||||
|
void disableDebugMode() => onEnvironmentChanged(Environment.normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `StatefulWidget` that manages and provides the `ArcaneEnvironment`.
|
||||||
|
///
|
||||||
|
/// This widget holds the internal state of the environment and rebuilds
|
||||||
|
/// its descendants when the environment changes.
|
||||||
|
class ArcaneEnvironmentProvider extends StatefulWidget {
|
||||||
|
/// The child widget that will have access to the `ArcaneEnvironment`.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// The initial environment state. Defaults to `Environment.normal`.
|
||||||
|
final Environment environment;
|
||||||
|
|
||||||
|
/// Creates an `ArcaneEnvironmentProvider`.
|
||||||
|
const ArcaneEnvironmentProvider({
|
||||||
|
required this.child,
|
||||||
|
Key? key,
|
||||||
|
this.environment = Environment.normal,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ArcaneEnvironmentProvider> createState() =>
|
||||||
|
_ArcaneEnvironmentProviderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ArcaneEnvironmentProviderState extends State<ArcaneEnvironmentProvider> {
|
||||||
|
late Environment _environment;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_environment = widget.environment;
|
||||||
|
}
|
||||||
|
|
||||||
/// Enables debug mode by setting the environment to `Environment.debug`.
|
/// Enables debug mode by setting the environment to `Environment.debug`.
|
||||||
void enableDebugMode() {
|
void enableDebugMode() {
|
||||||
if (state == Environment.debug) return;
|
if (_environment == Environment.debug) return;
|
||||||
|
setState(() {
|
||||||
emit(Environment.debug);
|
_environment = Environment.debug;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disables debug mode by setting the environment to `Environment.normal`.
|
/// Disables debug mode by setting the environment to `Environment.normal`.
|
||||||
void disableDebugMode() {
|
void disableDebugMode() {
|
||||||
if (state == Environment.normal) return;
|
if (_environment == Environment.normal) return;
|
||||||
|
setState(() {
|
||||||
emit(Environment.normal);
|
_environment = Environment.normal;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// A widget that provides `ArcaneEnvironment` to the widget tree using `BlocProvider`.
|
|
||||||
///
|
|
||||||
/// This widget wraps around a child widget and makes `ArcaneEnvironment` available
|
|
||||||
/// to the rest of the widget tree. It should be used in combination with `BlocProvider`
|
|
||||||
/// from the `flutter_bloc` package.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// ArcaneEnvironmentProvider(
|
|
||||||
/// child: MyApp(),
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
class ArcaneEnvironmentProvider extends StatelessWidget {
|
|
||||||
/// The widget that will be provided with access to the `ArcaneEnvironment`.
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
/// Constructs an `ArcaneEnvironmentProvider` with the given [child].
|
|
||||||
const ArcaneEnvironmentProvider({required this.child, super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return ArcaneEnvironment(
|
||||||
create: (context) => ArcaneEnvironment(),
|
environment: _environment,
|
||||||
child: child,
|
onEnvironmentChanged: (Environment environment) {
|
||||||
|
setState(() {
|
||||||
|
_environment = environment;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,14 +53,12 @@ class ArcaneServiceProvider extends InheritedNotifier {
|
|||||||
/// final provider = ArcaneServiceProvider.of(context);
|
/// final provider = ArcaneServiceProvider.of(context);
|
||||||
/// ```
|
/// ```
|
||||||
static ArcaneServiceProvider of(BuildContext context) {
|
static ArcaneServiceProvider of(BuildContext context) {
|
||||||
final ArcaneServiceProvider? result =
|
try {
|
||||||
context.dependOnInheritedWidgetOfExactType<ArcaneServiceProvider>();
|
return context
|
||||||
|
.dependOnInheritedWidgetOfExactType<ArcaneServiceProvider>()!;
|
||||||
if (result == null) {
|
} catch (e) {
|
||||||
throw Exception("ArcaneServiceProvider not found in context");
|
throw Exception("ArcaneServiceProvider not found in context");
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import "dart:async";
|
|||||||
|
|
||||||
import "package:arcane_framework/arcane_framework.dart";
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
import "package:flutter/widgets.dart";
|
import "package:flutter/widgets.dart";
|
||||||
import "package:flutter_bloc/flutter_bloc.dart";
|
|
||||||
|
|
||||||
part "authentication_enums.dart";
|
part "authentication_enums.dart";
|
||||||
part "authentication_interface.dart";
|
part "authentication_interface.dart";
|
||||||
@@ -59,12 +58,15 @@ class ArcaneAuthenticationService extends ArcaneService {
|
|||||||
Future<String?> get refreshToken =>
|
Future<String?> get refreshToken =>
|
||||||
authInterface?.refreshToken ?? Future.value("");
|
authInterface?.refreshToken ?? Future.value("");
|
||||||
|
|
||||||
|
AuthenticationStatus? _previousModeWhenSettingDebug;
|
||||||
|
|
||||||
/// Removes any registered `ArcaneAuthInterface` and resets all values to
|
/// Removes any registered `ArcaneAuthInterface` and resets all values to
|
||||||
/// default.
|
/// default.
|
||||||
Future<void> reset() async {
|
Future<void> reset() async {
|
||||||
_authInterface = null;
|
_authInterface = null;
|
||||||
_notifier.value = AuthenticationStatus.unauthenticated;
|
_notifier.value = AuthenticationStatus.unauthenticated;
|
||||||
_isSignedIn.value = isAuthenticated;
|
_isSignedIn.value = isAuthenticated;
|
||||||
|
_previousModeWhenSettingDebug = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,17 +87,18 @@ class ArcaneAuthenticationService extends ArcaneService {
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
Future<void> Function()? onDebugModeSet,
|
Future<void> Function()? onDebugModeSet,
|
||||||
}) async {
|
}) async {
|
||||||
ArcaneEnvironment? environment;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
environment = context.read<ArcaneEnvironment>();
|
final ArcaneEnvironment arcaneEnvironment = ArcaneEnvironment.of(context);
|
||||||
final Environment previousEnvironment = environment.state;
|
|
||||||
|
final Environment previousEnvironment = arcaneEnvironment.environment;
|
||||||
|
|
||||||
if (previousEnvironment == Environment.debug) return;
|
if (previousEnvironment == Environment.debug) return;
|
||||||
|
|
||||||
environment.enableDebugMode();
|
_previousModeWhenSettingDebug = status;
|
||||||
|
|
||||||
final Environment currentEnvironment = environment.state;
|
arcaneEnvironment.enableDebugMode();
|
||||||
|
|
||||||
|
final Environment currentEnvironment = arcaneEnvironment.environment;
|
||||||
|
|
||||||
if (previousEnvironment == currentEnvironment) {
|
if (previousEnvironment == currentEnvironment) {
|
||||||
throw Exception("Unable to switch to debug mode.");
|
throw Exception("Unable to switch to debug mode.");
|
||||||
@@ -103,8 +106,8 @@ class ArcaneAuthenticationService extends ArcaneService {
|
|||||||
|
|
||||||
_setStatus(AuthenticationStatus.debug);
|
_setStatus(AuthenticationStatus.debug);
|
||||||
if (onDebugModeSet != null) await onDebugModeSet();
|
if (onDebugModeSet != null) await onDebugModeSet();
|
||||||
} catch (_) {
|
} catch (e) {
|
||||||
throw Exception("No ArcaneEnvironment found in BuildContext");
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,23 +118,24 @@ class ArcaneAuthenticationService extends ArcaneService {
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
Future<void> Function()? onDebugModeUnset,
|
Future<void> Function()? onDebugModeUnset,
|
||||||
}) async {
|
}) async {
|
||||||
ArcaneEnvironment? environment;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
environment = context.read<ArcaneEnvironment>();
|
final ArcaneEnvironment arcaneEnvironment = ArcaneEnvironment.of(context);
|
||||||
final Environment previousEnvironment = environment.state;
|
|
||||||
|
final Environment previousEnvironment = arcaneEnvironment.environment;
|
||||||
|
|
||||||
if (previousEnvironment == Environment.normal) return;
|
if (previousEnvironment == Environment.normal) return;
|
||||||
|
|
||||||
environment.disableDebugMode();
|
arcaneEnvironment.disableDebugMode();
|
||||||
|
|
||||||
final Environment currentEnvironment = environment.state;
|
final Environment currentEnvironment = arcaneEnvironment.environment;
|
||||||
|
|
||||||
if (previousEnvironment == currentEnvironment) {
|
if (previousEnvironment == currentEnvironment) {
|
||||||
throw Exception("Unable to switch to normal mode.");
|
throw Exception("Unable to switch to normal mode.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_setStatus(AuthenticationStatus.debug);
|
_setStatus(
|
||||||
|
_previousModeWhenSettingDebug ?? AuthenticationStatus.unauthenticated,
|
||||||
|
);
|
||||||
if (onDebugModeUnset != null) await onDebugModeUnset();
|
if (onDebugModeUnset != null) await onDebugModeUnset();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
throw Exception("No ArcaneEnvironment found in BuildContext");
|
throw Exception("No ArcaneEnvironment found in BuildContext");
|
||||||
@@ -174,6 +178,8 @@ class ArcaneAuthenticationService extends ArcaneService {
|
|||||||
if (onLoggedOut != null) await onLoggedOut();
|
if (onLoggedOut != null) await onLoggedOut();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_previousModeWhenSettingDebug = null;
|
||||||
|
|
||||||
return loggedOut;
|
return loggedOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,4 +129,16 @@ class ArcaneFeatureFlags extends ArcaneService {
|
|||||||
I._initialized = true;
|
I._initialized = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resets the feature flags to their initial state.
|
||||||
|
///
|
||||||
|
/// This method clears all enabled features, resets notification values,
|
||||||
|
/// marks the flags as uninitialized, and notifies listeners of the changes.
|
||||||
|
void reset() {
|
||||||
|
_enabledFeatures.clear();
|
||||||
|
_notifier.value.clear();
|
||||||
|
|
||||||
|
I._initialized = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import "dart:async";
|
import "dart:async";
|
||||||
|
|
||||||
import "package:arcane_helper_utils/arcane_helper_utils.dart";
|
import "package:arcane_helper_utils/arcane_helper_utils.dart";
|
||||||
|
import "package:flutter/foundation.dart";
|
||||||
|
|
||||||
part "logging_enums.dart";
|
part "logging_enums.dart";
|
||||||
part "logging_interface.dart";
|
part "logging_interface.dart";
|
||||||
@@ -174,9 +175,27 @@ class ArcaneLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a [LoggingInterface] with the [ArcaneLogger]. Due to iOS app
|
/// Registers a [LoggingInterface] with the [ArcaneLogger].
|
||||||
/// tracking permissions, permission to track must first be checked for
|
/// Due to iOS app tracking permissions, permission to track must first be
|
||||||
/// and (optionally) granted before the interface is automatically initialized.
|
/// checked for and (optionally) granted before the interface is automatically
|
||||||
|
/// initialized.
|
||||||
|
///
|
||||||
|
/// Once your [LoggingInterface] has been registered and initialized, logs
|
||||||
|
/// will automatically be sent to the interface.
|
||||||
|
Future<ArcaneLogger> registerInterface(
|
||||||
|
LoggingInterface loggingInterface,
|
||||||
|
) async {
|
||||||
|
if (!initialized) await _init();
|
||||||
|
|
||||||
|
I._interfaces.add(loggingInterface);
|
||||||
|
|
||||||
|
return I;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a `List` of [LoggingInterface] with the [ArcaneLogger].
|
||||||
|
/// Due to iOS app tracking permissions, permission to track must first be
|
||||||
|
/// checked for and (optionally) granted before the interface is automatically
|
||||||
|
/// initialized.
|
||||||
///
|
///
|
||||||
/// Once your [LoggingInterface] has been registered and initialized, logs
|
/// Once your [LoggingInterface] has been registered and initialized, logs
|
||||||
/// will automatically be sent to the interface.
|
/// will automatically be sent to the interface.
|
||||||
@@ -192,15 +211,37 @@ class ArcaneLogger {
|
|||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unregisters a `List` of [LoggingInterface] from the [ArcaneLogger], if
|
||||||
|
/// they were previously registered.
|
||||||
|
Future<ArcaneLogger> unregisterInterfaces(
|
||||||
|
List<LoggingInterface> interfaces,
|
||||||
|
) async {
|
||||||
|
if (!initialized) await _init();
|
||||||
|
|
||||||
|
for (final LoggingInterface i in interfaces) {
|
||||||
|
I._interfaces.remove(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return I;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unregisters all previously registered [LoggingInterface] from the
|
||||||
|
/// [ArcaneLogger], if any were previously registered.
|
||||||
|
Future<ArcaneLogger> unregisterAllInterfaces() async {
|
||||||
|
if (!initialized) await _init();
|
||||||
|
I._interfaces.clear();
|
||||||
|
return I;
|
||||||
|
}
|
||||||
|
|
||||||
/// Initializes all registered [LoggingInterface]s by calling their
|
/// Initializes all registered [LoggingInterface]s by calling their
|
||||||
/// [LoggingInterface.init] methods.
|
/// [LoggingInterface.init] methods.
|
||||||
Future<ArcaneLogger> initializeInterfaces() async {
|
Future<ArcaneLogger> initializeInterfaces() async {
|
||||||
assert(
|
if (!initialized) await _init();
|
||||||
I._interfaces.isNotEmpty,
|
|
||||||
"No logging interfaces have been registered.",
|
if (I._interfaces.isEmptyOrNull) {
|
||||||
);
|
throw Exception("No logging interfaces have been registered.");
|
||||||
|
}
|
||||||
|
|
||||||
if (!I._initialized) await _init();
|
|
||||||
for (final LoggingInterface i in I._interfaces) {
|
for (final LoggingInterface i in I._interfaces) {
|
||||||
if (!i.initialized) await i.init();
|
if (!i.initialized) await i.init();
|
||||||
}
|
}
|
||||||
@@ -245,4 +286,11 @@ class ArcaneLogger {
|
|||||||
|
|
||||||
/// Clears all persistent metadata.
|
/// Clears all persistent metadata.
|
||||||
void clearPersistentMetadata() => _additionalMetadata.clear();
|
void clearPersistentMetadata() => _additionalMetadata.clear();
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
void reset() {
|
||||||
|
I._interfaces.clear();
|
||||||
|
I._initialized = false;
|
||||||
|
I._additionalMetadata.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,29 +18,30 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
|
|
||||||
ArcaneReactiveTheme._internal();
|
ArcaneReactiveTheme._internal();
|
||||||
|
|
||||||
/// Whether the current theme is dark.
|
final ValueNotifier<ThemeMode> _systemThemeNotifier =
|
||||||
bool _isDark = false;
|
ValueNotifier(ThemeMode.light);
|
||||||
|
|
||||||
/// Returns the current theme mode based on `_isDark`.
|
/// Returns the current theme mode based on `_isDark`.
|
||||||
///
|
///
|
||||||
/// If `_isDark` is true, it returns `ThemeMode.dark`, otherwise it returns `ThemeMode.light`.
|
/// If `_isDark` is true, it returns `ThemeMode.dark`, otherwise it returns `ThemeMode.light`.
|
||||||
ThemeMode get currentMode => _isDark ? ThemeMode.dark : ThemeMode.light;
|
ThemeMode get currentMode => I._systemThemeNotifier.value;
|
||||||
|
|
||||||
/// The `ThemeData` for the dark theme.
|
/// The `ThemeData` for the dark theme.
|
||||||
ThemeData _darkTheme = ThemeData.dark();
|
final ValueNotifier<ThemeData> _darkTheme = ValueNotifier(ThemeData.dark());
|
||||||
|
|
||||||
/// The `ThemeData` for the light theme.
|
/// The `ThemeData` for the light theme.
|
||||||
ThemeData _lightTheme = ThemeData.light();
|
final ValueNotifier<ThemeData> _lightTheme = ValueNotifier(ThemeData.light());
|
||||||
|
|
||||||
/// Returns the current dark theme `ThemeData`.
|
/// Returns the current dark theme `ThemeData`.
|
||||||
ThemeData get dark => _darkTheme;
|
ThemeData get dark => _darkTheme.value;
|
||||||
|
ValueNotifier<ThemeData> get darkTheme => I._darkTheme;
|
||||||
|
|
||||||
/// Returns the current light theme `ThemeData`.
|
/// Returns the current light theme `ThemeData`.
|
||||||
ThemeData get light => _lightTheme;
|
ThemeData get light => _lightTheme.value;
|
||||||
|
ValueNotifier<ThemeData> get lightTheme => I._lightTheme;
|
||||||
|
|
||||||
/// A listenable that notifies listeners when the syste theme mode changes.
|
/// A listenable that notifies listeners when the syste theme mode changes.
|
||||||
ValueListenable<ThemeMode> get systemTheme =>
|
ValueListenable<ThemeMode> get systemTheme => I._systemThemeNotifier;
|
||||||
ValueNotifier<ThemeMode>(_isDark ? ThemeMode.dark : ThemeMode.light);
|
|
||||||
|
|
||||||
/// Switches the current theme between light and dark modes.
|
/// Switches the current theme between light and dark modes.
|
||||||
///
|
///
|
||||||
@@ -52,7 +53,9 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
/// ArcaneReactiveTheme.I.switchTheme();
|
/// ArcaneReactiveTheme.I.switchTheme();
|
||||||
/// ```
|
/// ```
|
||||||
ArcaneReactiveTheme switchTheme() {
|
ArcaneReactiveTheme switchTheme() {
|
||||||
_isDark = !_isDark;
|
_systemThemeNotifier.value = _systemThemeNotifier.value == ThemeMode.light
|
||||||
|
? ThemeMode.dark
|
||||||
|
: ThemeMode.light;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
return I;
|
return I;
|
||||||
@@ -87,7 +90,7 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
/// ArcaneReactiveTheme.I.setDarkTheme(customDarkTheme);
|
/// ArcaneReactiveTheme.I.setDarkTheme(customDarkTheme);
|
||||||
/// ```
|
/// ```
|
||||||
ArcaneReactiveTheme setDarkTheme(ThemeData theme) {
|
ArcaneReactiveTheme setDarkTheme(ThemeData theme) {
|
||||||
_darkTheme = theme;
|
_darkTheme.value = theme;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
@@ -102,8 +105,16 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
/// ArcaneReactiveTheme.I.setLightTheme(customLightTheme);
|
/// ArcaneReactiveTheme.I.setLightTheme(customLightTheme);
|
||||||
/// ```
|
/// ```
|
||||||
ArcaneReactiveTheme setLightTheme(ThemeData theme) {
|
ArcaneReactiveTheme setLightTheme(ThemeData theme) {
|
||||||
_lightTheme = theme;
|
_lightTheme.value = theme;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
void reset() {
|
||||||
|
_darkTheme.value = ThemeData.dark();
|
||||||
|
_lightTheme.value = ThemeData.light();
|
||||||
|
_systemThemeNotifier.value = ThemeMode.light;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -12,14 +12,16 @@ environment:
|
|||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
arcane_helper_utils: ^1.2.6
|
arcane_helper_utils: ^1.3.2
|
||||||
collection: ^1.18.0
|
collection: ^1.19.0
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_bloc: ^9.0.0
|
flutter_bloc: ^9.1.0
|
||||||
result_monad: ^2.3.2
|
result_monad: ^2.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
arcane_analysis: ^1.0.3
|
arcane_analysis: ^1.0.3
|
||||||
|
build_runner: ^2.4.1
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
mockito: ^5.4.5
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
|
import "package:flutter_test/flutter_test.dart";
|
||||||
|
import "package:mockito/mockito.dart";
|
||||||
|
|
||||||
|
class MockLoggingInterface extends Mock implements LoggingInterface {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUpAll(() {
|
||||||
|
ArcaneFeatureFlags.I.reset();
|
||||||
|
ArcaneAuthenticationService.I.reset();
|
||||||
|
ArcaneReactiveTheme.I.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Arcane", () {
|
||||||
|
test("services getter returns all core services", () {
|
||||||
|
final services = Arcane.services;
|
||||||
|
expect(services, contains(isA<ArcaneFeatureFlags>()));
|
||||||
|
expect(services, contains(isA<ArcaneAuthenticationService>()));
|
||||||
|
expect(services, contains(isA<ArcaneReactiveTheme>()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_test/flutter_test.dart";
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group("ArcaneServiceProvider", () {
|
||||||
|
late List<ArcaneService> testServices;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
testServices = [
|
||||||
|
MockArcaneService(),
|
||||||
|
AnotherMockService(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("provides services to widget tree", (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ArcaneApp(
|
||||||
|
services: testServices,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final provider = ArcaneServiceProvider.of(context);
|
||||||
|
expect(provider.serviceInstances, equals(testServices));
|
||||||
|
return const SizedBox();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("serviceOfType extension returns correct service",
|
||||||
|
(tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ArcaneApp(
|
||||||
|
services: testServices,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final service = context.serviceOfType<MockArcaneService>();
|
||||||
|
expect(service, isNotNull);
|
||||||
|
expect(service, isA<MockArcaneService>());
|
||||||
|
return const SizedBox();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("serviceOfType returns null for unregistered service",
|
||||||
|
(tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ArcaneApp(
|
||||||
|
services: testServices,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final service = context.serviceOfType<UnregisteredService>();
|
||||||
|
expect(service, isNull);
|
||||||
|
return const SizedBox();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("of() throws when no provider is found", () {
|
||||||
|
final context = MockBuildContext();
|
||||||
|
expect(
|
||||||
|
() => ArcaneServiceProvider.of(context),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("updateShouldNotify always returns true", (tester) async {
|
||||||
|
final provider = ArcaneServiceProvider(
|
||||||
|
serviceInstances: testServices,
|
||||||
|
child: const SizedBox(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
provider.updateShouldNotify(
|
||||||
|
ArcaneServiceProvider(
|
||||||
|
serviceInstances: testServices,
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockArcaneService extends ArcaneService {}
|
||||||
|
|
||||||
|
class AnotherMockService extends ArcaneService {}
|
||||||
|
|
||||||
|
class UnregisteredService extends ArcaneService {}
|
||||||
|
|
||||||
|
class MockBuildContext extends Fake implements BuildContext {}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_test/flutter_test.dart";
|
||||||
|
import "package:mockito/annotations.dart";
|
||||||
|
import "package:mockito/mockito.dart";
|
||||||
|
|
||||||
|
import "authentication_service_test.mocks.dart";
|
||||||
|
|
||||||
|
@GenerateMocks([
|
||||||
|
ArcaneAuthInterface,
|
||||||
|
ArcaneEnvironmentProvider,
|
||||||
|
])
|
||||||
|
void main() {
|
||||||
|
late ArcaneAuthInterface mockInterface;
|
||||||
|
|
||||||
|
group("ArcaneAuthenticationService", () {
|
||||||
|
setUp(() async {
|
||||||
|
// Initialize mocks
|
||||||
|
mockInterface = MockArcaneAuthInterface();
|
||||||
|
|
||||||
|
// Initialize the service
|
||||||
|
await ArcaneAuthenticationService.I.reset();
|
||||||
|
|
||||||
|
// Set up default mock behaviors
|
||||||
|
when(mockInterface.login(input: anyNamed("input"))).thenAnswer(
|
||||||
|
(_) async => Result.ok(null),
|
||||||
|
);
|
||||||
|
when(mockInterface.logout()).thenAnswer(
|
||||||
|
(_) async => Result.ok(null),
|
||||||
|
);
|
||||||
|
when(mockInterface.init()).thenAnswer(
|
||||||
|
(_) async {},
|
||||||
|
);
|
||||||
|
|
||||||
|
await ArcaneAuthenticationService.I.registerInterface(mockInterface);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("login with success", (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ArcaneEnvironmentProvider(
|
||||||
|
environment: Environment.normal,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return Container();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final result = await ArcaneAuthenticationService.I.login(
|
||||||
|
input: {"username": "test"},
|
||||||
|
);
|
||||||
|
expect(result.isSuccess, true);
|
||||||
|
expect(
|
||||||
|
ArcaneAuthenticationService.I.status,
|
||||||
|
equals(AuthenticationStatus.authenticated),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("login with failure", (WidgetTester tester) async {
|
||||||
|
// Reset the mock behavior for this specific test
|
||||||
|
when(mockInterface.login(input: anyNamed("input")))
|
||||||
|
.thenAnswer((_) async => Result.error("error"));
|
||||||
|
|
||||||
|
final result = await ArcaneAuthenticationService.I
|
||||||
|
.login(input: {"username": "test"});
|
||||||
|
expect(result.isFailure, true);
|
||||||
|
expect(
|
||||||
|
ArcaneAuthenticationService.I.status,
|
||||||
|
equals(AuthenticationStatus.unauthenticated),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("logout with success", (WidgetTester tester) async {
|
||||||
|
ArcaneAuthenticationService.I.setAuthenticated();
|
||||||
|
final result = await ArcaneAuthenticationService.I.logOut();
|
||||||
|
expect(result.isSuccess, true);
|
||||||
|
expect(
|
||||||
|
ArcaneAuthenticationService.I.status,
|
||||||
|
equals(AuthenticationStatus.unauthenticated),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("setDebug enables debug mode", (WidgetTester tester) async {
|
||||||
|
late BuildContext capturedContext;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ArcaneEnvironmentProvider(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
capturedContext = context;
|
||||||
|
return Container();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
ArcaneEnvironment.of(capturedContext).enableDebugMode();
|
||||||
|
await tester.pump();
|
||||||
|
expect(
|
||||||
|
ArcaneEnvironment.of(capturedContext).environment,
|
||||||
|
equals(Environment.debug),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("setNormal disables debug mode", (WidgetTester tester) async {
|
||||||
|
late BuildContext capturedContext;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ArcaneEnvironmentProvider(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
capturedContext = context;
|
||||||
|
return Container();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
ArcaneEnvironment.of(capturedContext).enableDebugMode();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
ArcaneEnvironment.of(capturedContext).environment,
|
||||||
|
equals(Environment.debug),
|
||||||
|
);
|
||||||
|
|
||||||
|
ArcaneEnvironment.of(capturedContext).disableDebugMode();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
ArcaneEnvironment.of(capturedContext).environment,
|
||||||
|
equals(Environment.normal),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
// Mocks generated by Mockito 5.4.5 from annotations
|
||||||
|
// in arcane_framework/test/services/authentication/authentication_service_test.dart.
|
||||||
|
// Do not manually edit this file.
|
||||||
|
|
||||||
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'dart:async' as _i5;
|
||||||
|
|
||||||
|
import 'package:arcane_framework/arcane_framework.dart' as _i2;
|
||||||
|
import 'package:flutter/foundation.dart' as _i4;
|
||||||
|
import 'package:flutter/widgets.dart' as _i3;
|
||||||
|
import 'package:mockito/mockito.dart' as _i1;
|
||||||
|
import 'package:mockito/src/dummies.dart' as _i6;
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: avoid_redundant_argument_values
|
||||||
|
// ignore_for_file: avoid_setters_without_getters
|
||||||
|
// ignore_for_file: comment_references
|
||||||
|
// ignore_for_file: deprecated_member_use
|
||||||
|
// ignore_for_file: deprecated_member_use_from_same_package
|
||||||
|
// ignore_for_file: implementation_imports
|
||||||
|
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||||
|
// ignore_for_file: must_be_immutable
|
||||||
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
// ignore_for_file: unnecessary_parenthesis
|
||||||
|
// ignore_for_file: camel_case_types
|
||||||
|
// ignore_for_file: subtype_of_sealed_class
|
||||||
|
|
||||||
|
class _FakeResult_0<T, E> extends _i1.SmartFake implements _i2.Result<T, E> {
|
||||||
|
_FakeResult_0(Object parent, Invocation parentInvocation)
|
||||||
|
: super(parent, parentInvocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeWidget_1 extends _i1.SmartFake implements _i3.Widget {
|
||||||
|
_FakeWidget_1(Object parent, Invocation parentInvocation)
|
||||||
|
: super(parent, parentInvocation);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) =>
|
||||||
|
super.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeState_2<T extends _i3.StatefulWidget> extends _i1.SmartFake
|
||||||
|
implements _i3.State<T> {
|
||||||
|
_FakeState_2(Object parent, Invocation parentInvocation)
|
||||||
|
: super(parent, parentInvocation);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) =>
|
||||||
|
super.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeStatefulElement_3 extends _i1.SmartFake
|
||||||
|
implements _i3.StatefulElement {
|
||||||
|
_FakeStatefulElement_3(Object parent, Invocation parentInvocation)
|
||||||
|
: super(parent, parentInvocation);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) =>
|
||||||
|
super.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeDiagnosticsNode_4 extends _i1.SmartFake
|
||||||
|
implements _i3.DiagnosticsNode {
|
||||||
|
_FakeDiagnosticsNode_4(Object parent, Invocation parentInvocation)
|
||||||
|
: super(parent, parentInvocation);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({
|
||||||
|
_i4.TextTreeConfiguration? parentConfiguration,
|
||||||
|
_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info,
|
||||||
|
}) => super.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A class which mocks [ArcaneAuthInterface].
|
||||||
|
///
|
||||||
|
/// See the documentation for Mockito's code generation for more information.
|
||||||
|
class MockArcaneAuthInterface extends _i1.Mock
|
||||||
|
implements _i2.ArcaneAuthInterface {
|
||||||
|
MockArcaneAuthInterface() {
|
||||||
|
_i1.throwOnMissingStub(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i5.Future<bool> get isSignedIn =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.getter(#isSignedIn),
|
||||||
|
returnValue: _i5.Future<bool>.value(false),
|
||||||
|
)
|
||||||
|
as _i5.Future<bool>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i5.Future<void> init() =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#init, []),
|
||||||
|
returnValue: _i5.Future<void>.value(),
|
||||||
|
returnValueForMissingStub: _i5.Future<void>.value(),
|
||||||
|
)
|
||||||
|
as _i5.Future<void>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i5.Future<_i2.Result<void, String>> logout() =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#logout, []),
|
||||||
|
returnValue: _i5.Future<_i2.Result<void, String>>.value(
|
||||||
|
_FakeResult_0<void, String>(this, Invocation.method(#logout, [])),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
as _i5.Future<_i2.Result<void, String>>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i5.Future<_i2.Result<void, String>> login<T>({
|
||||||
|
T? input,
|
||||||
|
_i5.Future<void> Function()? onLoggedIn,
|
||||||
|
}) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#login, [], {
|
||||||
|
#input: input,
|
||||||
|
#onLoggedIn: onLoggedIn,
|
||||||
|
}),
|
||||||
|
returnValue: _i5.Future<_i2.Result<void, String>>.value(
|
||||||
|
_FakeResult_0<void, String>(
|
||||||
|
this,
|
||||||
|
Invocation.method(#login, [], {
|
||||||
|
#input: input,
|
||||||
|
#onLoggedIn: onLoggedIn,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
as _i5.Future<_i2.Result<void, String>>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A class which mocks [ArcaneEnvironmentProvider].
|
||||||
|
///
|
||||||
|
/// See the documentation for Mockito's code generation for more information.
|
||||||
|
class MockArcaneEnvironmentProvider extends _i1.Mock
|
||||||
|
implements _i2.ArcaneEnvironmentProvider {
|
||||||
|
MockArcaneEnvironmentProvider() {
|
||||||
|
_i1.throwOnMissingStub(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i3.Widget get child =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.getter(#child),
|
||||||
|
returnValue: _FakeWidget_1(this, Invocation.getter(#child)),
|
||||||
|
)
|
||||||
|
as _i3.Widget);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i2.Environment get environment =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.getter(#environment),
|
||||||
|
returnValue: _i2.Environment.debug,
|
||||||
|
)
|
||||||
|
as _i2.Environment);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i3.State<_i2.ArcaneEnvironmentProvider> createState() =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#createState, []),
|
||||||
|
returnValue: _FakeState_2<_i2.ArcaneEnvironmentProvider>(
|
||||||
|
this,
|
||||||
|
Invocation.method(#createState, []),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
as _i3.State<_i2.ArcaneEnvironmentProvider>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i3.StatefulElement createElement() =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#createElement, []),
|
||||||
|
returnValue: _FakeStatefulElement_3(
|
||||||
|
this,
|
||||||
|
Invocation.method(#createElement, []),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
as _i3.StatefulElement);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toStringShort() =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#toStringShort, []),
|
||||||
|
returnValue: _i6.dummyValue<String>(
|
||||||
|
this,
|
||||||
|
Invocation.method(#toStringShort, []),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
as String);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(_i4.DiagnosticPropertiesBuilder? properties) =>
|
||||||
|
super.noSuchMethod(
|
||||||
|
Invocation.method(#debugFillProperties, [properties]),
|
||||||
|
returnValueForMissingStub: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toStringShallow({
|
||||||
|
String? joiner = ', ',
|
||||||
|
_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug,
|
||||||
|
}) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#toStringShallow, [], {
|
||||||
|
#joiner: joiner,
|
||||||
|
#minLevel: minLevel,
|
||||||
|
}),
|
||||||
|
returnValue: _i6.dummyValue<String>(
|
||||||
|
this,
|
||||||
|
Invocation.method(#toStringShallow, [], {
|
||||||
|
#joiner: joiner,
|
||||||
|
#minLevel: minLevel,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
as String);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toStringDeep({
|
||||||
|
String? prefixLineOne = '',
|
||||||
|
String? prefixOtherLines,
|
||||||
|
_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug,
|
||||||
|
int? wrapWidth = 65,
|
||||||
|
}) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#toStringDeep, [], {
|
||||||
|
#prefixLineOne: prefixLineOne,
|
||||||
|
#prefixOtherLines: prefixOtherLines,
|
||||||
|
#minLevel: minLevel,
|
||||||
|
#wrapWidth: wrapWidth,
|
||||||
|
}),
|
||||||
|
returnValue: _i6.dummyValue<String>(
|
||||||
|
this,
|
||||||
|
Invocation.method(#toStringDeep, [], {
|
||||||
|
#prefixLineOne: prefixLineOne,
|
||||||
|
#prefixOtherLines: prefixOtherLines,
|
||||||
|
#minLevel: minLevel,
|
||||||
|
#wrapWidth: wrapWidth,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
as String);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i3.DiagnosticsNode toDiagnosticsNode({
|
||||||
|
String? name,
|
||||||
|
_i4.DiagnosticsTreeStyle? style,
|
||||||
|
}) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#toDiagnosticsNode, [], {
|
||||||
|
#name: name,
|
||||||
|
#style: style,
|
||||||
|
}),
|
||||||
|
returnValue: _FakeDiagnosticsNode_4(
|
||||||
|
this,
|
||||||
|
Invocation.method(#toDiagnosticsNode, [], {
|
||||||
|
#name: name,
|
||||||
|
#style: style,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
as _i3.DiagnosticsNode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<_i3.DiagnosticsNode> debugDescribeChildren() =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#debugDescribeChildren, []),
|
||||||
|
returnValue: <_i3.DiagnosticsNode>[],
|
||||||
|
)
|
||||||
|
as List<_i3.DiagnosticsNode>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) =>
|
||||||
|
super.toString();
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
|
import "package:flutter_test/flutter_test.dart";
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group("ArcaneFeatureFlags", () {
|
||||||
|
late ArcaneFeatureFlags featureFlags;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
featureFlags = ArcaneFeatureFlags.I;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("singleton instance is consistent", () {
|
||||||
|
expect(identical(ArcaneFeatureFlags.I, featureFlags), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
group("feature management", () {
|
||||||
|
setUp(() {
|
||||||
|
Arcane.features.reset();
|
||||||
|
});
|
||||||
|
test("enableFeature adds feature to enabled list", () {
|
||||||
|
featureFlags.enableFeature(MockFeature.test);
|
||||||
|
expect(featureFlags.enabledFeatures, contains(MockFeature.test));
|
||||||
|
expect(featureFlags.isEnabled(MockFeature.test), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("disableFeature removes feature from enabled list", () {
|
||||||
|
featureFlags.enableFeature(MockFeature.test);
|
||||||
|
featureFlags.disableFeature(MockFeature.test);
|
||||||
|
expect(featureFlags.enabledFeatures, isNot(contains(MockFeature.test)));
|
||||||
|
expect(featureFlags.isDisabled(MockFeature.test), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("enabling already enabled feature has no effect", () {
|
||||||
|
featureFlags.enableFeature(MockFeature.test);
|
||||||
|
final initialCount = featureFlags.enabledFeatures.length;
|
||||||
|
featureFlags.enableFeature(MockFeature.test);
|
||||||
|
expect(featureFlags.enabledFeatures.length, equals(initialCount));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("disabling already disabled feature has no effect", () {
|
||||||
|
final initialCount = featureFlags.enabledFeatures.length;
|
||||||
|
featureFlags.disableFeature(MockFeature.test);
|
||||||
|
expect(featureFlags.enabledFeatures.length, equals(initialCount));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("notifications", () {
|
||||||
|
test("enableFeature notifies listeners", () {
|
||||||
|
var notified = false;
|
||||||
|
featureFlags.addListener(() => notified = true);
|
||||||
|
featureFlags.enableFeature(MockFeature.test);
|
||||||
|
expect(notified, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("disableFeature notifies listeners", () {
|
||||||
|
featureFlags.enableFeature(MockFeature.test);
|
||||||
|
var notified = false;
|
||||||
|
featureFlags.addListener(() => notified = true);
|
||||||
|
featureFlags.disableFeature(MockFeature.test);
|
||||||
|
expect(notified, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MockFeature {
|
||||||
|
test,
|
||||||
|
another,
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
|
import "package:flutter_test/flutter_test.dart";
|
||||||
|
import "package:mockito/annotations.dart";
|
||||||
|
import "package:mockito/mockito.dart";
|
||||||
|
|
||||||
|
import "logging_service_test.mocks.dart";
|
||||||
|
|
||||||
|
class MyOtherLoggingInterface extends Mock implements MockLoggingInterface {}
|
||||||
|
|
||||||
|
@GenerateNiceMocks([
|
||||||
|
MockSpec<LoggingInterface>(
|
||||||
|
onMissingStub: OnMissingStub.returnDefault,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
void main() {
|
||||||
|
final LoggingInterface myInterface = MockLoggingInterface();
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
Arcane.logger.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
group("ArcaneLogger", () {
|
||||||
|
group("interface management", () {
|
||||||
|
test("registerInterfaces adds interfaces correctly", () async {
|
||||||
|
await Arcane.logger.registerInterface(myInterface);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Arcane.logger.interfaces,
|
||||||
|
contains(isA<LoggingInterface>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("registering an interface doesn't initialize it", () async {
|
||||||
|
await Arcane.logger.registerInterface(myInterface);
|
||||||
|
|
||||||
|
expect(Arcane.logger.interfaces.first, isA<LoggingInterface>());
|
||||||
|
|
||||||
|
expect(myInterface.initialized, false);
|
||||||
|
verifyNever(myInterface.init());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("registering an interface initializes the logger", () async {
|
||||||
|
expect(Arcane.logger.initialized, false);
|
||||||
|
|
||||||
|
await Arcane.logger.registerInterface(myInterface);
|
||||||
|
|
||||||
|
expect(Arcane.logger.initialized, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("interfaces can be initialized through the logger", () async {
|
||||||
|
await Arcane.logger.registerInterface(myInterface);
|
||||||
|
|
||||||
|
expect(Arcane.logger.interfaces.first.initialized, false);
|
||||||
|
|
||||||
|
await Arcane.logger.initializeInterfaces();
|
||||||
|
|
||||||
|
verify(Arcane.logger.interfaces.first.init()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("multiple interfaces can be registered", () async {
|
||||||
|
await Arcane.logger.registerInterfaces([
|
||||||
|
MockLoggingInterface(),
|
||||||
|
MyOtherLoggingInterface(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Arcane.logger.interfaces,
|
||||||
|
contains(isA<MockLoggingInterface>()),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
Arcane.logger.interfaces,
|
||||||
|
contains(isA<MyOtherLoggingInterface>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("persistent metadata", () {
|
||||||
|
test("addPersistentMetadata adds metadata correctly", () {
|
||||||
|
Arcane.logger.addPersistentMetadata({"test": "value"});
|
||||||
|
expect(Arcane.logger.additionalMetadata["test"], equals("value"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removePersistentMetadata removes specific key", () {
|
||||||
|
Arcane.logger.addPersistentMetadata({"test": "value", "keep": "this"});
|
||||||
|
Arcane.logger.removePersistentMetadata("test");
|
||||||
|
expect(Arcane.logger.additionalMetadata.containsKey("test"), false);
|
||||||
|
expect(Arcane.logger.additionalMetadata["keep"], equals("this"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clearPersistentMetadata removes all metadata", () {
|
||||||
|
Arcane.logger
|
||||||
|
.addPersistentMetadata({"test": "value", "another": "value"});
|
||||||
|
Arcane.logger.clearPersistentMetadata();
|
||||||
|
expect(Arcane.logger.additionalMetadata.isEmpty, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("logging messages", () {
|
||||||
|
const String logMessage = "Test";
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await Arcane.logger.registerInterface(myInterface);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logging a basic message works", () async {
|
||||||
|
Arcane.log(logMessage);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
myInterface.log(
|
||||||
|
logMessage,
|
||||||
|
metadata: anyNamed("metadata"),
|
||||||
|
level: anyNamed("level"),
|
||||||
|
stackTrace: anyNamed("stackTrace"),
|
||||||
|
extra: anyNamed("extra"),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logging at a different level works", () async {
|
||||||
|
Arcane.log(
|
||||||
|
logMessage,
|
||||||
|
level: Level.info,
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
myInterface.log(
|
||||||
|
logMessage,
|
||||||
|
metadata: anyNamed("metadata"),
|
||||||
|
level: Level.info,
|
||||||
|
stackTrace: anyNamed("stackTrace"),
|
||||||
|
extra: anyNamed("extra"),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
|
||||||
|
Arcane.log(
|
||||||
|
logMessage,
|
||||||
|
level: Level.warning,
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
myInterface.log(
|
||||||
|
logMessage,
|
||||||
|
metadata: anyNamed("metadata"),
|
||||||
|
level: Level.warning,
|
||||||
|
stackTrace: anyNamed("stackTrace"),
|
||||||
|
extra: anyNamed("extra"),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logging a stacktrace works", () async {
|
||||||
|
final stackTrace = StackTrace.current;
|
||||||
|
Arcane.log(logMessage, stackTrace: stackTrace);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
myInterface.log(
|
||||||
|
logMessage,
|
||||||
|
metadata: anyNamed("metadata"),
|
||||||
|
level: anyNamed("level"),
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
extra: anyNamed("extra"),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logging an extra object works", () async {
|
||||||
|
const bool extraObject = true;
|
||||||
|
Arcane.log(
|
||||||
|
logMessage,
|
||||||
|
extra: extraObject,
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
myInterface.log(
|
||||||
|
logMessage,
|
||||||
|
metadata: anyNamed("metadata"),
|
||||||
|
level: anyNamed("level"),
|
||||||
|
stackTrace: anyNamed("stackTrace"),
|
||||||
|
extra: extraObject,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logging metadata works", () async {
|
||||||
|
final Map<String, String> metadata = {"test": "value"};
|
||||||
|
Arcane.log(
|
||||||
|
logMessage,
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
myInterface.log(
|
||||||
|
logMessage,
|
||||||
|
metadata: metadata,
|
||||||
|
level: anyNamed("level"),
|
||||||
|
stackTrace: anyNamed("stackTrace"),
|
||||||
|
extra: anyNamed("extra"),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Mocks generated by Mockito 5.4.5 from annotations
|
||||||
|
// in arcane_framework/test/services/logging/logging_service_test.dart.
|
||||||
|
// Do not manually edit this file.
|
||||||
|
|
||||||
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'dart:async' as _i3;
|
||||||
|
|
||||||
|
import 'package:arcane_framework/src/services/logging/logging_service.dart'
|
||||||
|
as _i2;
|
||||||
|
import 'package:mockito/mockito.dart' as _i1;
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: avoid_redundant_argument_values
|
||||||
|
// ignore_for_file: avoid_setters_without_getters
|
||||||
|
// ignore_for_file: comment_references
|
||||||
|
// ignore_for_file: deprecated_member_use
|
||||||
|
// ignore_for_file: deprecated_member_use_from_same_package
|
||||||
|
// ignore_for_file: implementation_imports
|
||||||
|
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||||
|
// ignore_for_file: must_be_immutable
|
||||||
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
// ignore_for_file: unnecessary_parenthesis
|
||||||
|
// ignore_for_file: camel_case_types
|
||||||
|
// ignore_for_file: subtype_of_sealed_class
|
||||||
|
|
||||||
|
/// A class which mocks [LoggingInterface].
|
||||||
|
///
|
||||||
|
/// See the documentation for Mockito's code generation for more information.
|
||||||
|
class MockLoggingInterface extends _i1.Mock implements _i2.LoggingInterface {
|
||||||
|
@override
|
||||||
|
bool get initialized =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.getter(#initialized),
|
||||||
|
returnValue: false,
|
||||||
|
returnValueForMissingStub: false,
|
||||||
|
)
|
||||||
|
as bool);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i3.Future<_i2.LoggingInterface?> init() =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#init, []),
|
||||||
|
returnValue: _i3.Future<_i2.LoggingInterface?>.value(),
|
||||||
|
returnValueForMissingStub:
|
||||||
|
_i3.Future<_i2.LoggingInterface?>.value(),
|
||||||
|
)
|
||||||
|
as _i3.Future<_i2.LoggingInterface?>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void log(
|
||||||
|
String? message, {
|
||||||
|
Map<String, dynamic>? metadata,
|
||||||
|
_i2.Level? level,
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
Object? extra,
|
||||||
|
}) => super.noSuchMethod(
|
||||||
|
Invocation.method(
|
||||||
|
#log,
|
||||||
|
[message],
|
||||||
|
{
|
||||||
|
#metadata: metadata,
|
||||||
|
#level: level,
|
||||||
|
#stackTrace: stackTrace,
|
||||||
|
#extra: extra,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
returnValueForMissingStub: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_test/flutter_test.dart";
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group("ArcaneReactiveTheme", () {
|
||||||
|
late ArcaneReactiveTheme theme;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
theme = ArcaneReactiveTheme.I;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("singleton instance is consistent", () {
|
||||||
|
expect(identical(ArcaneReactiveTheme.I, theme), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
group("theme mode", () {
|
||||||
|
test("initial mode is light", () {
|
||||||
|
expect(theme.currentMode, equals(ThemeMode.light));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("switchTheme toggles between light and dark", () {
|
||||||
|
expect(theme.currentMode, equals(ThemeMode.light));
|
||||||
|
theme.switchTheme();
|
||||||
|
expect(theme.currentMode, equals(ThemeMode.dark));
|
||||||
|
theme.switchTheme();
|
||||||
|
expect(theme.currentMode, equals(ThemeMode.light));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("switching theme notifies listeners", () {
|
||||||
|
var notified = false;
|
||||||
|
theme.addListener(() => notified = true);
|
||||||
|
theme.switchTheme();
|
||||||
|
expect(notified, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("theme customization", () {
|
||||||
|
test("setDarkTheme updates dark theme", () {
|
||||||
|
final customTheme = ThemeData.dark().copyWith(
|
||||||
|
primaryColor: Colors.purple,
|
||||||
|
);
|
||||||
|
theme.setDarkTheme(customTheme);
|
||||||
|
expect(theme.dark.primaryColor, equals(Colors.purple));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("setLightTheme updates light theme", () {
|
||||||
|
final customTheme = ThemeData.light().copyWith(
|
||||||
|
primaryColor: Colors.orange,
|
||||||
|
);
|
||||||
|
theme.setLightTheme(customTheme);
|
||||||
|
expect(theme.light.primaryColor, equals(Colors.orange));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("theme updates notify listeners", () {
|
||||||
|
bool darkNotified = false;
|
||||||
|
bool lightNotified = false;
|
||||||
|
ThemeMode currentTheme = ThemeMode.system;
|
||||||
|
|
||||||
|
theme.darkTheme.addListener(() {
|
||||||
|
darkNotified = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
theme.lightTheme.addListener(() {
|
||||||
|
lightNotified = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
theme.addListener(() {
|
||||||
|
currentTheme = theme.currentMode;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(currentTheme, ThemeMode.system);
|
||||||
|
|
||||||
|
theme.setDarkTheme(ThemeData.dark());
|
||||||
|
theme.setLightTheme(ThemeData.light());
|
||||||
|
|
||||||
|
expect(darkNotified, true);
|
||||||
|
expect(lightNotified, true);
|
||||||
|
|
||||||
|
theme.switchTheme();
|
||||||
|
expect(currentTheme, ThemeMode.light);
|
||||||
|
|
||||||
|
theme.switchTheme();
|
||||||
|
expect(currentTheme, ThemeMode.dark);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("system theme following", () {
|
||||||
|
setUp(() {
|
||||||
|
Arcane.theme.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("followSystemTheme updates theme based on context brightness",
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
// Create widgets with different brightness contexts
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MediaQuery(
|
||||||
|
data: MediaQueryData(platformBrightness: Brightness.light),
|
||||||
|
child: SizedBox(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final BuildContext lightContext = tester.element(find.byType(SizedBox));
|
||||||
|
theme.followSystemTheme(lightContext);
|
||||||
|
|
||||||
|
expect(theme.currentMode, equals(ThemeMode.light));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MediaQuery(
|
||||||
|
data: MediaQueryData(platformBrightness: Brightness.dark),
|
||||||
|
child: SizedBox(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final BuildContext darkContext = tester.element(find.byType(SizedBox));
|
||||||
|
|
||||||
|
theme.followSystemTheme(darkContext);
|
||||||
|
expect(theme.currentMode, equals(ThemeMode.dark));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("followSystemTheme only switches when needed",
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MediaQuery(
|
||||||
|
data: MediaQueryData(platformBrightness: Brightness.light),
|
||||||
|
child: SizedBox(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final BuildContext lightContext = tester.element(find.byType(SizedBox));
|
||||||
|
|
||||||
|
int switchCount = 0;
|
||||||
|
theme.addListener(() => switchCount++);
|
||||||
|
|
||||||
|
// Already light, shouldn't switch
|
||||||
|
theme.followSystemTheme(lightContext);
|
||||||
|
expect(switchCount, equals(0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user