// routing_screen_test.dart — 分流规则子屏冒烟测试:模式段选 + 规则行 + 添加按钮均可见。 // // 仿 test/widget/invite_page_test.dart 的 _FakeNotifier 范式:直接 extends // RoutingProfileNotifier 覆写 build(),绕过真实账户 API。 import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.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/state/app_providers.dart'; import 'package:pangolin_vpn/state/routing_provider.dart'; import 'package:pangolin_vpn/widgets/routing_screen.dart'; import '../helpers/harness.dart'; void main() { setUpAll(disableGoogleFontsFetching); testWidgets('routing screen shows mode seg + rules + add', (tester) async { final profile = RoutingProfile.fromJson({ 'mode': 'rule', 'builtin': {'china_direct': true, 'lan_direct': true, 'private_via_tunnel': true}, 'rules': [ {'type': 'domain_suffix', 'value': 'x.com', 'action': 'direct'}, ], 'final': 'proxy', }); await tester.pumpWidget(ProviderScope( overrides: [ appTextProvider.overrideWithValue(StringsZh()), routingProfileProvider.overrideWith(() => _FakeRoutingNotifier(profile)), ], child: MaterialApp( debugShowCheckedModeBanner: false, theme: PangolinTheme.light, home: const RoutingScreen(), ), )); await tester.pumpAndSettle(); // 模式段选(全局/智能分流/全部直连)存在。 expect(find.text(StringsZh().routingModeSmart), findsWidgets); // 规则行:目标值展示。 expect(find.text('x.com'), findsOneWidget); // 添加规则按钮存在。 expect(find.text(StringsZh().routingAddRule), findsOneWidget); }); testWidgets('non-smart mode dims the my-rules section', (tester) async { final profile = RoutingProfile.fromJson({ 'mode': 'global', 'builtin': {'china_direct': true, 'lan_direct': true, 'private_via_tunnel': true}, 'rules': [ {'type': 'domain_suffix', 'value': 'x.com', 'action': 'direct'}, ], 'final': 'proxy', }); await tester.pumpWidget(ProviderScope( overrides: [ appTextProvider.overrideWithValue(StringsZh()), routingProfileProvider.overrideWith(() => _FakeRoutingNotifier(profile)), ], child: MaterialApp( debugShowCheckedModeBanner: false, theme: PangolinTheme.light, home: const RoutingScreen(), ), )); await tester.pumpAndSettle(); final opacity = tester.widget(find .ancestor(of: find.text(StringsZh().routingMyRulesOrdered), matching: find.byType(Opacity)) .first); expect(opacity.opacity, lessThan(1)); }); testWidgets('a rule shadowed by an earlier identical rule is flagged', (tester) async { final profile = RoutingProfile.fromJson({ 'mode': 'rule', 'builtin': {'china_direct': true, 'lan_direct': true, 'private_via_tunnel': true}, 'rules': [ {'type': 'domain_suffix', 'value': 'dup.com', 'action': 'direct'}, {'type': 'domain_suffix', 'value': 'dup.com', 'action': 'proxy'}, ], 'final': 'proxy', }); await tester.pumpWidget(ProviderScope( overrides: [ appTextProvider.overrideWithValue(StringsZh()), routingProfileProvider.overrideWith(() => _FakeRoutingNotifier(profile)), ], child: MaterialApp( debugShowCheckedModeBanner: false, theme: PangolinTheme.light, home: const RoutingScreen(), ), )); await tester.pumpAndSettle(); expect(find.text(StringsZh().routingRuleShadowed), findsOneWidget); }); testWidgets('a domain rule matching a system-locked domain is flagged forced', (tester) async { final profile = RoutingProfile.fromJson({ 'mode': 'rule', 'builtin': {'china_direct': true, 'lan_direct': true, 'private_via_tunnel': true}, 'rules': [ {'type': 'domain_suffix', 'value': 'git.yanmeiai.com', 'action': 'direct'}, ], 'final': 'proxy', 'system_locked_domains': ['git.yanmeiai.com'], }); await tester.pumpWidget(ProviderScope( overrides: [ appTextProvider.overrideWithValue(StringsZh()), routingProfileProvider.overrideWith(() => _FakeRoutingNotifier(profile)), ], child: MaterialApp( debugShowCheckedModeBanner: false, theme: PangolinTheme.light, home: const RoutingScreen(), ), )); await tester.pumpAndSettle(); expect(find.text(StringsZh().routingForcedRow), findsOneWidget); }); testWidgets('reset button opens confirm dialog and resets on confirm', (tester) async { final profile = RoutingProfile.fromJson({ 'mode': 'rule', 'builtin': {'china_direct': true, 'lan_direct': true, 'private_via_tunnel': true}, 'rules': [ {'type': 'domain_suffix', 'value': 'x.com', 'action': 'direct'}, ], 'final': 'proxy', }); final notifier = _FakeRoutingNotifier(profile); await tester.pumpWidget(ProviderScope( overrides: [ appTextProvider.overrideWithValue(StringsZh()), routingProfileProvider.overrideWith(() => notifier), ], child: MaterialApp( debugShowCheckedModeBanner: false, theme: PangolinTheme.light, home: const RoutingScreen(), ), )); await tester.pumpAndSettle(); expect(find.text('x.com'), findsOneWidget); // 重置按钮在 FINAL 区之后,滚到可见再点(屏内不止一个 Scrollable,取外层 ListView)。 await tester.scrollUntilVisible( find.text(StringsZh().routingReset), 200, scrollable: find.byType(Scrollable).first, ); await tester.pumpAndSettle(); await tester.tap(find.text(StringsZh().routingReset)); await tester.pumpAndSettle(); // 确认弹层出现,取消不触发重置。 expect(find.text(StringsZh().routingResetConfirmBody), findsOneWidget); await tester.tap(find.text(StringsZh().devCancel)); await tester.pumpAndSettle(); expect(notifier.resetCalled, isFalse); expect(find.text('x.com'), findsOneWidget); // 再次点击并确认 → 触发重置。 await tester.tap(find.text(StringsZh().routingReset)); await tester.pumpAndSettle(); await tester.tap(find.text(StringsZh().routingResetConfirmAction)); await tester.pumpAndSettle(); expect(notifier.resetCalled, isTrue); expect(find.text('x.com'), findsNothing); }); } class _FakeRoutingNotifier extends RoutingProfileNotifier { _FakeRoutingNotifier(this._v); final RoutingProfile _v; bool resetCalled = false; @override Future build() async => _v; /// 绕过真实 _persist(需要 accountApiProvider/网络),只验证「确认后触发重置」的 /// UI 交互链路;真正的 _persist 复用行为由 routing_provider_test.dart 覆盖。 @override Future resetToDefault() async { resetCalled = true; state = AsyncData(RoutingProfile.defaults().copyWith(systemLockedDomains: _v.systemLockedDomains)); } }