결재 템플릿 화면 위젯 테스트 추가
This commit is contained in:
@@ -212,8 +212,10 @@ class _ApprovalTemplateEnabledPageState
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
SizedBox(
|
||||
height: 480,
|
||||
child: ShadTable.list(
|
||||
header:
|
||||
['ID', '템플릿코드', '템플릿명', '설명', '사용', '변경일시', '동작']
|
||||
@@ -250,12 +252,11 @@ class _ApprovalTemplateEnabledPageState
|
||||
),
|
||||
),
|
||||
ShadTableCell(
|
||||
child: ShadBadge(
|
||||
variant: template.isActive
|
||||
? ShadBadgeVariant.defaultVariant
|
||||
: ShadBadgeVariant.outline,
|
||||
child: Text(template.isActive ? '사용' : '미사용'),
|
||||
),
|
||||
child: template.isActive
|
||||
? const ShadBadge(child: Text('사용'))
|
||||
: const ShadBadge.outline(
|
||||
child: Text('미사용'),
|
||||
),
|
||||
),
|
||||
ShadTableCell(
|
||||
child: Text(
|
||||
@@ -451,6 +452,7 @@ class _ApprovalTemplateEnabledPageState
|
||||
final statusNotifier = ValueNotifier<bool>(template?.isActive ?? true);
|
||||
bool isSaving = false;
|
||||
String? errorText;
|
||||
StateSetter? modalSetState;
|
||||
|
||||
final result = await showSuperportDialog<bool>(
|
||||
context: context,
|
||||
@@ -458,6 +460,7 @@ class _ApprovalTemplateEnabledPageState
|
||||
barrierDismissible: !isSaving,
|
||||
body: StatefulBuilder(
|
||||
builder: (dialogContext, setModalState) {
|
||||
modalSetState = setModalState;
|
||||
final theme = ShadTheme.of(dialogContext);
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
@@ -482,8 +485,8 @@ class _ApprovalTemplateEnabledPageState
|
||||
label: '설명',
|
||||
child: ShadTextarea(
|
||||
controller: descriptionController,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
minHeight: 80,
|
||||
maxHeight: 200,
|
||||
),
|
||||
),
|
||||
_FormField(
|
||||
@@ -510,8 +513,8 @@ class _ApprovalTemplateEnabledPageState
|
||||
label: '비고',
|
||||
child: ShadTextarea(
|
||||
controller: noteController,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
minHeight: 80,
|
||||
maxHeight: 200,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -594,19 +597,19 @@ class _ApprovalTemplateEnabledPageState
|
||||
final codeValue = codeController.text.trim();
|
||||
final nameValue = nameController.text.trim();
|
||||
if (!isEdit && codeValue.isEmpty) {
|
||||
setModalState(() => errorText = '템플릿 코드를 입력하세요.');
|
||||
modalSetState?.call(() => errorText = '템플릿 코드를 입력하세요.');
|
||||
return;
|
||||
}
|
||||
if (nameValue.isEmpty) {
|
||||
setModalState(() => errorText = '템플릿명을 입력하세요.');
|
||||
modalSetState?.call(() => errorText = '템플릿명을 입력하세요.');
|
||||
return;
|
||||
}
|
||||
final validation = _validateSteps(steps);
|
||||
if (validation != null) {
|
||||
setModalState(() => errorText = validation);
|
||||
modalSetState?.call(() => errorText = validation);
|
||||
return;
|
||||
}
|
||||
setModalState(() => errorText = null);
|
||||
modalSetState?.call(() => errorText = null);
|
||||
final stepInputs = steps
|
||||
.map(
|
||||
(field) => ApprovalTemplateStepInput(
|
||||
@@ -634,7 +637,7 @@ class _ApprovalTemplateEnabledPageState
|
||||
: noteController.text.trim(),
|
||||
isActive: statusNotifier.value,
|
||||
);
|
||||
setModalState(() => isSaving = true);
|
||||
modalSetState?.call(() => isSaving = true);
|
||||
final success = isEdit
|
||||
? await _controller.update(
|
||||
template!.id,
|
||||
@@ -645,7 +648,7 @@ class _ApprovalTemplateEnabledPageState
|
||||
if (success != null && mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
} else {
|
||||
setModalState(() => isSaving = false);
|
||||
modalSetState?.call(() => isSaving = false);
|
||||
}
|
||||
},
|
||||
child: isSaving
|
||||
@@ -812,8 +815,8 @@ class _StepEditorRow extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
ShadTextarea(
|
||||
controller: field.noteController,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
minHeight: 60,
|
||||
maxHeight: 160,
|
||||
placeholder: const Text('비고 (선택)'),
|
||||
),
|
||||
if (isEdit && field.id != null)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user