// 功能描述:以独立 Fixture 生成 A1 五页截图,验证多宽度与文本缩放无溢出。 // 版本:1.0.0 import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:user_app/app/dependencies.dart'; import 'package:user_app/app/router.dart'; import 'package:user_app/data/services/secure_session_store.dart'; import 'package:user_app/data/services/repair_draft_store.dart'; import 'package:user_app/ui/core/app_theme.dart'; import '../test/support/a1_fixture.dart'; /// 仅截图使用的会话,不提供真实令牌或网络凭据。 class VisualSession extends UserSession { VisualSession(this.signedIn) : super(SecureSessionStore()); final bool signedIn; @override bool get isAuthenticated => signedIn; } /// 视觉夹具不访问本机持久化,但保留生产页面的草稿入口。 class VisualDraftStore implements RepairDraftStore { @override Future?> read(String owner) async => null; @override Future write(String owner, Map draft) async {} @override Future delete(String owner) async {} } /// 图片输出为新截图基线,不能单凭生成成功声称匹配设计稿。 void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUpAll(() async { // 本文件是由 flutter test 执行的视觉测试,位置在tool目录。 // ignore: invalid_use_of_visible_for_testing_member PackageInfo.setMockInitialValues( appName: '瓶安芯', packageName: 'user_app', version: '1.0.0', buildNumber: '1', buildSignature: '', ); final font = File('C:/Windows/Fonts/msyh.ttc'); if (await font.exists()) { final loader = FontLoader('VisualChinese') ..addFont(Future.value(ByteData.sublistView(await font.readAsBytes()))); await loader.load(); // Widget 测试默认使用 Ahem;组件主题中的独立文字样式也需真实字体。 for (final family in ['Roboto', 'Ahem']) { final fallback = FontLoader(family) ..addFont(Future.value(ByteData.sublistView(await font.readAsBytes()))); await fallback.load(); } } final icons = FontLoader('MaterialIcons') ..addFont(rootBundle.load('fonts/MaterialIcons-Regular.otf')); await icons.load(); }); for (final (number, path) in [ ('01', '/login'), ('03', '/home'), ('05', '/repair'), ('11', '/shop'), ('12', '/products/p1'), ('13', '/cart'), ('14', '/cart/checkout'), ('15', '/favorites'), ('16', '/deposits'), ('17', '/deposits/return?deposit=d1'), ('18', '/gas/order'), ('19', '/payment/gas/payment-1'), ('20', '/orders'), ('20-shop', '/orders?tab=shop'), ('21', '/shop/orders/order-1'), ('21-gas', '/gas/orders/gas-1'), ('22', '/gas/orders/gas-1/delivery'), ('23', '/gas/orders/gas-1/delivery/track'), ('24', '/invoice/gas/gas-1'), ('25', '/me'), ('26', '/records'), ('27', '/usage'), ('28', '/wallet'), ('29', '/addresses'), ('30', '/messages'), ('31', '/settings'), ('31-payment', '/settings/payment-password'), ('32', '/records/contracts'), ('33', '/family'), ('39', '/wallet/withdraw'), ('40', '/wallet/banks'), ('41', '/profile/edit'), ('42', '/tickets/t1'), ]) { for (final width in [320, 360, 390, 430]) { for (final scale in [1.0, 1.3]) { testWidgets('$number ${width}px 文本$scale', (tester) async { // 图30原稿是853×1873双倍像素,390宽归一化后高度约856像素。 tester.view.physicalSize = Size(width.toDouble(), number == '30' ? 856 : 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final session = VisualSession(path != '/login'); final router = createRouter( AppDependencies( session: session, repository: A1FixtureRepository(), repairDraftStore: VisualDraftStore(), ), initialLocation: path, ); final theme = AppTheme.light(); await tester.pumpWidget( MaterialApp.router( routerConfig: router, theme: theme.copyWith( textTheme: theme.textTheme.apply(fontFamily: 'VisualChinese'), appBarTheme: theme.appBarTheme.copyWith( titleTextStyle: theme.appBarTheme.titleTextStyle?.copyWith( fontFamily: 'VisualChinese', ), ), filledButtonTheme: FilledButtonThemeData( style: theme.filledButtonTheme.style?.copyWith( textStyle: WidgetStatePropertyAll( theme.textTheme.labelLarge?.copyWith(fontFamily: 'VisualChinese'), ), ), ), textButtonTheme: TextButtonThemeData( style: theme.textButtonTheme.style?.copyWith( textStyle: WidgetStatePropertyAll( theme.textTheme.labelLarge?.copyWith(fontFamily: 'VisualChinese'), ), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: theme.outlinedButtonTheme.style?.copyWith( textStyle: WidgetStatePropertyAll( theme.textTheme.labelLarge?.copyWith(fontFamily: 'VisualChinese'), ), ), ), chipTheme: theme.chipTheme.copyWith( labelStyle: theme.chipTheme.labelStyle?.copyWith(fontFamily: 'VisualChinese'), ), ), builder: (context, child) => MediaQuery( data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(scale)), child: RepaintBoundary(key: const ValueKey('capture'), child: child!), ), ), ); await tester.pumpAndSettle(); if (number == '17') { // 产品稿展示用户已逐项确认的状态,视觉样本先完成同样的真实交互。 final checkboxCount = find.byType(Checkbox).evaluate().length; for (var index = 0; index < checkboxCount; index++) { await tester.tap(find.byType(Checkbox).at(index)); await tester.pump(); } } if (number == '18') { // 产品稿默认选择15kg一只;通过真实“增加”按钮进入相同视觉状态。 await tester.tap(find.byTooltip('增加').last); await tester.pumpAndSettle(); } if (number == '39') { await tester.enterText(find.byKey(const Key('withdrawal-amount')), '300'); await tester.pumpAndSettle(); } expect(tester.takeException(), isNull); await expectLater( find.byKey(const ValueKey('capture')), matchesGoldenFile( Uri.file( '${Directory.current.path}/../../docs/视觉验收/A1/${number}_${width}_$scale.png', ), ), ); // 验证滚动到底部后仍无文字、按钮或导航溢出。 final list = find.byType(Scrollable).first; if (list.evaluate().isNotEmpty) { await tester.drag(list, const Offset(0, -1600)); await tester.pumpAndSettle(); } expect(tester.takeException(), isNull); await tester.pumpWidget(const SizedBox()); router.dispose(); session.dispose(); }); } } } }