// Widget 测试:登录页表单交互 // // 测什么(都是不触网络的本地交互): // ① 空账号点「立即登录」→ SnackBar 提示"请填写账号"; // ② 只有账号没密码 → 提示"请填写密码"; // ③ 账号密码都有但没勾协议 → 提示"请先阅读并同意《用户协议》和《隐私政策》"; // ④ 密码"眼睛"点击 → obscureText 在打码/明文间切换; // ⑤ 勾选协议 → 未勾选图标消失(协议状态真的变了)。 // // 为什么能跑: // 登录页校验顺序是"先本地校验 → 再调接口"(login_page.dart _handleLogin), // ①②③ 在调接口之前就 return,完全不碰网络/Dio。 // initState 异步读"记住的账号/密码"要 mock 两个存储插件(同 widget_test.dart)。 // LoginPage 用了 .w/.h/.sp → 必须包 ScreenUtilInit;用了 ref → 包 ProviderScope。 // // 运行:flutter test test/widgets/login_form_test.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:new_learn/features/auth/login_page.dart'; /// 按登录页真实运行环境 pump(ScreenUtilInit + ProviderScope + MaterialApp) Future pumpLoginPage(WidgetTester tester) async { // ⚠ 测试默认窗口是 800x600,ScreenUtil 会把 375 设计稿放大 ~2.1 倍, // 登录页元素全部溢出屏幕外:find.text 找得到,但 tap 命中测试点不到。 // 先把手持窗口设成手机尺寸(逻辑 375x812,dpr=1),缩放比例回到 1。 tester.view.physicalSize = const Size(375, 812); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); await tester.pumpWidget( ProviderScope( child: ScreenUtilInit( designSize: const Size(375, 812), builder: (context, child) => const MaterialApp(home: LoginPage()), ), ), ); await tester.pumpAndSettle(); } /// 推进 3 秒走完 SnackBar 的 2s 计时器 + 退场动画(防测试结束时报 pending timer) Future flushSnackBar(WidgetTester tester) async { await tester.pump(const Duration(seconds: 3)); await tester.pumpAndSettle(); } void main() { setUp(() { SharedPreferences.setMockInitialValues({}); FlutterSecureStorage.setMockInitialValues({}); }); testWidgets('① 空账号点登录 → 提示"请填写账号"', (WidgetTester tester) async { await pumpLoginPage(tester); await tester.tap(find.text('立即登录')); await tester.pump(); // SnackBar 入场动画第一帧 expect(find.text('请填写账号'), findsOneWidget); await flushSnackBar(tester); }); testWidgets('② 只有账号没密码 → 提示"请填写密码"', (WidgetTester tester) async { await pumpLoginPage(tester); await tester.enterText(find.byType(TextField).first, '13800138000'); await tester.tap(find.text('立即登录')); await tester.pump(); expect(find.text('请填写密码'), findsOneWidget); await flushSnackBar(tester); }); testWidgets('③ 账号密码齐全但没勾协议 → 提示先阅读并同意协议', (WidgetTester tester) async { await pumpLoginPage(tester); await tester.enterText(find.byType(TextField).first, '13800138000'); await tester.enterText(find.byType(TextField).at(1), '123456'); await tester.tap(find.text('立即登录')); await tester.pump(); expect(find.text('请先阅读并同意《用户协议》和《隐私政策》'), findsOneWidget); await flushSnackBar(tester); }); testWidgets('④ 密码"眼睛"点击 → obscureText 在打码/明文间切换', (WidgetTester tester) async { await pumpLoginPage(tester); final passwordField = find.byType(TextField).at(1); // 初始:密码打码 expect(tester.widget(passwordField).obscureText, isTrue); // 点击"眼睛"图标 → 明文 await tester.tap(find.byIcon(Icons.visibility_off_outlined)); await tester.pump(); expect(tester.widget(passwordField).obscureText, isFalse); // 图标本身也切换了 expect(find.byIcon(Icons.visibility_outlined), findsOneWidget); // 再点一次 → 回到打码 await tester.tap(find.byIcon(Icons.visibility_outlined)); await tester.pump(); expect(tester.widget(passwordField).obscureText, isTrue); }); testWidgets('⑤ 勾选协议 → 未勾选图标消失', (WidgetTester tester) async { await pumpLoginPage(tester); // 初始:记住密码默认勾选(check_circle),协议未勾选(radio_button_unchecked) final agreementCheckbox = find.byIcon(Icons.radio_button_unchecked); expect(agreementCheckbox, findsOneWidget); // 协议区在页面底部,SingleChildScrollView 内容超出 812 视口, // 直接 tap 会点到屏幕外 → 先 ensureVisible 滚到可见再点 await tester.ensureVisible(agreementCheckbox); await tester.tap(agreementCheckbox); await tester.pump(); // 协议已勾选 → 页面上不再有未勾选图标 expect(find.byIcon(Icons.radio_button_unchecked), findsNothing); }); }