Files
SpotiFLAC-Mobile/test/appearance_settings_test.dart
T

327 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/models/theme_settings.dart';
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
import 'package:spotiflac_android/providers/theme_provider.dart';
import 'package:spotiflac_android/screens/settings/appearance_settings_page.dart';
import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
void main() {
test('glass clarity restores safely from old and invalid settings', () {
expect(
ThemeSettings.fromJson({}).mornyeGlassClarity,
kDefaultMornyeGlassClarity,
);
for (final (saved, expected) in [
(0, 0.0),
(1, 1.0),
(-1, 0.0),
(2, 1.0),
(double.nan, kDefaultMornyeGlassClarity),
]) {
final settings = ThemeSettings.fromJson({kMornyeGlassClarityKey: saved});
expect(settings.mornyeGlassClarity, expected);
expect(ThemeSettings.fromJson(settings.toJson()), settings);
}
});
test('custom accents keep text readable in both appearances', () {
double contrast(Color first, Color second) {
final a = first.computeLuminance();
final b = second.computeLuminance();
return a > b ? (a + 0.05) / (b + 0.05) : (b + 0.05) / (a + 0.05);
}
for (final brightness in Brightness.values) {
for (final accent in MornyeAccent.values.where(
(accent) => accent != MornyeAccent.red,
)) {
final scheme = MornyeTheme.build(
brightness,
accent: accent,
).colorScheme;
expect(
contrast(scheme.primary, scheme.surface),
greaterThanOrEqualTo(4.5),
reason: '$accent on $brightness',
);
expect(
contrast(scheme.onPrimary, scheme.primary),
greaterThanOrEqualTo(4.5),
reason: 'Button text for $accent on $brightness',
);
}
}
});
Future<void> openSettings(
WidgetTester tester,
SharedPreferences prefs, {
bool lowEnd = false,
}) async {
tester.view.physicalSize = const Size(390, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(
ProviderScope(
overrides: [
initialThemeSettingsProvider.overrideWithValue(
loadBootstrapThemeSettings(prefs),
),
// Low-end Android without forced blur renders flat, opaque glass.
if (lowEnd) lowEndDeviceProvider.overrideWithValue(true),
],
child: DynamicColorWrapper(
builder: (light, dark, mode) => MaterialApp(
theme: light,
darkTheme: dark,
themeMode: mode,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const MornyeSettingsTheme(child: AppearanceSettingsPage()),
),
),
),
);
await tester.pumpAndSettle();
}
for (final mode in [ThemeMode.light, ThemeMode.dark]) {
testWidgets('glass clarity previews live and survives restart ($mode)', (
tester,
) async {
SharedPreferences.setMockInitialValues({
kThemeStyleKey: 'mornye',
kThemeModeKey: mode.name,
});
final prefs = await SharedPreferences.getInstance();
await openSettings(tester, prefs);
final slider = find.byKey(const ValueKey('mornye-glass-clarity'));
await tester.ensureVisible(slider);
await tester.pumpAndSettle();
await tester.drag(slider, const Offset(160, 0));
await tester.pumpAndSettle();
expect(tester.widget<Slider>(slider).value, 1);
expect(prefs.getDouble(kMornyeGlassClarityKey), 1);
final context = tester.element(slider);
expect(MornyeTheme.glassClarityOf(context), 1);
expect(
MornyeTheme.fromContext(
context,
brightness: Brightness.dark,
).extension<MornyeTheme>()!.glassClarity,
1,
);
await tester.pumpWidget(const SizedBox());
await openSettings(tester, prefs);
expect(tester.widget<Slider>(slider).value, 1);
await tester.ensureVisible(slider);
await tester.pumpAndSettle();
await tester.drag(slider, const Offset(-320, 0));
await tester.pumpAndSettle();
expect(tester.widget<Slider>(slider).value, 0);
expect(prefs.getDouble(kMornyeGlassClarityKey), 0);
expect(tester.takeException(), isNull);
});
testWidgets('glass clarity waits for blur on flat devices ($mode)', (
tester,
) async {
SharedPreferences.setMockInitialValues({
kThemeStyleKey: 'mornye',
kThemeModeKey: mode.name,
});
final prefs = await SharedPreferences.getInstance();
await openSettings(tester, prefs, lowEnd: true);
final slider = find.byKey(const ValueKey('mornye-glass-clarity'));
final hint = find.byKey(
const ValueKey('mornye-glass-clarity-unavailable'),
);
await tester.ensureVisible(slider);
await tester.pumpAndSettle();
expect(tester.widget<Slider>(slider).onChanged, isNull);
expect(
tester.widget<Text>(hint).data,
'Glass is opaque on this device. Turn on '
'“Always use blur effects” to adjust clarity.',
);
await tester.drag(slider, const Offset(160, 0));
await tester.pumpAndSettle();
expect(tester.widget<Slider>(slider).value, kDefaultMornyeGlassClarity);
expect(prefs.getDouble(kMornyeGlassClarityKey), isNull);
final forceBlur = find.text('Always use blur effects');
await tester.scrollUntilVisible(
forceBlur,
200,
scrollable: find.byType(Scrollable).first,
);
await tester.tap(forceBlur);
await tester.pumpAndSettle();
// The lazily built slider scrolled away; bring it back.
await tester.scrollUntilVisible(
slider,
-200,
scrollable: find.byType(Scrollable).first,
);
await tester.pumpAndSettle();
expect(hint, findsNothing);
expect(tester.widget<Slider>(slider).onChanged, isNotNull);
await tester.drag(slider, const Offset(160, 0));
await tester.pumpAndSettle();
expect(prefs.getDouble(kMornyeGlassClarityKey), 1);
expect(tester.takeException(), isNull);
});
testWidgets('glass clarity explains high contrast ($mode)', (tester) async {
tester.platformDispatcher.accessibilityFeaturesTestValue =
const FakeAccessibilityFeatures(highContrast: true);
addTearDown(
tester.platformDispatcher.clearAccessibilityFeaturesTestValue,
);
SharedPreferences.setMockInitialValues({
kThemeStyleKey: 'mornye',
kThemeModeKey: mode.name,
});
final prefs = await SharedPreferences.getInstance();
await openSettings(tester, prefs);
final slider = find.byKey(const ValueKey('mornye-glass-clarity'));
await tester.ensureVisible(slider);
await tester.pumpAndSettle();
expect(tester.widget<Slider>(slider).onChanged, isNull);
expect(
tester
.widget<Text>(
find.byKey(const ValueKey('mornye-glass-clarity-unavailable')),
)
.data,
'Glass stays opaque while the system’s high contrast setting is on.',
);
expect(tester.takeException(), isNull);
});
testWidgets('Mornye accent updates live and survives restart ($mode)', (
tester,
) async {
SharedPreferences.setMockInitialValues({
kThemeStyleKey: 'mornye',
kThemeModeKey: mode.name,
kSeedColorKey: 0xff123456,
});
final prefs = await SharedPreferences.getInstance();
await openSettings(tester, prefs);
final page = find.byType(AppearanceSettingsPage);
final initial = Theme.of(tester.element(page));
expect(
initial.colorScheme.primary,
MornyeTheme.accentColor(MornyeAccent.red, initial.brightness),
);
final blue = find.byTooltip('Blue');
await tester.ensureVisible(blue);
await tester.tap(blue);
await tester.pumpAndSettle();
final theme = Theme.of(tester.element(page));
expect(
theme.colorScheme.primary,
MornyeTheme.accentColor(MornyeAccent.blue, theme.brightness),
);
expect(prefs.getString(kMornyeAccentKey), 'blue');
expect(prefs.getInt(kSeedColorKey), 0xff123456);
final overlay = MornyeTheme.fromContext(
tester.element(page),
brightness: Brightness.dark,
);
expect(overlay.extension<MornyeTheme>()!.accent, MornyeAccent.blue);
expect(
overlay.colorScheme.primary,
MornyeTheme.accentColor(MornyeAccent.blue, Brightness.dark),
);
await tester.pumpWidget(const SizedBox());
await openSettings(tester, prefs);
expect(
Theme.of(tester.element(page)).colorScheme.primary,
theme.colorScheme.primary,
);
await tester.ensureVisible(find.byTooltip('Red (default)'));
await tester.tap(find.byTooltip('Red (default)'));
await tester.pumpAndSettle();
expect(
Theme.of(tester.element(page)).colorScheme.primary,
initial.colorScheme.primary,
);
expect(tester.takeException(), isNull);
});
}
for (final style in AppThemeStyle.values) {
testWidgets(
'system font persists and restores the default ($style)',
(tester) async {
SharedPreferences.setMockInitialValues({
kThemeStyleKey: style.name,
kMornyeAccentKey: 'blue',
kUseDynamicColorKey: false,
});
final prefs = await SharedPreferences.getInstance();
await openSettings(tester, prefs);
final page = find.byType(AppearanceSettingsPage);
final original = Theme.of(
tester.element(page),
).textTheme.bodyLarge!.fontFamily;
final setting = find.text('Use system font');
await tester.scrollUntilVisible(
setting,
200,
scrollable: find.byType(Scrollable).first,
);
await tester.pumpAndSettle();
await tester.tap(setting);
await tester.pumpAndSettle();
final theme = Theme.of(tester.element(page));
final systemFamily = theme.textTheme.bodyLarge!.fontFamily;
expect(systemFamily, isNot('Inter'));
expect(systemFamily, isNot('Google Sans Flex'));
expect(prefs.getBool(kUseSystemFontKey), isTrue);
if (style == AppThemeStyle.mornye) {
final overlay = MornyeTheme.fromContext(
tester.element(page),
brightness: Brightness.dark,
);
expect(overlay.extension<MornyeTheme>()!.useSystemFont, isTrue);
expect(overlay.extension<MornyeTheme>()!.accent, MornyeAccent.blue);
expect(overlay.textTheme.bodyLarge!.fontFamily, systemFamily);
}
await tester.pumpWidget(const SizedBox());
await openSettings(tester, prefs);
expect(
Theme.of(tester.element(page)).textTheme.bodyLarge!.fontFamily,
systemFamily,
);
await tester.scrollUntilVisible(
setting,
200,
scrollable: find.byType(Scrollable).first,
);
await tester.pumpAndSettle();
await tester.tap(setting);
await tester.pumpAndSettle();
expect(prefs.getBool(kUseSystemFontKey), isFalse);
expect(
Theme.of(tester.element(page)).textTheme.bodyLarge!.fontFamily,
original,
);
expect(tester.takeException(), isNull);
},
variant: const TargetPlatformVariant({
TargetPlatform.android,
TargetPlatform.iOS,
}),
);
}
}