// 功能描述:购物车服务端确认、失效处理、管理删除与多商品重试回归;版本: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 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 items})>[]; int writes = 0; bool failWrite = false; final targets = []; @override Future> cart() async => List.of(items); @override Future cartItem(String identity) async => items.firstWhere((i) => i.product.identity == identity); @override Future 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> submitCartOrder({ required String requestNo, required List 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 _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); }); }