결재 단계 목록 화면과 테스트 도입

This commit is contained in:
JiWoong Sul
2025-09-25 16:41:22 +09:00
parent 86d3f5bf21
commit 35b9002688
10 changed files with 981 additions and 24 deletions

View File

@@ -0,0 +1,144 @@
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/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';
class _MockApprovalStepRepository extends Mock
implements ApprovalStepRepository {}
void main() {
late ApprovalStepController controller;
late _MockApprovalStepRepository repository;
final sampleRecord = ApprovalStepRecord(
approvalId: 10,
approvalNo: 'APP-2024-0001',
transactionNo: 'TRX-2024-01',
templateName: '입고 기본',
step: ApprovalStep(
id: 100,
stepOrder: 1,
approver: ApprovalApprover(id: 21, employeeNo: 'E001', name: '최승인'),
status: ApprovalStatus(id: 1, name: '승인대기', color: null),
assignedAt: DateTime(2024, 4, 1, 9),
decidedAt: null,
note: '확인 요청',
),
);
PaginatedResult<ApprovalStepRecord> createResult(
List<ApprovalStepRecord> items,
) {
return PaginatedResult<ApprovalStepRecord>(
items: items,
page: 1,
pageSize: 20,
total: items.length,
);
}
setUp(() {
repository = _MockApprovalStepRepository();
controller = ApprovalStepController(repository: repository);
});
test('fetch 성공 시 결과를 갱신한다', () async {
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]));
await controller.fetch();
expect(controller.result?.items, isNotEmpty);
expect(controller.errorMessage, isNull);
verify(
() => repository.list(
page: 1,
pageSize: 20,
query: null,
statusId: null,
approverId: null,
approvalId: null,
),
).called(1);
});
test('에러 발생 시 errorMessage를 설정한다', () async {
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'),
),
).thenThrow(Exception('fail'));
await controller.fetch();
expect(controller.errorMessage, isNotNull);
expect(controller.result, isNull);
});
test('필터 갱신 후 fetch 시 파라미터에 반영한다', () async {
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]));
controller.updateQuery('APP-2024');
controller.updateStatusId(2);
await controller.fetch(page: 3);
verify(
() => repository.list(
page: 3,
pageSize: 20,
query: 'APP-2024',
statusId: 2,
approverId: null,
approvalId: null,
),
).called(1);
});
test('fetchDetail 성공 시 selected가 설정된다', () async {
when(
() => repository.fetchDetail(any()),
).thenAnswer((_) async => sampleRecord);
final detail = await controller.fetchDetail(100);
expect(detail, isNotNull);
expect(controller.selected, isNotNull);
verify(() => repository.fetchDetail(100)).called(1);
});
test('fetchDetail 실패 시 null을 반환한다', () async {
when(() => repository.fetchDetail(any())).thenThrow(Exception('fail'));
final detail = await controller.fetchDetail(100);
expect(detail, isNull);
expect(controller.errorMessage, isNotNull);
});
}

View File

@@ -0,0 +1,116 @@
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:two_dimensional_scrollables/two_dimensional_scrollables.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_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';
class _MockApprovalStepRepository extends Mock
implements ApprovalStepRepository {}
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 _MockApprovalStepRepository repository;
final record = ApprovalStepRecord(
approvalId: 10,
approvalNo: 'APP-2024-0001',
transactionNo: 'TRX-2024-001',
templateName: '입고 기본',
step: ApprovalStep(
id: 501,
stepOrder: 1,
approver: ApprovalApprover(id: 21, employeeNo: 'E001', name: '최승인'),
status: ApprovalStatus(id: 1, name: '승인대기', color: null),
assignedAt: DateTime(2024, 4, 1, 9),
decidedAt: null,
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 ApprovalStepPage()));
await tester.pump();
expect(find.text('결재 단계 관리'), findsOneWidget);
expect(find.text('결재 단계 순서와 승인자를 구성합니다.'), findsOneWidget);
});
testWidgets('목록을 렌더링하고 상세 다이얼로그를 연다', (tester) async {
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
repository = _MockApprovalStepRepository();
GetIt.I.registerLazySingleton<ApprovalStepRepository>(() => repository);
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.fetchDetail(any())).thenAnswer((_) async => record);
await tester.pumpWidget(_buildApp(const ApprovalStepPage()));
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('APP-2024-0001'), findsOneWidget);
expect(find.text('최승인'), findsOneWidget);
await tester.dragUntilVisible(
find.widgetWithText(ShadButton, '상세'),
find.byType(TwoDimensionalScrollable),
const Offset(-200, 0),
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(ShadButton, '상세').first);
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('결재 단계 상세'), findsOneWidget);
expect(find.text('검토 필요'), findsOneWidget);
verify(() => repository.fetchDetail(501)).called(1);
await tester.tap(find.text('닫기'));
await tester.pumpAndSettle();
});
}