mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-08-12 06:10:55 +02:00
v2.0.6: Remove export of Error and Ok from result_monad (#12)
* Update Logging feature to replace @LoggingFeature annotation with LoggerName mixin for runtime accessibility Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Bump version to 2.0.2 in pubspec.yaml and update CHANGELOG to reflect the new version Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add migration steps for replacing @LoggingFeature annotation with LoggerName mixin in CHANGELOG Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix class declaration in migration example for LoggerName mixin Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Enhance CI workflow, coverage enforcement, and environment tests (#10) * Enhance CI workflow and coverage enforcement; update tests and add new environment tests Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Revert version bump in pubspec.yaml and remove test coverage note from CHANGELOG Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update test to expect a thrown exception when no interfaces are registered Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add ArcaneEnvironmentModeController and related tests for debug mode management Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Refactor theme switcher tests to use platform dispatcher for brightness change simulation Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> --------- Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(api): Remove export of Error and Ok from result_monad (#11) The `Error` and `Ok` symbols from the `result_monad` package are no longer re-exported directly by `arcane_framework`. Consumers needing these symbols must now explicitly import them from `package:result_monad`. This clarifies dependency ownership and reduces `arcane_framework`'s public API surface. --------- Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
@@ -10,6 +11,10 @@ void main() {
|
||||
});
|
||||
|
||||
group("Arcane", () {
|
||||
tearDown(() {
|
||||
Arcane.clearRegistry();
|
||||
});
|
||||
|
||||
test("services getter returns all core services", () {
|
||||
final services = Arcane.services;
|
||||
expect(services, contains(isA<ArcaneFeatureFlagService>()));
|
||||
@@ -17,5 +22,17 @@ void main() {
|
||||
expect(services, contains(isA<ArcaneReactiveTheme>()));
|
||||
expect(services, contains(isA<ArcaneEnvironmentService>()));
|
||||
});
|
||||
|
||||
test(
|
||||
"fallback getters return singletons when registry is present but empty",
|
||||
() {
|
||||
final notifier = ValueNotifier<List<ArcaneService>>(<ArcaneService>[]);
|
||||
Arcane.setRegistry(notifier);
|
||||
|
||||
expect(Arcane.features, same(ArcaneFeatureFlagService.I));
|
||||
expect(Arcane.auth, same(ArcaneAuthenticationService.I));
|
||||
expect(Arcane.theme, same(ArcaneThemeService.I));
|
||||
expect(Arcane.environment, same(ArcaneEnvironmentService.I));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -286,6 +286,120 @@ void main() {
|
||||
expect(buildCount, 3);
|
||||
});
|
||||
|
||||
testWidgets("maybeOf returns null when provider is missing",
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
expect(ArcaneServiceProvider.maybeOf(context), isNull);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("of asserts when provider is missing", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
expect(
|
||||
() => ArcaneServiceProvider.of(context),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
"requiredServiceOfType returns service and asserts when missing",
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
ArcaneApp(
|
||||
services: testServices,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final service = ArcaneServiceProvider.requiredServiceOfType<
|
||||
MockArcaneService>(context);
|
||||
expect(service, isA<MockArcaneService>());
|
||||
|
||||
expect(
|
||||
() => ArcaneServiceProvider.requiredServiceOfType<
|
||||
UnregisteredService>(context),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("addService replaces same runtime type", (tester) async {
|
||||
late ArcaneServiceProvider provider;
|
||||
final first = MockArcaneService();
|
||||
final replacement = MockArcaneService();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneServiceProvider(
|
||||
serviceInstances: [first],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
provider = ArcaneServiceProvider.of(context);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
provider.addService(replacement);
|
||||
await tester.pump();
|
||||
|
||||
final matches =
|
||||
provider.registeredServices.whereType<MockArcaneService>();
|
||||
expect(matches.length, 1);
|
||||
expect(matches.single, same(replacement));
|
||||
});
|
||||
|
||||
testWidgets("removeService removes matching type and reports status",
|
||||
(tester) async {
|
||||
late ArcaneServiceProvider provider;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneServiceProvider(
|
||||
serviceInstances: [MockArcaneService(), AnotherMockService()],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
provider = ArcaneServiceProvider.of(context);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final removed = provider.removeService<MockArcaneService>();
|
||||
await tester.pump();
|
||||
|
||||
expect(removed, isTrue);
|
||||
expect(
|
||||
provider.registeredServices.whereType<MockArcaneService>(),
|
||||
isEmpty,
|
||||
);
|
||||
|
||||
final secondRemoval = provider.removeService<MockArcaneService>();
|
||||
expect(secondRemoval, isFalse);
|
||||
});
|
||||
|
||||
testWidgets("ArcaneService.of<T> static helper works", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
ArcaneApp(
|
||||
@@ -371,6 +485,38 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
"ArcaneApp.didUpdateWidget updates notifier when service list changes",
|
||||
(tester) async {
|
||||
final first = MockArcaneService();
|
||||
final second = AnotherMockService();
|
||||
late StateSetter setStateRef;
|
||||
var useSecond = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
setStateRef = setState;
|
||||
return ArcaneApp(
|
||||
services: useSecond ? [first, second] : [first],
|
||||
child: const SizedBox(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Trigger didUpdateWidget with a different service list.
|
||||
setStateRef(() => useSecond = true);
|
||||
await tester.pump();
|
||||
|
||||
// Verify AnotherMockService is now in the merged service list.
|
||||
final notifier = Arcane.registry!;
|
||||
expect(
|
||||
notifier.value.whereType<AnotherMockService>(),
|
||||
isNotEmpty,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,32 @@ class MockAuth implements ArcaneAuthInterface {
|
||||
}
|
||||
}
|
||||
|
||||
class MinimalAuth extends ArcaneAuthInterface {
|
||||
@override
|
||||
Future<bool> get isSignedIn => Future.value(false);
|
||||
|
||||
@override
|
||||
Future<String?>? get accessToken => Future.value(null);
|
||||
|
||||
@override
|
||||
Future<String?>? get refreshToken => Future.value(null);
|
||||
|
||||
@override
|
||||
Future<Result<void, String>> login<T>({
|
||||
T? input,
|
||||
Future<void> Function()? onLoggedIn,
|
||||
}) async {
|
||||
return const Result.ok(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void, String>> logout({
|
||||
Future<void> Function()? onLoggedOut,
|
||||
}) async {
|
||||
return const Result.ok(null);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
test("MockAuth fulfills ArcaneAuthInterface contract", () async {
|
||||
final auth = MockAuth();
|
||||
@@ -47,4 +73,9 @@ void main() {
|
||||
);
|
||||
expect(called, isTrue);
|
||||
});
|
||||
|
||||
test("default init implementation completes", () async {
|
||||
final auth = MinimalAuth();
|
||||
await auth.init();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,6 +221,53 @@ void main() {
|
||||
ArcaneAuthenticationService.I.dispose();
|
||||
// No assertion needed; just ensure no crash
|
||||
});
|
||||
|
||||
test("notifier getter reflects unauthenticated default", () {
|
||||
expect(
|
||||
ArcaneAuthenticationService.I.notifier.value,
|
||||
AuthenticationStatus.unauthenticated,
|
||||
);
|
||||
});
|
||||
|
||||
test("accessToken and refreshToken fall back to empty string", () async {
|
||||
expect(await ArcaneAuthenticationService.I.accessToken, "");
|
||||
expect(await ArcaneAuthenticationService.I.refreshToken, "");
|
||||
});
|
||||
|
||||
testWidgets("setDebug and setNormal callbacks run on state transitions",
|
||||
(tester) async {
|
||||
late BuildContext capturedContext;
|
||||
var debugCallbackCalled = false;
|
||||
var normalCallbackCalled = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
capturedContext = context;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await ArcaneAuthenticationService.I.setDebug(
|
||||
capturedContext,
|
||||
onDebugModeSet: () async {
|
||||
debugCallbackCalled = true;
|
||||
},
|
||||
);
|
||||
|
||||
await ArcaneAuthenticationService.I.setNormal(
|
||||
capturedContext,
|
||||
onDebugModeUnset: () async {
|
||||
normalCallbackCalled = true;
|
||||
},
|
||||
);
|
||||
|
||||
expect(debugCallbackCalled, isTrue);
|
||||
expect(normalCallbackCalled, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
late ArcaneAuthInterface authInterface;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
group("Environment", () {
|
||||
test("built-in values have expected names", () {
|
||||
expect(Environment.debug.name, "debug");
|
||||
expect(Environment.normal.name, "normal");
|
||||
});
|
||||
|
||||
test("isDebug and isNormal reflect built-in values", () {
|
||||
expect(Environment.debug.isDebug, isTrue);
|
||||
expect(Environment.debug.isNormal, isFalse);
|
||||
|
||||
expect(Environment.normal.isNormal, isTrue);
|
||||
expect(Environment.normal.isDebug, isFalse);
|
||||
|
||||
const staging = Environment("staging");
|
||||
expect(staging.isDebug, isFalse);
|
||||
expect(staging.isNormal, isFalse);
|
||||
});
|
||||
|
||||
test("equality and hashCode are name-based", () {
|
||||
const first = Environment("staging");
|
||||
const second = Environment("staging");
|
||||
const prod = Environment("prod");
|
||||
|
||||
expect(first, second);
|
||||
expect(first.hashCode, second.hashCode);
|
||||
expect(first, isNot(prod));
|
||||
});
|
||||
|
||||
test("toString includes environment name", () {
|
||||
const staging = Environment("staging");
|
||||
expect(staging.toString(), "Environment(staging)");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
group("ArcaneEnvironment", () {
|
||||
setUp(() {
|
||||
Arcane.environment.reset();
|
||||
});
|
||||
|
||||
testWidgets("maybeOf returns null without provider", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
expect(ArcaneEnvironment.maybeOf(context), isNull);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("of throws without provider", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
expect(
|
||||
() => ArcaneEnvironment.of(context),
|
||||
throwsA(isA<StateError>()),
|
||||
);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("updateShouldNotify changes only when environment changes", () {
|
||||
final normalA = ArcaneEnvironment(
|
||||
environment: Environment.normal,
|
||||
switchEnvironment: (_) {},
|
||||
child: const SizedBox(),
|
||||
);
|
||||
final normalB = ArcaneEnvironment(
|
||||
environment: Environment.normal,
|
||||
switchEnvironment: (_) {},
|
||||
child: const SizedBox(),
|
||||
);
|
||||
final debug = ArcaneEnvironment(
|
||||
environment: Environment.debug,
|
||||
switchEnvironment: (_) {},
|
||||
child: const SizedBox(),
|
||||
);
|
||||
|
||||
expect(normalA.updateShouldNotify(normalB), isFalse);
|
||||
expect(debug.updateShouldNotify(normalB), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group("ArcaneEnvironmentProvider", () {
|
||||
setUp(() {
|
||||
Arcane.environment.reset();
|
||||
});
|
||||
|
||||
testWidgets("uses widget initial environment on init", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneEnvironmentProvider(
|
||||
environment: Environment.debug,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final env = ArcaneEnvironment.of(context);
|
||||
expect(env.environment, Environment.debug);
|
||||
expect(env.current, Environment.debug);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("setEnvironment updates service and inherited widget",
|
||||
(tester) async {
|
||||
late BuildContext capturedContext;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneEnvironmentProvider(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
capturedContext = context;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
ArcaneEnvironment.of(capturedContext).setEnvironment(Environment.debug);
|
||||
await tester.pump();
|
||||
expect(Arcane.environment.current, Environment.debug);
|
||||
});
|
||||
|
||||
testWidgets("enableDebugMode and disableDebugMode proxy correctly",
|
||||
(tester) async {
|
||||
late BuildContext capturedContext;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneEnvironmentProvider(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
capturedContext = context;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
ArcaneEnvironment.of(capturedContext).enableDebugMode();
|
||||
await tester.pump();
|
||||
expect(Arcane.environment.current, Environment.debug);
|
||||
|
||||
ArcaneEnvironment.of(capturedContext).disableDebugMode();
|
||||
await tester.pump();
|
||||
expect(Arcane.environment.current, Environment.normal);
|
||||
});
|
||||
|
||||
testWidgets("provider rebuilds when service environment changes",
|
||||
(tester) async {
|
||||
var buildCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneEnvironmentProvider(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
buildCount++;
|
||||
// ignore: unnecessary_statements
|
||||
ArcaneEnvironment.of(context).environment;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(buildCount, 1);
|
||||
|
||||
Arcane.environment.setEnvironment(Environment.debug);
|
||||
await tester.pump();
|
||||
expect(buildCount, 2);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
"state enableDebugMode updates to debug and no-ops when already debug",
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneEnvironmentProvider(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final ArcaneEnvironmentModeController state =
|
||||
tester.state<State<ArcaneEnvironmentProvider>>(
|
||||
find.byType(ArcaneEnvironmentProvider),
|
||||
) as ArcaneEnvironmentModeController;
|
||||
|
||||
expect(Arcane.environment.current, Environment.normal);
|
||||
|
||||
state.enableDebugMode();
|
||||
await tester.pump();
|
||||
expect(Arcane.environment.current, Environment.debug);
|
||||
|
||||
state.enableDebugMode();
|
||||
await tester.pump();
|
||||
expect(Arcane.environment.current, Environment.debug);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
"state disableDebugMode updates to normal and no-ops when already normal",
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneEnvironmentProvider(
|
||||
environment: Environment.debug,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final ArcaneEnvironmentModeController state =
|
||||
tester.state<State<ArcaneEnvironmentProvider>>(
|
||||
find.byType(ArcaneEnvironmentProvider),
|
||||
) as ArcaneEnvironmentModeController;
|
||||
|
||||
expect(Arcane.environment.current, Environment.debug);
|
||||
|
||||
state.disableDebugMode();
|
||||
await tester.pump();
|
||||
expect(Arcane.environment.current, Environment.normal);
|
||||
|
||||
state.disableDebugMode();
|
||||
await tester.pump();
|
||||
expect(Arcane.environment.current, Environment.normal);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
group("ArcaneEnvironmentService", () {
|
||||
late ArcaneEnvironmentService service;
|
||||
|
||||
setUp(() {
|
||||
service = ArcaneEnvironmentService.I;
|
||||
service.reset();
|
||||
});
|
||||
|
||||
test("singleton instance is consistent", () {
|
||||
expect(identical(ArcaneEnvironmentService.I, service), isTrue);
|
||||
expect(identical(Arcane.environment, service), isTrue);
|
||||
});
|
||||
|
||||
test("default environment is normal", () {
|
||||
expect(service.current, Environment.normal);
|
||||
});
|
||||
|
||||
test("setEnvironment updates current value", () {
|
||||
service.setEnvironment(Environment.debug);
|
||||
expect(service.current, Environment.debug);
|
||||
});
|
||||
|
||||
test("setEnvironment with same value does not emit stream event", () async {
|
||||
var didEmit = false;
|
||||
final sub = service.environmentChanges.listen((_) {
|
||||
didEmit = true;
|
||||
});
|
||||
|
||||
service.setEnvironment(Environment.normal);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(didEmit, isFalse);
|
||||
await sub.cancel();
|
||||
});
|
||||
|
||||
test("setEnvironment with different value emits stream event", () async {
|
||||
final event = expectLater(
|
||||
service.environmentChanges,
|
||||
emits(Environment.debug),
|
||||
);
|
||||
|
||||
service.setEnvironment(Environment.debug);
|
||||
await event;
|
||||
});
|
||||
|
||||
test("notifier listeners are notified on changes", () {
|
||||
var notified = false;
|
||||
service.notifier.addListener(() {
|
||||
notified = true;
|
||||
});
|
||||
|
||||
service.setEnvironment(Environment.debug);
|
||||
expect(notified, isTrue);
|
||||
});
|
||||
|
||||
test("enableDebugMode and disableDebugMode switch built-in modes", () {
|
||||
service.enableDebugMode();
|
||||
expect(service.current, Environment.debug);
|
||||
|
||||
service.disableDebugMode();
|
||||
expect(service.current, Environment.normal);
|
||||
});
|
||||
|
||||
test("reset restores normal and emits current state", () async {
|
||||
service.setEnvironment(Environment.debug);
|
||||
|
||||
final event = expectLater(
|
||||
service.environmentChanges,
|
||||
emits(Environment.normal),
|
||||
);
|
||||
|
||||
service.reset();
|
||||
|
||||
expect(service.current, Environment.normal);
|
||||
await event;
|
||||
});
|
||||
|
||||
test("environmentChanges works after listener cancellation", () async {
|
||||
final first = service.environmentChanges.listen((_) {});
|
||||
await first.cancel();
|
||||
|
||||
final event = expectLater(
|
||||
service.environmentChanges,
|
||||
emits(Environment.debug),
|
||||
);
|
||||
|
||||
service.setEnvironment(Environment.debug);
|
||||
await event;
|
||||
});
|
||||
|
||||
test("dispose closes stream and subsequent reads recreate it", () async {
|
||||
service.dispose();
|
||||
|
||||
final event = expectLater(
|
||||
service.environmentChanges,
|
||||
emits(Environment.debug),
|
||||
);
|
||||
|
||||
service.setEnvironment(Environment.debug);
|
||||
await event;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -27,6 +27,23 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("ArcaneFeatureFlagProvider.of throws without provider",
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
expect(
|
||||
() => ArcaneFeatureFlagProvider.of(context),
|
||||
throwsA(isA<StateError>()),
|
||||
);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("feature flag updates trigger rebuilds for dependent widgets",
|
||||
(tester) async {
|
||||
int buildCount = 0;
|
||||
@@ -100,6 +117,20 @@ void main() {
|
||||
await tester.tap(find.text("disable"));
|
||||
await tester.pump();
|
||||
expect(find.text("off"), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneApp(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final scope = context.featureFlags;
|
||||
expect(scope.isDisabled(TestFeature.beta), isTrue);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("scope exposes notifier and stream for reactive consumers",
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
class FeatureFlagLoggingInterface extends LoggingInterface {
|
||||
final List<LogEvent> events = <LogEvent>[];
|
||||
|
||||
@override
|
||||
void log(
|
||||
String message, {
|
||||
Map<String, Object?>? metadata,
|
||||
Level? level,
|
||||
StackTrace? stackTrace,
|
||||
Object? extra,
|
||||
}) {
|
||||
events.add(
|
||||
LogEvent(
|
||||
message: message,
|
||||
metadata: metadata == null ? null : Map<String, Object?>.from(metadata),
|
||||
level: level,
|
||||
stackTrace: stackTrace,
|
||||
extra: extra,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group("ArcaneFeatureFlagService", () {
|
||||
late ArcaneFeatureFlagService featureFlags;
|
||||
@@ -8,12 +31,21 @@ void main() {
|
||||
setUp(() {
|
||||
featureFlags = ArcaneFeatureFlagService.I;
|
||||
Arcane.features.reset();
|
||||
Arcane.logger.reset();
|
||||
});
|
||||
|
||||
test("singleton instance is consistent", () {
|
||||
expect(identical(ArcaneFeatureFlagService.I, featureFlags), true);
|
||||
});
|
||||
|
||||
test("initialized reflects init and reset transitions", () {
|
||||
expect(ArcaneFeatureFlagService.initialized, isFalse);
|
||||
featureFlags.enableFeature(MockFeature.test);
|
||||
expect(ArcaneFeatureFlagService.initialized, isTrue);
|
||||
featureFlags.reset();
|
||||
expect(ArcaneFeatureFlagService.initialized, isFalse);
|
||||
});
|
||||
|
||||
group("feature management", () {
|
||||
test("enableFeature adds feature to enabled list", () {
|
||||
featureFlags.enableFeature(MockFeature.test);
|
||||
@@ -99,6 +131,50 @@ void main() {
|
||||
|
||||
await secondSubscription.cancel();
|
||||
});
|
||||
|
||||
test("enableFeature logs when logger is initialized", () async {
|
||||
final logger = FeatureFlagLoggingInterface();
|
||||
await Arcane.logger.registerInterface(logger);
|
||||
|
||||
featureFlags.enableFeature(MockFeature.test);
|
||||
|
||||
expect(logger.events, isNotEmpty);
|
||||
expect(logger.events.last.message, contains("Feature enabled"));
|
||||
expect(logger.events.last.level, Level.info);
|
||||
expect(
|
||||
logger.events.last.metadata?[MockFeature.test.toString()],
|
||||
"✅",
|
||||
);
|
||||
});
|
||||
|
||||
test("disableFeature logs when logger is initialized", () async {
|
||||
final logger = FeatureFlagLoggingInterface();
|
||||
await Arcane.logger.registerInterface(logger);
|
||||
|
||||
featureFlags.enableFeature(MockFeature.test);
|
||||
featureFlags.disableFeature(MockFeature.test);
|
||||
|
||||
expect(logger.events, isNotEmpty);
|
||||
expect(logger.events.last.message, contains("Feature disabled"));
|
||||
expect(logger.events.last.level, Level.info);
|
||||
expect(
|
||||
logger.events.last.metadata?[MockFeature.test.toString()],
|
||||
"❌",
|
||||
);
|
||||
});
|
||||
|
||||
test("dispose closes stream and future subscribers still receive events",
|
||||
() async {
|
||||
featureFlags.dispose();
|
||||
|
||||
final event = expectLater(
|
||||
featureFlags.enabledFeaturesChanges,
|
||||
emitsThrough(contains(MockFeature.another)),
|
||||
);
|
||||
|
||||
featureFlags.enableFeature(MockFeature.another);
|
||||
await event;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
group("LogEvent", () {
|
||||
test("constructor stores provided values", () {
|
||||
final event = LogEvent(
|
||||
message: "with-data",
|
||||
metadata: {
|
||||
"flag": true,
|
||||
"items": [1, 2, 3],
|
||||
},
|
||||
level: Level.warning,
|
||||
stackTrace: StackTrace.fromString("trace-here"),
|
||||
extra: {
|
||||
"child": ["x"],
|
||||
},
|
||||
);
|
||||
|
||||
expect(event.message, "with-data");
|
||||
expect(event.metadata?["flag"], isTrue);
|
||||
expect(event.metadata?["items"], [1, 2, 3]);
|
||||
expect(event.level, Level.warning);
|
||||
expect(event.stackTrace.toString(), "trace-here");
|
||||
expect(event.extra, {
|
||||
"child": ["x"],
|
||||
});
|
||||
});
|
||||
|
||||
test("constructor leaves optional fields null when omitted", () {
|
||||
const event = LogEvent(message: "plain");
|
||||
|
||||
expect(event.message, "plain");
|
||||
expect(event.metadata, isNull);
|
||||
expect(event.level, isNull);
|
||||
expect(event.stackTrace, isNull);
|
||||
expect(event.extra, isNull);
|
||||
});
|
||||
|
||||
test("copyWith keeps existing values when sentinel defaults are used", () {
|
||||
final original = LogEvent(
|
||||
message: "original",
|
||||
metadata: {"a": 1},
|
||||
level: Level.info,
|
||||
stackTrace: StackTrace.fromString("trace"),
|
||||
extra: "extra",
|
||||
);
|
||||
|
||||
final copy = original.copyWith();
|
||||
|
||||
expect(copy.message, "original");
|
||||
expect(copy.metadata, original.metadata);
|
||||
expect(copy.level, Level.info);
|
||||
expect(copy.stackTrace, original.stackTrace);
|
||||
expect(copy.extra, "extra");
|
||||
});
|
||||
|
||||
test("copyWith supports explicit nulling of nullable fields", () {
|
||||
final original = LogEvent(
|
||||
message: "original",
|
||||
metadata: {"a": 1},
|
||||
level: Level.error,
|
||||
stackTrace: StackTrace.fromString("trace"),
|
||||
extra: 99,
|
||||
);
|
||||
|
||||
final cleared = original.copyWith(
|
||||
metadata: null,
|
||||
level: null,
|
||||
stackTrace: null,
|
||||
extra: null,
|
||||
);
|
||||
|
||||
expect(cleared.message, "original");
|
||||
expect(cleared.metadata, isNull);
|
||||
expect(cleared.level, isNull);
|
||||
expect(cleared.stackTrace, isNull);
|
||||
expect(cleared.extra, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group("LogEvent.fromJson", () {
|
||||
test("round-trips a fully populated event", () {
|
||||
const original = LogEvent(
|
||||
message: "hello",
|
||||
metadata: {"key": "value"},
|
||||
level: Level.warning,
|
||||
extra: 42,
|
||||
);
|
||||
|
||||
final json = original.toJson();
|
||||
final restored = LogEvent.fromJson(json);
|
||||
|
||||
expect(restored.message, "hello");
|
||||
expect(restored.metadata, {"key": "value"});
|
||||
expect(restored.level, Level.warning);
|
||||
expect(restored.extra, 42);
|
||||
});
|
||||
|
||||
test("round-trips an event with a stack trace", () {
|
||||
final original = LogEvent(
|
||||
message: "crash",
|
||||
stackTrace: StackTrace.fromString("frame #0"),
|
||||
);
|
||||
|
||||
final json = original.toJson();
|
||||
final restored = LogEvent.fromJson(json);
|
||||
|
||||
expect(restored.stackTrace.toString(), "frame #0");
|
||||
});
|
||||
|
||||
test("omits null optional fields from toJson output", () {
|
||||
const event = LogEvent(message: "bare");
|
||||
final json = event.toJson();
|
||||
|
||||
expect(json.containsKey("metadata"), isFalse);
|
||||
expect(json.containsKey("level"), isFalse);
|
||||
expect(json.containsKey("stackTrace"), isFalse);
|
||||
expect(json.containsKey("extra"), isFalse);
|
||||
});
|
||||
|
||||
test("fromJson handles missing optional fields gracefully", () {
|
||||
final event = LogEvent.fromJson({"message": "minimal"});
|
||||
|
||||
expect(event.message, "minimal");
|
||||
expect(event.metadata, isNull);
|
||||
expect(event.level, isNull);
|
||||
expect(event.stackTrace, isNull);
|
||||
expect(event.extra, isNull);
|
||||
});
|
||||
|
||||
test("fromJson falls back to Level.debug for unrecognised level name", () {
|
||||
final event = LogEvent.fromJson({
|
||||
"message": "x",
|
||||
"level": "nonExistentLevel",
|
||||
});
|
||||
|
||||
expect(event.level, Level.debug);
|
||||
});
|
||||
|
||||
test("toJson encodes metadata and extra recursively", () {
|
||||
const event = LogEvent(
|
||||
message: "nested",
|
||||
metadata: {
|
||||
"map": {"inner": 1},
|
||||
"list": [1, 2, 3],
|
||||
},
|
||||
extra: {"deep": true},
|
||||
);
|
||||
|
||||
final json = event.toJson();
|
||||
expect((json["metadata"] as Map)["map"], {"inner": 1});
|
||||
expect((json["metadata"] as Map)["list"], [1, 2, 3]);
|
||||
expect(json["extra"], {"deep": true});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
class _InterceptorsTestInterface extends LoggingInterface {
|
||||
_InterceptorsTestInterface(this.name);
|
||||
|
||||
final String name;
|
||||
|
||||
@override
|
||||
void log(
|
||||
String message, {
|
||||
Map<String, Object?>? metadata,
|
||||
Level? level,
|
||||
StackTrace? stackTrace,
|
||||
Object? extra,
|
||||
}) {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group("LoggingInterceptorsService", () {
|
||||
late _InterceptorsTestInterface primary;
|
||||
late _InterceptorsTestInterface secondary;
|
||||
late LogInterceptor global;
|
||||
late LogInterceptor interfaceScoped;
|
||||
late LogInterceptor typeScoped;
|
||||
|
||||
setUp(() {
|
||||
Arcane.logger.reset();
|
||||
Arcane.logger.interceptors.clear();
|
||||
|
||||
primary = _InterceptorsTestInterface("primary");
|
||||
secondary = _InterceptorsTestInterface("secondary");
|
||||
|
||||
global = LogInterceptor((event, context) => event);
|
||||
interfaceScoped = LogInterceptor((event, context) => event);
|
||||
typeScoped = LogInterceptor((event, context) => event);
|
||||
});
|
||||
|
||||
test("clearGlobal removes global interceptors only", () {
|
||||
Arcane.logger.interceptors.add(global);
|
||||
Arcane.logger.interceptors
|
||||
.registerForInterface(primary, [interfaceScoped]);
|
||||
|
||||
Arcane.logger.interceptors.clearGlobal();
|
||||
|
||||
final resolved = Arcane.logger.interceptors.resolveForInterface(primary);
|
||||
expect(resolved, isNot(contains(global)));
|
||||
expect(resolved, contains(interfaceScoped));
|
||||
});
|
||||
|
||||
test("resolveForInterface includes only matching registrations", () {
|
||||
Arcane.logger.interceptors.add(global);
|
||||
Arcane.logger.interceptors
|
||||
.registerForInterface(primary, [interfaceScoped]);
|
||||
|
||||
Arcane.logger.interceptors.add(
|
||||
typeScoped,
|
||||
matcher: (interface) => interface is _InterceptorsTestInterface,
|
||||
);
|
||||
|
||||
final forPrimary =
|
||||
Arcane.logger.interceptors.resolveForInterface(primary);
|
||||
final forSecondary =
|
||||
Arcane.logger.interceptors.resolveForInterface(secondary);
|
||||
|
||||
expect(forPrimary, contains(global));
|
||||
expect(forPrimary, contains(interfaceScoped));
|
||||
expect(forPrimary, contains(typeScoped));
|
||||
|
||||
expect(forSecondary, contains(global));
|
||||
expect(forSecondary, isNot(contains(interfaceScoped)));
|
||||
expect(forSecondary, contains(typeScoped));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -372,6 +372,45 @@ void main() {
|
||||
|
||||
expect(secondary.events.single.message, "[future] hello");
|
||||
});
|
||||
|
||||
test("registerInterfaces accepts empty interceptor map", () async {
|
||||
await Arcane.logger.registerInterfaces(
|
||||
[myInterface],
|
||||
interceptors: <LoggingInterface, List<LogInterceptor>>{},
|
||||
);
|
||||
|
||||
Arcane.log("hello");
|
||||
expect(myInterface.events.single.message, "hello");
|
||||
});
|
||||
|
||||
test("unregisterInterfaces removes all listed interfaces", () async {
|
||||
final TestLoggingInterface secondary =
|
||||
TestLoggingInterface("secondary");
|
||||
|
||||
await Arcane.logger.registerInterfaces([myInterface, secondary]);
|
||||
await Arcane.logger.unregisterInterfaces([myInterface, secondary]);
|
||||
|
||||
expect(Arcane.logger.interfaces, isEmpty);
|
||||
});
|
||||
|
||||
test("unregisterAllInterfaces removes all registered interfaces",
|
||||
() async {
|
||||
final TestLoggingInterface secondary =
|
||||
TestLoggingInterface("secondary");
|
||||
|
||||
await Arcane.logger.registerInterfaces([myInterface, secondary]);
|
||||
await Arcane.logger.unregisterAllInterfaces();
|
||||
|
||||
expect(Arcane.logger.interfaces, isEmpty);
|
||||
});
|
||||
|
||||
test("initializeInterfaces throws when no interfaces are registered",
|
||||
() async {
|
||||
await expectLater(
|
||||
Arcane.logger.initializeInterfaces(),
|
||||
throwsException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group("persistent metadata", () {
|
||||
@@ -393,6 +432,18 @@ void main() {
|
||||
Arcane.logger.clearPersistentMetadata();
|
||||
expect(Arcane.logger.additionalMetadata.isEmpty, true);
|
||||
});
|
||||
|
||||
test("addPersistentMetadata removes an existing key on empty value", () {
|
||||
Arcane.logger.addPersistentMetadata({"token": "abc"});
|
||||
Arcane.logger.addPersistentMetadata({"token": ""});
|
||||
|
||||
expect(Arcane.logger.additionalMetadata.containsKey("token"), isFalse);
|
||||
});
|
||||
|
||||
test("addPersistentMetadata ignores null values for new keys", () {
|
||||
Arcane.logger.addPersistentMetadata({"token": null});
|
||||
expect(Arcane.logger.additionalMetadata.containsKey("token"), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group("logging messages", () {
|
||||
@@ -455,6 +506,31 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test("explicit method parameter is preserved in metadata", () async {
|
||||
Arcane.log(
|
||||
logMessage,
|
||||
method: "customMethod",
|
||||
skipAutodetection: true,
|
||||
);
|
||||
|
||||
expect(myInterface.events.single.metadata?["method"], "customMethod");
|
||||
});
|
||||
|
||||
test("module and method can be inferred from provided metadata",
|
||||
() async {
|
||||
Arcane.log(
|
||||
logMessage,
|
||||
metadata: {
|
||||
"module": "InjectedModule",
|
||||
"method": "InjectedMethod",
|
||||
},
|
||||
skipAutodetection: true,
|
||||
);
|
||||
|
||||
expect(myInterface.events.single.metadata?["module"], "InjectedModule");
|
||||
expect(myInterface.events.single.metadata?["method"], "InjectedMethod");
|
||||
});
|
||||
|
||||
test("global interceptors run in registration order", () async {
|
||||
Arcane.logger.interceptors.addAll([
|
||||
LogInterceptor((event, context) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "package:arcane_framework/src/services/theme/theme_extensions.dart";
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
@@ -30,4 +30,37 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("themeMode reads value from nearest ArcaneTheme", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneTheme(
|
||||
themeMode: ThemeMode.dark,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
expect(context.themeMode, ThemeMode.dark);
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("themeMode falls back to Arcane theme service without provider",
|
||||
(tester) async {
|
||||
Arcane.theme.reset();
|
||||
Arcane.theme.switchTheme(themeMode: ThemeMode.dark);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
expect(context.themeMode, ArcaneReactiveTheme.I.currentThemeMode);
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,6 +34,56 @@ void main() {
|
||||
expect(ArcaneThemeService.I.light, customLight);
|
||||
});
|
||||
|
||||
test("dark and light setters delegate to setDarkTheme/setLightTheme", () {
|
||||
final customDark =
|
||||
ThemeData(primaryColor: Colors.teal, brightness: Brightness.dark);
|
||||
final customLight =
|
||||
ThemeData(primaryColor: Colors.amber, brightness: Brightness.light);
|
||||
|
||||
ArcaneThemeService.I.dark = customDark;
|
||||
ArcaneThemeService.I.light = customLight;
|
||||
|
||||
expect(ArcaneThemeService.I.dark, customDark);
|
||||
expect(ArcaneThemeService.I.light, customLight);
|
||||
});
|
||||
|
||||
testWidgets("currentModeOf returns context themeMode", (tester) async {
|
||||
ArcaneThemeService.I.switchTheme(themeMode: ThemeMode.dark);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
expect(
|
||||
ArcaneThemeService.I.currentModeOf(context),
|
||||
ThemeMode.dark,
|
||||
);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("setInitialTheme handles explicit dark mode", (tester) async {
|
||||
ArcaneThemeService.I.switchTheme(themeMode: ThemeMode.dark);
|
||||
|
||||
late BuildContext capturedContext;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
capturedContext = context;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
ArcaneThemeService.I.setInitialTheme(capturedContext);
|
||||
expect(ArcaneThemeService.I.currentTheme.brightness, Brightness.dark);
|
||||
});
|
||||
|
||||
test("reset restores defaults", () {
|
||||
ArcaneThemeService.I.setDarkTheme(
|
||||
ThemeData(primaryColor: Colors.red, brightness: Brightness.dark),
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
group("ArcaneThemeSwitcher", () {
|
||||
setUp(() {
|
||||
ArcaneThemeService.I.reset();
|
||||
});
|
||||
|
||||
testWidgets("renders child inside ArcaneTheme", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ArcaneThemeSwitcher(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final theme = ArcaneTheme.of(context);
|
||||
expect(theme, isNotNull);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
"didChangePlatformBrightness calls followSystemTheme when following system",
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: ArcaneThemeSwitcher(child: SizedBox()),
|
||||
),
|
||||
);
|
||||
|
||||
// followSystemTheme is set during didChangeDependencies; enable it.
|
||||
ArcaneThemeService.I.followSystemTheme(
|
||||
tester.element(find.byType(ArcaneThemeSwitcher)),
|
||||
);
|
||||
expect(ArcaneThemeService.I.isFollowingSystemTheme, isTrue);
|
||||
|
||||
// Simulate a platform brightness change via the WidgetsBindingObserver.
|
||||
tester.binding.platformDispatcher.onPlatformBrightnessChanged?.call();
|
||||
await tester.pump();
|
||||
|
||||
// After the post-frame callback the service should still be in
|
||||
// follow-system mode — the key assertion is that no exception was thrown
|
||||
// and the widget remained mounted.
|
||||
expect(ArcaneThemeService.I.isFollowingSystemTheme, isTrue);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
"didChangePlatformBrightness is a no-op when not following system",
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: ArcaneThemeSwitcher(child: SizedBox()),
|
||||
),
|
||||
);
|
||||
|
||||
// Explicitly switch to a manual dark mode (disables follow-system).
|
||||
ArcaneThemeService.I.switchTheme(themeMode: ThemeMode.dark);
|
||||
expect(ArcaneThemeService.I.isFollowingSystemTheme, isFalse);
|
||||
|
||||
// Simulate platform brightness change — should be a no-op without error.
|
||||
tester.binding.platformDispatcher.onPlatformBrightnessChanged?.call();
|
||||
await tester.pump();
|
||||
|
||||
expect(ArcaneThemeService.I.currentThemeMode, ThemeMode.dark);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user