// nodes_provider_test.dart — 节点选择 / 智能选择派生 import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pangolin_vpn/l10n/app_text.dart'; import 'package:pangolin_vpn/models/node.dart'; import 'package:pangolin_vpn/services/token_store.dart'; import 'package:pangolin_vpn/state/auth_provider.dart'; import 'package:pangolin_vpn/state/nodes_provider.dart'; // ── Stub TokenStore(无平台依赖)────────────────────────────────── class _NullTokenStore implements TokenStore { const _NullTokenStore(); @override Future saveTokens({required String access, required String refresh}) async {} @override Future loadAccessToken() async => null; @override Future loadRefreshToken() async => null; @override Future clear() async {} @override Future markOnboarded() async {} @override Future isOnboarded() async => true; @override Future saveLastEmail(String email) async {} @override Future loadLastEmail() async => null; } // 直接喂固定节点列表(不触发真实探针/HTTP)。 class _FakeNodes extends NodesNotifier { _FakeNodes(this.list); final List list; @override Future> build() async => list; } const _nodes = [ Node(code: 'HK', nameZh: '香港', nameEn: 'Hong Kong', ping: 30, uuid: 'hk'), Node(code: 'JP', nameZh: '日本', nameEn: 'Tokyo', ping: 12, uuid: 'jp'), Node(code: 'US', nameZh: '美国', nameEn: 'LA', ping: 200, uuid: 'us'), ]; ProviderContainer makeContainer({List overrides = const []}) => ProviderContainer(overrides: [ tokenStoreProvider.overrideWithValue(const _NullTokenStore()), ...overrides, ]); void main() { test('默认智能选择(AUTO)', () { final c = makeContainer(); addTearDown(c.dispose); expect(c.read(selectedNodeCodeProvider), kSmartNodeCode); expect(c.read(isSmartSelectProvider), true); }); test('无节点时 effectiveNode 返回占位(不再伪造演示节点)', () { final c = makeContainer(); addTearDown(c.dispose); expect(c.read(effectiveNodeProvider).code, kPlaceholderNode.code); }); test('智能选择取实测延迟最小节点', () async { final c = makeContainer(overrides: [nodesProvider.overrideWith(() => _FakeNodes(_nodes))]); addTearDown(c.dispose); await c.read(nodesProvider.future); expect(c.read(effectiveNodeProvider).code, 'JP'); // ping 12 最小 }); test('选中具体节点后生效节点随之改变', () async { final c = makeContainer(overrides: [nodesProvider.overrideWith(() => _FakeNodes(_nodes))]); addTearDown(c.dispose); await c.read(nodesProvider.future); c.read(selectedNodeCodeProvider.notifier).state = 'US'; expect(c.read(isSmartSelectProvider), false); expect(c.read(effectiveNodeProvider).code, 'US'); }); test('pingLabel:未测得显示 —', () { expect(const Node(code: 'X', nameZh: 'x', nameEn: 'x', ping: 0).pingLabel, '—'); expect(const Node(code: 'X', nameZh: 'x', nameEn: 'x', ping: 42).pingLabel, '42ms'); }); test('localizedSub 不串语言:无标签用拉丁地名', () { const tw = Node(code: 'TW', nameZh: '台湾 台北', nameEn: 'Taipei', ping: 28); expect(tw.localizedSub(AppLang.zh), 'Taipei'); expect(tw.localizedSub(AppLang.en), 'Taipei'); const hk = Node(code: 'HK', nameZh: '香港 · 流媒体', nameEn: 'Hong Kong', ping: 18, tag: NodeTag.streaming); expect(hk.localizedSub(AppLang.zh), '流媒体优化'); expect(hk.localizedSub(AppLang.en), 'Streaming'); }); }