전역 구조 리팩터링 및 테스트 확장

This commit is contained in:
JiWoong Sul
2025-09-29 01:51:47 +09:00
parent c00c0c9ab2
commit fef7108479
70 changed files with 7709 additions and 3185 deletions

View File

@@ -55,6 +55,5 @@ void main() {
repository = _MockApprovalRepository();
GetIt.I.registerLazySingleton<ApprovalRepository>(() => repository);
});
});
}

View File

@@ -138,6 +138,48 @@ void main() {
expect(find.text('고객사명을 입력하세요.'), findsOneWidget);
});
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(CustomerPage(routeUri: Uri(path: '/masters/customers'))),
);
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), 'C-200');
await tester.enterText(fields.at(1), '검색 필요 고객');
await tester.enterText(fields.at(4), '06000');
await tester.tap(find.text('등록'));
await tester.pump();
expect(find.text('우편번호 검색으로 주소를 선택하세요.'), findsOneWidget);
});
testWidgets('신규 등록 성공 시 repository.create 호출', (tester) async {
var listCallCount = 0;
when(

View File

@@ -139,7 +139,7 @@ void main() {
await tester.pumpWidget(_buildApp(const GroupPermissionPage()));
await tester.pumpAndSettle();
expect(find.text('대시보드'), findsOneWidget);
expect(find.text('대시보드'), findsWidgets);
expect(find.text('관리자'), findsOneWidget);
});
@@ -278,7 +278,7 @@ void main() {
expect(capturedInput?.canCreate, isTrue);
expect(capturedInput?.canUpdate, isTrue);
expect(find.byType(Dialog), findsNothing);
expect(find.text('대시보드'), findsOneWidget);
expect(find.text('대시보드'), findsWidgets);
verify(() => permissionRepository.create(any())).called(1);
});
});

View File

@@ -49,7 +49,9 @@ void main() {
testWidgets('플래그 Off 시 스펙 문서 화면을 노출한다', (tester) async {
dotenv.testLoad(fileInput: 'FEATURE_PRODUCTS_ENABLED=false\n');
await tester.pumpWidget(_buildApp(const ProductPage()));
await tester.pumpWidget(
_buildApp(ProductPage(routeUri: Uri(path: '/masters/products'))),
);
await tester.pump();
expect(find.text('장비 모델(제품) 관리'), findsOneWidget);
@@ -134,7 +136,9 @@ void main() {
),
);
await tester.pumpWidget(_buildApp(const ProductPage()));
await tester.pumpWidget(
_buildApp(ProductPage(routeUri: Uri(path: '/masters/products'))),
);
await tester.pumpAndSettle();
expect(find.text('P-001'), findsOneWidget);
@@ -169,7 +173,9 @@ void main() {
),
);
await tester.pumpWidget(_buildApp(const ProductPage()));
await tester.pumpWidget(
_buildApp(ProductPage(routeUri: Uri(path: '/masters/products'))),
);
await tester.pumpAndSettle();
await tester.tap(find.text('신규 등록'));
@@ -243,7 +249,9 @@ void main() {
);
});
await tester.pumpWidget(_buildApp(const ProductPage()));
await tester.pumpWidget(
_buildApp(ProductPage(routeUri: Uri(path: '/masters/products'))),
);
await tester.pumpAndSettle();
await tester.tap(find.text('신규 등록'));

View File

