- 모든 서비스 메서드 시그니처를 실제 구현에 맞게 수정 - TestDataGenerator 제거하고 직접 객체 생성으로 변경 - 모델 필드명 및 타입 불일치 수정 - 불필요한 Either 패턴 사용 제거 - null safety 관련 이슈 해결 수정된 파일: - test/integration/screens/company_integration_test.dart - test/integration/screens/equipment_integration_test.dart - test/integration/screens/user_integration_test.dart - test/integration/screens/login_integration_test.dart
324 lines
8.5 KiB
Dart
324 lines
8.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:mocktail/mocktail.dart';
|
|
|
|
// FlutterSecureStorage Mock 클래스
|
|
class MockFlutterSecureStorage extends Mock implements FlutterSecureStorage {}
|
|
|
|
/// 테스트용 GetIt 인스턴스 초기화
|
|
GetIt setupTestGetIt() {
|
|
final getIt = GetIt.instance;
|
|
|
|
// 기존 등록된 서비스들 모두 제거
|
|
getIt.reset();
|
|
|
|
// FlutterSecureStorage mock 등록
|
|
final mockSecureStorage = MockFlutterSecureStorage();
|
|
when(() => mockSecureStorage.read(key: any(named: 'key')))
|
|
.thenAnswer((_) async => null);
|
|
when(() => mockSecureStorage.write(key: any(named: 'key'), value: any(named: 'value')))
|
|
.thenAnswer((_) async {});
|
|
when(() => mockSecureStorage.delete(key: any(named: 'key')))
|
|
.thenAnswer((_) async {});
|
|
when(() => mockSecureStorage.deleteAll())
|
|
.thenAnswer((_) async {});
|
|
|
|
getIt.registerSingleton<FlutterSecureStorage>(mockSecureStorage);
|
|
|
|
return getIt;
|
|
}
|
|
|
|
/// 테스트용 위젯 래퍼
|
|
/// 모든 위젯 테스트에서 필요한 기본 설정을 제공
|
|
class TestWidgetWrapper extends StatelessWidget {
|
|
final Widget child;
|
|
final List<ChangeNotifierProvider>? providers;
|
|
final NavigatorObserver? navigatorObserver;
|
|
final Map<String, WidgetBuilder>? routes;
|
|
final String? initialRoute;
|
|
|
|
const TestWidgetWrapper({
|
|
super.key,
|
|
required this.child,
|
|
this.providers,
|
|
this.navigatorObserver,
|
|
this.routes,
|
|
this.initialRoute,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget wrappedChild = MaterialApp(
|
|
title: 'Test App',
|
|
theme: ThemeData(
|
|
primarySwatch: Colors.blue,
|
|
useMaterial3: true,
|
|
),
|
|
localizationsDelegates: const [
|
|
GlobalMaterialLocalizations.delegate,
|
|
GlobalWidgetsLocalizations.delegate,
|
|
GlobalCupertinoLocalizations.delegate,
|
|
],
|
|
supportedLocales: const [
|
|
Locale('ko', 'KR'),
|
|
Locale('en', 'US'),
|
|
],
|
|
home: Scaffold(body: child),
|
|
routes: routes ?? {},
|
|
initialRoute: initialRoute,
|
|
navigatorObservers: navigatorObserver != null ? [navigatorObserver!] : [],
|
|
);
|
|
|
|
// Provider가 있는 경우 래핑
|
|
if (providers != null && providers!.isNotEmpty) {
|
|
return MultiProvider(
|
|
providers: providers!,
|
|
child: wrappedChild,
|
|
);
|
|
}
|
|
|
|
return wrappedChild;
|
|
}
|
|
}
|
|
|
|
/// 위젯을 테스트 환경에서 펌프하는 헬퍼 함수
|
|
Future<void> pumpTestWidget(
|
|
WidgetTester tester,
|
|
Widget widget, {
|
|
List<ChangeNotifierProvider>? providers,
|
|
NavigatorObserver? navigatorObserver,
|
|
Map<String, WidgetBuilder>? routes,
|
|
String? initialRoute,
|
|
Size? screenSize,
|
|
}) async {
|
|
// 화면 크기 설정
|
|
if (screenSize != null) {
|
|
tester.view.physicalSize = screenSize;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
} else {
|
|
// 기본값: 태블릿 크기 (테이블 UI를 위해 충분한 크기)
|
|
tester.view.physicalSize = const Size(1024, 768);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
TestWidgetWrapper(
|
|
providers: providers,
|
|
navigatorObserver: navigatorObserver,
|
|
routes: routes,
|
|
initialRoute: initialRoute,
|
|
child: widget,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 비동기 작업을 기다리고 위젯을 리빌드하는 헬퍼
|
|
Future<void> pumpAndSettleWithTimeout(
|
|
WidgetTester tester, {
|
|
Duration timeout = const Duration(seconds: 10),
|
|
}) async {
|
|
await tester.pump();
|
|
await tester.pumpAndSettle(timeout);
|
|
}
|
|
|
|
/// TextField에 텍스트를 입력하는 헬퍼
|
|
Future<void> enterTextByLabel(
|
|
WidgetTester tester,
|
|
String label,
|
|
String text,
|
|
) async {
|
|
final textFieldFinder = find.ancestor(
|
|
of: find.text(label),
|
|
matching: find.byType(TextFormField),
|
|
);
|
|
|
|
if (textFieldFinder.evaluate().isEmpty) {
|
|
// 라벨로 찾지 못한 경우, 가까운 TextFormField 찾기
|
|
final textField = find.byType(TextFormField).first;
|
|
await tester.enterText(textField, text);
|
|
} else {
|
|
await tester.enterText(textFieldFinder, text);
|
|
}
|
|
}
|
|
|
|
/// 버튼을 찾고 탭하는 헬퍼
|
|
Future<void> tapButtonByText(
|
|
WidgetTester tester,
|
|
String buttonText,
|
|
) async {
|
|
final buttonFinder = find.widgetWithText(ElevatedButton, buttonText);
|
|
|
|
if (buttonFinder.evaluate().isEmpty) {
|
|
// ElevatedButton이 아닌 경우 다른 버튼 타입 시도
|
|
final textButtonFinder = find.widgetWithText(TextButton, buttonText);
|
|
if (textButtonFinder.evaluate().isNotEmpty) {
|
|
await tester.tap(textButtonFinder);
|
|
return;
|
|
}
|
|
|
|
final outlinedButtonFinder = find.widgetWithText(OutlinedButton, buttonText);
|
|
if (outlinedButtonFinder.evaluate().isNotEmpty) {
|
|
await tester.tap(outlinedButtonFinder);
|
|
return;
|
|
}
|
|
|
|
// 아무 버튼도 찾지 못한 경우 텍스트만으로 시도
|
|
await tester.tap(find.text(buttonText));
|
|
} else {
|
|
await tester.tap(buttonFinder);
|
|
}
|
|
}
|
|
|
|
/// 스낵바 메시지 검증 헬퍼
|
|
void expectSnackBar(WidgetTester tester, String message) {
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(SnackBar),
|
|
matching: find.text(message),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
}
|
|
|
|
/// 로딩 인디케이터 검증 헬퍼
|
|
void expectLoading(WidgetTester tester, {bool isLoading = true}) {
|
|
expect(
|
|
find.byType(CircularProgressIndicator),
|
|
isLoading ? findsOneWidget : findsNothing,
|
|
);
|
|
}
|
|
|
|
/// 에러 메시지 검증 헬퍼
|
|
void expectErrorMessage(WidgetTester tester, String errorMessage) {
|
|
expect(find.text(errorMessage), findsOneWidget);
|
|
}
|
|
|
|
/// 화면 전환 대기 헬퍼
|
|
Future<void> waitForNavigation(WidgetTester tester) async {
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 300)); // 애니메이션 대기
|
|
}
|
|
|
|
/// 다이얼로그 검증 헬퍼
|
|
void expectDialog(WidgetTester tester, {String? title, String? content}) {
|
|
expect(find.byType(Dialog), findsOneWidget);
|
|
|
|
if (title != null) {
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(Dialog),
|
|
matching: find.text(title),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
}
|
|
|
|
if (content != null) {
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(Dialog),
|
|
matching: find.text(content),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 다이얼로그 닫기 헬퍼
|
|
Future<void> closeDialog(WidgetTester tester) async {
|
|
// 다이얼로그 외부 탭하여 닫기
|
|
await tester.tapAt(const Offset(10, 10));
|
|
await tester.pump();
|
|
}
|
|
|
|
/// 스크롤하여 위젯 찾기 헬퍼
|
|
Future<void> scrollUntilVisible(
|
|
WidgetTester tester,
|
|
Finder finder, {
|
|
double delta = 300,
|
|
int maxScrolls = 10,
|
|
Finder? scrollable,
|
|
}) async {
|
|
final scrollableFinder = scrollable ?? find.byType(Scrollable).first;
|
|
|
|
for (int i = 0; i < maxScrolls; i++) {
|
|
if (finder.evaluate().isNotEmpty) {
|
|
return;
|
|
}
|
|
|
|
await tester.drag(scrollableFinder, Offset(0, -delta));
|
|
await tester.pump();
|
|
}
|
|
}
|
|
|
|
/// 테이블이나 리스트에서 특정 행 찾기 헬퍼
|
|
Finder findRowContaining(String text) {
|
|
return find.ancestor(
|
|
of: find.text(text),
|
|
matching: find.byType(Row),
|
|
);
|
|
}
|
|
|
|
/// 폼 필드 검증 헬퍼
|
|
void expectFormFieldError(WidgetTester tester, String fieldLabel, String errorText) {
|
|
final formField = find.ancestor(
|
|
of: find.text(fieldLabel),
|
|
matching: find.byType(TextFormField),
|
|
);
|
|
|
|
final errorFinder = find.descendant(
|
|
of: formField,
|
|
matching: find.text(errorText),
|
|
);
|
|
|
|
expect(errorFinder, findsOneWidget);
|
|
}
|
|
|
|
/// 드롭다운 선택 헬퍼
|
|
Future<void> selectDropdownItem(
|
|
WidgetTester tester,
|
|
String dropdownLabel,
|
|
String itemText,
|
|
) async {
|
|
// 드롭다운 찾기
|
|
final dropdown = find.ancestor(
|
|
of: find.text(dropdownLabel),
|
|
matching: find.byType(DropdownButtonFormField),
|
|
);
|
|
|
|
// 드롭다운 열기
|
|
await tester.tap(dropdown);
|
|
await tester.pump();
|
|
|
|
// 아이템 선택
|
|
await tester.tap(find.text(itemText).last);
|
|
await tester.pump();
|
|
}
|
|
|
|
/// 날짜 선택 헬퍼
|
|
Future<void> selectDate(
|
|
WidgetTester tester,
|
|
String dateFieldLabel,
|
|
DateTime date,
|
|
) async {
|
|
// 날짜 필드 탭
|
|
final dateField = find.ancestor(
|
|
of: find.text(dateFieldLabel),
|
|
matching: find.byType(TextFormField),
|
|
);
|
|
|
|
await tester.tap(dateField);
|
|
await tester.pump();
|
|
|
|
// 날짜 선택 (간단한 구현, 실제로는 더 복잡할 수 있음)
|
|
await tester.tap(find.text(date.day.toString()));
|
|
await tester.pump();
|
|
|
|
// 확인 버튼 탭
|
|
await tester.tap(find.text('확인'));
|
|
await tester.pump();
|
|
} |