Files
FlClash/test/views/theme_test.dart
T
chen08209 7c9627fffb feat(ui): add Liquid Glass and slide-to-pick popup menus
Changelog: Optional two-tier Liquid Glass and slide-to-pick menus
2026-10-17 17:21:43 +08:00

417 lines
13 KiB
Dart

import 'dart:io';
import 'package:fl_clash/common/feature.dart';
import 'package:fl_clash/common/system_fonts.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/providers/database.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/views/font_family.dart';
import 'package:fl_clash/views/theme.dart';
import 'package:fl_clash/views/theme_preview.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import '../helpers/test_app.dart';
import '../helpers/test_profiles.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late ProviderContainer container;
setUp(() {
container = ProviderContainer(
overrides: [profilesProvider.overrideWith(TestProfiles.new)],
);
globalState.container = container;
container.read(viewSizeProvider.notifier).value = const Size(1400, 2400);
});
tearDown(() => container.dispose());
Future<void> pumpThemeView(WidgetTester tester) async {
tester.view.physicalSize = const Size(1400, 2400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: const TestApp(child: ThemeView()),
),
);
await tester.pumpAndSettle();
}
ThemeProps readTheme() => container.read(themeSettingProvider);
group('theme mode', () {
testWidgets('defaults to following the system', (tester) async {
await pumpThemeView(tester);
expect(readTheme().themeMode, ThemeMode.system);
});
testWidgets('switches to light and back to dark', (tester) async {
await pumpThemeView(tester);
await tester.tap(find.text('Light'));
await tester.pumpAndSettle();
expect(readTheme().themeMode, ThemeMode.light);
await tester.tap(find.text('Dark'));
await tester.pumpAndSettle();
expect(readTheme().themeMode, ThemeMode.dark);
await tester.tap(find.text('Auto'));
await tester.pumpAndSettle();
expect(readTheme().themeMode, ThemeMode.system);
});
});
group('primary color', () {
testWidgets('blocks the back gesture only while a color is removable', (
tester,
) async {
await pumpThemeView(tester);
RoutePopDisposition disposition() {
return ModalRoute.of(
tester.element(find.byType(ThemeView)),
)!.popDisposition;
}
expect(disposition(), isNot(RoutePopDisposition.doNotPop));
await tester.longPress(find.byType(ColorSchemeBox).at(1));
await tester.pumpAndSettle();
expect(disposition(), RoutePopDisposition.doNotPop);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
expect(disposition(), isNot(RoutePopDisposition.doNotPop));
});
});
group('sidebar blur', () {
testWidgets('stays hidden while the feature is off', (tester) async {
await pumpThemeView(tester);
expect(find.text('Sidebar blur'), findsNothing);
});
testWidgets('shows a working toggle only on supported platforms', (
tester,
) async {
feature = const Feature(sidebarBlur: true);
addTearDown(() => feature = const Feature());
await pumpThemeView(tester);
final toggle = find.text('Sidebar blur');
expect(readTheme().sidebarBlur, isTrue);
if (!Platform.isMacOS && !Platform.isWindows) {
expect(toggle, findsNothing);
return;
}
expect(toggle, findsOneWidget);
await tester.tap(toggle);
await tester.pumpAndSettle();
expect(readTheme().sidebarBlur, isFalse);
});
});
group('liquid glass', () {
testWidgets('is off until chosen, and switches back', (tester) async {
await pumpThemeView(tester);
final group = find.ancestor(
of: find.text('Liquid Glass').first,
matching: find.byType(PreviewChoiceGroup<bool>),
);
Finder choice(String label) =>
find.descendant(of: group, matching: find.text(label)).last;
expect(readTheme().liquidGlass, isFalse);
await tester.tap(choice('Liquid Glass'));
await tester.pumpAndSettle();
expect(readTheme().liquidGlass, isTrue);
await tester.tap(choice('Standard'));
await tester.pumpAndSettle();
expect(readTheme().liquidGlass, isFalse);
});
});
group('pure black', () {
testWidgets('switches both ways', (tester) async {
await pumpThemeView(tester);
expect(readTheme().pureBlack, isFalse);
await tester.tap(find.text('Pure black'));
await tester.pumpAndSettle();
expect(readTheme().pureBlack, isTrue);
await tester.tap(find.text('Standard').first);
await tester.pumpAndSettle();
expect(readTheme().pureBlack, isFalse);
});
});
group('home navigation', () {
AppSettingProps readSetting() => container.read(appSettingProvider);
testWidgets('picks a floating or docked bottom bar', (tester) async {
await pumpThemeView(tester);
expect(readSetting().floatingNavigationBar, isTrue);
await tester.tap(find.text('Docked'));
await tester.pumpAndSettle();
expect(readSetting().floatingNavigationBar, isFalse);
await tester.tap(find.text('Floating'));
await tester.pumpAndSettle();
expect(readSetting().floatingNavigationBar, isTrue);
});
testWidgets('picks a sliding or fading tab switch', (tester) async {
await pumpThemeView(tester);
expect(readSetting().tabAnimation, TabAnimation.slide);
await tester.tap(find.text('Fade'));
await tester.pumpAndSettle();
expect(readSetting().tabAnimation, TabAnimation.fade);
await tester.tap(find.text('Slide'));
await tester.pumpAndSettle();
expect(readSetting().tabAnimation, TabAnimation.slide);
});
testWidgets('the preview switches to the destination tapped', (
tester,
) async {
await pumpThemeView(tester);
Finder inPreview(Type type) => find.descendant(
of: find.byType(ThemeLivePreview),
matching: find.byType(type),
);
int selected() =>
tester.widget<MiniScreen>(inPreview(MiniScreen)).selected;
for (final destination in [2, 1]) {
await tester.tap(inPreview(GestureDetector).at(destination));
await tester.pumpAndSettle();
expect(selected(), destination);
}
});
});
group('font family', () {
setUp(() {
final reader = systemFontFamiliesReader;
addTearDown(() => systemFontFamiliesReader = reader);
systemFontFamiliesReader = () async => ['Alpha Sans', 'Beta Serif'];
});
Finder inPage(String text) => find.descendant(
of: find.byType(FontFamilyView),
matching: find.text(text),
);
final fontsIllustration = find.descendant(
of: find.byType(FontFamilyView),
matching: find.byKey(const ValueKey(NullStatusIllustration.fonts)),
);
String? previewFamilyOf(WidgetTester tester, String text) =>
DefaultTextStyle.of(tester.element(inPage(text))).style.fontFamily;
Future<void> pumpPastArrivalHold(WidgetTester tester) async {
await tester.pump(const Duration(seconds: 1));
await tester.pumpAndSettle();
}
Future<void> openFonts(WidgetTester tester) async {
await tester.tap(find.text('Aa').first);
await tester.pumpAndSettle();
await pumpPastArrivalHold(tester);
}
testWidgets('picks an installed font on its page and back to default', (
tester,
) async {
await pumpThemeView(tester);
expect(readTheme().fontFamily, isNull);
expect(find.text('Default'), findsOneWidget);
await openFonts(tester);
expect(
SheetProvider.of(tester.element(find.byType(FontFamilyView)))?.type,
SheetType.sideSheet,
);
expect(inPage('Installed fonts'), findsOneWidget);
expect(previewFamilyOf(tester, 'Beta Serif'), 'Beta Serif');
await tester.tap(inPage('Beta Serif'));
await tester.pumpAndSettle();
expect(readTheme().fontFamily, 'Beta Serif');
expect(find.byType(FontFamilyView), findsOneWidget);
await tester.tap(inPage('Default'));
await tester.pumpAndSettle();
expect(readTheme().fontFamily, isNull);
});
testWidgets('shows the loading illustration until the sheet has opened', (
tester,
) async {
var reads = 0;
systemFontFamiliesReader = () async {
reads++;
return ['Alpha Sans'];
};
await pumpThemeView(tester);
await tester.tap(find.text('Aa').first);
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(find.byType(FontFamilyView), findsOneWidget);
expect(reads, 0);
expect(fontsIllustration, findsOneWidget);
expect(inPage('Default'), findsNothing);
await pumpPastArrivalHold(tester);
expect(reads, 1);
expect(inPage('Alpha Sans'), findsOneWidget);
expect(fontsIllustration, findsNothing);
});
testWidgets('previews the chosen font on its card', (tester) async {
container
.read(themeSettingProvider.notifier)
.update((state) => state.copyWith(fontFamily: 'Beta Serif'));
await pumpThemeView(tester);
expect(
tester.widget<Text>(find.text('Beta Serif')).style?.fontFamily,
'Beta Serif',
);
expect(
tester.widget<Text>(find.text('Aa').first).style?.fontFamily,
'Beta Serif',
);
});
testWidgets('gives each name its own font one per frame', (tester) async {
const families = ['Preview One', 'Preview Two', 'Preview Three'];
var loaded = false;
systemFontFamiliesReader = () async {
loaded = true;
return families;
};
await pumpThemeView(tester);
await tester.tap(find.text('Aa').first);
for (var i = 0; i < 20 && !loaded; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
await tester.pump(const Duration(seconds: 1));
int shown() => families
.where(
(family) =>
inPage(family).evaluate().isNotEmpty &&
previewFamilyOf(tester, family) == family,
)
.length;
final counts = [shown()];
while (counts.last < families.length && counts.length < 10) {
await tester.pump();
counts.add(shown());
}
expect(counts.first, lessThan(families.length));
expect(counts.last, families.length);
for (var i = 1; i < counts.length; i++) {
expect(counts[i] - counts[i - 1], lessThanOrEqualTo(1));
}
});
testWidgets('still offers the default when no font can be listed', (
tester,
) async {
systemFontFamiliesReader = () async => throw const OSError('fc', 1);
await pumpThemeView(tester);
await openFonts(tester);
expect(inPage('Default'), findsOneWidget);
expect(inPage('Installed fonts'), findsNothing);
expect(fontsIllustration, findsNothing);
});
});
group('text scale', () {
testWidgets('follows the system until custom is picked', (tester) async {
await pumpThemeView(tester);
expect(readTheme().textScale.enable, isFalse);
expect(tester.widget<Slider>(find.byType(Slider)).onChanged, isNull);
await tester.tap(find.text('Custom'));
await tester.pumpAndSettle();
expect(readTheme().textScale.enable, isTrue);
expect(tester.widget<Slider>(find.byType(Slider)).onChanged, isNotNull);
await tester.tap(find.text('Follow system'));
await tester.pumpAndSettle();
expect(readTheme().textScale.enable, isFalse);
});
testWidgets('writes the scale only when the drag ends', (tester) async {
await pumpThemeView(tester);
await tester.tap(find.text('Custom'));
await tester.pumpAndSettle();
final before = readTheme().textScale.scale;
final slider = find.byType(Slider);
final gesture = await tester.startGesture(tester.getCenter(slider));
await gesture.moveBy(const Offset(120, 0));
await tester.pump();
expect(readTheme().textScale.scale, before);
await gesture.up();
await tester.pumpAndSettle();
expect(readTheme().textScale.scale, isNot(before));
});
testWidgets('resets a custom scale to 100%', (tester) async {
container
.read(themeSettingProvider.notifier)
.update(
(state) => state.copyWith.textScale(enable: true, scale: 1.2),
);
await pumpThemeView(tester);
expect(find.text('120%'), findsOneWidget);
await tester.tap(find.byTooltip('Reset').last);
await tester.pumpAndSettle();
expect(readTheme().textScale.scale, 1);
expect(find.text('100%'), findsOneWidget);
});
});
}