diff --git a/lib/app/app_shell_desktop.dart b/lib/app/app_shell_desktop.dart index 3cc18f06..dc3ca2c7 100644 --- a/lib/app/app_shell_desktop.dart +++ b/lib/app/app_shell_desktop.dart @@ -222,7 +222,16 @@ class _AppShellState extends State { sidebarState: sidebarState, appLanguage: controller.appLanguage, themeMode: controller.themeMode, - onSectionChanged: controller.navigateTo, + onSectionChanged: (destination) { + if (destination == + WorkspaceDestination.settings) { + controller.openSettings( + tab: SettingsTab.general, + ); + return; + } + controller.navigateTo(destination); + }, onToggleLanguage: controller.toggleAppLanguage, onCycleSidebarState: controller.cycleSidebarState, onExpandFromCollapsed: () => controller diff --git a/lib/app/app_shell_web.dart b/lib/app/app_shell_web.dart index 7ca9b651..9691e37e 100644 --- a/lib/app/app_shell_web.dart +++ b/lib/app/app_shell_web.dart @@ -152,7 +152,13 @@ class _AppShellState extends State { sidebarState: _sidebarState, appLanguage: controller.appLanguage, themeMode: controller.themeMode, - onSectionChanged: controller.navigateTo, + onSectionChanged: (destination) { + if (destination == WorkspaceDestination.settings) { + controller.openSettings(tab: SettingsTab.general); + return; + } + controller.navigateTo(destination); + }, onToggleLanguage: controller.toggleAppLanguage, onCycleSidebarState: _cycleSidebarState, onExpandFromCollapsed: () { @@ -238,7 +244,10 @@ class _AppShellState extends State { WorkspaceDestination.aiGateway => WebAiGatewayPage( controller: controller, ), - WorkspaceDestination.settings => WebSettingsPage(controller: controller), + WorkspaceDestination.settings => WebSettingsPage( + controller: controller, + showSectionTabs: false, + ), _ => WebAssistantPage(controller: controller), }; } diff --git a/lib/app/workspace_page_registry.dart b/lib/app/workspace_page_registry.dart index b622d22a..baefefef 100644 --- a/lib/app/workspace_page_registry.dart +++ b/lib/app/workspace_page_registry.dart @@ -127,12 +127,14 @@ workspacePageSpecsInternal = { initialTab: controller.settingsTab, initialDetail: controller.settingsDetail, navigationContext: controller.settingsNavigationContext, + showSectionTabs: false, ), mobileBuilder: (controller, onOpenDetail) => SettingsPage( controller: controller, initialTab: controller.settingsTab, initialDetail: controller.settingsDetail, navigationContext: controller.settingsNavigationContext, + showSectionTabs: false, ), ), WorkspaceDestination.account: WorkspacePageSpec( diff --git a/lib/features/settings/settings_page_core.dart b/lib/features/settings/settings_page_core.dart index ebce3d8b..b799717a 100644 --- a/lib/features/settings/settings_page_core.dart +++ b/lib/features/settings/settings_page_core.dart @@ -36,12 +36,14 @@ class SettingsPage extends StatefulWidget { this.initialTab = SettingsTab.general, this.initialDetail, this.navigationContext, + this.showSectionTabs = false, }); final AppController controller; final SettingsTab initialTab; final SettingsDetailPage? initialDetail; final SettingsNavigationContext? navigationContext; + final bool showSectionTabs; @override State createState() => SettingsPageStateInternal(); @@ -214,8 +216,11 @@ class SettingsPageStateInternal extends State { final settings = controller.settingsDraft; final showingDetail = detailInternal != null; final showGlobalApplyBar = - tabInternal != SettingsTab.gateway || - integrationSubTabInternal == GatewayIntegrationSubTabInternal.acp; + !showingDetail && + (!widget.showSectionTabs || + tabInternal != SettingsTab.gateway || + integrationSubTabInternal == + GatewayIntegrationSubTabInternal.acp); return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(32, 32, 32, 8), child: Column( @@ -265,7 +270,7 @@ class SettingsPageStateInternal extends State { buildGlobalApplyBarInternal(context, controller), const SizedBox(height: 16), ], - if (!showingDetail) ...[ + if (!showingDetail && widget.showSectionTabs) ...[ SectionTabs( items: availableTabs.map((item) => item.label).toList(), value: tabInternal.label, @@ -280,12 +285,26 @@ class SettingsPageStateInternal extends State { ), const SizedBox(height: 24), ], - ...buildContentForCurrentStateInternal( - context, - controller, - settings, - uiFeatures, - ), + ...(showingDetail + ? buildContentForCurrentStateInternal( + context, + controller, + settings, + uiFeatures, + ) + : widget.showSectionTabs + ? buildContentForCurrentStateInternal( + context, + controller, + settings, + uiFeatures, + ) + : buildOverviewContentInternal( + context, + controller, + settings, + uiFeatures, + )), ], ), ); diff --git a/lib/features/settings/settings_page_gateway.dart b/lib/features/settings/settings_page_gateway.dart index 1bd203cc..cd2b19b2 100644 --- a/lib/features/settings/settings_page_gateway.dart +++ b/lib/features/settings/settings_page_gateway.dart @@ -34,6 +34,14 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { SettingsSnapshot settings, UiFeatureAccess uiFeatures, ) { + if (!widget.showSectionTabs) { + return buildUnifiedGatewaySectionsInternal( + context, + controller, + settings, + uiFeatures, + ); + } final tabLabel = switch (integrationSubTabInternal) { GatewayIntegrationSubTabInternal.gateway => 'OpenClaw Gateway', GatewayIntegrationSubTabInternal.vault => appText( @@ -147,6 +155,59 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { ]; } + List buildUnifiedGatewaySectionsInternal( + BuildContext context, + AppController controller, + SettingsSnapshot settings, + UiFeatureAccess uiFeatures, + ) { + return [ + buildCollapsibleGatewaySectionInternal( + context: context, + title: 'OpenClaw Gateway', + expanded: openClawGatewayExpandedInternal, + onChanged: (value) => setStateInternal(() { + openClawGatewayExpandedInternal = value; + }), + child: buildOpenClawGatewayCardInternal(context, controller, settings), + ), + const SizedBox(height: 16), + if (uiFeatures.supportsVaultServer) + buildCollapsibleGatewaySectionInternal( + context: context, + title: appText('Vault Server', 'Vault Server'), + expanded: vaultServerExpandedInternal, + onChanged: (value) => setStateInternal(() { + vaultServerExpandedInternal = value; + }), + child: buildVaultProviderCardInternal(context, controller, settings), + ) + else + SurfaceCard( + child: Text( + appText( + '当前发布配置未开放 Vault Server 参数。', + 'Vault Server settings are disabled in this release configuration.', + ), + ), + ), + const SizedBox(height: 16), + buildCollapsibleGatewaySectionInternal( + context: context, + title: appText('LLM 接入点', 'LLM Endpoints'), + expanded: aiGatewayExpandedInternal, + onChanged: (value) => setStateInternal(() { + aiGatewayExpandedInternal = value; + }), + child: buildLlmEndpointManagerInternal(context, controller, settings), + ), + const SizedBox(height: 16), + buildExternalAcpEndpointManagerInternal(context, controller, settings), + const SizedBox(height: 16), + SkillDirectoryAuthorizationCard(controller: controller), + ]; + } + Widget buildExternalAcpEndpointManagerInternal( BuildContext context, AppController controller, diff --git a/lib/features/settings/settings_page_gateway_connection.dart b/lib/features/settings/settings_page_gateway_connection.dart index 7036b074..76c67eda 100644 --- a/lib/features/settings/settings_page_gateway_connection.dart +++ b/lib/features/settings/settings_page_gateway_connection.dart @@ -137,23 +137,56 @@ extension SettingsPageGatewayConnectionMixinInternal if (selectedProfileIndex != kGatewayLocalProfileIndex && !forceSetupCodeMode && setupCodeFeatureEnabled) ...[ - SectionTabs( - items: [appText('配置码', 'Setup Code'), appText('手动配置', 'Manual')], - value: useSetupCode - ? appText('配置码', 'Setup Code') - : appText('手动配置', 'Manual'), - size: SectionTabsSize.small, - onChanged: (value) { - final nextUseSetupCode = value == appText('配置码', 'Setup Code'); - unawaited( - saveGatewayProfileInternal( - controller, - settings, - gatewayProfile.copyWith(useSetupCode: nextUseSetupCode), - ).catchError((_) {}), - ); - }, - ), + if (widget.showSectionTabs) + SectionTabs( + items: [ + appText('配置码', 'Setup Code'), + appText('手动配置', 'Manual'), + ], + value: useSetupCode + ? appText('配置码', 'Setup Code') + : appText('手动配置', 'Manual'), + size: SectionTabsSize.small, + onChanged: (value) { + final nextUseSetupCode = value == appText('配置码', 'Setup Code'); + unawaited( + saveGatewayProfileInternal( + controller, + settings, + gatewayProfile.copyWith(useSetupCode: nextUseSetupCode), + ).catchError((_) {}), + ); + }, + ) + else + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + FilterChip( + label: Text(appText('配置码', 'Setup Code')), + selected: useSetupCode, + onSelected: (_) => unawaited( + saveGatewayProfileInternal( + controller, + settings, + gatewayProfile.copyWith(useSetupCode: true), + ).catchError((_) {}), + ), + ), + FilterChip( + label: Text(appText('手动配置', 'Manual')), + selected: !useSetupCode, + onSelected: (_) => unawaited( + saveGatewayProfileInternal( + controller, + settings, + gatewayProfile.copyWith(useSetupCode: false), + ).catchError((_) {}), + ), + ), + ], + ), const SizedBox(height: 12), ], if (selectedProfileIndex != kGatewayLocalProfileIndex && diff --git a/lib/features/settings/settings_page_sections.dart b/lib/features/settings/settings_page_sections.dart index e42021fe..88f3eb02 100644 --- a/lib/features/settings/settings_page_sections.dart +++ b/lib/features/settings/settings_page_sections.dart @@ -28,6 +28,67 @@ import 'settings_page_device.dart'; import 'settings_page_widgets.dart'; extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal { + List orderedOverviewTabsInternal( + AppController controller, + UiFeatureAccess uiFeatures, + ) { + final availableTabs = uiFeatures.availableSettingsTabs; + final current = uiFeatures.sanitizeSettingsTab(controller.settingsTab); + return [ + current, + ...availableTabs.where((item) => item != current), + ]; + } + + List buildOverviewContentInternal( + BuildContext context, + AppController controller, + SettingsSnapshot settings, + UiFeatureAccess uiFeatures, + ) { + final orderedTabs = orderedOverviewTabsInternal(controller, uiFeatures); + final sections = []; + for (final tab in orderedTabs) { + final content = switch (tab) { + SettingsTab.general => buildGeneralInternal( + context, + controller, + settings, + uiFeatures, + ), + SettingsTab.workspace => buildWorkspaceInternal( + context, + controller, + settings, + ), + SettingsTab.gateway => buildGatewayInternal( + context, + controller, + settings, + uiFeatures, + ), + SettingsTab.agents => buildAgentsInternal(context, controller, settings), + SettingsTab.appearance => buildAppearanceInternal(context, controller), + SettingsTab.diagnostics => buildDiagnosticsInternal(context, controller), + SettingsTab.experimental => buildExperimentalInternal( + context, + controller, + settings, + uiFeatures, + ), + SettingsTab.about => buildAboutInternal(context, controller), + }; + if (content.isEmpty) { + continue; + } + if (sections.isNotEmpty) { + sections.add(const SizedBox(height: 24)); + } + sections.addAll(content); + } + return sections; + } + List buildContentForCurrentStateInternal( BuildContext context, AppController controller, diff --git a/lib/web/web_settings_page_core.dart b/lib/web/web_settings_page_core.dart index 62b7db03..be0aa4c9 100644 --- a/lib/web/web_settings_page_core.dart +++ b/lib/web/web_settings_page_core.dart @@ -18,9 +18,14 @@ import 'web_settings_page_gateway.dart'; import 'web_settings_page_support.dart'; class WebSettingsPage extends StatefulWidget { - const WebSettingsPage({super.key, required this.controller}); + const WebSettingsPage({ + super.key, + required this.controller, + this.showSectionTabs = false, + }); final AppController controller; + final bool showSectionTabs; @override State createState() => WebSettingsPageStateInternal(); @@ -243,6 +248,7 @@ class WebSettingsPageStateInternal extends State { controller.settingsTab, ); final showGlobalApplyBar = + !widget.showSectionTabs || currentTab != SettingsTab.gateway || gatewaySubTabInternal == WebGatewaySettingsSubTabInternal.acp; return DesktopWorkspaceScaffold( @@ -285,34 +291,33 @@ class WebSettingsPageStateInternal extends State { buildGlobalApplyBarInternal(context, controller), const SizedBox(height: 16), ], - SectionTabs( - items: availableTabs.map((item) => item.label).toList(), - value: currentTab.label, - onChanged: (label) { - final tab = availableTabs.firstWhere( - (item) => item.label == label, - ); - controller.setSettingsTab(tab); - }, - ), - const SizedBox(height: 24), - ...switch (currentTab) { - SettingsTab.general => buildGeneralInternal( - context, - controller, - controller.settingsDraft, + if (widget.showSectionTabs) ...[ + SectionTabs( + items: availableTabs.map((item) => item.label).toList(), + value: currentTab.label, + onChanged: (label) { + final tab = availableTabs.firstWhere( + (item) => item.label == label, + ); + controller.setSettingsTab(tab); + }, ), - SettingsTab.gateway => buildGatewayInternal( - context, - controller, - controller.settingsDraft, - ), - SettingsTab.appearance => buildAppearanceInternal( - context, - controller, - ), - _ => buildAboutInternal(context), - }, + const SizedBox(height: 24), + ], + ...(widget.showSectionTabs + ? buildTabContentInternal( + context, + controller, + controller.settingsDraft, + currentTab, + ) + : buildOverviewContentInternal( + context, + controller, + controller.settingsDraft, + availableTabs, + currentTab, + )), ], ), ), diff --git a/lib/web/web_settings_page_sections.dart b/lib/web/web_settings_page_sections.dart index f6971577..fa96bc46 100644 --- a/lib/web/web_settings_page_sections.dart +++ b/lib/web/web_settings_page_sections.dart @@ -18,6 +18,45 @@ import 'web_settings_page_gateway.dart'; import 'web_settings_page_support.dart'; extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal { + List buildTabContentInternal( + BuildContext context, + AppController controller, + SettingsSnapshot settings, + SettingsTab tab, + ) { + return switch (tab) { + SettingsTab.general => buildGeneralInternal(context, controller, settings), + SettingsTab.gateway => buildGatewayInternal(context, controller, settings), + SettingsTab.appearance => buildAppearanceInternal(context, controller), + _ => buildAboutInternal(context), + }; + } + + List buildOverviewContentInternal( + BuildContext context, + AppController controller, + SettingsSnapshot settings, + List availableTabs, + SettingsTab currentTab, + ) { + final orderedTabs = [ + currentTab, + ...availableTabs.where((item) => item != currentTab), + ]; + final sections = []; + for (final tab in orderedTabs) { + final content = buildTabContentInternal(context, controller, settings, tab); + if (content.isEmpty) { + continue; + } + if (sections.isNotEmpty) { + sections.add(const SizedBox(height: 24)); + } + sections.addAll(content); + } + return sections; + } + Widget buildGlobalApplyBarInternal( BuildContext context, AppController controller, @@ -147,6 +186,15 @@ extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal { AppController controller, SettingsSnapshot settings, ) { + if (!widget.showSectionTabs) { + return [ + ...buildGatewayOverviewInternal(context, controller), + const SizedBox(height: 16), + ...buildLlmEndpointManagerInternal(context, controller, settings), + const SizedBox(height: 16), + buildExternalAcpEndpointManagerInternal(context, controller), + ]; + } return [ SectionTabs( items: [ diff --git a/test/features/settings_ai_gateway_persistence_suite.dart b/test/features/settings_ai_gateway_persistence_suite.dart index 44f646e2..4ee28304 100644 --- a/test/features/settings_ai_gateway_persistence_suite.dart +++ b/test/features/settings_ai_gateway_persistence_suite.dart @@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:xworkmate/app/app_controller.dart'; import 'package:xworkmate/features/settings/settings_page.dart'; +import 'package:xworkmate/models/app_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; import '../test_support.dart'; @@ -61,13 +62,9 @@ void main() { ); }); + controller.setSettingsTab(SettingsTab.gateway); await pumpPage(tester, child: SettingsPage(controller: controller)); - await tester.tap(find.text('集成')); - await tester.pump(const Duration(milliseconds: 300)); - await tester.tap(find.text('LLM 接入点')); - await tester.pump(const Duration(milliseconds: 300)); - await tester.enterText( find.byKey(const ValueKey('ai-gateway-name-field')), 'default', @@ -104,7 +101,7 @@ void main() { ); expect( find.byKey(const ValueKey('settings-global-apply-button')), - findsNothing, + findsOneWidget, ); expect( diff --git a/test/features/settings_page_suite.dart b/test/features/settings_page_suite.dart index c8694522..a8f2ad14 100644 --- a/test/features/settings_page_suite.dart +++ b/test/features/settings_page_suite.dart @@ -10,6 +10,7 @@ import 'package:xworkmate/models/app_models.dart'; import 'package:xworkmate/runtime/desktop_platform_service.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/skill_directory_access.dart'; +import 'package:xworkmate/widgets/section_tabs.dart'; import '../test_support.dart'; @@ -133,20 +134,36 @@ class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService { } } +Future _pumpSettingsPage( + WidgetTester tester, + AppController controller, { + SettingsTab tab = SettingsTab.general, + TargetPlatform platform = TargetPlatform.macOS, +}) async { + controller.setSettingsTab(tab); + await pumpPage( + tester, + child: SettingsPage(controller: controller), + platform: platform, + ); +} + +Future _ensureVisible(WidgetTester tester, Finder finder) async { + await tester.ensureVisible(finder.first); + await tester.pumpAndSettle(); +} + void main() { testWidgets('SettingsPage theme chips update controller theme mode', ( WidgetTester tester, ) async { final controller = await createTestController(tester); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.appearance, ); - - await tester.tap(find.text('外观')); - await tester.pumpAndSettle(); await tester.tap(find.text('深色')); await tester.pumpAndSettle(); @@ -204,33 +221,52 @@ void main() { (WidgetTester tester) async { final controller = await createTestController(tester); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.workspace, ); - await tester.tap(find.text('工作区')); - await tester.pumpAndSettle(); - expect(find.text('远程项目根目录'), findsNothing); expect(find.text('Remote Project Root'), findsNothing); }, ); - testWidgets('SettingsPage workspace edits enable the top save-and-apply flow', ( + testWidgets('SettingsPage can render as a unified page without internal tabs', ( WidgetTester tester, ) async { final controller = await createTestController(tester); await pumpPage( tester, - child: SettingsPage(controller: controller), + child: SettingsPage( + controller: controller, + showSectionTabs: false, + ), platform: TargetPlatform.macOS, ); - await tester.tap(find.text('工作区')); - await tester.pumpAndSettle(); + expect(find.byType(SectionTabs), findsNothing); + expect(find.text('Application'), findsOneWidget); + expect(find.text('工作区'), findsWidgets); + expect(find.text('OpenClaw Gateway'), findsWidgets); + expect(find.text('LLM 接入点'), findsWidgets); + expect( + find.byKey(const ValueKey('external-acp-provider-add-button')), + findsOneWidget, + ); + }); + + testWidgets('SettingsPage workspace edits enable the top save-and-apply flow', ( + WidgetTester tester, + ) async { + final controller = await createTestController(tester); + + await _pumpSettingsPage( + tester, + controller, + tab: SettingsTab.workspace, + ); await tester.enterText( find.byType(TextFormField).first, @@ -252,24 +288,15 @@ void main() { ) async { final controller = await createTestController(tester); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.gateway, ); - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); - expect(find.text('OpenClaw Gateway'), findsWidgets); expect(find.text('LLM 接入点'), findsOneWidget); - expect(find.text('ACP 外部接入'), findsOneWidget); - expect(find.text('Vault Server'), findsOneWidget); - expect(find.byKey(const ValueKey('vault-server-url-field')), findsNothing); - expect( - find.byKey(const ValueKey('vault-root-access-token-field')), - findsNothing, - ); + expect(find.text('Vault Server'), findsAtLeastNWidgets(1)); expect(find.byKey(const ValueKey('gateway-test-button')), findsOneWidget); expect(find.byKey(const ValueKey('gateway-save-button')), findsNothing); expect(find.byKey(const ValueKey('gateway-apply-button')), findsOneWidget); @@ -306,9 +333,6 @@ void main() { findsOneWidget, ); - await tester.tap(find.text('Vault Server').first); - await tester.pumpAndSettle(); - expect( find.byKey(const ValueKey('vault-server-url-field')), findsOneWidget, @@ -317,9 +341,13 @@ void main() { find.byKey(const ValueKey('vault-root-access-token-field')), findsOneWidget, ); - expect(find.byKey(const ValueKey('ai-gateway-url-field')), findsNothing); + expect(find.byKey(const ValueKey('ai-gateway-url-field')), findsOneWidget); expect(find.byKey(const ValueKey('gateway-mode-field')), findsNothing); - expect(find.text('认证诊断'), findsNothing); + expect(find.text('认证诊断'), findsOneWidget); + expect( + find.byKey(const ValueKey('external-acp-provider-add-button')), + findsOneWidget, + ); expect(find.text('保存并生效'), findsWidgets); }); @@ -328,18 +356,13 @@ void main() { ) async { final controller = await createTestController(tester); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.gateway, ); - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Vault Server').first); - await tester.pumpAndSettle(); - - expect(find.text('Vault Server'), findsWidgets); + expect(find.text('Vault Server'), findsAtLeastNWidgets(1)); expect(find.text('VAULT_SERVER_URL'), findsOneWidget); expect( find.textContaining('VAULT_SERVER_ROOT_ACCESS_TOKEN'), @@ -354,17 +377,12 @@ void main() { ) async { final controller = await createTestController(tester); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.gateway, ); - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); - await tester.tap(find.text('ACP 外部接入').first); - await tester.pumpAndSettle(); - expect(find.text('外部 ACP Server Endpoint'), findsOneWidget); expect(find.textContaining('Codex'), findsWidgets); expect(find.textContaining('OpenCode'), findsWidgets); @@ -386,7 +404,7 @@ void main() { find.byKey(const ValueKey('settings-global-apply-button')), findsOneWidget, ); - expect(find.text('保存并生效'), findsOneWidget); + expect(find.text('保存并生效'), findsWidgets); }); testWidgets('SettingsPage ACP wizard adds a custom provider card', ( @@ -394,17 +412,16 @@ void main() { ) async { final controller = await createTestController(tester); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.gateway, ); - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); - await tester.tap(find.text('ACP 外部接入').first); - await tester.pumpAndSettle(); - + await _ensureVisible( + tester, + find.byKey(const ValueKey('external-acp-provider-add-button')), + ); await tester.tap( find.byKey(const ValueKey('external-acp-provider-add-button')), ); @@ -435,17 +452,12 @@ void main() { _FakeSkillDirectoryAccessService(userHomeDirectory: '/Users/tester'), ); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.gateway, ); - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); - await tester.tap(find.text('SKILLS 目录授权')); - await tester.pumpAndSettle(); - expect(find.text('~/.agents/skills'), findsOneWidget); expect(find.text('/Users/tester/.agents/skills'), findsOneWidget); expect(find.text('~/.codex/skills'), findsOneWidget); @@ -474,16 +486,15 @@ void main() { ), ); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.gateway, + ); + await _ensureVisible( + tester, + find.byKey(const ValueKey('skill-directory-batch-add-button')), ); - - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); - await tester.tap(find.text('SKILLS 目录授权')); - await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('skill-directory-batch-add-button')), ); @@ -536,16 +547,15 @@ paths: ), ); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.gateway, + ); + await _ensureVisible( + tester, + find.byKey(const ValueKey('skill-directory-batch-add-button')), ); - - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); - await tester.tap(find.text('SKILLS 目录授权')); - await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('skill-directory-batch-add-button')), ); @@ -581,16 +591,15 @@ paths: _FakeSkillDirectoryAccessService(userHomeDirectory: '/Users/tester'), ); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.gateway, + ); + await _ensureVisible( + tester, + find.byKey(const ValueKey('skill-directory-batch-add-button')), ); - - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); - await tester.tap(find.text('SKILLS 目录授权')); - await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('skill-directory-batch-add-button')), ); @@ -627,15 +636,12 @@ paths: ) async { final controller = await createTestController(tester); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.gateway, ); - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); - await tester.tap(find.byTooltip('折叠').first); await tester.pumpAndSettle(); @@ -665,11 +671,7 @@ paths: desktopPlatformService: _DesktopServiceStub(), ); - await pumpPage( - tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, - ); + await _pumpSettingsPage(tester, controller); expect( find.byKey(const ValueKey('linux-desktop-integration-card')), @@ -700,6 +702,7 @@ paths: child: const SizedBox(width: 1100, height: 900, child: Placeholder()), platform: TargetPlatform.macOS, ); + controller.setSettingsTab(SettingsTab.agents); await pumpPage( tester, child: SizedBox( @@ -710,9 +713,6 @@ paths: platform: TargetPlatform.macOS, ); - await tester.tap(find.text('多 Agent')); - await tester.pumpAndSettle(); - final titleFinder = find.text('多 Agent 协作'); expect(titleFinder, findsOneWidget); expect(tester.getSize(titleFinder).width, greaterThan(80)); @@ -728,14 +728,11 @@ paths: ) async { final controller = await createTestController(tester); - await pumpPage( + await _pumpSettingsPage( tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, + controller, + tab: SettingsTab.gateway, ); - - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('gateway-profile-chip-1'))); await tester.pumpAndSettle(); @@ -762,10 +759,12 @@ paths: message: 'pairing required', ); - await pumpPage(tester, child: SettingsPage(controller: controller)); - - await tester.tap(find.text('诊断')); - await tester.pumpAndSettle(); + await _pumpSettingsPage( + tester, + controller, + tab: SettingsTab.diagnostics, + platform: TargetPlatform.android, + ); expect(find.byKey(const ValueKey('runtime-log-card')), findsOneWidget); expect(find.textContaining('connected remote gateway'), findsOneWidget); @@ -807,11 +806,7 @@ paths: uiFeatureManifest: manifest, ); - await pumpPage( - tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, - ); + await _pumpSettingsPage(tester, controller); expect(find.text('诊断'), findsNothing); expect(find.text('实验特性'), findsNothing); @@ -822,10 +817,11 @@ paths: (WidgetTester tester) async { final controller = await createTestController(tester); - await pumpPage(tester, child: SettingsPage(controller: controller)); - - await tester.tap(find.text('诊断')); - await tester.pump(const Duration(milliseconds: 300)); + await _pumpSettingsPage( + tester, + controller, + tab: SettingsTab.diagnostics, + ); expect( find.byKey(const ValueKey('assistant-local-state-card')), @@ -889,6 +885,33 @@ paths: expect(find.text('搜索设置'), findsOneWidget); }); + testWidgets('Sidebar settings entry resets to general overview', ( + WidgetTester tester, + ) async { + final controller = await createTestController(tester); + controller.openSettings(tab: SettingsTab.workspace); + + controller.navigateTo(WorkspaceDestination.assistant); + controller.openSettings(tab: SettingsTab.general); + + expect(controller.destination, WorkspaceDestination.settings); + expect(controller.settingsTab, SettingsTab.general); + + await pumpPage( + tester, + child: SettingsPage( + controller: controller, + initialTab: controller.settingsTab, + initialDetail: controller.settingsDetail, + navigationContext: controller.settingsNavigationContext, + ), + platform: TargetPlatform.macOS, + ); + + expect(find.byType(SectionTabs), findsNothing); + expect(find.text('Application'), findsOneWidget); + }); + testWidgets('SettingsPage expands optional LLM endpoints with add button', ( WidgetTester tester, ) async { diff --git a/test/features/settings_vault_persistence_suite.dart b/test/features/settings_vault_persistence_suite.dart index 997c83e2..7d552407 100644 --- a/test/features/settings_vault_persistence_suite.dart +++ b/test/features/settings_vault_persistence_suite.dart @@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:xworkmate/app/app_controller.dart'; import 'package:xworkmate/features/settings/settings_page.dart'; +import 'package:xworkmate/models/app_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; import '../test_support.dart'; @@ -40,17 +41,13 @@ void main() { } }); + controller.setSettingsTab(SettingsTab.gateway); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); - await tester.tap(find.text('集成')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Vault Server').first); - await tester.pumpAndSettle(); - expect( find.byKey(const ValueKey('vault-server-url-field')), findsOneWidget,