86e5b6d451
- settings_page.dart: smartRoute 布尔开关行 → 分流规则下钻行(标题 + 当前模式 pill + chevron),点击 desktop/tablet 切 navViewProvider 内容区下钻,mobile 全屏 push RoutingScreen(与 account_page.dart 的 open() 双形态导航同一范式)。 - navigation_provider.dart: 新增 NavView.routing + kSettingsSubViews 集合(登记 settings 的下钻子页,供 shell backTarget 判断返回 settings 而非 account)。 - desktop_shell.dart/tablet_shell.dart: 内容区 switch 挂 RoutingScreen(embedded: true),backTarget 优先判 kSettingsSubViews → 回 settings。 - settings_ia_test.dart: 追加下钻行断言(标题+模式pill+点击导航到 RoutingScreen)。 - 重录 desktop_settings golden(唯一因本刀布局变化受影响的 golden)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
137 lines
5.6 KiB
Dart
137 lines
5.6 KiB
Dart
// settings_ia_test.dart — 设置页 IA 结构断言:三个分组标题存在 / 购买入口不在设置页(归 Account)。
|
|
//
|
|
// 桌面态(windows 平台 + 宽 surface),经 HomeShell 渲染真实 DesktopShell → SettingsPage,
|
|
// 与 account_ia_test.dart 同一套 harness。
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pangolin_vpn/bridge/vpn_bridge_mock.dart';
|
|
import 'package:pangolin_vpn/bridge/vpn_bridge_provider.dart';
|
|
import 'package:pangolin_vpn/l10n/app_text.dart';
|
|
import 'package:pangolin_vpn/l10n/strings_zh.dart';
|
|
import 'package:pangolin_vpn/models/routing_profile.dart';
|
|
import 'package:pangolin_vpn/pangolin_theme.dart';
|
|
import 'package:pangolin_vpn/screens/settings_page.dart';
|
|
import 'package:pangolin_vpn/services/token_store.dart';
|
|
import 'package:pangolin_vpn/shell/desktop_shell.dart';
|
|
import 'package:pangolin_vpn/shell/home_shell.dart';
|
|
import 'package:pangolin_vpn/state/app_providers.dart';
|
|
import 'package:pangolin_vpn/state/auth_provider.dart';
|
|
import 'package:pangolin_vpn/state/navigation_provider.dart';
|
|
import 'package:pangolin_vpn/state/routing_provider.dart';
|
|
import 'package:pangolin_vpn/widgets/routing_screen.dart';
|
|
|
|
import '../helpers/harness.dart';
|
|
|
|
final AppText t = StringsZh();
|
|
|
|
class _NullTokenStore implements TokenStore {
|
|
const _NullTokenStore();
|
|
@override
|
|
Future<void> saveTokens({required String access, required String refresh}) async {}
|
|
@override
|
|
Future<String?> loadAccessToken() async => null;
|
|
@override
|
|
Future<String?> loadRefreshToken() async => null;
|
|
@override
|
|
Future<void> clear() async {}
|
|
@override
|
|
Future<void> markOnboarded() async {}
|
|
@override
|
|
Future<bool> isOnboarded() async => true;
|
|
@override
|
|
Future<void> saveLastEmail(String email) async {}
|
|
@override
|
|
Future<String?> loadLastEmail() async => null;
|
|
}
|
|
|
|
void main() {
|
|
setUpAll(disableGoogleFontsFetching);
|
|
|
|
testWidgets('settings page IA: three group labels present / purchase entry moved out to account', (tester) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.windows;
|
|
tester.view.physicalSize = const Size(920, 1400);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
try {
|
|
await tester.pumpWidget(ProviderScope(
|
|
overrides: [
|
|
tokenStoreProvider.overrideWithValue(const _NullTokenStore()),
|
|
vpnBridgeProvider.overrideWithValue(VpnBridgeMock()),
|
|
appTextProvider.overrideWithValue(t),
|
|
navViewProvider.overrideWith((ref) => NavView.settings),
|
|
],
|
|
child: MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
theme: PangolinTheme.light,
|
|
home: const HomeShell(),
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(DesktopShell), findsOneWidget);
|
|
expect(find.byType(SettingsPage), findsOneWidget);
|
|
|
|
// 三个分组标题存在(连接/外观/关于)。
|
|
expect(find.descendant(of: find.byType(SettingsPage), matching: find.text(t.settingsGroupConn)), findsOneWidget);
|
|
expect(find.descendant(of: find.byType(SettingsPage), matching: find.text(t.settingsGroupAppearance)), findsOneWidget);
|
|
expect(find.descendant(of: find.byType(SettingsPage), matching: find.text(t.settingsGroupAbout)), findsOneWidget);
|
|
|
|
// 购买入口不在设置页(归 Account)。
|
|
expect(find.descendant(of: find.byType(SettingsPage), matching: find.text(t.purchaseTitle)), findsNothing);
|
|
} finally {
|
|
debugDefaultTargetPlatformOverride = null;
|
|
}
|
|
});
|
|
|
|
testWidgets('settings 连接组:smartRoute 开关被「分流规则」下钻行取代(标题+当前模式pill+chevron),点击导航到 RoutingScreen',
|
|
(tester) async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.windows;
|
|
tester.view.physicalSize = const Size(920, 1400);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
try {
|
|
await tester.pumpWidget(ProviderScope(
|
|
overrides: [
|
|
tokenStoreProvider.overrideWithValue(const _NullTokenStore()),
|
|
vpnBridgeProvider.overrideWithValue(VpnBridgeMock()),
|
|
appTextProvider.overrideWithValue(t),
|
|
navViewProvider.overrideWith((ref) => NavView.settings),
|
|
routingProfileProvider.overrideWith(() => _FakeRoutingNotifier(const RoutingProfile(mode: 'global'))),
|
|
],
|
|
child: MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
theme: PangolinTheme.light,
|
|
home: const HomeShell(),
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
// 旧的 smartRoute 布尔开关行已消失。
|
|
expect(find.descendant(of: find.byType(SettingsPage), matching: find.text(t.smartRoute)), findsNothing);
|
|
|
|
// 新的下钻行:标题「分流规则」+ 当前模式(全局代理)pill。
|
|
final routingRow = find.descendant(of: find.byType(SettingsPage), matching: find.text(t.routingRulesTitle));
|
|
expect(routingRow, findsOneWidget);
|
|
expect(find.descendant(of: find.byType(SettingsPage), matching: find.text(t.routingModeGlobal)), findsOneWidget);
|
|
|
|
await tester.tap(routingRow);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(RoutingScreen), findsOneWidget);
|
|
} finally {
|
|
debugDefaultTargetPlatformOverride = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
class _FakeRoutingNotifier extends RoutingProfileNotifier {
|
|
_FakeRoutingNotifier(this._v);
|
|
final RoutingProfile _v;
|
|
@override
|
|
Future<RoutingProfile> build() async => _v;
|
|
}
|