diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index ed0b1fb9..a726698d 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -219,6 +219,10 @@ class AppController extends ChangeNotifier { _settingsController.buildSecretReferences(); List get secretAuditTrail => _settingsController.auditTrail; List get runtimeLogs => _runtime.logs; + List get assistantNavigationDestinations => + normalizeAssistantNavigationDestinations( + settings.assistantNavigationDestinations, + ); List get chatMessages { final items = List.from(_chatController.messages); @@ -627,6 +631,22 @@ class AppController extends ChangeNotifier { } } + Future toggleAssistantNavigationDestination( + WorkspaceDestination destination, + ) async { + if (!kAssistantNavigationDestinationCandidates.contains(destination)) { + return; + } + final current = assistantNavigationDestinations; + final next = current.contains(destination) + ? current.where((item) => item != destination).toList(growable: false) + : [...current, destination]; + await saveSettings( + settings.copyWith(assistantNavigationDestinations: next), + refreshAfterSave: false, + ); + } + Future testOllamaConnection({required bool cloud}) { return _settingsController.testOllamaConnection(cloud: cloud); } diff --git a/lib/app/app_shell.dart b/lib/app/app_shell.dart index 7a3a59d4..6ee4892c 100644 --- a/lib/app/app_shell.dart +++ b/lib/app/app_shell.dart @@ -14,6 +14,7 @@ import '../features/tasks/tasks_page.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../theme/app_palette.dart'; +import '../widgets/assistant_focus_panel.dart'; import '../widgets/detail_drawer.dart'; import '../widgets/pane_resize_handle.dart'; import '../widgets/sidebar_navigation.dart'; @@ -235,6 +236,11 @@ class _AppShellState extends State { sidebarState == AppSidebarState.expanded ? expandedSidebarWidth : null, + favoriteDestinations: controller + .assistantNavigationDestinations + .toSet(), + onToggleFavorite: + controller.toggleAssistantNavigationDestination, ), if (sidebarState == AppSidebarState.expanded && !embedSidebarIntoAssistant) @@ -329,35 +335,7 @@ class _AppShellState extends State { navigationPanelBuilder: widget.controller.sidebarState == AppSidebarState.hidden ? null - : (contentWidth) => SidebarNavigation( - currentSection: widget.controller.destination, - sidebarState: AppSidebarState.expanded, - appLanguage: widget.controller.appLanguage, - themeMode: widget.controller.themeMode, - onSectionChanged: widget.controller.navigateTo, - onToggleLanguage: widget.controller.toggleAppLanguage, - onCycleSidebarState: widget.controller.cycleSidebarState, - onExpandFromCollapsed: () => - widget.controller.setSidebarState(AppSidebarState.expanded), - onOpenAccount: () => - widget.controller.navigateTo(WorkspaceDestination.account), - onOpenThemeToggle: () => widget.controller.setThemeMode( - widget.controller.themeMode == ThemeMode.dark - ? ThemeMode.light - : ThemeMode.dark, - ), - accountName: - widget.controller.settings.accountUsername.trim().isEmpty - ? appText('本地操作员', 'Local Operator') - : widget.controller.settings.accountUsername, - accountSubtitle: - widget.controller.settings.accountWorkspace.trim().isEmpty - ? appText('账号', 'Account') - : widget.controller.settings.accountWorkspace, - expandedWidthOverride: contentWidth, - marginOverride: EdgeInsets.zero, - showCollapseControl: false, - ), + : (_) => AssistantFocusPanel(controller: widget.controller), showStandaloneTaskRail: false, unifiedPaneStartsCollapsed: widget.controller.sidebarState == AppSidebarState.collapsed, diff --git a/lib/models/app_models.dart b/lib/models/app_models.dart index b7a7cdf5..2c77f0e5 100644 --- a/lib/models/app_models.dart +++ b/lib/models/app_models.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../i18n/app_language.dart'; - enum WorkspaceDestination { +enum WorkspaceDestination { assistant, tasks, skills, @@ -14,7 +14,7 @@ import '../i18n/app_language.dart'; aiGateway, settings, account, - } +} extension WorkspaceDestinationCopy on WorkspaceDestination { String get label => switch (this) { @@ -91,6 +91,55 @@ extension WorkspaceDestinationCopy on WorkspaceDestination { 'Identity, workspace switching, and session management.', ), }; + + static WorkspaceDestination? fromJsonValue(String? value) { + if (value == null || value.trim().isEmpty) { + return null; + } + for (final item in WorkspaceDestination.values) { + if (item.name == value.trim()) { + return item; + } + } + return null; + } +} + +const List kAssistantNavigationDestinationDefaults = + [ + WorkspaceDestination.tasks, + WorkspaceDestination.skills, + WorkspaceDestination.nodes, + WorkspaceDestination.agents, + WorkspaceDestination.aiGateway, + ]; + +const List kAssistantNavigationDestinationCandidates = + [ + WorkspaceDestination.tasks, + WorkspaceDestination.skills, + WorkspaceDestination.nodes, + WorkspaceDestination.agents, + WorkspaceDestination.mcpServer, + WorkspaceDestination.clawHub, + WorkspaceDestination.secrets, + WorkspaceDestination.aiGateway, + WorkspaceDestination.settings, + ]; + +List normalizeAssistantNavigationDestinations( + Iterable destinations, +) { + final allowed = kAssistantNavigationDestinationCandidates.toSet(); + final seen = {}; + final normalized = []; + for (final destination in destinations) { + if (!allowed.contains(destination) || !seen.add(destination)) { + continue; + } + normalized.add(destination); + } + return normalized; } enum StatusTone { neutral, accent, success, warning, danger } diff --git a/lib/runtime/runtime_models.dart b/lib/runtime/runtime_models.dart index 6c32f379..e715a63d 100644 --- a/lib/runtime/runtime_models.dart +++ b/lib/runtime/runtime_models.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import '../i18n/app_language.dart'; +import '../models/app_models.dart'; enum RuntimeConnectionMode { unconfigured, local, remote } @@ -527,6 +528,7 @@ class SettingsSnapshot { required this.accountLocalMode, required this.assistantExecutionTarget, required this.assistantPermissionLevel, + required this.assistantNavigationDestinations, }); final AppLanguage appLanguage; @@ -554,6 +556,7 @@ class SettingsSnapshot { final bool accountLocalMode; final AssistantExecutionTarget assistantExecutionTarget; final AssistantPermissionLevel assistantPermissionLevel; + final List assistantNavigationDestinations; factory SettingsSnapshot.defaults() { return SettingsSnapshot( @@ -582,6 +585,7 @@ class SettingsSnapshot { accountLocalMode: true, assistantExecutionTarget: AssistantExecutionTarget.local, assistantPermissionLevel: AssistantPermissionLevel.defaultAccess, + assistantNavigationDestinations: kAssistantNavigationDestinationDefaults, ); } @@ -611,6 +615,7 @@ class SettingsSnapshot { bool? accountLocalMode, AssistantExecutionTarget? assistantExecutionTarget, AssistantPermissionLevel? assistantPermissionLevel, + List? assistantNavigationDestinations, }) { return SettingsSnapshot( appLanguage: appLanguage ?? this.appLanguage, @@ -640,6 +645,9 @@ class SettingsSnapshot { assistantExecutionTarget ?? this.assistantExecutionTarget, assistantPermissionLevel: assistantPermissionLevel ?? this.assistantPermissionLevel, + assistantNavigationDestinations: + assistantNavigationDestinations ?? + this.assistantNavigationDestinations, ); } @@ -670,10 +678,26 @@ class SettingsSnapshot { 'accountLocalMode': accountLocalMode, 'assistantExecutionTarget': assistantExecutionTarget.name, 'assistantPermissionLevel': assistantPermissionLevel.name, + 'assistantNavigationDestinations': assistantNavigationDestinations + .map((item) => item.name) + .toList(growable: false), }; } factory SettingsSnapshot.fromJson(Map json) { + final rawAssistantNavigationDestinations = + json['assistantNavigationDestinations']; + final assistantNavigationDestinations = + rawAssistantNavigationDestinations is List + ? normalizeAssistantNavigationDestinations( + rawAssistantNavigationDestinations + .map( + (item) => + WorkspaceDestinationCopy.fromJsonValue(item?.toString()), + ) + .whereType(), + ) + : kAssistantNavigationDestinationDefaults; return SettingsSnapshot( appLanguage: AppLanguageCopy.fromJsonValue( json['appLanguage'] as String?, @@ -735,6 +759,7 @@ class SettingsSnapshot { assistantPermissionLevel: AssistantPermissionLevelCopy.fromJsonValue( json['assistantPermissionLevel'] as String?, ), + assistantNavigationDestinations: assistantNavigationDestinations, ); } diff --git a/lib/widgets/assistant_focus_panel.dart b/lib/widgets/assistant_focus_panel.dart new file mode 100644 index 00000000..0d6b2437 --- /dev/null +++ b/lib/widgets/assistant_focus_panel.dart @@ -0,0 +1,256 @@ +import 'package:flutter/material.dart'; + +import '../app/app_controller.dart'; +import '../i18n/app_language.dart'; +import '../models/app_models.dart'; +import '../theme/app_palette.dart'; +import 'surface_card.dart'; + +class AssistantFocusPanel extends StatelessWidget { + const AssistantFocusPanel({super.key, required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final palette = context.palette; + final favorites = controller.assistantNavigationDestinations; + final available = kAssistantNavigationDestinationCandidates + .where((item) => !favorites.contains(item)) + .toList(growable: false); + + return SurfaceCard( + borderRadius: 16, + padding: EdgeInsets.zero, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(14, 14, 14, 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appText('关注入口', 'Focused navigation'), + key: const Key('assistant-focus-panel-title'), + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 6), + Text( + appText( + '把常看的功能菜单放到这里。左侧菜单点亮星标,也会加入这个关注面板。', + 'Pin the destinations you care about here. Starred menu items also appear in this focused panel.', + ), + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textSecondary, + height: 1.35, + ), + ), + ], + ), + ), + Divider(height: 1, color: palette.strokeSoft), + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB(10, 12, 10, 10), + children: [ + Text( + appText('已关注', 'Following'), + style: theme.textTheme.labelLarge?.copyWith( + color: palette.textMuted, + ), + ), + const SizedBox(height: 8), + if (favorites.isEmpty) + _AssistantFocusEmptyState( + message: appText( + '还没有关注入口。给左侧菜单点星标,或从下面添加。', + 'No focused entries yet. Star a menu item on the left or add one below.', + ), + ) + else + ...favorites.map( + (destination) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _AssistantFocusTile( + destination: destination, + selected: controller.destination == destination, + onOpen: () => controller.navigateTo(destination), + onToggleFavorite: () async { + await controller.toggleAssistantNavigationDestination( + destination, + ); + }, + ), + ), + ), + const SizedBox(height: 10), + Text( + appText('添加入口', 'Add destinations'), + style: theme.textTheme.labelLarge?.copyWith( + color: palette.textMuted, + ), + ), + const SizedBox(height: 8), + if (available.isEmpty) + _AssistantFocusEmptyState( + message: appText( + '候选菜单都已经加入关注入口了。', + 'All available destinations are already pinned.', + ), + ) + else + Wrap( + spacing: 8, + runSpacing: 8, + children: available + .map( + (destination) => ActionChip( + key: ValueKey( + 'assistant-focus-add-${destination.name}', + ), + avatar: Icon(destination.icon, size: 16), + label: Text(destination.label), + onPressed: () async { + await controller + .toggleAssistantNavigationDestination( + destination, + ); + }, + ), + ) + .toList(growable: false), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _AssistantFocusTile extends StatelessWidget { + const _AssistantFocusTile({ + required this.destination, + required this.selected, + required this.onOpen, + required this.onToggleFavorite, + }); + + final WorkspaceDestination destination; + final bool selected; + final VoidCallback onOpen; + final Future Function() onToggleFavorite; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final palette = context.palette; + + return Material( + color: selected + ? palette.accentMuted.withValues(alpha: 0.5) + : Colors.transparent, + borderRadius: BorderRadius.circular(14), + child: InkWell( + key: ValueKey('assistant-focus-item-${destination.name}'), + borderRadius: BorderRadius.circular(14), + onTap: onOpen, + child: Container( + padding: const EdgeInsets.fromLTRB(12, 12, 10, 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: selected ? palette.accent : palette.strokeSoft, + ), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + destination.icon, + size: 18, + color: selected ? palette.accent : palette.textSecondary, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + destination.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 3), + Text( + destination.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textSecondary, + height: 1.25, + ), + ), + ], + ), + ), + IconButton( + key: ValueKey( + 'assistant-focus-remove-${destination.name}', + ), + tooltip: appText('取消关注', 'Remove from focused panel'), + onPressed: () async { + await onToggleFavorite(); + }, + icon: Icon(Icons.star_rounded, color: palette.accent), + ), + ], + ), + ), + ), + ); + } +} + +class _AssistantFocusEmptyState extends StatelessWidget { + const _AssistantFocusEmptyState({required this.message}); + + final String message; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: palette.strokeSoft), + ), + child: Text( + message, + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textSecondary, + height: 1.35, + ), + ), + ); + } +} diff --git a/lib/widgets/sidebar_navigation.dart b/lib/widgets/sidebar_navigation.dart index 7e99d3df..d8701694 100644 --- a/lib/widgets/sidebar_navigation.dart +++ b/lib/widgets/sidebar_navigation.dart @@ -23,6 +23,8 @@ class SidebarNavigation extends StatelessWidget { this.expandedWidthOverride, this.marginOverride, this.showCollapseControl = true, + this.favoriteDestinations = const {}, + this.onToggleFavorite, }); final WorkspaceDestination currentSection; @@ -40,6 +42,8 @@ class SidebarNavigation extends StatelessWidget { final double? expandedWidthOverride; final EdgeInsetsGeometry? marginOverride; final bool showCollapseControl; + final Set favoriteDestinations; + final Future Function(WorkspaceDestination section)? onToggleFavorite; static const _primarySections = [ WorkspaceDestination.assistant, @@ -111,6 +115,8 @@ class SidebarNavigation extends StatelessWidget { currentSection: currentSection, collapsed: isCollapsed, emphasis: _SidebarItemEmphasis.primary, + favoriteDestinations: favoriteDestinations, + onToggleFavorite: onToggleFavorite, onSectionChanged: onSectionChanged, ), const SizedBox(height: AppSpacing.md), @@ -120,6 +126,8 @@ class SidebarNavigation extends StatelessWidget { currentSection: currentSection, collapsed: isCollapsed, emphasis: _SidebarItemEmphasis.secondary, + favoriteDestinations: favoriteDestinations, + onToggleFavorite: onToggleFavorite, onSectionChanged: onSectionChanged, ), ], @@ -132,6 +140,8 @@ class SidebarNavigation extends StatelessWidget { currentSection: currentSection, collapsed: isCollapsed, emphasis: _SidebarItemEmphasis.secondary, + favoriteDestinations: favoriteDestinations, + onToggleFavorite: onToggleFavorite, onSectionChanged: onSectionChanged, ), const SizedBox(height: AppSpacing.sm), @@ -213,6 +223,8 @@ class _SidebarSectionGroup extends StatelessWidget { required this.currentSection, required this.collapsed, required this.emphasis, + required this.favoriteDestinations, + this.onToggleFavorite, required this.onSectionChanged, }); @@ -221,6 +233,8 @@ class _SidebarSectionGroup extends StatelessWidget { final WorkspaceDestination currentSection; final bool collapsed; final _SidebarItemEmphasis emphasis; + final Set favoriteDestinations; + final Future Function(WorkspaceDestination section)? onToggleFavorite; final ValueChanged onSectionChanged; @override @@ -250,6 +264,16 @@ class _SidebarSectionGroup extends StatelessWidget { selected: currentSection == section, collapsed: collapsed, emphasis: emphasis, + favorite: favoriteDestinations.contains(section), + showFavoriteToggle: + !collapsed && + onToggleFavorite != null && + kAssistantNavigationDestinationCandidates.contains(section), + onToggleFavorite: onToggleFavorite == null + ? null + : () async { + await onToggleFavorite!(section); + }, onTap: () => onSectionChanged(section), ), ), @@ -265,6 +289,9 @@ class _SidebarNavItem extends StatefulWidget { required this.selected, required this.collapsed, required this.emphasis, + required this.favorite, + required this.showFavoriteToggle, + this.onToggleFavorite, required this.onTap, }); @@ -272,6 +299,9 @@ class _SidebarNavItem extends StatefulWidget { final bool selected; final bool collapsed; final _SidebarItemEmphasis emphasis; + final bool favorite; + final bool showFavoriteToggle; + final Future Function()? onToggleFavorite; final VoidCallback onTap; @override @@ -352,6 +382,29 @@ class _SidebarNavItemState extends State<_SidebarNavItem> { ), ), ), + if (widget.showFavoriteToggle) + IconButton( + key: ValueKey( + 'sidebar-favorite-${widget.section.name}', + ), + tooltip: widget.favorite + ? appText('取消关注', 'Remove from focused panel') + : appText('加入关注', 'Add to focused panel'), + visualDensity: VisualDensity.compact, + splashRadius: 16, + onPressed: () async { + await widget.onToggleFavorite?.call(); + }, + icon: Icon( + widget.favorite + ? Icons.star_rounded + : Icons.star_outline_rounded, + size: 18, + color: widget.favorite + ? palette.accent + : palette.textMuted, + ), + ), ], ), ), @@ -494,6 +547,8 @@ class SidebarFooter extends StatelessWidget { selected: currentSection == WorkspaceDestination.settings, collapsed: false, emphasis: _SidebarItemEmphasis.secondary, + favorite: false, + showFavoriteToggle: false, onTap: onOpenSettings, ), const SizedBox(height: AppSpacing.xs), diff --git a/test/runtime/app_controller_navigation_favorites_test.dart b/test/runtime/app_controller_navigation_favorites_test.dart new file mode 100644 index 00000000..2fee89f5 --- /dev/null +++ b/test/runtime/app_controller_navigation_favorites_test.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:xworkmate/app/app_controller.dart'; +import 'package:xworkmate/models/app_models.dart'; + +void main() { + test('AppController toggles focused navigation destinations', () async { + SharedPreferences.setMockInitialValues({}); + final controller = AppController(); + addTearDown(controller.dispose); + + await _waitFor(() => !controller.initializing); + + await controller.saveSettings( + controller.settings.copyWith( + assistantNavigationDestinations: const [ + WorkspaceDestination.tasks, + WorkspaceDestination.skills, + ], + ), + refreshAfterSave: false, + ); + + await controller.toggleAssistantNavigationDestination( + WorkspaceDestination.aiGateway, + ); + expect( + controller.assistantNavigationDestinations, + const [ + WorkspaceDestination.tasks, + WorkspaceDestination.skills, + WorkspaceDestination.aiGateway, + ], + ); + + await controller.toggleAssistantNavigationDestination( + WorkspaceDestination.tasks, + ); + expect( + controller.assistantNavigationDestinations, + const [ + WorkspaceDestination.skills, + WorkspaceDestination.aiGateway, + ], + ); + }); +} + +Future _waitFor( + bool Function() condition, { + Duration timeout = const Duration(seconds: 5), +}) async { + final deadline = DateTime.now().add(timeout); + while (!condition()) { + if (DateTime.now().isAfter(deadline)) { + throw TimeoutException('condition not met within $timeout'); + } + await Future.delayed(const Duration(milliseconds: 20)); + } +} diff --git a/test/runtime/secure_config_store_test.dart b/test/runtime/secure_config_store_test.dart index d062ef39..2eeb6708 100644 --- a/test/runtime/secure_config_store_test.dart +++ b/test/runtime/secure_config_store_test.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:xworkmate/models/app_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; @@ -17,6 +18,11 @@ void main() { accountWorkspace: 'QA', codeAgentRuntimeMode: CodeAgentRuntimeMode.externalCli, codexCliPath: '/opt/homebrew/bin/codex', + assistantNavigationDestinations: const [ + WorkspaceDestination.tasks, + WorkspaceDestination.aiGateway, + WorkspaceDestination.secrets, + ], gateway: GatewayConnectionProfile.defaults().copyWith( host: 'gateway.example.com', port: 9443, @@ -39,6 +45,14 @@ void main() { CodeAgentRuntimeMode.externalCli, ); expect(loadedSnapshot.codexCliPath, '/opt/homebrew/bin/codex'); + expect( + loadedSnapshot.assistantNavigationDestinations, + const [ + WorkspaceDestination.tasks, + WorkspaceDestination.aiGateway, + WorkspaceDestination.secrets, + ], + ); expect(loadedSnapshot.gateway.host, 'gateway.example.com'); expect(loadedSnapshot.gateway.port, 9443); expect(secureRefs['gateway_token'], 'token-secret'); diff --git a/test/widgets/assistant_focus_panel_test.dart b/test/widgets/assistant_focus_panel_test.dart new file mode 100644 index 00000000..b184e59d --- /dev/null +++ b/test/widgets/assistant_focus_panel_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/models/app_models.dart'; +import 'package:xworkmate/widgets/assistant_focus_panel.dart'; + +import '../test_support.dart'; + +void main() { + testWidgets( + 'AssistantFocusPanel renders focused and available destinations', + (WidgetTester tester) async { + final controller = await createTestController(tester); + await controller.saveSettings( + controller.settings.copyWith( + assistantNavigationDestinations: const [ + WorkspaceDestination.tasks, + ], + ), + refreshAfterSave: false, + ); + + await pumpPage( + tester, + child: AssistantFocusPanel(controller: controller), + ); + + expect( + find.byKey(const Key('assistant-focus-panel-title')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('assistant-focus-item-tasks')), + findsOneWidget, + ); + + expect( + find.byKey(const ValueKey('assistant-focus-add-aiGateway')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('assistant-focus-remove-tasks')), + findsOneWidget, + ); + }, + ); +} diff --git a/test/widgets/sidebar_navigation_test.dart b/test/widgets/sidebar_navigation_test.dart index 1652d70d..92cebb26 100644 --- a/test/widgets/sidebar_navigation_test.dart +++ b/test/widgets/sidebar_navigation_test.dart @@ -14,6 +14,7 @@ void main() { var themeToggled = 0; var sidebarCycled = 0; var accountOpened = 0; + var favoriteToggled = 0; await tester.pumpWidget( MaterialApp( @@ -32,6 +33,14 @@ void main() { onOpenThemeToggle: () => themeToggled++, accountName: 'Tester', accountSubtitle: 'Workspace', + favoriteDestinations: const { + WorkspaceDestination.tasks, + }, + onToggleFavorite: (value) async { + if (value == WorkspaceDestination.tasks) { + favoriteToggled++; + } + }, ), ), ), @@ -45,6 +54,12 @@ void main() { await tester.pumpAndSettle(); expect(selected, WorkspaceDestination.tasks); + await tester.tap( + find.byKey(const ValueKey('sidebar-favorite-tasks')), + ); + await tester.pumpAndSettle(); + expect(favoriteToggled, 1); + await tester.tap(find.byTooltip('切换语言')); await tester.pumpAndSettle(); expect(languageToggled, 1);