mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-02 22:26:52 +02:00
327 lines
12 KiB
Dart
327 lines
12 KiB
Dart
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,
|
||
}),
|
||
);
|
||
}
|
||
}
|