feat(inventory): 재고 현황 요약/상세 플로우를 릴리스
- lib/features/inventory/summary 계층과 warehouse select 위젯을 추가해 목록/상세, 자동 새로고침, 필터, 상세 시트를 구현 - PermissionBootstrapper, scope 파서, 라우트 가드로 inventory.view 기반 권한 부여와 메뉴 노출을 통합(lib/core, lib/main.dart 등) - Inventory Summary API/QA/Audit 문서와 PR 템플릿, CHANGELOG를 신규 스펙과 검증 커맨드로 업데이트 - DTO 직렬화 의존성을 추가하고 Golden·Widget·단위 테스트를 작성했으며 flutter analyze / flutter test --coverage를 통과
This commit is contained in:
@@ -170,11 +170,7 @@ void main() {
|
||||
|
||||
record = historyRepository.listResult!.items.first;
|
||||
|
||||
user = const AuthenticatedUser(
|
||||
id: 42,
|
||||
name: '결재자',
|
||||
employeeNo: 'E042',
|
||||
);
|
||||
user = const AuthenticatedUser(id: 42, name: '결재자', employeeNo: 'E042');
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
@@ -199,148 +195,143 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'showApprovalHistoryDetailDialog 결재 요약과 타임라인을 표시한다',
|
||||
(tester) async {
|
||||
await openDialog(tester);
|
||||
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.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(
|
||||
find.textContaining(
|
||||
'상신자 ${sampleApproval.requester.name} (${sampleApproval.requester.employeeNo})',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.textContaining('총 ${sampleApproval.steps.length}단계'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('승인'), findsWidgets);
|
||||
|
||||
expect(approvalRepository.listHistoryCalls, isNotEmpty);
|
||||
},
|
||||
);
|
||||
expect(approvalRepository.listHistoryCalls, isNotEmpty);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'회수 버튼을 누르면 recallApproval이 호출되어 감사 로그가 새로고침된다',
|
||||
(tester) async {
|
||||
await openDialog(tester);
|
||||
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 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 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 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();
|
||||
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));
|
||||
},
|
||||
);
|
||||
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),
|
||||
);
|
||||
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,
|
||||
);
|
||||
approvalRepository
|
||||
..detail = rejectedApproval
|
||||
..resubmitResult = resubmittedApproval;
|
||||
record = record.copyWith(
|
||||
action: ApprovalAction(id: 33, name: '반려', code: 'reject'),
|
||||
toStatus: statusRejected,
|
||||
stepOrder: 2,
|
||||
);
|
||||
|
||||
await openDialog(tester);
|
||||
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 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 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 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();
|
||||
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));
|
||||
},
|
||||
);
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user