import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jiu_client/core/models/page_result.dart'; import 'package:jiu_client/models/product.dart'; import 'package:jiu_client/providers/product_provider.dart'; import 'package:jiu_client/screens/products/products_screen.dart'; // --------------------------------------------------------------------------- // Fake notifier helpers // --------------------------------------------------------------------------- /// A notifier that immediately emits a fixed state. class _FakeProductNotifier extends ProductListNotifier { final AsyncValue> _fixed; _FakeProductNotifier(this._fixed); @override Future> build() async { state = _fixed; return state.value ?? const PageResult(data: [], total: 0, page: 1, pageSize: 20); } @override void reload() {} @override void setKeyword(String keyword) {} @override void setPage(int page) {} @override Future createProduct(Map data) async {} @override Future updateProduct(int id, Map data) async {} @override Future deleteProduct(int id) async {} } /// A notifier that stays permanently in loading state (build never completes). class _FakeLoadingProductNotifier extends ProductListNotifier { @override Future> build() { // Never completes — keeps the widget in CircularProgressIndicator state. return Completer>().future; } @override void reload() {} @override void setKeyword(String keyword) {} @override void setPage(int page) {} @override Future createProduct(Map data) async {} @override Future updateProduct(int id, Map data) async {} @override Future deleteProduct(int id) async {} } /// Helper to build a testable app wrapping [ProductsScreen]. Widget _buildApp(AsyncValue> state) { return ProviderScope( overrides: [ productListProvider.overrideWith(() => _FakeProductNotifier(state)), ], child: const MaterialApp(home: Scaffold(body: ProductsScreen())), ); } Widget _buildLoadingApp() { return ProviderScope( overrides: [ productListProvider.overrideWith(() => _FakeLoadingProductNotifier()), ], child: const MaterialApp(home: Scaffold(body: ProductsScreen())), ); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- // TODO: ProductsScreen 已重构为 options tab 页,productListProvider 已不挂载,测试待重写 void main() { testWidgets('shows CircularProgressIndicator while loading', skip: true, (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget(_buildLoadingApp()); await tester.pump(); expect(find.byType(CircularProgressIndicator), findsOneWidget); }); testWidgets('shows error message on load failure', skip: true, (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget( _buildApp(AsyncValue.error('网络连接失败', StackTrace.empty)), ); await tester.pump(); expect(find.text('暂无数据,网络不可用'), findsOneWidget); expect(find.text('重试'), findsOneWidget); }); testWidgets('shows 暂无商品 when list is empty', skip: true, (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const emptyResult = PageResult( data: [], total: 0, page: 1, pageSize: 20, ); await tester.pumpWidget(_buildApp(const AsyncValue.data(emptyResult))); await tester.pump(); expect(find.text('暂无商品'), findsOneWidget); }); testWidgets('shows product list with name code and unit', skip: true, (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const products = [ Product(id: 1, code: 'P001', name: '五粮液', unit: '瓶'), Product(id: 2, code: 'P002', name: '茅台', unit: '瓶'), ]; final result = PageResult( data: products, total: 2, page: 1, pageSize: 20, ); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); expect(find.text('五粮液'), findsOneWidget); expect(find.text('茅台'), findsOneWidget); expect(find.text('P001'), findsOneWidget); expect(find.text('P002'), findsOneWidget); }); testWidgets('shows 新建 button in toolbar', skip: true, (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const emptyResult = PageResult( data: [], total: 0, page: 1, pageSize: 20, ); await tester.pumpWidget(_buildApp(const AsyncValue.data(emptyResult))); await tester.pump(); expect(find.text('新建'), findsOneWidget); }); testWidgets('tapping 新建 opens product form dialog', skip: true, (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const emptyResult = PageResult( data: [], total: 0, page: 1, pageSize: 20, ); await tester.pumpWidget(_buildApp(const AsyncValue.data(emptyResult))); await tester.pump(); await tester.tap(find.text('新建')); await tester.pumpAndSettle(); // Dialog should appear with "新建商品" title expect(find.text('新建商品'), findsOneWidget); // Form should have 商品名称 and 商品编码 fields expect(find.text('商品名称'), findsAtLeastNWidgets(1)); expect(find.text('商品编码'), findsAtLeastNWidgets(1)); }); testWidgets('product form shows not-empty error', skip: true, (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const emptyResult = PageResult( data: [], total: 0, page: 1, pageSize: 20, ); await tester.pumpWidget(_buildApp(const AsyncValue.data(emptyResult))); await tester.pump(); await tester.tap(find.text('新建')); await tester.pumpAndSettle(); // Tap save without filling required fields await tester.tap(find.text('保存')); await tester.pump(); expect(find.text('不能为空'), findsAtLeastNWidgets(1)); }); testWidgets('shows delete confirmation dialog', skip: true, (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const products = [ Product(id: 1, code: 'P001', name: '五粮液', unit: '瓶'), ]; final result = PageResult( data: products, total: 1, page: 1, pageSize: 20, ); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); await tester.tap(find.byKey(const Key('btn_delete_1'))); await tester.pumpAndSettle(); expect(find.text('确认删除'), findsOneWidget); expect(find.textContaining('五粮液'), findsAtLeastNWidgets(1)); // Confirm dialog has 取消 and 删除 buttons — 删除 may appear in both the // list row action button and the dialog, so use findsAtLeastNWidgets(1). expect(find.text('取消'), findsOneWidget); expect(find.text('删除'), findsAtLeastNWidgets(1)); }); testWidgets('tapping edit opens edit dialog with prefilled values', skip: true, (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const products = [ Product(id: 1, code: 'P001', name: '五粮液', unit: '瓶', brand: '五粮液集团'), ]; final result = PageResult( data: products, total: 1, page: 1, pageSize: 20, ); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); await tester.tap(find.byKey(const Key('btn_edit_1'))); await tester.pumpAndSettle(); // Edit dialog title expect(find.text('编辑商品'), findsOneWidget); // Fields should be prefilled expect(find.widgetWithText(TextFormField, '五粮液'), findsOneWidget); expect(find.widgetWithText(TextFormField, 'P001'), findsOneWidget); }); testWidgets('shows total count in pagination bar', skip: true, (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const products = [ Product(id: 1, code: 'P001', name: '五粮液', unit: '瓶'), ]; final result = PageResult( data: products, total: 42, page: 1, pageSize: 20, ); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); expect(find.textContaining('42'), findsAtLeastNWidgets(1)); }); }