flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
// 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<void> 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<void> 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<TextField>(passwordField).obscureText, isTrue);
// 点击"眼睛"图标 → 明文
await tester.tap(find.byIcon(Icons.visibility_off_outlined));
await tester.pump();
expect(tester.widget<TextField>(passwordField).obscureText, isFalse);
// 图标本身也切换了
expect(find.byIcon(Icons.visibility_outlined), findsOneWidget);
// 再点一次 → 回到打码
await tester.tap(find.byIcon(Icons.visibility_outlined));
await tester.pump();
expect(tester.widget<TextField>(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);
});
}