Files
clide/test/builtin/theme_picker/widget_test.dart
T
jpmschweitzerandClaude Opus 4.8 6d0ebab721 chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:11:53 +02:00

192 lines
8.7 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: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');
});
});
}