사용자 마스터 UI 및 테스트 구현

This commit is contained in:
JiWoong Sul
2025-09-22 21:27:45 +09:00
parent 2106d13b12
commit b6e50464d2
16 changed files with 1921 additions and 54 deletions

View File

@@ -0,0 +1,231 @@
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/masters/group/domain/entities/group.dart';
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
import 'package:superport_v2/features/masters/user/domain/repositories/user_repository.dart';
import 'package:superport_v2/features/masters/user/presentation/pages/user_page.dart';
class _MockUserRepository extends Mock implements UserRepository {}
class _MockGroupRepository extends Mock implements GroupRepository {}
class _FakeUserInput extends Fake implements UserInput {}
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(_FakeUserInput());
});
tearDown(() async {
await GetIt.I.reset();
dotenv.clean();
});
testWidgets('플래그 Off 시 스펙 화면 유지', (tester) async {
dotenv.testLoad(fileInput: 'FEATURE_USERS_ENABLED=false\n');
await tester.pumpWidget(_buildApp(const UserPage()));
await tester.pump();
expect(find.text('사용자(사원) 관리'), findsOneWidget);
expect(find.text('테이블 리스트'), findsOneWidget);
});
group('플래그 On', () {
late _MockUserRepository userRepository;
late _MockGroupRepository groupRepository;
setUp(() {
dotenv.testLoad(fileInput: 'FEATURE_USERS_ENABLED=true\n');
userRepository = _MockUserRepository();
groupRepository = _MockGroupRepository();
GetIt.I.registerLazySingleton<UserRepository>(() => userRepository);
GetIt.I.registerLazySingleton<GroupRepository>(() => groupRepository);
when(
() => groupRepository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
isActive: any(named: 'isActive'),
),
).thenAnswer(
(_) async => PaginatedResult<Group>(
items: [Group(id: 1, groupName: '관리자')],
page: 1,
pageSize: 20,
total: 1,
),
);
});
testWidgets('목록 조회 후 테이블 렌더', (tester) async {
when(
() => userRepository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
groupId: any(named: 'groupId'),
isActive: any(named: 'isActive'),
),
).thenAnswer(
(_) async => PaginatedResult<UserAccount>(
items: [
UserAccount(
id: 1,
employeeNo: 'A001',
employeeName: '홍길동',
email: 'hong@superport.com',
group: UserGroup(id: 1, groupName: '관리자'),
),
],
page: 1,
pageSize: 20,
total: 1,
),
);
await tester.pumpWidget(_buildApp(const UserPage()));
await tester.pumpAndSettle();
expect(find.text('A001'), findsOneWidget);
verify(
() => userRepository.list(
page: 1,
pageSize: 20,
query: null,
groupId: null,
isActive: null,
),
).called(1);
});
testWidgets('폼 검증: 필수값 누락', (tester) async {
when(
() => userRepository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
groupId: any(named: 'groupId'),
isActive: any(named: 'isActive'),
),
).thenAnswer(
(_) async => PaginatedResult<UserAccount>(
items: const [],
page: 1,
pageSize: 20,
total: 0,
),
);
await tester.pumpWidget(_buildApp(const UserPage()));
await tester.pumpAndSettle();
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.pump();
expect(find.text('사번을 입력하세요.'), findsOneWidget);
expect(find.text('성명을 입력하세요.'), findsOneWidget);
});
testWidgets('신규 등록 성공', (tester) async {
var listCallCount = 0;
when(
() => userRepository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
groupId: any(named: 'groupId'),
isActive: any(named: 'isActive'),
),
).thenAnswer((_) async {
listCallCount += 1;
if (listCallCount == 1) {
return PaginatedResult<UserAccount>(
items: const [],
page: 1,
pageSize: 20,
total: 0,
);
}
return PaginatedResult<UserAccount>(
items: [
UserAccount(
id: 2,
employeeNo: 'A010',
employeeName: '신규 사용자',
email: 'new@superport.com',
group: UserGroup(id: 1, groupName: '관리자'),
),
],
page: 1,
pageSize: 20,
total: 1,
);
});
UserInput? capturedInput;
when(() => userRepository.create(any())).thenAnswer((invocation) async {
capturedInput = invocation.positionalArguments.first as UserInput;
return UserAccount(
id: 2,
employeeNo: capturedInput!.employeeNo,
employeeName: capturedInput!.employeeName,
group: UserGroup(id: capturedInput!.groupId, groupName: '관리자'),
);
});
await tester.pumpWidget(_buildApp(const UserPage()));
await tester.pumpAndSettle();
await tester.tap(find.text('신규 등록'));
await tester.pumpAndSettle();
final dialog = find.byType(Dialog);
final editableTexts = find.descendant(
of: dialog,
matching: find.byType(EditableText),
);
await tester.enterText(editableTexts.at(0), 'A010');
await tester.enterText(editableTexts.at(1), '신규 사용자');
await tester.tap(find.text('그룹을 선택하세요'));
await tester.pumpAndSettle();
await tester.tap(find.text('관리자'));
await tester.pumpAndSettle();
await tester.tap(find.text('등록'));
await tester.pumpAndSettle();
expect(capturedInput, isNotNull);
expect(capturedInput?.employeeNo, 'A010');
expect(find.byType(Dialog), findsNothing);
expect(find.text('A010'), findsOneWidget);
verify(() => userRepository.create(any())).called(1);
});
});
}