198 lines
7.7 KiB
Dart
198 lines
7.7 KiB
Dart
|
|
// 功能描述:以独立 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();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|