@@ -55,7 +55,6 @@ void main() {
group('플래그 On', () {
late _MockUserRepository userRepository;
late _MockGroupRepository groupRepository;
setUp(() {
dotenv.testLoad(fileInput: 'FEATURE_USERS_ENABLED=true\n');
userRepository = _MockUserRepository();
@@ -153,6 +152,14 @@ void main() {
});
testWidgets('신규 등록 성공', (tester) async {
final view = tester.view;
view.physicalSize = const Size(1280, 800);
view.devicePixelRatio = 1.0;
addTearDown(() {
view.resetPhysicalSize();
view.resetDevicePixelRatio();
});
var listCallCount = 0;
when(
() => userRepository.list(
@@ -214,9 +221,21 @@ void main() {
await tester.enterText(editableTexts.at(0), 'A010');
await tester.enterText(editableTexts.at(1), '신규 사용자');
await tester.tap(find.text('그룹을 선택하세요'));
await tester.pumpAndSettle();
await tester.tap(find.text('관리자'));
final selectFinder = find.descendant(
of: dialog,
matching: find.byType(ShadSelect<int?>),
);
final selectElement = tester.element(selectFinder);
final renderBox = selectElement.renderObject as RenderBox;
final globalCenter = renderBox.localToGlobal(
renderBox.size.center(Offset.zero),
);
await tester.tapAt(globalCenter);
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
final adminOption = find.text('관리자', skipOffstage: false);
expect(adminOption, findsWidgets);
await tester.tap(adminOption.first, warnIfMissed: false);
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));

View File

@@ -41,7 +41,9 @@ void main() {
testWidgets('FEATURE_VENDORS_ENABLED=false 이면 스펙 페이지를 노출한다', (tester) async {
dotenv.testLoad(fileInput: 'FEATURE_VENDORS_ENABLED=false\n');
await tester.pumpWidget(_buildApp(const VendorPage()));
await tester.pumpWidget(
_buildApp(VendorPage(routeUri: Uri(path: '/masters/vendors'))),
);
await tester.pump();
expect(find.text('제조사(벤더) 관리'), findsOneWidget);
@@ -71,7 +73,9 @@ void main() {
),
);
await tester.pumpWidget(_buildApp(const VendorPage()));
await tester.pumpWidget(
_buildApp(VendorPage(routeUri: Uri(path: '/masters/vendors'))),
);
await tester.pumpAndSettle();
expect(find.text('V-001'), findsOneWidget);
@@ -101,7 +105,9 @@ void main() {
),
);
await tester.pumpWidget(_buildApp(const VendorPage()));
await tester.pumpWidget(
_buildApp(VendorPage(routeUri: Uri(path: '/masters/vendors'))),
);
await tester.pumpAndSettle();
await tester.tap(find.text('신규 등록'));
@@ -155,7 +161,9 @@ void main() {
);
});
await tester.pumpWidget(_buildApp(const VendorPage()));
await tester.pumpWidget(
_buildApp(VendorPage(routeUri: Uri(path: '/masters/vendors'))),
);
await tester.pumpAndSettle();
await tester.tap(find.text('신규 등록'));

View File

@@ -9,11 +9,16 @@ 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';
import 'package:superport_v2/features/util/postal_search/domain/entities/postal_code.dart';
import 'package:superport_v2/features/util/postal_search/domain/repositories/postal_search_repository.dart';
class _MockWarehouseRepository extends Mock implements WarehouseRepository {}
class _FakeWarehouseInput extends Fake implements WarehouseInput {}
class _MockPostalSearchRepository extends Mock
implements PostalSearchRepository {}
Widget _buildApp(Widget child) {
return MaterialApp(
home: ShadTheme(
@@ -41,7 +46,9 @@ void main() {
testWidgets('플래그 Off 시 스펙 화면', (tester) async {
dotenv.testLoad(fileInput: 'FEATURE_WAREHOUSES_ENABLED=false\n');
await tester.pumpWidget(_buildApp(const WarehousePage()));
await tester.pumpWidget(
_buildApp(WarehousePage(routeUri: Uri(path: '/masters/warehouses'))),
);
await tester.pump();
expect(find.text('입고지(창고) 관리'), findsOneWidget);
@@ -50,11 +57,32 @@ void main() {
group('플래그 On', () {
late _MockWarehouseRepository repository;
late _MockPostalSearchRepository postalRepository;
setUp(() {
dotenv.testLoad(fileInput: 'FEATURE_WAREHOUSES_ENABLED=true\n');
repository = _MockWarehouseRepository();
postalRepository = _MockPostalSearchRepository();
GetIt.I.registerLazySingleton<WarehouseRepository>(() => repository);
GetIt.I.registerLazySingleton<PostalSearchRepository>(
() => postalRepository,
);
when(
() => postalRepository.search(
keyword: any(named: 'keyword'),
limit: any(named: 'limit'),
),
).thenAnswer(
(_) async => [
PostalCode(
zipcode: '06000',
sido: '서울특별시',
sigungu: '강남구',
roadName: '테헤란로',
buildingMainNo: 100,
),
],
);
});
testWidgets('목록 조회 후 테이블 표시', (tester) async {
@@ -81,7 +109,9 @@ void main() {
),
);
await tester.pumpWidget(_buildApp(const WarehousePage()));
await tester.pumpWidget(
_buildApp(WarehousePage(routeUri: Uri(path: '/masters/warehouses'))),
);
await tester.pumpAndSettle();
expect(find.text('WH-001'), findsOneWidget);
@@ -108,7 +138,9 @@ void main() {
),
);
await tester.pumpWidget(_buildApp(const WarehousePage()));
await tester.pumpWidget(
_buildApp(WarehousePage(routeUri: Uri(path: '/masters/warehouses'))),
);
await tester.pumpAndSettle();
await tester.tap(find.text('신규 등록'));
@@ -164,7 +196,9 @@ void main() {
);
});
await tester.pumpWidget(_buildApp(const WarehousePage()));
await tester.pumpWidget(
_buildApp(WarehousePage(routeUri: Uri(path: '/masters/warehouses'))),
);
await tester.pumpAndSettle();
await tester.tap(find.text('신규 등록'));
@@ -177,14 +211,31 @@ void main() {
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.enterText(fields.at(2), '06000');
await tester.tap(
find.descendant(
of: find.byType(Dialog),
matching: find.widgetWithText(ShadButton, '검색'),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('06000').last);
await tester.pumpAndSettle();
final updatedFields = find.descendant(
of: find.byType(Dialog),
matching: find.byType(EditableText),
);
await tester.enterText(updatedFields.at(3), '주소');
await tester.tap(find.text('등록'));
await tester.pumpAndSettle();
expect(capturedInput, isNotNull);
expect(capturedInput?.warehouseCode, 'WH-100');
expect(capturedInput?.zipcode, '06000');
expect(find.byType(Dialog), findsNothing);
expect(find.text('WH-100'), findsOneWidget);
verify(() => repository.create(any())).called(1);

View File

@@ -0,0 +1,154 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport_v2/features/util/postal_search/presentation/models/postal_search_result.dart';
import 'package:superport_v2/features/util/postal_search/presentation/widgets/postal_search_dialog.dart';
class _PostalSearchHarness extends StatefulWidget {
const _PostalSearchHarness({required this.fetcher, this.initialKeyword});
final PostalSearchFetcher fetcher;
final String? initialKeyword;
@override
State<_PostalSearchHarness> createState() => _PostalSearchHarnessState();
}
class _PostalSearchHarnessState extends State<_PostalSearchHarness> {
PostalSearchResult? _selection;
bool _dialogScheduled = false;
void _ensureDialogShown(BuildContext innerContext) {
if (_dialogScheduled) {
return;
}
_dialogScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) async {
final result = await showPostalSearchDialog(
innerContext,
fetcher: widget.fetcher,
initialKeyword: widget.initialKeyword,
);
if (!mounted) {
return;
}
setState(() {
_selection = result;
});
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(
builder: (innerContext) {
return ShadTheme(
data: ShadThemeData(
colorScheme: const ShadSlateColorScheme.light(),
brightness: Brightness.light,
),
child: Builder(
builder: (themeContext) {
_ensureDialogShown(themeContext);
return Scaffold(
body: Center(
child: Text(
_selection?.zipcode ?? '선택 없음',
key: const Key('selected_zipcode'),
),
),
);
},
),
);
},
),
);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
testWidgets('검색 전 안내 문구를 노출하고 fetcher를 호출하지 않는다', (tester) async {
var called = false;
await tester.pumpWidget(
_PostalSearchHarness(
fetcher: (keyword) async {
called = true;
return [];
},
),
);
await tester.pumpAndSettle();
expect(find.text('검색어를 입력한 뒤 엔터 또는 검색 버튼을 눌러 주세요.'), findsOneWidget);
expect(called, isFalse);
});
testWidgets('검색 실행 후 결과를 선택하면 선택 정보가 반환된다', (tester) async {
var receivedKeyword = '';
await tester.pumpWidget(
_PostalSearchHarness(
fetcher: (keyword) async {
receivedKeyword = keyword;
return [
PostalSearchResult(
zipcode: '06000',
sido: '서울특별시',
sigungu: '강남구',
roadName: '언주로',
buildingNumber: '100',
),
];
},
),
);
await tester.pumpAndSettle();
final inputFinder = find.byType(EditableText);
expect(inputFinder, findsOneWidget);
await tester.enterText(inputFinder, '언주로');
await tester.tap(find.text('검색'));
await tester.pumpAndSettle();
expect(receivedKeyword, '언주로');
expect(find.text('검색 결과 1건'), findsOneWidget);
await tester.tap(find.text('06000'));
await tester.pumpAndSettle();
final selectedFinder = find.byKey(const Key('selected_zipcode'));
expect(selectedFinder, findsOneWidget);
final selectedText = tester.widget<Text>(selectedFinder);
expect(selectedText.data, '06000');
});
testWidgets('initialKeyword가 주어지면 자동으로 검색을 수행한다', (tester) async {
String? receivedKeyword;
await tester.pumpWidget(
_PostalSearchHarness(
fetcher: (keyword) async {
receivedKeyword = keyword;
return [];
},
initialKeyword: '06236',
),
);
await tester.pumpAndSettle();
expect(receivedKeyword, '06236');
await tester.tap(find.text('닫기'));
await tester.pumpAndSettle();
});
}