결재 템플릿 화면 위젯 테스트 추가

This commit is contained in:
JiWoong Sul
2025-09-25 15:23:50 +09:00
parent 1fbed565b7
commit 2accac85f3
2 changed files with 292 additions and 19 deletions

View File

@@ -0,0 +1,270 @@
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_template.dart';
import 'package:superport_v2/features/approvals/domain/repositories/approval_template_repository.dart';
import 'package:superport_v2/features/approvals/template/presentation/pages/approval_template_page.dart';
class _MockApprovalTemplateRepository extends Mock
implements ApprovalTemplateRepository {}
class _FakeTemplateInput extends Fake implements ApprovalTemplateInput {}
class _FakeTemplateStepInput extends Fake
implements ApprovalTemplateStepInput {}
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();
setUpAll(() {
registerFallbackValue(_FakeTemplateInput());
registerFallbackValue(_FakeTemplateStepInput());
registerFallbackValue(<ApprovalTemplateStepInput>[]);
});
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 ApprovalTemplatePage()));
await tester.pump();
expect(find.text('결재 템플릿 관리'), findsOneWidget);
expect(find.text('반복되는 결재 단계를 템플릿으로 구성합니다.'), findsOneWidget);
});
group('플래그 On', () {
late _MockApprovalTemplateRepository repository;
setUp(() {
dotenv.testLoad(fileInput: 'FEATURE_APPROVALS_ENABLED=true\n');
repository = _MockApprovalTemplateRepository();
GetIt.I.registerLazySingleton<ApprovalTemplateRepository>(
() => repository,
);
});
ApprovalTemplate _template({bool isActive = true}) {
return ApprovalTemplate(
id: 10,
code: 'AP_INBOUND',
name: '입고 템플릿',
description: '입고 2단계',
note: '기본 템플릿',
isActive: isActive,
createdBy: null,
createdAt: DateTime(2024, 4, 1, 9),
updatedAt: DateTime(2024, 4, 2, 9),
steps: [
ApprovalTemplateStep(
id: 1,
stepOrder: 1,
approver: ApprovalTemplateApprover(
id: 21,
employeeNo: 'E001',
name: '최승인',
),
note: '확인',
),
],
);
}
testWidgets('목록을 조회해 테이블로 렌더링한다', (tester) async {
when(
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
isActive: any(named: 'isActive'),
),
).thenAnswer(
(_) async => PaginatedResult<ApprovalTemplate>(
items: [_template()],
page: 1,
pageSize: 20,
total: 1,
),
);
await tester.pumpWidget(_buildApp(const ApprovalTemplatePage()));
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('AP_INBOUND'), findsOneWidget);
expect(find.text('입고 템플릿'), findsOneWidget);
verify(
() =>
repository.list(page: 1, pageSize: 20, query: null, isActive: null),
).called(1);
});
testWidgets('생성 플로우에서 create 호출 후 목록을 새로고침한다', (tester) async {
when(
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
isActive: any(named: 'isActive'),
),
).thenAnswer(
(_) async => PaginatedResult<ApprovalTemplate>(
items: const [],
page: 1,
pageSize: 20,
total: 0,
),
);
when(
() => repository.create(any(), steps: any(named: 'steps')),
).thenAnswer((_) async => _template());
await tester.pumpWidget(_buildApp(const ApprovalTemplatePage()));
await tester.pump();
await tester.pumpAndSettle();
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),
'신규 템플릿',
);
final stepFieldsFinder = find.descendant(
of: find.byKey(const ValueKey('step_field_0')),
matching: find.byType(EditableText),
skipOffstage: false,
);
final stepFieldElements = stepFieldsFinder.evaluate().toList();
expect(stepFieldElements.length, greaterThanOrEqualTo(2));
await tester.enterText(find.byWidget(stepFieldElements[1].widget), '33');
await tester.tap(find.text('생성 완료'));
await tester.pump();
await tester.pumpAndSettle();
verify(
() => repository.create(any(), steps: any(named: 'steps')),
).called(1);
verify(
() =>
repository.list(page: 1, pageSize: 20, query: null, isActive: null),
).called(greaterThanOrEqualTo(2));
expect(find.text('템플릿 "신규 템플릿"을 생성했습니다.'), findsOneWidget);
});
testWidgets('수정 플로우에서 fetchDetail 후 update를 호출한다', (tester) async {
final activeTemplate = _template();
when(
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
isActive: any(named: 'isActive'),
),
).thenAnswer(
(_) async => PaginatedResult<ApprovalTemplate>(
items: [activeTemplate],
page: 1,
pageSize: 20,
total: 1,
),
);
when(
() => repository.fetchDetail(
any(),
includeSteps: any(named: 'includeSteps'),
),
).thenAnswer((_) async => activeTemplate);
when(
() => repository.update(any(), any(), steps: any(named: 'steps')),
).thenAnswer((_) async => activeTemplate.copyWith(name: '수정된 템플릿'));
await tester.pumpWidget(_buildApp(const ApprovalTemplatePage()));
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('입고 템플릿'), findsOneWidget);
verify(
() =>
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.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));
await tester.enterText(
find.byWidget(editFieldElements[0].widget),
'수정된 템플릿',
);
await tester.tap(find.text('수정 완료'));
await tester.pump();
await tester.pumpAndSettle();
verify(() => repository.fetchDetail(10, includeSteps: true)).called(1);
verify(
() => repository.update(10, any(), steps: any(named: 'steps')),
).called(1);
expect(find.text('템플릿 "입고 템플릿"을(를) 수정했습니다.'), findsOneWidget);
});
});
}