feat(inventory): 재고 현황 요약/상세 플로우를 릴리스
- lib/features/inventory/summary 계층과 warehouse select 위젯을 추가해 목록/상세, 자동 새로고침, 필터, 상세 시트를 구현 - PermissionBootstrapper, scope 파서, 라우트 가드로 inventory.view 기반 권한 부여와 메뉴 노출을 통합(lib/core, lib/main.dart 등) - Inventory Summary API/QA/Audit 문서와 PR 템플릿, CHANGELOG를 신규 스펙과 검증 커맨드로 업데이트 - DTO 직렬화 의존성을 추가하고 Golden·Widget·단위 테스트를 작성했으며 flutter analyze / flutter test --coverage를 통과
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,204 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/application/inventory_service.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_detail.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_event.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_product.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_summary.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_summary_list_result.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_vendor.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_warehouse.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_warehouse_balance.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/repositories/inventory_repository.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_filters.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/presentation/pages/inventory_summary_page.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/entities/warehouse.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/repositories/warehouse_repository.dart';
|
||||
|
||||
class _MockInventoryRepository extends Mock implements InventoryRepository {}
|
||||
|
||||
class _MockWarehouseRepository extends Mock implements WarehouseRepository {}
|
||||
|
||||
Widget _buildApp(Widget child) {
|
||||
return MaterialApp(
|
||||
home: ShadTheme(
|
||||
data: ShadThemeData(
|
||||
colorScheme: const ShadSlateColorScheme.light(),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
child: Scaffold(body: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InventorySummaryListResult _buildSummaryResult() {
|
||||
final warehouse = InventoryWarehouse(id: 1, code: 'WH-001', name: '본사');
|
||||
final summary = InventorySummary(
|
||||
product: InventoryProduct(
|
||||
id: 10,
|
||||
code: 'INV-10',
|
||||
name: '테스트 장비',
|
||||
vendor: const InventoryVendor(id: 55, name: '테스트 벤더'),
|
||||
),
|
||||
totalQuantity: 120,
|
||||
warehouseBalances: [
|
||||
InventoryWarehouseBalance(warehouse: warehouse, quantity: 80),
|
||||
InventoryWarehouseBalance(
|
||||
warehouse: InventoryWarehouse(id: 2, code: 'WH-002', name: '보관창고'),
|
||||
quantity: 40,
|
||||
),
|
||||
],
|
||||
recentEvent: InventoryEvent(
|
||||
eventId: 900,
|
||||
eventKind: 'receipt',
|
||||
eventLabel: '입고',
|
||||
deltaQuantity: 30,
|
||||
occurredAt: DateTime.utc(2025, 1, 3, 9, 0),
|
||||
warehouse: warehouse,
|
||||
),
|
||||
updatedAt: DateTime.utc(2025, 1, 3, 9, 15),
|
||||
lastRefreshedAt: DateTime.utc(2025, 1, 3, 9, 5),
|
||||
);
|
||||
return InventorySummaryListResult(
|
||||
result: PaginatedResult<InventorySummary>(
|
||||
items: [summary],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
total: 1,
|
||||
),
|
||||
lastRefreshedAt: summary.lastRefreshedAt,
|
||||
);
|
||||
}
|
||||
|
||||
InventoryDetail _buildDetail() {
|
||||
final warehouse1 = InventoryWarehouse(id: 1, code: 'WH-001', name: '본사');
|
||||
final warehouse2 = InventoryWarehouse(id: 2, code: 'WH-002', name: '보관창고');
|
||||
return InventoryDetail(
|
||||
product: InventoryProduct(
|
||||
id: 10,
|
||||
code: 'INV-10',
|
||||
name: '테스트 장비',
|
||||
vendor: const InventoryVendor(id: 55, name: '테스트 벤더'),
|
||||
),
|
||||
totalQuantity: 120,
|
||||
warehouseBalances: [
|
||||
InventoryWarehouseBalance(warehouse: warehouse1, quantity: 80),
|
||||
InventoryWarehouseBalance(warehouse: warehouse2, quantity: 40),
|
||||
],
|
||||
recentEvents: [
|
||||
InventoryEvent(
|
||||
eventId: 901,
|
||||
eventKind: 'receipt',
|
||||
eventLabel: '입고',
|
||||
deltaQuantity: 20,
|
||||
occurredAt: DateTime.utc(2025, 1, 3, 9, 10),
|
||||
warehouse: warehouse1,
|
||||
),
|
||||
],
|
||||
updatedAt: DateTime.utc(2025, 1, 3, 9, 20),
|
||||
lastRefreshedAt: DateTime.utc(2025, 1, 3, 9, 5),
|
||||
);
|
||||
}
|
||||
|
||||
void _registerDependencies({
|
||||
required InventoryRepository inventoryRepository,
|
||||
required WarehouseRepository warehouseRepository,
|
||||
}) {
|
||||
GetIt.I.registerSingleton<InventoryService>(
|
||||
InventoryService(repository: inventoryRepository),
|
||||
);
|
||||
GetIt.I.registerSingleton<WarehouseRepository>(warehouseRepository);
|
||||
}
|
||||
|
||||
void _stubWarehouseList(_MockWarehouseRepository repository) {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
includeZipcode: any(named: 'includeZipcode'),
|
||||
),
|
||||
).thenAnswer((invocation) async {
|
||||
final page = invocation.namedArguments[const Symbol('page')] as int? ?? 1;
|
||||
final items = page == 1
|
||||
? [Warehouse(id: 1, warehouseCode: 'WH-001', warehouseName: '본사 창고')]
|
||||
: const <Warehouse>[];
|
||||
return PaginatedResult<Warehouse>(
|
||||
items: items,
|
||||
page: page,
|
||||
pageSize:
|
||||
invocation.namedArguments[const Symbol('pageSize')] as int? ?? 20,
|
||||
total: items.length,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpInventoryPage(WidgetTester tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(1600, 1200));
|
||||
await tester.pumpWidget(
|
||||
_buildApp(
|
||||
InventorySummaryPage(
|
||||
routeUri: Uri(path: '/inventory/summary'),
|
||||
debugRowHeight: 200,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
final binding = TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(const InventorySummaryFilter());
|
||||
registerFallbackValue(const InventoryDetailFilter());
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
final inventoryRepository = _MockInventoryRepository();
|
||||
final warehouseRepository = _MockWarehouseRepository();
|
||||
_registerDependencies(
|
||||
inventoryRepository: inventoryRepository,
|
||||
warehouseRepository: warehouseRepository,
|
||||
);
|
||||
_stubWarehouseList(warehouseRepository);
|
||||
when(
|
||||
() => inventoryRepository.listSummaries(filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async => _buildSummaryResult());
|
||||
when(
|
||||
() =>
|
||||
inventoryRepository.fetchDetail(any(), filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async => _buildDetail());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await binding.setSurfaceSize(null);
|
||||
await GetIt.I.reset();
|
||||
});
|
||||
|
||||
testWidgets('Inventory summary page matches golden', (tester) async {
|
||||
await _pumpInventoryPage(tester);
|
||||
|
||||
await expectLater(
|
||||
find.byType(InventorySummaryPage),
|
||||
matchesGoldenFile('goldens/inventory_summary_page_default.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Inventory detail sheet matches golden', (tester) async {
|
||||
await _pumpInventoryPage(tester);
|
||||
await tester.tap(find.text('테스트 장비'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await expectLater(
|
||||
find.byType(InventorySummaryPage),
|
||||
matchesGoldenFile('goldens/inventory_summary_detail_sheet.png'),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/application/inventory_service.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_counterparty.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_detail.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_event.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_filters.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_product.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_summary.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_summary_list_result.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_vendor.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_warehouse.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/entities/inventory_warehouse_balance.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/domain/repositories/inventory_repository.dart';
|
||||
import 'package:superport_v2/features/inventory/summary/presentation/pages/inventory_summary_page.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/entities/warehouse.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/repositories/warehouse_repository.dart';
|
||||
|
||||
class _MockInventoryRepository extends Mock implements InventoryRepository {}
|
||||
|
||||
class _MockWarehouseRepository extends Mock implements WarehouseRepository {}
|
||||
|
||||
Widget _buildApp(Widget child) {
|
||||
return MaterialApp(
|
||||
home: ShadTheme(
|
||||
data: ShadThemeData(
|
||||
colorScheme: const ShadSlateColorScheme.light(),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
child: Scaffold(body: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _registerDependencies({
|
||||
required InventoryRepository inventoryRepository,
|
||||
required WarehouseRepository warehouseRepository,
|
||||
}) {
|
||||
GetIt.I.registerSingleton<InventoryService>(
|
||||
InventoryService(repository: inventoryRepository),
|
||||
);
|
||||
GetIt.I.registerSingleton<WarehouseRepository>(warehouseRepository);
|
||||
}
|
||||
|
||||
void _stubWarehouseList(_MockWarehouseRepository repository) {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
includeZipcode: any(named: 'includeZipcode'),
|
||||
),
|
||||
).thenAnswer((invocation) async {
|
||||
final page = invocation.namedArguments[const Symbol('page')] as int? ?? 1;
|
||||
final items = page == 1
|
||||
? [Warehouse(id: 1, warehouseCode: 'WH-001', warehouseName: '본사 창고')]
|
||||
: const <Warehouse>[];
|
||||
return PaginatedResult<Warehouse>(
|
||||
items: items,
|
||||
page: page,
|
||||
pageSize:
|
||||
invocation.namedArguments[const Symbol('pageSize')] as int? ?? 20,
|
||||
total: items.length,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
InventorySummaryListResult _buildSummaryResult() {
|
||||
final product = InventoryProduct(
|
||||
id: 10,
|
||||
code: 'INV-10',
|
||||
name: '테스트 장비',
|
||||
vendor: const InventoryVendor(id: 55, name: '테스트 벤더'),
|
||||
);
|
||||
final warehouse = InventoryWarehouse(id: 1, code: 'WH-001', name: '본사');
|
||||
final summary = InventorySummary(
|
||||
product: product,
|
||||
totalQuantity: 120,
|
||||
warehouseBalances: [
|
||||
InventoryWarehouseBalance(warehouse: warehouse, quantity: 80),
|
||||
InventoryWarehouseBalance(
|
||||
warehouse: InventoryWarehouse(id: 2, code: 'WH-002', name: '보관창고'),
|
||||
quantity: 40,
|
||||
),
|
||||
],
|
||||
recentEvent: InventoryEvent(
|
||||
eventId: 900,
|
||||
eventKind: 'receipt',
|
||||
eventLabel: '입고',
|
||||
deltaQuantity: 30,
|
||||
occurredAt: DateTime.utc(2025, 1, 3, 9, 0),
|
||||
counterparty: const InventoryCounterparty(
|
||||
type: InventoryCounterpartyType.vendor,
|
||||
name: 'QA 파트너',
|
||||
),
|
||||
warehouse: warehouse,
|
||||
),
|
||||
updatedAt: DateTime.utc(2025, 1, 3, 9, 15),
|
||||
lastRefreshedAt: DateTime.utc(2025, 1, 3, 9, 5),
|
||||
);
|
||||
return InventorySummaryListResult(
|
||||
result: PaginatedResult<InventorySummary>(
|
||||
items: [summary],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
total: 1,
|
||||
),
|
||||
lastRefreshedAt: summary.lastRefreshedAt,
|
||||
);
|
||||
}
|
||||
|
||||
InventoryDetail _buildDetail() {
|
||||
final warehouse1 = InventoryWarehouse(id: 1, code: 'WH-001', name: '본사');
|
||||
final warehouse2 = InventoryWarehouse(id: 2, code: 'WH-002', name: '보관창고');
|
||||
return InventoryDetail(
|
||||
product: InventoryProduct(
|
||||
id: 10,
|
||||
code: 'INV-10',
|
||||
name: '테스트 장비',
|
||||
vendor: const InventoryVendor(id: 55, name: '테스트 벤더'),
|
||||
),
|
||||
totalQuantity: 120,
|
||||
warehouseBalances: [
|
||||
InventoryWarehouseBalance(warehouse: warehouse1, quantity: 80),
|
||||
InventoryWarehouseBalance(warehouse: warehouse2, quantity: 40),
|
||||
],
|
||||
recentEvents: [
|
||||
InventoryEvent(
|
||||
eventId: 901,
|
||||
eventKind: 'receipt',
|
||||
eventLabel: '입고',
|
||||
deltaQuantity: 20,
|
||||
occurredAt: DateTime.utc(2025, 1, 3, 9, 10),
|
||||
counterparty: const InventoryCounterparty(
|
||||
type: InventoryCounterpartyType.vendor,
|
||||
name: 'QA 파트너',
|
||||
),
|
||||
warehouse: warehouse1,
|
||||
),
|
||||
],
|
||||
updatedAt: DateTime.utc(2025, 1, 3, 9, 20),
|
||||
lastRefreshedAt: DateTime.utc(2025, 1, 3, 9, 5),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(const InventorySummaryFilter());
|
||||
registerFallbackValue(const InventoryDetailFilter());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await GetIt.I.reset();
|
||||
});
|
||||
|
||||
testWidgets('자동 새로고침 토글이 주기적 재조회 동작을 제어한다', (tester) async {
|
||||
final inventoryRepository = _MockInventoryRepository();
|
||||
final warehouseRepository = _MockWarehouseRepository();
|
||||
final summaryResult = _buildSummaryResult();
|
||||
final detail = _buildDetail();
|
||||
var listCallCount = 0;
|
||||
|
||||
when(
|
||||
() => inventoryRepository.listSummaries(filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async {
|
||||
listCallCount += 1;
|
||||
return summaryResult;
|
||||
});
|
||||
when(
|
||||
() =>
|
||||
inventoryRepository.fetchDetail(any(), filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async => detail);
|
||||
_stubWarehouseList(warehouseRepository);
|
||||
_registerDependencies(
|
||||
inventoryRepository: inventoryRepository,
|
||||
warehouseRepository: warehouseRepository,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildApp(
|
||||
InventorySummaryPage(routeUri: Uri(path: '/inventory/summary')),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(listCallCount, 1);
|
||||
expect(find.text('테스트 장비'), findsOneWidget);
|
||||
expect(find.textContaining('마지막 리프레시'), findsOneWidget);
|
||||
expect(find.text('자동 새로고침'), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(seconds: 31));
|
||||
await tester.pump();
|
||||
|
||||
expect(listCallCount, 2);
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('자동 새로고침 전환'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.pump(const Duration(seconds: 31));
|
||||
await tester.pump();
|
||||
|
||||
expect(listCallCount, 2);
|
||||
});
|
||||
|
||||
testWidgets('행을 탭하면 상세 시트에서 창고 차트와 최근 이벤트를 확인할 수 있다', (tester) async {
|
||||
final inventoryRepository = _MockInventoryRepository();
|
||||
final warehouseRepository = _MockWarehouseRepository();
|
||||
final summaryResult = _buildSummaryResult();
|
||||
final detail = _buildDetail();
|
||||
|
||||
when(
|
||||
() => inventoryRepository.listSummaries(filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async => summaryResult);
|
||||
when(
|
||||
() =>
|
||||
inventoryRepository.fetchDetail(any(), filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async => detail);
|
||||
_stubWarehouseList(warehouseRepository);
|
||||
_registerDependencies(
|
||||
inventoryRepository: inventoryRepository,
|
||||
warehouseRepository: warehouseRepository,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildApp(
|
||||
InventorySummaryPage(routeUri: Uri(path: '/inventory/summary')),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('테스트 장비'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('창고 잔량'), findsOneWidget);
|
||||
expect(find.byType(LinearProgressIndicator), findsWidgets);
|
||||
expect(find.text('최근 이벤트'), findsOneWidget);
|
||||
expect(find.textContaining('거래처: QA 파트너'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('권한 오류가 발생하면 경고 배너를 노출한다', (tester) async {
|
||||
final inventoryRepository = _MockInventoryRepository();
|
||||
final warehouseRepository = _MockWarehouseRepository();
|
||||
|
||||
when(
|
||||
() => inventoryRepository.listSummaries(filter: any(named: 'filter')),
|
||||
).thenThrow(Exception('재고 조회 권한이 없습니다.'));
|
||||
_stubWarehouseList(warehouseRepository);
|
||||
_registerDependencies(
|
||||
inventoryRepository: inventoryRepository,
|
||||
warehouseRepository: warehouseRepository,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildApp(
|
||||
InventorySummaryPage(routeUri: Uri(path: '/inventory/summary')),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('재고 조회 권한이 없습니다.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('검색 적용 시 입력값이 필터에 반영된다', (tester) async {
|
||||
final inventoryRepository = _MockInventoryRepository();
|
||||
final warehouseRepository = _MockWarehouseRepository();
|
||||
final summaryResult = _buildSummaryResult();
|
||||
final detail = _buildDetail();
|
||||
final capturedFilters = <InventorySummaryFilter>[];
|
||||
|
||||
when(
|
||||
() => inventoryRepository.listSummaries(filter: any(named: 'filter')),
|
||||
).thenAnswer((invocation) async {
|
||||
final filter =
|
||||
invocation.namedArguments[const Symbol('filter')]
|
||||
as InventorySummaryFilter?;
|
||||
if (filter != null) {
|
||||
capturedFilters.add(filter);
|
||||
}
|
||||
return summaryResult;
|
||||
});
|
||||
when(
|
||||
() =>
|
||||
inventoryRepository.fetchDetail(any(), filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async => detail);
|
||||
_stubWarehouseList(warehouseRepository);
|
||||
_registerDependencies(
|
||||
inventoryRepository: inventoryRepository,
|
||||
warehouseRepository: warehouseRepository,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildApp(
|
||||
InventorySummaryPage(routeUri: Uri(path: '/inventory/summary')),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(capturedFilters, isNotEmpty);
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('inventory_filter_query_field')),
|
||||
'카메라',
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byKey(const Key('inventory_filter_apply')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(capturedFilters.length, greaterThanOrEqualTo(2));
|
||||
final latest = capturedFilters.last;
|
||||
expect(latest.query, '카메라');
|
||||
expect(latest.page, 1);
|
||||
});
|
||||
|
||||
testWidgets('목록이 비어 있으면 안내 문구를 노출한다', (tester) async {
|
||||
final inventoryRepository = _MockInventoryRepository();
|
||||
final warehouseRepository = _MockWarehouseRepository();
|
||||
final emptyResult = InventorySummaryListResult(
|
||||
result: PaginatedResult<InventorySummary>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
total: 0,
|
||||
),
|
||||
lastRefreshedAt: DateTime.utc(2025, 1, 3, 9, 0),
|
||||
);
|
||||
|
||||
when(
|
||||
() => inventoryRepository.listSummaries(filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async => emptyResult);
|
||||
when(
|
||||
() =>
|
||||
inventoryRepository.fetchDetail(any(), filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async => _buildDetail());
|
||||
_stubWarehouseList(warehouseRepository);
|
||||
_registerDependencies(
|
||||
inventoryRepository: inventoryRepository,
|
||||
warehouseRepository: warehouseRepository,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildApp(
|
||||
InventorySummaryPage(routeUri: Uri(path: '/inventory/summary')),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('조건에 맞는 재고 데이터가 없습니다.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('총 수량 헤더를 탭하면 정렬 파라미터가 토글된다', (tester) async {
|
||||
final inventoryRepository = _MockInventoryRepository();
|
||||
final warehouseRepository = _MockWarehouseRepository();
|
||||
final summaryResult = _buildSummaryResult();
|
||||
final detail = _buildDetail();
|
||||
final recordedFilters = <InventorySummaryFilter>[];
|
||||
|
||||
when(
|
||||
() => inventoryRepository.listSummaries(filter: any(named: 'filter')),
|
||||
).thenAnswer((invocation) async {
|
||||
final filter =
|
||||
invocation.namedArguments[const Symbol('filter')]
|
||||
as InventorySummaryFilter?;
|
||||
if (filter != null) {
|
||||
recordedFilters.add(filter);
|
||||
}
|
||||
return summaryResult;
|
||||
});
|
||||
when(
|
||||
() =>
|
||||
inventoryRepository.fetchDetail(any(), filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async => detail);
|
||||
_stubWarehouseList(warehouseRepository);
|
||||
_registerDependencies(
|
||||
inventoryRepository: inventoryRepository,
|
||||
warehouseRepository: warehouseRepository,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildApp(
|
||||
InventorySummaryPage(routeUri: Uri(path: '/inventory/summary')),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(recordedFilters, isNotEmpty);
|
||||
// 첫 정렬: 총 수량 헤더 탭 → 오름차순
|
||||
await tester.tap(find.text('총 수량').first);
|
||||
await tester.pumpAndSettle();
|
||||
final ascFilter = recordedFilters.last;
|
||||
expect(ascFilter.sort, 'total_quantity');
|
||||
expect(ascFilter.order, 'asc');
|
||||
|
||||
// 두 번째 탭 → 내림차순
|
||||
await tester.tap(find.text('총 수량').first);
|
||||
await tester.pumpAndSettle();
|
||||
final descFilter = recordedFilters.last;
|
||||
expect(descFilter.sort, 'total_quantity');
|
||||
expect(descFilter.order, 'desc');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user