0f05385995
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 28s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 37s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 22s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 13s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 29s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 8s
ci-pangolin / Go — build + test (pull_request) Successful in 14s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 9s
ci-pangolin / Lint — shellcheck (pull_request) Failing after 11m2s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 13m15s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 13m22s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 15m0s
早前 i18n 把默认语种改英文(localeProvider 默认 AppLang.en),但这几个测试硬编码 const t = StringsZh() 中文预期,被测代码经 appTextProvider 渲染英文 → 断言失配 (既存 broken,先于 ds-flow;DinD 修复后 Flutter test job 才跑到暴露)。 修:各失败测试的 ProviderScope/Container overrides 加 localeProvider.overrideWith((ref) => _FixedLocale(AppLang.zh)),把被测代码钉回 中文(与硬编码 StringsZh 预期一致)。只动测试文件,未碰 lib/golden/test_config。 - connection_watchdog_test(节点异常提示 / 免费额度归零切断) - node_connect_confirm_test(点节点/智能卡弹确认取消不切换) - stats_page_test(PeriodCard 周期卡中文标签) 容器复跑 test/unit+widget+contract:+97 All passed。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
143 lines
5.9 KiB
Dart
143 lines
5.9 KiB
Dart
// node_connect_confirm_test.dart — 节点列表防误触:已连接时点其他节点先弹确认。
|
|
//
|
|
// off → 点节点直接选(跳连接页,无破坏性,不弹确认)。
|
|
// on → 点其他节点弹确认框,取消则不切换(防误碰把当前连接切走)。
|
|
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.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/node.dart';
|
|
import 'package:pangolin_vpn/pangolin_theme.dart';
|
|
import 'package:pangolin_vpn/screens/nodes_page.dart';
|
|
import 'package:pangolin_vpn/state/app_providers.dart';
|
|
import 'package:pangolin_vpn/state/connection_provider.dart';
|
|
import 'package:pangolin_vpn/state/nodes_provider.dart';
|
|
import 'package:pangolin_vpn/widgets/smart_select_card.dart';
|
|
|
|
import '../helpers/harness.dart';
|
|
|
|
// 固定两节点的桩,绕过远端拉取与 10s 限流刷新。
|
|
class _StubNodes extends NodesNotifier {
|
|
@override
|
|
Future<List<Node>> build() async => _nodes;
|
|
@override
|
|
Future<void> refresh() async {}
|
|
}
|
|
|
|
const _nodes = [
|
|
Node(code: 'HK', nameZh: '香港', nameEn: 'Hong Kong', ping: 20, uuid: 'hk-uuid', host: 'hk', port: 443),
|
|
Node(code: 'JP', nameZh: '东京', nameEn: 'Tokyo', ping: 35, uuid: 'jp-uuid', host: 'jp', port: 443),
|
|
];
|
|
|
|
// localeProvider 现默认英文;测试硬编码 StringsZh 预期 → 钉死中文 locale。
|
|
class _FixedLocale extends LocaleNotifier {
|
|
_FixedLocale(AppLang lang) {
|
|
state = lang;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
setUpAll(disableGoogleFontsFetching);
|
|
const t = StringsZh();
|
|
|
|
ProviderContainer makeContainer(VpnBridge bridge) => ProviderContainer(overrides: [
|
|
localeProvider.overrideWith((ref) => _FixedLocale(AppLang.zh)),
|
|
nodesProvider.overrideWith(_StubNodes.new),
|
|
vpnBridgeProvider.overrideWithValue(bridge),
|
|
]);
|
|
|
|
Future<void> pump(WidgetTester tester, ProviderContainer container) async {
|
|
await tester.binding.setSurfaceSize(const Size(440, 900));
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
await tester.pumpWidget(UncontrolledProviderScope(
|
|
container: container,
|
|
child: MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
theme: PangolinTheme.light,
|
|
home: Scaffold(body: NodesPage(isWide: false, onPicked: () {})),
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('未连接:点节点直接选,不弹确认', (tester) async {
|
|
final container = makeContainer(VpnBridgeMock());
|
|
addTearDown(container.dispose);
|
|
await pump(tester, container);
|
|
|
|
expect(find.text('香港'), findsOneWidget);
|
|
await tester.tap(find.text('香港'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text(t.nodeSwitchConfirm), findsNothing, reason: 'off 态不应弹确认');
|
|
expect(container.read(selectedNodeCodeProvider), 'HK', reason: '直接选中该节点');
|
|
});
|
|
|
|
testWidgets('已连接:点其他节点弹确认 → 取消不切换', (tester) async {
|
|
final bridge = VpnBridgeMock(connectDelay: const Duration(milliseconds: 10));
|
|
final container = makeContainer(bridge);
|
|
addTearDown(bridge.dispose);
|
|
addTearDown(container.dispose);
|
|
await pump(tester, container);
|
|
|
|
// 创建控制器(订阅状态流)后驱动到 on;此后只用 pump(固定时长),
|
|
// 不能 pumpAndSettle(连接计时器每秒触发,永不 settle)。
|
|
container.read(connectionProvider);
|
|
await bridge.start('{}');
|
|
await tester.pump(const Duration(milliseconds: 15));
|
|
expect(container.read(connectionProvider).phase, VpnPhase.on);
|
|
|
|
await tester.tap(find.text('东京'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 350)); // 弹窗入场
|
|
|
|
expect(find.text(t.nodeSwitchTitle('东京')), findsOneWidget, reason: 'on 态切换前先弹确认');
|
|
expect(find.text(t.nodeSwitchConfirm), findsOneWidget);
|
|
expect(container.read(selectedNodeCodeProvider), kSmartNodeCode, reason: '弹窗期间还未切');
|
|
|
|
await tester.tap(find.text(t.devCancel));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 350)); // 弹窗退场
|
|
expect(container.read(selectedNodeCodeProvider), kSmartNodeCode, reason: '取消后维持原节点');
|
|
|
|
// 收尾:停桥 → 取消 stats 计时器 + 触发 off 回调取消 elapsed 计时器,
|
|
// 否则 fake-async 在用例结束时报「Pending timers」。
|
|
await bridge.stop();
|
|
await tester.pump();
|
|
});
|
|
|
|
testWidgets('已连接:点智能选择卡也弹确认 → 取消不切换', (tester) async {
|
|
final bridge = VpnBridgeMock(connectDelay: const Duration(milliseconds: 10));
|
|
final container = makeContainer(bridge);
|
|
addTearDown(bridge.dispose);
|
|
addTearDown(container.dispose);
|
|
// 先选具体节点(否则默认就是智能选择),便于验证取消不切到智能。
|
|
container.read(selectedNodeCodeProvider.notifier).state = 'HK';
|
|
await pump(tester, container);
|
|
|
|
container.read(connectionProvider);
|
|
await bridge.start('{}');
|
|
await tester.pump(const Duration(milliseconds: 15));
|
|
expect(container.read(connectionProvider).phase, VpnPhase.on);
|
|
|
|
await tester.tap(find.byType(SmartSelectCard));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 350));
|
|
|
|
expect(find.text(t.nodeSwitchTitle(t.smartSelect)), findsOneWidget, reason: '智能选择也应弹确认');
|
|
expect(container.read(selectedNodeCodeProvider), 'HK', reason: '弹窗期间未切到智能');
|
|
|
|
await tester.tap(find.text(t.devCancel));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 350));
|
|
expect(container.read(selectedNodeCodeProvider), 'HK', reason: '取消后维持原节点');
|
|
|
|
await bridge.stop();
|
|
await tester.pump();
|
|
});
|
|
}
|