feat(dialog): 상세 팝업 SuperportDetailDialog 통합

- SuperportDetailDialog 위젯과 showSuperportDetailDialog 헬퍼를 추가하고 metadata/섹션 패턴을 표준화

- 결재/재고/마스터 각 상세 다이얼로그를 dialogs 디렉터리에 신설하고 기존 페이지를 신규 팝업으로 전환

- SuperportTable 행 선택과 우편번호 검색 다이얼로그 onRowTap 보정을 통해 헤더 오프셋 버그를 제거

- 상세 다이얼로그 및 트랜잭션/상세 뷰 전용 위젯 테스트와 tester_extensions 유틸을 추가하여 회귀를 방지

- detail_dialog_unification_plan.md로 작업 배경과 필드 통합 계획을 문서화
This commit is contained in:
JiWoong Sul
2025-11-07 19:02:43 +09:00
parent 1f78171294
commit 2f8b529506
64 changed files with 13721 additions and 7545 deletions

View File

@@ -0,0 +1,91 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/intl.dart' as intl;
import 'package:superport_v2/features/masters/customer/domain/entities/customer.dart';
import 'package:superport_v2/features/masters/customer/presentation/dialogs/customer_detail_dialog.dart';
import '../../../../../helpers/test_app.dart';
void main() {
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
Future<Customer?> noopCreate(CustomerInput _) async => null;
Future<Customer?> noopUpdate(int _, CustomerInput __) async => null;
Future<bool> noopDelete(int _) async => false;
Future<Customer?> noopRestore(int _) async => null;
testWidgets('showCustomerDetailDialog 등록 모드 폼을 렌더링한다', (tester) async {
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showCustomerDetailDialog(
context: context,
dateFormat: dateFormat,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
expect(find.text('고객사 등록'), findsOneWidget);
expect(find.text('고객사코드'), findsOneWidget);
expect(find.text('유형'), findsOneWidget);
expect(find.text('등록'), findsWidgets);
});
testWidgets('showCustomerDetailDialog 상세 모드는 고객 요약을 표시한다', (tester) async {
await tester.binding.setSurfaceSize(const Size(1280, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
final customer = Customer(
id: 10,
customerCode: 'C-001',
customerName: '슈퍼고객',
contactName: '홍길동',
isPartner: true,
isGeneral: false,
email: 'customer@example.com',
mobileNo: '010-1111-2222',
zipcode: CustomerZipcode(
zipcode: '06000',
sido: '서울',
sigungu: '강남구',
roadName: '테헤란로',
),
addressDetail: '101호',
isActive: true,
isDeleted: false,
note: 'VIP',
createdAt: DateTime(2024, 3, 1, 9),
updatedAt: DateTime(2024, 3, 2, 10),
);
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showCustomerDetailDialog(
context: context,
dateFormat: dateFormat,
customer: customer,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
expect(find.text('고객사 상세'), findsOneWidget);
expect(find.textContaining('슈퍼고객').first, findsOneWidget);
expect(find.text('고객사 코드'), findsWidgets);
expect(find.text('유형'), findsWidgets);
});
}

View File

@@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../helpers/tester_extensions.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';
@@ -131,7 +132,7 @@ void main() {
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록', settle: false);
await tester.pump();
expect(find.text('고객사코드를 입력하세요.'), findsOneWidget);
@@ -174,7 +175,7 @@ void main() {
await tester.enterText(fields.at(1), '검색 필요 고객');
await tester.enterText(fields.at(4), '06000');
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록', settle: false);
await tester.pump();
expect(find.text('검색 버튼을 눌러 주소를 선택하세요.'), findsOneWidget);
@@ -251,11 +252,10 @@ void main() {
await tester.enterText(editableTexts.at(4), '02-0000-0000');
// 유형 체크박스: 기본값 partner=false, general=true. partner on 추가
await tester.tap(find.text('파트너'));
await tester.tap(find.text('파트너'), warnIfMissed: false);
await tester.pump();
await tester.tap(find.text('등록'));
await tester.pumpAndSettle();
await tester.tapShadButton('등록');
expect(capturedInput, isNotNull);
expect(capturedInput?.customerCode, 'C-100');

View File

@@ -0,0 +1,135 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/intl.dart' as intl;
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
import 'package:superport_v2/features/masters/group/presentation/dialogs/group_detail_dialog.dart';
import '../../../../../helpers/test_app.dart';
void main() {
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
Future<Group?> noopCreate(GroupInput _) async => null;
Future<Group?> noopUpdate(int _, GroupInput __) async => null;
Future<bool> noopDelete(int _) async => false;
Future<Group?> noopRestore(int _) async => null;
testWidgets('showGroupDetailDialog 등록 모드 폼을 노출한다', (tester) async {
await tester.binding.setSurfaceSize(const Size(960, 720));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showGroupDetailDialog(
context: context,
dateFormat: dateFormat,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
expect(find.text('그룹 등록'), findsOneWidget);
expect(find.text('그룹명'), findsOneWidget);
expect(find.text('설명'), findsOneWidget);
expect(find.widgetWithText(ShadButton, '등록'), findsWidgets);
});
testWidgets('showGroupDetailDialog 상세 모드는 요약과 메타데이터를 제공한다', (tester) async {
await tester.binding.setSurfaceSize(const Size(1280, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
final group = Group(
id: 12,
groupName: '운영팀',
description: '운영 담당',
isDefault: false,
isActive: true,
isDeleted: false,
note: '연중무휴',
createdAt: DateTime(2024, 2, 1, 9),
updatedAt: DateTime(2024, 3, 1, 10),
);
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showGroupDetailDialog(
context: context,
dateFormat: dateFormat,
group: group,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
expect(find.text('그룹 상세'), findsWidgets);
expect(find.text('운영팀'), findsWidgets);
expect(find.text('운영 담당'), findsWidgets);
expect(find.text('기본 여부'), findsWidgets);
expect(find.text('사용 여부'), findsWidgets);
expect(find.text('삭제'), findsOneWidget);
});
testWidgets('삭제 섹션에서 경고와 버튼 상태를 노출한다', (tester) async {
await tester.binding.setSurfaceSize(const Size(1280, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
final group = Group(
id: 20,
groupName: '삭제 대상',
isDefault: false,
isActive: true,
isDeleted: false,
createdAt: DateTime(2024, 1, 10, 8),
updatedAt: DateTime(2024, 1, 12, 9),
);
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showGroupDetailDialog(
context: context,
dateFormat: dateFormat,
group: group,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: (_) async => true,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
final deleteTab = find.text('삭제').first;
await tester.tap(deleteTab, warnIfMissed: false);
await tester.pumpAndSettle();
expect(find.textContaining('삭제하면'), findsOneWidget);
final dialog = find.byType(Dialog);
final deleteButton = find
.descendant(
of: dialog,
matching: find.widgetWithText(ShadButton, '삭제', skipOffstage: false),
)
.first;
final buttonWidget = tester.widget<ShadButton>(deleteButton);
expect(buttonWidget.onPressed, isNotNull);
});
}

View File

@@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../helpers/tester_extensions.dart';
import 'package:superport_v2/core/common/models/paginated_result.dart';
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
@@ -120,7 +121,7 @@ void main() {
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록', settle: false);
await tester.pump();
expect(find.text('그룹명을 입력하세요.'), findsOneWidget);
@@ -179,8 +180,7 @@ void main() {
await tester.enterText(editableTexts.at(0), '운영팀');
await tester.enterText(editableTexts.at(1), '운영 담당');
await tester.tap(find.text('등록'));
await tester.pumpAndSettle();
await tester.tapShadButton('등록');
expect(capturedInput, isNotNull);
expect(capturedInput?.groupName, '운영팀');

View File

@@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../helpers/tester_extensions.dart';
import 'package:superport_v2/core/common/models/paginated_result.dart';
import 'package:superport_v2/core/permissions/permission_manager.dart';
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
@@ -179,7 +180,7 @@ void main() {
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록', settle: false);
await tester.pump();
expect(find.text('그룹을 선택하세요.'), findsOneWidget);
@@ -260,7 +261,7 @@ void main() {
final dialog = find.byType(Dialog);
final selects = find.descendant(
of: dialog,
matching: find.byType(ShadSelect<int?>),
matching: find.byWidgetPredicate((widget) => widget is ShadSelect),
);
// 그룹 선택
@@ -287,8 +288,7 @@ void main() {
await tester.tap(switches.at(3));
await tester.pump();
await tester.tap(find.text('등록'));
await tester.pumpAndSettle();
await tester.tapShadButton('등록');
expect(capturedInput, isNotNull);
expect(capturedInput?.groupId, 1);

View File

@@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../helpers/tester_extensions.dart';
import 'package:superport_v2/core/common/models/paginated_result.dart';
import 'package:superport_v2/features/masters/menu/domain/entities/menu.dart';
import 'package:superport_v2/features/masters/menu/domain/repositories/menu_repository.dart';
@@ -107,7 +108,7 @@ void main() {
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록', settle: false);
await tester.pump();
expect(find.text('메뉴코드를 입력하세요.'), findsOneWidget);
@@ -168,8 +169,7 @@ void main() {
await tester.enterText(editableTexts.at(0), 'MENU010');
await tester.enterText(editableTexts.at(1), '신규 메뉴');
await tester.tap(find.text('등록'));
await tester.pumpAndSettle();
await tester.tapShadButton('등록');
expect(capturedInput, isNotNull);
expect(capturedInput?.menuCode, 'MENU010');

View File

@@ -0,0 +1,102 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/intl.dart' as intl;
import 'package:superport_v2/features/masters/product/domain/entities/product.dart';
import 'package:superport_v2/features/masters/product/presentation/dialogs/product_detail_dialog.dart';
import 'package:superport_v2/features/masters/vendor/domain/entities/vendor.dart';
import 'package:superport_v2/features/masters/uom/domain/entities/uom.dart';
import '../../../../../helpers/test_app.dart';
void main() {
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
final vendorOptions = [
Vendor(id: 1, vendorCode: 'V-001', vendorName: '슈퍼벤더'),
];
final uomOptions = [Uom(id: 1, uomName: 'EA')];
Future<Product?> noopCreate(ProductInput _) async => null;
Future<Product?> noopUpdate(int _, ProductInput __) async => null;
Future<bool> noopDelete(int _) async => false;
Future<Product?> noopRestore(int _) async => null;
testWidgets('showProductDetailDialog 등록 모드는 입력 폼을 표시한다', (tester) async {
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showProductDetailDialog(
context: context,
dateFormat: dateFormat,
vendorOptions: vendorOptions,
uomOptions: uomOptions,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
expect(find.text('제품 등록'), findsOneWidget);
expect(find.text('제품코드'), findsOneWidget);
expect(find.text('제조사'), findsOneWidget);
expect(find.text('단위'), findsOneWidget);
expect(find.text('등록'), findsWidgets);
});
testWidgets('showProductDetailDialog 상세 모드는 기본/연결/히스토리 정보를 제공한다', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(1280, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
final product = Product(
id: 42,
productCode: 'P-100',
productName: '테스트 제품',
vendor: ProductVendor(id: 1, vendorCode: 'V-001', vendorName: '슈퍼벤더'),
uom: ProductUom(id: 1, uomName: 'EA'),
isActive: true,
isDeleted: false,
note: '비고 메모',
createdAt: DateTime(2024, 1, 1, 9),
updatedAt: DateTime(2024, 2, 2, 10),
);
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showProductDetailDialog(
context: context,
dateFormat: dateFormat,
product: product,
vendorOptions: vendorOptions,
uomOptions: uomOptions,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
expect(find.text('제품 상세'), findsOneWidget);
expect(find.text('테스트 제품'), findsWidgets);
expect(find.text('제품코드'), findsOneWidget);
expect(find.text('연결 관계'), findsOneWidget);
await tester.tap(find.text('연결 관계'));
await tester.pumpAndSettle();
expect(find.textContaining('슈퍼벤더'), findsWidgets);
await tester.tap(find.text('히스토리'));
await tester.pumpAndSettle();
expect(find.textContaining('변경 이력 데이터는 준비 중입니다.'), findsOneWidget);
});
}

View File

@@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../helpers/tester_extensions.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';
@@ -208,7 +209,7 @@ void main() {
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록', settle: false);
await tester.pump();
expect(find.text('제품코드를 입력하세요.'), findsOneWidget);
@@ -303,8 +304,7 @@ void main() {
await tester.tap(find.text('EA'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.pumpAndSettle();
await tester.tapShadButton('등록');
expect(capturedInput, isNotNull);
expect(capturedInput?.productCode, 'NP-001');

View File

@@ -0,0 +1,152 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/intl.dart' as intl;
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
import 'package:superport_v2/features/masters/user/presentation/dialogs/user_detail_dialog.dart';
import 'package:superport_v2/widgets/components/superport_dialog.dart';
import '../../../../../helpers/test_app.dart';
void main() {
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
final groups = [Group(id: 1, groupName: '관리자')];
Future<UserAccount?> noopCreate(UserInput _) async => null;
Future<UserAccount?> noopUpdate(int _, UserInput __) async => null;
Future<bool> noopDelete(int _) async => false;
Future<UserAccount?> noopRestore(int _) async => null;
Future<UserAccount?> noopReset(int _) async => null;
testWidgets('showUserDetailDialog 등록 모드 폼 렌더링', (tester) async {
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showUserDetailDialog(
context: context,
dateFormat: dateFormat,
groupOptions: groups,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
onResetPassword: noopReset,
),
);
await tester.pumpAndSettle();
expect(find.text('사용자 등록'), findsOneWidget);
expect(find.text('사번'), findsOneWidget);
expect(find.text('그룹'), findsOneWidget);
expect(find.text('등록'), findsWidgets);
});
testWidgets('상세 모드에서 비밀번호 재설정을 실행한다', (tester) async {
await tester.binding.setSurfaceSize(const Size(1280, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
final user = UserAccount(
id: 7,
employeeNo: 'A007',
employeeName: '홍길동',
email: 'hong@superport.com',
mobileNo: '010-1234-5678',
group: UserGroup(id: 1, groupName: '관리자'),
createdAt: DateTime(2024, 1, 1, 9),
updatedAt: DateTime(2024, 1, 2, 10),
passwordUpdatedAt: DateTime(2024, 1, 2, 9),
);
var resetCalled = false;
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
final dialogFuture = showUserDetailDialog(
context: context,
dateFormat: dateFormat,
user: user,
groupOptions: groups,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
onResetPassword: (id) async {
resetCalled = id == 7;
return user;
},
);
await tester.pumpAndSettle();
await tester.tap(find.text('보안'));
await tester.pumpAndSettle();
final resetButton = find.widgetWithText(ShadButton, '비밀번호 재설정').first;
await tester.ensureVisible(resetButton);
await tester.tap(resetButton, warnIfMissed: false);
await tester.pumpAndSettle();
expect(find.text('비밀번호 재설정'), findsWidgets);
final confirmButton = find.widgetWithText(ShadButton, '재설정').last;
await tester.ensureVisible(confirmButton);
await tester.tap(confirmButton, warnIfMissed: false);
await tester.pumpAndSettle();
final result = await dialogFuture;
expect(resetCalled, isTrue);
expect(result?.action, UserDetailDialogAction.passwordReset);
expect(find.byType(SuperportDialog), findsNothing);
});
testWidgets('상세 모드에서 삭제 섹션을 노출한다', (tester) async {
await tester.binding.setSurfaceSize(const Size(1280, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
final user = UserAccount(
id: 9,
employeeNo: 'A009',
employeeName: '삭제 대상',
group: UserGroup(id: 1, groupName: '관리자'),
isActive: true,
isDeleted: false,
createdAt: DateTime(2024, 1, 3, 9),
updatedAt: DateTime(2024, 1, 4, 12),
);
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
final dialogFuture = showUserDetailDialog(
context: context,
dateFormat: dateFormat,
user: user,
groupOptions: groups,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
onResetPassword: noopReset,
);
await tester.pumpAndSettle();
await tester.tap(find.text('삭제').first);
await tester.pumpAndSettle();
expect(find.textContaining('삭제하면'), findsOneWidget);
expect(find.widgetWithText(ShadButton, '삭제'), findsWidgets);
await tester.tap(find.byTooltip('닫기'), warnIfMissed: false);
await tester.pumpAndSettle();
await dialogFuture;
});
}

View File

@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -5,6 +7,7 @@ import 'package:get_it/get_it.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../helpers/tester_extensions.dart';
import 'package:superport_v2/core/common/models/paginated_result.dart';
import 'package:superport_v2/core/permissions/permission_manager.dart';
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
@@ -14,6 +17,7 @@ import 'package:superport_v2/features/masters/group_permission/domain/repositori
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
import 'package:superport_v2/features/masters/user/domain/repositories/user_repository.dart';
import 'package:superport_v2/features/masters/user/presentation/pages/user_page.dart';
import 'package:superport_v2/widgets/components/superport_dialog.dart';
class _MockUserRepository extends Mock implements UserRepository {}
@@ -191,7 +195,7 @@ void main() {
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록', settle: false);
await tester.pump();
expect(find.text('사번을 입력하세요.'), findsOneWidget);
@@ -259,7 +263,7 @@ void main() {
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
final dialog = find.byType(Dialog);
final dialog = find.byType(SuperportDialog);
await tester.enterText(
find.byKey(const ValueKey('user_form_employee')),
'A010',
@@ -298,8 +302,7 @@ void main() {
await tester.tap(adminOption.first, warnIfMissed: false);
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.pumpAndSettle();
await tester.tapShadButton('등록');
expect(capturedInput, isNotNull);
expect(capturedInput?.employeeNo, 'A010');
@@ -307,7 +310,7 @@ void main() {
expect(capturedInput?.forcePasswordChange, isTrue);
expect(capturedInput?.email, 'new@superport.com');
expect(capturedInput?.mobileNo, '010-1111-2222');
expect(find.byType(Dialog), findsNothing);
expect(find.byType(SuperportDialog), findsNothing);
expect(find.text('A010'), findsOneWidget);
verify(() => userRepository.create(any())).called(1);
});
@@ -344,7 +347,7 @@ void main() {
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
final dialog = find.byType(Dialog);
final dialog = find.byType(SuperportDialog);
await tester.enterText(
find.byKey(const ValueKey('user_form_employee')),
'A011',
@@ -381,14 +384,14 @@ void main() {
await tester.tap(find.text('관리자', skipOffstage: false).first);
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록', settle: false);
await tester.pump();
expect(find.textContaining('최소 8자 이상 입력해야 합니다.'), findsOneWidget);
verifyNever(() => userRepository.create(any()));
});
testWidgets('비밀번호 재설정 버튼을 통해 확인 후 API 호출', (tester) async {
testWidgets('상세 팝업에서 비밀번호 재설정 액션 수행', (tester) async {
final view = tester.view;
view.physicalSize = const Size(1280, 800);
view.devicePixelRatio = 1.0;
@@ -432,20 +435,37 @@ void main() {
await tester.pumpWidget(_buildApp(const UserPage()));
await tester.pumpAndSettle();
final resetFinder = find
.widgetWithIcon(ShadButton, LucideIcons.refreshCcw)
.first;
final resetButton = tester.widget<ShadButton>(resetFinder);
resetButton.onPressed?.call();
final rowFinder = find.text('A001');
expect(rowFinder, findsOneWidget);
final rowRect = tester.getRect(rowFinder);
await tester.tapAt(
rowRect.center,
kind: PointerDeviceKind.mouse,
);
await tester.pumpAndSettle();
expect(find.byType(Dialog), findsOneWidget);
expect(find.text('재설정'), findsOneWidget);
await tester.tap(find.text('재설정'));
expect(find.byType(SuperportDialog), findsOneWidget);
await tester.tap(find.text('보안'));
await tester.pumpAndSettle();
final resetButton =
find.widgetWithText(ShadButton, '비밀번호 재설정').first;
await tester.ensureVisible(resetButton);
await tester.tap(resetButton, warnIfMissed: false);
await tester.pumpAndSettle();
expect(find.text('비밀번호 재설정'), findsWidgets);
final confirmButton =
find.widgetWithText(ShadButton, '재설정').last;
await tester.ensureVisible(confirmButton);
await tester.tap(confirmButton, warnIfMissed: false);
await tester.pumpAndSettle();
verify(() => userRepository.resetPassword(1)).called(1);
expect(find.text('비밀번호 재설정'), findsNothing);
expect(find.byType(SuperportDialog), findsNothing);
});
});
}

View File

@@ -0,0 +1,135 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/intl.dart' as intl;
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport_v2/features/masters/vendor/domain/entities/vendor.dart';
import 'package:superport_v2/features/masters/vendor/presentation/dialogs/vendor_detail_dialog.dart';
import '../../../../../helpers/test_app.dart';
void main() {
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
Future<Vendor?> noopCreate(VendorInput _) async => null;
Future<Vendor?> noopUpdate(int _, VendorInput __) async => null;
Future<bool> noopDelete(int _) async => false;
Future<Vendor?> noopRestore(int _) async => null;
testWidgets('showVendorDetailDialog 등록 모드 폼을 노출한다', (tester) async {
await tester.binding.setSurfaceSize(const Size(1080, 720));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showVendorDetailDialog(
context: context,
dateFormat: dateFormat,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
expect(find.text('벤더 등록'), findsOneWidget);
expect(find.text('벤더코드'), findsOneWidget);
expect(find.text('벤더명'), findsOneWidget);
expect(find.widgetWithText(ShadButton, '등록'), findsWidgets);
});
testWidgets('showVendorDetailDialog 상세 모드는 요약 정보를 제공한다', (tester) async {
await tester.binding.setSurfaceSize(const Size(1280, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
final vendor = Vendor(
id: 7,
vendorCode: 'V-007',
vendorName: '슈퍼벤더',
isActive: true,
isDeleted: false,
note: '테스트 벤더',
createdAt: DateTime(2024, 3, 1, 9, 30),
updatedAt: DateTime(2024, 3, 5, 12, 0),
);
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showVendorDetailDialog(
context: context,
dateFormat: dateFormat,
vendor: vendor,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: (id) async {
expect(id, equals(7));
return true;
},
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
expect(find.text('벤더 상세'), findsWidgets);
expect(find.text('슈퍼벤더'), findsWidgets);
expect(find.text('삭제 상태'), findsOneWidget);
expect(find.text('삭제'), findsOneWidget);
});
testWidgets('삭제 섹션에서 경고 메시지와 버튼을 노출한다', (tester) async {
await tester.binding.setSurfaceSize(const Size(1280, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
final vendor = Vendor(
id: 9,
vendorCode: 'V-009',
vendorName: '삭제 대상',
isActive: true,
isDeleted: false,
createdAt: DateTime(2024, 4, 1, 10),
updatedAt: DateTime(2024, 4, 10, 18),
);
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showVendorDetailDialog(
context: context,
dateFormat: dateFormat,
vendor: vendor,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: (_) async => true,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
final deleteTab = find.text('삭제').first;
await tester.tap(deleteTab, warnIfMissed: false);
await tester.pumpAndSettle();
expect(find.textContaining('삭제하면'), findsOneWidget);
final dialog = find.byType(Dialog);
final deleteButton = find
.descendant(
of: dialog,
matching: find.widgetWithText(ShadButton, '삭제', skipOffstage: false),
)
.first;
final buttonWidget = tester.widget<ShadButton>(deleteButton);
expect(buttonWidget.onPressed, isNotNull);
});
}

View File

@@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../helpers/tester_extensions.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';
@@ -142,7 +143,7 @@ void main() {
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록', settle: false);
await tester.pump();
expect(find.text('벤더코드를 입력하세요.'), findsOneWidget);
@@ -207,14 +208,14 @@ void main() {
await tester.enterText(editableTexts.at(0), 'NV-001');
await tester.enterText(editableTexts.at(1), '신규벤더');
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록');
await tester.pumpAndSettle();
expect(find.byType(Dialog), findsNothing);
verify(() => repository.create(any())).called(1);
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);
});
testWidgets('좁은 폭에서도 오버플로 없이 렌더링', (tester) async {

View File

@@ -0,0 +1,89 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/intl.dart' as intl;
import 'package:superport_v2/features/masters/warehouse/domain/entities/warehouse.dart';
import 'package:superport_v2/features/masters/warehouse/presentation/dialogs/warehouse_detail_dialog.dart';
import '../../../../../helpers/test_app.dart';
void main() {
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
Future<Warehouse?> noopCreate(WarehouseInput _) async => null;
Future<Warehouse?> noopUpdate(int _, WarehouseInput __) async => null;
Future<bool> noopDelete(int _) async => false;
Future<Warehouse?> noopRestore(int _) async => null;
testWidgets('showWarehouseDetailDialog 등록 모드 UI', (tester) async {
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showWarehouseDetailDialog(
context: context,
dateFormat: dateFormat,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
expect(find.text('창고 등록'), findsOneWidget);
expect(find.text('창고코드'), findsOneWidget);
expect(find.text('우편번호'), findsOneWidget);
expect(find.text('등록'), findsWidgets);
});
testWidgets('showWarehouseDetailDialog 상세 모드 metadata/위험 섹션 제공', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(1280, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
final warehouse = Warehouse(
id: 7,
warehouseCode: 'WH-001',
warehouseName: '서울 1창고',
zipcode: WarehouseZipcode(zipcode: '06000', sido: '서울', sigungu: '강남구'),
addressDetail: '강남대로 123',
note: '비고',
isActive: true,
isDeleted: false,
createdAt: DateTime(2024, 1, 1, 9),
updatedAt: DateTime(2024, 1, 2, 10),
);
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
final context = tester.element(find.byType(SizedBox));
unawaited(
showWarehouseDetailDialog(
context: context,
dateFormat: dateFormat,
warehouse: warehouse,
onCreate: noopCreate,
onUpdate: noopUpdate,
onDelete: noopDelete,
onRestore: noopRestore,
),
);
await tester.pumpAndSettle();
expect(find.text('창고 상세'), findsOneWidget);
expect(find.textContaining('서울 1창고'), findsWidgets);
expect(find.text('기본주소'), findsOneWidget);
expect(find.text('서울 강남구'), findsWidgets);
expect(find.text('수정'), findsOneWidget);
await tester.tap(find.text('삭제'));
await tester.pumpAndSettle();
expect(find.textContaining('삭제하면 창고가'), findsOneWidget);
});
}

View File

@@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../helpers/tester_extensions.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';
@@ -152,7 +153,7 @@ void main() {
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.tapShadButton('등록', settle: false);
await tester.pump();
expect(find.text('창고코드를 입력하세요.'), findsOneWidget);
@@ -237,8 +238,7 @@ void main() {
await tester.enterText(updatedFields.at(3), '주소');
await tester.tap(find.text('등록'));
await tester.pumpAndSettle();
await tester.tapShadButton('등록');
expect(capturedInput, isNotNull);
expect(capturedInput?.warehouseCode, 'WH-100');