Files
clide/test/builtin/theme_picker/widget_test.dart
T
jpmschweitzerandClaude Opus 4.8 21d435d1f1 feat(settings): Inter as default UI font + UI font picker (T-460)
Vendor Inter (variable + italic, OFL) under assets/fonts/inter/ and make it
the default application UI face, replacing Josefin Sans (which stays bundled
as a selectable option). pubspec font family + licenses.yaml entry per D-42.

Settings → Appearance gains a UI-font select (Inter / Josefin Sans). The root
DefaultTextStyle reads app.ui.font (kUiFontSettingKey) over the default and
re-applies live on settings change, so a pick takes effect immediately. Bump
the default UI weight to w400 — Inter reads better at Regular than Josefin's
Light.

The monospace picker is deferred to T-471: clideMonoFamily is a const at ~38
call sites (not inherited), so a live mono picker needs the family routed
through context first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:15:01 +02:00

216 lines
10 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:clide/builtin/theme_picker/src/theme_status_item.dart';
import 'package:clide/builtin/theme_picker/theme_picker.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart' show kUiFontSettingKey;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
import '../../helpers/widget_harness.dart';
ThemeDefinition _def(String name) => ThemeDefinition(
name: name,
displayName: name,
dark: true,
palette: Palette(const {
'primary': Color(0xFF00A3D2),
'accent': Color(0xFFFA5F8B),
'background': Color(0xFF21262F),
'surface': Color(0xFF393E48),
'panel': Color(0xFF292E38),
'foreground': Color(0xFFE2E8F5),
'success': Color(0xFF00AB9A),
'warning': Color(0xFFD08447),
'error': Color(0xFFF06C6F),
}),
);
void main() {
group('ThemePickerExtension', () {
late KernelFixture f;
setUp(() async {
f = await KernelFixture.create(
bundledThemes: [_def('summer-night'), _def('forest')],
i18nCatalogs: {
'builtin.theme-picker': {
const Locale('en', 'US'): const {
'modal.title': {'translation': 'Settings'},
'section.appearance': {'translation': 'Appearance'},
'toggle.highContrast': {'translation': 'High contrast'},
'modal.cancel': {'translation': 'Cancel'},
'modal.cancel.hint': {'translation': 'Dismiss'},
'row.select.hint': {'translation': 'Activate this theme'},
},
},
},
);
});
tearDown(() async => f.dispose());
test('contributes a theme.pick command', () async {
f.services.extensions.register(ThemePickerExtension());
await f.services.extensions.activateAll();
expect(f.services.commands.get('theme.pick'), isNotNull);
});
test('default binding ctrl+k is registered', () async {
f.services.extensions.register(ThemePickerExtension());
await f.services.extensions.activateAll();
expect(f.services.keybindings.commandFor(Keybinding.parse('ctrl+k')), 'theme.pick');
});
testWidgets('settings modal lists base themes + a High contrast toggle', (tester) async {
await tester.pumpWidget(harness(f, SettingsView(controller: f.services.theme, onDismiss: ([_]) {})));
// Each row renders both displayName and name; displayName==name in
// test fixtures so the label appears twice per row.
expect(find.text('summer-night'), findsNWidgets(2));
expect(find.text('forest'), findsNWidgets(2));
// Promoted to a Settings modal with an Appearance section (T-238).
expect(find.text('Settings'), findsOneWidget);
expect(find.text('Appearance'), findsOneWidget);
expect(find.text('High contrast'), findsOneWidget);
expect(find.text('Cancel'), findsOneWidget);
});
testWidgets('tapping a row calls controller.select + onDismiss', (tester) async {
String? dismissed;
await tester.pumpWidget(harness(f, SettingsView(controller: f.services.theme, onDismiss: ([v]) => dismissed = v)));
await tester.tap(find.bySemanticsLabel('forest'));
await tester.pumpAndSettle();
expect(f.services.theme.currentName, 'forest');
expect(dismissed, 'forest');
});
testWidgets('Cancel button dismisses without selecting', (tester) async {
String? dismissed = 'not-called';
await tester.pumpWidget(harness(f, SettingsView(controller: f.services.theme, onDismiss: ([v]) => dismissed = v)));
await tester.tap(find.bySemanticsLabel('Cancel'));
await tester.pumpAndSettle();
expect(dismissed, isNull);
});
testWidgets('settings modal hides -hc siblings; High contrast applies them (T-238)', (tester) async {
// KernelFixture.create does real file I/O (temp dir + boot); awaiting it
// inside the testWidgets fake-async body would hang (the T-122 trap), so
// build it on the real event loop via runAsync.
late KernelFixture hf;
await tester.runAsync(() async => hf = await KernelFixture.create(bundledThemes: [_def('paper'), _def('paper-hc')]));
addTearDown(hf.dispose);
await tester.pumpWidget(harness(hf, SettingsView(controller: hf.services.theme, onDismiss: ([_]) {})));
// Base theme listed once (displayName + muted name); the -hc sibling is
// folded into the toggle, not shown as a row.
expect(find.text('paper'), findsNWidgets(2));
expect(find.text('paper-hc'), findsNothing);
// Toggling High contrast applies the -hc sibling live.
expect(hf.services.theme.currentName, 'paper');
await tester.tap(find.bySemanticsLabel('High contrast'));
await tester.pump();
expect(hf.services.theme.currentName, 'paper-hc');
});
testWidgets('status switcher shows the active theme and opens a popover (T-234)', (tester) async {
// anchoredHarness gives a real 800×600 overlay with the control pinned at
// its real corner (bottom-right) so the above-anchored popover renders and
// hit-tests on-screen — the shared canSizeOverlay harness mispositions it.
await tester.pumpWidget(anchoredHarness(f, const ThemeSwitcherStatusItem(), alignment: Alignment.bottomRight));
await tester.pump();
// Controller starts on the first bundled theme.
expect(f.services.theme.currentName, 'summer-night');
expect(find.text('summer-night'), findsOneWidget);
// Open the popover; it lists every theme (the other one appears).
await tester.tap(find.bySemanticsLabel('Theme: summer-night'));
await tester.pumpAndSettle();
expect(find.text('forest'), findsOneWidget);
});
testWidgets('selecting in the popover applies live and closes (T-234)', (tester) async {
// anchoredHarness gives a real 800×600 overlay with the control pinned at
// its real corner (bottom-right) so the above-anchored popover renders and
// hit-tests on-screen — the shared canSizeOverlay harness mispositions it.
await tester.pumpWidget(anchoredHarness(f, const ThemeSwitcherStatusItem(), alignment: Alignment.bottomRight));
await tester.pump();
await tester.tap(find.bySemanticsLabel('Theme: summer-night'));
await tester.pumpAndSettle();
await tester.tap(find.bySemanticsLabel('forest'));
await tester.pumpAndSettle();
expect(f.services.theme.currentName, 'forest'); // applied live
// Popover closed: the only 'forest' left is the trigger label.
expect(find.bySemanticsLabel('forest'), findsNothing);
});
testWidgets('Esc dismisses the popover without changing the theme (T-234)', (tester) async {
// anchoredHarness gives a real 800×600 overlay with the control pinned at
// its real corner (bottom-right) so the above-anchored popover renders and
// hit-tests on-screen — the shared canSizeOverlay harness mispositions it.
await tester.pumpWidget(anchoredHarness(f, const ThemeSwitcherStatusItem(), alignment: Alignment.bottomRight));
await tester.pump();
await tester.tap(find.bySemanticsLabel('Theme: summer-night'));
await tester.pumpAndSettle();
expect(find.text('forest'), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
expect(find.text('forest'), findsNothing); // popover gone
expect(f.services.theme.currentName, 'summer-night'); // unchanged
});
test('_pick before activate returns a not-activated error', () async {
// Reach the command's run callback without going through activate —
// _ctx is still null, so _pick hits the defensive error branch.
final ext = ThemePickerExtension();
final cmd = ext.contributions.whereType<CommandContribution>().single;
final resp = await cmd.run(const []);
expect(resp.ok, isFalse);
expect(resp.error?.message, contains('not activated'));
});
testWidgets('_pick (after activate) opens a dialog and resolves the user selection', (tester) async {
f.services.extensions.register(ThemePickerExtension());
await f.services.extensions.activateAll();
// Pump a tree so dialog has a parent BuildContext to render under.
await tester.pumpWidget(harness(f, const SizedBox()));
await tester.pump();
// Kick off the command. _pick awaits ctx.dialog.show; the future
// resolves once dialog is dismissed.
final responseFuture = f.services.commands.execute('theme.pick');
await tester.pump();
expect(f.services.dialog.isOpen, isTrue);
// Simulate user picking 'forest' and closing.
f.services.dialog.dismiss('forest');
final resp = await responseFuture;
expect(resp.ok, isTrue);
expect(resp.data['selected'], 'forest');
});
test('contributes an Appearance category + theme.picker control (T-452)', () {
final ext = ThemePickerExtension();
final cat = ext.contributions.whereType<SettingsCategoryContribution>().firstWhere((c) => c.id == 'appearance').category;
expect(cat.title, 'Appearance');
expect(ext.contributions.whereType<SettingsControlContribution>().any((c) => c.customId == 'theme.picker'), isTrue);
});
test('Appearance includes a UI font picker (T-460)', () {
final cat = ThemePickerExtension().contributions.whereType<SettingsCategoryContribution>().firstWhere((c) => c.id == 'appearance').category;
final font = cat.sections.expand((s) => s.fields).firstWhere((f) => f.key == kUiFontSettingKey);
expect(font.kind, SettingsFieldKind.select);
expect(font.options.map((o) => o.value), containsAll(['Inter', 'JosefinSans']));
});
testWidgets('AppearanceThemeControl lists base themes and applies a pick (T-452)', (tester) async {
await tester.pumpWidget(harness(f, const SizedBox(width: 420, child: AppearanceThemeControl())));
expect(find.text('summer-night'), findsOneWidget);
expect(find.text('forest'), findsOneWidget);
await tester.tap(find.text('forest'));
await tester.pump();
expect(f.services.theme.currentName, 'forest');
});
});
}