已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
126
apps/user_app/test/ui/addresses_page_test.dart
Normal file
126
apps/user_app/test/ui/addresses_page_test.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
// 功能描述:验证地址新增、失败重试、删除确认和默认地址刷新。
|
||||
// 版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/shipping_address.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/address/addresses_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class AddressFixture extends A1FixtureRepository {
|
||||
List<ShippingAddress> values = [];
|
||||
final requests = <String>[];
|
||||
int deletes = 0;
|
||||
@override
|
||||
Future<List<ShippingAddress>> shippingAddresses() async => values;
|
||||
@override
|
||||
Future<void> saveShippingAddress(ShippingAddress address, {required String requestNo}) async {
|
||||
requests.add(requestNo);
|
||||
if (this.fail) throw const ApiException(500, '保存失败,请重试');
|
||||
values = [
|
||||
ShippingAddress(
|
||||
identity: 'saved',
|
||||
address: address.address,
|
||||
contactName: address.contactName,
|
||||
contactPhone: address.contactPhone,
|
||||
isDefault: address.isDefault,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setDefaultAddress(String identity) async {
|
||||
if (this.fail) throw const ApiException(500, '设置失败');
|
||||
final a = values.single;
|
||||
values = [
|
||||
ShippingAddress(
|
||||
identity: a.identity,
|
||||
address: a.address,
|
||||
contactName: a.contactName,
|
||||
contactPhone: a.contactPhone,
|
||||
isDefault: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAddress(String identity) async {
|
||||
deletes++;
|
||||
values = [];
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('新增校验,失败保留输入与请求号,成功刷新列表', (tester) async {
|
||||
final repo = AddressFixture()..fail = true;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: AddressesPage(repository: repo),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('新增收货地址'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('保存地址'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.requests, isEmpty);
|
||||
final fields = find.byType(TextFormField);
|
||||
await tester.enterText(fields.at(0), '收货人');
|
||||
await tester.enterText(fields.at(1), '13800000001');
|
||||
await tester.enterText(fields.at(2), '广东省广州市测试路 1 号');
|
||||
await tester.tap(find.text('保存地址'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('保存失败,请重试'), findsOneWidget);
|
||||
repo.fail = false;
|
||||
await tester.tap(find.text('保存地址'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.requests.length, 2);
|
||||
expect(repo.requests.first, repo.requests.last);
|
||||
expect(find.text('收货人'), findsOneWidget);
|
||||
expect(find.text('138****0001'), findsOneWidget);
|
||||
expect(find.text('地址管理'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('默认切换失败不假更新,成功刷新,删除需要确认', (tester) async {
|
||||
final repo = AddressFixture()
|
||||
..values = [
|
||||
const ShippingAddress(
|
||||
identity: 'owned',
|
||||
address: '测试地址',
|
||||
contactName: '收货人',
|
||||
contactPhone: '13800000001',
|
||||
),
|
||||
];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: AddressesPage(repository: repo),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
repo.fail = true;
|
||||
await tester.tap(find.text('设为默认'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.values.single.isDefault, false);
|
||||
repo.fail = false;
|
||||
await tester.tap(find.text('设为默认'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.values.single.isDefault, true);
|
||||
await tester.tap(find.text('编辑'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('删除地址'));
|
||||
await tester.tap(find.text('删除地址'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('取消'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.deletes, 0);
|
||||
await tester.tap(find.text('删除地址'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('删除'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.deletes, 1);
|
||||
expect(find.text('暂无收货地址'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
188
apps/user_app/test/ui/cart_page_test.dart
Normal file
188
apps/user_app/test/ui/cart_page_test.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
// 功能描述:购物车服务端确认、失效处理、管理删除与多商品重试回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/domain/models/cart_item.dart';
|
||||
import 'package:user_app/domain/models/primary_models.dart';
|
||||
import 'package:user_app/domain/models/shipping_address.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/ui/features/shop/cart_page.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/shop/checkout_page.dart';
|
||||
import 'package:user_app/ui/features/shop/product_detail_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Repo extends A1FixtureRepository {
|
||||
List<CartItem> items = [
|
||||
const CartItem(
|
||||
product: ProductSummary(identity: 'p1', name: '测试商品一', price: 1999, stock: 4),
|
||||
quantity: 1,
|
||||
selected: true,
|
||||
available: true,
|
||||
revision: 'one',
|
||||
),
|
||||
const CartItem(
|
||||
product: ProductSummary(identity: 'p2', name: '测试商品二', price: 3000, stock: 5),
|
||||
quantity: 2,
|
||||
selected: true,
|
||||
available: true,
|
||||
revision: 'two',
|
||||
),
|
||||
const CartItem(
|
||||
product: ProductSummary(identity: 'p3', name: '失效商品', price: 1000, stock: 0),
|
||||
quantity: 1,
|
||||
selected: true,
|
||||
available: false,
|
||||
revision: 'three',
|
||||
),
|
||||
];
|
||||
final submitted = <({String request, int amount, List<CartItem> items})>[];
|
||||
int writes = 0;
|
||||
bool failWrite = false;
|
||||
final targets = <int>[];
|
||||
@override
|
||||
Future<List<CartItem>> cart() async => List.of(items);
|
||||
@override
|
||||
Future<CartItem> cartItem(String identity) async =>
|
||||
items.firstWhere((i) => i.product.identity == identity);
|
||||
@override
|
||||
Future<CartItem> setCartItem(
|
||||
CartItem item, {
|
||||
required int quantity,
|
||||
required bool selected,
|
||||
}) async {
|
||||
writes++;
|
||||
targets.add(quantity);
|
||||
final updated = CartItem(
|
||||
product: item.product,
|
||||
quantity: quantity,
|
||||
selected: selected,
|
||||
available: item.available,
|
||||
revision: 'v$writes',
|
||||
);
|
||||
items = [
|
||||
for (final old in items)
|
||||
if (old.product.identity != item.product.identity) old else if (quantity > 0) updated,
|
||||
];
|
||||
if (failWrite) {
|
||||
failWrite = false;
|
||||
throw const ApiException(500, '结果待确认');
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, Object?>> submitCartOrder({
|
||||
required String requestNo,
|
||||
required List<CartItem> items,
|
||||
required ShippingAddress address,
|
||||
required int expectedAmount,
|
||||
String remark = '',
|
||||
}) async {
|
||||
submitted.add((request: requestNo, amount: expectedAmount, items: List.of(items)));
|
||||
if (submitted.length == 1) throw const ApiException(500, '提交结果未知');
|
||||
return {'identity': 'order'};
|
||||
}
|
||||
}
|
||||
|
||||
Future<GoRouter> _show(WidgetTester tester, _Repo repo, {String path = '/cart'}) async {
|
||||
final router = GoRouter(
|
||||
initialLocation: path,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/cart',
|
||||
builder: (_, s) => CartPage(repository: repo),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/cart/checkout',
|
||||
builder: (_, s) => CheckoutPage(repository: repo, productIdentity: '', fromCart: true),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/products/p1',
|
||||
builder: (_, s) => ProductDetailPage(repository: repo, identity: 'p1'),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/orders',
|
||||
builder: (_, s) => const Scaffold(body: Text('订单列表')),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/payment/shop/:identity',
|
||||
builder: (_, s) => Scaffold(body: Text('支付订单 ${s.pathParameters['identity']}')),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(router.dispose);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router, theme: AppTheme.light()));
|
||||
await tester.pumpAndSettle();
|
||||
return router;
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('服务端数量与失败重读,全选、失效取消、管理删除', (tester) async {
|
||||
final repo = _Repo();
|
||||
await _show(tester, repo);
|
||||
expect(find.text('合计:¥79.99'), findsOneWidget);
|
||||
expect(find.text('商品已失效'), findsOneWidget);
|
||||
repo.failWrite = true;
|
||||
await tester.tap(find.byTooltip('增加 测试商品一'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.items.first.quantity, 2);
|
||||
expect(find.text('合计:¥99.98'), findsOneWidget);
|
||||
expect(find.text('结果待确认'), findsOneWidget);
|
||||
await tester.tap(find.text('重新加载'));
|
||||
await tester.pumpAndSettle();
|
||||
final invalid = find.descendant(
|
||||
of: find.bySemanticsLabel('选择 失效商品'),
|
||||
matching: find.byType(Checkbox),
|
||||
);
|
||||
await tester.scrollUntilVisible(invalid, 180);
|
||||
await tester.drag(find.byType(Scrollable).first, const Offset(0, -120));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(invalid);
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.items.last.selected, isFalse);
|
||||
await tester.tap(find.text('管理'));
|
||||
await tester.pumpAndSettle();
|
||||
final all = find.descendant(of: find.bySemanticsLabel('全选商品'), matching: find.byType(Checkbox));
|
||||
await tester.tap(all);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('删除(3)'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(TextButton, '删除'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.items, isEmpty);
|
||||
expect(find.text('购物车还是空的'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('多商品确认与未知结果重试保留同一请求和整数金额', (tester) async {
|
||||
final repo = _Repo()..items.removeLast();
|
||||
await _show(tester, repo);
|
||||
await tester.tap(find.text('去结算(2)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('测试商品一'), findsOneWidget);
|
||||
expect(find.text('测试商品二'), findsOneWidget);
|
||||
expect(repo.submitted, isEmpty);
|
||||
await tester.tap(find.widgetWithText(FilledButton, '提交订单'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '重试提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.submitted.length, 2);
|
||||
expect(repo.submitted[0].request, repo.submitted[1].request);
|
||||
expect(repo.submitted[0].amount, 7999);
|
||||
expect(repo.submitted[1].items.last.quantity, 2);
|
||||
expect(find.text('支付订单 order'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('加入购物车未知结果重试不在新数量上重复累加', (tester) async {
|
||||
final repo = _Repo()..failWrite = true;
|
||||
await _show(tester, repo, path: '/products/p1');
|
||||
await tester.ensureVisible(find.text('加入购物车'));
|
||||
await tester.tap(find.text('加入购物车'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.targets, [2]);
|
||||
await tester.tap(find.text('重试加入'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.targets, [2, 2]);
|
||||
expect(repo.items.first.quantity, 2);
|
||||
});
|
||||
}
|
||||
77
apps/user_app/test/ui/contract_change_requests_test.dart
Normal file
77
apps/user_app/test/ui/contract_change_requests_test.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
// 功能描述:合同申请未知失败重试保持请求号,取消必须确认并刷新;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/domain/models/gas_order_detail.dart';
|
||||
import 'package:user_app/ui/features/orders/contract_change_requests.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Repo extends A1FixtureRepository {
|
||||
final ids = <String>[];
|
||||
bool created = false, cancelled = false;
|
||||
@override
|
||||
Future<bool> requestContractChange(String identity, String description, String requestNo) async {
|
||||
expect(identity, 'c1');
|
||||
expect(description, '申请变更配送约定');
|
||||
ids.add(requestNo);
|
||||
if (ids.length == 1) throw Exception('网络异常');
|
||||
created = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ContractChangeRequest>> contractChangeRequests(String identity) async => !created
|
||||
? []
|
||||
: [
|
||||
ContractChangeRequest.fromJson({
|
||||
'identity': 'request',
|
||||
'ticket_no': 'TK1',
|
||||
'status_name': cancelled ? '已取消' : '待受理',
|
||||
'description': '申请变更配送约定',
|
||||
'result': '',
|
||||
'allowed_actions': cancelled ? [] : ['cancel'],
|
||||
}),
|
||||
];
|
||||
@override
|
||||
Future<void> cancelTicket(String identity) async {
|
||||
expect(identity, 'request');
|
||||
cancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('申请失败重试和取消确认', (tester) async {
|
||||
final repo = _Repo();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ContractChangeRequests(repository: repo, identity: 'c1'),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), '申请变更配送约定');
|
||||
await tester.tap(find.text('提交申请'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('提交申请'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.ids.length, 2);
|
||||
expect(repo.ids.first, repo.ids.last);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('取消申请'),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('取消申请'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('返回'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.cancelled, isFalse);
|
||||
await tester.tap(find.text('取消申请'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('确认'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.cancelled, isTrue);
|
||||
expect(find.textContaining('已取消'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
42
apps/user_app/test/ui/contract_download_button_test.dart
Normal file
42
apps/user_app/test/ui/contract_download_button_test.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
// 功能描述:PDF保存失败重试、取消及防重复提交;版本:1.0.0。
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/features/orders/contract_download_button.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Repo extends A1FixtureRepository {
|
||||
@override
|
||||
Future<Uint8List> gasContractPdf(String identity) async =>
|
||||
Uint8List.fromList('%PDF-1.7'.codeUnits);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('失败可重试,系统取消不报告成功', (tester) async {
|
||||
var calls = 0;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ContractDownloadButton(
|
||||
repository: _Repo(),
|
||||
identity: 'c1',
|
||||
number: '../HT1',
|
||||
saver: (name, bytes) async {
|
||||
expect(name, '___HT1_合同');
|
||||
expect(String.fromCharCodes(bytes), '%PDF-1.7');
|
||||
if (++calls == 1) throw Exception('保存失败');
|
||||
return false;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('下载PDF'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('保存失败,请重试'), findsOneWidget);
|
||||
await tester.tap(find.text('下载PDF'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(calls, 2);
|
||||
expect(find.text('合同已保存'), findsNothing);
|
||||
});
|
||||
}
|
||||
29
apps/user_app/test/ui/delivery_detail_page_test.dart
Normal file
29
apps/user_app/test/ui/delivery_detail_page_test.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
// 功能描述:验证配送详情使用本人接口事实并明确展示首期缺项;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/features/orders/delivery_detail_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('配送详情展示真实分组并提示受控联系暂未开放', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: DeliveryDetailPage(repository: A1FixtureRepository(), identity: 'gas-1'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('配送中'), findsOneWidget);
|
||||
expect(find.text('张师傅'), findsOneWidget);
|
||||
expect(find.text('危险品从业资质 · 已核验'), findsOneWidget);
|
||||
expect(find.textContaining('178509050920001'), findsOneWidget);
|
||||
expect(find.text('电话与订单消息暂未开放'), findsOneWidget);
|
||||
await tester.ensureVisible(find.text('电话联系'));
|
||||
await tester.tap(find.text('电话联系'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('电话联系暂未开放'), findsOneWidget);
|
||||
expect(find.textContaining('受控呼叫服务'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
28
apps/user_app/test/ui/delivery_track_page_test.dart
Normal file
28
apps/user_app/test/ui/delivery_track_page_test.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
// 功能描述:验证配送轨迹使用隐私化接口事实并保留首期未开放提示;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/features/orders/delivery_track_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('配送轨迹展示路线、进度、配送人员并刷新', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: DeliveryTrackPage(repository: A1FixtureRepository(), identity: 'gas-1'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('配送中'), findsOneWidget);
|
||||
expect(find.text('薛海气站'), findsOneWidget);
|
||||
expect(find.text('气站已接单'), findsOneWidget);
|
||||
expect(find.text('商品装车完成'), findsOneWidget);
|
||||
expect(find.text('正在前往'), findsOneWidget);
|
||||
expect(find.text('张师傅'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('刷新轨迹'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('实时更新 09:56'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
28
apps/user_app/test/ui/deposit_page_test.dart
Normal file
28
apps/user_app/test/ui/deposit_page_test.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
// 功能描述:押金汇总、筛选、规则与退瓶状态入口回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/wallet/deposit_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('展示服务端押金并按状态筛选,退瓶入口不伪造提交', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: DepositPage(repository: A1FixtureRepository()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('¥200.00'), findsOneWidget);
|
||||
expect(find.text('GZ-15-0286'), findsOneWidget);
|
||||
await tester.tap(find.text('退款中').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('GZ-15-0193'), findsOneWidget);
|
||||
expect(find.text('GZ-15-0286'), findsNothing);
|
||||
await tester.tap(find.text('使用中').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('申请退瓶'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
43
apps/user_app/test/ui/deposit_return_page_test.dart
Normal file
43
apps/user_app/test/ui/deposit_return_page_test.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
// 功能描述:退瓶退押金页状态确认与最终确认回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/wallet/deposit_return_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('完整展示退瓶要素且最终确认前不提交', (tester) async {
|
||||
final repository = A1FixtureRepository();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: DepositReturnPage(repository: repository, depositIdentity: 'd1'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('退瓶退押金'), findsOneWidget);
|
||||
expect(find.text('15kg 液化气钢瓶'), findsOneWidget);
|
||||
expect(find.text('上门回收'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(find.text('预计退款'), 220);
|
||||
expect(find.text('¥100.00'), findsOneWidget);
|
||||
for (final label in [
|
||||
'瓶体完整,无变形、锈蚀',
|
||||
'阀门无缺失,无泄漏',
|
||||
'已停止使用并关闭阀门',
|
||||
'我已阅读《退瓶规则》',
|
||||
]) {
|
||||
await tester.scrollUntilVisible(find.text(label), 160);
|
||||
await tester.tap(find.text(label));
|
||||
await tester.pump();
|
||||
}
|
||||
await tester.ensureVisible(find.text('提交退瓶申请'));
|
||||
await tester.tap(find.text('提交退瓶申请'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('确认提交退瓶申请'), findsOneWidget);
|
||||
expect(repository.depositReturnCalls, 0);
|
||||
await tester.tap(find.text('再检查一下'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.depositReturnCalls, 0);
|
||||
});
|
||||
}
|
||||
95
apps/user_app/test/ui/device_groups_page_test.dart
Normal file
95
apps/user_app/test/ui/device_groups_page_test.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
// 功能:设备分组首期资料管理与设备归组交互回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
import 'package:user_app/domain/models/device_group.dart';
|
||||
import 'package:user_app/ui/features/profile/device_groups_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _GroupFixture extends A1FixtureRepository {
|
||||
final groups = <DeviceGroup>[
|
||||
const DeviceGroup(identity: 'kitchen', name: '厨房', sortNo: 0, deviceIdentities: {'valve'}),
|
||||
];
|
||||
String assignedGroup = 'kitchen';
|
||||
|
||||
@override
|
||||
Future<List<ClientRecord>> devices() async => const [
|
||||
ClientRecord(identity: 'valve', title: '厨房角阀', subtitle: 'V001', raw: {'kind': 'valve'}),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<DeviceGroup>> deviceGroups() async => List.of(groups);
|
||||
|
||||
@override
|
||||
Future<void> saveDeviceGroup({
|
||||
String? identity,
|
||||
required String name,
|
||||
required String requestNo,
|
||||
}) async {
|
||||
if (identity == null) {
|
||||
groups.add(
|
||||
DeviceGroup(
|
||||
identity: 'new-group',
|
||||
name: name,
|
||||
sortNo: groups.length,
|
||||
deviceIdentities: const {},
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final index = groups.indexWhere((group) => group.identity == identity);
|
||||
final old = groups[index];
|
||||
groups[index] = DeviceGroup(
|
||||
identity: old.identity,
|
||||
name: name,
|
||||
sortNo: old.sortNo,
|
||||
deviceIdentities: old.deviceIdentities,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteDeviceGroup(String identity) async {
|
||||
groups.removeWhere((group) => group.identity == identity);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> assignDeviceGroup(String deviceIdentity, String groupIdentity) async {
|
||||
assignedGroup = groupIdentity;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('可新建设备分组并保留未开放控制提示', (tester) async {
|
||||
final repository = _GroupFixture();
|
||||
await tester.pumpWidget(MaterialApp(home: DeviceGroupsPage(repository: repository)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('厨房'), findsOneWidget);
|
||||
expect(find.textContaining('分组开关及安全检查暂未开放'), findsOneWidget);
|
||||
await tester.tap(find.text('新建分组'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), '客厅');
|
||||
await tester.tap(find.text('保存'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repository.groups.any((group) => group.name == '客厅'), isTrue);
|
||||
expect(find.text('客厅'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('设备可真实移回未分组', (tester) async {
|
||||
final repository = _GroupFixture();
|
||||
await tester.pumpWidget(MaterialApp(home: DeviceGroupsPage(repository: repository)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('管理分组'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('更改'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('未分组'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repository.assignedGroup, isEmpty);
|
||||
expect(find.text('设备分组已更新'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
23
apps/user_app/test/ui/device_status_placeholder_test.dart
Normal file
23
apps/user_app/test/ui/device_status_placeholder_test.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
// 功能:未开放设备卡在窄屏和放大文字下不溢出、不展示模拟遥测;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/features/home/device_status_placeholder.dart';
|
||||
|
||||
void main() {
|
||||
for (final width in [320.0, 360.0, 390.0, 430.0]) {
|
||||
testWidgets('设备未开放卡片$width宽度放大文字', (tester) async {
|
||||
tester.view.physicalSize = Size(width, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(MaterialApp(home: MediaQuery(
|
||||
data: MediaQueryData(size: Size(width, 844), textScaler: const TextScaler.linear(1.3)),
|
||||
child: const Scaffold(body: Padding(padding: EdgeInsets.all(32), child: DeviceStatusPlaceholder())),
|
||||
)));
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('暂不可查询'), findsNWidgets(4));
|
||||
expect(find.text('在线'), findsNothing);
|
||||
expect(find.text('正常'), findsNothing);
|
||||
});
|
||||
}
|
||||
}
|
||||
61
apps/user_app/test/ui/devices_page_test.dart
Normal file
61
apps/user_app/test/ui/devices_page_test.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
// 功能:本人设备搜索、类型筛选和未知状态展示;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
import 'package:user_app/domain/models/device_group.dart';
|
||||
import 'package:user_app/ui/features/profile/devices_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _DeviceFixture extends A1FixtureRepository {
|
||||
@override
|
||||
Future<List<ClientRecord>> devices() async => [
|
||||
ClientRecord(identity: 'valve', title: '厨房阀门', subtitle: 'V001', raw: {'kind': 'valve'}),
|
||||
ClientRecord(identity: 'alarm', title: '厨房报警器', subtitle: 'A002', raw: {'kind': 'alarm'}),
|
||||
];
|
||||
@override
|
||||
Future<List<DeviceGroup>> deviceGroups() async => const [
|
||||
DeviceGroup(identity: 'kitchen', name: '厨房组', sortNo: 0, deviceIdentities: {'valve', 'alarm'}),
|
||||
];
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('按类型及编号筛选,未知状态不冒充零告警或离线', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(home: DevicesPage(repository: _DeviceFixture())));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('—'), findsNWidgets(3));
|
||||
expect(find.text('厨房组'), findsOneWidget);
|
||||
expect(find.text('厨房阀门'), findsOneWidget);
|
||||
await tester.tap(find.text('报警器'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('厨房阀门'), findsNothing);
|
||||
expect(find.text('厨房报警器'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), 'V001');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('没有符合条件的设备'), findsOneWidget);
|
||||
await tester.tap(find.text('全部'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('厨房阀门'), findsOneWidget);
|
||||
await tester.tap(find.text('控制'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('该功能暂未开放,目前无法使用。'), findsOneWidget);
|
||||
});
|
||||
for (final width in [320.0, 390.0, 430.0]) {
|
||||
testWidgets('设备页$width宽度放大字体无溢出', (tester) async {
|
||||
tester.view.physicalSize = Size(width, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.3)),
|
||||
child: child!,
|
||||
),
|
||||
home: DevicesPage(repository: _DeviceFixture()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
81
apps/user_app/test/ui/emergency_contacts_page_test.dart
Normal file
81
apps/user_app/test/ui/emergency_contacts_page_test.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
// 功能:联系人真实成功刷新与失败幂等重试回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/ui/features/profile/emergency_contacts_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _ContactsFixture extends A1FixtureRepository {
|
||||
final calls = <String>[];
|
||||
bool failContactSave = true;
|
||||
int? failureCode;
|
||||
final contacts = <ClientRecord>[];
|
||||
@override
|
||||
Future<List<ClientRecord>> emergencyContacts() async => List.of(contacts);
|
||||
@override
|
||||
Future<void> saveEmergencyContact({String? identity,required String name,required String phone,
|
||||
required String relationship,required String requestNo}) async {
|
||||
calls.add(requestNo);
|
||||
if (failureCode != null) throw ApiException(failureCode!, 'server message');
|
||||
if (failContactSave) throw StateError('timeout');
|
||||
contacts.add(ClientRecord(identity:'test-contact',title:name,subtitle:phone,raw:{'relationship':relationship}));
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
for (final entry in <int,String>{2501:'最多可添加5位联系人',2502:'该手机号已在联系人列表中',2503:'该新增请求已处理'}.entries) {
|
||||
testWidgets('业务拒绝${entry.key}显示具体原因且不添加记录', (tester) async {
|
||||
final repo = _ContactsFixture()..failureCode=entry.key;
|
||||
await tester.pumpWidget(MaterialApp(home:EmergencyContactsPage(repository:repo)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('添加')); await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextFormField).at(0),'联系人');
|
||||
await tester.enterText(find.byType(TextFormField).at(1),'13800000002');
|
||||
await tester.tap(find.text('保存')); await tester.pumpAndSettle();
|
||||
expect(find.textContaining(entry.value),findsOneWidget);
|
||||
expect(repo.contacts,isEmpty);
|
||||
});
|
||||
}
|
||||
for (final width in [320.0,390.0,430.0]) {
|
||||
testWidgets('联系人长姓名在$width宽度和放大文字下保持可操作', (tester) async {
|
||||
tester.view.physicalSize = Size(width,844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final repo = _ContactsFixture();
|
||||
repo.contacts.add(ClientRecord(identity:'long',title:'用于核对长姓名换行的联系人',subtitle:'13800000002',raw:{'relationship':'需要保持完整显示的关系描述'}));
|
||||
await tester.pumpWidget(MaterialApp(builder: (context,child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.3)),child:child!),
|
||||
home:EmergencyContactsPage(repository:repo)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byTooltip('编辑联系人'),findsOneWidget);
|
||||
expect(tester.takeException(),isNull);
|
||||
});
|
||||
}
|
||||
testWidgets('失败保留输入及新增请求号,成功后刷新且权限仍未开放', (tester) async {
|
||||
final repo = _ContactsFixture();
|
||||
await tester.pumpWidget(MaterialApp(home:EmergencyContactsPage(repository:repo)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('添加'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextFormField).at(0),'测试联系人');
|
||||
await tester.enterText(find.byType(TextFormField).at(1),'13800000002');
|
||||
await tester.tap(find.text('保存'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('未能确认保存结果'),findsOneWidget);
|
||||
expect(repo.contacts,isEmpty);
|
||||
repo.failContactSave = false;
|
||||
await tester.tap(find.text('保存'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.calls.length,2);
|
||||
expect(repo.calls[0],repo.calls[1]);
|
||||
expect(find.text('测试联系人'),findsOneWidget);
|
||||
expect(find.text('138****0002'),findsOneWidget);
|
||||
expect(find.text('权限 · 暂未开放'),findsOneWidget);
|
||||
await tester.tap(find.text('远程控制'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('该功能暂未开放,目前无法使用。'),findsOneWidget);
|
||||
expect(tester.takeException(),isNull);
|
||||
});
|
||||
}
|
||||
101
apps/user_app/test/ui/family_sharing_page_test.dart
Normal file
101
apps/user_app/test/ui/family_sharing_page_test.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
// 功能:家庭共享页面布局、邀请幂等和撤销交互回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/domain/models/family_sharing.dart';
|
||||
import 'package:user_app/ui/features/profile/family_sharing_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _FamilyFixture extends A1FixtureRepository {
|
||||
String? invitedPhone;
|
||||
String? requestNo;
|
||||
String? revokedIdentity;
|
||||
|
||||
@override
|
||||
Future<FamilyDashboardData> familyDashboard() async => FamilyDashboardData(
|
||||
householdName: '薛海路之家',
|
||||
ownerName: '张先生',
|
||||
memberCount: 2,
|
||||
deviceCount: 1,
|
||||
members: [
|
||||
const FamilyMember(
|
||||
identity: 'owner',
|
||||
name: '张先生',
|
||||
phoneMasked: '138****5678',
|
||||
relationship: '房主',
|
||||
inviteStatus: 20,
|
||||
isOwner: true,
|
||||
permissions: [],
|
||||
),
|
||||
FamilyMember(
|
||||
identity: 'pending',
|
||||
name: '王女士',
|
||||
phoneMasked: '137****9056',
|
||||
relationship: '家人',
|
||||
inviteStatus: 10,
|
||||
isOwner: false,
|
||||
permissions: const [],
|
||||
expiresAt: DateTime(2026, 9, 20),
|
||||
),
|
||||
],
|
||||
devices: const [
|
||||
FamilyDevice(
|
||||
identity: 'device-1',
|
||||
name: '厨房报警器',
|
||||
kind: 'alarm',
|
||||
shareCount: 0,
|
||||
mappingConfigured: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> inviteFamilyMember({
|
||||
required String name,
|
||||
required String phone,
|
||||
required String relationship,
|
||||
required String requestNo,
|
||||
required List<FamilyDevicePermission> permissions,
|
||||
}) async {
|
||||
invitedPhone = phone;
|
||||
this.requestNo = requestNo;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> revokeFamilyMember(String identity) async {
|
||||
revokedIdentity = identity;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('家庭共享展示房主、待确认成员、设备和安全规则', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(MaterialApp(home: FamilySharingPage(repository: _FamilyFixture())));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('薛海路之家'), findsOneWidget);
|
||||
expect(find.text('张先生'), findsWidgets);
|
||||
expect(find.text('王女士'), findsOneWidget);
|
||||
expect(find.text('待确认'), findsOneWidget);
|
||||
expect(find.text('厨房报警器'), findsOneWidget);
|
||||
expect(find.text('远程控制需二次确认,保障用气安全'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('邀请校验通过后提交手机号和稳定请求号', (tester) async {
|
||||
final repository = _FamilyFixture();
|
||||
await tester.pumpWidget(MaterialApp(home: FamilySharingPage(repository: repository)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('添加'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextFormField).at(0), '李女士');
|
||||
await tester.enterText(find.byType(TextFormField).at(1), '13900000003');
|
||||
await tester.tap(find.text('发送邀请'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.invitedPhone, '13900000003');
|
||||
expect(repository.requestNo, isNotEmpty);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
136
apps/user_app/test/ui/favorites_page_test.dart
Normal file
136
apps/user_app/test/ui/favorites_page_test.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
// 功能描述:收藏分类、下架保留、管理取消和加购重试回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/domain/models/product_favorite.dart';
|
||||
import 'package:user_app/domain/models/primary_models.dart';
|
||||
import 'package:user_app/domain/models/cart_item.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/shop/favorites_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Repo extends A1FixtureRepository {
|
||||
List<ProductFavorite> items = const [
|
||||
ProductFavorite(
|
||||
state: ProductFavoriteState(productIdentity: 'p1', active: true, revision: 'r1'),
|
||||
product: ProductSummary(
|
||||
identity: 'p1',
|
||||
name: '燃气报警器',
|
||||
price: 16900,
|
||||
stock: 5,
|
||||
category: '配件',
|
||||
),
|
||||
available: true,
|
||||
specifications: ['家用款'],
|
||||
),
|
||||
ProductFavorite(
|
||||
state: ProductFavoriteState(productIdentity: 'p2', active: true, revision: 'r2'),
|
||||
product: ProductSummary(
|
||||
identity: 'p2',
|
||||
name: '家用燃气灶',
|
||||
price: 69900,
|
||||
stock: 0,
|
||||
category: '灶具',
|
||||
),
|
||||
available: false,
|
||||
),
|
||||
];
|
||||
final removed = <String>[];
|
||||
final quantities = <int>[];
|
||||
int quantity = 2;
|
||||
bool failAdd = true;
|
||||
@override
|
||||
Future<List<ProductFavorite>> favorites() async => List.of(items);
|
||||
@override
|
||||
Future<ProductFavoriteState> setFavorite(ProductFavoriteState state, bool active) async {
|
||||
removed.add(state.productIdentity);
|
||||
items = items.where((i) => i.product.identity != state.productIdentity).toList();
|
||||
return ProductFavoriteState(
|
||||
productIdentity: state.productIdentity,
|
||||
active: active,
|
||||
revision: 'new',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CartItem> cartItem(String identity) async => CartItem(
|
||||
product: items.first.product,
|
||||
quantity: quantity,
|
||||
selected: true,
|
||||
available: true,
|
||||
revision: 'cart',
|
||||
);
|
||||
@override
|
||||
Future<CartItem> setCartItem(
|
||||
CartItem item, {
|
||||
required int quantity,
|
||||
required bool selected,
|
||||
}) async {
|
||||
quantities.add(quantity);
|
||||
this.quantity = quantity;
|
||||
if (failAdd) {
|
||||
failAdd = false;
|
||||
throw const ApiException(500, '加购结果未知');
|
||||
}
|
||||
return CartItem(
|
||||
product: item.product,
|
||||
quantity: quantity,
|
||||
selected: selected,
|
||||
available: true,
|
||||
revision: 'next',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('按分类计数、下架保留且无加购,管理取消恢复空态', (tester) async {
|
||||
final repo = _Repo();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: FavoritesPage(repository: repo),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('全部 2'), findsOneWidget);
|
||||
expect(find.text('暂不可售'), findsOneWidget);
|
||||
expect(find.text('加入购物车'), findsOneWidget);
|
||||
await tester.tap(find.text('灶具 1'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('家用燃气灶'), findsOneWidget);
|
||||
expect(find.text('燃气报警器'), findsNothing);
|
||||
await tester.tap(find.text('删除'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.removed, ['p2']);
|
||||
expect(find.text('全部 1'), findsOneWidget);
|
||||
await tester.tap(find.text('管理'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byType(Checkbox).last);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('取消收藏(1)'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(TextButton, '取消收藏'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.removed, ['p2', 'p1']);
|
||||
expect(find.text('暂无收藏商品'), findsOneWidget);
|
||||
});
|
||||
testWidgets('收藏加购未知结果复用绝对目标数量', (tester) async {
|
||||
final repo = _Repo();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: FavoritesPage(repository: repo),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('加入购物车'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.quantities, [3]);
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('重试加入'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.quantities, [3, 3]);
|
||||
expect(repo.quantity, 3);
|
||||
});
|
||||
}
|
||||
34
apps/user_app/test/ui/feature_entry_test.dart
Normal file
34
apps/user_app/test/ui/feature_entry_test.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
// 功能:验证首期缺项与明确二期入口的不同提示,以及已开放入口行为;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/core/feature_entry.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('未指定阶段的缺项保留入口并提示暂未开放', (tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: Scaffold(body:
|
||||
FeatureEntry(icon: Icons.devices, label: '设备测试'))));
|
||||
expect(find.text('暂未开放'), findsOneWidget);
|
||||
expect(find.text('即将开放'), findsNothing);
|
||||
await tester.tap(find.text('设备测试'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(UnavailableStage.firstPhase.message), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('明确二期显示即将开放而已开放功能继续执行', (tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: Scaffold(body:
|
||||
FeatureEntry(icon: Icons.devices, label: '二期测试', stage: UnavailableStage.secondPhase))));
|
||||
expect(find.text('即将开放'), findsOneWidget);
|
||||
await tester.tap(find.text('二期测试'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(UnavailableStage.secondPhase.message), findsOneWidget);
|
||||
await tester.tap(find.text('知道了'));
|
||||
await tester.pumpAndSettle();
|
||||
var invoked = false;
|
||||
await tester.pumpWidget(MaterialApp(home: Scaffold(body:
|
||||
FeatureEntry(icon: Icons.devices, label: '可用测试', onTap: () => invoked = true))));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('暂未开放'), findsNothing);
|
||||
await tester.tap(find.text('可用测试'));
|
||||
expect(invoked, isTrue);
|
||||
});
|
||||
}
|
||||
54
apps/user_app/test/ui/gas_contract_history_test.dart
Normal file
54
apps/user_app/test/ui/gas_contract_history_test.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
// 功能描述:合同历史读取失败重试、事实显示及关闭;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/domain/models/gas_order_detail.dart';
|
||||
import 'package:user_app/ui/features/orders/gas_contract_history.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Repo extends A1FixtureRepository {
|
||||
int calls = 0;
|
||||
@override
|
||||
Future<List<GasContractEvent>> gasContractHistory(String identity) async {
|
||||
expect(identity, 'c1');
|
||||
if (++calls == 1) throw Exception('网络故障');
|
||||
return [
|
||||
GasContractEvent.fromJson({
|
||||
'identity': 'r1',
|
||||
'action': 'renew',
|
||||
'contract_status': 11,
|
||||
'occurred_at': '2026-09-08T10:00:00Z',
|
||||
'effective_at': '2026-09-08T00:00:00Z',
|
||||
'expired_at': '2027-09-08T00:00:00Z',
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('历史错误可重试且不冒充签署记录', (tester) async {
|
||||
final repo = _Repo();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => TextButton(
|
||||
onPressed: () => showGasContractHistory(context, repo, 'c1'),
|
||||
child: const Text('打开'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('打开'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('加载失败'), findsOneWidget);
|
||||
await tester.tap(find.text('重新加载'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('合同续期'), findsOneWidget);
|
||||
expect(find.text('变更后状态:生效中'), findsOneWidget);
|
||||
expect(find.text('到期日期:2027-09-08'), findsOneWidget);
|
||||
await tester.tap(find.byIcon(Icons.close));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('合同续期'), findsNothing);
|
||||
});
|
||||
}
|
||||
47
apps/user_app/test/ui/gas_contracts_page_test.dart
Normal file
47
apps/user_app/test/ui/gas_contracts_page_test.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
// 功能描述:合同页面筛选、搜索、正文读取及失败重试;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/domain/models/gas_order_detail.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/orders/gas_contracts_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Repo extends A1FixtureRepository {
|
||||
int calls = 0;
|
||||
@override
|
||||
Future<List<GasContractSummary>> gasContracts() async {
|
||||
if (++calls == 1) throw Exception('网络错误');
|
||||
return super.gasContracts();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('加载失败可重试,按真实状态和合同编号筛选,查看正文', (tester) async {
|
||||
final repo = _Repo();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: GasContractsPage(repository: repo),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('加载失败'), findsOneWidget);
|
||||
await tester.tap(find.text('重新加载'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('居民瓶装燃气供用气合同'), findsOneWidget);
|
||||
await tester.tap(find.text('已到期'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('暂无符合条件的合同'), findsOneWidget);
|
||||
await tester.tap(find.text('全部'));
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), '不存在');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('暂无符合条件的合同'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), 'HT20260905001');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('查看合同'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('测试合同正文'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
25
apps/user_app/test/ui/gas_order_create_page_test.dart
Normal file
25
apps/user_app/test/ui/gas_order_create_page_test.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
// 功能描述:气瓶下单页金额和二次确认提交回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/features/orders/gas_order_create_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('展示服务端报价且最终确认前不创建订单', (tester) async {
|
||||
final repository = A1FixtureRepository();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: GasOrderCreatePage(repository: repository)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('薛海气站'), findsOneWidget);
|
||||
expect(find.text('15kg 家用瓶'), findsOneWidget);
|
||||
expect(find.text('确认下单'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('增加').last);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('确认下单'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('确认创建气瓶订单?'), findsOneWidget);
|
||||
expect(repository.gasOrderCreateCalls, 0);
|
||||
});
|
||||
}
|
||||
107
apps/user_app/test/ui/gas_order_detail_test.dart
Normal file
107
apps/user_app/test/ui/gas_order_detail_test.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
// 功能描述:供气订单事实、合同阅读及本人签收确认和重试;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/gas_order_detail.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/orders/shop_order_detail_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Repo extends A1FixtureRepository {
|
||||
final requests = <String>[];
|
||||
int contractReads = 0;
|
||||
bool done = false, failContract = true;
|
||||
@override
|
||||
Future<GasOrderDetail> gasOrderDetail(String identity) async {
|
||||
if (!done) return super.gasOrderDetail(identity);
|
||||
return GasOrderDetail.fromJson({
|
||||
'identity': identity,
|
||||
'order_no': 'G1',
|
||||
'status_name': '已完成',
|
||||
'allowed_actions': [],
|
||||
'product_amount': 12800,
|
||||
'delivery_fee': 0,
|
||||
'discount_amount': 0,
|
||||
'payable_amount': 12800,
|
||||
'items': [],
|
||||
'timeline': [],
|
||||
'payments': [],
|
||||
}, (s) => s);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GasContractDetail> gasContractDetail(String identity) async {
|
||||
contractReads++;
|
||||
if (failContract) {
|
||||
failContract = false;
|
||||
throw const ApiException(500, '合同暂时读取失败');
|
||||
}
|
||||
return super.gasContractDetail(identity);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> confirmGasReceipt(String identity, {required String requestNo}) async {
|
||||
requests.add(requestNo);
|
||||
if (requests.length == 1) throw const ApiException(500, '签收结果待确认');
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('读取气站、历史支付与合同正文,签收须确认且重试保持请求号', (tester) async {
|
||||
final repo = _Repo();
|
||||
final router = GoRouter(
|
||||
initialLocation: '/gas/orders/g1',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/gas/orders/:id',
|
||||
builder: (_, s) => ShopOrderDetailPage(
|
||||
repository: repo,
|
||||
identity: s.pathParameters['id']!,
|
||||
business: 'gas',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(router.dispose);
|
||||
await tester.pumpWidget(MaterialApp.router(theme: AppTheme.light(), routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('薛海气站'), findsOneWidget);
|
||||
expect(find.text('配送员:李师傅'), findsOneWidget);
|
||||
expect(repo.requests, isEmpty);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('查看合同'),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('查看合同'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('加载失败'), findsOneWidget);
|
||||
await tester.tap(find.text('重新加载'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('测试合同正文,仅用于自动验证,不作为真实合同。'), findsOneWidget);
|
||||
expect(repo.contractReads, 2);
|
||||
await tester.tap(find.byWidgetPredicate((w) => w is IconButton && w.tooltip == '关闭合同'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('确认收货'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('返回'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.requests, isEmpty);
|
||||
await tester.tap(find.text('确认收货'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '确认收货').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('签收结果待确认'), findsOneWidget);
|
||||
await tester.tap(find.text('确认收货'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '确认收货').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.requests.length, 2);
|
||||
expect(repo.requests[0], repo.requests[1]);
|
||||
expect(find.text('确认收货'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
35
apps/user_app/test/ui/invoice_preview_page_test.dart
Normal file
35
apps/user_app/test/ui/invoice_preview_page_test.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
// 功能描述:验证电子发票二期页面只展示真实订单并明确提示即将开放;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/features/orders/invoice_preview_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('电子发票页面显示订单事实且不提交申请', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: InvoicePreviewPage(
|
||||
repository: A1FixtureRepository(),
|
||||
business: 'gas',
|
||||
identity: 'gas-1',
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('二期功能 · 即将开放'), findsOneWidget);
|
||||
expect(find.textContaining('G202609050920001'), findsOneWidget);
|
||||
expect(find.text('押金不计入发票金额'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('提交开票申请 · 即将开放'),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.tap(find.text('提交开票申请 · 即将开放'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('电子发票即将开放'), findsOneWidget);
|
||||
expect(find.textContaining('不会提交开票申请'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
@@ -14,6 +14,14 @@ class _RecordingUserSession extends UserSession {
|
||||
int loginCallCount = 0;
|
||||
String? lastPhone;
|
||||
String? lastPassword;
|
||||
String? lastCode;
|
||||
int codeCalls = 0;
|
||||
|
||||
@override
|
||||
Future<String> sendCode(String phone, String purpose) async {
|
||||
codeCalls++;
|
||||
return 'request-1';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> login({
|
||||
@@ -25,30 +33,84 @@ class _RecordingUserSession extends UserSession {
|
||||
loginCallCount += 1;
|
||||
lastPhone = phone;
|
||||
lastPassword = password;
|
||||
lastCode = verificationCode;
|
||||
}
|
||||
}
|
||||
|
||||
/// 模拟服务端确认短信未实际发送,验证页面不会显示虚假倒计时。
|
||||
class _UnavailableCodeSession extends _RecordingUserSession {
|
||||
@override
|
||||
Future<String> sendCode(String phone, String purpose) async {
|
||||
throw const ApiException(2410, '短信验证码暂未开放');
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证用户端登录界面的关键交互。
|
||||
void main() {
|
||||
testWidgets('显隐保留密码和焦点,重复选择当前登录方式不清空输入', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: _RecordingUserSession())));
|
||||
await tester.tap(find.text('密码登录'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField).at(1), 'Input-test-123');
|
||||
await tester.tap(find.byTooltip('显示密码'));
|
||||
await tester.pumpAndSettle();
|
||||
var field = tester.widget<TextField>(find.byType(TextField).at(1));
|
||||
expect(field.controller!.text, 'Input-test-123');
|
||||
expect(field.obscureText, isFalse);
|
||||
expect(field.focusNode!.hasFocus, isTrue);
|
||||
await tester.tap(find.text('密码登录'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<TextField>(find.byType(TextField).at(1)).controller!.text,
|
||||
'Input-test-123',
|
||||
);
|
||||
await tester.tap(find.byTooltip('隐藏密码'));
|
||||
await tester.pumpAndSettle();
|
||||
field = tester.widget<TextField>(find.byType(TextField).at(1));
|
||||
expect(field.obscureText, isTrue);
|
||||
expect(field.controller!.text, 'Input-test-123');
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('有焦点时切换验证码建立新连接并清除上一种凭据', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: _RecordingUserSession())));
|
||||
await tester.tap(find.text('密码登录'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField).at(1), 'Input-test-123');
|
||||
await tester.tap(find.text('验证码登录'));
|
||||
await tester.pumpAndSettle();
|
||||
final field = tester.widget<TextField>(find.byType(TextField).at(1));
|
||||
expect(field.controller!.text, isEmpty);
|
||||
expect(field.keyboardType, TextInputType.number);
|
||||
expect(field.obscureText, isFalse);
|
||||
expect(field.focusNode!.hasFocus, isTrue);
|
||||
await tester.enterText(find.byType(TextField).at(1), '314159');
|
||||
expect(field.controller!.text, '314159');
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('login page exposes phone and password fields', (tester) async {
|
||||
final session = UserSession(SecureSessionStore());
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
|
||||
|
||||
expect(find.text('瓶安芯'), findsOneWidget);
|
||||
expect(find.bySemanticsLabel('瓶安芯'), findsOneWidget);
|
||||
expect(find.byType(TextField), findsNWidgets(2));
|
||||
expect(find.text('安全登录'), findsOneWidget);
|
||||
expect(find.text('登录'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('手机号错误且密码为空时同时提示并阻止登录调用', (tester) async {
|
||||
final session = _RecordingUserSession();
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
|
||||
await tester.tap(find.text('密码登录'));
|
||||
await tester.pump();
|
||||
|
||||
await tester.enterText(find.byType(TextField).first, '+8613800138000');
|
||||
await tester.tap(find.text('安全登录'));
|
||||
await tester.ensureVisible(find.text('登录'));
|
||||
await tester.tap(find.text('登录'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('请输入正确的11位手机号'), findsOneWidget);
|
||||
expect(find.text('请输入密码'), findsOneWidget);
|
||||
expect(tester.widget<TextField>(find.byType(TextField).at(1)).decoration?.errorText, '请输入密码');
|
||||
expect(find.text('Invalid Argument'), findsNothing);
|
||||
expect(session.loginCallCount, 0);
|
||||
expect(tester.widget<TextField>(find.byType(TextField).first).focusNode?.hasFocus, isTrue);
|
||||
@@ -57,13 +119,16 @@ void main() {
|
||||
testWidgets('合法手机号配空密码时提示密码必填并阻止登录调用', (tester) async {
|
||||
final session = _RecordingUserSession();
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
|
||||
await tester.tap(find.text('密码登录'));
|
||||
await tester.pump();
|
||||
|
||||
await tester.enterText(find.byType(TextField).first, '13800138000');
|
||||
await tester.tap(find.text('安全登录'));
|
||||
await tester.ensureVisible(find.text('登录'));
|
||||
await tester.tap(find.text('登录'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('请输入正确的11位手机号'), findsNothing);
|
||||
expect(find.text('请输入密码'), findsOneWidget);
|
||||
expect(tester.widget<TextField>(find.byType(TextField).at(1)).decoration?.errorText, '请输入密码');
|
||||
expect(session.loginCallCount, 0);
|
||||
expect(tester.widget<TextField>(find.byType(TextField).at(1)).focusNode?.hasFocus, isTrue);
|
||||
});
|
||||
@@ -71,18 +136,21 @@ void main() {
|
||||
testWidgets('修改字段清除对应错误并由回车原样提交密码', (tester) async {
|
||||
final session = _RecordingUserSession();
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
|
||||
await tester.tap(find.text('密码登录'));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('安全登录'));
|
||||
await tester.ensureVisible(find.text('登录'));
|
||||
await tester.tap(find.text('登录'));
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(TextField).first, '13800138000');
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('请输入正确的11位手机号'), findsNothing);
|
||||
expect(find.text('请输入密码'), findsOneWidget);
|
||||
expect(tester.widget<TextField>(find.byType(TextField).at(1)).decoration?.errorText, '请输入密码');
|
||||
|
||||
await tester.enterText(find.byType(TextField).at(1), ' 密码123 ');
|
||||
await tester.pump();
|
||||
expect(find.text('请输入密码'), findsNothing);
|
||||
expect(tester.widget<TextField>(find.byType(TextField).at(1)).decoration?.errorText, isNull);
|
||||
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pump();
|
||||
@@ -95,7 +163,33 @@ void main() {
|
||||
test('接口错误优先按错误码中文化并屏蔽未知英文', () {
|
||||
expect(localizeApiErrorMessage(1704, 'Invalid Argument'), '请求参数错误');
|
||||
expect(localizeApiErrorMessage(1108, 'Password Incorrect'), '密码错误');
|
||||
expect(localizeApiErrorMessage(2410, 'service unavailable'), '短信验证码暂未开放');
|
||||
expect(localizeApiErrorMessage(9999, '业务处理失败'), '业务处理失败');
|
||||
expect(localizeApiErrorMessage(9999, 'Unexpected Error'), '操作失败,请稍后重试');
|
||||
});
|
||||
|
||||
testWidgets('验证码发送后修改手机号不得复用旧请求', (tester) async {
|
||||
final session = _RecordingUserSession();
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
|
||||
await tester.enterText(find.byType(TextField).first, '13800138000');
|
||||
await tester.tap(find.text('获取验证码'));
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(TextField).first, '13800138001');
|
||||
await tester.enterText(find.byType(TextField).at(1), '123456');
|
||||
await tester.ensureVisible(find.text('登录'));
|
||||
await tester.tap(find.text('登录'));
|
||||
await tester.pump();
|
||||
expect(find.text('请先获取当前手机号的验证码'), findsOneWidget);
|
||||
expect(session.loginCallCount, 0);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
});
|
||||
|
||||
testWidgets('短信未实际发送时明确提示暂未开放且不启动倒计时', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: _UnavailableCodeSession())));
|
||||
await tester.enterText(find.byType(TextField).first, '13800138000');
|
||||
await tester.tap(find.text('获取验证码'));
|
||||
await tester.pump();
|
||||
expect(find.text('短信验证码暂未开放'), findsOneWidget);
|
||||
expect(find.text('60s'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
31
apps/user_app/test/ui/message_center_page_test.dart
Normal file
31
apps/user_app/test/ui/message_center_page_test.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// 功能描述:验证消息分类、筛选和全部已读使用真实仓储动作;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/features/profile/message_center_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('消息中心展示分类并执行全部已读', (tester) async {
|
||||
final repository = A1FixtureRepository();
|
||||
await tester.pumpWidget(MaterialApp(home: MessageCenterPage(repository: repository)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('安全告警'), findsOneWidget);
|
||||
expect(find.text('厨房燃气浓度异常'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(TextButton, '全部已读'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.markedMessageKeys, containsAll(['safety-1', 'order-1']));
|
||||
});
|
||||
|
||||
testWidgets('未读筛选只展示未读消息', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: MessageCenterPage(repository: A1FixtureRepository())),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('未读'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('厨房燃气浓度异常'), findsOneWidget);
|
||||
expect(find.text('配送员已出发'), findsOneWidget);
|
||||
expect(find.text('秋季安全用气提醒'), findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -16,29 +16,73 @@ class _FakeRepository extends ClientRepository {
|
||||
int refundCalls = 0;
|
||||
int ticketCalls = 0;
|
||||
int createRefundCalls = 0;
|
||||
int cancellations = 0, gasCancellations = 0, receipts = 0;
|
||||
bool serveGas = false;
|
||||
bool completed = false;
|
||||
List<String> actions = ['refund'];
|
||||
|
||||
@override
|
||||
Future<List<ClientRecord>> shopOrders() async {
|
||||
shopCalls += 1;
|
||||
return const [
|
||||
return [
|
||||
ClientRecord(
|
||||
identity: 'shop-1',
|
||||
title: '商城订单一',
|
||||
subtitle: '',
|
||||
status: 18,
|
||||
status: completed ? 21 : 18,
|
||||
raw: {
|
||||
'order_no': 'EC-SEARCH-001',
|
||||
'status_name': completed ? '已完成' : '待履约',
|
||||
'allowed_actions': actions,
|
||||
'items': [
|
||||
{'identity': 'item-1', 'quantity': 1},
|
||||
{
|
||||
'identity': 'item-1',
|
||||
'quantity': 1,
|
||||
'product_snapshot': '{"name":"燃气报警器"}',
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelShopOrder(String identity) async {
|
||||
cancellations++;
|
||||
actions = [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> confirmShopReceipt(String identity) async {
|
||||
receipts++;
|
||||
actions = [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ClientRecord>> gasOrders() async {
|
||||
gasCalls += 1;
|
||||
return const [];
|
||||
if (!serveGas) return const [];
|
||||
return [
|
||||
ClientRecord(
|
||||
identity: 'gas-1',
|
||||
title: '气瓶订单一',
|
||||
subtitle: '',
|
||||
status: 16,
|
||||
raw: {
|
||||
'status_name': '待付款',
|
||||
'allowed_actions': actions,
|
||||
'items': [
|
||||
{'identity': 'gas-item-1', 'quantity': 1, 'product_type_name': '15kg液化气'},
|
||||
],
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelGasOrder(String identity) async {
|
||||
gasCancellations++;
|
||||
actions = [];
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -66,6 +110,46 @@ class _FakeRepository extends ClientRepository {
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('气瓶订单取消调用气瓶接口并刷新气瓶列表', (tester) async {
|
||||
final repository = _FakeRepository()
|
||||
..serveGas = true
|
||||
..actions = ['cancel'];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: OrdersPage(repository: repository, initialTab: 1)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('取消订单'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '确认取消'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.gasCancellations, 1);
|
||||
expect(repository.cancellations, 0);
|
||||
expect(repository.gasCalls, 2);
|
||||
});
|
||||
|
||||
for (final action in ['cancel', 'confirm_receipt']) {
|
||||
testWidgets('$action 必须确认,完成后刷新动作', (tester) async {
|
||||
final repository = _FakeRepository()..actions = [action];
|
||||
await tester.pumpWidget(MaterialApp(home: OrdersPage(repository: repository)));
|
||||
await tester.pumpAndSettle();
|
||||
final label = action == 'cancel' ? '取消订单' : '确认收货';
|
||||
await tester.tap(find.text(label));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('返回'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.cancellations + repository.receipts, 0);
|
||||
await tester.tap(find.text(label));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.widgetWithText(FilledButton, action == 'cancel' ? '确认取消' : '确认收货').last,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.cancellations + repository.receipts, 1);
|
||||
expect(repository.shopCalls, 2);
|
||||
expect(find.text(label), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
testWidgets('退款成功后刷新商城订单和退款记录', (tester) async {
|
||||
final repository = _FakeRepository();
|
||||
await tester.pumpWidget(
|
||||
@@ -73,25 +157,62 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('退款'));
|
||||
await tester.tap(find.text('退款/售后'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.refundCalls, 1);
|
||||
|
||||
await tester.tap(find.text('商城'));
|
||||
await tester.tap(find.byTooltip('关闭退款记录'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.shopCalls, 1);
|
||||
|
||||
await tester.tap(find.text('商城订单一'));
|
||||
await tester.tap(find.text('查看操作'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('申请退款'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), '商品问题');
|
||||
await tester.enterText(
|
||||
find.descendant(of: find.byType(AlertDialog), matching: find.byType(TextField)),
|
||||
'商品问题',
|
||||
);
|
||||
await tester.tap(find.text('提交'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repository.createRefundCalls, 1);
|
||||
expect(repository.shopCalls, 2);
|
||||
await tester.tap(find.text('退款/售后'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.refundCalls, 2);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('订单搜索按订单号和商品名称过滤,并可关闭清空', (tester) async {
|
||||
final repository = _FakeRepository()..actions = [];
|
||||
await tester.pumpWidget(MaterialApp(home: OrdersPage(repository: repository)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('搜索订单'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('搜索订单号或商品名称'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), '不存在');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('暂无订单'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), '燃气报警器');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('燃气报警器 × 1'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('关闭搜索'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('EC-SEARCH-001'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('已完成订单的二期功能显示即将开放说明', (tester) async {
|
||||
final repository = _FakeRepository()
|
||||
..actions = []
|
||||
..completed = true;
|
||||
await tester.pumpWidget(MaterialApp(home: OrdersPage(repository: repository)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('开发票'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('电子发票'), findsOneWidget);
|
||||
expect(find.text('该功能将在后续版本开放,敬请期待。'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
144
apps/user_app/test/ui/payment_confirmation_page_test.dart
Normal file
144
apps/user_app/test/ui/payment_confirmation_page_test.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
// 功能描述:验证图19真实订单金额、余额密码支付和未开放优惠提示;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/domain/models/shop_order_detail.dart';
|
||||
import 'package:user_app/domain/models/recharge.dart';
|
||||
import 'package:user_app/ui/features/orders/payment_confirmation_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _PaymentFixture extends A1FixtureRepository {
|
||||
String? channel, password;
|
||||
|
||||
@override
|
||||
Future<ShopOrderDetail> shopOrderDetail(String identity) async => ShopOrderDetail.fromJson({
|
||||
'identity': identity,
|
||||
'order_no': 'EC20260911001',
|
||||
'status_name': '待付款',
|
||||
'allowed_actions': ['pay', 'cancel'],
|
||||
'address': '测试地址',
|
||||
'contact_name': '测试用户',
|
||||
'contact_phone': '13800000001',
|
||||
'remark': '',
|
||||
'logistics_no': '',
|
||||
'logistics_company': '',
|
||||
'product_amount': 23800,
|
||||
'discount_amount': 1000,
|
||||
'payable_amount': 22800,
|
||||
'created_at': '2026-09-11T10:00:00+08:00',
|
||||
'paid_at': null,
|
||||
'shipped_at': null,
|
||||
'received_at': null,
|
||||
'items': [
|
||||
{
|
||||
'identity': 'item-1',
|
||||
'product_identity': 'product-1',
|
||||
'name': '15kg家用液化气',
|
||||
'quantity': 1,
|
||||
'sale_amount': 23800,
|
||||
'image_url': '',
|
||||
},
|
||||
],
|
||||
}, resolveImageUrl);
|
||||
|
||||
@override
|
||||
Future<Map<String, Object?>> payOrder({
|
||||
required String business,
|
||||
required String identity,
|
||||
required String requestNo,
|
||||
required String channel,
|
||||
required String payType,
|
||||
String openid = '',
|
||||
String paymentPassword = '',
|
||||
}) async {
|
||||
this.channel = channel;
|
||||
password = paymentPassword;
|
||||
return {'paid': true};
|
||||
}
|
||||
}
|
||||
|
||||
/// 模拟生产环境未配置外部商户,验证页面不会把不可用渠道伪装成可支付。
|
||||
class _UnavailablePaymentFixture extends _PaymentFixture {
|
||||
@override
|
||||
Future<RechargeOptions> rechargeOptions() async => const RechargeOptions(
|
||||
min: 1,
|
||||
max: 500000,
|
||||
channels: [
|
||||
RechargeChannel('wechat', app: false, wap: false),
|
||||
RechargeChannel('alipay', app: false, wap: false),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('支付确认页使用服务端金额并通过六位支付密码提交余额支付', (tester) async {
|
||||
final repository = _PaymentFixture();
|
||||
final router = GoRouter(
|
||||
initialLocation: '/',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (_, _) => PaymentConfirmationPage(
|
||||
repository: repository,
|
||||
business: 'shop',
|
||||
identity: 'order-1',
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/shop/orders/order-1',
|
||||
builder: (_, _) => const Scaffold(body: Text('订单详情')),
|
||||
),
|
||||
],
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('¥228.00'), findsWidgets);
|
||||
expect(find.text('余额 ¥568.00'), findsOneWidget);
|
||||
expect(find.text('暂未开放'), findsOneWidget);
|
||||
|
||||
await tester.ensureVisible(find.text('优惠券'));
|
||||
await tester.drag(find.byType(ListView), const Offset(0, -140));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('优惠券'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('优惠券暂未开放'), findsOneWidget);
|
||||
await tester.tap(find.text('知道了'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.ensureVisible(find.byKey(const Key('payment-password')));
|
||||
await tester.enterText(find.byKey(const Key('payment-password')), '123456');
|
||||
await tester.tap(find.byKey(const Key('confirm-payment')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('订单号:EC20260911001\n支付方式:余额支付\n支付金额:¥228.00'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(FilledButton, '确认支付'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repository.channel, 'wallet');
|
||||
expect(repository.password, '123456');
|
||||
expect(find.text('订单详情'), findsOneWidget);
|
||||
router.dispose();
|
||||
});
|
||||
|
||||
testWidgets('外部商户未配置时显示暂未开放且不会提交支付', (tester) async {
|
||||
final repository = _UnavailablePaymentFixture();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: PaymentConfirmationPage(
|
||||
repository: repository,
|
||||
business: 'shop',
|
||||
identity: 'order-1',
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('暂未开放'), findsNWidgets(3));
|
||||
final wechat = tester.widget<RadioListTile<String>>(
|
||||
find.widgetWithText(RadioListTile<String>, '微信支付'),
|
||||
);
|
||||
expect(wechat.enabled, isFalse);
|
||||
expect(repository.channel, isNull);
|
||||
});
|
||||
}
|
||||
122
apps/user_app/test/ui/payment_password_page_test.dart
Normal file
122
apps/user_app/test/ui/payment_password_page_test.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
// 功能描述:支付密码状态分支、六位校验、验证码用途和失败重试测试;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/settings/payment_password_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class PaymentPasswordFixture extends A1FixtureRepository {
|
||||
bool? hasPassword = false;
|
||||
bool failSave = false;
|
||||
int codes = 0, saves = 0;
|
||||
bool? resetting;
|
||||
String? oldPassword, requestId, codeValue;
|
||||
@override
|
||||
Future<WalletSummary> wallet() async =>
|
||||
WalletSummary(balance: 0, withdrawalBalance: 0, paymentPasswordSet: hasPassword);
|
||||
@override
|
||||
Future<PaymentCodeRequest> requestPaymentPasswordCode({required bool resetting}) async {
|
||||
codes++;
|
||||
this.resetting = resetting;
|
||||
return const PaymentCodeRequest(
|
||||
identity: 'test-request',
|
||||
delivered: false,
|
||||
maskedPhone: '138****0001',
|
||||
retryAfter: 60,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setPaymentPassword({
|
||||
required String newPassword,
|
||||
String? currentPassword,
|
||||
String? requestIdentity,
|
||||
String? code,
|
||||
}) async {
|
||||
saves++;
|
||||
oldPassword = currentPassword;
|
||||
requestId = requestIdentity;
|
||||
codeValue = code;
|
||||
if (failSave) throw const ApiException(1306, '密码错误');
|
||||
hasPassword = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openPayment(WidgetTester tester, PaymentPasswordFixture repo) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: PaymentPasswordPage(repository: repo),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> fillNext(WidgetTester tester) async {
|
||||
await tester.enterText(find.byKey(const ValueKey('payment-next')), '782913');
|
||||
await tester.enterText(find.byKey(const ValueKey('payment-repeat')), '782913');
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('未知支付密码状态不能误当首次设置', (tester) async {
|
||||
final repo = PaymentPasswordFixture()..hasPassword = null;
|
||||
await openPayment(tester, repo);
|
||||
expect(find.text('支付密码状态读取失败'), findsOneWidget);
|
||||
expect(find.text('获取验证码'), findsNothing);
|
||||
});
|
||||
testWidgets('首次设置明确Mock未发送,验证码请求与确认成功后切换状态', (tester) async {
|
||||
final repo = PaymentPasswordFixture();
|
||||
await openPayment(tester, repo);
|
||||
expect(find.text('设置支付密码'), findsOneWidget);
|
||||
await tester.tap(find.text('获取验证码'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.resetting, false);
|
||||
expect(find.text('验证码请求已建立,但当前环境未发送短信'), findsOneWidget);
|
||||
expect(find.textContaining('已发送至'), findsNothing);
|
||||
await tester.enterText(find.byKey(const ValueKey('payment-code')), '314159');
|
||||
await fillNext(tester);
|
||||
await tester.tap(find.text('确认'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.saves, 1);
|
||||
expect(repo.oldPassword, isNull);
|
||||
expect(repo.requestId, 'test-request');
|
||||
expect(repo.codeValue, '314159');
|
||||
expect(find.text('支付密码已更新'), findsOneWidget);
|
||||
expect(find.text('修改支付密码'), findsOneWidget);
|
||||
});
|
||||
testWidgets('旧密码修改失败保留表单,找回使用独立用途且不传旧密码', (tester) async {
|
||||
final repo = PaymentPasswordFixture()
|
||||
..hasPassword = true
|
||||
..failSave = true;
|
||||
await openPayment(tester, repo);
|
||||
await tester.enterText(find.byKey(const ValueKey('payment-old')), '123');
|
||||
await fillNext(tester);
|
||||
await tester.tap(find.text('确认'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.saves, 0);
|
||||
await tester.enterText(find.byKey(const ValueKey('payment-old')), '123456');
|
||||
await tester.tap(find.text('确认'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.saves, 1);
|
||||
expect(find.text('密码错误'), findsOneWidget);
|
||||
expect(
|
||||
tester.widget<TextFormField>(find.byKey(const ValueKey('payment-next'))).controller!.text,
|
||||
'782913',
|
||||
);
|
||||
await tester.tap(find.text('忘记支付密码?'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('获取验证码'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.resetting, true);
|
||||
await tester.enterText(find.byKey(const ValueKey('payment-code')), '314159');
|
||||
repo.failSave = false;
|
||||
await tester.tap(find.text('确认'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.saves, 2);
|
||||
expect(repo.oldPassword, isNull);
|
||||
expect(repo.requestId, 'test-request');
|
||||
});
|
||||
}
|
||||
147
apps/user_app/test/ui/primary_pages_test.dart
Normal file
147
apps/user_app/test/ui/primary_pages_test.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
// 功能描述:验证一级页面保留入口、游客数据边界、筛选及刷新错误恢复。
|
||||
// 版本:1.0.0
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
import 'package:user_app/domain/models/order_summary.dart';
|
||||
import 'package:user_app/ui/features/home/home_page.dart';
|
||||
import 'package:user_app/ui/features/shop/shop_page.dart';
|
||||
import 'package:user_app/ui/core/async_content.dart';
|
||||
import 'package:user_app/ui/core/text_entry_dialog.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('刷新失败保留上次数据,较迟的旧请求不覆盖新数据', (tester) async {
|
||||
final key = GlobalKey<AsyncContentState<String>>();
|
||||
final pending = <Completer<String>>[];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AsyncContent<String>(
|
||||
key: key,
|
||||
load: () {
|
||||
final c = Completer<String>();
|
||||
pending.add(c);
|
||||
return c.future;
|
||||
},
|
||||
builder: (_, value) => ListView(children: [Text(value)]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
pending[0].complete('初始数据');
|
||||
await tester.pumpAndSettle();
|
||||
final first = key.currentState!.refresh();
|
||||
final second = key.currentState!.refresh();
|
||||
pending[2].complete('最新数据');
|
||||
await second;
|
||||
await tester.pump();
|
||||
pending[1].complete('旧请求结果');
|
||||
await first;
|
||||
await tester.pump();
|
||||
expect(find.text('最新数据'), findsOneWidget);
|
||||
expect(find.text('旧请求结果'), findsNothing);
|
||||
final failure = key.currentState!.refresh();
|
||||
pending[3].completeError(Exception('offline'));
|
||||
await failure;
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('最新数据'), findsOneWidget);
|
||||
expect(find.textContaining('刷新失败,当前显示上次数据'), findsOneWidget);
|
||||
});
|
||||
testWidgets('输入弹窗关闭动画期间控制器有效,空值阻止提交', (tester) async {
|
||||
String? result;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () async {
|
||||
result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => const TextEntryDialog(title: '新增地址', label: '详细地址', action: '保存'),
|
||||
);
|
||||
},
|
||||
child: const Text('打开'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('打开'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('保存'));
|
||||
await tester.pump();
|
||||
expect(find.text('请填写详细地址'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), '测试地址');
|
||||
await tester.tap(find.text('保存'));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
expect(result, '测试地址');
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
testWidgets('游客首页不读取个人归属并保留设备入口', (tester) async {
|
||||
final source = A1FixtureRepository();
|
||||
await tester.pumpWidget(MaterialApp(home: HomePage(repository: source, authenticated: false)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(source.relationCalls, 0);
|
||||
await tester.ensureVisible(find.text('扫码添加'));
|
||||
await tester.tap(find.text('扫码添加'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('该功能暂未开放'), findsOneWidget);
|
||||
expect(find.text('85%'), findsNothing);
|
||||
});
|
||||
testWidgets('首页真实入口可进入设备页且未接入入口明确提示', (tester) async {
|
||||
final source = A1FixtureRepository();
|
||||
final router = GoRouter(
|
||||
initialLocation: '/home',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
builder: (_, _) => HomePage(repository: source),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/devices',
|
||||
builder: (_, _) => const Scaffold(body: Text('设备页已打开')),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(router.dispose);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('查看设备状态与告警记录'), findsOneWidget);
|
||||
expect(find.text('查看气瓶使用与检测信息'), findsOneWidget);
|
||||
await tester.tap(find.text('查看详情'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('设备页已打开'), findsOneWidget);
|
||||
});
|
||||
testWidgets('首次加载失败可重试恢复真实内容', (tester) async {
|
||||
final source = A1FixtureRepository()..fail = true;
|
||||
await tester.pumpWidget(MaterialApp(home: HomePage(repository: source)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('加载失败'), findsOneWidget);
|
||||
source.fail = false;
|
||||
await tester.tap(find.text('重新加载'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('当前服务归属'), findsOneWidget);
|
||||
});
|
||||
testWidgets('商城搜索与分类联动并显示空结果', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(home: ShopPage(repository: A1FixtureRepository())));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), '报警');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('燃气报警器'), findsOneWidget);
|
||||
expect(find.text('15kg家用液化气'), findsNothing);
|
||||
await tester.enterText(find.byType(TextField), '不存在');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('暂无符合条件'), findsOneWidget);
|
||||
});
|
||||
test('旧响应缺少 allowed_actions 时不从状态码推测权限', () {
|
||||
final order = OrderSummary.fromRecord(
|
||||
const ClientRecord(identity: 'o', title: '', subtitle: '', status: 16, raw: {}),
|
||||
);
|
||||
expect(order.actions, isEmpty);
|
||||
expect(order.amount, isNull);
|
||||
});
|
||||
}
|
||||
109
apps/user_app/test/ui/product_detail_page_test.dart
Normal file
109
apps/user_app/test/ui/product_detail_page_test.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
// 功能描述:商品详情的公开浏览、售罄与错误边界、数量向结算传递回归。
|
||||
// 版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/product_detail.dart';
|
||||
import 'package:user_app/ui/features/shop/product_detail_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Repository extends A1FixtureRepository {
|
||||
int stock = 2;
|
||||
bool unavailable = false;
|
||||
@override
|
||||
Future<ProductDetail> productDetail(String identity) async {
|
||||
if (unavailable) throw const ApiException(1112, '商品已下架');
|
||||
return ProductDetail(
|
||||
identity: identity,
|
||||
name: '测试商品',
|
||||
price: 1999,
|
||||
stock: stock,
|
||||
attributes: const [(name: '材质', value: '不锈钢')],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('查看商品与参数,数量限制库存,购买只进入结算', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final repo = _Repository();
|
||||
final router = GoRouter(
|
||||
initialLocation: '/products/p1',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/products/:identity',
|
||||
builder: (_, state) =>
|
||||
ProductDetailPage(repository: repo, identity: state.pathParameters['identity']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/checkout/:identity',
|
||||
builder: (_, state) => Scaffold(
|
||||
body: Text(
|
||||
'结算 ${state.pathParameters['identity']} 数量${state.uri.queryParameters['quantity']}',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('测试商品'), findsOneWidget);
|
||||
expect(find.text('¥19.99'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('增加数量'));
|
||||
await tester.pump();
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(
|
||||
find.byWidgetPredicate((widget) => widget is IconButton && widget.tooltip == '增加数量'),
|
||||
)
|
||||
.onPressed,
|
||||
isNull,
|
||||
);
|
||||
await tester.scrollUntilVisible(
|
||||
find.textContaining('不锈钢'),
|
||||
180,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.textContaining('不锈钢'), findsOneWidget);
|
||||
await tester.tap(find.text('立即购买'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('结算 p1 数量2'), findsOneWidget);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
testWidgets('售罄可查看,禁止购买;下架响应可重试恢复', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final repo = _Repository()..stock = 0;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ProductDetailPage(repository: repo, identity: 'p1'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<FilledButton>(find.widgetWithText(FilledButton, '暂时售罄')).onPressed,
|
||||
isNull,
|
||||
);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
repo.unavailable = true;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ProductDetailPage(repository: repo, identity: 'missing'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('商品已下架'), findsOneWidget);
|
||||
expect(find.text('立即购买'), findsNothing);
|
||||
repo.unavailable = false;
|
||||
await tester.tap(find.text('重新加载'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('测试商品'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
195
apps/user_app/test/ui/product_recommendations_test.dart
Normal file
195
apps/user_app/test/ui/product_recommendations_test.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
// 功能描述:推荐分页、加购不重复、购物车刷新及收藏入口回归;版本:1.0.0。
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/cart_item.dart';
|
||||
import 'package:user_app/domain/models/product_favorite.dart';
|
||||
import 'package:user_app/domain/models/primary_models.dart';
|
||||
import 'package:user_app/domain/models/product_recommendations.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/shop/cart_page.dart';
|
||||
import 'package:user_app/ui/features/shop/favorites_page.dart';
|
||||
import 'package:user_app/ui/features/shop/product_recommendations.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
/// 模拟请求已写入却丢失响应,确认重试仍发送同一目标。
|
||||
class _Repo extends A1FixtureRepository {
|
||||
final requests = <(String, int)>[];
|
||||
final targets = <int>[];
|
||||
List<CartItem> items = [];
|
||||
bool failLoad = false, failWrite = false;
|
||||
int stock = 5;
|
||||
Completer<void>? gate;
|
||||
ProductSummary get product =>
|
||||
ProductSummary(identity: 'r', name: '真实推荐', price: 3900, stock: stock);
|
||||
@override
|
||||
Future<List<ProductFavorite>> favorites() async => [];
|
||||
@override
|
||||
Future<ProductRecommendations> recommendations({required String source, int page = 1}) async {
|
||||
requests.add((source, page));
|
||||
if (failLoad) {
|
||||
failLoad = false;
|
||||
throw const ApiException(500, '暂时无法加载');
|
||||
}
|
||||
return ProductRecommendations(
|
||||
items: [
|
||||
page == 1
|
||||
? product
|
||||
: const ProductSummary(identity: 'other', name: '下一批商品', price: 500, stock: 3),
|
||||
],
|
||||
page: page,
|
||||
hasMore: page == 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<CartItem>> cart() async => List.of(items);
|
||||
@override
|
||||
Future<CartItem> cartItem(String identity) async => items.isNotEmpty
|
||||
? items.first
|
||||
: CartItem(
|
||||
product: product,
|
||||
quantity: 0,
|
||||
selected: false,
|
||||
available: true,
|
||||
revision: 'original',
|
||||
);
|
||||
@override
|
||||
Future<CartItem> setCartItem(
|
||||
CartItem item, {
|
||||
required int quantity,
|
||||
required bool selected,
|
||||
}) async {
|
||||
targets.add(quantity);
|
||||
await gate?.future;
|
||||
final updated = CartItem(
|
||||
product: product,
|
||||
quantity: quantity,
|
||||
selected: selected,
|
||||
available: true,
|
||||
revision: 'new',
|
||||
);
|
||||
items = [updated];
|
||||
if (failWrite) {
|
||||
failWrite = false;
|
||||
throw const ApiException(500, '加入结果待确认');
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
|
||||
Finder _button(String label) =>
|
||||
find.byWidgetPredicate((w) => w is IconButton && w.tooltip == label);
|
||||
Future<void> _show(WidgetTester tester, Widget child) async {
|
||||
await tester.pumpWidget(MaterialApp(theme: AppTheme.light(), home: child));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('从推荐进入详情修改购物车,返回后重读合计', (tester) async {
|
||||
final repo = _Repo();
|
||||
final router = GoRouter(
|
||||
initialLocation: '/cart',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/cart',
|
||||
builder: (_, _) => CartPage(repository: repo),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/products/:id',
|
||||
builder: (context, _) => Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () async {
|
||||
await repo.setCartItem(await repo.cartItem('r'), quantity: 1, selected: true);
|
||||
if (context.mounted) context.pop();
|
||||
},
|
||||
child: const Text('详情加购后返回'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(router.dispose);
|
||||
await tester.pumpWidget(MaterialApp.router(theme: AppTheme.light(), routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('真实推荐'));
|
||||
await tester.tap(find.text('真实推荐'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('详情加购后返回'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('合计:¥39.00'), findsOneWidget);
|
||||
expect(find.text('去结算(1)'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
testWidgets('推荐分页可轮换,失败可重读,加购未知结果复用绝对数量', (tester) async {
|
||||
final repo = _Repo()
|
||||
..failLoad = true
|
||||
..failWrite = true;
|
||||
var changes = 0;
|
||||
await _show(
|
||||
tester,
|
||||
Scaffold(
|
||||
body: ProductRecommendationSection(
|
||||
repository: repo,
|
||||
source: 'cart',
|
||||
onCartChanged: () async {
|
||||
changes++;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('暂时无法加载'), findsOneWidget);
|
||||
await tester.tap(find.text('重新加载'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('换一换'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('下一批商品'), findsOneWidget);
|
||||
await tester.tap(find.text('换一换'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.requests.map((r) => r.$2), [1, 1, 2, 1]);
|
||||
await tester.tap(_button('加入购物车 真实推荐'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(changes, 0);
|
||||
expect(find.text('加入结果待确认'), findsOneWidget);
|
||||
await tester.tap(_button('重试加入 真实推荐'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.targets, [1, 1]);
|
||||
expect(changes, 1);
|
||||
expect(find.text('已加入购物车'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
testWidgets('加购期间阻止结算,确认后购物车数量与合计刷新', (tester) async {
|
||||
final repo = _Repo()..gate = Completer<void>();
|
||||
await _show(tester, CartPage(repository: repo));
|
||||
await tester.ensureVisible(_button('加入购物车 真实推荐'));
|
||||
await tester.tap(_button('加入购物车 真实推荐'));
|
||||
await tester.pump();
|
||||
expect(tester.widget<FilledButton>(find.widgetWithText(FilledButton, '处理中')).onPressed, isNull);
|
||||
repo.gate!.complete();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('合计:¥39.00'), findsOneWidget);
|
||||
expect(find.text('去结算(1)'), findsOneWidget);
|
||||
expect(repo.items.single.quantity, 1);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
testWidgets('库存变为零不写购物车,收藏入口只在打开后读取推荐', (tester) async {
|
||||
final repo = _Repo();
|
||||
await _show(tester, FavoritesPage(repository: repo));
|
||||
expect(repo.requests, isEmpty);
|
||||
await tester.tap(find.text('猜你喜欢'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.requests, [('favorites', 1)]);
|
||||
expect(repo.targets, isEmpty);
|
||||
repo.stock = 0;
|
||||
await tester.tap(_button('加入购物车 真实推荐'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('商品库存不足'), findsOneWidget);
|
||||
expect(repo.targets, isEmpty);
|
||||
await tester.tap(_button('关闭推荐'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ProductRecommendationSection), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
152
apps/user_app/test/ui/profile_edit_page_test.dart
Normal file
152
apps/user_app/test/ui/profile_edit_page_test.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
// 功能描述:验证个人资料的真实保存、头像草稿、取消及失败重试边界。
|
||||
// 版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/profile/profile_edit_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
/// 记录写入调用,用于确认失败不会误报成功或重复上传。
|
||||
class ProfileFixture extends A1FixtureRepository {
|
||||
int uploads = 0, saves = 0;
|
||||
String? savedName, savedAvatar;
|
||||
bool failSave = false;
|
||||
bool hasAvatar = false;
|
||||
@override
|
||||
Future<UserProfile> profile() async => UserProfile(
|
||||
identity: 'user-fixture',
|
||||
name: '张先生',
|
||||
phone: '13800005678',
|
||||
avatar: hasAvatar ? '/uploads/avatars/owned.png' : '',
|
||||
realName: '张实名',
|
||||
);
|
||||
@override
|
||||
Future<Uint8List?> profileAvatar() async => _png;
|
||||
@override
|
||||
Future<List<ClientRecord>> addresses() async => [];
|
||||
@override
|
||||
Future<String> uploadAvatar(Uint8List bytes, String filename) async {
|
||||
uploads++;
|
||||
return '/uploads/avatars/owned.png';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateProfile(String name, {String? avatar}) async {
|
||||
saves++;
|
||||
if (failSave) throw const ApiException(500, '保存失败,请重试');
|
||||
savedName = name;
|
||||
savedAvatar = avatar;
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试使用真实有效的 1 像素 PNG,仅用于验证文件选择与请求,不作为产品素材。
|
||||
final _png = base64Decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||
);
|
||||
|
||||
Future<void> openEditor(WidgetTester tester, ProfileFixture repo, AvatarPicker picker) async {
|
||||
final router = GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () async {
|
||||
final result = await context.push<bool>('/edit');
|
||||
if (context.mounted && result == true) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存已确认')));
|
||||
}
|
||||
},
|
||||
child: const Text('编辑'),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/edit',
|
||||
builder: (context, state) => ProfileEditPage(repository: repo, pickImage: picker),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(router.dispose);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router, theme: AppTheme.light()));
|
||||
await tester.tap(find.text('编辑'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectPhoto(WidgetTester tester) async {
|
||||
await tester.tap(find.text('更换头像'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('从相册选择'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('服务端清空头像后下拉刷新不残留旧图片', (tester) async {
|
||||
final repo = ProfileFixture()..hasAvatar = true;
|
||||
await openEditor(tester, repo, (_) async => null);
|
||||
expect(find.text('已认证 张实名'), findsOneWidget);
|
||||
expect(find.byKey(const Key('profile-avatar-image')), findsOneWidget);
|
||||
repo.hasAvatar = false;
|
||||
await tester.drag(find.byType(ListView).first, const Offset(0, 350));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const Key('profile-avatar-image')), findsNothing);
|
||||
expect(find.byKey(const Key('profile-avatar-fallback')), findsOneWidget);
|
||||
});
|
||||
testWidgets('选图保存失败保留草稿,重试不重复上传,成功才返回', (tester) async {
|
||||
final repo = ProfileFixture()..failSave = true;
|
||||
await openEditor(tester, repo, (_) async => XFile.fromData(_png, name: 'test.png'));
|
||||
await selectPhoto(tester);
|
||||
await tester.enterText(find.byKey(const Key('profile-name-input')), '新昵称');
|
||||
await tester.drag(find.byType(ListView).first, const Offset(0, 350));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<TextFormField>(find.byKey(const Key('profile-name-input'))).controller!.text,
|
||||
'新昵称',
|
||||
);
|
||||
await tester.tap(find.text('保存'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.uploads, 1);
|
||||
expect(repo.saves, 1);
|
||||
expect(find.text('个人资料'), findsOneWidget);
|
||||
expect(find.byKey(const Key('profile-save-error')), findsOneWidget);
|
||||
repo.failSave = false;
|
||||
await tester.tap(find.text('保存'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.uploads, 1);
|
||||
expect(repo.saves, 2);
|
||||
expect(repo.savedName, '新昵称');
|
||||
expect(repo.savedAvatar, '/uploads/avatars/owned.png');
|
||||
expect(find.text('保存已确认'), findsOneWidget);
|
||||
});
|
||||
testWidgets('取消选图后昵称保存不覆盖原头像', (tester) async {
|
||||
final repo = ProfileFixture();
|
||||
await openEditor(tester, repo, (_) async => null);
|
||||
await selectPhoto(tester);
|
||||
await tester.tap(find.text('保存'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.uploads, 0);
|
||||
expect(repo.savedAvatar, isNull);
|
||||
expect(repo.saves, 1);
|
||||
});
|
||||
testWidgets('空昵称不能提交;离开有草稿需要确认', (tester) async {
|
||||
final repo = ProfileFixture();
|
||||
await openEditor(tester, repo, (_) async => null);
|
||||
await tester.enterText(find.byKey(const Key('profile-name-input')), ' ');
|
||||
await tester.tap(find.text('保存'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.saves, 0);
|
||||
expect(find.text('昵称不能为空'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('返回'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('放弃未保存的修改?'), findsOneWidget);
|
||||
await tester.tap(find.text('继续编辑'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('个人资料'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
133
apps/user_app/test/ui/recharge_page_test.dart
Normal file
133
apps/user_app/test/ui/recharge_page_test.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
// 功能描述:充值页面缺少协议保护和待确认请求恢复;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/data/services/recharge_draft_store.dart';
|
||||
import 'package:user_app/domain/models/recharge.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
import 'package:user_app/ui/features/wallet/recharge_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Store implements RechargeDraftStore {
|
||||
PendingRecharge? draft;
|
||||
@override
|
||||
Future<PendingRecharge?> read(String owner) async => draft;
|
||||
@override
|
||||
Future<void> write(String owner, PendingRecharge value) async {
|
||||
draft = value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String owner) async {
|
||||
draft = null;
|
||||
}
|
||||
}
|
||||
|
||||
class _Repo extends A1FixtureRepository {
|
||||
bool failBalanceRefresh = false;
|
||||
int balanceReads = 0;
|
||||
@override
|
||||
Future<WalletSummary> wallet() async {
|
||||
if (++balanceReads > 1 && failBalanceRefresh) throw StateError('余额断网');
|
||||
return super.wallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> rechargeDraftOwner() async => 'api#alice';
|
||||
@override
|
||||
Future<RechargeOptions> rechargeOptions() async => const RechargeOptions(
|
||||
min: 1,
|
||||
max: 500000,
|
||||
channels: [RechargeChannel('alipay', app: true, wap: true)],
|
||||
);
|
||||
@override
|
||||
Future<RechargeRecord> rechargeResult(String request) async => RechargeRecord(
|
||||
identity: 'one',
|
||||
number: request,
|
||||
amount: 10000,
|
||||
status: 23,
|
||||
channel: 'alipay',
|
||||
createdAt: DateTime(2026),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
for (final width in [320.0, 390.0]) {
|
||||
testWidgets('充值页窄屏与放大字体无溢出 $width', (tester) async {
|
||||
tester.view.physicalSize = Size(width, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.3)),
|
||||
child: child!,
|
||||
),
|
||||
home: RechargePage(repository: _Repo(), store: _Store()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(FilledButton),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
testWidgets('缺少协议不能创建充值,但可恢复并查询已有订单', (tester) async {
|
||||
final store = _Store()
|
||||
..draft = const PendingRecharge(
|
||||
request: 'request-1',
|
||||
amount: 10000,
|
||||
channel: 'alipay',
|
||||
payType: 'app',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: RechargePage(repository: _Repo()..failBalanceRefresh = true, store: store),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<TextField>(find.byType(TextField)).enabled, isFalse);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('查询到账结果'),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<FilledButton>(find.byType(FilledButton)).onPressed, isNull);
|
||||
await tester.tap(find.text('查询到账结果'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(store.draft, isNull);
|
||||
expect(find.text('查询到账结果'), findsNothing);
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(TextField),
|
||||
-250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<TextField>(find.byType(TextField)).enabled, isTrue);
|
||||
await tester.tap(find.byTooltip('清空金额'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<TextField>(find.byType(TextField)).controller!.text, isEmpty);
|
||||
await tester.enterText(find.byType(TextField), '200.29');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('确认充值 ¥200.29'),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('确认充值 ¥200.29'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('已到账,余额刷新失败,请重新进入页面刷新'),
|
||||
-250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('暂未确认充值结果,请稍后查询'), findsNothing);
|
||||
});
|
||||
}
|
||||
51
apps/user_app/test/ui/recharge_records_page_test.dart
Normal file
51
apps/user_app/test/ui/recharge_records_page_test.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
// 功能描述:充值记录分页去重和刷新失败恢复;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/domain/models/recharge.dart';
|
||||
import 'package:user_app/ui/features/wallet/recharge_records_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Repository extends A1FixtureRepository {
|
||||
bool broken = false;
|
||||
final cursors = <String>[];
|
||||
@override
|
||||
Future<RechargeRecordPage> rechargeRecords({String cursor = ''}) async {
|
||||
cursors.add(cursor);
|
||||
if (broken) throw Exception('断网');
|
||||
RechargeRecord item(String id) => RechargeRecord(
|
||||
identity: id,
|
||||
number: id,
|
||||
amount: 10000,
|
||||
status: 10,
|
||||
channel: 'wechat',
|
||||
createdAt: DateTime(2026, 9, 11),
|
||||
);
|
||||
return RechargeRecordPage(
|
||||
cursor.isEmpty ? [item('one')] : [item('one'), item('two')],
|
||||
cursor.isEmpty ? '7' : '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('续页去重,刷新失败保留记录并可重试', (tester) async {
|
||||
final repo = _Repository();
|
||||
await tester.pumpWidget(MaterialApp(home: RechargeRecordsPage(repository: repo)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('待确认到账'), findsOneWidget);
|
||||
await tester.tap(find.text('加载更多'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.cursors, ['', '7']);
|
||||
expect(find.text('充值单号:one'), findsOneWidget);
|
||||
expect(find.text('充值单号:two'), findsOneWidget);
|
||||
repo.broken = true;
|
||||
await tester.tap(find.byTooltip('刷新充值状态'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('充值单号:two'), findsOneWidget);
|
||||
repo.broken = false;
|
||||
await tester.tap(find.text('充值记录加载失败,请重试'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('充值单号:two'), findsNothing);
|
||||
expect(repo.cursors.last, '');
|
||||
});
|
||||
}
|
||||
214
apps/user_app/test/ui/repair_draft_test.dart
Normal file
214
apps/user_app/test/ui/repair_draft_test.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
// 功能描述:草稿重开恢复、跨账户隔离、提交前持久化与存储失败不发单。
|
||||
// 版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/data/services/repair_draft_store.dart';
|
||||
import 'package:user_app/ui/features/tickets/repair_page.dart';
|
||||
|
||||
class _Drafts implements RepairDraftStore {
|
||||
final records = <String, Map<String, Object?>>{};
|
||||
bool fail = false;
|
||||
@override
|
||||
Future<Map<String, Object?>?> read(String owner) async => records[owner];
|
||||
@override
|
||||
Future<void> write(String owner, Map<String, Object?> draft) async {
|
||||
if (fail) throw Exception('storage');
|
||||
records[owner] = draft;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String owner) async {
|
||||
records.remove(owner);
|
||||
}
|
||||
}
|
||||
|
||||
class _Repository extends ClientRepository {
|
||||
_Repository(this.store) : super(ApiClient(() => '', baseUrl: 'https://api.example.com'));
|
||||
final _Drafts store;
|
||||
String owner = 'api#alice';
|
||||
int submissions = 0;
|
||||
@override
|
||||
Future<String> repairDraftOwner() async => owner;
|
||||
@override
|
||||
Future<Uint8List?> uploadedTicketPhoto(String uri) async => base64Decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aX1cAAAAASUVORK5CYII=',
|
||||
);
|
||||
@override
|
||||
Future<String> submitRepair({
|
||||
required String requestNo,
|
||||
required String description,
|
||||
required String faultType,
|
||||
required String addressIdentity,
|
||||
DateTime? appointment,
|
||||
List<Map<String, Object?>> photos = const [],
|
||||
}) async {
|
||||
expect(requestNo, 'persisted-request');
|
||||
expect(store.records[owner]!['attempted'], true);
|
||||
expect(store.records[owner]!['request_no'], requestNo);
|
||||
expect(photos.length, 1);
|
||||
submissions++;
|
||||
if (submissions == 1) throw const ApiException(500, '结果待确认');
|
||||
return 'ticket-1';
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object?> draft({bool pending = true}) => {
|
||||
'schema': 1,
|
||||
'request_no': 'persisted-request',
|
||||
'description': '已保存故障',
|
||||
'fault_type': 'valve',
|
||||
'step': pending ? 2 : 0,
|
||||
'attempted': pending,
|
||||
'address': pending
|
||||
? {
|
||||
'identity': 'address-1',
|
||||
'address': '草稿地址',
|
||||
'contact_name': '联系人',
|
||||
'contact_phone': '13800000001',
|
||||
}
|
||||
: null,
|
||||
'photos': pending
|
||||
? [
|
||||
{
|
||||
'uri': '/uploads/ticket-photos/owned/photo.png',
|
||||
'source': 'gallery',
|
||||
'added_at': '2026-09-07T01:00:00Z',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
GoRouter routerFor(ClientRepository repository, RepairDraftStore store) => GoRouter(
|
||||
initialLocation: '/repair',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/repair',
|
||||
builder: (_, s) => RepairPage(repository: repository, draftStore: store),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/me',
|
||||
builder: (_, s) => const Scaffold(body: Text('已暂存退出')),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/tickets/:identity',
|
||||
builder: (_, s) => const Scaffold(body: Text('已提交')),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/orders',
|
||||
builder: (_, s) => const Scaffold(body: Text('工单列表')),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
void main() {
|
||||
testWidgets('损坏草稿不会自动丢弃,明确清除后才可重新填写', (tester) async {
|
||||
final store = _Drafts()..records['api#alice'] = {'schema': 1};
|
||||
final repository = _Repository(store);
|
||||
final router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('恢复草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(store.records['api#alice'], isNotNull);
|
||||
await tester.tap(find.text('清除无法读取的草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(store.records['api#alice'], isNotNull);
|
||||
await tester.tap(find.text('清除草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(store.records['api#alice'], isNull);
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
expect(repository.submissions, 0);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
testWidgets('结果未知重开后恢复原请求,成功清理草稿', (tester) async {
|
||||
final store = _Drafts()..records['api#alice'] = draft();
|
||||
final repository = _Repository(store);
|
||||
var router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.submissions, 0);
|
||||
await tester.tap(find.text('恢复草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('重试提交'));
|
||||
await tester.tap(find.text('重试提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.submissions, 1);
|
||||
expect(store.records['api#alice']!['request_no'], 'persisted-request');
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('恢复草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('已保存故障'), findsOneWidget);
|
||||
expect(repository.submissions, 1);
|
||||
await tester.ensureVisible(find.text('重试提交'));
|
||||
await tester.tap(find.text('重试提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.submissions, 2);
|
||||
expect(store.records['api#alice'], isNull);
|
||||
expect(find.text('已提交'), findsOneWidget);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
testWidgets('保存失败不发送工单且保留输入', (tester) async {
|
||||
final store = _Drafts()
|
||||
..records['api#alice'] = draft()
|
||||
..fail = true;
|
||||
final repository = _Repository(store);
|
||||
final router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('恢复草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('重试提交'));
|
||||
await tester.tap(find.text('重试提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.submissions, 0);
|
||||
expect(find.textContaining('提交前保存失败'), findsOneWidget);
|
||||
expect(find.text('已保存故障'), findsOneWidget);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
testWidgets('其他账户不会出现已有草稿', (tester) async {
|
||||
final store = _Drafts()..records['api#alice'] = draft();
|
||||
final repository = _Repository(store)..owner = 'api#bob';
|
||||
final router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('发现报修草稿'), findsNothing);
|
||||
expect(find.text('已保存故障'), findsNothing);
|
||||
expect(store.records['api#alice'], isNotNull);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
testWidgets('暂存退出保存编辑和同一个请求号', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final store = _Drafts()..records['api#alice'] = draft(pending: false);
|
||||
final repository = _Repository(store);
|
||||
final router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('恢复草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), '修改后暂存');
|
||||
await tester.ensureVisible(find.text('暂存并退出'));
|
||||
await tester.tap(find.text('暂存并退出'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('已暂存退出'), findsOneWidget);
|
||||
expect(store.records['api#alice']!['description'], '修改后暂存');
|
||||
expect(store.records['api#alice']!['request_no'], 'persisted-request');
|
||||
expect(repository.submissions, 0);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
}
|
||||
155
apps/user_app/test/ui/repair_page_test.dart
Normal file
155
apps/user_app/test/ui/repair_page_test.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
// 功能描述:验证报修分步校验、地址返回、显式提交与未知结果重试。
|
||||
// 版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/shipping_address.dart';
|
||||
import 'package:user_app/ui/features/tickets/repair_page.dart';
|
||||
|
||||
class _RepairRepository extends ClientRepository {
|
||||
_RepairRepository() : super(ApiClient(() => '', baseUrl: 'https://api.example.com'));
|
||||
final requests = <String>[];
|
||||
int uploads = 0;
|
||||
@override
|
||||
Future<String> uploadTicketPhoto(Uint8List bytes, String filename) async {
|
||||
uploads++;
|
||||
return '/uploads/ticket-photos/owned/test.png';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> submitRepair({
|
||||
required String requestNo,
|
||||
required String description,
|
||||
required String faultType,
|
||||
required String addressIdentity,
|
||||
DateTime? appointment,
|
||||
List<Map<String, Object?>> photos = const [],
|
||||
}) async {
|
||||
expect(description, '阀门异响');
|
||||
expect(addressIdentity, 'address-1');
|
||||
expect(faultType, 'valve');
|
||||
requests.add(requestNo);
|
||||
expect(photos.length, 1);
|
||||
expect(photos.first['uri'], '/uploads/ticket-photos/owned/test.png');
|
||||
if (requests.length == 1) throw const ApiException(500, '结果待确认');
|
||||
return 'ticket-1';
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('紧急电话只在用户点击后调用系统拨号入口', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
Uri? opened;
|
||||
final repository = _RepairRepository();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: RepairPage(
|
||||
repository: repository,
|
||||
launchEmergency: (uri) async {
|
||||
opened = uri;
|
||||
return true;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.drag(find.byType(ListView), const Offset(0, -500));
|
||||
await tester.pumpAndSettle();
|
||||
expect(opened, isNull);
|
||||
await tester.tap(find.widgetWithText(TextButton, '紧急电话'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(opened, Uri(scheme: 'tel', path: '119'));
|
||||
});
|
||||
|
||||
testWidgets('分步校验,失败冻结原请求,重试不重新生成请求号', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final repository = _RepairRepository();
|
||||
final router = GoRouter(
|
||||
initialLocation: '/repair',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/repair',
|
||||
builder: (_, state) => RepairPage(
|
||||
repository: repository,
|
||||
pickPhoto: (_) async => XFile.fromData(
|
||||
base64Decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aX1cAAAAASUVORK5CYII=',
|
||||
),
|
||||
name: 'test.png',
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/addresses',
|
||||
builder: (context, state) => Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () => context.pop(
|
||||
const ShippingAddress(
|
||||
identity: 'address-1',
|
||||
address: '测试路88号',
|
||||
contactName: '联系人',
|
||||
contactPhone: '13800000001',
|
||||
),
|
||||
),
|
||||
child: const Text('选这个地址'),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/tickets/:identity',
|
||||
builder: (_, state) => const Scaffold(body: Text('已打开工单详情')),
|
||||
),
|
||||
],
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('下一步'));
|
||||
await tester.tap(find.text('下一步'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('请描述故障现象'), findsWidgets);
|
||||
await tester.enterText(find.byType(TextField), '阀门异响');
|
||||
expect(find.text('添加照片'), findsNWidgets(3));
|
||||
await tester.ensureVisible(find.text('添加照片').first);
|
||||
await tester.tap(find.text('添加照片').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('从相册选择'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('下一步'));
|
||||
await tester.tap(find.text('下一步'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('下一步'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('请选择并补齐联系人、电话和地址'), findsOneWidget);
|
||||
await tester.tap(find.text('选择报修地址'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('选这个地址'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('下一步'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.requests, isEmpty);
|
||||
await tester.ensureVisible(find.widgetWithText(FilledButton, '确认提交'));
|
||||
await tester.tap(find.widgetWithText(FilledButton, '确认提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('结果待确认'), findsOneWidget);
|
||||
await tester.ensureVisible(find.text('重试提交'));
|
||||
await tester.tap(find.text('重试提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.requests.length, 2);
|
||||
expect(repository.uploads, 1);
|
||||
expect(repository.requests[0], repository.requests[1]);
|
||||
expect(find.text('已打开工单详情'), findsOneWidget);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
}
|
||||
165
apps/user_app/test/ui/repair_speech_test.dart
Normal file
165
apps/user_app/test/ui/repair_speech_test.dart
Normal file
@@ -0,0 +1,165 @@
|
||||
// 功能描述:语音结果选区替换、重复中间结果、拒绝权限、离页取消与长度边界回归。
|
||||
// 版本:1.0.0。
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/data/services/repair_speech.dart';
|
||||
import 'package:user_app/ui/features/tickets/repair_speech_button.dart';
|
||||
|
||||
class _Speech implements RepairSpeech {
|
||||
bool available = true;
|
||||
int cancels = 0, starts = 0;
|
||||
Completer<bool>? pending;
|
||||
late void Function(String) words, error;
|
||||
late void Function() done;
|
||||
@override
|
||||
Future<bool> start({
|
||||
required void Function(String) onWords,
|
||||
required void Function(String) onError,
|
||||
required void Function() onDone,
|
||||
}) async {
|
||||
starts++;
|
||||
words = onWords;
|
||||
error = onError;
|
||||
done = onDone;
|
||||
return pending?.future ?? available;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
done();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancel() async {
|
||||
cancels++;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('权限弹窗不取消初始化,切后台立即取消', (tester) async {
|
||||
final speech = _Speech()..pending = Completer<bool>();
|
||||
final controller = TextEditingController(text: '保留');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: RepairSpeechButton(controller: controller, onBusy: (_) {}, speech: speech),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('语音输入'));
|
||||
await tester.pump();
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
||||
await tester.pump();
|
||||
expect(speech.cancels, 0);
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused);
|
||||
await tester.pump();
|
||||
expect(speech.cancels, 1);
|
||||
speech.pending!.complete(true);
|
||||
speech.words('迟到');
|
||||
await tester.pump();
|
||||
expect(controller.text, '保留');
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
controller.dispose();
|
||||
});
|
||||
testWidgets('选区替换和中间结果不重复追加,结束后可再次录入', (tester) async {
|
||||
final speech = _Speech(), controller = TextEditingController(text: '前旧后');
|
||||
controller.selection = const TextSelection(baseOffset: 1, extentOffset: 2);
|
||||
final busy = <bool>[];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: RepairSpeechButton(controller: controller, onBusy: busy.add, speech: speech),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('语音输入'));
|
||||
await tester.pump();
|
||||
speech.words('阀门');
|
||||
speech.words('阀门异响');
|
||||
await tester.pump();
|
||||
expect(controller.text, '前阀门异响后');
|
||||
await tester.tap(find.text('结束识别'));
|
||||
await tester.pump();
|
||||
speech.words('迟到结果');
|
||||
expect(controller.text, '前阀门异响后');
|
||||
expect(busy.last, false);
|
||||
await tester.tap(find.text('语音输入'));
|
||||
await tester.pump();
|
||||
speech.words('补充');
|
||||
speech.done();
|
||||
await tester.pump();
|
||||
expect(controller.text, '前阀门异响补充后');
|
||||
expect(speech.starts, 2);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
controller.dispose();
|
||||
});
|
||||
testWidgets('不可用和权限拒绝保留输入并解除忙碌', (tester) async {
|
||||
final speech = _Speech()..available = false, controller = TextEditingController(text: '已有内容');
|
||||
final busy = <bool>[];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: RepairSpeechButton(controller: controller, onBusy: busy.add, speech: speech),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('语音输入'));
|
||||
await tester.pump();
|
||||
expect(find.textContaining('无法使用语音识别'), findsOneWidget);
|
||||
expect(controller.text, '已有内容');
|
||||
expect(busy.last, false);
|
||||
speech.available = true;
|
||||
await tester.tap(find.text('语音输入'));
|
||||
await tester.pump();
|
||||
speech.error('error_permission');
|
||||
await tester.pump();
|
||||
expect(find.textContaining('未获得麦克风'), findsOneWidget);
|
||||
expect(controller.text, '已有内容');
|
||||
expect(busy.last, false);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
controller.dispose();
|
||||
});
|
||||
testWidgets('初始化中离页取消,迟到结果不改变文字', (tester) async {
|
||||
final speech = _Speech()..pending = Completer<bool>(),
|
||||
controller = TextEditingController(text: '保留');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: RepairSpeechButton(controller: controller, onBusy: (_) {}, speech: speech),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('语音输入'));
|
||||
await tester.pump();
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
speech.pending!.complete(true);
|
||||
speech.words('迟到');
|
||||
await tester.pump();
|
||||
expect(speech.cancels, 1);
|
||||
expect(controller.text, '保留');
|
||||
expect(tester.takeException(), isNull);
|
||||
controller.dispose();
|
||||
});
|
||||
testWidgets('识别遵守2000字符上限,不截断表情字符', (tester) async {
|
||||
final speech = _Speech(),
|
||||
controller = TextEditingController(text: List.filled(1999, '字').join());
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: RepairSpeechButton(controller: controller, onBusy: (_) {}, speech: speech),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('语音输入'));
|
||||
await tester.pump();
|
||||
speech.words('😀多余');
|
||||
speech.done();
|
||||
await tester.pump();
|
||||
expect(controller.text.characters.length, 2000);
|
||||
expect(controller.text.endsWith('😀'), true);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
controller.dispose();
|
||||
});
|
||||
}
|
||||
43
apps/user_app/test/ui/safety_contents_page_test.dart
Normal file
43
apps/user_app/test/ui/safety_contents_page_test.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
// 功能:公告中心搜索、协议隔离和下架内容提示回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/ui/features/home/safety_contents_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _ContentsRepository extends A1FixtureRepository {
|
||||
@override
|
||||
Future<ClientRecord> safetyContent(String identity) async => throw const ApiException(1112, '未发布');
|
||||
@override
|
||||
Future<List<ClientRecord>> contents() async => const [
|
||||
ClientRecord(identity: 'notice', title: '配送调整', subtitle: '', raw: {'content_type': 'notice', 'body': '测试正文'}),
|
||||
ClientRecord(identity: 'agreement', title: '用户协议', subtitle: '', raw: {'content_type': 'agreement'}),
|
||||
ClientRecord(identity: 'law', title: '测试法规', subtitle: '', raw: {'content_type': 'law', 'body': '测试法规正文'}),
|
||||
];
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('公告中心不展示协议且支持标题搜索', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(home: SafetyContentsPage(repository: _ContentsRepository())));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('配送调整'), findsOneWidget);
|
||||
expect(find.text('用户协议'), findsNothing);
|
||||
await tester.enterText(find.byType(TextField), '无匹配');
|
||||
await tester.pump();
|
||||
expect(find.text('暂无符合条件的内容'), findsOneWidget);
|
||||
});
|
||||
testWidgets('法律法规分类只显示该类型的已发布内容', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(home: SafetyContentsPage(repository: _ContentsRepository())));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('法律法规'));
|
||||
await tester.pump();
|
||||
expect(find.text('测试法规'), findsOneWidget);
|
||||
expect(find.text('配送调整'), findsNothing);
|
||||
});
|
||||
testWidgets('未知或非公告标识不能显示协议正文', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(home: SafetyContentsPage(repository: _ContentsRepository(), identity: 'agreement')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('内容不存在或已下架'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
136
apps/user_app/test/ui/settings_page_test.dart
Normal file
136
apps/user_app/test/ui/settings_page_test.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
// 功能描述:设置确认操作、附属读取失败降级与登录密码表单回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/app/dependencies.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/data/services/app_settings_service.dart';
|
||||
import 'package:user_app/data/services/secure_session_store.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/settings/settings_page.dart';
|
||||
import 'package:user_app/ui/features/settings/login_password_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class SettingsSession extends UserSession {
|
||||
SettingsSession() : super(SecureSessionStore());
|
||||
int logouts = 0;
|
||||
@override
|
||||
Future<void> logout() async {
|
||||
logouts++;
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsFixture extends A1FixtureRepository {
|
||||
bool failProfile = false, failPassword = true;
|
||||
int changes = 0;
|
||||
String? receivedCurrent, receivedNext;
|
||||
@override
|
||||
Future<UserProfile> profile() async {
|
||||
if (failProfile) throw const ApiException(500, '资料暂时不可用');
|
||||
return super.profile();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> changeLoginPassword(String currentPassword, String newPassword) async {
|
||||
changes++;
|
||||
receivedCurrent = currentPassword;
|
||||
receivedNext = newPassword;
|
||||
if (failPassword) throw const ApiException(1305, '当前密码错误');
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsServiceFixture extends AppSettingsService {
|
||||
int clears = 0;
|
||||
@override
|
||||
Future<String> version() async => '1.0.0 (1)';
|
||||
@override
|
||||
int get imageCacheBytes => clears == 0 ? 1048576 : 0;
|
||||
@override
|
||||
void clearImageCache() {
|
||||
clears++;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('资料读取失败仍可清图片缓存,取消不清除,退出需确认', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final repo = SettingsFixture()..failProfile = true;
|
||||
final session = SettingsSession(), service = SettingsServiceFixture();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: SettingsPage(repository: repo, session: session, service: service),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('读取失败,点击重试'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(find.text('清除缓存'), 300);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('清除缓存'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('取消'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(service.clears, 0);
|
||||
await tester.tap(find.text('清除缓存'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('确认'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(service.clears, 1);
|
||||
expect(find.text('0.0MB 图片缓存'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(find.text('退出登录'), 300);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('退出登录'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('取消'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(session.logouts, 0);
|
||||
await tester.tap(find.text('退出登录'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('确认'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(session.logouts, 1);
|
||||
});
|
||||
|
||||
testWidgets('密码校验阻止错误提交,服务端失败保留输入,成功才退出', (tester) async {
|
||||
final repo = SettingsFixture(), session = SettingsSession();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: LoginPasswordPage(repository: repo, session: session),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('确认修改并重新登录'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.changes, 0);
|
||||
await tester.enterText(find.byKey(const ValueKey('password-0')), 'old-test-password');
|
||||
await tester.enterText(find.byKey(const ValueKey('password-1')), 'new-test-password');
|
||||
await tester.enterText(find.byKey(const ValueKey('password-2')), 'mismatch');
|
||||
await tester.tap(find.text('确认修改并重新登录'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('两次新密码不一致'), findsOneWidget);
|
||||
expect(repo.changes, 0);
|
||||
await tester.enterText(find.byKey(const ValueKey('password-2')), 'new-test-password');
|
||||
await tester.tap(find.text('确认修改并重新登录'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('当前密码错误'), findsOneWidget);
|
||||
expect(session.logouts, 0);
|
||||
expect(
|
||||
tester.widget<TextFormField>(find.byKey(const ValueKey('password-1'))).controller!.text,
|
||||
'new-test-password',
|
||||
);
|
||||
repo.failPassword = false;
|
||||
await tester.tap(find.text('确认修改并重新登录'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.changes, 2);
|
||||
expect(session.logouts, 1);
|
||||
expect(repo.receivedCurrent, 'old-test-password');
|
||||
expect(repo.receivedNext, 'new-test-password');
|
||||
expect(
|
||||
tester.widget<TextFormField>(find.byKey(const ValueKey('password-1'))).controller!.text,
|
||||
'',
|
||||
);
|
||||
});
|
||||
}
|
||||
130
apps/user_app/test/ui/shop_address_selection_test.dart
Normal file
130
apps/user_app/test/ui/shop_address_selection_test.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
// 功能描述:验证商城显式选择收货地址并提交该地址的联系人,不使用账户联系人。
|
||||
// 版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/domain/models/shipping_address.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/shop/shop_page.dart';
|
||||
import 'package:user_app/ui/features/shop/product_detail_page.dart';
|
||||
import 'package:user_app/ui/features/shop/checkout_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class CheckoutFixture extends A1FixtureRepository {
|
||||
String? selected, name, phone;
|
||||
final requests = <String>[];
|
||||
int? sentQuantity, sentAmount;
|
||||
@override
|
||||
Future<Map<String, Object?>> submitShopOrder({
|
||||
required String requestNo,
|
||||
required String productIdentity,
|
||||
required ShippingAddress address,
|
||||
required int quantity,
|
||||
required int expectedAmount,
|
||||
String remark = '',
|
||||
}) async {
|
||||
requests.add(requestNo);
|
||||
sentQuantity = quantity;
|
||||
sentAmount = expectedAmount;
|
||||
if (requests.length == 1) throw const ApiException(500, '提交结果未知');
|
||||
selected = address.identity;
|
||||
name = address.contactName;
|
||||
phone = address.contactPhone;
|
||||
return {'identity': 'created'};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ShippingAddress>> shippingAddresses() async => [];
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('确认前不下单,提交选中地址的联系人', (tester) async {
|
||||
final repo = CheckoutFixture();
|
||||
final router = GoRouter(
|
||||
initialLocation: '/shop',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/products/:identity',
|
||||
builder: (_, state) =>
|
||||
ProductDetailPage(repository: repo, identity: state.pathParameters['identity']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/checkout/:identity',
|
||||
builder: (_, state) =>
|
||||
CheckoutPage(repository: repo, productIdentity: state.pathParameters['identity']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/shop',
|
||||
builder: (_, state) => ShopPage(repository: repo),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/addresses',
|
||||
builder: (context, state) => Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () => context.pop(
|
||||
const ShippingAddress(
|
||||
identity: 'selected-address',
|
||||
address: '收货地址',
|
||||
contactName: '收货联系人',
|
||||
contactPhone: '13900000001',
|
||||
),
|
||||
),
|
||||
child: const Text('选择此地址'),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/orders',
|
||||
builder: (_, state) => const Scaffold(body: Text('订单列表')),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/payment/shop/:identity',
|
||||
builder: (_, state) => Scaffold(
|
||||
body: Text('支付订单 ${state.pathParameters['identity']}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(router.dispose);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router, theme: AppTheme.light()));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('15kg家用液化气'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('15kg家用液化气'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.requests, isEmpty);
|
||||
await tester.tap(find.text('立即购买'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.selected, isNull);
|
||||
await tester.tap(find.text('选择收货地址'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('选择此地址'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('收货联系人 139****0001'), findsOneWidget);
|
||||
expect(find.text('配送时间'), findsOneWidget);
|
||||
expect(repo.selected, isNull);
|
||||
await tester.tap(find.byTooltip('增加数量'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '提交订单'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('提交结果未知'), findsOneWidget);
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(
|
||||
find.byWidgetPredicate((widget) => widget is IconButton && widget.tooltip == '增加数量'),
|
||||
)
|
||||
.onPressed,
|
||||
isNull,
|
||||
);
|
||||
await tester.tap(find.widgetWithText(FilledButton, '重试提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.selected, 'selected-address');
|
||||
expect(repo.name, '收货联系人');
|
||||
expect(repo.phone, '13900000001');
|
||||
expect(repo.requests[0], repo.requests[1]);
|
||||
expect(repo.sentQuantity, 2);
|
||||
expect(repo.sentAmount, 25600);
|
||||
expect(find.text('支付订单 created'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
36
apps/user_app/test/ui/shop_category_scroll_test.dart
Normal file
36
apps/user_app/test/ui/shop_category_scroll_test.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
// 功能描述:验证商城分类支持鼠标、触屏横向拖动及拖动后的筛选。
|
||||
// 版本:1.0.0
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/features/shop/shop_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
for (final kind in [PointerDeviceKind.mouse, PointerDeviceKind.touch]) {
|
||||
testWidgets('分类拖动与筛选:${kind.name}', (tester) async {
|
||||
tester.view.physicalSize = const Size(320, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(MaterialApp(home: ShopPage(repository: A1FixtureRepository())));
|
||||
await tester.pumpAndSettle();
|
||||
final strip = find.byKey(const ValueKey('shop-category-scroll'));
|
||||
final scrollable = find.descendant(of: strip, matching: find.byType(Scrollable));
|
||||
final state = tester.state<ScrollableState>(scrollable);
|
||||
expect(state.position.pixels, 0);
|
||||
expect(state.position.maxScrollExtent, greaterThan(0));
|
||||
final gesture = await tester.startGesture(tester.getCenter(strip), kind: kind);
|
||||
await gesture.moveBy(const Offset(-30, 0));
|
||||
await tester.pump();
|
||||
await gesture.moveBy(const Offset(-100, 0));
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.position.pixels, greaterThan(0));
|
||||
await tester.tap(find.widgetWithText(ChoiceChip, '燃气配件'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('不锈钢减压阀'), findsOneWidget);
|
||||
expect(find.text('燃气报警器'), findsNothing);
|
||||
});
|
||||
}
|
||||
}
|
||||
134
apps/user_app/test/ui/shop_order_detail_test.dart
Normal file
134
apps/user_app/test/ui/shop_order_detail_test.dart
Normal file
@@ -0,0 +1,134 @@
|
||||
// 功能描述:订单深链详情、历史快照、确认操作及状态变化回归;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/shop_order_detail.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/orders/shop_order_detail_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
class _Repo extends A1FixtureRepository {
|
||||
int reads = 0, cancels = 0;
|
||||
bool failLoad = false, failCancel = false, stale = false;
|
||||
String status = '待付款';
|
||||
@override
|
||||
Future<ShopOrderDetail> shopOrderDetail(String identity) async {
|
||||
reads++;
|
||||
if (failLoad) {
|
||||
failLoad = false;
|
||||
throw const ApiException(500, '暂时读取失败');
|
||||
}
|
||||
return ShopOrderDetail.fromJson({
|
||||
'identity': identity,
|
||||
'order_no': 'EC123',
|
||||
'status_name': status,
|
||||
'allowed_actions': status == '待付款' && !(stale && reads > 1) ? ['cancel'] : [],
|
||||
'contact_name': '历史联系人',
|
||||
'contact_phone': '13800000001',
|
||||
'address': '成交时地址',
|
||||
'product_amount': 3998,
|
||||
'discount_amount': 0,
|
||||
'payable_amount': 3998,
|
||||
'remark': '原订单备注',
|
||||
'created_at': '2026-09-05T09:20:00+08:00',
|
||||
'paid_at': null,
|
||||
'items': [
|
||||
{'identity': 'line', 'name': '成交时商品', 'quantity': 2, 'sale_amount': 1999},
|
||||
],
|
||||
}, (s) => s);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelShopOrder(String identity) async {
|
||||
cancels++;
|
||||
if (failCancel) {
|
||||
failCancel = false;
|
||||
throw const ApiException(500, '取消结果待确认');
|
||||
}
|
||||
status = '已取消';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _show(WidgetTester tester, _Repo repo) async {
|
||||
final router = GoRouter(
|
||||
initialLocation: '/shop/orders/one',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/shop/orders/:id',
|
||||
builder: (_, s) => ShopOrderDetailPage(repository: repo, identity: s.pathParameters['id']!),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(router.dispose);
|
||||
await tester.pumpWidget(MaterialApp.router(theme: AppTheme.light(), routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('深链读取快照,失败重试、金额、复制订单号与取消确认', (tester) async {
|
||||
final repo = _Repo()..failLoad = true;
|
||||
String? copied;
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
|
||||
call,
|
||||
) async {
|
||||
if (call.method == 'Clipboard.setData') copied = (call.arguments as Map)['text'] as String;
|
||||
return null;
|
||||
});
|
||||
addTearDown(
|
||||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
null,
|
||||
),
|
||||
);
|
||||
await _show(tester, repo);
|
||||
await tester.tap(find.text('重新加载'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('成交时商品'), findsOneWidget);
|
||||
expect(find.text('成交时地址'), findsOneWidget);
|
||||
expect(find.text('¥19.99'), findsOneWidget);
|
||||
expect(find.text('已付款'), findsNothing);
|
||||
await tester.scrollUntilVisible(find.text('复制'), 250);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('复制'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(copied, 'EC123');
|
||||
await tester.tap(find.text('取消订单'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('返回'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.cancels, 0);
|
||||
await tester.tap(find.text('取消订单'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('确认取消'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.cancels, 1);
|
||||
expect(find.text('取消订单'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
testWidgets('刷新后不再允许的动作不会提交,失败操作保留重试', (tester) async {
|
||||
final repo = _Repo()..stale = true;
|
||||
await _show(tester, repo);
|
||||
await tester.tap(find.text('取消订单'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.cancels, 0);
|
||||
expect(find.text('订单状态已变化,请查看最新信息'), findsOneWidget);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pumpAndSettle();
|
||||
final retry = _Repo()..failCancel = true;
|
||||
await _show(tester, retry);
|
||||
await tester.tap(find.text('取消订单'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('确认取消'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('取消结果待确认'), findsOneWidget);
|
||||
expect(find.text('取消订单'), findsOneWidget);
|
||||
await tester.tap(find.text('取消订单'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('确认取消'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(retry.cancels, 2);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
90
apps/user_app/test/ui/ticket_detail_page_test.dart
Normal file
90
apps/user_app/test/ui/ticket_detail_page_test.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
// 功能描述:工单详情动作确认、失败保留、重试刷新与终态隐藏回归。
|
||||
// 版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
import 'package:user_app/ui/features/tickets/ticket_detail_page.dart';
|
||||
|
||||
class _Tickets extends ClientRepository {
|
||||
_Tickets() : super(ApiClient(() => '', baseUrl: 'https://api.example.com'));
|
||||
int calls = 0, writes = 0;
|
||||
bool fail = false;
|
||||
List<String> actions = ['cancel', 'confirm'];
|
||||
@override
|
||||
Future<ClientRecord> ticket(String identity) async {
|
||||
calls++;
|
||||
return ClientRecord(
|
||||
identity: identity,
|
||||
title: 'TK测试',
|
||||
subtitle: '',
|
||||
raw: {
|
||||
'status_name': actions.isEmpty ? '已完成' : '待确认',
|
||||
'allowed_actions': actions,
|
||||
'description': '阀门异响',
|
||||
'category': 'repair',
|
||||
'result': '已更换密封件',
|
||||
'created_at': '2026-09-07T01:00:00Z',
|
||||
'address': '测试路88号',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelTicket(String identity) async => _write();
|
||||
@override
|
||||
Future<void> confirmTicket(String identity) async => _write();
|
||||
void _write() {
|
||||
writes++;
|
||||
if (fail) throw const ApiException(500, '网络异常,请重试');
|
||||
actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
for (final cancel in [true, false]) {
|
||||
testWidgets('${cancel ? '取消' : '完成'}必须确认,失败后允许幂等重试', (tester) async {
|
||||
final repository = _Tickets();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: TicketDetailPage(repository: repository, identity: 'owned'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('已更换密封件'),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.text('已更换密封件'), findsOneWidget);
|
||||
final action = find.text(cancel ? '取消工单' : '确认处理完成');
|
||||
await tester.scrollUntilVisible(action, 200, scrollable: find.byType(Scrollable).first);
|
||||
await tester.tap(action);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('返回'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.writes, 0);
|
||||
repository.fail = true;
|
||||
await tester.tap(action);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(cancel ? '确认取消' : '确认完成'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.writes, 1);
|
||||
expect(find.text('网络异常,请重试'), findsOneWidget);
|
||||
expect(action, findsOneWidget);
|
||||
repository.fail = false;
|
||||
// 等待错误提示消失,避免遮挡底部操作按钮。
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(action);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(cancel ? '确认取消' : '确认完成'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.writes, 2);
|
||||
expect(repository.calls, 2);
|
||||
expect(find.text('取消工单'), findsNothing);
|
||||
expect(find.text('确认处理完成'), findsNothing);
|
||||
});
|
||||
}
|
||||
}
|
||||
82
apps/user_app/test/ui/usage_statistics_page_test.dart
Normal file
82
apps/user_app/test/ui/usage_statistics_page_test.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
// 功能描述:验证用气统计仅展示权威数据并保留缺失能力提示;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/features/profile/usage_statistics_page.dart';
|
||||
import 'package:user_app/domain/models/usage_statistics.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('日期确认传入查询并在切换周期时保留,取消不查询', (tester) async {
|
||||
final repository = _DateRepository();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: UsageStatisticsPage(repository: repository),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('选择日期'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('取消'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.anchors.length, 1);
|
||||
await tester.tap(find.byTooltip('选择日期'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('确定'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.anchors.last, DateUtils.dateOnly(DateTime.now()));
|
||||
await tester.tap(find.text('年'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.anchors.length, 3);
|
||||
expect(repository.anchors.last, repository.anchors[1]);
|
||||
});
|
||||
testWidgets('用气统计展示口径、趋势和明细', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: UsageStatisticsPage(repository: A1FixtureRepository())),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('用气统计'), findsOneWidget);
|
||||
expect(find.textContaining('厨房15kg气瓶'), findsOneWidget);
|
||||
expect(find.text('本期用气总览'), findsOneWidget);
|
||||
expect(find.text('每日用气量(kg)'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('用气明细'),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.text('安全趋势暂未开放'), findsOneWidget);
|
||||
await tester.ensureVisible(find.text('用气明细'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('用气明细'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('09月30日'), findsWidgets);
|
||||
expect(find.text('0.230 kg'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('日周月年切换触发重新读取', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: UsageStatisticsPage(repository: A1FixtureRepository())),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('年'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('本期用气总览'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
// 记录真实页面传给数据层的日期,避免仅验证弹窗是否出现。
|
||||
class _DateRepository extends A1FixtureRepository {
|
||||
final anchors = <DateTime?>[];
|
||||
|
||||
@override
|
||||
Future<UsageStatistics> usageStatistics({
|
||||
required String deviceIdentity,
|
||||
String period = 'month',
|
||||
DateTime? anchor,
|
||||
}) {
|
||||
anchors.add(anchor);
|
||||
return super.usageStatistics(
|
||||
deviceIdentity: deviceIdentity, period: period, anchor: anchor,
|
||||
);
|
||||
}
|
||||
}
|
||||
52
apps/user_app/test/ui/user_records_page_test.dart
Normal file
52
apps/user_app/test/ui/user_records_page_test.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
// 功能描述:验证我的记录聚合真实用气与报修数据,并正确区分首期缺项和二期能力;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/features/profile/user_records_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('我的记录展示真实记录并可筛选', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: UserRecordsPage(repository: A1FixtureRepository())),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('我的记录'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(
|
||||
find.textContaining('15kg家用液化气配送'),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.textContaining('15kg家用液化气配送'), findsOneWidget);
|
||||
expect(find.text('报修工单'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('筛选'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('报修记录').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('报修工单'), findsOneWidget);
|
||||
expect(find.textContaining('15kg家用液化气配送'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('首期缺项和二期入口显示不同提示', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: UserRecordsPage(repository: A1FixtureRepository())),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('角阀操作'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('角阀操作'), findsNWidgets(2));
|
||||
expect(find.text('该功能暂未开放,目前无法使用。'), findsOneWidget);
|
||||
await tester.tap(find.text('知道了'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('发票记录'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('发票记录'), findsNWidgets(2));
|
||||
expect(find.text('该功能将在后续版本开放,敬请期待。'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
51
apps/user_app/test/ui/wallet_page_test.dart
Normal file
51
apps/user_app/test/ui/wallet_page_test.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
// 功能描述:钱包隐私开关、真实空错状态、账单筛选和详情;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/wallet/wallet_page.dart';
|
||||
import 'package:user_app/ui/features/wallet/wallet_bills_page.dart';
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('金额隐藏覆盖余额与最近账单,账单失败可独立重试', (tester) async {
|
||||
final repo = A1FixtureRepository()..fail = true;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: WalletPage(repository: repo),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('¥568.00'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('隐藏金额'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('¥568.00'), findsNothing);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('账单加载失败,点击重试'),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
repo.fail = false;
|
||||
await tester.tap(find.text('账单加载失败,点击重试'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('-¥228.00'), findsNothing);
|
||||
expect(find.text('气瓶订单'), findsOneWidget);
|
||||
});
|
||||
testWidgets('账单筛选只显示对应方向并能查看记账详情', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: WalletBillsPage(repository: A1FixtureRepository()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('收入'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('气瓶订单'), findsNothing);
|
||||
expect(find.text('+¥59.00'), findsOneWidget);
|
||||
await tester.tap(find.text('退款入账'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('记账后余额:¥568.00'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
40
apps/user_app/test/ui/withdrawal_page_test.dart
Normal file
40
apps/user_app/test/ui/withdrawal_page_test.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
// 功能描述:提现页面真实校验、二次确认和状态语义测试;版本:1.0.0。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/ui/core/app_theme.dart';
|
||||
import 'package:user_app/ui/features/wallet/withdrawal_page.dart';
|
||||
|
||||
import '../support/a1_fixture.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('提现展示真实可提余额并在二次确认后提交', (tester) async {
|
||||
final repo = A1FixtureRepository();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: WithdrawalPage(repository: repo),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('¥568.00'), findsOneWidget);
|
||||
await tester.enterText(find.byKey(const Key('withdrawal-amount')), '50');
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('支付密码验证'),
|
||||
260,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.enterText(find.byKey(const Key('withdrawal-password')), '123456');
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('提交提现申请'),
|
||||
260,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.tap(find.text('提交提现申请'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('确认提现申请'), findsOneWidget);
|
||||
await tester.tap(find.text('确认提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.withdrawalCalls, 1);
|
||||
expect(find.textContaining('提现申请已提交'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user