Flatten settings navigation into unified overview

This commit is contained in:
Haitao Pan 2026-03-29 21:31:33 +08:00
parent fb248928a1
commit f2e1cb2609
12 changed files with 457 additions and 193 deletions

View File

@ -222,7 +222,16 @@ class _AppShellState extends State<AppShell> {
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

View File

@ -152,7 +152,13 @@ class _AppShellState extends State<AppShell> {
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<AppShell> {
WorkspaceDestination.aiGateway => WebAiGatewayPage(
controller: controller,
),
WorkspaceDestination.settings => WebSettingsPage(controller: controller),
WorkspaceDestination.settings => WebSettingsPage(
controller: controller,
showSectionTabs: false,
),
_ => WebAssistantPage(controller: controller),
};
}

View File

@ -127,12 +127,14 @@ workspacePageSpecsInternal = <WorkspaceDestination, WorkspacePageSpec>{
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(

View File

@ -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<SettingsPage> createState() => SettingsPageStateInternal();
@ -214,8 +216,11 @@ class SettingsPageStateInternal extends State<SettingsPage> {
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<SettingsPage> {
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<SettingsPage> {
),
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,
)),
],
),
);

View File

@ -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<Widget> 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,

View File

@ -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 &&

View File

@ -28,6 +28,67 @@ import 'settings_page_device.dart';
import 'settings_page_widgets.dart';
extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal {
List<SettingsTab> orderedOverviewTabsInternal(
AppController controller,
UiFeatureAccess uiFeatures,
) {
final availableTabs = uiFeatures.availableSettingsTabs;
final current = uiFeatures.sanitizeSettingsTab(controller.settingsTab);
return <SettingsTab>[
current,
...availableTabs.where((item) => item != current),
];
}
List<Widget> buildOverviewContentInternal(
BuildContext context,
AppController controller,
SettingsSnapshot settings,
UiFeatureAccess uiFeatures,
) {
final orderedTabs = orderedOverviewTabsInternal(controller, uiFeatures);
final sections = <Widget>[];
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<Widget> buildContentForCurrentStateInternal(
BuildContext context,
AppController controller,

View File

@ -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<WebSettingsPage> createState() => WebSettingsPageStateInternal();
@ -243,6 +248,7 @@ class WebSettingsPageStateInternal extends State<WebSettingsPage> {
controller.settingsTab,
);
final showGlobalApplyBar =
!widget.showSectionTabs ||
currentTab != SettingsTab.gateway ||
gatewaySubTabInternal == WebGatewaySettingsSubTabInternal.acp;
return DesktopWorkspaceScaffold(
@ -285,34 +291,33 @@ class WebSettingsPageStateInternal extends State<WebSettingsPage> {
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,
)),
],
),
),

View File

@ -18,6 +18,45 @@ import 'web_settings_page_gateway.dart';
import 'web_settings_page_support.dart';
extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal {
List<Widget> 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<Widget> buildOverviewContentInternal(
BuildContext context,
AppController controller,
SettingsSnapshot settings,
List<SettingsTab> availableTabs,
SettingsTab currentTab,
) {
final orderedTabs = <SettingsTab>[
currentTab,
...availableTabs.where((item) => item != currentTab),
];
final sections = <Widget>[];
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: <String>[

View File

@ -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(

View File

@ -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<void> _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<void> _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 {

View File

@ -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,