feat(dialog): 상세 팝업 SuperportDetailDialog 통합
- SuperportDetailDialog 위젯과 showSuperportDetailDialog 헬퍼를 추가하고 metadata/섹션 패턴을 표준화 - 결재/재고/마스터 각 상세 다이얼로그를 dialogs 디렉터리에 신설하고 기존 페이지를 신규 팝업으로 전환 - SuperportTable 행 선택과 우편번호 검색 다이얼로그 onRowTap 보정을 통해 헤더 오프셋 버그를 제거 - 상세 다이얼로그 및 트랜잭션/상세 뷰 전용 위젯 테스트와 tester_extensions 유틸을 추가하여 회귀를 방지 - detail_dialog_unification_plan.md로 작업 배경과 필드 통합 계획을 문서화
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'package:shadcn_ui/shadcn_ui.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/presentation/dialogs/approval_step_detail_dialog.dart';
|
||||
import 'package:superport_v2/widgets/components/superport_detail_dialog.dart';
|
||||
|
||||
import '../../../../../helpers/test_app.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
|
||||
|
||||
late ApprovalStepRecord sampleRecord;
|
||||
late ApprovalApprover approver;
|
||||
|
||||
setUp(() {
|
||||
final statusPending = ApprovalStatus(
|
||||
id: 1,
|
||||
name: '대기',
|
||||
color: '#0EA5E9',
|
||||
isTerminal: false,
|
||||
);
|
||||
approver = ApprovalApprover(id: 800, employeeNo: 'E800', name: '김승인');
|
||||
|
||||
final step = ApprovalStep(
|
||||
id: 900,
|
||||
stepOrder: 1,
|
||||
approver: approver,
|
||||
status: statusPending,
|
||||
assignedAt: DateTime(2024, 1, 10, 9),
|
||||
);
|
||||
|
||||
sampleRecord = ApprovalStepRecord(
|
||||
approvalId: 400,
|
||||
approvalNo: 'APP-2024-0400',
|
||||
transactionNo: 'TRX-400',
|
||||
templateName: '일반 결재선',
|
||||
step: step,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('결재 단계 상세 다이얼로그는 요약과 메타 정보를 노출한다', (tester) async {
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
final resultFuture = showApprovalStepDetailDialog(
|
||||
context: context,
|
||||
record: sampleRecord,
|
||||
dateFormat: dateFormat,
|
||||
onUpdate: (_, __) => Future.value(sampleRecord),
|
||||
onDelete: (_) => Future.value(true),
|
||||
onRestore: (_) => Future.value(sampleRecord),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text('결재 단계 상세'), findsAtLeastNWidgets(1));
|
||||
expect(find.text('결재 단계 1'), findsAtLeastNWidgets(1));
|
||||
expect(find.textContaining(approver.name), findsAtLeastNWidgets(1));
|
||||
expect(find.text('단계 순서'), findsWidgets);
|
||||
expect(find.text('상태'), findsWidgets);
|
||||
expect(find.text('승인자 사번'), findsWidgets);
|
||||
|
||||
await tester.tap(find.byTooltip('닫기'), warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
expect(await resultFuture, isNull);
|
||||
});
|
||||
|
||||
testWidgets('수정 탭에서 저장을 누르면 onUpdate 콜백이 입력값과 함께 호출된다', (tester) async {
|
||||
ApprovalStepInput? capturedInput;
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
final resultFuture = showApprovalStepDetailDialog(
|
||||
context: context,
|
||||
record: sampleRecord,
|
||||
dateFormat: dateFormat,
|
||||
onUpdate: (_, input) async {
|
||||
capturedInput = input;
|
||||
return sampleRecord.copyWith(
|
||||
step: sampleRecord.step.copyWith(
|
||||
stepOrder: input.stepOrder,
|
||||
approver: ApprovalApprover(
|
||||
id: input.approverId,
|
||||
employeeNo: 'E${input.approverId}',
|
||||
name: '변경 승인자',
|
||||
),
|
||||
note: input.note,
|
||||
),
|
||||
);
|
||||
},
|
||||
onDelete: (_) => Future.value(false),
|
||||
onRestore: (_) => Future.value(sampleRecord),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final tabsFinder = find.descendant(
|
||||
of: find.byType(SuperportDetailDialog),
|
||||
matching: find.byType(ShadTabs<String>),
|
||||
);
|
||||
final tabsState = tester.state(tabsFinder);
|
||||
final controller =
|
||||
(tabsState as dynamic).controller as ShadTabsController<String>;
|
||||
controller.select('edit');
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('approval_step_detail_step_order')),
|
||||
'3',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('approval_step_detail_approver_id')),
|
||||
'901',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('approval_step_detail_note')),
|
||||
'조정된 결재 단계',
|
||||
);
|
||||
|
||||
final submitButtonFinder = find.byKey(
|
||||
const ValueKey('approval_step_detail_submit'),
|
||||
);
|
||||
final submitButton = tester.widget<ShadButton>(submitButtonFinder);
|
||||
expect(submitButton.onPressed, isNotNull);
|
||||
submitButton.onPressed!();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final result = await resultFuture;
|
||||
expect(result, isNotNull);
|
||||
expect(result!.action, ApprovalStepDetailAction.updated);
|
||||
expect(capturedInput, isNotNull);
|
||||
expect(capturedInput!.stepOrder, 3);
|
||||
expect(capturedInput!.approverId, 901);
|
||||
expect(capturedInput!.note, '조정된 결재 단계');
|
||||
});
|
||||
|
||||
testWidgets('삭제 탭에서 삭제 버튼을 누르면 onDelete 콜백 성공 시 결과가 반환된다', (tester) async {
|
||||
var deleteCalled = false;
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const SizedBox.shrink()));
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
|
||||
final resultFuture = showApprovalStepDetailDialog(
|
||||
context: context,
|
||||
record: sampleRecord,
|
||||
dateFormat: dateFormat,
|
||||
onUpdate: (_, __) => Future.value(sampleRecord),
|
||||
onDelete: (_) async {
|
||||
deleteCalled = true;
|
||||
return true;
|
||||
},
|
||||
onRestore: (_) => Future.value(sampleRecord),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tabsFinder = find.descendant(
|
||||
of: find.byType(SuperportDetailDialog),
|
||||
matching: find.byType(ShadTabs<String>),
|
||||
);
|
||||
final tabsState = tester.state(tabsFinder);
|
||||
final controller =
|
||||
(tabsState as dynamic).controller as ShadTabsController<String>;
|
||||
controller.select('delete');
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final deleteButtonFinder = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ShadButton &&
|
||||
widget.variant == ShadButtonVariant.destructive &&
|
||||
widget.child is Text &&
|
||||
(widget.child as Text).data == '삭제',
|
||||
);
|
||||
expect(deleteButtonFinder, findsOneWidget);
|
||||
final deleteButton = tester.widget<ShadButton>(deleteButtonFinder);
|
||||
expect(deleteButton.onPressed, isNotNull);
|
||||
deleteButton.onPressed!();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final result = await resultFuture;
|
||||
expect(deleteCalled, isTrue);
|
||||
expect(result, isNotNull);
|
||||
expect(result!.action, ApprovalStepDetailAction.deleted);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user