Fixes broken tests

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2025-04-28 13:56:04 +02:00
parent 23f0387389
commit e402308f7b
7 changed files with 154 additions and 94 deletions
+44 -16
View File
@@ -59,25 +59,10 @@ class MainApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: Arcane.theme.light, theme: Arcane.theme.light,
darkTheme: Arcane.theme.dark, darkTheme: Arcane.theme.dark,
themeMode: themeMode: Arcane.theme.currentModeOf(context),
ArcaneTheme.of(context)?.themeMode ?? Arcane.theme.currentTheme,
home: Scaffold( home: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Arcane Framework Example"), title: const Text("Arcane Framework Example"),
actions: [
IconButton(
icon: const Icon(Icons.settings_system_daydream),
onPressed: () {
Arcane.theme.followSystemTheme(context);
},
),
IconButton(
icon: const Icon(Icons.contrast),
onPressed: () {
Arcane.theme.switchTheme();
},
),
],
), ),
body: const HomeScreen(), body: const HomeScreen(),
), ),
@@ -101,6 +86,49 @@ class _HomeScreenState extends State<HomeScreen> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Column(
children: [
Checkbox(
value: Arcane.theme.isFollowingSystemTheme,
onChanged: (value) {
if (value == true) {
Arcane.theme.followSystemTheme(context);
} else {
Arcane.theme.switchTheme(
themeMode: Arcane.theme.systemTheme,
);
}
},
),
const Text("Use system theme"),
],
),
Switch(
value: Arcane.theme.currentTheme == ThemeMode.dark,
thumbIcon: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const Icon(Icons.dark_mode);
}
return const Icon(Icons.light_mode);
}),
onChanged: (_) {
Arcane.theme.switchTheme();
},
),
Text(
"The current theme mode is ${context.themeMode.name} and\n"
"is ${Arcane.theme.isFollowingSystemTheme ? "" : "not "}"
"following the system theme.",
),
],
),
),
),
Text( Text(
"Authentication status: ${Arcane.auth.status.name}", "Authentication status: ${Arcane.auth.status.name}",
), ),
+3 -16
View File
@@ -1,4 +1,5 @@
import "package:arcane_framework/arcane_framework.dart"; import "package:arcane_framework/arcane_framework.dart";
import "package:arcane_framework/src/services/reactive_theme/reactive_theme_switcher.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
/// A root widget for an Arcane-powered application. /// A root widget for an Arcane-powered application.
@@ -54,22 +55,9 @@ class _ArcaneAppState extends State<ArcaneApp> with WidgetsBindingObserver {
return ArcaneEnvironmentProvider( return ArcaneEnvironmentProvider(
child: ArcaneServiceProvider( child: ArcaneServiceProvider(
serviceInstances: widget.services, serviceInstances: widget.services,
child: Builder( child: ArcaneThemeSwitcher(
key: _appKey, key: _appKey,
builder: (BuildContext currentContext) {
return StreamBuilder<ThemeMode>(
stream: ArcaneReactiveTheme.I.currentThemeStream,
initialData: ArcaneReactiveTheme.I.currentTheme,
builder: (context, AsyncSnapshot<ThemeMode> snapshot) {
final ThemeMode themeMode = snapshot.data ?? ThemeMode.light;
return ArcaneTheme(
themeMode: themeMode,
child: widget.child, child: widget.child,
);
},
);
},
), ),
), ),
); );
@@ -95,10 +83,9 @@ class _ArcaneAppState extends State<ArcaneApp> with WidgetsBindingObserver {
// and use it to check the system theme // and use it to check the system theme
if (mounted && _appKey.currentContext != null) { if (mounted && _appKey.currentContext != null) {
// Use the current context from the key to check system theme // Use the current context from the key to check system theme
final BuildContext currentContext = _appKey.currentContext!;
if (ArcaneReactiveTheme.I.isFollowingSystemTheme) { if (ArcaneReactiveTheme.I.isFollowingSystemTheme) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
ArcaneReactiveTheme.I.checkSystemTheme(currentContext); ArcaneReactiveTheme.I.followSystemTheme(context);
}); });
} }
} }
@@ -19,3 +19,11 @@ extension DarkMode on BuildContext {
return brightness == Brightness.dark; return brightness == Brightness.dark;
} }
} }
extension ArcaneThemeContext on BuildContext {
/// Get the current theme mode from the nearest ArcaneThemeInherited widget
ThemeMode get themeMode {
return ArcaneTheme.of(this)?.themeMode ??
ArcaneReactiveTheme.I.currentTheme;
}
}
@@ -27,12 +27,16 @@ class ArcaneReactiveTheme extends ArcaneService {
/// Whether the theme service is currently following the system theme. /// Whether the theme service is currently following the system theme.
/// ///
/// When true, the theme will automatically switch between light and dark /// When `true`, the theme will automatically switch between light and dark
/// based on the system's brightness setting. /// based on the system's brightness setting.
bool get isFollowingSystemTheme => _followingSystemTheme; bool get isFollowingSystemTheme => _followingSystemTheme;
final ValueNotifier<ThemeMode> _systemThemeNotifier = final StreamController<ThemeMode> _systemStreamController =
ValueNotifier(ThemeMode.system); StreamController<ThemeMode>.broadcast(
onCancel: () {
I._systemStreamController.close();
},
);
final StreamController<ThemeMode> _themeStreamController = final StreamController<ThemeMode> _themeStreamController =
StreamController<ThemeMode>.broadcast( StreamController<ThemeMode>.broadcast(
@@ -41,17 +45,20 @@ class ArcaneReactiveTheme extends ArcaneService {
}, },
); );
/// Stream of theme mode changes that can be listened to for reactive UI updates. /// Stream of `ThemeMode` changes that can be listened to for reactive UI updates.
Stream<ThemeMode> get currentThemeStream => I._themeStreamController.stream; Stream<ThemeMode> get themeChanges => I._themeStreamController.stream;
/// Returns the `ThemeData` corresponding to the current system theme
ThemeMode get systemTheme => _currentSystemTheme;
/// Tracks the current system theme mode
ThemeMode _currentSystemTheme = ThemeMode.system;
ThemeMode _currentTheme = ThemeMode.light; ThemeMode _currentTheme = ThemeMode.light;
/// The currently active theme mode (light or dark). /// The currently active theme mode (light or dark).
ThemeMode get currentTheme => _currentTheme; ThemeMode get currentTheme => _currentTheme;
/// A listenable that notifies listeners when the system theme mode changes.
ThemeMode get systemTheme => I._systemThemeNotifier.value;
/// The `ThemeData` for the dark theme. /// The `ThemeData` for the dark theme.
final ValueNotifier<ThemeData> _darkTheme = ValueNotifier(ThemeData.dark()); final ValueNotifier<ThemeData> _darkTheme = ValueNotifier(ThemeData.dark());
@@ -70,14 +77,22 @@ class ArcaneReactiveTheme extends ArcaneService {
/// ValueNotifier for the light theme that can be observed for changes. /// ValueNotifier for the light theme that can be observed for changes.
ValueNotifier<ThemeData> get lightTheme => I._lightTheme; ValueNotifier<ThemeData> get lightTheme => I._lightTheme;
/// Returns the current `ThemeMode` being used by `ArcaneReactiveTheme`.
/// Will automatically update when the theme changes.
ThemeMode currentModeOf(BuildContext context) => context.themeMode;
/// Switches the current theme between light and dark modes. /// Switches the current theme between light and dark modes.
/// ///
/// If the theme is currently light, it switches to dark, and vice versa. It also /// If the theme is currently light, it switches to dark, and vice versa. It
/// notifies listeners to update the UI accordingly. /// also notifies listeners to update the UI accordingly.
/// ///
/// Example: /// Example:
/// ```dart /// ```dart
/// ArcaneReactiveTheme.I.switchTheme(); /// ArcaneReactiveTheme.I.switchTheme();
/// // or
/// ArcaneReactiveTheme.I.switchTheme(themeMode: ThemeMode.dark);
/// // or
/// Arcane.theme.switchTheme(themeMode: ThemeMode.light);
/// ``` /// ```
ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) { ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) {
_followingSystemTheme = false; _followingSystemTheme = false;
@@ -103,40 +118,25 @@ class ArcaneReactiveTheme extends ArcaneService {
/// Example: /// Example:
/// ```dart /// ```dart
/// ArcaneReactiveTheme.I.followSystemTheme(context); /// ArcaneReactiveTheme.I.followSystemTheme(context);
/// // or
/// Arcane.theme.followSystemTheme(context);
/// ``` /// ```
ArcaneReactiveTheme followSystemTheme(BuildContext context) { ArcaneReactiveTheme followSystemTheme(BuildContext context) {
_followingSystemTheme = true; _followingSystemTheme = true;
// Always check the system theme when this method is called _currentSystemTheme = context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
checkSystemTheme(context); _systemStreamController.add(_currentSystemTheme);
_updateTheme(_currentSystemTheme);
notifyListeners();
return I; return I;
} }
/// Check and apply the system theme if we're following it
///
/// This is called automatically when the system brightness changes if
/// [followSystemTheme] has been enabled.
void checkSystemTheme(BuildContext context) {
if (!_followingSystemTheme) return;
final Brightness systemBrightness =
MediaQuery.platformBrightnessOf(context);
final ThemeMode systemMode =
systemBrightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light;
// Only update and notify if the theme actually changed
if (systemMode != _currentTheme) {
_updateTheme(systemMode);
notifyListeners();
}
}
/// Sets a custom `ThemeData` for the dark theme. /// Sets a custom `ThemeData` for the dark theme.
/// ///
/// This allows you to customize the dark theme and notify listeners to apply the /// This allows you to customize the dark theme and notify listeners to apply
/// changes immediately. /// the changes immediately.
/// ///
/// Example: /// Example:
/// ```dart /// ```dart
@@ -150,8 +150,8 @@ class ArcaneReactiveTheme extends ArcaneService {
/// Sets a custom `ThemeData` for the light theme. /// Sets a custom `ThemeData` for the light theme.
/// ///
/// This allows you to customize the light theme and notify listeners to apply the /// This allows you to customize the light theme and notify listeners to apply
/// changes immediately. /// the changes immediately.
/// ///
/// Example: /// Example:
/// ```dart /// ```dart
@@ -0,0 +1,47 @@
import "dart:async";
import "package:arcane_framework/arcane_framework.dart";
import "package:flutter/material.dart";
class ArcaneThemeSwitcher extends StatefulWidget {
final Widget child;
const ArcaneThemeSwitcher({
required this.child,
Key? key,
}) : super(key: key);
@override
State<ArcaneThemeSwitcher> createState() => _ArcaneThemeSwitcherState();
}
class _ArcaneThemeSwitcherState extends State<ArcaneThemeSwitcher> {
late final StreamSubscription<ThemeMode> _themeModeSubscription;
ThemeMode _currentThemeMode = ArcaneReactiveTheme.I.currentTheme;
@override
void initState() {
super.initState();
_themeModeSubscription =
ArcaneReactiveTheme.I.themeChanges.listen((ThemeMode newMode) {
if (mounted)
setState(() {
_currentThemeMode = newMode;
});
});
}
@override
void dispose() {
_themeModeSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ArcaneTheme(
themeMode: _currentThemeMode,
child: widget.child,
);
}
}
@@ -1,3 +1,5 @@
import "dart:async";
import "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart"; import "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
@@ -16,12 +18,12 @@ class ArcaneTheme extends InheritedWidget {
@override @override
bool updateShouldNotify(ArcaneTheme oldWidget) { bool updateShouldNotify(ArcaneTheme oldWidget) {
return oldWidget.themeMode != themeMode; return themeMode != oldWidget.themeMode;
} }
static ArcaneReactiveTheme get service => ArcaneReactiveTheme.I; static ArcaneReactiveTheme get service => ArcaneReactiveTheme.I;
static bool get isFollowingSystemTheme => service.isFollowingSystemTheme; static bool get isFollowingSystemTheme => service.isFollowingSystemTheme;
static Stream<ThemeMode> get currentThemeStream => service.currentThemeStream; static Stream<ThemeMode> get themeChanges => service.themeChanges;
static ThemeMode get currentTheme => service.currentTheme; static ThemeMode get currentTheme => service.currentTheme;
static ThemeMode get systemTheme => service.systemTheme; static ThemeMode get systemTheme => service.systemTheme;
static ThemeData get dark => service.dark; static ThemeData get dark => service.dark;
@@ -33,7 +35,6 @@ class ArcaneTheme extends InheritedWidget {
service.switchTheme; service.switchTheme;
static ArcaneReactiveTheme Function(BuildContext context) static ArcaneReactiveTheme Function(BuildContext context)
get followSystemTheme => service.followSystemTheme; get followSystemTheme => service.followSystemTheme;
static void get checkSystemTheme => service.checkSystemTheme;
static ArcaneReactiveTheme Function(ThemeData theme) get setDarkTheme => static ArcaneReactiveTheme Function(ThemeData theme) get setDarkTheme =>
service.setDarkTheme; service.setDarkTheme;
static ArcaneReactiveTheme Function(ThemeData theme) get setLightTheme => static ArcaneReactiveTheme Function(ThemeData theme) get setLightTheme =>
@@ -96,44 +96,33 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
const MediaQuery( const MediaQuery(
data: MediaQueryData(platformBrightness: Brightness.light), data: MediaQueryData(platformBrightness: Brightness.light),
child: ArcaneApp(
child: SizedBox(), child: SizedBox(),
), ),
),
); );
final BuildContext lightContext = tester.element(find.byType(SizedBox)); final BuildContext lightContext = tester.element(find.byType(SizedBox));
theme.followSystemTheme(lightContext); Arcane.theme.followSystemTheme(lightContext);
await tester.pumpAndSettle();
expect(theme.currentTheme, equals(ThemeMode.light)); expect(theme.currentTheme, equals(ThemeMode.light));
await tester.pumpWidget( await tester.pumpWidget(
const MediaQuery( const MediaQuery(
data: MediaQueryData(platformBrightness: Brightness.dark), data: MediaQueryData(platformBrightness: Brightness.dark),
child: ArcaneApp(
child: SizedBox(), child: SizedBox(),
), ),
),
); );
final BuildContext darkContext = tester.element(find.byType(SizedBox)); final BuildContext darkContext = tester.element(find.byType(SizedBox));
theme.followSystemTheme(darkContext); Arcane.theme.followSystemTheme(darkContext);
await tester.pumpAndSettle();
expect(theme.currentTheme, equals(ThemeMode.dark)); expect(theme.currentTheme, 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));
});
}); });
}); });
} }