결재 단계 편집 다이얼로그 구현
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/entities/approval.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';
|
||||
|
||||
class _MockApprovalHistoryRepository extends Mock
|
||||
implements ApprovalHistoryRepository {}
|
||||
|
||||
void main() {
|
||||
late ApprovalHistoryController controller;
|
||||
late _MockApprovalHistoryRepository repository;
|
||||
|
||||
final record = ApprovalHistoryRecord(
|
||||
id: 1,
|
||||
approvalId: 10,
|
||||
approvalNo: 'APP-2024-0001',
|
||||
stepOrder: 1,
|
||||
action: ApprovalAction(id: 11, name: 'approve'),
|
||||
fromStatus: ApprovalStatus(id: 1, name: '대기', color: null),
|
||||
toStatus: ApprovalStatus(id: 2, name: '승인', color: null),
|
||||
approver: ApprovalApprover(id: 21, employeeNo: 'E001', name: '최승인'),
|
||||
actionAt: DateTime(2024, 4, 1, 9, 30),
|
||||
note: '승인 완료',
|
||||
);
|
||||
|
||||
PaginatedResult<ApprovalHistoryRecord> createResult(
|
||||
List<ApprovalHistoryRecord> items,
|
||||
) {
|
||||
return PaginatedResult<ApprovalHistoryRecord>(
|
||||
items: items,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: items.length,
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
repository = _MockApprovalHistoryRepository();
|
||||
controller = ApprovalHistoryController(repository: repository);
|
||||
});
|
||||
|
||||
test('fetch 성공 시 결과를 갱신한다', () async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
action: any(named: 'action'),
|
||||
from: any(named: 'from'),
|
||||
to: any(named: 'to'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult([record]));
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.result?.items, isNotEmpty);
|
||||
expect(controller.errorMessage, isNull);
|
||||
verify(
|
||||
() => repository.list(
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
query: null,
|
||||
action: null,
|
||||
from: null,
|
||||
to: null,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('필터 적용 시 파라미터를 전달한다', () async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
action: any(named: 'action'),
|
||||
from: any(named: 'from'),
|
||||
to: any(named: 'to'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult([record]));
|
||||
|
||||
controller.updateQuery('APP');
|
||||
controller.updateActionFilter(ApprovalHistoryActionFilter.approve);
|
||||
controller.updateDateRange(DateTime(2024, 4, 1), DateTime(2024, 4, 30));
|
||||
|
||||
await controller.fetch(page: 2);
|
||||
|
||||
verify(
|
||||
() => repository.list(
|
||||
page: 2,
|
||||
pageSize: 20,
|
||||
query: 'APP',
|
||||
action: 'approve',
|
||||
from: DateTime(2024, 4, 1),
|
||||
to: DateTime(2024, 4, 30),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('에러 발생 시 errorMessage에 저장한다', () async {
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
action: any(named: 'action'),
|
||||
from: any(named: 'from'),
|
||||
to: any(named: 'to'),
|
||||
),
|
||||
).thenThrow(Exception('fail'));
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
expect(controller.result, isNull);
|
||||
});
|
||||
|
||||
test('clearFilters가 상태를 초기화한다', () {
|
||||
controller.updateQuery('APP');
|
||||
controller.updateActionFilter(ApprovalHistoryActionFilter.comment);
|
||||
controller.updateDateRange(DateTime(2024, 4, 1), DateTime(2024, 4, 5));
|
||||
|
||||
controller.clearFilters();
|
||||
|
||||
expect(controller.query, isEmpty);
|
||||
expect(controller.actionFilter, ApprovalHistoryActionFilter.all);
|
||||
expect(controller.from, isNull);
|
||||
expect(controller.to, isNull);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/approvals/domain/entities/approval.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/pages/approval_history_page.dart';
|
||||
|
||||
class _MockApprovalHistoryRepository extends Mock
|
||||
implements ApprovalHistoryRepository {}
|
||||
|
||||
Widget _buildApp(Widget child) {
|
||||
return MaterialApp(
|
||||
home: ShadTheme(
|
||||
data: ShadThemeData(
|
||||
colorScheme: const ShadSlateColorScheme.light(),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
child: Scaffold(body: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late _MockApprovalHistoryRepository repository;
|
||||
|
||||
final record = ApprovalHistoryRecord(
|
||||
id: 1,
|
||||
approvalId: 10,
|
||||
approvalNo: 'APP-2024-0001',
|
||||
stepOrder: 1,
|
||||
action: ApprovalAction(id: 11, name: 'approve'),
|
||||
fromStatus: ApprovalStatus(id: 1, name: '대기', color: null),
|
||||
toStatus: ApprovalStatus(id: 2, name: '승인', color: null),
|
||||
approver: ApprovalApprover(id: 21, employeeNo: 'E001', name: '최승인'),
|
||||
actionAt: DateTime(2024, 4, 1, 9, 30),
|
||||
note: '승인 완료',
|
||||
);
|
||||
|
||||
tearDown(() async {
|
||||
await GetIt.I.reset();
|
||||
dotenv.clean();
|
||||
});
|
||||
|
||||
testWidgets('플래그 Off 시 스펙 페이지를 노출한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=false\n');
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ApprovalHistoryPage()));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('결재 이력 조회'), findsOneWidget);
|
||||
expect(find.text('결재 단계별 변경 이력을 조회합니다.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('이력 목록을 렌더링하고 검색 필터를 적용한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
|
||||
repository = _MockApprovalHistoryRepository();
|
||||
GetIt.I.registerLazySingleton<ApprovalHistoryRepository>(() => repository);
|
||||
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
action: any(named: 'action'),
|
||||
from: any(named: 'from'),
|
||||
to: any(named: 'to'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<ApprovalHistoryRecord>(
|
||||
items: [record],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ApprovalHistoryPage()));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('APP-2024-0001'), findsOneWidget);
|
||||
expect(find.text('승인 완료'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.byType(ShadInput).first, 'APP-2024');
|
||||
await tester.tap(find.text('검색 적용'));
|
||||
await tester.pump();
|
||||
|
||||
verify(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: 20,
|
||||
query: 'APP-2024',
|
||||
action: null,
|
||||
from: null,
|
||||
to: null,
|
||||
),
|
||||
).called(greaterThanOrEqualTo(1));
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:mocktail/mocktail.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/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/domain/repositories/approval_step_repository.dart';
|
||||
import 'package:superport_v2/features/approvals/step/presentation/controllers/approval_step_controller.dart';
|
||||
@@ -11,6 +12,12 @@ class _MockApprovalStepRepository extends Mock
|
||||
implements ApprovalStepRepository {}
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(
|
||||
ApprovalStepInput(approvalId: 1, stepOrder: 1, approverId: 1),
|
||||
);
|
||||
});
|
||||
|
||||
late ApprovalStepController controller;
|
||||
late _MockApprovalStepRepository repository;
|
||||
|
||||
@@ -142,4 +149,76 @@ void main() {
|
||||
expect(detail, isNull);
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
|
||||
test('createStep 성공 시 생성된 레코드를 반환한다', () async {
|
||||
when(() => repository.create(any())).thenAnswer((_) async => sampleRecord);
|
||||
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
statusId: any(named: 'statusId'),
|
||||
approverId: any(named: 'approverId'),
|
||||
approvalId: any(named: 'approvalId'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult([sampleRecord]));
|
||||
|
||||
final result = await controller.createStep(
|
||||
ApprovalStepInput(approvalId: 10, stepOrder: 1, approverId: 21),
|
||||
);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(controller.errorMessage, isNull);
|
||||
verify(() => repository.create(any())).called(1);
|
||||
});
|
||||
|
||||
test('createStep 실패 시 null을 반환하고 에러를 기록한다', () async {
|
||||
when(() => repository.create(any())).thenThrow(Exception('fail'));
|
||||
|
||||
final result = await controller.createStep(
|
||||
ApprovalStepInput(approvalId: 10, stepOrder: 1, approverId: 21),
|
||||
);
|
||||
|
||||
expect(result, isNull);
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
|
||||
test('updateStep 성공 시 수정된 레코드를 반환한다', () async {
|
||||
when(
|
||||
() => repository.update(any(), any()),
|
||||
).thenAnswer((_) async => sampleRecord);
|
||||
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
statusId: any(named: 'statusId'),
|
||||
approverId: any(named: 'approverId'),
|
||||
approvalId: any(named: 'approvalId'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult([sampleRecord]));
|
||||
|
||||
final result = await controller.updateStep(
|
||||
100,
|
||||
ApprovalStepInput(stepOrder: 2, approverId: 25, approvalId: 10),
|
||||
);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(controller.errorMessage, isNull);
|
||||
verify(() => repository.update(100, any())).called(1);
|
||||
});
|
||||
|
||||
test('updateStep 실패 시 null을 반환한다', () async {
|
||||
when(() => repository.update(any(), any())).thenThrow(Exception('fail'));
|
||||
|
||||
final result = await controller.updateStep(
|
||||
100,
|
||||
ApprovalStepInput(stepOrder: 2, approverId: 25, approvalId: 10),
|
||||
);
|
||||
|
||||
expect(result, isNull);
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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/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/domain/repositories/approval_step_repository.dart';
|
||||
import 'package:superport_v2/features/approvals/step/presentation/pages/approval_step_page.dart';
|
||||
@@ -29,6 +30,12 @@ Widget _buildApp(Widget child) {
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(
|
||||
ApprovalStepInput(approvalId: 1, stepOrder: 1, approverId: 1),
|
||||
);
|
||||
});
|
||||
|
||||
late _MockApprovalStepRepository repository;
|
||||
|
||||
final record = ApprovalStepRecord(
|
||||
@@ -107,4 +114,153 @@ void main() {
|
||||
await tester.tap(find.text('닫기'));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('단계 추가 다이얼로그에서 저장을 호출한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
|
||||
repository = _MockApprovalStepRepository();
|
||||
GetIt.I.registerLazySingleton<ApprovalStepRepository>(() => repository);
|
||||
|
||||
final createdRecord = ApprovalStepRecord(
|
||||
approvalId: 12,
|
||||
approvalNo: 'APP-2024-0012',
|
||||
transactionNo: 'TRX-2024-012',
|
||||
templateName: '입고 기본',
|
||||
step: ApprovalStep(
|
||||
id: 777,
|
||||
stepOrder: 2,
|
||||
approver: ApprovalApprover(id: 33, employeeNo: 'E033', name: '김승인2'),
|
||||
status: ApprovalStatus(id: 1, name: '승인대기', color: null),
|
||||
assignedAt: DateTime(2024, 4, 2, 9),
|
||||
decidedAt: null,
|
||||
note: '신규 단계',
|
||||
),
|
||||
);
|
||||
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
statusId: any(named: 'statusId'),
|
||||
approverId: any(named: 'approverId'),
|
||||
approvalId: any(named: 'approvalId'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<ApprovalStepRecord>(
|
||||
items: [record],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
when(() => repository.create(any())).thenAnswer((_) async => createdRecord);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ApprovalStepPage()));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('approval_step_create')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('step_form_approval_id')),
|
||||
'12',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('step_form_step_order')),
|
||||
'2',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('step_form_approver_id')),
|
||||
'33',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('step_form_note')),
|
||||
'신규 단계',
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('step_form_submit')));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => repository.create(any())).called(1);
|
||||
expect(find.text('결재번호 APP-2024-0012 단계가 추가되었습니다.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('단계 수정 다이얼로그에서 저장을 호출한다', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
|
||||
repository = _MockApprovalStepRepository();
|
||||
GetIt.I.registerLazySingleton<ApprovalStepRepository>(() => repository);
|
||||
|
||||
final updatedRecord = ApprovalStepRecord(
|
||||
approvalId: record.approvalId,
|
||||
approvalNo: record.approvalNo,
|
||||
transactionNo: record.transactionNo,
|
||||
templateName: record.templateName,
|
||||
step: ApprovalStep(
|
||||
id: record.step.id,
|
||||
stepOrder: 2,
|
||||
approver: ApprovalApprover(id: 30, employeeNo: 'E030', name: '박수정'),
|
||||
status: record.step.status,
|
||||
assignedAt: record.step.assignedAt,
|
||||
decidedAt: record.step.decidedAt,
|
||||
note: '수정됨',
|
||||
),
|
||||
);
|
||||
|
||||
when(
|
||||
() => repository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
statusId: any(named: 'statusId'),
|
||||
approverId: any(named: 'approverId'),
|
||||
approvalId: any(named: 'approvalId'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<ApprovalStepRecord>(
|
||||
items: [record],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
when(
|
||||
() => repository.update(any(), any()),
|
||||
).thenAnswer((_) async => updatedRecord);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const ApprovalStepPage()));
|
||||
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.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('step_form_step_order')),
|
||||
'2',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('step_form_approver_id')),
|
||||
'30',
|
||||
);
|
||||
await tester.enterText(find.byKey(const ValueKey('step_form_note')), '수정됨');
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('step_form_submit')));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => repository.update(record.step.id!, any())).called(1);
|
||||
expect(
|
||||
find.text('결재번호 ${record.approvalNo} 단계 정보를 수정했습니다.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user