Files
platforms/apps/user_app/tool/a1_visual_test.dart
czl231 3ef33b531d 已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
2026-09-13 00:57:32 +08:00

198 lines
7.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 功能描述:以独立 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<Map<String, Object?>?> read(String owner) async => null;
@override
Future<void> write(String owner, Map<String, Object?> draft) async {}
@override
Future<void> 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();
});
}
}
}
}