마스터 고객/제품/창고 테스트 및 UI 구현
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/masters/customer/domain/entities/customer.dart';
|
||||
import 'package:superport_v2/features/masters/customer/domain/repositories/customer_repository.dart';
|
||||
import 'package:superport_v2/features/masters/customer/presentation/controllers/customer_controller.dart';
|
||||
|
||||
class _MockCustomerRepository extends Mock implements CustomerRepository {}
|
||||
|
||||
class _FakeCustomerInput extends Fake implements CustomerInput {}
|
||||
|
||||
void main() {
|
||||
late CustomerController controller;
|
||||
late _MockCustomerRepository repository;
|
||||
|
||||
final sampleCustomer = Customer(
|
||||
id: 1,
|
||||
customerCode: 'C-001',
|
||||
customerName: '슈퍼포트',
|
||||
isPartner: true,
|
||||
isGeneral: true,
|
||||
);
|
||||
|
||||
PaginatedResult<Customer> createResult({List<Customer>? items}) {
|
||||
final list = items ?? [sampleCustomer];
|
||||
return PaginatedResult<Customer>(
|
||||
items: list,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: list.length,
|
||||
);
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeCustomerInput());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
repository = _MockCustomerRepository();
|
||||
controller = CustomerController(repository: repository);
|
||||
});
|
||||
|
||||
group('fetch', () {
|
||||
test('정상 조회 시 결과 저장', () async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isPartner: any(named: 'isPartner'),
|
||||
isGeneral: any(named: 'isGeneral'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult());
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.result?.items, isNotEmpty);
|
||||
verify(
|
||||
() => repository.list(
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
query: null,
|
||||
isPartner: null,
|
||||
isGeneral: null,
|
||||
isActive: null,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('에러 발생 시 errorMessage 설정', () async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isPartner: any(named: 'isPartner'),
|
||||
isGeneral: any(named: 'isGeneral'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenThrow(Exception('fail'));
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('필터 업데이트 반영', () {
|
||||
controller.updateQuery('abc');
|
||||
controller.updateTypeFilter(CustomerTypeFilter.partner);
|
||||
controller.updateStatusFilter(CustomerStatusFilter.inactiveOnly);
|
||||
|
||||
expect(controller.query, 'abc');
|
||||
expect(controller.typeFilter, CustomerTypeFilter.partner);
|
||||
expect(controller.statusFilter, CustomerStatusFilter.inactiveOnly);
|
||||
});
|
||||
|
||||
group('mutations', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isPartner: any(named: 'isPartner'),
|
||||
isGeneral: any(named: 'isGeneral'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult());
|
||||
});
|
||||
|
||||
final input = CustomerInput(
|
||||
customerCode: 'C-001',
|
||||
customerName: '슈퍼포트',
|
||||
isPartner: true,
|
||||
isGeneral: true,
|
||||
);
|
||||
|
||||
test('create 성공', () async {
|
||||
when(
|
||||
() => repository.create(any()),
|
||||
).thenAnswer((_) async => sampleCustomer);
|
||||
|
||||
final created = await controller.create(input);
|
||||
|
||||
expect(created, isNotNull);
|
||||
verify(() => repository.create(any())).called(1);
|
||||
});
|
||||
|
||||
test('update 성공', () async {
|
||||
when(
|
||||
() => repository.update(any(), any()),
|
||||
).thenAnswer((_) async => sampleCustomer);
|
||||
|
||||
final updated = await controller.update(1, input);
|
||||
|
||||
expect(updated, isNotNull);
|
||||
verify(() => repository.update(1, any())).called(1);
|
||||
});
|
||||
|
||||
test('delete 성공', () async {
|
||||
when(() => repository.delete(any())).thenAnswer((_) async {});
|
||||
|
||||
final success = await controller.delete(1);
|
||||
|
||||
expect(success, isTrue);
|
||||
verify(() => repository.delete(1)).called(1);
|
||||
});
|
||||
|
||||
test('restore 성공', () async {
|
||||
when(
|
||||
() => repository.restore(any()),
|
||||
).thenAnswer((_) async => sampleCustomer);
|
||||
|
||||
final restored = await controller.restore(1);
|
||||
|
||||
expect(restored, isNotNull);
|
||||
verify(() => repository.restore(1)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.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/masters/customer/domain/entities/customer.dart';
|
||||
import 'package:superport_v2/features/masters/customer/domain/repositories/customer_repository.dart';
|
||||
import 'package:superport_v2/features/masters/customer/presentation/pages/customer_page.dart';
|
||||
|
||||
class _MockCustomerRepository extends Mock implements CustomerRepository {}
|
||||
|
||||
class _FakeCustomerInput extends Fake implements CustomerInput {}
|
||||
|
||||
Widget _buildApp(Widget child) {
|
||||
return MaterialApp(
|
||||
home: ShadTheme(
|
||||
data: ShadThemeData(
|
||||
colorScheme: const ShadSlateColorScheme.light(),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
child: Scaffold(body: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeCustomerInput());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await GetIt.I.reset();
|
||||
dotenv.clean();
|
||||
});
|
||||
|
||||
testWidgets('플래그 Off 시 스펙 화면 유지', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_CUSTOMERS_ENABLED=false\n');
|
||||
|
||||
await tester.pumpWidget(_buildApp(const CustomerPage()));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('회사(고객사) 관리'), findsOneWidget);
|
||||
expect(find.text('테이블 리스트'), findsOneWidget);
|
||||
});
|
||||
|
||||
group('플래그 On', () {
|
||||
late _MockCustomerRepository repository;
|
||||
|
||||
setUp(() {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_CUSTOMERS_ENABLED=true\n');
|
||||
repository = _MockCustomerRepository();
|
||||
GetIt.I.registerLazySingleton<CustomerRepository>(() => repository);
|
||||
});
|
||||
|
||||
testWidgets('목록 조회 후 테이블 렌더', (tester) async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isPartner: any(named: 'isPartner'),
|
||||
isGeneral: any(named: 'isGeneral'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Customer>(
|
||||
items: [
|
||||
Customer(
|
||||
id: 1,
|
||||
customerCode: 'C-001',
|
||||
customerName: '슈퍼포트',
|
||||
isPartner: true,
|
||||
isGeneral: false,
|
||||
email: 'partner@superport.com',
|
||||
),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const CustomerPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('C-001'), findsOneWidget);
|
||||
verify(
|
||||
() => repository.list(
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
query: null,
|
||||
isPartner: null,
|
||||
isGeneral: null,
|
||||
isActive: null,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
testWidgets('폼 검증: 필수값 누락 시 경고', (tester) async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isPartner: any(named: 'isPartner'),
|
||||
isGeneral: any(named: 'isGeneral'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Customer>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const CustomerPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('고객사코드를 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('고객사명을 입력하세요.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('신규 등록 성공 시 repository.create 호출', (tester) async {
|
||||
var listCallCount = 0;
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isPartner: any(named: 'isPartner'),
|
||||
isGeneral: any(named: 'isGeneral'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async {
|
||||
listCallCount += 1;
|
||||
if (listCallCount == 1) {
|
||||
return PaginatedResult<Customer>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
);
|
||||
}
|
||||
return PaginatedResult<Customer>(
|
||||
items: [
|
||||
Customer(
|
||||
id: 2,
|
||||
customerCode: 'C-100',
|
||||
customerName: '신규 고객',
|
||||
isPartner: true,
|
||||
isGeneral: true,
|
||||
),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
);
|
||||
});
|
||||
|
||||
CustomerInput? capturedInput;
|
||||
when(() => repository.create(any())).thenAnswer((invocation) async {
|
||||
capturedInput = invocation.positionalArguments.first as CustomerInput;
|
||||
return Customer(
|
||||
id: 2,
|
||||
customerCode: capturedInput!.customerCode,
|
||||
customerName: capturedInput!.customerName,
|
||||
isPartner: capturedInput!.isPartner,
|
||||
isGeneral: capturedInput!.isGeneral,
|
||||
);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(_buildApp(const CustomerPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dialog = find.byType(Dialog);
|
||||
final editableTexts = find.descendant(
|
||||
of: dialog,
|
||||
matching: find.byType(EditableText),
|
||||
);
|
||||
|
||||
await tester.enterText(editableTexts.at(0), 'C-100');
|
||||
await tester.enterText(editableTexts.at(1), '신규 고객');
|
||||
await tester.enterText(editableTexts.at(2), 'new@superport.com');
|
||||
await tester.enterText(editableTexts.at(3), '02-0000-0000');
|
||||
|
||||
// 유형 체크박스: 기본값 partner=false, general=true. partner on 추가
|
||||
await tester.tap(find.text('파트너'));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(capturedInput, isNotNull);
|
||||
expect(capturedInput?.customerCode, 'C-100');
|
||||
expect(find.byType(Dialog), findsNothing);
|
||||
expect(find.text('C-100'), findsOneWidget);
|
||||
verify(() => repository.create(any())).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/masters/product/domain/entities/product.dart';
|
||||
import 'package:superport_v2/features/masters/product/domain/repositories/product_repository.dart';
|
||||
import 'package:superport_v2/features/masters/product/presentation/controllers/product_controller.dart';
|
||||
import 'package:superport_v2/features/masters/uom/domain/entities/uom.dart';
|
||||
import 'package:superport_v2/features/masters/uom/domain/repositories/uom_repository.dart';
|
||||
import 'package:superport_v2/features/masters/vendor/domain/entities/vendor.dart';
|
||||
import 'package:superport_v2/features/masters/vendor/domain/repositories/vendor_repository.dart';
|
||||
|
||||
class _MockProductRepository extends Mock implements ProductRepository {}
|
||||
|
||||
class _MockVendorRepository extends Mock implements VendorRepository {}
|
||||
|
||||
class _MockUomRepository extends Mock implements UomRepository {}
|
||||
|
||||
class _FakeProductInput extends Fake implements ProductInput {}
|
||||
|
||||
void main() {
|
||||
late ProductController controller;
|
||||
late _MockProductRepository productRepository;
|
||||
late _MockVendorRepository vendorRepository;
|
||||
late _MockUomRepository uomRepository;
|
||||
|
||||
final sampleProduct = Product(
|
||||
id: 1,
|
||||
productCode: 'P-001',
|
||||
productName: '테스트 제품',
|
||||
vendor: ProductVendor(id: 10, vendorCode: 'V-10', vendorName: '벤더'),
|
||||
uom: ProductUom(id: 5, uomName: 'EA'),
|
||||
);
|
||||
|
||||
PaginatedResult<Product> createResult({List<Product>? items}) {
|
||||
final list = items ?? [sampleProduct];
|
||||
return PaginatedResult<Product>(
|
||||
items: list,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: list.length,
|
||||
);
|
||||
}
|
||||
|
||||
PaginatedResult<Vendor> createVendorResult() {
|
||||
return PaginatedResult<Vendor>(
|
||||
items: [Vendor(id: 10, vendorCode: 'V-10', vendorName: '벤더')],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
);
|
||||
}
|
||||
|
||||
PaginatedResult<Uom> createUomResult() {
|
||||
return PaginatedResult<Uom>(
|
||||
items: [Uom(id: 5, uomName: 'EA')],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
);
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeProductInput());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
productRepository = _MockProductRepository();
|
||||
vendorRepository = _MockVendorRepository();
|
||||
uomRepository = _MockUomRepository();
|
||||
controller = ProductController(
|
||||
productRepository: productRepository,
|
||||
vendorRepository: vendorRepository,
|
||||
uomRepository: uomRepository,
|
||||
);
|
||||
});
|
||||
|
||||
group('fetch', () {
|
||||
test('정상 조회 시 결과를 저장한다', () async {
|
||||
when(
|
||||
() => productRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
vendorId: any(named: 'vendorId'),
|
||||
uomId: any(named: 'uomId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult());
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.result?.items, isNotEmpty);
|
||||
verify(
|
||||
() => productRepository.list(
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
query: null,
|
||||
vendorId: null,
|
||||
uomId: null,
|
||||
isActive: null,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('에러 발생 시 errorMessage가 설정된다', () async {
|
||||
when(
|
||||
() => productRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
vendorId: any(named: 'vendorId'),
|
||||
uomId: any(named: 'uomId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenThrow(Exception('fail'));
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('loadLookups 호출 시 벤더/단위 목록을 설정한다', () async {
|
||||
when(
|
||||
() => vendorRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createVendorResult());
|
||||
when(
|
||||
() => uomRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createUomResult());
|
||||
|
||||
await controller.loadLookups();
|
||||
|
||||
expect(controller.vendorOptions, isNotEmpty);
|
||||
expect(controller.uomOptions, isNotEmpty);
|
||||
});
|
||||
|
||||
test('쿼리/필터 업데이트 시 상태 반영', () {
|
||||
controller.updateQuery('abc');
|
||||
controller.updateVendorFilter(1);
|
||||
controller.updateUomFilter(2);
|
||||
controller.updateStatusFilter(ProductStatusFilter.activeOnly);
|
||||
|
||||
expect(controller.query, 'abc');
|
||||
expect(controller.vendorFilter, 1);
|
||||
expect(controller.uomFilter, 2);
|
||||
expect(controller.statusFilter, ProductStatusFilter.activeOnly);
|
||||
});
|
||||
|
||||
group('mutations', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => productRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
vendorId: any(named: 'vendorId'),
|
||||
uomId: any(named: 'uomId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult());
|
||||
});
|
||||
|
||||
final input = ProductInput(
|
||||
productCode: 'P-001',
|
||||
productName: '테스트 제품',
|
||||
vendorId: 10,
|
||||
uomId: 5,
|
||||
);
|
||||
|
||||
test('create 성공 시 목록을 갱신한다', () async {
|
||||
when(
|
||||
() => productRepository.create(any()),
|
||||
).thenAnswer((_) async => sampleProduct);
|
||||
|
||||
final created = await controller.create(input);
|
||||
|
||||
expect(created, isNotNull);
|
||||
verify(() => productRepository.create(any())).called(1);
|
||||
});
|
||||
|
||||
test('update 성공 시 현재 페이지 유지하며 갱신한다', () async {
|
||||
when(
|
||||
() => productRepository.update(any(), any()),
|
||||
).thenAnswer((_) async => sampleProduct);
|
||||
|
||||
final updated = await controller.update(1, input);
|
||||
|
||||
expect(updated, isNotNull);
|
||||
verify(() => productRepository.update(1, any())).called(1);
|
||||
});
|
||||
|
||||
test('delete 성공 시 true 반환 및 갱신', () async {
|
||||
when(() => productRepository.delete(any())).thenAnswer((_) async {});
|
||||
|
||||
final success = await controller.delete(1);
|
||||
|
||||
expect(success, isTrue);
|
||||
verify(() => productRepository.delete(1)).called(1);
|
||||
});
|
||||
|
||||
test('restore 성공 시 갱신', () async {
|
||||
when(
|
||||
() => productRepository.restore(any()),
|
||||
).thenAnswer((_) async => sampleProduct);
|
||||
|
||||
final restored = await controller.restore(1);
|
||||
|
||||
expect(restored, isNotNull);
|
||||
verify(() => productRepository.restore(1)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.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/masters/product/domain/entities/product.dart';
|
||||
import 'package:superport_v2/features/masters/product/domain/repositories/product_repository.dart';
|
||||
import 'package:superport_v2/features/masters/product/presentation/pages/product_page.dart';
|
||||
import 'package:superport_v2/features/masters/uom/domain/entities/uom.dart';
|
||||
import 'package:superport_v2/features/masters/uom/domain/repositories/uom_repository.dart';
|
||||
import 'package:superport_v2/features/masters/vendor/domain/entities/vendor.dart';
|
||||
import 'package:superport_v2/features/masters/vendor/domain/repositories/vendor_repository.dart';
|
||||
|
||||
class _MockProductRepository extends Mock implements ProductRepository {}
|
||||
|
||||
class _MockVendorRepository extends Mock implements VendorRepository {}
|
||||
|
||||
class _MockUomRepository extends Mock implements UomRepository {}
|
||||
|
||||
class _FakeProductInput extends Fake implements ProductInput {}
|
||||
|
||||
Widget _buildApp(Widget child) {
|
||||
return MaterialApp(
|
||||
home: ShadTheme(
|
||||
data: ShadThemeData(
|
||||
colorScheme: const ShadSlateColorScheme.light(),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
child: Scaffold(body: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeProductInput());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await GetIt.I.reset();
|
||||
dotenv.clean();
|
||||
});
|
||||
|
||||
testWidgets('플래그 Off 시 스펙 문서 화면을 노출한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_PRODUCTS_ENABLED=false\n');
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ProductPage()));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('장비 모델(제품) 관리'), findsOneWidget);
|
||||
expect(find.text('테이블 리스트'), findsOneWidget);
|
||||
});
|
||||
|
||||
group('플래그 On 환경', () {
|
||||
late _MockProductRepository productRepository;
|
||||
late _MockVendorRepository vendorRepository;
|
||||
late _MockUomRepository uomRepository;
|
||||
|
||||
setUp(() {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_PRODUCTS_ENABLED=true\n');
|
||||
productRepository = _MockProductRepository();
|
||||
vendorRepository = _MockVendorRepository();
|
||||
uomRepository = _MockUomRepository();
|
||||
GetIt.I.registerLazySingleton<ProductRepository>(() => productRepository);
|
||||
GetIt.I.registerLazySingleton<VendorRepository>(() => vendorRepository);
|
||||
GetIt.I.registerLazySingleton<UomRepository>(() => uomRepository);
|
||||
|
||||
when(
|
||||
() => vendorRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Vendor>(
|
||||
items: [Vendor(id: 1, vendorCode: 'V-001', vendorName: '슈퍼벤더')],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
when(
|
||||
() => uomRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Uom>(
|
||||
items: [Uom(id: 5, uomName: 'EA')],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('목록 조회 후 테이블에 표시한다', (tester) async {
|
||||
when(
|
||||
() => productRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
vendorId: any(named: 'vendorId'),
|
||||
uomId: any(named: 'uomId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Product>(
|
||||
items: [
|
||||
Product(
|
||||
id: 1,
|
||||
productCode: 'P-001',
|
||||
productName: '테스트 제품',
|
||||
vendor: ProductVendor(
|
||||
id: 1,
|
||||
vendorCode: 'V-001',
|
||||
vendorName: '슈퍼벤더',
|
||||
),
|
||||
uom: ProductUom(id: 5, uomName: 'EA'),
|
||||
),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ProductPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('P-001'), findsOneWidget);
|
||||
verify(
|
||||
() => productRepository.list(
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
query: null,
|
||||
vendorId: null,
|
||||
uomId: null,
|
||||
isActive: null,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
testWidgets('폼 검증: 필수값 미입력 시 에러 메시지를 표시한다', (tester) async {
|
||||
when(
|
||||
() => productRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
vendorId: any(named: 'vendorId'),
|
||||
uomId: any(named: 'uomId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Product>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ProductPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('제품코드를 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('제품명을 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('제조사를 선택하세요.'), findsOneWidget);
|
||||
expect(find.text('단위를 선택하세요.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('신규 등록 성공 시 repository.create 호출', (tester) async {
|
||||
var listCallCount = 0;
|
||||
when(
|
||||
() => productRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
vendorId: any(named: 'vendorId'),
|
||||
uomId: any(named: 'uomId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async {
|
||||
listCallCount += 1;
|
||||
if (listCallCount == 1) {
|
||||
return PaginatedResult<Product>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
);
|
||||
}
|
||||
return PaginatedResult<Product>(
|
||||
items: [
|
||||
Product(
|
||||
id: 99,
|
||||
productCode: 'NP-001',
|
||||
productName: '신규 제품',
|
||||
vendor: ProductVendor(
|
||||
id: 1,
|
||||
vendorCode: 'V-001',
|
||||
vendorName: '슈퍼벤더',
|
||||
),
|
||||
uom: ProductUom(id: 5, uomName: 'EA'),
|
||||
),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
);
|
||||
});
|
||||
|
||||
ProductInput? capturedInput;
|
||||
when(() => productRepository.create(any())).thenAnswer((
|
||||
invocation,
|
||||
) async {
|
||||
capturedInput = invocation.positionalArguments.first as ProductInput;
|
||||
return Product(
|
||||
id: 99,
|
||||
productCode: capturedInput!.productCode,
|
||||
productName: capturedInput!.productName,
|
||||
vendor: ProductVendor(
|
||||
id: capturedInput!.vendorId,
|
||||
vendorCode: 'V',
|
||||
vendorName: '슈퍼벤더',
|
||||
),
|
||||
uom: ProductUom(id: capturedInput!.uomId, uomName: 'EA'),
|
||||
);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ProductPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dialog = find.byType(Dialog);
|
||||
final editableTexts = find.descendant(
|
||||
of: dialog,
|
||||
matching: find.byType(EditableText),
|
||||
);
|
||||
|
||||
await tester.enterText(editableTexts.at(0), 'NP-001');
|
||||
await tester.enterText(editableTexts.at(1), '신규 제품');
|
||||
|
||||
await tester.tap(find.text('제조사를 선택하세요'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('슈퍼벤더'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('단위를 선택하세요'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('EA'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(capturedInput, isNotNull);
|
||||
expect(capturedInput?.productCode, 'NP-001');
|
||||
expect(find.byType(Dialog), findsNothing);
|
||||
expect(find.text('NP-001'), findsOneWidget);
|
||||
verify(() => productRepository.create(any())).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
156
test/features/masters/vendor/presentation/controllers/vendor_controller_test.dart
vendored
Normal file
156
test/features/masters/vendor/presentation/controllers/vendor_controller_test.dart
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/masters/vendor/domain/entities/vendor.dart';
|
||||
import 'package:superport_v2/features/masters/vendor/domain/repositories/vendor_repository.dart';
|
||||
import 'package:superport_v2/features/masters/vendor/presentation/controllers/vendor_controller.dart';
|
||||
|
||||
class _MockVendorRepository extends Mock implements VendorRepository {}
|
||||
|
||||
class _FakeVendorInput extends Fake implements VendorInput {}
|
||||
|
||||
void main() {
|
||||
late VendorController controller;
|
||||
late _MockVendorRepository repository;
|
||||
|
||||
final sampleVendor = Vendor(
|
||||
id: 1,
|
||||
vendorCode: 'V-001',
|
||||
vendorName: '슈퍼벤더',
|
||||
isActive: true,
|
||||
isDeleted: false,
|
||||
);
|
||||
|
||||
PaginatedResult<Vendor> createResult({List<Vendor>? items}) {
|
||||
final list = items ?? [sampleVendor];
|
||||
return PaginatedResult<Vendor>(
|
||||
items: list,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: list.length,
|
||||
);
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeVendorInput());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
repository = _MockVendorRepository();
|
||||
controller = VendorController(repository: repository);
|
||||
});
|
||||
|
||||
group('fetch', () {
|
||||
test('정상 조회 시 결과가 저장된다', () async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult());
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.isLoading, isFalse);
|
||||
expect(controller.errorMessage, isNull);
|
||||
expect(controller.result?.items, isNotEmpty);
|
||||
verify(
|
||||
() =>
|
||||
repository.list(page: 1, pageSize: 20, query: null, isActive: null),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('에러 발생 시 errorMessage가 세팅된다', () async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenThrow(Exception('fail'));
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.isLoading, isFalse);
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('updateQuery / updateStatusFilter 변경 시 상태 반영', () {
|
||||
controller.updateQuery('abc');
|
||||
controller.updateStatusFilter(VendorStatusFilter.activeOnly);
|
||||
|
||||
expect(controller.query, 'abc');
|
||||
expect(controller.statusFilter, VendorStatusFilter.activeOnly);
|
||||
});
|
||||
|
||||
group('create/update/delete/restore', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult());
|
||||
});
|
||||
|
||||
test('create 성공 시 목록을 새로고침한다', () async {
|
||||
when(
|
||||
() => repository.create(any()),
|
||||
).thenAnswer((_) async => sampleVendor);
|
||||
|
||||
final result = await controller.create(
|
||||
VendorInput(vendorCode: 'V-001', vendorName: '슈퍼벤더'),
|
||||
);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(controller.result?.items.length, 1);
|
||||
verify(() => repository.create(any())).called(1);
|
||||
verify(
|
||||
() =>
|
||||
repository.list(page: 1, pageSize: 20, query: null, isActive: null),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('update 성공 시 현재 페이지를 유지하며 갱신한다', () async {
|
||||
when(
|
||||
() => repository.update(any(), any()),
|
||||
).thenAnswer((_) async => sampleVendor);
|
||||
|
||||
final result = await controller.update(
|
||||
1,
|
||||
VendorInput(vendorCode: 'V-001', vendorName: '슈퍼벤더'),
|
||||
);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(controller.result?.items, isNotEmpty);
|
||||
verify(() => repository.update(1, any())).called(1);
|
||||
});
|
||||
|
||||
test('delete 성공 시 fetch가 재호출된다', () async {
|
||||
when(() => repository.delete(any())).thenAnswer((_) async {});
|
||||
|
||||
final result = await controller.delete(1);
|
||||
|
||||
expect(result, isTrue);
|
||||
verify(() => repository.delete(1)).called(1);
|
||||
});
|
||||
|
||||
test('restore 성공 시 fetch가 재호출된다', () async {
|
||||
when(
|
||||
() => repository.restore(any()),
|
||||
).thenAnswer((_) async => sampleVendor);
|
||||
|
||||
final restored = await controller.restore(1);
|
||||
|
||||
expect(restored, isNotNull);
|
||||
verify(() => repository.restore(1)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
182
test/features/masters/vendor/presentation/pages/vendor_page_test.dart
vendored
Normal file
182
test/features/masters/vendor/presentation/pages/vendor_page_test.dart
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.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/masters/vendor/domain/entities/vendor.dart';
|
||||
import 'package:superport_v2/features/masters/vendor/domain/repositories/vendor_repository.dart';
|
||||
import 'package:superport_v2/features/masters/vendor/presentation/pages/vendor_page.dart';
|
||||
|
||||
class _MockVendorRepository extends Mock implements VendorRepository {}
|
||||
|
||||
class _FakeVendorInput extends Fake implements VendorInput {}
|
||||
|
||||
Widget _buildApp(Widget child) {
|
||||
return MaterialApp(
|
||||
home: ShadTheme(
|
||||
data: ShadThemeData(
|
||||
colorScheme: const ShadSlateColorScheme.light(),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
child: Scaffold(body: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeVendorInput());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await GetIt.I.reset();
|
||||
dotenv.clean();
|
||||
});
|
||||
|
||||
testWidgets('FEATURE_VENDORS_ENABLED=false 이면 스펙 페이지를 노출한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_VENDORS_ENABLED=false\n');
|
||||
|
||||
await tester.pumpWidget(_buildApp(const VendorPage()));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('제조사(벤더) 관리'), findsOneWidget);
|
||||
expect(find.text('비활성화 (백엔드 준비 중)'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('기능 플래그 on 시 목록을 조회하여 표에 렌더링한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_VENDORS_ENABLED=true\n');
|
||||
final repository = _MockVendorRepository();
|
||||
GetIt.I.registerLazySingleton<VendorRepository>(() => repository);
|
||||
|
||||
final vendor = Vendor(id: 1, vendorCode: 'V-001', vendorName: '슈퍼벤더');
|
||||
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Vendor>(
|
||||
items: [vendor],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const VendorPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('V-001'), findsOneWidget);
|
||||
verify(
|
||||
() => repository.list(page: 1, pageSize: 20, query: null, isActive: null),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
testWidgets('신규 등록 폼에서 필수값 미입력 시 검증 메시지를 보여준다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_VENDORS_ENABLED=true\n');
|
||||
final repository = _MockVendorRepository();
|
||||
GetIt.I.registerLazySingleton<VendorRepository>(() => repository);
|
||||
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Vendor>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const VendorPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('벤더코드를 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('벤더명을 입력하세요.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('신규 등록 성공 시 repository.create가 호출된다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_VENDORS_ENABLED=true\n');
|
||||
final repository = _MockVendorRepository();
|
||||
GetIt.I.registerLazySingleton<VendorRepository>(() => repository);
|
||||
|
||||
var listCallCount = 0;
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async {
|
||||
listCallCount += 1;
|
||||
if (listCallCount == 1) {
|
||||
return PaginatedResult<Vendor>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
);
|
||||
}
|
||||
return PaginatedResult<Vendor>(
|
||||
items: [Vendor(id: 99, vendorCode: 'NV-001', vendorName: '신규벤더')],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
);
|
||||
});
|
||||
|
||||
VendorInput? capturedInput;
|
||||
when(() => repository.create(any())).thenAnswer((invocation) async {
|
||||
capturedInput = invocation.positionalArguments.first as VendorInput;
|
||||
return Vendor(
|
||||
id: 99,
|
||||
vendorCode: capturedInput!.vendorCode,
|
||||
vendorName: capturedInput!.vendorName,
|
||||
);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(_buildApp(const VendorPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dialog = find.byType(Dialog);
|
||||
final editableTexts = find.descendant(
|
||||
of: dialog,
|
||||
matching: find.byType(EditableText),
|
||||
);
|
||||
|
||||
await tester.enterText(editableTexts.at(0), 'NV-001');
|
||||
await tester.enterText(editableTexts.at(1), '신규벤더');
|
||||
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(capturedInput, isNotNull);
|
||||
expect(capturedInput?.vendorCode, 'NV-001');
|
||||
expect(find.byType(Dialog), findsNothing);
|
||||
expect(find.text('NV-001'), findsOneWidget);
|
||||
verify(() => repository.create(any())).called(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/entities/warehouse.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/repositories/warehouse_repository.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/presentation/controllers/warehouse_controller.dart';
|
||||
|
||||
class _MockWarehouseRepository extends Mock implements WarehouseRepository {}
|
||||
|
||||
class _FakeWarehouseInput extends Fake implements WarehouseInput {}
|
||||
|
||||
void main() {
|
||||
late WarehouseController controller;
|
||||
late _MockWarehouseRepository repository;
|
||||
|
||||
final sampleWarehouse = Warehouse(
|
||||
id: 1,
|
||||
warehouseCode: 'WH-001',
|
||||
warehouseName: '테스트 창고',
|
||||
zipcode: WarehouseZipcode(zipcode: '06000'),
|
||||
);
|
||||
|
||||
PaginatedResult<Warehouse> createResult({List<Warehouse>? items}) {
|
||||
final list = items ?? [sampleWarehouse];
|
||||
return PaginatedResult<Warehouse>(
|
||||
items: list,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: list.length,
|
||||
);
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeWarehouseInput());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
repository = _MockWarehouseRepository();
|
||||
controller = WarehouseController(repository: repository);
|
||||
});
|
||||
|
||||
group('fetch', () {
|
||||
test('정상 조회 시 결과 저장', () async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult());
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.result?.items, isNotEmpty);
|
||||
verify(
|
||||
() =>
|
||||
repository.list(page: 1, pageSize: 20, query: null, isActive: null),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('에러 발생 시 errorMessage 설정', () async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenThrow(Exception('fail'));
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('쿼리/필터 업데이트 반영', () {
|
||||
controller.updateQuery('abc');
|
||||
controller.updateStatusFilter(WarehouseStatusFilter.inactiveOnly);
|
||||
|
||||
expect(controller.query, 'abc');
|
||||
expect(controller.statusFilter, WarehouseStatusFilter.inactiveOnly);
|
||||
});
|
||||
|
||||
group('mutations', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult());
|
||||
});
|
||||
|
||||
final input = WarehouseInput(
|
||||
warehouseCode: 'WH-001',
|
||||
warehouseName: '테스트 창고',
|
||||
);
|
||||
|
||||
test('create 성공', () async {
|
||||
when(
|
||||
() => repository.create(any()),
|
||||
).thenAnswer((_) async => sampleWarehouse);
|
||||
|
||||
final created = await controller.create(input);
|
||||
|
||||
expect(created, isNotNull);
|
||||
verify(() => repository.create(any())).called(1);
|
||||
});
|
||||
|
||||
test('update 성공', () async {
|
||||
when(
|
||||
() => repository.update(any(), any()),
|
||||
).thenAnswer((_) async => sampleWarehouse);
|
||||
|
||||
final updated = await controller.update(1, input);
|
||||
|
||||
expect(updated, isNotNull);
|
||||
verify(() => repository.update(1, any())).called(1);
|
||||
});
|
||||
|
||||
test('delete 성공', () async {
|
||||
when(() => repository.delete(any())).thenAnswer((_) async {});
|
||||
|
||||
final success = await controller.delete(1);
|
||||
|
||||
expect(success, isTrue);
|
||||
verify(() => repository.delete(1)).called(1);
|
||||
});
|
||||
|
||||
test('restore 성공', () async {
|
||||
when(
|
||||
() => repository.restore(any()),
|
||||
).thenAnswer((_) async => sampleWarehouse);
|
||||
|
||||
final restored = await controller.restore(1);
|
||||
|
||||
expect(restored, isNotNull);
|
||||
verify(() => repository.restore(1)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.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/masters/warehouse/domain/entities/warehouse.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/repositories/warehouse_repository.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/presentation/pages/warehouse_page.dart';
|
||||
|
||||
class _MockWarehouseRepository extends Mock implements WarehouseRepository {}
|
||||
|
||||
class _FakeWarehouseInput extends Fake implements WarehouseInput {}
|
||||
|
||||
Widget _buildApp(Widget child) {
|
||||
return MaterialApp(
|
||||
home: ShadTheme(
|
||||
data: ShadThemeData(
|
||||
colorScheme: const ShadSlateColorScheme.light(),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
child: Scaffold(body: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeWarehouseInput());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await GetIt.I.reset();
|
||||
dotenv.clean();
|
||||
});
|
||||
|
||||
testWidgets('플래그 Off 시 스펙 화면', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_WAREHOUSES_ENABLED=false\n');
|
||||
|
||||
await tester.pumpWidget(_buildApp(const WarehousePage()));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('입고지(창고) 관리'), findsOneWidget);
|
||||
expect(find.text('테이블 리스트'), findsOneWidget);
|
||||
});
|
||||
|
||||
group('플래그 On', () {
|
||||
late _MockWarehouseRepository repository;
|
||||
|
||||
setUp(() {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_WAREHOUSES_ENABLED=true\n');
|
||||
repository = _MockWarehouseRepository();
|
||||
GetIt.I.registerLazySingleton<WarehouseRepository>(() => repository);
|
||||
});
|
||||
|
||||
testWidgets('목록 조회 후 테이블 표시', (tester) async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Warehouse>(
|
||||
items: [
|
||||
Warehouse(
|
||||
id: 1,
|
||||
warehouseCode: 'WH-001',
|
||||
warehouseName: '서울 창고',
|
||||
zipcode: WarehouseZipcode(zipcode: '06000'),
|
||||
),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const WarehousePage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('WH-001'), findsOneWidget);
|
||||
verify(
|
||||
() =>
|
||||
repository.list(page: 1, pageSize: 20, query: null, isActive: null),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
testWidgets('폼 검증: 필수값 누락', (tester) async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Warehouse>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const WarehousePage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('창고코드를 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('창고명을 입력하세요.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('신규 등록 성공', (tester) async {
|
||||
var listCallCount = 0;
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async {
|
||||
listCallCount += 1;
|
||||
if (listCallCount == 1) {
|
||||
return PaginatedResult<Warehouse>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
);
|
||||
}
|
||||
return PaginatedResult<Warehouse>(
|
||||
items: [
|
||||
Warehouse(
|
||||
id: 7,
|
||||
warehouseCode: 'WH-100',
|
||||
warehouseName: '신규 창고',
|
||||
zipcode: WarehouseZipcode(zipcode: '12345'),
|
||||
),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
);
|
||||
});
|
||||
|
||||
WarehouseInput? capturedInput;
|
||||
when(() => repository.create(any())).thenAnswer((invocation) async {
|
||||
capturedInput = invocation.positionalArguments.first as WarehouseInput;
|
||||
return Warehouse(
|
||||
id: 7,
|
||||
warehouseCode: capturedInput!.warehouseCode,
|
||||
warehouseName: capturedInput!.warehouseName,
|
||||
);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(_buildApp(const WarehousePage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final fields = find.descendant(
|
||||
of: find.byType(Dialog),
|
||||
matching: find.byType(EditableText),
|
||||
);
|
||||
|
||||
await tester.enterText(fields.at(0), 'WH-100');
|
||||
await tester.enterText(fields.at(1), '신규 창고');
|
||||
await tester.enterText(fields.at(2), '12345');
|
||||
await tester.enterText(fields.at(3), '주소');
|
||||
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(capturedInput, isNotNull);
|
||||
expect(capturedInput?.warehouseCode, 'WH-100');
|
||||
expect(find.byType(Dialog), findsNothing);
|
||||
expect(find.text('WH-100'), findsOneWidget);
|
||||
verify(() => repository.create(any())).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user