import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:observer/auth/auth_screen.dart'; import 'package:observer/auth/auth_view_model.dart'; import 'package:observer/auth/session_store.dart'; import 'package:observer/home/home_screen.dart'; import 'package:observer/home/home_view_model.dart'; import 'package:observer/payment/license_service.dart'; import 'package:observer/payment/models.dart'; import 'package:observer/payment/order_api.dart'; import 'package:provider/provider.dart'; /// 测试环境无 secure storage 平台实现:mock 通道让读写立即返回空。 void _mockSecureStorage() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'), (call) async => null, ); } /// 内存版 SessionStore:不触平台通道(flutter_secure_storage 在测试环境不可用) class FakeSession extends SessionStore { String? token; String? phone; bool cleared = false; @override Future readToken() async => token; @override Future readPhone() async => phone; @override Future save(String phone, String token) async { this.phone = phone; this.token = token; } @override Future clear() async { token = null; phone = null; cleared = true; } } /// 内存版 OrderApi:登录/注册/授权全部本地返回,不触网络 class FakeOrderApi extends OrderApi { final LicenseStatus licenseResult; FakeOrderApi(SessionStore s, {this.licenseResult = const LicenseStatus(active: false)}) : super(sessionStore: s); @override Future register({required String phone, required String password}) async {} @override Future login({required String phone, required String password}) async => 'fake-token'; @override Future fetchLicense() async => licenseResult; } Widget _app(FakeSession session, FakeOrderApi api, {String initialRoute = '/login'}) { final authVm = AuthViewModel(orderApi: api, sessionStore: session); final homeVm = HomeViewModel( licenseService: LicenseService(orderApi: api, sessionStore: session)); return MultiProvider( providers: [ Provider.value(value: session), ChangeNotifierProvider.value(value: authVm), ChangeNotifierProvider.value(value: homeVm), ], child: MaterialApp( initialRoute: initialRoute, routes: { '/login': (_) => const AuthScreen(), '/home': (_) => const HomeScreen(), '/camera': (_) => const Scaffold(body: Text('camera-stub')), }, ), ); } Future _enterCredentials(WidgetTester tester, {required bool register}) async { if (register) { await tester.tap(find.text('没有账号?去注册')); await tester.pumpAndSettle(); } await tester.enterText(find.widgetWithText(TextField, '手机号'), '13912345678'); await tester.enterText(find.widgetWithText(TextField, '密码'), 'pass123456'); } void main() { setUp(_mockSecureStorage); testWidgets('登录流程:输错密码提示错误,不跳转', (tester) async { final session = FakeSession(); final api = FakeOrderApi(session); await tester.pumpWidget(_app(session, api)); await tester.tap(find.text('登录')); await tester.pumpAndSettle(); // 本地校验:空手机号提示 expect(find.text('请输入手机号和至少 6 位密码'), findsOneWidget); }); testWidgets('注册并登录成功 → 跳转主界面展示未充值', (tester) async { final session = FakeSession(); final api = FakeOrderApi(session); await tester.pumpWidget(_app(session, api)); await _enterCredentials(tester, register: true); await tester.tap(find.text('注册并登录')); await tester.pumpAndSettle(); expect(find.byType(HomeScreen), findsOneWidget); expect(session.token, 'fake-token'); expect(session.phone, '13912345678'); expect(find.textContaining('未充值'), findsOneWidget); expect(find.text('搜索'), findsOneWidget); expect(find.text('充值'), findsOneWidget); }); testWidgets('搜索按钮强制服务端校验:未授权弹出拦截弹窗', (tester) async { final session = FakeSession()..token = 'fake-token'; final api = FakeOrderApi(session); // active: false await tester.pumpWidget(_app(session, api, initialRoute: '/home')); await tester.tap(find.text('搜索')); await tester.pumpAndSettle(); expect(find.text('授权不可用'), findsOneWidget); expect(find.text('去充值'), findsOneWidget); }); testWidgets('搜索按钮校验通过 → 进入相机页', (tester) async { final session = FakeSession()..token = 'fake-token'; final api = FakeOrderApi( session, licenseResult: LicenseStatus( active: true, expiresAt: DateTime.now().add(const Duration(days: 1))), ); await tester.pumpWidget(_app(session, api, initialRoute: '/home')); await tester.tap(find.text('搜索')); await tester.pumpAndSettle(); expect(find.text('camera-stub'), findsOneWidget); }); }