feat(dialog): 상세 팝업 SuperportDetailDialog 통합
- SuperportDetailDialog 위젯과 showSuperportDetailDialog 헬퍼를 추가하고 metadata/섹션 패턴을 표준화 - 결재/재고/마스터 각 상세 다이얼로그를 dialogs 디렉터리에 신설하고 기존 페이지를 신규 팝업으로 전환 - SuperportTable 행 선택과 우편번호 검색 다이얼로그 onRowTap 보정을 통해 헤더 오프셋 버그를 제거 - 상세 다이얼로그 및 트랜잭션/상세 뷰 전용 위젯 테스트와 tester_extensions 유틸을 추가하여 회귀를 방지 - detail_dialog_unification_plan.md로 작업 배경과 필드 통합 계획을 문서화
This commit is contained in:
@@ -16,6 +16,7 @@ import 'package:superport_v2/features/approvals/domain/repositories/approval_tem
|
||||
import 'package:superport_v2/features/approvals/presentation/pages/approval_page.dart';
|
||||
import 'package:superport_v2/features/inventory/lookups/domain/entities/lookup_item.dart';
|
||||
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||
import 'package:superport_v2/widgets/components/superport_detail_dialog.dart';
|
||||
|
||||
import '../../helpers/test_app.dart';
|
||||
import '../../helpers/fixture_loader.dart';
|
||||
@@ -43,6 +44,19 @@ void main() {
|
||||
.toSet();
|
||||
}
|
||||
|
||||
Future<void> selectDetailTab(WidgetTester tester, String id) async {
|
||||
final tabsFinder = find.descendant(
|
||||
of: find.byType(SuperportDetailDialog),
|
||||
matching: find.byType(ShadTabs<String>),
|
||||
);
|
||||
final tabsState = tester.state(tabsFinder);
|
||||
final controller =
|
||||
(tabsState as dynamic).controller as ShadTabsController<String>;
|
||||
controller.select(id);
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
setUpAll(() async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true');
|
||||
await Environment.initialize();
|
||||
@@ -95,10 +109,7 @@ void main() {
|
||||
await tester.tap(rowFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tabContext = tester.element(find.byType(TabBar));
|
||||
final tabController = DefaultTabController.of(tabContext);
|
||||
tabController.animateTo(1);
|
||||
await tester.pumpAndSettle();
|
||||
await selectDetailTab(tester, 'steps');
|
||||
|
||||
final approveButton = tester.widget<ShadButton>(
|
||||
find.byKey(const ValueKey('step_action_100_approve')),
|
||||
@@ -128,10 +139,7 @@ void main() {
|
||||
await tester.tap(rowFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tabContext = tester.element(find.byType(TabBar));
|
||||
final tabController = DefaultTabController.of(tabContext);
|
||||
tabController.animateTo(1);
|
||||
await tester.pumpAndSettle();
|
||||
await selectDetailTab(tester, 'steps');
|
||||
|
||||
final approveButton = tester.widget<ShadButton>(
|
||||
find.byKey(const ValueKey('step_action_100_approve')),
|
||||
@@ -161,10 +169,7 @@ void main() {
|
||||
await tester.tap(rowFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tabContext = tester.element(find.byType(TabBar));
|
||||
final tabController = DefaultTabController.of(tabContext);
|
||||
tabController.animateTo(1);
|
||||
await tester.pumpAndSettle();
|
||||
await selectDetailTab(tester, 'steps');
|
||||
|
||||
expect(find.textContaining('선행 단계가 완료되지 않았습니다.'), findsWidgets);
|
||||
|
||||
|
||||
@@ -0,0 +1,555 @@
|
||||
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/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/entities/approval_proceed_status.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/repositories/approval_repository.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/usecases/recall_approval_use_case.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/usecases/resubmit_approval_use_case.dart';
|
||||
import 'package:superport_v2/features/approvals/history/domain/entities/approval_history_record.dart';
|
||||
import 'package:superport_v2/features/approvals/history/domain/repositories/approval_history_repository.dart';
|
||||
import 'package:superport_v2/features/approvals/history/presentation/controllers/approval_history_controller.dart';
|
||||
import 'package:superport_v2/features/approvals/history/presentation/dialogs/approval_history_detail_dialog.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/authenticated_user.dart';
|
||||
import 'package:superport_v2/widgets/components/superport_dialog.dart';
|
||||
|
||||
import '../../../../../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
|
||||
|
||||
late _FakeApprovalRepository approvalRepository;
|
||||
late _FakeApprovalHistoryRepository historyRepository;
|
||||
late ApprovalHistoryController controller;
|
||||
late ApprovalHistoryRecord record;
|
||||
late AuthenticatedUser user;
|
||||
late Approval sampleApproval;
|
||||
late ApprovalStatus statusInProgress;
|
||||
late ApprovalStatus statusCompleted;
|
||||
late ApprovalStatus statusRejected;
|
||||
|
||||
setUp(() {
|
||||
approvalRepository = _FakeApprovalRepository();
|
||||
historyRepository = _FakeApprovalHistoryRepository();
|
||||
|
||||
statusInProgress = ApprovalStatus(
|
||||
id: 1,
|
||||
name: '진행중',
|
||||
color: '#0EA5E9',
|
||||
isTerminal: false,
|
||||
);
|
||||
statusCompleted = ApprovalStatus(
|
||||
id: 2,
|
||||
name: '완료',
|
||||
color: '#22C55E',
|
||||
isTerminal: true,
|
||||
);
|
||||
statusRejected = ApprovalStatus(
|
||||
id: 3,
|
||||
name: '반려',
|
||||
color: '#F97316',
|
||||
isTerminal: true,
|
||||
);
|
||||
|
||||
final requester = ApprovalRequester(
|
||||
id: 901,
|
||||
employeeNo: 'E901',
|
||||
name: '상신자',
|
||||
);
|
||||
final approver1 = ApprovalApprover(
|
||||
id: 801,
|
||||
employeeNo: 'E801',
|
||||
name: '선결자',
|
||||
);
|
||||
final approver2 = ApprovalApprover(
|
||||
id: 802,
|
||||
employeeNo: 'E802',
|
||||
name: '최종자',
|
||||
);
|
||||
|
||||
final steps = [
|
||||
ApprovalStep(
|
||||
id: 1001,
|
||||
stepOrder: 1,
|
||||
approver: approver1,
|
||||
status: statusInProgress,
|
||||
assignedAt: DateTime(2024, 1, 10, 9),
|
||||
note: '1단계',
|
||||
),
|
||||
ApprovalStep(
|
||||
id: 1002,
|
||||
stepOrder: 2,
|
||||
approver: approver2,
|
||||
status: statusCompleted,
|
||||
assignedAt: DateTime(2024, 1, 10, 10),
|
||||
decidedAt: DateTime(2024, 1, 10, 12),
|
||||
note: '2단계',
|
||||
),
|
||||
];
|
||||
|
||||
final histories = [
|
||||
ApprovalHistory(
|
||||
id: 5001,
|
||||
action: ApprovalAction(id: 11, name: '상신', code: 'submit'),
|
||||
fromStatus: null,
|
||||
toStatus: statusInProgress,
|
||||
approver: approver1,
|
||||
actionAt: DateTime(2024, 1, 10, 9, 5),
|
||||
note: '상신 완료',
|
||||
),
|
||||
ApprovalHistory(
|
||||
id: 5002,
|
||||
action: ApprovalAction(id: 12, name: '승인', code: 'approve'),
|
||||
fromStatus: statusInProgress,
|
||||
toStatus: statusCompleted,
|
||||
approver: approver2,
|
||||
actionAt: DateTime(2024, 1, 10, 12, 30),
|
||||
note: '승인 완료',
|
||||
),
|
||||
];
|
||||
|
||||
sampleApproval = Approval(
|
||||
id: 300,
|
||||
approvalNo: 'APP-2024-0300',
|
||||
transactionNo: 'TRX-0300',
|
||||
transactionUpdatedAt: DateTime(2024, 1, 10, 12, 45),
|
||||
status: statusInProgress,
|
||||
requester: requester,
|
||||
requestedAt: DateTime(2024, 1, 10, 8, 30),
|
||||
steps: steps,
|
||||
histories: histories,
|
||||
updatedAt: DateTime(2024, 1, 10, 12, 50),
|
||||
);
|
||||
|
||||
approvalRepository
|
||||
..detail = sampleApproval
|
||||
..recallResult = sampleApproval
|
||||
..resubmitResult = sampleApproval.copyWith(
|
||||
status: statusInProgress,
|
||||
updatedAt: DateTime(2024, 1, 10, 13),
|
||||
)
|
||||
..historyResult = PaginatedResult<ApprovalHistory>(
|
||||
items: histories,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: histories.length,
|
||||
);
|
||||
|
||||
historyRepository.listResult = PaginatedResult<ApprovalHistoryRecord>(
|
||||
items: [
|
||||
ApprovalHistoryRecord(
|
||||
id: 700,
|
||||
approvalId: sampleApproval.id!,
|
||||
approvalNo: sampleApproval.approvalNo,
|
||||
stepOrder: 1,
|
||||
action: ApprovalAction(id: 21, name: '상신 완료', code: 'submit'),
|
||||
fromStatus: null,
|
||||
toStatus: statusInProgress,
|
||||
approver: approver1,
|
||||
actionAt: DateTime(2024, 1, 10, 9, 5),
|
||||
note: '상신 후 대기 중',
|
||||
),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
);
|
||||
|
||||
controller = ApprovalHistoryController(
|
||||
repository: historyRepository,
|
||||
approvalRepository: approvalRepository,
|
||||
recallUseCase: RecallApprovalUseCase(repository: approvalRepository),
|
||||
resubmitUseCase: ResubmitApprovalUseCase(repository: approvalRepository),
|
||||
);
|
||||
|
||||
record = historyRepository.listResult!.items.first;
|
||||
|
||||
user = const AuthenticatedUser(
|
||||
id: 42,
|
||||
name: '결재자',
|
||||
employeeNo: 'E042',
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
Future<void> openDialog(WidgetTester tester) async {
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
unawaited(
|
||||
showApprovalHistoryDetailDialog(
|
||||
context: context,
|
||||
controller: controller,
|
||||
record: record,
|
||||
dateFormat: dateFormat,
|
||||
currentUser: user,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'showApprovalHistoryDetailDialog 결재 요약과 타임라인을 표시한다',
|
||||
(tester) async {
|
||||
await openDialog(tester);
|
||||
|
||||
expect(find.text('결재 이력 상세'), findsOneWidget);
|
||||
expect(find.textContaining('결재번호 ${record.approvalNo}'), findsWidgets);
|
||||
expect(find.text('상태 타임라인'), findsOneWidget);
|
||||
expect(find.text('감사 로그'), findsOneWidget);
|
||||
|
||||
expect(
|
||||
find.textContaining(
|
||||
'상신자 ${sampleApproval.requester.name} (${sampleApproval.requester.employeeNo})',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.textContaining('총 ${sampleApproval.steps.length}단계'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('승인'), findsWidgets);
|
||||
|
||||
expect(approvalRepository.listHistoryCalls, isNotEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'회수 버튼을 누르면 recallApproval이 호출되어 감사 로그가 새로고침된다',
|
||||
(tester) async {
|
||||
await openDialog(tester);
|
||||
|
||||
final recallButton = find.widgetWithText(ShadButton, '회수');
|
||||
expect(recallButton, findsOneWidget);
|
||||
await tester.ensureVisible(recallButton);
|
||||
await tester.tap(recallButton, warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dialogFinder = find.ancestor(
|
||||
of: find.text('결재 회수'),
|
||||
matching: find.byType(SuperportDialog),
|
||||
);
|
||||
expect(dialogFinder, findsOneWidget);
|
||||
|
||||
final memoField = find.descendant(
|
||||
of: dialogFinder,
|
||||
matching: find.byType(ShadTextarea),
|
||||
);
|
||||
expect(memoField, findsOneWidget);
|
||||
await tester.enterText(memoField, '긴급 회수');
|
||||
|
||||
final confirmButton = find.descendant(
|
||||
of: dialogFinder,
|
||||
matching: find.widgetWithText(ShadButton, '회수'),
|
||||
);
|
||||
await tester.tap(confirmButton, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(approvalRepository.recallInputs, hasLength(1));
|
||||
expect(approvalRepository.recallInputs.first.note, '긴급 회수');
|
||||
expect(approvalRepository.listHistoryCalls.length, greaterThanOrEqualTo(2));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'재상신 버튼을 누르면 resubmitApproval이 호출되어 최신 단계 정보가 전달된다',
|
||||
(tester) async {
|
||||
final rejectedApproval = sampleApproval.copyWith(
|
||||
status: statusRejected,
|
||||
steps: sampleApproval.steps
|
||||
.map(
|
||||
(step) => step.stepOrder == 1
|
||||
? step.copyWith(
|
||||
status: statusRejected,
|
||||
decidedAt: DateTime(2024, 1, 10, 11),
|
||||
)
|
||||
: step,
|
||||
)
|
||||
.toList(growable: false),
|
||||
);
|
||||
final resubmittedStatus = ApprovalStatus(
|
||||
id: 4,
|
||||
name: '재상신',
|
||||
color: '#6366F1',
|
||||
isTerminal: false,
|
||||
);
|
||||
final resubmittedApproval = rejectedApproval.copyWith(
|
||||
status: resubmittedStatus,
|
||||
updatedAt: DateTime(2024, 1, 10, 13, 10),
|
||||
);
|
||||
|
||||
approvalRepository
|
||||
..detail = rejectedApproval
|
||||
..resubmitResult = resubmittedApproval;
|
||||
record = record.copyWith(
|
||||
action: ApprovalAction(id: 33, name: '반려', code: 'reject'),
|
||||
toStatus: statusRejected,
|
||||
stepOrder: 2,
|
||||
);
|
||||
|
||||
await openDialog(tester);
|
||||
|
||||
final resubmitButton = find.widgetWithText(ShadButton, '재상신');
|
||||
expect(resubmitButton, findsOneWidget);
|
||||
await tester.ensureVisible(resubmitButton);
|
||||
await tester.tap(resubmitButton, warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dialogFinder = find.ancestor(
|
||||
of: find.text('결재 재상신'),
|
||||
matching: find.byType(SuperportDialog),
|
||||
);
|
||||
expect(dialogFinder, findsOneWidget);
|
||||
|
||||
final memoField = find.descendant(
|
||||
of: dialogFinder,
|
||||
matching: find.byType(ShadTextarea),
|
||||
);
|
||||
expect(memoField, findsOneWidget);
|
||||
await tester.enterText(memoField, '재상신 메모');
|
||||
|
||||
final confirmButton = find.descendant(
|
||||
of: dialogFinder,
|
||||
matching: find.widgetWithText(ShadButton, '재상신'),
|
||||
);
|
||||
await tester.tap(confirmButton, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(approvalRepository.resubmitInputs, hasLength(1));
|
||||
final input = approvalRepository.resubmitInputs.first;
|
||||
expect(input.note, '재상신 메모');
|
||||
expect(input.submission.steps.length, rejectedApproval.steps.length);
|
||||
expect(
|
||||
input.submission.steps.first.stepOrder,
|
||||
rejectedApproval.steps.first.stepOrder,
|
||||
);
|
||||
expect(approvalRepository.listHistoryCalls.length, greaterThanOrEqualTo(2));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeApprovalRepository implements ApprovalRepository {
|
||||
Approval? detail;
|
||||
Approval? recallResult;
|
||||
Approval? resubmitResult;
|
||||
PaginatedResult<ApprovalHistory>? historyResult;
|
||||
|
||||
final List<ApprovalRecallInput> recallInputs = [];
|
||||
final List<ApprovalResubmissionInput> resubmitInputs = [];
|
||||
final List<int> fetchDetailIds = [];
|
||||
final List<_ListHistoryCall> listHistoryCalls = [];
|
||||
|
||||
@override
|
||||
Future<Approval> assignSteps(ApprovalStepAssignmentInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> fetchDetail(
|
||||
int id, {
|
||||
bool includeSteps = true,
|
||||
bool includeHistories = true,
|
||||
}) async {
|
||||
fetchDetailIds.add(id);
|
||||
final result = detail;
|
||||
if (result == null) {
|
||||
throw StateError('detail이 설정되지 않았습니다.');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApprovalProceedStatus> canProceed(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ApprovalAction>> listActions({bool activeOnly = true}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> performStepAction(ApprovalStepActionInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PaginatedResult<Approval>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
int? transactionId,
|
||||
int? approvalStatusId,
|
||||
int? requestedById,
|
||||
List<String>? statusCodes,
|
||||
bool includePending = false,
|
||||
bool includeHistories = false,
|
||||
bool includeSteps = false,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> submit(ApprovalSubmissionInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> resubmit(ApprovalResubmissionInput input) async {
|
||||
resubmitInputs.add(input);
|
||||
final result = resubmitResult ?? detail;
|
||||
if (result == null) {
|
||||
throw StateError('resubmitResult가 설정되지 않았습니다.');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> approve(ApprovalDecisionInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> reject(ApprovalDecisionInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> recall(ApprovalRecallInput input) async {
|
||||
recallInputs.add(input);
|
||||
final result = recallResult ?? detail;
|
||||
if (result == null) {
|
||||
throw StateError('recallResult가 설정되지 않았습니다.');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PaginatedResult<ApprovalHistory>> listHistory({
|
||||
required int approvalId,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
int? actorId,
|
||||
int? approvalActionId,
|
||||
}) async {
|
||||
listHistoryCalls.add(
|
||||
_ListHistoryCall(
|
||||
approvalId: approvalId,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
actorId: actorId,
|
||||
approvalActionId: approvalActionId,
|
||||
),
|
||||
);
|
||||
return historyResult ??
|
||||
PaginatedResult<ApprovalHistory>(
|
||||
items: const [],
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
total: 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> create(ApprovalCreateInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> update(ApprovalUpdateInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> restore(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeApprovalHistoryRepository implements ApprovalHistoryRepository {
|
||||
PaginatedResult<ApprovalHistoryRecord>? listResult;
|
||||
final List<_HistoryListCall> listCalls = [];
|
||||
|
||||
@override
|
||||
Future<PaginatedResult<ApprovalHistoryRecord>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String? query,
|
||||
int? approvalActionId,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
}) async {
|
||||
listCalls.add(
|
||||
_HistoryListCall(
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
query: query,
|
||||
approvalActionId: approvalActionId,
|
||||
from: from,
|
||||
to: to,
|
||||
),
|
||||
);
|
||||
return listResult ??
|
||||
PaginatedResult<ApprovalHistoryRecord>(
|
||||
items: const [],
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
total: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ListHistoryCall {
|
||||
_ListHistoryCall({
|
||||
required this.approvalId,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
this.actorId,
|
||||
this.approvalActionId,
|
||||
});
|
||||
|
||||
final int approvalId;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
final int? actorId;
|
||||
final int? approvalActionId;
|
||||
}
|
||||
|
||||
class _HistoryListCall {
|
||||
_HistoryListCall({
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
this.query,
|
||||
this.approvalActionId,
|
||||
this.from,
|
||||
this.to,
|
||||
});
|
||||
|
||||
final int page;
|
||||
final int pageSize;
|
||||
final String? query;
|
||||
final int? approvalActionId;
|
||||
final DateTime? from;
|
||||
final DateTime? to;
|
||||
}
|
||||
@@ -395,9 +395,10 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final recallButton = find.widgetWithText(ShadButton, '회수').first;
|
||||
await tester.ensureVisible(recallButton);
|
||||
await tester.tap(recallButton);
|
||||
await tester.tap(find.widgetWithText(ShadButton, '회수').first);
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('결재 회수'), findsOneWidget);
|
||||
@@ -406,8 +407,120 @@ void main() {
|
||||
await tester.tap(confirmButton);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(fetchCount, equals(2));
|
||||
expect(find.text('결재 상세를 새로고침하지 못했습니다. 다시 시도해 주세요.'), findsOneWidget);
|
||||
expect(find.textContaining('결재 상세를 새로고침하지 못했습니다'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('닫기'));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('회수 성공 시 토스트를 노출한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
|
||||
|
||||
when(
|
||||
() => historyRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
approvalActionId: any(named: 'approvalActionId'),
|
||||
from: any(named: 'from'),
|
||||
to: any(named: 'to'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<ApprovalHistoryRecord>(
|
||||
items: [record],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
final recallable = recallableFlow();
|
||||
when(
|
||||
() => approvalRepository.fetchDetail(
|
||||
any(),
|
||||
includeSteps: any(named: 'includeSteps'),
|
||||
includeHistories: any(named: 'includeHistories'),
|
||||
),
|
||||
).thenAnswer((_) async => recallable.approval);
|
||||
|
||||
when(() => recallUseCase.call(any())).thenAnswer((_) async => recallable);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ApprovalHistoryPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final table = tester.widget<SuperportTable>(find.byType(SuperportTable));
|
||||
table.onRowTap?.call(0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.widgetWithText(ShadButton, '회수').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('결재 회수'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(ShadButton, '회수').last);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(find.text('결재(APP-2024-0001) 회수를 완료했습니다.'), findsOneWidget);
|
||||
verify(() => recallUseCase.call(any())).called(1);
|
||||
|
||||
await tester.tap(find.byTooltip('닫기'));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('재상신 성공 시 토스트를 노출한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
|
||||
|
||||
when(
|
||||
() => historyRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
approvalActionId: any(named: 'approvalActionId'),
|
||||
from: any(named: 'from'),
|
||||
to: any(named: 'to'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<ApprovalHistoryRecord>(
|
||||
items: [record],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
final flow = resubmittableFlow();
|
||||
when(
|
||||
() => approvalRepository.fetchDetail(
|
||||
any(),
|
||||
includeSteps: any(named: 'includeSteps'),
|
||||
includeHistories: any(named: 'includeHistories'),
|
||||
),
|
||||
).thenAnswer((_) async => flow.approval);
|
||||
|
||||
when(() => resubmitUseCase.call(any())).thenAnswer((_) async => flow);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ApprovalHistoryPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final table = tester.widget<SuperportTable>(find.byType(SuperportTable));
|
||||
table.onRowTap?.call(0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.widgetWithText(ShadButton, '재상신').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('결재 재상신'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(ShadButton, '재상신').last);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(find.text('결재(APP-2024-0001) 재상신을 완료했습니다.'), findsOneWidget);
|
||||
verify(() => resubmitUseCase.call(any())).called(1);
|
||||
|
||||
await tester.tap(find.byTooltip('닫기'));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
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/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/entities/approval_proceed_status.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/entities/approval_template.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/repositories/approval_repository.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/repositories/approval_template_repository.dart';
|
||||
import 'package:superport_v2/features/approvals/presentation/controllers/approval_controller.dart';
|
||||
import 'package:superport_v2/features/approvals/presentation/dialogs/approval_detail_dialog.dart';
|
||||
import 'package:superport_v2/widgets/components/superport_dialog.dart';
|
||||
|
||||
import '../../../../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
|
||||
|
||||
late _FakeApprovalRepository approvalRepository;
|
||||
late _FakeApprovalTemplateRepository templateRepository;
|
||||
late ApprovalController controller;
|
||||
late Approval sampleApproval;
|
||||
late ApprovalTemplate sampleTemplate;
|
||||
|
||||
setUp(() async {
|
||||
approvalRepository = _FakeApprovalRepository();
|
||||
templateRepository = _FakeApprovalTemplateRepository();
|
||||
|
||||
controller = ApprovalController(
|
||||
approvalRepository: approvalRepository,
|
||||
templateRepository: templateRepository,
|
||||
);
|
||||
|
||||
final statusInProgress = ApprovalStatus(id: 1, name: '진행중');
|
||||
final requester = ApprovalRequester(
|
||||
id: 99,
|
||||
employeeNo: 'E099',
|
||||
name: '요청자',
|
||||
);
|
||||
final approver = ApprovalApprover(id: 77, employeeNo: 'E077', name: '승인자');
|
||||
|
||||
final step = ApprovalStep(
|
||||
id: 501,
|
||||
stepOrder: 1,
|
||||
approver: approver,
|
||||
status: statusInProgress,
|
||||
assignedAt: DateTime(2024, 1, 1, 9),
|
||||
);
|
||||
|
||||
sampleApproval = Approval(
|
||||
id: 100,
|
||||
approvalNo: 'APP-2024-0100',
|
||||
transactionNo: 'TRX-100',
|
||||
status: statusInProgress,
|
||||
requester: requester,
|
||||
requestedAt: DateTime(2024, 1, 1, 9),
|
||||
steps: [step],
|
||||
histories: const [],
|
||||
);
|
||||
|
||||
approvalRepository.detail = sampleApproval;
|
||||
approvalRepository.updatedAfterAssign = sampleApproval;
|
||||
approvalRepository.updatedAfterAction = sampleApproval;
|
||||
|
||||
sampleTemplate = ApprovalTemplate(
|
||||
id: 200,
|
||||
code: 'TEMP-200',
|
||||
name: '샘플 템플릿',
|
||||
isActive: true,
|
||||
steps: [
|
||||
ApprovalTemplateStep(
|
||||
id: null,
|
||||
stepOrder: 1,
|
||||
approver: ApprovalTemplateApprover(
|
||||
id: 42,
|
||||
employeeNo: 'E042',
|
||||
name: '템플 승인자',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
templateRepository
|
||||
..listResult = [sampleTemplate]
|
||||
..detailResult = sampleTemplate;
|
||||
|
||||
approvalRepository.actions = [
|
||||
ApprovalAction(id: 1, name: '승인', code: 'approve'),
|
||||
ApprovalAction(id: 2, name: '반려', code: 'reject'),
|
||||
ApprovalAction(id: 3, name: '코멘트', code: 'comment'),
|
||||
];
|
||||
|
||||
expect(controller.templates, isEmpty);
|
||||
|
||||
await controller.loadTemplates(force: true);
|
||||
await controller.loadActionOptions(force: true);
|
||||
await controller.selectApproval(sampleApproval.id!);
|
||||
expect(controller.templates, isNotEmpty);
|
||||
expect(controller.selected, isNotNull);
|
||||
expect(controller.canProceedSelected, isTrue);
|
||||
expect(controller.hasActionOptions, isTrue);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
Future<void> openDialog(WidgetTester tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(1280, 800));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
unawaited(
|
||||
showApprovalDetailDialog(
|
||||
context: context,
|
||||
controller: controller,
|
||||
dateFormat: dateFormat,
|
||||
canPerformStepActions: true,
|
||||
canApplyTemplate: true,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('showApprovalDetailDialog 결재 요약과 템플릿 선택을 표시한다', (tester) async {
|
||||
await openDialog(tester);
|
||||
|
||||
expect(
|
||||
find.textContaining('결재번호 ${sampleApproval.approvalNo}'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.textContaining(sampleApproval.transactionNo), findsWidgets);
|
||||
|
||||
final stepsTab = find.text('단계');
|
||||
await tester.tap(stepsTab, warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Step 1'), findsOneWidget);
|
||||
expect(find.text('템플릿 적용'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('템플릿 적용 버튼을 누르면 assignSteps가 호출되고 성공 토스트를 노출한다', (tester) async {
|
||||
await openDialog(tester);
|
||||
|
||||
await tester.tap(find.text('단계'), warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final applyButton = find.byKey(
|
||||
const ValueKey('approval_apply_template_button'),
|
||||
);
|
||||
expect(applyButton, findsOneWidget);
|
||||
await tester.ensureVisible(applyButton);
|
||||
await tester.tap(applyButton, warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump();
|
||||
|
||||
expect(approvalRepository.assignmentInputs, hasLength(1));
|
||||
expect(
|
||||
approvalRepository.assignmentInputs.first.steps.first.approverId,
|
||||
sampleTemplate.steps.first.approver.id,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('승인 버튼을 누르면 performStepAction이 호출되고 성공 메시지를 노출한다', (
|
||||
tester,
|
||||
) async {
|
||||
await openDialog(tester);
|
||||
|
||||
await tester.tap(find.text('단계'), warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final stepText = find.text('Step 1').first;
|
||||
final stepCard = find
|
||||
.ancestor(of: stepText, matching: find.byType(ShadCard))
|
||||
.first;
|
||||
expect(stepCard, findsOneWidget);
|
||||
|
||||
final approveButtonFinder = find.byKey(
|
||||
const ValueKey('step_action_501_approve'),
|
||||
);
|
||||
expect(approveButtonFinder, findsOneWidget);
|
||||
final approveButton = tester.widget<ShadButton>(approveButtonFinder);
|
||||
expect(approveButton.onPressed, isNotNull);
|
||||
await tester.ensureVisible(approveButtonFinder);
|
||||
await tester.tap(approveButtonFinder, warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final confirmDialog = find
|
||||
.ancestor(
|
||||
of: find.text('결재 승인'),
|
||||
matching: find.byType(SuperportDialog),
|
||||
)
|
||||
.first;
|
||||
|
||||
final noteField = find.descendant(
|
||||
of: confirmDialog,
|
||||
matching: find.byType(EditableText),
|
||||
);
|
||||
await tester.enterText(noteField, '승인 메모');
|
||||
|
||||
final confirmButton = find
|
||||
.descendant(
|
||||
of: confirmDialog,
|
||||
matching: find.widgetWithText(ShadButton, '승인'),
|
||||
)
|
||||
.first;
|
||||
await tester.tap(confirmButton, warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(approvalRepository.actionInputs, hasLength(1));
|
||||
expect(approvalRepository.actionInputs.first.actionId, 1);
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeApprovalRepository implements ApprovalRepository {
|
||||
Approval? detail;
|
||||
Approval? updatedAfterAssign;
|
||||
Approval? updatedAfterAction;
|
||||
bool canProceedResult = true;
|
||||
List<ApprovalAction> actions = const [];
|
||||
|
||||
final List<ApprovalStepAssignmentInput> assignmentInputs = [];
|
||||
final List<ApprovalStepActionInput> actionInputs = [];
|
||||
|
||||
@override
|
||||
Future<Approval> assignSteps(ApprovalStepAssignmentInput input) async {
|
||||
assignmentInputs.add(input);
|
||||
return updatedAfterAssign ?? detail!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> fetchDetail(
|
||||
int id, {
|
||||
bool includeSteps = true,
|
||||
bool includeHistories = true,
|
||||
}) async {
|
||||
return detail!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApprovalProceedStatus> canProceed(int id) async {
|
||||
return ApprovalProceedStatus(approvalId: id, canProceed: canProceedResult);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ApprovalAction>> listActions({bool activeOnly = true}) async {
|
||||
return actions;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> performStepAction(ApprovalStepActionInput input) async {
|
||||
actionInputs.add(input);
|
||||
return updatedAfterAction ?? detail!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PaginatedResult<Approval>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
int? transactionId,
|
||||
int? approvalStatusId,
|
||||
int? requestedById,
|
||||
List<String>? statusCodes,
|
||||
bool includePending = false,
|
||||
bool includeHistories = false,
|
||||
bool includeSteps = false,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> submit(ApprovalSubmissionInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> resubmit(ApprovalResubmissionInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> approve(ApprovalDecisionInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> reject(ApprovalDecisionInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> recall(ApprovalRecallInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PaginatedResult<ApprovalHistory>> listHistory({
|
||||
required int approvalId,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
int? actorId,
|
||||
int? approvalActionId,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> create(ApprovalCreateInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> update(ApprovalUpdateInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> restore(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeApprovalTemplateRepository implements ApprovalTemplateRepository {
|
||||
List<ApprovalTemplate> listResult = const [];
|
||||
ApprovalTemplate? detailResult;
|
||||
|
||||
@override
|
||||
Future<PaginatedResult<ApprovalTemplate>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String? query,
|
||||
bool? isActive,
|
||||
}) async {
|
||||
return PaginatedResult(
|
||||
items: listResult,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
total: listResult.length,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApprovalTemplate> fetchDetail(
|
||||
int id, {
|
||||
bool includeSteps = true,
|
||||
}) async {
|
||||
return detailResult!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApprovalTemplate> create(
|
||||
ApprovalTemplateInput input, {
|
||||
List<ApprovalTemplateStepInput> steps = const [],
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApprovalTemplate> update(
|
||||
int id,
|
||||
ApprovalTemplateInput input, {
|
||||
List<ApprovalTemplateStepInput>? steps,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApprovalTemplate> restore(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
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/approvals/domain/entities/approval.dart';
|
||||
import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_input.dart';
|
||||
import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_record.dart';
|
||||
import 'package:superport_v2/features/approvals/step/presentation/dialogs/approval_step_detail_dialog.dart';
|
||||
import 'package:superport_v2/widgets/components/superport_detail_dialog.dart';
|
||||
|
||||
import '../../../../../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
|
||||
|
||||
late ApprovalStepRecord sampleRecord;
|
||||
late ApprovalApprover approver;
|
||||
|
||||
setUp(() {
|
||||
final statusPending = ApprovalStatus(
|
||||
id: 1,
|
||||
name: '대기',
|
||||
color: '#0EA5E9',
|
||||
isTerminal: false,
|
||||
);
|
||||
approver = ApprovalApprover(id: 800, employeeNo: 'E800', name: '김승인');
|
||||
|
||||
final step = ApprovalStep(
|
||||
id: 900,
|
||||
stepOrder: 1,
|
||||
approver: approver,
|
||||
status: statusPending,
|
||||
assignedAt: DateTime(2024, 1, 10, 9),
|
||||
);
|
||||
|
||||
sampleRecord = ApprovalStepRecord(
|
||||
approvalId: 400,
|
||||
approvalNo: 'APP-2024-0400',
|
||||
transactionNo: 'TRX-400',
|
||||
templateName: '일반 결재선',
|
||||
step: step,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('결재 단계 상세 다이얼로그는 요약과 메타 정보를 노출한다', (tester) async {
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
final resultFuture = showApprovalStepDetailDialog(
|
||||
context: context,
|
||||
record: sampleRecord,
|
||||
dateFormat: dateFormat,
|
||||
onUpdate: (_, __) => Future.value(sampleRecord),
|
||||
onDelete: (_) => Future.value(true),
|
||||
onRestore: (_) => Future.value(sampleRecord),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text('결재 단계 상세'), findsAtLeastNWidgets(1));
|
||||
expect(find.text('결재 단계 1'), findsAtLeastNWidgets(1));
|
||||
expect(find.textContaining(approver.name), findsAtLeastNWidgets(1));
|
||||
expect(find.text('단계 순서'), findsWidgets);
|
||||
expect(find.text('상태'), findsWidgets);
|
||||
expect(find.text('승인자 사번'), findsWidgets);
|
||||
|
||||
await tester.tap(find.byTooltip('닫기'), warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
expect(await resultFuture, isNull);
|
||||
});
|
||||
|
||||
testWidgets('수정 탭에서 저장을 누르면 onUpdate 콜백이 입력값과 함께 호출된다', (tester) async {
|
||||
ApprovalStepInput? capturedInput;
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
final resultFuture = showApprovalStepDetailDialog(
|
||||
context: context,
|
||||
record: sampleRecord,
|
||||
dateFormat: dateFormat,
|
||||
onUpdate: (_, input) async {
|
||||
capturedInput = input;
|
||||
return sampleRecord.copyWith(
|
||||
step: sampleRecord.step.copyWith(
|
||||
stepOrder: input.stepOrder,
|
||||
approver: ApprovalApprover(
|
||||
id: input.approverId,
|
||||
employeeNo: 'E${input.approverId}',
|
||||
name: '변경 승인자',
|
||||
),
|
||||
note: input.note,
|
||||
),
|
||||
);
|
||||
},
|
||||
onDelete: (_) => Future.value(false),
|
||||
onRestore: (_) => Future.value(sampleRecord),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final tabsFinder = find.descendant(
|
||||
of: find.byType(SuperportDetailDialog),
|
||||
matching: find.byType(ShadTabs<String>),
|
||||
);
|
||||
final tabsState = tester.state(tabsFinder);
|
||||
final controller =
|
||||
(tabsState as dynamic).controller as ShadTabsController<String>;
|
||||
controller.select('edit');
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('approval_step_detail_step_order')),
|
||||
'3',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('approval_step_detail_approver_id')),
|
||||
'901',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('approval_step_detail_note')),
|
||||
'조정된 결재 단계',
|
||||
);
|
||||
|
||||
final submitButtonFinder = find.byKey(
|
||||
const ValueKey('approval_step_detail_submit'),
|
||||
);
|
||||
final submitButton = tester.widget<ShadButton>(submitButtonFinder);
|
||||
expect(submitButton.onPressed, isNotNull);
|
||||
submitButton.onPressed!();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final result = await resultFuture;
|
||||
expect(result, isNotNull);
|
||||
expect(result!.action, ApprovalStepDetailAction.updated);
|
||||
expect(capturedInput, isNotNull);
|
||||
expect(capturedInput!.stepOrder, 3);
|
||||
expect(capturedInput!.approverId, 901);
|
||||
expect(capturedInput!.note, '조정된 결재 단계');
|
||||
});
|
||||
|
||||
testWidgets('삭제 탭에서 삭제 버튼을 누르면 onDelete 콜백 성공 시 결과가 반환된다', (tester) async {
|
||||
var deleteCalled = false;
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
final resultFuture = showApprovalStepDetailDialog(
|
||||
context: context,
|
||||
record: sampleRecord,
|
||||
dateFormat: dateFormat,
|
||||
onUpdate: (_, __) => Future.value(sampleRecord),
|
||||
onDelete: (_) async {
|
||||
deleteCalled = true;
|
||||
return true;
|
||||
},
|
||||
onRestore: (_) => Future.value(sampleRecord),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tabsFinder = find.descendant(
|
||||
of: find.byType(SuperportDetailDialog),
|
||||
matching: find.byType(ShadTabs<String>),
|
||||
);
|
||||
final tabsState = tester.state(tabsFinder);
|
||||
final controller =
|
||||
(tabsState as dynamic).controller as ShadTabsController<String>;
|
||||
controller.select('delete');
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final deleteButtonFinder = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ShadButton &&
|
||||
widget.variant == ShadButtonVariant.destructive &&
|
||||
widget.child is Text &&
|
||||
(widget.child as Text).data == '삭제',
|
||||
);
|
||||
expect(deleteButtonFinder, findsOneWidget);
|
||||
final deleteButton = tester.widget<ShadButton>(deleteButtonFinder);
|
||||
expect(deleteButton.onPressed, isNotNull);
|
||||
deleteButton.onPressed!();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final result = await resultFuture;
|
||||
expect(deleteCalled, isTrue);
|
||||
expect(result, isNotNull);
|
||||
expect(result!.action, ApprovalStepDetailAction.deleted);
|
||||
});
|
||||
}
|
||||
@@ -108,20 +108,18 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('APP-2024-0001'), findsOneWidget);
|
||||
expect(find.text('최승인'), findsOneWidget);
|
||||
expect(find.text('APP-2024-0001'), findsWidgets);
|
||||
expect(find.text('최승인'), findsWidgets);
|
||||
|
||||
final detailButtonFinder = find.byKey(const ValueKey('step_detail_501_1'));
|
||||
final detailButton = tester.widget<ShadButton>(detailButtonFinder);
|
||||
detailButton.onPressed?.call();
|
||||
await tester.tap(find.text('APP-2024-0001'));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('결재 단계 상세'), findsOneWidget);
|
||||
expect(find.text('검토 필요'), findsOneWidget);
|
||||
expect(find.text('결재 단계 상세'), findsWidgets);
|
||||
expect(find.text('검토 필요'), findsWidgets);
|
||||
verify(() => repository.fetchDetail(501)).called(1);
|
||||
|
||||
await tester.tap(find.text('닫기'));
|
||||
await tester.tap(find.byTooltip('닫기'));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
@@ -198,7 +196,7 @@ void main() {
|
||||
expect(find.text('결재번호 APP-2024-0012 단계가 추가되었습니다.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('단계 수정 다이얼로그에서 저장을 호출한다', (tester) async {
|
||||
testWidgets('상세 다이얼로그에서 단계를 수정한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
|
||||
repository = _MockApprovalStepRepository();
|
||||
GetIt.I.registerLazySingleton<ApprovalStepRepository>(() => repository);
|
||||
@@ -237,6 +235,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
when(() => repository.fetchDetail(any())).thenAnswer((_) async => record);
|
||||
when(
|
||||
() => repository.update(any(), any()),
|
||||
).thenAnswer((_) async => updatedRecord);
|
||||
@@ -245,36 +244,43 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editButtonFinder = find.byKey(
|
||||
ValueKey('step_edit_${record.step.id}_${record.step.stepOrder}'),
|
||||
);
|
||||
final editButton = tester.widget<ShadButton>(editButtonFinder);
|
||||
editButton.onPressed?.call();
|
||||
await tester.tap(find.text('APP-2024-0001'));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editTabsState =
|
||||
tester.state(find.byWidgetPredicate((widget) => widget is ShadTabs))
|
||||
as dynamic;
|
||||
editTabsState.controller.select('edit');
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('step_form_step_order')),
|
||||
find.byKey(const ValueKey('approval_step_detail_step_order')),
|
||||
'2',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('step_form_approver_id')),
|
||||
find.byKey(const ValueKey('approval_step_detail_approver_id')),
|
||||
'30',
|
||||
);
|
||||
await tester.enterText(find.byKey(const ValueKey('step_form_note')), '수정됨');
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('approval_step_detail_note')),
|
||||
'수정됨',
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('step_form_submit')));
|
||||
final submitButtonFinder = find.byKey(
|
||||
const ValueKey('approval_step_detail_submit'),
|
||||
);
|
||||
final submitButton = tester.widget<ShadButton>(submitButtonFinder);
|
||||
submitButton.onPressed?.call();
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => repository.update(record.step.id!, any())).called(1);
|
||||
expect(
|
||||
find.text('결재번호 ${record.approvalNo} 단계 정보를 수정했습니다.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('결재 단계 정보를 수정했습니다.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('삭제 버튼 확인 후 저장소 삭제를 호출한다', (tester) async {
|
||||
testWidgets('상세 다이얼로그에서 단계를 삭제한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
|
||||
repository = _MockApprovalStepRepository();
|
||||
GetIt.I.registerLazySingleton<ApprovalStepRepository>(() => repository);
|
||||
@@ -297,26 +303,37 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
when(() => repository.fetchDetail(any())).thenAnswer((_) async => record);
|
||||
when(() => repository.delete(any())).thenAnswer((_) async {});
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ApprovalStepPage()));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final deleteFinder = find.byKey(const ValueKey('step_delete_501_1'));
|
||||
final deleteButton = tester.widget<ShadButton>(deleteFinder);
|
||||
await tester.tap(find.text('APP-2024-0001'));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final deleteTabsState =
|
||||
tester.state(find.byWidgetPredicate((widget) => widget is ShadTabs))
|
||||
as dynamic;
|
||||
deleteTabsState.controller.select('delete');
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final deleteButtonFinder = find.byKey(
|
||||
const ValueKey('approval_step_detail_delete'),
|
||||
);
|
||||
final deleteButton = tester.widget<ShadButton>(deleteButtonFinder);
|
||||
deleteButton.onPressed?.call();
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.widgetWithText(ShadButton, '삭제').last);
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => repository.delete(501)).called(1);
|
||||
expect(find.text('결재 단계를 삭제했습니다.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('복구 버튼 확인 후 저장소 복구를 호출한다', (tester) async {
|
||||
testWidgets('상세 다이얼로그에서 삭제된 단계를 복구한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
|
||||
repository = _MockApprovalStepRepository();
|
||||
GetIt.I.registerLazySingleton<ApprovalStepRepository>(() => repository);
|
||||
@@ -343,22 +360,35 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
when(
|
||||
() => repository.fetchDetail(any()),
|
||||
).thenAnswer((_) async => deletedRecord);
|
||||
when(() => repository.restore(any())).thenAnswer((_) async => record);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ApprovalStepPage()));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final restoreFinder = find.byKey(const ValueKey('step_restore_501_1'));
|
||||
final restoreButton = tester.widget<ShadButton>(restoreFinder);
|
||||
await tester.tap(find.text('APP-2024-0001'));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tabsState =
|
||||
tester.state(find.byWidgetPredicate((widget) => widget is ShadTabs))
|
||||
as dynamic;
|
||||
tabsState.controller.select('restore');
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final restoreButtonFinder = find.byKey(
|
||||
const ValueKey('approval_step_detail_restore'),
|
||||
);
|
||||
final restoreButton = tester.widget<ShadButton>(restoreButtonFinder);
|
||||
restoreButton.onPressed?.call();
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.widgetWithText(ShadButton, '복구').last);
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => repository.restore(501)).called(1);
|
||||
expect(find.text('결재 단계를 복구했습니다.'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport_v2/core/services/token_storage.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/entities/approval_template.dart';
|
||||
import 'package:superport_v2/features/approvals/template/presentation/dialogs/approval_template_detail_dialog.dart';
|
||||
import 'package:superport_v2/features/auth/application/auth_service.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/auth_session.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/authenticated_user.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/login_request.dart';
|
||||
import 'package:superport_v2/features/auth/domain/repositories/auth_repository.dart';
|
||||
import 'package:superport_v2/widgets/components/superport_detail_dialog.dart';
|
||||
|
||||
import '../../../../../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
|
||||
|
||||
late ApprovalTemplate sampleTemplate;
|
||||
late ApprovalTemplateApprover approver1;
|
||||
late ApprovalTemplateApprover approver2;
|
||||
|
||||
setUp(() {
|
||||
final getIt = GetIt.I;
|
||||
if (!getIt.isRegistered<AuthService>()) {
|
||||
getIt.registerSingleton<AuthService>(
|
||||
_StubAuthService(
|
||||
AuthSession(
|
||||
accessToken: 'access',
|
||||
refreshToken: 'refresh',
|
||||
expiresAt: DateTime.now().add(const Duration(hours: 1)),
|
||||
user: const AuthenticatedUser(
|
||||
id: 1,
|
||||
name: '테스터',
|
||||
employeeNo: 'E001',
|
||||
),
|
||||
permissions: const [],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
approver1 = ApprovalTemplateApprover(
|
||||
id: 501,
|
||||
employeeNo: 'E501',
|
||||
name: '1차 승인자',
|
||||
);
|
||||
approver2 = ApprovalTemplateApprover(
|
||||
id: 502,
|
||||
employeeNo: 'E502',
|
||||
name: '2차 승인자',
|
||||
);
|
||||
|
||||
sampleTemplate = ApprovalTemplate(
|
||||
id: 10,
|
||||
code: 'TEMP-10',
|
||||
name: '자산 구매 결재',
|
||||
description: '자산 구매 시 사용하는 기본 결재선',
|
||||
note: '월별로 재검토 필요',
|
||||
isActive: true,
|
||||
createdAt: DateTime(2024, 1, 1, 9),
|
||||
updatedAt: DateTime(2024, 1, 10, 10),
|
||||
steps: [
|
||||
ApprovalTemplateStep(
|
||||
id: 1001,
|
||||
stepOrder: 1,
|
||||
approver: approver1,
|
||||
note: '팀 리더 확인',
|
||||
),
|
||||
ApprovalTemplateStep(
|
||||
id: 1002,
|
||||
stepOrder: 2,
|
||||
approver: approver2,
|
||||
note: '경영진 승인',
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
final getIt = GetIt.I;
|
||||
if (getIt.isRegistered<AuthService>()) {
|
||||
getIt.unregister<AuthService>();
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('템플릿 상세 다이얼로그는 요약과 메타데이터를 info panel에 표시한다', (tester) async {
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
final resultFuture = showApprovalTemplateDetailDialog(
|
||||
context: context,
|
||||
dateFormat: dateFormat,
|
||||
template: sampleTemplate,
|
||||
onCreate: (_, __) => Future.value(null),
|
||||
onUpdate: (_, __, ___) => Future.value(sampleTemplate),
|
||||
onDelete: (_) => Future.value(false),
|
||||
onRestore: (_) => Future.value(null),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text('결재 템플릿 상세'), findsAtLeastNWidgets(1));
|
||||
expect(find.text(sampleTemplate.name), findsAtLeastNWidgets(1));
|
||||
expect(find.text('단계'), findsOneWidget);
|
||||
expect(find.text('코드'), findsWidgets);
|
||||
expect(find.text('TEMP-10'), findsWidgets);
|
||||
expect(find.text('상태'), findsWidgets);
|
||||
expect(find.text('생성일시'), findsWidgets);
|
||||
|
||||
expect(find.byType(SuperportDetailDialog), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('닫기'), warnIfMissed: false);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(await resultFuture, isNull);
|
||||
});
|
||||
|
||||
testWidgets('생성 모드에서 필수 정보를 입력하면 onCreate 콜백이 호출된다', (tester) async {
|
||||
final createdTemplates = <ApprovalTemplateInput>[];
|
||||
final createdSteps = <List<ApprovalTemplateStepInput>>[];
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
final resultFuture = showApprovalTemplateDetailDialog(
|
||||
context: context,
|
||||
dateFormat: dateFormat,
|
||||
template: null,
|
||||
onCreate: (input, steps) async {
|
||||
createdTemplates.add(input);
|
||||
createdSteps.add(steps);
|
||||
return sampleTemplate.copyWith(name: input.name);
|
||||
},
|
||||
onUpdate: (_, __, ___) => Future.value(null),
|
||||
onDelete: (_) => Future.value(false),
|
||||
onRestore: (_) => Future.value(null),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('template_form_name')),
|
||||
'신규 결재 템플릿',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('template_step_0_order')),
|
||||
'1',
|
||||
);
|
||||
|
||||
final approverFieldFinder = find.byKey(
|
||||
const ValueKey('template_step_0_approver'),
|
||||
);
|
||||
final approverFieldState = tester.state(approverFieldFinder) as dynamic;
|
||||
approverFieldState.widget.idController.text = approver1.id.toString();
|
||||
await tester.pump();
|
||||
|
||||
final submitButton = tester.widget<ShadButton>(
|
||||
find.widgetWithText(ShadButton, '등록'),
|
||||
);
|
||||
expect(submitButton.onPressed, isNotNull);
|
||||
submitButton.onPressed!();
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
expect(createdTemplates, isNotEmpty);
|
||||
|
||||
final result = await resultFuture;
|
||||
expect(result, isNotNull);
|
||||
expect(result!.action, ApprovalTemplateDetailAction.created);
|
||||
expect(createdTemplates, hasLength(1));
|
||||
expect(createdTemplates.first.name, '신규 결재 템플릿');
|
||||
expect(createdSteps.single.first.approverId, approver1.id);
|
||||
});
|
||||
|
||||
testWidgets('삭제 탭에서 삭제 버튼을 누르면 onDelete 콜백이 호출되어 결과가 반환된다', (tester) async {
|
||||
var deleteCalled = false;
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
final resultFuture = showApprovalTemplateDetailDialog(
|
||||
context: context,
|
||||
dateFormat: dateFormat,
|
||||
template: sampleTemplate,
|
||||
onCreate: (_, __) => Future.value(null),
|
||||
onUpdate: (_, __, ___) => Future.value(sampleTemplate),
|
||||
onDelete: (_) async {
|
||||
deleteCalled = true;
|
||||
return true;
|
||||
},
|
||||
onRestore: (_) => Future.value(null),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tabsFinder = find.descendant(
|
||||
of: find.byType(SuperportDetailDialog),
|
||||
matching: find.byType(ShadTabs<String>),
|
||||
);
|
||||
final tabsState = tester.state(tabsFinder);
|
||||
final controller =
|
||||
(tabsState as dynamic).controller as ShadTabsController<String>;
|
||||
controller.select('delete');
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final deleteButtonFinder = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ShadButton &&
|
||||
widget.variant == ShadButtonVariant.destructive &&
|
||||
widget.child is Text &&
|
||||
(widget.child as Text).data == '삭제',
|
||||
);
|
||||
expect(deleteButtonFinder, findsOneWidget);
|
||||
final deleteButton = tester.widget<ShadButton>(deleteButtonFinder);
|
||||
expect(deleteButton.onPressed, isNotNull);
|
||||
deleteButton.onPressed!();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final result = await resultFuture;
|
||||
expect(deleteCalled, isTrue);
|
||||
expect(result, isNotNull);
|
||||
expect(result!.action, ApprovalTemplateDetailAction.deleted);
|
||||
});
|
||||
}
|
||||
|
||||
class _StubAuthService extends AuthService {
|
||||
_StubAuthService(this._session)
|
||||
: super(
|
||||
repository: _FakeAuthRepository(),
|
||||
tokenStorage: _FakeTokenStorage(),
|
||||
);
|
||||
|
||||
final AuthSession _session;
|
||||
|
||||
@override
|
||||
AuthSession? get session => _session;
|
||||
}
|
||||
|
||||
class _FakeAuthRepository implements AuthRepository {
|
||||
@override
|
||||
Future<AuthSession> login(LoginRequest request) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthSession> refresh(String refreshToken) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeTokenStorage implements TokenStorage {
|
||||
String? access;
|
||||
String? refresh;
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
access = null;
|
||||
refresh = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> readAccessToken() async => access;
|
||||
|
||||
@override
|
||||
Future<String?> readRefreshToken() async => refresh;
|
||||
|
||||
@override
|
||||
Future<void> writeAccessToken(String? token) async {
|
||||
access = token;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> writeRefreshToken(String? token) async {
|
||||
refresh = token;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import 'package:superport_v2/features/approvals/domain/usecases/save_approval_te
|
||||
import 'package:superport_v2/features/approvals/template/presentation/pages/approval_template_page.dart';
|
||||
import 'package:superport_v2/features/approvals/shared/domain/entities/approval_approver_candidate.dart';
|
||||
import 'package:superport_v2/features/approvals/shared/domain/repositories/approval_approver_repository.dart';
|
||||
import 'package:superport_v2/features/approvals/shared/widgets/approver_autocomplete_field.dart';
|
||||
import 'package:superport_v2/features/auth/application/auth_service.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/auth_session.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/authenticated_user.dart';
|
||||
@@ -169,8 +170,8 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('AP_INBOUND'), findsOneWidget);
|
||||
expect(find.text('입고 템플릿'), findsOneWidget);
|
||||
expect(find.text('AP_INBOUND'), findsWidgets);
|
||||
expect(find.text('입고 템플릿'), findsWidgets);
|
||||
expect(find.textContaining('1. 최승인'), findsOneWidget);
|
||||
|
||||
verify(
|
||||
@@ -198,7 +199,7 @@ void main() {
|
||||
|
||||
when(
|
||||
() => repository.create(any(), steps: any(named: 'steps')),
|
||||
).thenAnswer((_) async => buildTemplate());
|
||||
).thenAnswer((_) async => buildTemplate().copyWith(name: '신규 템플릿'));
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ApprovalTemplatePage()));
|
||||
await tester.pump();
|
||||
@@ -207,43 +208,26 @@ void main() {
|
||||
await tester.tap(find.text('템플릿 생성'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dialogFieldsFinder = find.descendant(
|
||||
of: find.byType(Dialog),
|
||||
matching: find.byType(EditableText),
|
||||
skipOffstage: false,
|
||||
);
|
||||
final dialogFieldElements = dialogFieldsFinder.evaluate().toList();
|
||||
expect(dialogFieldElements.length, greaterThanOrEqualTo(4));
|
||||
|
||||
await tester.enterText(
|
||||
find.byWidget(dialogFieldElements[0].widget),
|
||||
'AP_NEW',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byWidget(dialogFieldElements[1].widget),
|
||||
find.byKey(const ValueKey('template_form_name')),
|
||||
'신규 템플릿',
|
||||
);
|
||||
|
||||
final stepFieldsFinder = find.descendant(
|
||||
of: find.byKey(const ValueKey('step_field_0')),
|
||||
matching: find.byType(EditableText),
|
||||
skipOffstage: false,
|
||||
final approverField = tester.widget<ApprovalApproverAutocompleteField>(
|
||||
find.byType(ApprovalApproverAutocompleteField).first,
|
||||
);
|
||||
final stepFieldElements = stepFieldsFinder.evaluate().toList();
|
||||
expect(stepFieldElements.length, greaterThanOrEqualTo(2));
|
||||
|
||||
await tester.enterText(find.byWidget(stepFieldElements[1].widget), '33');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
approverField.idController.text = '33';
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('생성 완료'));
|
||||
final createButtonFinder = find.widgetWithText(ShadButton, '등록').last;
|
||||
final createButton = tester.widget<ShadButton>(createButtonFinder);
|
||||
createButton.onPressed?.call();
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(
|
||||
() => repository.create(any(), steps: any(named: 'steps')),
|
||||
).called(1);
|
||||
expect(find.text('템플릿 "신규 템플릿"을 생성했습니다.'), findsOneWidget);
|
||||
expect(find.textContaining('템플릿 "신규 템플릿"'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('보기 버튼을 눌러 템플릿 단계를 미리본다', (tester) async {
|
||||
@@ -273,25 +257,25 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final previewFinder = find.text('보기', skipOffstage: false);
|
||||
|
||||
await tester.dragUntilVisible(
|
||||
previewFinder,
|
||||
find.text(template.name),
|
||||
const Offset(-200, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(previewFinder);
|
||||
await tester.tap(find.text(template.name));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(template.name), findsWidgets);
|
||||
final detailTabsState =
|
||||
tester.state(find.byWidgetPredicate((widget) => widget is ShadTabs))
|
||||
as dynamic;
|
||||
detailTabsState.controller.select('steps');
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('사번 E001'), findsOneWidget);
|
||||
|
||||
verify(
|
||||
() => repository.fetchDetail(template.id, includeSteps: true),
|
||||
).called(1);
|
||||
|
||||
await tester.tap(find.byTooltip('닫기'));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('수정 플로우에서 fetchDetail 후 update를 호출한다', (tester) async {
|
||||
@@ -334,31 +318,25 @@ void main() {
|
||||
repository.list(page: 1, pageSize: 20, query: null, isActive: null),
|
||||
).called(1);
|
||||
|
||||
await tester.dragUntilVisible(
|
||||
find.text('수정'),
|
||||
find.text('입고 템플릿'),
|
||||
const Offset(-200, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('수정').first);
|
||||
await tester.tap(find.text('입고 템플릿'));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editDialogFields = find.descendant(
|
||||
of: find.byType(Dialog),
|
||||
matching: find.byType(EditableText),
|
||||
skipOffstage: false,
|
||||
);
|
||||
final editFieldElements = editDialogFields.evaluate().toList();
|
||||
expect(editFieldElements.length, greaterThanOrEqualTo(1));
|
||||
final tabsState =
|
||||
tester.state(find.byWidgetPredicate((widget) => widget is ShadTabs))
|
||||
as dynamic;
|
||||
tabsState.controller.select('edit');
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byWidget(editFieldElements[0].widget),
|
||||
find.byKey(const ValueKey('template_form_name')),
|
||||
'수정된 템플릿',
|
||||
);
|
||||
|
||||
await tester.tap(find.text('수정 완료'));
|
||||
final saveButtonFinder = find.widgetWithText(ShadButton, '저장').last;
|
||||
final saveButton = tester.widget<ShadButton>(saveButtonFinder);
|
||||
saveButton.onPressed?.call();
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -367,7 +345,7 @@ void main() {
|
||||
() => repository.update(10, any(), steps: any(named: 'steps')),
|
||||
).called(1);
|
||||
|
||||
expect(find.text('템플릿 "입고 템플릿"을(를) 수정했습니다.'), findsOneWidget);
|
||||
expect(find.text('템플릿 "수정된 템플릿"을(를) 수정했습니다.'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'package:superport_v2/features/inventory/inbound/presentation/models/inbound_record.dart';
|
||||
import 'package:superport_v2/features/inventory/inbound/presentation/widgets/inbound_detail_view.dart';
|
||||
|
||||
import '../../../../../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
final currencyFormatter = intl.NumberFormat.currency(
|
||||
locale: 'ko_KR',
|
||||
symbol: '₩',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
|
||||
InboundRecord buildRecord() {
|
||||
return InboundRecord(
|
||||
id: 1,
|
||||
number: 'IN-1',
|
||||
transactionNumber: 'IN-1',
|
||||
transactionType: '입고',
|
||||
processedAt: DateTime(2024, 1, 1, 9),
|
||||
warehouse: '서울 1창고',
|
||||
status: '완료',
|
||||
writer: '홍길동',
|
||||
remark: '비고',
|
||||
items: [
|
||||
InboundLineItem(
|
||||
id: 10,
|
||||
product: '테스트 제품',
|
||||
manufacturer: '테스트 제조사',
|
||||
unit: 'EA',
|
||||
quantity: 3,
|
||||
price: 1000,
|
||||
remark: '정상',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('InboundDetailView는 라인 품목과 경고 배지를 렌더링한다', (tester) async {
|
||||
final record = buildRecord();
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildTestApp(
|
||||
InboundDetailView(
|
||||
record: record,
|
||||
currencyFormatter: currencyFormatter,
|
||||
transitionsEnabled: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('재고 상태 전이가 비활성화된 상태입니다.'), findsOneWidget);
|
||||
expect(find.text('라인 품목'), findsOneWidget);
|
||||
expect(find.text('테스트 제품'), findsOneWidget);
|
||||
expect(find.text('테스트 제조사'), findsOneWidget);
|
||||
expect(find.text('EA'), findsOneWidget);
|
||||
expect(find.text('3'), findsWidgets);
|
||||
expect(find.text('₩1,000'), findsOneWidget);
|
||||
expect(find.text('정상'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'package:superport_v2/features/inventory/outbound/presentation/models/outbound_record.dart';
|
||||
import 'package:superport_v2/features/inventory/outbound/presentation/widgets/outbound_detail_view.dart';
|
||||
|
||||
import '../../../../../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
final currencyFormatter = intl.NumberFormat.currency(
|
||||
locale: 'ko_KR',
|
||||
symbol: '₩',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
|
||||
OutboundRecord buildRecord() {
|
||||
return OutboundRecord(
|
||||
id: 1,
|
||||
number: 'OUT-1',
|
||||
transactionNumber: 'OUT-1',
|
||||
transactionType: '출고',
|
||||
processedAt: DateTime(2024, 2, 10, 11),
|
||||
warehouse: '서울 1창고',
|
||||
status: '출고 완료',
|
||||
writer: '관리자',
|
||||
remark: '긴급 출고',
|
||||
items: [
|
||||
OutboundLineItem(
|
||||
id: 20,
|
||||
product: '출고 제품',
|
||||
manufacturer: '제조사',
|
||||
unit: 'EA',
|
||||
quantity: 5,
|
||||
price: 2000,
|
||||
remark: '양호',
|
||||
),
|
||||
],
|
||||
customers: [
|
||||
OutboundCustomer(id: 1, customerId: 7, code: 'CUS-01', name: '테스트 고객'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('OutboundDetailView는 고객 배지와 라인 테이블을 표시한다', (tester) async {
|
||||
final record = buildRecord();
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildTestApp(
|
||||
OutboundDetailView(
|
||||
record: record,
|
||||
currencyFormatter: currencyFormatter,
|
||||
transitionsEnabled: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('출고 고객사'), findsOneWidget);
|
||||
expect(find.text('테스트 고객 · CUS-01'), findsOneWidget);
|
||||
expect(find.text('라인 품목'), findsOneWidget);
|
||||
expect(find.text('출고 제품'), findsOneWidget);
|
||||
expect(find.text('제조사'), findsWidgets);
|
||||
expect(find.text('EA'), findsWidgets);
|
||||
expect(find.text('5'), findsWidgets);
|
||||
expect(find.text('₩2,000'), findsOneWidget);
|
||||
expect(find.text('양호'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'package:superport_v2/features/inventory/rental/presentation/models/rental_record.dart';
|
||||
import 'package:superport_v2/features/inventory/rental/presentation/widgets/rental_detail_view.dart';
|
||||
|
||||
import '../../../../../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
final currencyFormatter = intl.NumberFormat.currency(
|
||||
locale: 'ko_KR',
|
||||
symbol: '₩',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
|
||||
RentalRecord buildRecord() {
|
||||
return RentalRecord(
|
||||
id: 1,
|
||||
number: 'RENT-1',
|
||||
transactionNumber: 'RENT-1',
|
||||
transactionType: '대여',
|
||||
rentalType: '대여',
|
||||
processedAt: DateTime(2024, 3, 5, 15),
|
||||
warehouse: '부산 창고',
|
||||
status: '반납 대기',
|
||||
writer: '김운영',
|
||||
remark: '대여 비고',
|
||||
returnDueDate: DateTime(2024, 3, 20),
|
||||
items: [
|
||||
RentalLineItem(
|
||||
id: 30,
|
||||
product: '대여 품목',
|
||||
manufacturer: '렌탈 제조사',
|
||||
unit: 'EA',
|
||||
quantity: 2,
|
||||
price: 5000,
|
||||
remark: '',
|
||||
),
|
||||
],
|
||||
customers: [
|
||||
RentalCustomer(id: 2, customerId: 9, code: 'RC-09', name: '렌탈 고객'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('RentalDetailView는 고객 배지와 라인 테이블을 렌더링한다', (tester) async {
|
||||
final record = buildRecord();
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildTestApp(
|
||||
RentalDetailView(
|
||||
record: record,
|
||||
currencyFormatter: currencyFormatter,
|
||||
transitionsEnabled: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('재고 상태 전이가 비활성화된 상태입니다.'), findsOneWidget);
|
||||
expect(find.text('연결 고객사'), findsOneWidget);
|
||||
expect(find.text('렌탈 고객 · RC-09'), findsOneWidget);
|
||||
expect(find.text('라인 품목'), findsOneWidget);
|
||||
expect(find.text('대여 품목'), findsOneWidget);
|
||||
expect(find.text('렌탈 제조사'), findsOneWidget);
|
||||
expect(find.text('EA'), findsOneWidget);
|
||||
expect(find.text('2'), findsWidgets);
|
||||
expect(find.text('₩5,000'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/features/inventory/transactions/presentation/widgets/transaction_detail_dialog.dart';
|
||||
|
||||
import '../../../../../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'showInventoryTransactionDetailDialog는 summary/metadata/섹션을 렌더링한다',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
unawaited(
|
||||
showInventoryTransactionDetailDialog<void>(
|
||||
context: context,
|
||||
title: '트랜잭션 상세',
|
||||
description: '라인 품목과 상태를 확인하세요.',
|
||||
summary: const Text('TRX-123'),
|
||||
summaryBadges: const [ShadBadge(child: Text('완료'))],
|
||||
metadata: [
|
||||
SuperportDetailMetadata.text(label: '상태', value: '완료'),
|
||||
SuperportDetailMetadata.text(label: '창고', value: '서울 1창고'),
|
||||
],
|
||||
sections: [
|
||||
SuperportDetailDialogSection(
|
||||
id: 'lines',
|
||||
label: '라인 품목',
|
||||
builder: (_) => const Text('라인 섹션'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('트랜잭션 상세'), findsOneWidget);
|
||||
expect(find.text('TRX-123'), findsOneWidget);
|
||||
expect(find.text('상태'), findsOneWidget);
|
||||
expect(find.text('라인 섹션'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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, '운영팀');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
135
test/features/masters/vendor/presentation/dialogs/vendor_detail_dialog_test.dart
vendored
Normal file
135
test/features/masters/vendor/presentation/dialogs/vendor_detail_dialog_test.dart
vendored
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
35
test/helpers/tester_extensions.dart
Normal file
35
test/helpers/tester_extensions.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
extension WidgetTesterDialogActions on WidgetTester {
|
||||
/// 다이얼로그 내 `ShadButton`을 텍스트 라벨로 찾아 탭한다.
|
||||
Future<void> tapShadButton(String label, {bool settle = true}) async {
|
||||
final buttonFinder = find.widgetWithText(ShadButton, label);
|
||||
expect(
|
||||
buttonFinder,
|
||||
findsWidgets,
|
||||
reason: '텍스트 "$label" 를 가진 ShadButton을 찾지 못했습니다.',
|
||||
);
|
||||
final elements = buttonFinder.evaluate().toList(growable: false);
|
||||
final targetIndex = elements.lastIndexWhere((element) {
|
||||
final widget = element.widget;
|
||||
return widget is ShadButton && widget.onPressed != null;
|
||||
});
|
||||
expect(
|
||||
targetIndex,
|
||||
isNot(-1),
|
||||
reason: '눌러야 할 활성화된 "$label" 버튼을 찾을 수 없습니다.',
|
||||
);
|
||||
final targetButton = buttonFinder.at(targetIndex);
|
||||
final targetText = find.descendant(
|
||||
of: targetButton,
|
||||
matching: find.byType(Text),
|
||||
);
|
||||
await ensureVisible(targetText.first);
|
||||
await tap(targetText.first, warnIfMissed: false);
|
||||
if (settle) {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
}
|
||||
99
test/widgets/superport_detail_dialog_test.dart
Normal file
99
test/widgets/superport_detail_dialog_test.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/widgets/components/superport_detail_dialog.dart';
|
||||
|
||||
import '../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('SuperportDetailDialog은 요약과 메타데이터를 렌더링한다', (tester) async {
|
||||
final widget = SuperportDetailDialog(
|
||||
summary: const Text('벤더 요약'),
|
||||
summaryBadges: const [ShadBadge(child: Text('활성'))],
|
||||
metadata: [
|
||||
SuperportDetailMetadata.text(label: 'ID', value: '#42'),
|
||||
SuperportDetailMetadata.text(label: '상태', value: '사용 중'),
|
||||
],
|
||||
sections: [
|
||||
SuperportDetailDialogSection(
|
||||
id: 'overview',
|
||||
label: '개요',
|
||||
builder: (_) => const Text('개요 섹션'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildTestApp(widget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('벤더 요약'), findsOneWidget);
|
||||
expect(find.text('활성'), findsOneWidget);
|
||||
expect(find.text('ID'), findsOneWidget);
|
||||
expect(find.text('#42'), findsOneWidget);
|
||||
expect(find.text('상태'), findsOneWidget);
|
||||
expect(find.text('사용 중'), findsOneWidget);
|
||||
expect(find.text('개요 섹션'), findsOneWidget);
|
||||
expect(find.byType(ShadTabs), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('SuperportDetailDialog은 여러 섹션 탭을 전환한다', (tester) async {
|
||||
final widget = SuperportDetailDialog(
|
||||
sections: [
|
||||
SuperportDetailDialogSection(
|
||||
id: 'overview',
|
||||
label: '개요',
|
||||
builder: (_) => const Text('개요 내용'),
|
||||
),
|
||||
SuperportDetailDialogSection(
|
||||
id: 'history',
|
||||
label: '이력',
|
||||
builder: (_) => const Text('이력 내용'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildTestApp(widget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tabsFinder = find.byWidgetPredicate((widget) => widget is ShadTabs);
|
||||
expect(tabsFinder, findsOneWidget);
|
||||
expect(find.text('개요 내용'), findsOneWidget);
|
||||
expect(find.text('이력 내용'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('이력'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('개요 내용'), findsNothing);
|
||||
expect(find.text('이력 내용'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('showSuperportDetailDialog는 기본 닫기 버튼을 렌더링하지 않는다', (tester) async {
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
unawaited(
|
||||
showSuperportDetailDialog<void>(
|
||||
context: context,
|
||||
title: '테스트 다이얼로그',
|
||||
sections: [
|
||||
SuperportDetailDialogSection(
|
||||
id: 'overview',
|
||||
label: '개요',
|
||||
builder: (_) => const Text('본문'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('테스트 다이얼로그'), findsOneWidget);
|
||||
expect(find.text('본문'), findsOneWidget);
|
||||
expect(find.text('닫기'), findsNothing);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user