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