fix: keep settings account summary on canonical state
This commit is contained in:
parent
6f66fd44bc
commit
9412149485
265
integration_test/desktop_settings_flow_test.dart
Normal file
265
integration_test/desktop_settings_flow_test.dart
Normal file
@ -0,0 +1,265 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_core.dart';
|
||||
import 'package:xworkmate/features/settings/settings_page.dart';
|
||||
import 'package:xworkmate/i18n/app_language.dart';
|
||||
import 'package:xworkmate/models/app_models.dart';
|
||||
import 'package:xworkmate/runtime/runtime_controllers_settings.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets(
|
||||
'settings page keeps canonical account status and logout behavior aligned',
|
||||
(tester) async {
|
||||
final fixtures = _buildSettingsPageFixtures();
|
||||
final controller = fixtures.controller;
|
||||
final canonicalSettings = fixtures.canonicalSettings;
|
||||
|
||||
final staleDraft = canonicalSettings.copyWith(
|
||||
accountBaseUrl: 'https://draft-accounts.svc.plus',
|
||||
accountUsername: 'draft@svc.plus',
|
||||
acpBridgeServerModeConfig: canonicalSettings.acpBridgeServerModeConfig
|
||||
.copyWith(
|
||||
cloudSynced: canonicalSettings
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.copyWith(
|
||||
accountBaseUrl: 'https://draft-accounts.svc.plus',
|
||||
accountIdentifier: 'draft@svc.plus',
|
||||
lastSyncAt: 987654321,
|
||||
remoteServerSummary:
|
||||
const AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: 'wss://draft-gateway.svc.plus',
|
||||
hasAdvancedOverrides: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await controller.saveSettingsDraft(staleDraft);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(platform: TargetPlatform.macOS),
|
||||
home: Scaffold(
|
||||
body: RepaintBoundary(
|
||||
key: const ValueKey('settings-page-boundary'),
|
||||
child: SizedBox(
|
||||
width: 1600,
|
||||
height: 1200,
|
||||
child: SettingsPage(controller: controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
final serviceUrlText = tester.widget<Text>(
|
||||
find.byKey(const ValueKey('settings-account-summary-service-url')),
|
||||
);
|
||||
final accountIdentifierText = tester.widget<Text>(
|
||||
find.byKey(
|
||||
const ValueKey('settings-account-summary-account-identifier'),
|
||||
),
|
||||
);
|
||||
expect(serviceUrlText.data ?? '', contains('https://accounts.svc.plus'));
|
||||
expect(
|
||||
serviceUrlText.data ?? '',
|
||||
isNot(contains('https://draft-accounts.svc.plus')),
|
||||
);
|
||||
expect(accountIdentifierText.data ?? '', contains('canonical@svc.plus'));
|
||||
expect(
|
||||
accountIdentifierText.data ?? '',
|
||||
isNot(contains('draft@svc.plus')),
|
||||
);
|
||||
|
||||
await controller.settingsController.syncAccountSettings(
|
||||
baseUrl: controller.settings.accountBaseUrl,
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
controller.settingsController.syncedBaseUrls,
|
||||
contains('https://accounts.svc.plus'),
|
||||
);
|
||||
expect(
|
||||
controller.settingsController.syncedBaseUrls,
|
||||
isNot(contains('https://draft-accounts.svc.plus')),
|
||||
);
|
||||
|
||||
await controller.settingsController.logoutAccount();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('未登录'), findsOneWidget);
|
||||
final loggedOutButton = tester.widget<FilledButton>(
|
||||
find.byKey(const ValueKey('settings-account-logout-button')),
|
||||
);
|
||||
expect(loggedOutButton.onPressed, isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
SettingsSnapshot _buildCanonicalSettings() {
|
||||
final defaults = SettingsSnapshot.defaults();
|
||||
return defaults.copyWith(
|
||||
accountBaseUrl: 'https://accounts.svc.plus',
|
||||
accountUsername: 'canonical@svc.plus',
|
||||
accountLocalMode: false,
|
||||
acpBridgeServerModeConfig: defaults.acpBridgeServerModeConfig.copyWith(
|
||||
cloudSynced: defaults.acpBridgeServerModeConfig.cloudSynced.copyWith(
|
||||
accountBaseUrl: 'https://accounts.svc.plus',
|
||||
accountIdentifier: 'canonical@svc.plus',
|
||||
lastSyncAt: 123456789,
|
||||
remoteServerSummary: const AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: 'wss://gateway.svc.plus',
|
||||
hasAdvancedOverrides: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_SettingsPageFixtures _buildSettingsPageFixtures() {
|
||||
final canonicalSettings = _buildCanonicalSettings().copyWith(
|
||||
appLanguage: AppLanguage.zh,
|
||||
);
|
||||
final settingsController = _FakeSettingsController()
|
||||
..seedSignedInState(canonicalSettings);
|
||||
final controller = _FakeSettingsPageController(
|
||||
settingsController: settingsController,
|
||||
settingsDraft: canonicalSettings,
|
||||
);
|
||||
addTearDown(() {
|
||||
controller.dispose();
|
||||
settingsController.dispose();
|
||||
});
|
||||
return _SettingsPageFixtures(
|
||||
controller: controller,
|
||||
canonicalSettings: canonicalSettings,
|
||||
);
|
||||
}
|
||||
|
||||
class _SettingsPageFixtures {
|
||||
_SettingsPageFixtures({
|
||||
required this.controller,
|
||||
required this.canonicalSettings,
|
||||
});
|
||||
|
||||
final _FakeSettingsPageController controller;
|
||||
final SettingsSnapshot canonicalSettings;
|
||||
}
|
||||
|
||||
class _FakeSettingsPageController extends ChangeNotifier
|
||||
implements AppController {
|
||||
_FakeSettingsPageController({
|
||||
required this.settingsController,
|
||||
required SettingsSnapshot settingsDraft,
|
||||
}) : _settingsDraft = settingsDraft;
|
||||
|
||||
@override
|
||||
final _FakeSettingsController settingsController;
|
||||
SettingsSnapshot _settingsDraft;
|
||||
|
||||
@override
|
||||
SettingsSnapshot get settings => settingsController.snapshot;
|
||||
|
||||
@override
|
||||
SettingsSnapshot get settingsDraft => _settingsDraft;
|
||||
|
||||
Future<void> saveSettingsDraft(SettingsSnapshot snapshot) async {
|
||||
_settingsDraft = snapshot;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> saveSettings(SettingsSnapshot snapshot) async {
|
||||
settingsController.snapshotInternal = snapshot;
|
||||
_settingsDraft = snapshot;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void navigateHome() {}
|
||||
|
||||
@override
|
||||
void openSettings({
|
||||
SettingsTab tab = SettingsTab.gateway,
|
||||
SettingsDetailPage? detail,
|
||||
SettingsNavigationContext? navigationContext,
|
||||
}) {}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class _FakeSettingsController extends SettingsController {
|
||||
_FakeSettingsController()
|
||||
: super(SecureConfigStore(enableSecureStorage: false));
|
||||
|
||||
final List<String> syncedBaseUrls = <String>[];
|
||||
|
||||
void seedSignedInState(SettingsSnapshot settings) {
|
||||
snapshotInternal = settings;
|
||||
lastSnapshotJsonInternal = settings.toJsonString();
|
||||
accountSessionTokenInternal = 'session-token';
|
||||
accountSessionInternal = const AccountSessionSummary(
|
||||
userId: 'u-1',
|
||||
email: 'canonical@svc.plus',
|
||||
name: 'Canonical',
|
||||
role: 'member',
|
||||
mfaEnabled: false,
|
||||
);
|
||||
accountSyncStateInternal = AccountSyncState.defaults().copyWith(
|
||||
syncState: 'ready',
|
||||
syncMessage: 'Remote defaults synced',
|
||||
lastSyncAtMs: 123456789,
|
||||
lastSyncSource: 'https://accounts.svc.plus',
|
||||
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
|
||||
openclawUrl: 'wss://gateway.svc.plus',
|
||||
apisixUrl: 'https://apisix.svc.plus',
|
||||
),
|
||||
);
|
||||
accountStatusInternal = 'Signed in as canonical@svc.plus';
|
||||
accountBusyInternal = false;
|
||||
pendingAccountMfaTicketInternal = '';
|
||||
pendingAccountBaseUrlInternal = '';
|
||||
}
|
||||
|
||||
Future<AccountSyncResult> syncAccountSettings({String baseUrl = ''}) async {
|
||||
syncedBaseUrls.add(baseUrl);
|
||||
accountBusyInternal = true;
|
||||
notifyListeners();
|
||||
accountSyncStateInternal = AccountSyncState.defaults().copyWith(
|
||||
syncState: 'ready',
|
||||
syncMessage: 'Remote defaults synced',
|
||||
lastSyncAtMs: 123456789,
|
||||
lastSyncSource: baseUrl,
|
||||
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
|
||||
openclawUrl: 'wss://gateway.svc.plus',
|
||||
apisixUrl: 'https://apisix.svc.plus',
|
||||
),
|
||||
);
|
||||
accountBusyInternal = false;
|
||||
final email = accountSessionInternal?.email.trim() ?? '';
|
||||
accountStatusInternal = email.isEmpty ? 'Signed in' : 'Signed in as $email';
|
||||
notifyListeners();
|
||||
return const AccountSyncResult(
|
||||
state: 'ready',
|
||||
message: 'Remote defaults synced',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logoutAccount() async {
|
||||
accountSessionTokenInternal = '';
|
||||
accountSessionInternal = null;
|
||||
accountSyncStateInternal = null;
|
||||
accountStatusInternal = 'Signed out';
|
||||
pendingAccountMfaTicketInternal = '';
|
||||
pendingAccountBaseUrlInternal = '';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@ -120,14 +120,12 @@ workspacePageSpecsInternal = <WorkspaceDestination, WorkspacePageSpec>{
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
showSectionTabs: true,
|
||||
),
|
||||
mobileBuilder: (controller, onOpenDetail) => SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
showSectionTabs: true,
|
||||
),
|
||||
),
|
||||
WorkspaceDestination.account: WorkspacePageSpec(
|
||||
|
||||
@ -19,15 +19,12 @@ class SettingsPage extends StatefulWidget {
|
||||
this.initialTab = SettingsTab.gateway,
|
||||
this.initialDetail,
|
||||
this.navigationContext,
|
||||
this.showSectionTabs = true,
|
||||
});
|
||||
|
||||
final AppController controller;
|
||||
final SettingsTab initialTab;
|
||||
final SettingsDetailPage? initialDetail;
|
||||
final SettingsNavigationContext? navigationContext;
|
||||
final bool showSectionTabs;
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
@ -75,20 +72,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
controller.settingsController,
|
||||
]),
|
||||
builder: (context, _) {
|
||||
final settings = controller.settingsDraft;
|
||||
final currentSettings = controller.settings;
|
||||
final settingsDraft = controller.settingsDraft;
|
||||
final accountState = controller.settingsController.accountSyncState;
|
||||
final accountBusy = controller.settingsController.accountBusy;
|
||||
final accountSignedIn = controller.settingsController.accountSignedIn;
|
||||
final accountSession = controller.settingsController.accountSession;
|
||||
final cloudSync = settings.acpBridgeServerModeConfig.cloudSynced;
|
||||
final cloudSync = currentSettings.acpBridgeServerModeConfig.cloudSynced;
|
||||
final remoteSummary = cloudSync.remoteServerSummary.endpoint.trim();
|
||||
final serviceUrl = cloudSync.accountBaseUrl.trim().isNotEmpty
|
||||
? cloudSync.accountBaseUrl.trim()
|
||||
: settings.accountBaseUrl.trim();
|
||||
: currentSettings.accountBaseUrl.trim();
|
||||
final accountIdentifier = cloudSync.accountIdentifier.trim().isNotEmpty
|
||||
? cloudSync.accountIdentifier.trim()
|
||||
: settings.accountUsername.trim().isNotEmpty
|
||||
? settings.accountUsername.trim()
|
||||
: currentSettings.accountUsername.trim().isNotEmpty
|
||||
? currentSettings.accountUsername.trim()
|
||||
: (accountSession?.email.trim() ?? '');
|
||||
final sessionLabel = accountSignedIn
|
||||
? appText(
|
||||
@ -191,14 +189,23 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${appText('服务地址', 'Service URL')}: ${serviceUrl.isEmpty ? appText('待配置', 'Pending') : serviceUrl}',
|
||||
key: const ValueKey(
|
||||
'settings-account-summary-service-url',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${appText('账户标识', 'Account Identifier')}: ${accountIdentifier.isEmpty ? appText('待登录', 'Not signed in') : accountIdentifier}',
|
||||
key: const ValueKey(
|
||||
'settings-account-summary-account-identifier',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${appText('最近同步', 'Last Sync')}: ${_formatSyncTime(cloudSync.lastSyncAt)}',
|
||||
key: const ValueKey(
|
||||
'settings-account-summary-last-sync',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -212,7 +219,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
key: const ValueKey('settings-account-sync-button'),
|
||||
onPressed: accountBusy
|
||||
? null
|
||||
: () => _syncAccount(settings),
|
||||
: () => _syncAccount(currentSettings),
|
||||
child: Text(appText('重新同步', 'Sync Again')),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
@ -295,7 +302,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
key: const ValueKey('settings-base-sync-button'),
|
||||
onPressed: accountBusy
|
||||
? null
|
||||
: () => _syncAccount(settings),
|
||||
: () => _syncAccount(settingsDraft),
|
||||
child: Text(appText('重新同步', 'Sync Again')),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
@ -304,7 +311,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
onPressed: accountBusy
|
||||
? null
|
||||
: () => _disconnectManagedBase(settings),
|
||||
: () => _disconnectManagedBase(settingsDraft),
|
||||
child: Text(appText('断开', 'Disconnect')),
|
||||
),
|
||||
],
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
308
test/features/settings/settings_page_core_test.dart
Normal file
308
test/features/settings/settings_page_core_test.dart
Normal file
@ -0,0 +1,308 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_core.dart';
|
||||
import 'package:xworkmate/features/settings/settings_page.dart';
|
||||
import 'package:xworkmate/i18n/app_language.dart';
|
||||
import 'package:xworkmate/models/app_models.dart';
|
||||
import 'package:xworkmate/runtime/runtime_controllers_settings.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Settings page account status', () {
|
||||
testWidgets(
|
||||
'reads canonical settings instead of a stale draft and syncs from the active account URL',
|
||||
(tester) async {
|
||||
final fixtures = _buildSettingsPageFixtures();
|
||||
final controller = fixtures.controller;
|
||||
final canonicalSettings = fixtures.canonicalSettings;
|
||||
|
||||
final staleDraft = canonicalSettings.copyWith(
|
||||
accountBaseUrl: 'https://draft-accounts.svc.plus',
|
||||
accountUsername: 'draft@svc.plus',
|
||||
acpBridgeServerModeConfig: canonicalSettings.acpBridgeServerModeConfig
|
||||
.copyWith(
|
||||
cloudSynced: canonicalSettings
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.copyWith(
|
||||
accountBaseUrl: 'https://draft-accounts.svc.plus',
|
||||
accountIdentifier: 'draft@svc.plus',
|
||||
lastSyncAt: 987654321,
|
||||
remoteServerSummary:
|
||||
const AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: 'wss://draft-gateway.svc.plus',
|
||||
hasAdvancedOverrides: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await controller.saveSettingsDraft(staleDraft);
|
||||
expect(controller.settings.accountBaseUrl, 'https://accounts.svc.plus');
|
||||
expect(controller.settingsController.accountSignedIn, isTrue);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(platform: TargetPlatform.macOS),
|
||||
home: Scaffold(
|
||||
body: RepaintBoundary(
|
||||
key: const ValueKey('settings-page-boundary'),
|
||||
child: SizedBox(
|
||||
width: 1600,
|
||||
height: 1200,
|
||||
child: SettingsPage(controller: controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
final serviceUrlText = tester.widget<Text>(
|
||||
find.byKey(const ValueKey('settings-account-summary-service-url')),
|
||||
);
|
||||
final accountIdentifierText = tester.widget<Text>(
|
||||
find.byKey(
|
||||
const ValueKey('settings-account-summary-account-identifier'),
|
||||
),
|
||||
);
|
||||
final syncButton = tester.widget<FilledButton>(
|
||||
find.byKey(const ValueKey('settings-account-sync-button')),
|
||||
);
|
||||
|
||||
final serviceUrlTextContent =
|
||||
serviceUrlText.data ?? serviceUrlText.textSpan?.toPlainText() ?? '';
|
||||
final accountIdentifierTextContent =
|
||||
accountIdentifierText.data ??
|
||||
accountIdentifierText.textSpan?.toPlainText() ??
|
||||
'';
|
||||
|
||||
expect(serviceUrlTextContent, contains('https://accounts.svc.plus'));
|
||||
expect(
|
||||
serviceUrlTextContent,
|
||||
isNot(contains('https://draft-accounts.svc.plus')),
|
||||
);
|
||||
expect(accountIdentifierTextContent, contains('canonical@svc.plus'));
|
||||
expect(accountIdentifierTextContent, isNot(contains('draft@svc.plus')));
|
||||
expect(syncButton.onPressed, isNotNull);
|
||||
|
||||
await controller.settingsController.syncAccountSettings(
|
||||
baseUrl: controller.settings.accountBaseUrl,
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
controller.settingsController.syncedBaseUrls,
|
||||
contains('https://accounts.svc.plus'),
|
||||
);
|
||||
expect(
|
||||
controller.settingsController.syncedBaseUrls,
|
||||
isNot(contains('https://draft-accounts.svc.plus')),
|
||||
);
|
||||
|
||||
await controller.settingsController.logoutAccount();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('未登录'), findsOneWidget);
|
||||
final loggedOutButton = tester.widget<FilledButton>(
|
||||
find.byKey(const ValueKey('settings-account-logout-button')),
|
||||
);
|
||||
expect(loggedOutButton.onPressed, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('renders the signed-in account status card consistently', (
|
||||
tester,
|
||||
) async {
|
||||
final fixtures = _buildSettingsPageFixtures();
|
||||
final controller = fixtures.controller;
|
||||
await tester.binding.setSurfaceSize(const Size(1600, 1200));
|
||||
addTearDown(() async => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(platform: TargetPlatform.macOS),
|
||||
home: Scaffold(
|
||||
body: RepaintBoundary(
|
||||
key: const ValueKey('settings-page-boundary'),
|
||||
child: SizedBox(
|
||||
width: 1600,
|
||||
height: 1200,
|
||||
child: SettingsPage(controller: controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
await expectLater(
|
||||
find.byKey(const ValueKey('settings-page-boundary')),
|
||||
matchesGoldenFile('goldens/settings_page_account_status_canonical.png'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
SettingsSnapshot _buildCanonicalSettings() {
|
||||
final defaults = SettingsSnapshot.defaults();
|
||||
return defaults.copyWith(
|
||||
accountBaseUrl: 'https://accounts.svc.plus',
|
||||
accountUsername: 'canonical@svc.plus',
|
||||
accountLocalMode: false,
|
||||
acpBridgeServerModeConfig: defaults.acpBridgeServerModeConfig.copyWith(
|
||||
cloudSynced: defaults.acpBridgeServerModeConfig.cloudSynced.copyWith(
|
||||
accountBaseUrl: 'https://accounts.svc.plus',
|
||||
accountIdentifier: 'canonical@svc.plus',
|
||||
lastSyncAt: 123456789,
|
||||
remoteServerSummary: const AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: 'wss://gateway.svc.plus',
|
||||
hasAdvancedOverrides: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_SettingsPageFixtures _buildSettingsPageFixtures() {
|
||||
final canonicalSettings = _buildCanonicalSettings().copyWith(
|
||||
appLanguage: AppLanguage.zh,
|
||||
);
|
||||
final settingsController = _FakeSettingsController()
|
||||
..seedSignedInState(canonicalSettings);
|
||||
final controller = _FakeSettingsPageController(
|
||||
settingsController: settingsController,
|
||||
settingsDraft: canonicalSettings,
|
||||
);
|
||||
addTearDown(() {
|
||||
controller.dispose();
|
||||
settingsController.dispose();
|
||||
});
|
||||
return _SettingsPageFixtures(
|
||||
controller: controller,
|
||||
canonicalSettings: canonicalSettings,
|
||||
);
|
||||
}
|
||||
|
||||
class _SettingsPageFixtures {
|
||||
_SettingsPageFixtures({
|
||||
required this.controller,
|
||||
required this.canonicalSettings,
|
||||
});
|
||||
|
||||
final _FakeSettingsPageController controller;
|
||||
final SettingsSnapshot canonicalSettings;
|
||||
}
|
||||
|
||||
class _FakeSettingsPageController extends ChangeNotifier
|
||||
implements AppController {
|
||||
_FakeSettingsPageController({
|
||||
required this.settingsController,
|
||||
required SettingsSnapshot settingsDraft,
|
||||
}) : _settingsDraft = settingsDraft;
|
||||
|
||||
@override
|
||||
final _FakeSettingsController settingsController;
|
||||
|
||||
SettingsSnapshot _settingsDraft;
|
||||
|
||||
@override
|
||||
SettingsSnapshot get settings => settingsController.snapshot;
|
||||
|
||||
@override
|
||||
SettingsSnapshot get settingsDraft => _settingsDraft;
|
||||
|
||||
Future<void> saveSettingsDraft(SettingsSnapshot snapshot) async {
|
||||
_settingsDraft = snapshot;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> saveSettings(SettingsSnapshot snapshot) async {
|
||||
settingsController.snapshotInternal = snapshot;
|
||||
_settingsDraft = snapshot;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void navigateHome() {}
|
||||
|
||||
@override
|
||||
void openSettings({
|
||||
SettingsTab tab = SettingsTab.gateway,
|
||||
SettingsDetailPage? detail,
|
||||
SettingsNavigationContext? navigationContext,
|
||||
}) {}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class _FakeSettingsController extends SettingsController {
|
||||
_FakeSettingsController()
|
||||
: super(SecureConfigStore(enableSecureStorage: false));
|
||||
|
||||
final List<String> syncedBaseUrls = <String>[];
|
||||
|
||||
void seedSignedInState(SettingsSnapshot settings) {
|
||||
snapshotInternal = settings;
|
||||
lastSnapshotJsonInternal = settings.toJsonString();
|
||||
accountSessionTokenInternal = 'session-token';
|
||||
accountSessionInternal = const AccountSessionSummary(
|
||||
userId: 'u-1',
|
||||
email: 'canonical@svc.plus',
|
||||
name: 'Canonical',
|
||||
role: 'member',
|
||||
mfaEnabled: false,
|
||||
);
|
||||
accountSyncStateInternal = AccountSyncState.defaults().copyWith(
|
||||
syncState: 'ready',
|
||||
syncMessage: 'Remote defaults synced',
|
||||
lastSyncAtMs: 123456789,
|
||||
lastSyncSource: 'https://accounts.svc.plus',
|
||||
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
|
||||
openclawUrl: 'wss://gateway.svc.plus',
|
||||
apisixUrl: 'https://apisix.svc.plus',
|
||||
),
|
||||
);
|
||||
accountStatusInternal = 'Signed in as canonical@svc.plus';
|
||||
accountBusyInternal = false;
|
||||
pendingAccountMfaTicketInternal = '';
|
||||
pendingAccountBaseUrlInternal = '';
|
||||
}
|
||||
|
||||
Future<AccountSyncResult> syncAccountSettings({String baseUrl = ''}) async {
|
||||
syncedBaseUrls.add(baseUrl);
|
||||
accountBusyInternal = true;
|
||||
notifyListeners();
|
||||
accountSyncStateInternal = AccountSyncState.defaults().copyWith(
|
||||
syncState: 'ready',
|
||||
syncMessage: 'Remote defaults synced',
|
||||
lastSyncAtMs: 123456789,
|
||||
lastSyncSource: baseUrl,
|
||||
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
|
||||
openclawUrl: 'wss://gateway.svc.plus',
|
||||
apisixUrl: 'https://apisix.svc.plus',
|
||||
),
|
||||
);
|
||||
accountBusyInternal = false;
|
||||
final email = accountSessionInternal?.email.trim() ?? '';
|
||||
accountStatusInternal = email.isEmpty ? 'Signed in' : 'Signed in as $email';
|
||||
notifyListeners();
|
||||
return const AccountSyncResult(
|
||||
state: 'ready',
|
||||
message: 'Remote defaults synced',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logoutAccount() async {
|
||||
accountSessionTokenInternal = '';
|
||||
accountSessionInternal = null;
|
||||
accountSyncStateInternal = null;
|
||||
accountStatusInternal = 'Signed out';
|
||||
pendingAccountMfaTicketInternal = '';
|
||||
pendingAccountBaseUrlInternal = '';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user